From 39ba2531610477af3725f52187a7fb512ae5c437 Mon Sep 17 00:00:00 2001 From: Chris Bono Date: Mon, 20 Feb 2023 13:17:51 -0600 Subject: [PATCH] [CI] Add integration test support (#357) The `PulsarFunctionAdministrationIntegrationTests` was causing an OOM because it copies a large archive into memory in order to setup its testcontainers. The unit tests running in parallel consume most of the available resources so we split the ITs into their own step in the workflow and all is happy. This is also good to have support for integration tests in the project. --- .github/workflows/ci-pr.yml | 8 +- .github/workflows/ci.yml | 7 +- buildSrc/build.gradle | 4 + .../convention/IntegrationTestPlugin.groovy | 116 +++++++ .../AutoConfigurationMetadata.java | 193 ------------ .../AutoConfigurationPlugin.java | 77 ----- .../DocumentAutoConfigurationClasses.java | 136 -------- .../artifactory/ArtifactoryRepository.java | 60 ---- .../gradle/context/properties/Asciidoc.java | 59 ---- ...AdditionalSpringConfigurationMetadata.java | 166 ---------- .../context/properties/CompoundRow.java | 55 ---- .../properties/ConfigurationProperties.java | 75 ----- .../ConfigurationPropertiesPlugin.java | 124 -------- .../properties/ConfigurationProperty.java | 88 ------ .../DocumentConfigurationProperties.java | 220 ------------- .../boot/gradle/context/properties/Row.java | 64 ---- .../gradle/context/properties/SingleRow.java | 84 ----- .../gradle/context/properties/Snippet.java | 102 ------ .../gradle/context/properties/Snippets.java | 131 -------- .../boot/gradle/context/properties/Table.java | 47 --- .../gradle/test/IntegrationTestPlugin.java | 82 ----- .../boot/gradle/test/SystemTestPlugin.java | 94 ------ .../gradle/JacocoConventionsPlugin.java | 16 +- .../pulsar/gradle/SpringDocsModulePlugin.java | 2 + gradle.properties | 2 +- .../build.gradle | 4 + ...unctionAdministrationIntegrationTests.java | 292 ++++++++++++++++++ .../resources/application.yml | 3 + .../resources/logback-test.xml | 13 + 29 files changed, 453 insertions(+), 1871 deletions(-) create mode 100644 buildSrc/src/main/groovy/io/spring/gradle/convention/IntegrationTestPlugin.groovy delete mode 100644 buildSrc/src/main/java/org/springframework/boot/gradle/autoconfigure/AutoConfigurationMetadata.java delete mode 100644 buildSrc/src/main/java/org/springframework/boot/gradle/autoconfigure/AutoConfigurationPlugin.java delete mode 100644 buildSrc/src/main/java/org/springframework/boot/gradle/autoconfigure/DocumentAutoConfigurationClasses.java delete mode 100644 buildSrc/src/main/java/org/springframework/boot/gradle/classpath/artifactory/ArtifactoryRepository.java delete mode 100644 buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/Asciidoc.java delete mode 100644 buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/CheckAdditionalSpringConfigurationMetadata.java delete mode 100644 buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/CompoundRow.java delete mode 100644 buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/ConfigurationProperties.java delete mode 100644 buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/ConfigurationPropertiesPlugin.java delete mode 100644 buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/ConfigurationProperty.java delete mode 100644 buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/DocumentConfigurationProperties.java delete mode 100644 buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/Row.java delete mode 100644 buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/SingleRow.java delete mode 100644 buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/Snippet.java delete mode 100644 buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/Snippets.java delete mode 100644 buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/Table.java delete mode 100644 buildSrc/src/main/java/org/springframework/boot/gradle/test/IntegrationTestPlugin.java delete mode 100644 buildSrc/src/main/java/org/springframework/boot/gradle/test/SystemTestPlugin.java create mode 100644 spring-pulsar-spring-boot-autoconfigure/src/integration-test/java/org/springframework/pulsar/autoconfigure/PulsarFunctionAdministrationIntegrationTests.java create mode 100644 spring-pulsar-spring-boot-autoconfigure/src/integration-test/resources/application.yml create mode 100644 spring-pulsar-spring-boot-autoconfigure/src/integration-test/resources/logback-test.xml diff --git a/.github/workflows/ci-pr.yml b/.github/workflows/ci-pr.yml index 8ab4fed1..eab6d398 100644 --- a/.github/workflows/ci-pr.yml +++ b/.github/workflows/ci-pr.yml @@ -34,9 +34,13 @@ jobs: env: GRADLE_USER_HOME: ~/.gradle - - name: Run Gradle build + - name: Build and run unit tests run: | - ./gradlew clean build -DdownloadRabbitConnector=false --continue --scan + ./gradlew clean build -x integrationTest --continue --scan + + - name: Run integration tests + run: | + ./gradlew integrationTest --rerun-tasks -DdownloadRabbitConnector=true --scan - name: Capture Test Results if: failure() diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2164f7f4..d7e2eb25 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,9 +39,12 @@ jobs: uses: gradle/gradle-build-action@v2 env: GRADLE_USER_HOME: ~/.gradle - - name: Run Gradle build + - name: Build and run unit tests run: | - ./gradlew clean build -DdownloadRabbitConnector=false --continue -PartifactoryUsername="$ARTIFACTORY_USERNAME" -PartifactoryPassword="$ARTIFACTORY_PASSWORD" + ./gradlew clean build -x integrationTest --continue --scan -PartifactoryUsername="$ARTIFACTORY_USERNAME" -PartifactoryPassword="$ARTIFACTORY_PASSWORD" + - name: Run integration tests + run: | + ./gradlew integrationTest --rerun-tasks -DdownloadRabbitConnector=true --scan - name: Capture test results if: failure() uses: actions/upload-artifact@v3 diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 450c8812..6468a855 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -106,6 +106,10 @@ gradlePlugin { id = "io.spring.convention.artfiactory" implementationClass = "io.spring.gradle.convention.ArtifactoryPlugin" } + integrationTestPlugin { + id = "io.spring.convention.integration-test" + implementationClass = "io.spring.gradle.convention.IntegrationTestPlugin" + } repositoryConventionPlugin { id = "io.spring.convention.repository" implementationClass = "io.spring.gradle.convention.RepositoryConventionPlugin" diff --git a/buildSrc/src/main/groovy/io/spring/gradle/convention/IntegrationTestPlugin.groovy b/buildSrc/src/main/groovy/io/spring/gradle/convention/IntegrationTestPlugin.groovy new file mode 100644 index 00000000..5ce8321b --- /dev/null +++ b/buildSrc/src/main/groovy/io/spring/gradle/convention/IntegrationTestPlugin.groovy @@ -0,0 +1,116 @@ +/* + * Copyright 2016-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package io.spring.gradle.convention + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.plugins.JavaPlugin +import org.gradle.api.tasks.testing.Test +import org.gradle.plugins.ide.eclipse.EclipsePlugin +import org.gradle.plugins.ide.idea.IdeaPlugin + +import org.springframework.boot.gradle.optional.OptionalDependenciesPlugin + +/** + * Adds support for integration tests to java projects. + * + * + * @author Rob Winch + * @author Chris Bono + */ +class IntegrationTestPlugin implements Plugin { + + @Override + void apply(Project project) { + project.plugins.withType(JavaPlugin.class) { + applyJava(project) + } + } + + private applyJava(Project project) { + if(!project.file('src/integration-test/').exists()) { + // ensure we don't add if no tests + return + } + project.configurations { + integrationTestCompile { + extendsFrom testImplementation + } + integrationTestRuntime { + extendsFrom integrationTestCompile, testRuntimeClasspath, testRuntimeOnly + } + integrationTestCompileClasspath { + extendsFrom integrationTestCompile + canBeResolved = true + } + integrationTestRuntimeClasspath { + extendsFrom integrationTestRuntime + canBeResolved = true + } + } + + project.sourceSets { + integrationTest { + java.srcDir project.file('src/integration-test/java') + resources.srcDir project.file('src/integration-test/resources') + compileClasspath = project.sourceSets.main.output + project.sourceSets.test.output + project.configurations.integrationTestCompileClasspath + runtimeClasspath = output + compileClasspath + project.configurations.integrationTestRuntimeClasspath + } + } + + Task integrationTestTask = project.tasks.create("integrationTest", Test) { + description = 'Runs integration tests.' + group = 'verification' + + testClassesDirs = project.sourceSets.integrationTest.output.classesDirs + classpath = project.sourceSets.integrationTest.runtimeClasspath + + mustRunAfter project.tasks.test + + useJUnitPlatform() + } + + project.tasks.check.dependsOn integrationTestTask + + project.plugins.withType(OptionalDependenciesPlugin) { + project.configurations { + integrationTestCompile { + extendsFrom optional + } + } + } + + project.plugins.withType(IdeaPlugin) { + project.idea { + module { + testSourceDirs += project.file('src/integration-test/java') + scopes.TEST.plus += [ project.configurations.integrationTestCompileClasspath ] + } + } + } + + project.plugins.withType(EclipsePlugin) { + project.eclipse.classpath { + plusConfigurations += [ project.configurations.integrationTestCompileClasspath ] + } + } + } +} diff --git a/buildSrc/src/main/java/org/springframework/boot/gradle/autoconfigure/AutoConfigurationMetadata.java b/buildSrc/src/main/java/org/springframework/boot/gradle/autoconfigure/AutoConfigurationMetadata.java deleted file mode 100644 index b674ab69..00000000 --- a/buildSrc/src/main/java/org/springframework/boot/gradle/autoconfigure/AutoConfigurationMetadata.java +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Copyright 2012-2022 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.gradle.autoconfigure; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileReader; -import java.io.FileWriter; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Properties; -import java.util.Set; -import java.util.concurrent.Callable; - -import org.gradle.api.DefaultTask; -import org.gradle.api.Task; -import org.gradle.api.tasks.OutputFile; -import org.gradle.api.tasks.PathSensitivity; -import org.gradle.api.tasks.SourceSet; -import org.gradle.api.tasks.TaskAction; - -import org.springframework.asm.ClassReader; -import org.springframework.asm.Opcodes; -import org.springframework.core.CollectionFactory; -import org.springframework.util.StringUtils; - -/** - * A {@link Task} for generating metadata describing a project's auto-configuration - * classes. - * - * @author Andy Wilkinson - */ -public class AutoConfigurationMetadata extends DefaultTask { - - private static final String COMMENT_START = "#"; - - private SourceSet sourceSet; - - private File outputFile; - - public AutoConfigurationMetadata() { - getInputs() - .file((Callable) () -> new File(this.sourceSet.getOutput().getResourcesDir(), - "META-INF/spring.factories")) - .withPathSensitivity(PathSensitivity.RELATIVE).withPropertyName("spring.factories"); - getInputs() - .file((Callable) () -> new File(this.sourceSet.getOutput().getResourcesDir(), - "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports")) - .withPathSensitivity(PathSensitivity.RELATIVE) - .withPropertyName("org.springframework.boot.autoconfigure.AutoConfiguration"); - - dependsOn((Callable) () -> this.sourceSet.getProcessResourcesTaskName()); - getProject().getConfigurations() - .maybeCreate(AutoConfigurationPlugin.AUTO_CONFIGURATION_METADATA_CONFIGURATION_NAME); - } - - public void setSourceSet(SourceSet sourceSet) { - this.sourceSet = sourceSet; - } - - @OutputFile - public File getOutputFile() { - return this.outputFile; - } - - public void setOutputFile(File outputFile) { - this.outputFile = outputFile; - } - - @TaskAction - void documentAutoConfiguration() throws IOException { - Properties autoConfiguration = readAutoConfiguration(); - getOutputFile().getParentFile().mkdirs(); - try (FileWriter writer = new FileWriter(getOutputFile())) { - autoConfiguration.store(writer, null); - } - } - - private Properties readAutoConfiguration() throws IOException { - Properties autoConfiguration = CollectionFactory.createSortedProperties(true); - Set classNames = new LinkedHashSet<>(); - classNames.addAll(readSpringFactories()); - classNames.addAll(readAutoConfigurationsFile()); - Set publicClassNames = new LinkedHashSet<>(); - for (String className : classNames) { - File classFile = findClassFile(className); - if (classFile == null) { - throw new IllegalStateException("Auto-configuration class '" + className + "' not found."); - } - try (InputStream in = new FileInputStream(classFile)) { - int access = new ClassReader(in).getAccess(); - if ((access & Opcodes.ACC_PUBLIC) == Opcodes.ACC_PUBLIC) { - publicClassNames.add(className); - } - } - } - autoConfiguration.setProperty("autoConfigurationClassNames", String.join(",", publicClassNames)); - autoConfiguration.setProperty("module", getProject().getName()); - return autoConfiguration; - } - - /** - * Reads auto-configurations from META-INF/spring.factories. - * @return auto-configurations - */ - private Set readSpringFactories() throws IOException { - File file = new File(this.sourceSet.getOutput().getResourcesDir(), "META-INF/spring.factories"); - if (!file.exists()) { - return Collections.emptySet(); - } - Properties springFactories = readSpringFactories(file); - String enableAutoConfiguration = springFactories - .getProperty("org.springframework.boot.autoconfigure.EnableAutoConfiguration"); - return StringUtils.commaDelimitedListToSet(enableAutoConfiguration); - } - - /** - * Reads auto-configurations from - * META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. - * @return auto-configurations - */ - private List readAutoConfigurationsFile() throws IOException { - File file = new File(this.sourceSet.getOutput().getResourcesDir(), - "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports"); - if (!file.exists()) { - return Collections.emptyList(); - } - // Nearly identical copy of - // org.springframework.boot.context.annotation.ImportCandidates.load - try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file)))) { - List autoConfigurations = new ArrayList<>(); - String line; - while ((line = reader.readLine()) != null) { - line = stripComment(line); - line = line.trim(); - if (line.isEmpty()) { - continue; - } - autoConfigurations.add(line); - } - return autoConfigurations; - } - } - - private String stripComment(String line) { - int commentStart = line.indexOf(COMMENT_START); - if (commentStart == -1) { - return line; - } - return line.substring(0, commentStart); - } - - private File findClassFile(String className) { - String classFileName = className.replace(".", "/") + ".class"; - for (File classesDir : this.sourceSet.getOutput().getClassesDirs()) { - File classFile = new File(classesDir, classFileName); - if (classFile.isFile()) { - return classFile; - } - } - return null; - } - - private Properties readSpringFactories(File file) throws IOException { - Properties springFactories = new Properties(); - try (Reader in = new FileReader(file)) { - springFactories.load(in); - } - return springFactories; - } - -} diff --git a/buildSrc/src/main/java/org/springframework/boot/gradle/autoconfigure/AutoConfigurationPlugin.java b/buildSrc/src/main/java/org/springframework/boot/gradle/autoconfigure/AutoConfigurationPlugin.java deleted file mode 100644 index bc667dbe..00000000 --- a/buildSrc/src/main/java/org/springframework/boot/gradle/autoconfigure/AutoConfigurationPlugin.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2012-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.gradle.autoconfigure; - -import java.io.File; -import java.util.Collections; -import java.util.concurrent.Callable; - -import org.gradle.api.Plugin; -import org.gradle.api.Project; -import org.gradle.api.artifacts.Configuration; -import org.gradle.api.plugins.JavaPlugin; -import org.gradle.api.plugins.JavaPluginExtension; -import org.gradle.api.tasks.SourceSet; - -import org.springframework.boot.gradle.context.properties.ConfigurationPropertiesPlugin; - -/** - * {@link Plugin} for projects that define auto-configuration. It reacts to the presence of the - * {@link JavaPlugin} by: - * - *
    - *
  • Applying the {@link ConfigurationPropertiesPlugin}. - *
  • Adding a dependency on the auto-configuration annotation processor. - *
  • Defining a task that produces metadata describing the auto-configuration. The - * metadata is made available as an artifact in the - *
- * - * @author Andy Wilkinson - */ -public class AutoConfigurationPlugin implements Plugin { - - /** - * Name of the {@link Configuration} that holds the auto-configuration metadata - * artifact. - */ - public static final String AUTO_CONFIGURATION_METADATA_CONFIGURATION_NAME = "autoConfigurationMetadata"; - - @Override - public void apply(Project project) { - project.getPlugins().withType(JavaPlugin.class, (javaPlugin) -> { - project.getPlugins().apply(ConfigurationPropertiesPlugin.class); - Configuration annotationProcessors = project.getConfigurations() - .getByName(JavaPlugin.ANNOTATION_PROCESSOR_CONFIGURATION_NAME); - annotationProcessors.getDependencies() - .add(project.getDependencies().project(Collections.singletonMap("path", - ":spring-boot-project:spring-boot-tools:spring-boot-autoconfigure-processor"))); - annotationProcessors.getDependencies() - .add(project.getDependencies().project(Collections.singletonMap("path", - ":spring-boot-project:spring-boot-tools:spring-boot-configuration-processor"))); - project.getTasks().create("autoConfigurationMetadata", AutoConfigurationMetadata.class, (task) -> { - SourceSet main = project.getExtensions().getByType(JavaPluginExtension.class).getSourceSets() - .getByName(SourceSet.MAIN_SOURCE_SET_NAME); - task.setSourceSet(main); - task.dependsOn(main.getClassesTaskName()); - task.setOutputFile(new File(project.getBuildDir(), "auto-configuration-metadata.properties")); - project.getArtifacts().add(AutoConfigurationPlugin.AUTO_CONFIGURATION_METADATA_CONFIGURATION_NAME, - project.provider((Callable) task::getOutputFile), (artifact) -> artifact.builtBy(task)); - }); - }); - } - -} diff --git a/buildSrc/src/main/java/org/springframework/boot/gradle/autoconfigure/DocumentAutoConfigurationClasses.java b/buildSrc/src/main/java/org/springframework/boot/gradle/autoconfigure/DocumentAutoConfigurationClasses.java deleted file mode 100644 index cd55c010..00000000 --- a/buildSrc/src/main/java/org/springframework/boot/gradle/autoconfigure/DocumentAutoConfigurationClasses.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright 2012-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF 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.autoconfigure; - -import java.io.File; -import java.io.FileReader; -import java.io.FileWriter; -import java.io.IOException; -import java.io.PrintWriter; -import java.io.Reader; -import java.util.Properties; -import java.util.Set; -import java.util.SortedSet; -import java.util.TreeSet; -import java.util.stream.Collectors; - -import org.gradle.api.DefaultTask; -import org.gradle.api.Task; -import org.gradle.api.file.FileCollection; -import org.gradle.api.tasks.InputFiles; -import org.gradle.api.tasks.OutputDirectory; -import org.gradle.api.tasks.TaskAction; - -import org.springframework.util.StringUtils; - -/** - * {@link Task} used to document auto-configuration classes. - * - * @author Andy Wilkinson - */ -public class DocumentAutoConfigurationClasses extends DefaultTask { - - private FileCollection autoConfiguration; - - private File outputDir; - - @InputFiles - public FileCollection getAutoConfiguration() { - return this.autoConfiguration; - } - - public void setAutoConfiguration(FileCollection autoConfiguration) { - this.autoConfiguration = autoConfiguration; - } - - @OutputDirectory - public File getOutputDir() { - return this.outputDir; - } - - public void setOutputDir(File outputDir) { - this.outputDir = outputDir; - } - - @TaskAction - void documentAutoConfigurationClasses() throws IOException { - for (File metadataFile : this.autoConfiguration) { - Properties metadata = new Properties(); - try (Reader reader = new FileReader(metadataFile)) { - metadata.load(reader); - } - AutoConfiguration autoConfiguration = new AutoConfiguration(metadata.getProperty("module"), new TreeSet<>( - StringUtils.commaDelimitedListToSet(metadata.getProperty("autoConfigurationClassNames")))); - writeTable(autoConfiguration); - } - } - - private void writeTable(AutoConfiguration autoConfigurationClasses) throws IOException { - this.outputDir.mkdirs(); - try (PrintWriter writer = new PrintWriter( - new FileWriter(new File(this.outputDir, autoConfigurationClasses.module + ".adoc")))) { - writer.println("[cols=\"4,1\"]"); - writer.println("|==="); - writer.println("| Configuration Class | Links"); - - for (AutoConfigurationClass autoConfigurationClass : autoConfigurationClasses.classes) { - writer.println(); - writer.printf("| {spring-boot-code}/spring-boot-project/%s/src/main/java/%s.java[`%s`]%n", - autoConfigurationClasses.module, autoConfigurationClass.path, autoConfigurationClass.name); - writer.printf("| {spring-boot-api}/%s.html[javadoc]%n", autoConfigurationClass.path); - } - - writer.println("|==="); - } - } - - private static final class AutoConfiguration { - - private final String module; - - private final SortedSet classes; - - private AutoConfiguration(String module, Set classNames) { - this.module = module; - this.classes = classNames.stream().map((className) -> { - String path = className.replace('.', '/'); - String name = className.substring(className.lastIndexOf('.') + 1); - return new AutoConfigurationClass(name, path); - }).collect(Collectors.toCollection(TreeSet::new)); - } - - } - - private static final class AutoConfigurationClass implements Comparable { - - private final String name; - - private final String path; - - private AutoConfigurationClass(String name, String path) { - this.name = name; - this.path = path; - } - - @Override - public int compareTo(AutoConfigurationClass other) { - return this.name.compareTo(other.name); - } - - } - -} diff --git a/buildSrc/src/main/java/org/springframework/boot/gradle/classpath/artifactory/ArtifactoryRepository.java b/buildSrc/src/main/java/org/springframework/boot/gradle/classpath/artifactory/ArtifactoryRepository.java deleted file mode 100644 index 5d18c3dd..00000000 --- a/buildSrc/src/main/java/org/springframework/boot/gradle/classpath/artifactory/ArtifactoryRepository.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2012-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF 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.classpath.artifactory; - -import org.gradle.api.Project; - -/** - * An Artifactory repository to which a build of Spring Boot can be published. - * - * @author Andy Wilkinson - */ -public final class ArtifactoryRepository { - - private final String name; - - private ArtifactoryRepository(String name) { - this.name = name; - } - - public String getName() { - return this.name; - } - - @Override - public String toString() { - return this.name; - } - - public static ArtifactoryRepository forProject(Project project) { - return new ArtifactoryRepository(determineArtifactoryRepo(project)); - } - - private static String determineArtifactoryRepo(Project project) { - String version = project.getVersion().toString(); - int modifierIndex = version.lastIndexOf('-'); - if (modifierIndex == -1) { - return "release"; - } - String type = version.substring(modifierIndex + 1); - if (type.startsWith("M") || type.startsWith("RC")) { - return "milestone"; - } - return "snapshot"; - } - -} diff --git a/buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/Asciidoc.java b/buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/Asciidoc.java deleted file mode 100644 index d0f86323..00000000 --- a/buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/Asciidoc.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2012-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.gradle.context.properties; - -/** - * Simple builder to help construct Asciidoc markup. - * - * @author Phillip Webb - */ -class Asciidoc { - - private final StringBuilder content; - - Asciidoc() { - this.content = new StringBuilder(); - } - - Asciidoc appendWithHardLineBreaks(Object... items) { - for (Object item : items) { - appendln("`+", item, "+` +"); - } - return this; - } - - Asciidoc appendln(Object... items) { - return append(items).newLine(); - } - - Asciidoc append(Object... items) { - for (Object item : items) { - this.content.append(item); - } - return this; - } - - Asciidoc newLine() { - return append(System.lineSeparator()); - } - - @Override - public String toString() { - return this.content.toString(); - } - -} diff --git a/buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/CheckAdditionalSpringConfigurationMetadata.java b/buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/CheckAdditionalSpringConfigurationMetadata.java deleted file mode 100644 index f9fc5044..00000000 --- a/buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/CheckAdditionalSpringConfigurationMetadata.java +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright 2012-2022 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.gradle.context.properties; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import com.fasterxml.jackson.core.JsonParseException; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.gradle.api.GradleException; -import org.gradle.api.file.FileTree; -import org.gradle.api.file.RegularFileProperty; -import org.gradle.api.tasks.InputFiles; -import org.gradle.api.tasks.OutputFile; -import org.gradle.api.tasks.PathSensitive; -import org.gradle.api.tasks.PathSensitivity; -import org.gradle.api.tasks.SourceTask; -import org.gradle.api.tasks.TaskAction; - -/** - * {@link SourceTask} that checks additional Spring configuration metadata files. - * - * @author Andy Wilkinson - */ -public class CheckAdditionalSpringConfigurationMetadata extends SourceTask { - - private final RegularFileProperty reportLocation; - - public CheckAdditionalSpringConfigurationMetadata() { - this.reportLocation = getProject().getObjects().fileProperty(); - } - - @OutputFile - public RegularFileProperty getReportLocation() { - return this.reportLocation; - } - - @Override - @InputFiles - @PathSensitive(PathSensitivity.RELATIVE) - public FileTree getSource() { - return super.getSource(); - } - - @TaskAction - void check() throws JsonParseException, IOException { - Report report = createReport(); - File reportFile = getReportLocation().get().getAsFile(); - Files.write(reportFile.toPath(), report, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); - if (report.hasProblems()) { - throw new GradleException( - "Problems found in additional Spring configuration metadata. See " + reportFile + " for details."); - } - } - - @SuppressWarnings("unchecked") - private Report createReport() throws IOException, JsonParseException, JsonMappingException { - ObjectMapper objectMapper = new ObjectMapper(); - Report report = new Report(); - for (File file : getSource().getFiles()) { - Analysis analysis = report.analysis(getProject().getProjectDir().toPath().relativize(file.toPath())); - Map json = objectMapper.readValue(file, Map.class); - check("groups", json, analysis); - check("properties", json, analysis); - check("hints", json, analysis); - } - return report; - } - - @SuppressWarnings("unchecked") - private void check(String key, Map json, Analysis analysis) { - List> groups = (List>) json.get(key); - List names = groups.stream().map((group) -> (String) group.get("name")).collect(Collectors.toList()); - List sortedNames = sortedCopy(names); - for (int i = 0; i < names.size(); i++) { - String actual = names.get(i); - String expected = sortedNames.get(i); - if (!actual.equals(expected)) { - analysis.problems.add("Wrong order at $." + key + "[" + i + "].name - expected '" + expected - + "' but found '" + actual + "'"); - } - } - } - - private List sortedCopy(Collection original) { - List copy = new ArrayList<>(original); - Collections.sort(copy); - return copy; - } - - private static final class Report implements Iterable { - - private final List analyses = new ArrayList<>(); - - private Analysis analysis(Path path) { - Analysis analysis = new Analysis(path); - this.analyses.add(analysis); - return analysis; - } - - private boolean hasProblems() { - for (Analysis analysis : this.analyses) { - if (!analysis.problems.isEmpty()) { - return true; - } - } - return false; - } - - @Override - public Iterator iterator() { - List lines = new ArrayList<>(); - for (Analysis analysis : this.analyses) { - lines.add(analysis.source.toString()); - lines.add(""); - if (analysis.problems.isEmpty()) { - lines.add("No problems found."); - } - else { - lines.addAll(analysis.problems); - } - lines.add(""); - } - return lines.iterator(); - } - - } - - private static final class Analysis { - - private final List problems = new ArrayList<>(); - - private final Path source; - - private Analysis(Path source) { - this.source = source; - } - - } - -} diff --git a/buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/CompoundRow.java b/buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/CompoundRow.java deleted file mode 100644 index 47fa8dbc..00000000 --- a/buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/CompoundRow.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2012-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.gradle.context.properties; - -import java.util.Set; -import java.util.TreeSet; - -/** - * Table row regrouping a list of configuration properties sharing the same description. - * - * @author Brian Clozel - * @author Phillip Webb - */ -class CompoundRow extends Row { - - private final Set propertyNames; - - private final String description; - - CompoundRow(Snippet snippet, String prefix, String description) { - super(snippet, prefix); - this.description = description; - this.propertyNames = new TreeSet<>(); - } - - void addProperty(ConfigurationProperty property) { - this.propertyNames.add(property.getDisplayName()); - } - - @Override - void write(Asciidoc asciidoc) { - asciidoc.append("|"); - asciidoc.append("[[" + getAnchor() + "]]"); - asciidoc.append("<<" + getAnchor() + ","); - this.propertyNames.forEach(asciidoc::appendWithHardLineBreaks); - asciidoc.appendln(">>"); - asciidoc.appendln("|+++", this.description, "+++"); - asciidoc.appendln("|"); - } - -} diff --git a/buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/ConfigurationProperties.java b/buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/ConfigurationProperties.java deleted file mode 100644 index 5e4945cc..00000000 --- a/buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/ConfigurationProperties.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2012-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.gradle.context.properties; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Stream; - -import com.fasterxml.jackson.databind.ObjectMapper; - -/** - * Configuration properties read from one or more - * {@code META-INF/spring-configuration-metadata.json} files. - * - * @author Andy Wilkinson - * @author Phillip Webb - */ -final class ConfigurationProperties { - - private final Map byName; - - private ConfigurationProperties(List properties) { - Map byName = new LinkedHashMap<>(); - for (ConfigurationProperty property : properties) { - byName.put(property.getName(), property); - } - this.byName = Collections.unmodifiableMap(byName); - } - - ConfigurationProperty get(String propertyName) { - return this.byName.get(propertyName); - } - - Stream stream() { - return this.byName.values().stream(); - } - - @SuppressWarnings("unchecked") - static ConfigurationProperties fromFiles(Iterable files) { - try { - ObjectMapper objectMapper = new ObjectMapper(); - List properties = new ArrayList<>(); - for (File file : files) { - Map json = objectMapper.readValue(file, Map.class); - for (Map property : (List>) json.get("properties")) { - properties.add(ConfigurationProperty.fromJsonProperties(property)); - } - } - return new ConfigurationProperties(properties); - } - catch (IOException ex) { - throw new RuntimeException("Failed to load configuration metadata", ex); - } - } - -} diff --git a/buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/ConfigurationPropertiesPlugin.java b/buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/ConfigurationPropertiesPlugin.java deleted file mode 100644 index b8489d42..00000000 --- a/buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/ConfigurationPropertiesPlugin.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright 2012-2022 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.gradle.context.properties; - -import java.util.Collections; -import java.util.stream.Collectors; - -import org.gradle.api.Plugin; -import org.gradle.api.Project; -import org.gradle.api.Task; -import org.gradle.api.artifacts.Configuration; -import org.gradle.api.plugins.JavaPlugin; -import org.gradle.api.plugins.JavaPluginExtension; -import org.gradle.api.tasks.PathSensitivity; -import org.gradle.api.tasks.SourceSet; -import org.gradle.api.tasks.TaskProvider; -import org.gradle.api.tasks.compile.JavaCompile; -import org.gradle.language.base.plugins.LifecycleBasePlugin; - -import org.springframework.util.StringUtils; - -/** - * {@link Plugin} for projects that define {@code @ConfigurationProperties}. When applied, - * the plugin reacts to the presence of the {@link JavaPlugin} by: - * - *
    - *
  • Adding a dependency on the configuration properties annotation processor. - *
  • Configuring the additional metadata locations annotation processor compiler - * argument. - *
  • Adding the outputs of the processResources task as inputs of the compileJava task - * to ensure that the additional metadata is available when the annotation processor runs. - *
  • Registering a {@link CheckAdditionalSpringConfigurationMetadata} task and - * configuring the {@code check} task to depend upon it. - *
  • Defining an artifact for the resulting configuration property metadata so that it - * can be consumed by downstream projects. - *
- * - * @author Andy Wilkinson - */ -public class ConfigurationPropertiesPlugin implements Plugin { - - /** - * Name of the {@link Configuration} that holds the configuration property metadata - * artifact. - */ - public static final String CONFIGURATION_PROPERTIES_METADATA_CONFIGURATION_NAME = "configurationPropertiesMetadata"; - - /** - * Name of the {@link CheckAdditionalSpringConfigurationMetadata} task. - */ - public static final String CHECK_ADDITIONAL_SPRING_CONFIGURATION_METADATA_TASK_NAME = "checkAdditionalSpringConfigurationMetadata"; - - @Override - public void apply(Project project) { - project.getPlugins().withType(JavaPlugin.class, (javaPlugin) -> { - addConfigurationProcessorDependency(project); - configureAdditionalMetadataLocationsCompilerArgument(project); - registerCheckAdditionalMetadataTask(project); - addMetadataArtifact(project); - }); - } - - private void addConfigurationProcessorDependency(Project project) { - Configuration annotationProcessors = project.getConfigurations() - .getByName(JavaPlugin.ANNOTATION_PROCESSOR_CONFIGURATION_NAME); - annotationProcessors.getDependencies().add(project.getDependencies().project(Collections.singletonMap("path", - ":spring-boot-project:spring-boot-tools:spring-boot-configuration-processor"))); - } - - private void addMetadataArtifact(Project project) { - SourceSet mainSourceSet = project.getExtensions().getByType(JavaPluginExtension.class).getSourceSets() - .getByName(SourceSet.MAIN_SOURCE_SET_NAME); - project.getConfigurations().maybeCreate(CONFIGURATION_PROPERTIES_METADATA_CONFIGURATION_NAME); - project.afterEvaluate((evaluatedProject) -> evaluatedProject.getArtifacts().add( - CONFIGURATION_PROPERTIES_METADATA_CONFIGURATION_NAME, - mainSourceSet.getJava().getDestinationDirectory().dir("META-INF/spring-configuration-metadata.json"), - (artifact) -> artifact - .builtBy(evaluatedProject.getTasks().getByName(mainSourceSet.getClassesTaskName())))); - } - - private void configureAdditionalMetadataLocationsCompilerArgument(Project project) { - JavaCompile compileJava = project.getTasks().withType(JavaCompile.class) - .getByName(JavaPlugin.COMPILE_JAVA_TASK_NAME); - ((Task) compileJava).getInputs().files(project.getTasks().getByName(JavaPlugin.PROCESS_RESOURCES_TASK_NAME)) - .withPathSensitivity(PathSensitivity.RELATIVE).withPropertyName("processed resources"); - SourceSet mainSourceSet = project.getExtensions().getByType(JavaPluginExtension.class).getSourceSets() - .getByName(SourceSet.MAIN_SOURCE_SET_NAME); - compileJava.getOptions().getCompilerArgs() - .add("-Aorg.springframework.boot.configurationprocessor.additionalMetadataLocations=" + StringUtils - .collectionToCommaDelimitedString(mainSourceSet.getResources().getSourceDirectories().getFiles() - .stream().map(project.getRootProject()::relativePath).collect(Collectors.toSet()))); - } - - private void registerCheckAdditionalMetadataTask(Project project) { - TaskProvider checkConfigurationMetadata = project.getTasks() - .register(CHECK_ADDITIONAL_SPRING_CONFIGURATION_METADATA_TASK_NAME, - CheckAdditionalSpringConfigurationMetadata.class); - checkConfigurationMetadata.configure((check) -> { - SourceSet mainSourceSet = project.getExtensions().getByType(JavaPluginExtension.class).getSourceSets() - .getByName(SourceSet.MAIN_SOURCE_SET_NAME); - check.setSource(mainSourceSet.getResources()); - check.include("META-INF/additional-spring-configuration-metadata.json"); - check.getReportLocation().set(project.getLayout().getBuildDirectory() - .file("reports/additional-spring-configuration-metadata/check.txt")); - }); - project.getTasks().named(LifecycleBasePlugin.CHECK_TASK_NAME) - .configure((check) -> check.dependsOn(checkConfigurationMetadata)); - } - -} diff --git a/buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/ConfigurationProperty.java b/buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/ConfigurationProperty.java deleted file mode 100644 index 44ab1426..00000000 --- a/buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/ConfigurationProperty.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2012-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.gradle.context.properties; - -import java.util.Map; - -/** - * A configuration property. - * - * @author Andy Wilkinson - */ -class ConfigurationProperty { - - private final String name; - - private final String type; - - private final Object defaultValue; - - private final String description; - - private final boolean deprecated; - - ConfigurationProperty(String name, String type) { - this(name, type, null, null, false); - } - - ConfigurationProperty(String name, String type, Object defaultValue, String description, boolean deprecated) { - this.name = name; - this.type = type; - this.defaultValue = defaultValue; - this.description = description; - this.deprecated = deprecated; - } - - String getName() { - return this.name; - } - - String getDisplayName() { - return (getType() != null && getType().startsWith("java.util.Map")) ? getName() + ".*" : getName(); - } - - String getType() { - return this.type; - } - - Object getDefaultValue() { - return this.defaultValue; - } - - String getDescription() { - return this.description; - } - - boolean isDeprecated() { - return this.deprecated; - } - - @Override - public String toString() { - return "ConfigurationProperty [name=" + this.name + ", type=" + this.type + "]"; - } - - static ConfigurationProperty fromJsonProperties(Map property) { - String name = (String) property.get("name"); - String type = (String) property.get("type"); - Object defaultValue = property.get("defaultValue"); - String description = (String) property.get("description"); - boolean deprecated = property.containsKey("deprecated"); - return new ConfigurationProperty(name, type, defaultValue, description, deprecated); - } - -} diff --git a/buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/DocumentConfigurationProperties.java b/buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/DocumentConfigurationProperties.java deleted file mode 100644 index 410e35e6..00000000 --- a/buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/DocumentConfigurationProperties.java +++ /dev/null @@ -1,220 +0,0 @@ -/* - * Copyright 2012-2022 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.gradle.context.properties; - -import java.io.File; -import java.io.IOException; - -import org.gradle.api.DefaultTask; -import org.gradle.api.Task; -import org.gradle.api.file.FileCollection; -import org.gradle.api.tasks.InputFiles; -import org.gradle.api.tasks.OutputDirectory; -import org.gradle.api.tasks.PathSensitive; -import org.gradle.api.tasks.PathSensitivity; -import org.gradle.api.tasks.TaskAction; - -import org.springframework.boot.gradle.context.properties.Snippet.Config; - -/** - * {@link Task} used to document auto-configuration classes. - * - * @author Andy Wilkinson - * @author Phillip Webb - */ -public class DocumentConfigurationProperties extends DefaultTask { - - private FileCollection configurationPropertyMetadata; - - private File outputDir; - - @InputFiles - @PathSensitive(PathSensitivity.RELATIVE) - public FileCollection getConfigurationPropertyMetadata() { - return this.configurationPropertyMetadata; - } - - public void setConfigurationPropertyMetadata(FileCollection configurationPropertyMetadata) { - this.configurationPropertyMetadata = configurationPropertyMetadata; - } - - @OutputDirectory - public File getOutputDir() { - return this.outputDir; - } - - public void setOutputDir(File outputDir) { - this.outputDir = outputDir; - } - - @TaskAction - void documentConfigurationProperties() throws IOException { - Snippets snippets = new Snippets(this.configurationPropertyMetadata); - snippets.add("application-properties.core", "Core Properties", this::corePrefixes); - snippets.add("application-properties.cache", "Cache Properties", this::cachePrefixes); - snippets.add("application-properties.mail", "Mail Properties", this::mailPrefixes); - snippets.add("application-properties.json", "JSON Properties", this::jsonPrefixes); - snippets.add("application-properties.data", "Data Properties", this::dataPrefixes); - snippets.add("application-properties.transaction", "Transaction Properties", this::transactionPrefixes); - snippets.add("application-properties.data-migration", "Data Migration Properties", this::dataMigrationPrefixes); - snippets.add("application-properties.integration", "Integration Properties", this::integrationPrefixes); - snippets.add("application-properties.web", "Web Properties", this::webPrefixes); - snippets.add("application-properties.templating", "Templating Properties", this::templatePrefixes); - snippets.add("application-properties.server", "Server Properties", this::serverPrefixes); - snippets.add("application-properties.security", "Security Properties", this::securityPrefixes); - snippets.add("application-properties.rsocket", "RSocket Properties", this::rsocketPrefixes); - snippets.add("application-properties.actuator", "Actuator Properties", this::actuatorPrefixes); - snippets.add("application-properties.devtools", "Devtools Properties", this::devtoolsPrefixes); - snippets.add("application-properties.testing", "Testing Properties", this::testingPrefixes); - snippets.writeTo(this.outputDir.toPath()); - } - - private void corePrefixes(Config config) { - config.accept("debug"); - config.accept("trace"); - config.accept("logging"); - config.accept("spring.aop"); - config.accept("spring.application"); - config.accept("spring.autoconfigure"); - config.accept("spring.banner"); - config.accept("spring.beaninfo"); - config.accept("spring.codec"); - config.accept("spring.config"); - config.accept("spring.info"); - config.accept("spring.jmx"); - config.accept("spring.lifecycle"); - config.accept("spring.main"); - config.accept("spring.messages"); - config.accept("spring.pid"); - config.accept("spring.profiles"); - config.accept("spring.quartz"); - config.accept("spring.reactor"); - config.accept("spring.task"); - config.accept("spring.mandatory-file-encoding"); - config.accept("info"); - config.accept("spring.output.ansi.enabled"); - } - - private void cachePrefixes(Config config) { - config.accept("spring.cache"); - } - - private void mailPrefixes(Config config) { - config.accept("spring.mail"); - config.accept("spring.sendgrid"); - } - - private void jsonPrefixes(Config config) { - config.accept("spring.jackson"); - config.accept("spring.gson"); - } - - private void dataPrefixes(Config config) { - config.accept("spring.couchbase"); - config.accept("spring.elasticsearch"); - config.accept("spring.h2"); - config.accept("spring.influx"); - config.accept("spring.ldap"); - config.accept("spring.mongodb"); - config.accept("spring.neo4j"); - config.accept("spring.redis"); - config.accept("spring.dao"); - config.accept("spring.data"); - config.accept("spring.datasource"); - config.accept("spring.jooq"); - config.accept("spring.jdbc"); - config.accept("spring.jpa"); - config.accept("spring.r2dbc"); - config.accept("spring.datasource.oracleucp", - "Oracle UCP specific settings bound to an instance of Oracle UCP's PoolDataSource"); - config.accept("spring.datasource.dbcp2", - "Commons DBCP2 specific settings bound to an instance of DBCP2's BasicDataSource"); - config.accept("spring.datasource.tomcat", - "Tomcat datasource specific settings bound to an instance of Tomcat JDBC's DataSource"); - config.accept("spring.datasource.hikari", - "Hikari specific settings bound to an instance of Hikari's HikariDataSource"); - - } - - private void transactionPrefixes(Config prefix) { - prefix.accept("spring.jta"); - prefix.accept("spring.transaction"); - } - - private void dataMigrationPrefixes(Config prefix) { - prefix.accept("spring.flyway"); - prefix.accept("spring.liquibase"); - prefix.accept("spring.sql.init"); - } - - private void integrationPrefixes(Config prefix) { - prefix.accept("spring.activemq"); - prefix.accept("spring.artemis"); - prefix.accept("spring.batch"); - prefix.accept("spring.integration"); - prefix.accept("spring.jms"); - prefix.accept("spring.kafka"); - prefix.accept("spring.rabbitmq"); - prefix.accept("spring.hazelcast"); - prefix.accept("spring.webservices"); - } - - private void webPrefixes(Config prefix) { - prefix.accept("spring.hateoas"); - prefix.accept("spring.http"); - prefix.accept("spring.servlet"); - prefix.accept("spring.mvc"); - prefix.accept("spring.netty"); - prefix.accept("spring.resources"); - prefix.accept("spring.session"); - prefix.accept("spring.web"); - prefix.accept("spring.webflux"); - } - - private void templatePrefixes(Config prefix) { - prefix.accept("spring.freemarker"); - prefix.accept("spring.groovy"); - prefix.accept("spring.mustache"); - prefix.accept("spring.thymeleaf"); - prefix.accept("spring.groovy.template.configuration", "See GroovyMarkupConfigurer"); - } - - private void serverPrefixes(Config prefix) { - prefix.accept("server"); - } - - private void securityPrefixes(Config prefix) { - prefix.accept("spring.security"); - } - - private void rsocketPrefixes(Config prefix) { - prefix.accept("spring.rsocket"); - } - - private void actuatorPrefixes(Config prefix) { - prefix.accept("management"); - } - - private void devtoolsPrefixes(Config prefix) { - prefix.accept("spring.devtools"); - } - - private void testingPrefixes(Config prefix) { - prefix.accept("spring.test"); - } - -} diff --git a/buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/Row.java b/buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/Row.java deleted file mode 100644 index 2b5fbf8a..00000000 --- a/buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/Row.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2012-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.gradle.context.properties; - -/** - * Abstract class for rows in {@link Table}. - * - * @author Brian Clozel - * @author Phillip Webb - */ -abstract class Row implements Comparable { - - private final Snippet snippet; - - private final String id; - - protected Row(Snippet snippet, String id) { - this.snippet = snippet; - this.id = id; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - Row other = (Row) obj; - return this.id.equals(other.id); - } - - @Override - public int hashCode() { - return this.id.hashCode(); - } - - @Override - public int compareTo(Row other) { - return this.id.compareTo(other.id); - } - - String getAnchor() { - return this.snippet.getAnchor() + "." + this.id; - } - - abstract void write(Asciidoc asciidoc); - -} diff --git a/buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/SingleRow.java b/buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/SingleRow.java deleted file mode 100644 index 1de34c37..00000000 --- a/buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/SingleRow.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2012-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.gradle.context.properties; - -import java.util.Arrays; -import java.util.stream.Collectors; - -/** - * Table row containing a single configuration property. - * - * @author Brian Clozel - * @author Phillip Webb - */ -class SingleRow extends Row { - - private final String displayName; - - private final String description; - - private final String defaultValue; - - SingleRow(Snippet snippet, ConfigurationProperty property) { - super(snippet, property.getName()); - this.displayName = property.getDisplayName(); - this.description = property.getDescription(); - this.defaultValue = getDefaultValue(property.getDefaultValue()); - } - - private String getDefaultValue(Object defaultValue) { - if (defaultValue == null) { - return null; - } - if (defaultValue.getClass().isArray()) { - return Arrays.stream((Object[]) defaultValue).map(Object::toString) - .collect(Collectors.joining("," + System.lineSeparator())); - } - return defaultValue.toString(); - } - - @Override - void write(Asciidoc asciidoc) { - asciidoc.append("|"); - asciidoc.append("[[" + getAnchor() + "]]"); - asciidoc.appendln("<<" + getAnchor() + ",`+", this.displayName, "+`>>"); - writeDescription(asciidoc); - writeDefaultValue(asciidoc); - } - - private void writeDescription(Asciidoc builder) { - if (this.description == null || this.description.isEmpty()) { - builder.appendln("|"); - } - else { - String cleanedDescription = this.description.replace("|", "\\|").replace("<", "<").replace(">", ">"); - builder.appendln("|+++", cleanedDescription, "+++"); - } - } - - private void writeDefaultValue(Asciidoc builder) { - String defaultValue = (this.defaultValue != null) ? this.defaultValue : ""; - if (defaultValue.isEmpty()) { - builder.appendln("|"); - } - else { - defaultValue = defaultValue.replace("\\", "\\\\").replace("|", "\\|"); - builder.appendln("|`+", defaultValue, "+`"); - } - } - -} diff --git a/buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/Snippet.java b/buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/Snippet.java deleted file mode 100644 index b4a2877f..00000000 --- a/buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/Snippet.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright 2012-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.gradle.context.properties; - -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.Map; -import java.util.Set; -import java.util.function.BiConsumer; -import java.util.function.Consumer; - -/** - * A configuration properties snippet. - * - * @author Brian Clozed - * @author Phillip Webb - */ -class Snippet { - - private final String anchor; - - private final String title; - - private final Set prefixes; - - private final Map overrides; - - Snippet(String anchor, String title, Consumer config) { - Set prefixes = new LinkedHashSet<>(); - Map overrides = new LinkedHashMap<>(); - if (config != null) { - config.accept(new Config() { - - @Override - public void accept(String prefix) { - prefixes.add(prefix); - } - - @Override - public void accept(String prefix, String description) { - overrides.put(prefix, description); - } - - }); - } - this.anchor = anchor; - this.title = title; - this.prefixes = prefixes; - this.overrides = overrides; - } - - String getAnchor() { - return this.anchor; - } - - String getTitle() { - return this.title; - } - - void forEachPrefix(Consumer action) { - this.prefixes.forEach(action); - } - - void forEachOverride(BiConsumer action) { - this.overrides.forEach(action); - } - - /** - * Callback to configure the snippet. - */ - interface Config { - - /** - * Accept the given prefix using the meta-data description. - * @param prefix the prefix to accept - */ - void accept(String prefix); - - /** - * Accept the given prefix with a defined description. - * @param prefix the prefix to accept - * @param description the description to use - */ - void accept(String prefix, String description); - - } - -} diff --git a/buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/Snippets.java b/buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/Snippets.java deleted file mode 100644 index 23885d98..00000000 --- a/buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/Snippets.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright 2012-2022 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.gradle.context.properties; - -import java.io.IOException; -import java.io.OutputStream; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.function.Consumer; -import java.util.stream.Collectors; - -import org.gradle.api.file.FileCollection; - -/** - * Configuration properties snippets. - * - * @author Brian Clozed - * @author Phillip Webb - */ -class Snippets { - - private final ConfigurationProperties properties; - - private final List snippets = new ArrayList<>(); - - Snippets(FileCollection configurationPropertyMetadata) { - this.properties = ConfigurationProperties.fromFiles(configurationPropertyMetadata); - } - - void add(String anchor, String title, Consumer config) { - this.snippets.add(new Snippet(anchor, title, config)); - } - - void writeTo(Path outputDirectory) throws IOException { - createDirectory(outputDirectory); - Set remaining = this.properties.stream().filter((property) -> !property.isDeprecated()) - .map(ConfigurationProperty::getName).collect(Collectors.toSet()); - for (Snippet snippet : this.snippets) { - Set written = writeSnippet(outputDirectory, snippet, remaining); - remaining.removeAll(written); - } - if (!remaining.isEmpty()) { - throw new IllegalStateException( - "The following keys were not written to the documentation: " + String.join(", ", remaining)); - } - } - - private Set writeSnippet(Path outputDirectory, Snippet snippet, Set remaining) throws IOException { - Table table = new Table(); - Set added = new HashSet<>(); - snippet.forEachOverride((prefix, description) -> { - CompoundRow row = new CompoundRow(snippet, prefix, description); - remaining.stream().filter((candidate) -> candidate.startsWith(prefix)).forEach((name) -> { - if (added.add(name)) { - row.addProperty(this.properties.get(name)); - } - }); - table.addRow(row); - }); - snippet.forEachPrefix((prefix) -> { - remaining.stream().filter((candidate) -> candidate.startsWith(prefix)).forEach((name) -> { - if (added.add(name)) { - table.addRow(new SingleRow(snippet, this.properties.get(name))); - } - }); - }); - Asciidoc asciidoc = getAsciidoc(snippet, table); - writeAsciidoc(outputDirectory, snippet, asciidoc); - return added; - } - - private Asciidoc getAsciidoc(Snippet snippet, Table table) { - Asciidoc asciidoc = new Asciidoc(); - // We have to prepend 'appendix.' as a section id here, otherwise the - // spring-asciidoctor-extensions:section-id asciidoctor extension complains - asciidoc.appendln("[[appendix." + snippet.getAnchor() + "]]"); - asciidoc.appendln("== ", snippet.getTitle()); - table.write(asciidoc); - return asciidoc; - } - - private void writeAsciidoc(Path outputDirectory, Snippet snippet, Asciidoc asciidoc) throws IOException { - String[] parts = (snippet.getAnchor()).split("\\."); - Path path = outputDirectory; - for (int i = 0; i < parts.length; i++) { - String name = (i < parts.length - 1) ? parts[i] : parts[i] + ".adoc"; - path = path.resolve(name); - } - createDirectory(path.getParent()); - Files.deleteIfExists(path); - try (OutputStream outputStream = Files.newOutputStream(path)) { - outputStream.write(asciidoc.toString().getBytes(StandardCharsets.UTF_8)); - } - } - - private void createDirectory(Path path) throws IOException { - assertValidOutputDirectory(path); - if (!Files.exists(path)) { - Files.createDirectory(path); - } - } - - private void assertValidOutputDirectory(Path path) { - if (path == null) { - throw new IllegalArgumentException("Directory path should not be null"); - } - if (Files.exists(path) && !Files.isDirectory(path)) { - throw new IllegalArgumentException("Path already exists and is not a directory"); - } - } - -} diff --git a/buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/Table.java b/buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/Table.java deleted file mode 100644 index dddb8ac1..00000000 --- a/buildSrc/src/main/java/org/springframework/boot/gradle/context/properties/Table.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2012-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.gradle.context.properties; - -import java.util.Set; -import java.util.TreeSet; - -/** - * Asciidoctor table listing configuration properties sharing to a common theme. - * - * @author Brian Clozel - */ -class Table { - - private final Set rows = new TreeSet<>(); - - void addRow(Row row) { - this.rows.add(row); - } - - void write(Asciidoc asciidoc) { - asciidoc.appendln("[cols=\"4,3,3\", options=\"header\"]"); - asciidoc.appendln("|==="); - asciidoc.appendln("|Name|Description|Default Value"); - asciidoc.appendln(); - this.rows.forEach((entry) -> { - entry.write(asciidoc); - asciidoc.appendln(); - }); - asciidoc.appendln("|==="); - } - -} diff --git a/buildSrc/src/main/java/org/springframework/boot/gradle/test/IntegrationTestPlugin.java b/buildSrc/src/main/java/org/springframework/boot/gradle/test/IntegrationTestPlugin.java deleted file mode 100644 index 59c06aa8..00000000 --- a/buildSrc/src/main/java/org/springframework/boot/gradle/test/IntegrationTestPlugin.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2012-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.gradle.test; - -import org.gradle.api.Plugin; -import org.gradle.api.Project; -import org.gradle.api.plugins.JavaPlugin; -import org.gradle.api.plugins.JavaPluginExtension; -import org.gradle.api.tasks.SourceSet; -import org.gradle.api.tasks.SourceSetContainer; -import org.gradle.api.tasks.testing.Test; -import org.gradle.language.base.plugins.LifecycleBasePlugin; -import org.gradle.plugins.ide.eclipse.EclipsePlugin; -import org.gradle.plugins.ide.eclipse.model.EclipseModel; - -/** - * A {@link Plugin} to configure integration testing support in a {@link Project}. - * - * @author Andy Wilkinson - */ -public class IntegrationTestPlugin implements Plugin { - - /** - * Name of the {@code intTest} task. - */ - public static String INT_TEST_TASK_NAME = "intTest"; - - /** - * Name of the {@code intTest} source set. - */ - public static String INT_TEST_SOURCE_SET_NAME = "intTest"; - - @Override - public void apply(Project project) { - project.getPlugins().withType(JavaPlugin.class, (javaPlugin) -> configureIntegrationTesting(project)); - } - - private void configureIntegrationTesting(Project project) { - SourceSet intTestSourceSet = createSourceSet(project); - Test intTest = createTestTask(project, intTestSourceSet); - project.getTasks().getByName(LifecycleBasePlugin.CHECK_TASK_NAME).dependsOn(intTest); - project.getPlugins().withType(EclipsePlugin.class, (eclipsePlugin) -> { - EclipseModel eclipse = project.getExtensions().getByType(EclipseModel.class); - eclipse.classpath((classpath) -> classpath.getPlusConfigurations().add( - project.getConfigurations().getByName(intTestSourceSet.getRuntimeClasspathConfigurationName()))); - }); - } - - private SourceSet createSourceSet(Project project) { - SourceSetContainer sourceSets = project.getExtensions().getByType(JavaPluginExtension.class).getSourceSets(); - SourceSet intTestSourceSet = sourceSets.create(INT_TEST_SOURCE_SET_NAME); - SourceSet main = sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME); - intTestSourceSet.setCompileClasspath(intTestSourceSet.getCompileClasspath().plus(main.getOutput())); - intTestSourceSet.setRuntimeClasspath(intTestSourceSet.getRuntimeClasspath().plus(main.getOutput())); - return intTestSourceSet; - } - - private Test createTestTask(Project project, SourceSet intTestSourceSet) { - Test intTest = project.getTasks().create(INT_TEST_TASK_NAME, Test.class); - intTest.setGroup(LifecycleBasePlugin.VERIFICATION_GROUP); - intTest.setDescription("Runs integration tests."); - intTest.setTestClassesDirs(intTestSourceSet.getOutput().getClassesDirs()); - intTest.setClasspath(intTestSourceSet.getRuntimeClasspath()); - intTest.shouldRunAfter(JavaPlugin.TEST_TASK_NAME); - return intTest; - } - -} diff --git a/buildSrc/src/main/java/org/springframework/boot/gradle/test/SystemTestPlugin.java b/buildSrc/src/main/java/org/springframework/boot/gradle/test/SystemTestPlugin.java deleted file mode 100644 index 4c1e9718..00000000 --- a/buildSrc/src/main/java/org/springframework/boot/gradle/test/SystemTestPlugin.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2012-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.gradle.test; - -import org.gradle.api.Plugin; -import org.gradle.api.Project; -import org.gradle.api.Task; -import org.gradle.api.plugins.JavaPlugin; -import org.gradle.api.plugins.JavaPluginExtension; -import org.gradle.api.specs.Spec; -import org.gradle.api.tasks.SourceSet; -import org.gradle.api.tasks.SourceSetContainer; -import org.gradle.api.tasks.testing.Test; -import org.gradle.language.base.plugins.LifecycleBasePlugin; -import org.gradle.plugins.ide.eclipse.EclipsePlugin; -import org.gradle.plugins.ide.eclipse.model.EclipseModel; - -/** - * A {@link Plugin} to configure system testing support in a {@link Project}. - * - * @author Andy Wilkinson - * @author Scott Frederick - */ -public class SystemTestPlugin implements Plugin { - - private static final Spec NEVER = (task) -> false; - - /** - * Name of the {@code systemTest} task. - */ - public static String SYSTEM_TEST_TASK_NAME = "systemTest"; - - /** - * Name of the {@code systemTest} source set. - */ - public static String SYSTEM_TEST_SOURCE_SET_NAME = "systemTest"; - - @Override - public void apply(Project project) { - project.getPlugins().withType(JavaPlugin.class, (javaPlugin) -> configureSystemTesting(project)); - } - - private void configureSystemTesting(Project project) { - SourceSet systemTestSourceSet = createSourceSet(project); - createTestTask(project, systemTestSourceSet); - project.getPlugins().withType(EclipsePlugin.class, (eclipsePlugin) -> { - EclipseModel eclipse = project.getExtensions().getByType(EclipseModel.class); - eclipse.classpath((classpath) -> classpath.getPlusConfigurations().add( - project.getConfigurations().getByName(systemTestSourceSet.getRuntimeClasspathConfigurationName()))); - }); - } - - private SourceSet createSourceSet(Project project) { - SourceSetContainer sourceSets = project.getExtensions().getByType(JavaPluginExtension.class).getSourceSets(); - SourceSet systemTestSourceSet = sourceSets.create(SYSTEM_TEST_SOURCE_SET_NAME); - SourceSet mainSourceSet = sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME); - systemTestSourceSet - .setCompileClasspath(systemTestSourceSet.getCompileClasspath().plus(mainSourceSet.getOutput())); - systemTestSourceSet - .setRuntimeClasspath(systemTestSourceSet.getRuntimeClasspath().plus(mainSourceSet.getOutput())); - return systemTestSourceSet; - } - - private void createTestTask(Project project, SourceSet systemTestSourceSet) { - Test systemTest = project.getTasks().create(SYSTEM_TEST_TASK_NAME, Test.class); - systemTest.setGroup(LifecycleBasePlugin.VERIFICATION_GROUP); - systemTest.setDescription("Runs system tests."); - systemTest.setTestClassesDirs(systemTestSourceSet.getOutput().getClassesDirs()); - systemTest.setClasspath(systemTestSourceSet.getRuntimeClasspath()); - systemTest.shouldRunAfter(JavaPlugin.TEST_TASK_NAME); - if (isCi()) { - systemTest.getOutputs().upToDateWhen(NEVER); - } - } - - private boolean isCi() { - return Boolean.parseBoolean(System.getenv("CI")); - } - -} diff --git a/buildSrc/src/main/java/org/springframework/pulsar/gradle/JacocoConventionsPlugin.java b/buildSrc/src/main/java/org/springframework/pulsar/gradle/JacocoConventionsPlugin.java index 91db2844..fe925a14 100644 --- a/buildSrc/src/main/java/org/springframework/pulsar/gradle/JacocoConventionsPlugin.java +++ b/buildSrc/src/main/java/org/springframework/pulsar/gradle/JacocoConventionsPlugin.java @@ -18,16 +18,14 @@ public class JacocoConventionsPlugin implements Plugin { @Override public void apply(final Project project) { project.getPlugins().withType(JavaPlugin.class, (javaPlugin) -> { + project.getPluginManager().apply(JacocoPlugin.class); - project.getExtensions().configure(JacocoPluginExtension.class, (jacocoExtension) -> { - jacocoExtension.setToolVersion("0.8.7"); - }); - project.getTasks().withType(Test.class, test -> { - project.getTasks().withType(JacocoReport.class, jacocoReport -> { - test.finalizedBy(jacocoReport); - jacocoReport.dependsOn(test); - }); - }); + + project.getExtensions().configure(JacocoPluginExtension.class, + (jacocoExtension) -> jacocoExtension.setToolVersion("0.8.7")); + + project.getTasks().withType(Test.class, (test) -> + project.getTasks().withType(JacocoReport.class, test::finalizedBy)); }); } } diff --git a/buildSrc/src/main/java/org/springframework/pulsar/gradle/SpringDocsModulePlugin.java b/buildSrc/src/main/java/org/springframework/pulsar/gradle/SpringDocsModulePlugin.java index 11885677..5a46a653 100644 --- a/buildSrc/src/main/java/org/springframework/pulsar/gradle/SpringDocsModulePlugin.java +++ b/buildSrc/src/main/java/org/springframework/pulsar/gradle/SpringDocsModulePlugin.java @@ -16,6 +16,7 @@ package org.springframework.pulsar.gradle; +import io.spring.gradle.convention.IntegrationTestPlugin; import io.spring.gradle.convention.RepositoryConventionPlugin; import org.gradle.api.Plugin; import org.gradle.api.Project; @@ -44,5 +45,6 @@ public class SpringDocsModulePlugin implements Plugin { pluginManager.apply(AsciidoctorConventionsPlugin.class); pluginManager.apply(SpringPublishPlugin.class); pluginManager.apply(OptionalDependenciesPlugin.class); + pluginManager.apply(IntegrationTestPlugin.class); } } diff --git a/gradle.properties b/gradle.properties index ab0d811a..e13fec84 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,4 +2,4 @@ version=0.1.1-SNAPSHOT org.gradle.caching=true org.gradle.parallel=true -org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 +org.gradle.jvmargs=-Xmx4g -XX:+HeapDumpOnOutOfMemoryError -XX:+UseParallelGC -Dfile.encoding=UTF-8 diff --git a/spring-pulsar-spring-boot-autoconfigure/build.gradle b/spring-pulsar-spring-boot-autoconfigure/build.gradle index 0631b7f7..2ed2f534 100644 --- a/spring-pulsar-spring-boot-autoconfigure/build.gradle +++ b/spring-pulsar-spring-boot-autoconfigure/build.gradle @@ -33,6 +33,10 @@ test { testLogging.showStandardStreams = true } +integrationTest { + maxHeapSize '2048m' +} + task downloadRabbitConnector { onlyIf { System.getProperty("downloadRabbitConnector") == "true" diff --git a/spring-pulsar-spring-boot-autoconfigure/src/integration-test/java/org/springframework/pulsar/autoconfigure/PulsarFunctionAdministrationIntegrationTests.java b/spring-pulsar-spring-boot-autoconfigure/src/integration-test/java/org/springframework/pulsar/autoconfigure/PulsarFunctionAdministrationIntegrationTests.java new file mode 100644 index 00000000..82ed07eb --- /dev/null +++ b/spring-pulsar-spring-boot-autoconfigure/src/integration-test/java/org/springframework/pulsar/autoconfigure/PulsarFunctionAdministrationIntegrationTests.java @@ -0,0 +1,292 @@ +/* + * Copyright 2023-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.pulsar.autoconfigure; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.IOException; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.stream.LongStream; + +import org.apache.pulsar.client.admin.PulsarAdmin; +import org.apache.pulsar.client.admin.PulsarAdminException; +import org.apache.pulsar.client.admin.PulsarAdminException.NotFoundException; +import org.apache.pulsar.client.api.PulsarClientException; +import org.apache.pulsar.common.io.SourceConfig; +import org.apache.pulsar.common.policies.data.SourceStatus; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIf; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.TestWatcher; +import org.testcontainers.containers.BindMode; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.PulsarContainer; +import org.testcontainers.containers.RabbitMQContainer; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.ResourcePatternUtils; +import org.springframework.core.log.LogAccessor; +import org.springframework.lang.Nullable; +import org.springframework.pulsar.annotation.PulsarListener; +import org.springframework.pulsar.autoconfigure.PulsarFunctionAdministrationIntegrationTests.ContainerLoggingTestWatcher; +import org.springframework.pulsar.function.PulsarFunctionAdministration; +import org.springframework.pulsar.function.PulsarFunctionOperations.FunctionStopPolicy; +import org.springframework.pulsar.function.PulsarSource; +import org.springframework.pulsar.test.support.PulsarTestContainerSupport; + +/** + * Integration tests for {@link PulsarFunctionAdministration}. + *

+ * Sets up a Rabbit container and a Rabbit source and verifies end-end functionality. + * + * @author Chris Bono + */ +@Testcontainers(disabledWithoutDocker = true) +@EnabledIf("rabbitConnectorExists") +@ExtendWith(ContainerLoggingTestWatcher.class) +class PulsarFunctionAdministrationIntegrationTests { + + private static final String RABBIT_QUEUE = "pft_foo_queue"; + + private static final String PULSAR_TOPIC = "pft_foo-topic"; + + private static final PulsarContainer PULSAR_CONTAINER = new PulsarContainer( + PulsarTestContainerSupport.getPulsarImage()); + + private static final RabbitMQContainer RABBITMQ_CONTAINER = new RabbitMQContainer("rabbitmq"); + + @BeforeAll + static void startContainers() { + Network sharedNetwork = Network.newNetwork(); + // @formatter:off + PULSAR_CONTAINER + .withNetwork(sharedNetwork) + .withFunctionsWorker() + .withClasspathResourceMapping("/connectors/", "/pulsar/connectors", BindMode.READ_ONLY) + .start(); + RABBITMQ_CONTAINER + .withNetwork(sharedNetwork) + .withNetworkAliases("rabbitmq") + .withExposedPorts(5672, 15672) + .withStartupTimeout(Duration.ofMinutes(1)) + .start(); + // @formatter:on + } + + private static final CountDownLatch RECEIVED_MESSAGE_LATCH = new CountDownLatch(10); + + private static final List RECEIVED_MESSAGES = new ArrayList<>(); + + static void messageReceived(String message) { + RECEIVED_MESSAGE_LATCH.countDown(); + RECEIVED_MESSAGES.add(message); + } + + @Test + void verifyRabbitSourceIsCreatedAndMessagesAreSourcedIntoPulsar() throws Exception { + SpringApplication app = new SpringApplication(PulsarFunctionTestConfiguration.class); + app.setWebApplicationType(WebApplicationType.NONE); + try (ConfigurableApplicationContext context = app.run( + "--spring.pulsar.client.service-url=" + PULSAR_CONTAINER.getPulsarBrokerUrl(), + "--spring.pulsar.administration.service-url=" + PULSAR_CONTAINER.getHttpServiceUrl(), + "--spring.rabbitmq.host=" + RABBITMQ_CONTAINER.getHost(), + "--spring.rabbitmq.port=" + RABBITMQ_CONTAINER.getAmqpPort())) { + + // Give source time to get ready + Thread.sleep(20000); + + // Send messages to rabbit and wait for them to come through the rabbit source + RabbitTemplate rabbitTemplate = context.getBean(RabbitTemplate.class); + List messages = LongStream.range(0, RECEIVED_MESSAGE_LATCH.getCount()).mapToObj((i) -> "bar" + i) + .toList(); + messages.forEach(msg -> rabbitTemplate.convertAndSend(RABBIT_QUEUE, msg)); + + assertThat(RECEIVED_MESSAGE_LATCH.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(RECEIVED_MESSAGES).containsExactlyElementsOf(messages); + } + } + + @Test + void verifyStopPolicyIsEnforcedOnShutdown() throws Exception { + SpringApplication app = new SpringApplication(PulsarFunctionStopPolicyTestConfiguration.class); + app.setWebApplicationType(WebApplicationType.NONE); + try (ConfigurableApplicationContext ignored = app.run( + "--spring.pulsar.client.service-url=" + PULSAR_CONTAINER.getPulsarBrokerUrl(), + "--spring.pulsar.administration.service-url=" + PULSAR_CONTAINER.getHttpServiceUrl(), + "--spring.rabbitmq.host=" + RABBITMQ_CONTAINER.getHost(), + "--spring.rabbitmq.port=" + RABBITMQ_CONTAINER.getAmqpPort())) { + + // Give source time to get ready + Thread.sleep(20000); + + // Verify the sources are up and running + try (PulsarAdmin admin = getAdmin()) { + assertSourceExistsWithStatus("rabbit-test-source-none", true, admin); + assertSourceExistsWithStatus("rabbit-test-source-stop", true, admin); + assertSourceExistsWithStatus("rabbit-test-source-delete", true, admin); + } + } + + // Stop policy runs after context close - verify source are in expected state + try (PulsarAdmin admin = getAdmin()) { + assertSourceExistsWithStatus("rabbit-test-source-none", true, admin); + assertSourceExistsWithStatus("rabbit-test-source-stop", false, admin); + assertSourceDoesNotExist("rabbit-test-source-delete", admin); + } + } + + private PulsarAdmin getAdmin() throws PulsarClientException { + return PulsarAdmin.builder().serviceHttpUrl(PULSAR_CONTAINER.getHttpServiceUrl()).build(); + } + + private void assertSourceExistsWithStatus(String name, boolean isRunning, PulsarAdmin admin) + throws PulsarAdminException { + assertThat(admin.sources().getSourceStatus("public", "default", name)).isNotNull() + .extracting(SourceStatus::getNumRunning).isEqualTo(isRunning ? 1 : 0); + } + + private void assertSourceDoesNotExist(String name, PulsarAdmin admin) { + assertThatThrownBy(() -> admin.sources().getSourceStatus("public", "default", name)) + .isInstanceOf(NotFoundException.class); + } + + static boolean rabbitConnectorExists() { + try { + Resource[] connectors = ResourcePatternUtils.getResourcePatternResolver(new DefaultResourceLoader()) + .getResources("classpath:/connectors/**"); + boolean available = Arrays.stream(connectors).map(Resource::getFilename).filter(Objects::nonNull) + .anyMatch((name) -> name.contains("pulsar-io-rabbitmq")); + if (!available) { + logTestDisabledReason(); + return false; + } + return true; + } + catch (IOException e) { + logTestDisabledReason(); + return false; + } + } + + private static void logTestDisabledReason() { + System.err.printf("Skipping %s - Rabbit connector was not available in 'src/test/resources/connectors/'%n", + PulsarFunctionAdministrationIntegrationTests.class.getName()); + } + + static PulsarSource rabbitPulsarSource(@Nullable FunctionStopPolicy stopPolicy) { + // This Rabbit host/port config is what the Pulsar container uses to contact + // the Rabbit container. So that container-container is reachable we use a + // custom network and a network alias 'rabbitmq' and the exposed port '5672'. + // This differs from typical RabbitTemplate/RabbitProperties coordinates which + // require the mapped host and port (outside the container). + String suffix = stopPolicy != null ? ("-" + stopPolicy.name().toLowerCase()) : ""; + Map configs = new HashMap<>(); + configs.put("host", "rabbitmq"); + configs.put("port", 5672); + configs.put("virtualHost", "/"); + configs.put("username", "guest"); + configs.put("password", "guest"); + configs.put("queueName", RABBIT_QUEUE + suffix); + configs.put("connectionName", "pft_foo_connection" + suffix); + SourceConfig sourceConfig = SourceConfig.builder().tenant("public").namespace("default") + .name("rabbit-test-source" + suffix).archive("builtin://rabbitmq").topicName(PULSAR_TOPIC + suffix) + .configs(configs).build(); + return new PulsarSource(sourceConfig, stopPolicy != null ? stopPolicy : FunctionStopPolicy.DELETE, null); + } + + @Configuration(proxyBeanMethods = false) + @Import({ PulsarAutoConfiguration.class, RabbitAutoConfiguration.class }) + static class PulsarFunctionTestConfiguration { + + @Bean + PulsarSource rabbitSource() { + return PulsarFunctionAdministrationIntegrationTests.rabbitPulsarSource(null); + } + + @PulsarListener(topics = PULSAR_TOPIC, subscriptionName = "pft-foo-sub") + public void listen(String msg) { + PulsarFunctionAdministrationIntegrationTests.messageReceived(msg); + } + + } + + @Configuration(proxyBeanMethods = false) + @Import(PulsarAutoConfiguration.class) + static class PulsarFunctionStopPolicyTestConfiguration { + + @Bean + PulsarSource rabbitSourceWithStopPolicyNone() { + return PulsarFunctionAdministrationIntegrationTests.rabbitPulsarSource(FunctionStopPolicy.NONE); + } + + @Bean + PulsarSource rabbitSourceWithStopPolicyStop() { + return PulsarFunctionAdministrationIntegrationTests.rabbitPulsarSource(FunctionStopPolicy.STOP); + } + + @Bean + PulsarSource rabbitSourceWithStopPolicyDelete() { + return PulsarFunctionAdministrationIntegrationTests.rabbitPulsarSource(FunctionStopPolicy.DELETE); + } + + } + + static class ContainerLoggingTestWatcher implements TestWatcher { + + private final LogAccessor logger = new LogAccessor(this.getClass()); + + @Override + public void testFailed(ExtensionContext context, Throwable cause) { + this.logger.error(() -> "Test %s failed due to: %s - inspect container logs below:%n%n%s" + .formatted(context.getDisplayName(), cause.getMessage(), getPulsarContainerLogs())); + } + + private String getPulsarContainerLogs() { + try { + return PULSAR_CONTAINER.getLogs(); + } + catch (Exception ex) { + String msg = "<---- Failed to retrieve container logs: %s ---->".formatted(ex.getMessage()); + this.logger.error(ex, msg); + return msg; + } + } + + } + +} diff --git a/spring-pulsar-spring-boot-autoconfigure/src/integration-test/resources/application.yml b/spring-pulsar-spring-boot-autoconfigure/src/integration-test/resources/application.yml new file mode 100644 index 00000000..15b7001c --- /dev/null +++ b/spring-pulsar-spring-boot-autoconfigure/src/integration-test/resources/application.yml @@ -0,0 +1,3 @@ +spring: + main: + banner-mode: log diff --git a/spring-pulsar-spring-boot-autoconfigure/src/integration-test/resources/logback-test.xml b/spring-pulsar-spring-boot-autoconfigure/src/integration-test/resources/logback-test.xml new file mode 100644 index 00000000..0fc8852d --- /dev/null +++ b/spring-pulsar-spring-boot-autoconfigure/src/integration-test/resources/logback-test.xml @@ -0,0 +1,13 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n + + + + + + + + +