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 + + + + + + + + +