[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.
This commit is contained in:
8
.github/workflows/ci-pr.yml
vendored
8
.github/workflows/ci-pr.yml
vendored
@@ -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()
|
||||
|
||||
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
* <ul>
|
||||
* <li>Adds 'integrationTestCompile' and 'integrationTestRuntime' configurations</li>
|
||||
* <li>Adds 'src/integration-test/java' source test folder</li>
|
||||
* <li>Adds 'integrationTest' task to run integration tests</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @author Chris Bono
|
||||
*/
|
||||
class IntegrationTestPlugin implements Plugin<Project> {
|
||||
|
||||
@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 ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<File>) () -> new File(this.sourceSet.getOutput().getResourcesDir(),
|
||||
"META-INF/spring.factories"))
|
||||
.withPathSensitivity(PathSensitivity.RELATIVE).withPropertyName("spring.factories");
|
||||
getInputs()
|
||||
.file((Callable<File>) () -> 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<String>) () -> 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<String> classNames = new LinkedHashSet<>();
|
||||
classNames.addAll(readSpringFactories());
|
||||
classNames.addAll(readAutoConfigurationsFile());
|
||||
Set<String> 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<String> 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<String> readAutoConfigurationsFile() throws IOException {
|
||||
File file = new File(this.sourceSet.getOutput().getResourcesDir(),
|
||||
"META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports");
|
||||
if (!file.exists()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
// Nearly identical copy of
|
||||
// org.springframework.boot.context.annotation.ImportCandidates.load
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file)))) {
|
||||
List<String> 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Applying the {@link ConfigurationPropertiesPlugin}.
|
||||
* <li>Adding a dependency on the auto-configuration annotation processor.
|
||||
* <li>Defining a task that produces metadata describing the auto-configuration. The
|
||||
* metadata is made available as an artifact in the
|
||||
* </ul>
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public class AutoConfigurationPlugin implements Plugin<Project> {
|
||||
|
||||
/**
|
||||
* 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<File>) task::getOutputFile), (artifact) -> artifact.builtBy(task));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<AutoConfigurationClass> classes;
|
||||
|
||||
private AutoConfiguration(String module, Set<String> 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<AutoConfigurationClass> {
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<String, Object> json = objectMapper.readValue(file, Map.class);
|
||||
check("groups", json, analysis);
|
||||
check("properties", json, analysis);
|
||||
check("hints", json, analysis);
|
||||
}
|
||||
return report;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void check(String key, Map<String, Object> json, Analysis analysis) {
|
||||
List<Map<String, Object>> groups = (List<Map<String, Object>>) json.get(key);
|
||||
List<String> names = groups.stream().map((group) -> (String) group.get("name")).collect(Collectors.toList());
|
||||
List<String> sortedNames = sortedCopy(names);
|
||||
for (int i = 0; i < names.size(); i++) {
|
||||
String actual = names.get(i);
|
||||
String expected = sortedNames.get(i);
|
||||
if (!actual.equals(expected)) {
|
||||
analysis.problems.add("Wrong order at $." + key + "[" + i + "].name - expected '" + expected
|
||||
+ "' but found '" + actual + "'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> sortedCopy(Collection<String> original) {
|
||||
List<String> copy = new ArrayList<>(original);
|
||||
Collections.sort(copy);
|
||||
return copy;
|
||||
}
|
||||
|
||||
private static final class Report implements Iterable<String> {
|
||||
|
||||
private final List<Analysis> analyses = new ArrayList<>();
|
||||
|
||||
private Analysis analysis(Path path) {
|
||||
Analysis analysis = new Analysis(path);
|
||||
this.analyses.add(analysis);
|
||||
return analysis;
|
||||
}
|
||||
|
||||
private boolean hasProblems() {
|
||||
for (Analysis analysis : this.analyses) {
|
||||
if (!analysis.problems.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<String> iterator() {
|
||||
List<String> lines = new ArrayList<>();
|
||||
for (Analysis analysis : this.analyses) {
|
||||
lines.add(analysis.source.toString());
|
||||
lines.add("");
|
||||
if (analysis.problems.isEmpty()) {
|
||||
lines.add("No problems found.");
|
||||
}
|
||||
else {
|
||||
lines.addAll(analysis.problems);
|
||||
}
|
||||
lines.add("");
|
||||
}
|
||||
return lines.iterator();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class Analysis {
|
||||
|
||||
private final List<String> problems = new ArrayList<>();
|
||||
|
||||
private final Path source;
|
||||
|
||||
private Analysis(Path source) {
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,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<String> propertyNames;
|
||||
|
||||
private final String description;
|
||||
|
||||
CompoundRow(Snippet snippet, String prefix, String description) {
|
||||
super(snippet, prefix);
|
||||
this.description = description;
|
||||
this.propertyNames = new TreeSet<>();
|
||||
}
|
||||
|
||||
void addProperty(ConfigurationProperty property) {
|
||||
this.propertyNames.add(property.getDisplayName());
|
||||
}
|
||||
|
||||
@Override
|
||||
void write(Asciidoc asciidoc) {
|
||||
asciidoc.append("|");
|
||||
asciidoc.append("[[" + getAnchor() + "]]");
|
||||
asciidoc.append("<<" + getAnchor() + ",");
|
||||
this.propertyNames.forEach(asciidoc::appendWithHardLineBreaks);
|
||||
asciidoc.appendln(">>");
|
||||
asciidoc.appendln("|+++", this.description, "+++");
|
||||
asciidoc.appendln("|");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<String, ConfigurationProperty> byName;
|
||||
|
||||
private ConfigurationProperties(List<ConfigurationProperty> properties) {
|
||||
Map<String, ConfigurationProperty> byName = new LinkedHashMap<>();
|
||||
for (ConfigurationProperty property : properties) {
|
||||
byName.put(property.getName(), property);
|
||||
}
|
||||
this.byName = Collections.unmodifiableMap(byName);
|
||||
}
|
||||
|
||||
ConfigurationProperty get(String propertyName) {
|
||||
return this.byName.get(propertyName);
|
||||
}
|
||||
|
||||
Stream<ConfigurationProperty> stream() {
|
||||
return this.byName.values().stream();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
static ConfigurationProperties fromFiles(Iterable<File> files) {
|
||||
try {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
List<ConfigurationProperty> properties = new ArrayList<>();
|
||||
for (File file : files) {
|
||||
Map<String, Object> json = objectMapper.readValue(file, Map.class);
|
||||
for (Map<String, Object> property : (List<Map<String, Object>>) json.get("properties")) {
|
||||
properties.add(ConfigurationProperty.fromJsonProperties(property));
|
||||
}
|
||||
}
|
||||
return new ConfigurationProperties(properties);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new RuntimeException("Failed to load configuration metadata", ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Adding a dependency on the configuration properties annotation processor.
|
||||
* <li>Configuring the additional metadata locations annotation processor compiler
|
||||
* argument.
|
||||
* <li>Adding the outputs of the processResources task as inputs of the compileJava task
|
||||
* to ensure that the additional metadata is available when the annotation processor runs.
|
||||
* <li>Registering a {@link CheckAdditionalSpringConfigurationMetadata} task and
|
||||
* configuring the {@code check} task to depend upon it.
|
||||
* <li>Defining an artifact for the resulting configuration property metadata so that it
|
||||
* can be consumed by downstream projects.
|
||||
* </ul>
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public class ConfigurationPropertiesPlugin implements Plugin<Project> {
|
||||
|
||||
/**
|
||||
* 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<CheckAdditionalSpringConfigurationMetadata> checkConfigurationMetadata = project.getTasks()
|
||||
.register(CHECK_ADDITIONAL_SPRING_CONFIGURATION_METADATA_TASK_NAME,
|
||||
CheckAdditionalSpringConfigurationMetadata.class);
|
||||
checkConfigurationMetadata.configure((check) -> {
|
||||
SourceSet mainSourceSet = project.getExtensions().getByType(JavaPluginExtension.class).getSourceSets()
|
||||
.getByName(SourceSet.MAIN_SOURCE_SET_NAME);
|
||||
check.setSource(mainSourceSet.getResources());
|
||||
check.include("META-INF/additional-spring-configuration-metadata.json");
|
||||
check.getReportLocation().set(project.getLayout().getBuildDirectory()
|
||||
.file("reports/additional-spring-configuration-metadata/check.txt"));
|
||||
});
|
||||
project.getTasks().named(LifecycleBasePlugin.CHECK_TASK_NAME)
|
||||
.configure((check) -> check.dependsOn(checkConfigurationMetadata));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<String, Object> property) {
|
||||
String name = (String) property.get("name");
|
||||
String type = (String) property.get("type");
|
||||
Object defaultValue = property.get("defaultValue");
|
||||
String description = (String) property.get("description");
|
||||
boolean deprecated = property.containsKey("deprecated");
|
||||
return new ConfigurationProperty(name, type, defaultValue, description, deprecated);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<Row> {
|
||||
|
||||
private final Snippet snippet;
|
||||
|
||||
private final String id;
|
||||
|
||||
protected Row(Snippet snippet, String id) {
|
||||
this.snippet = snippet;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
Row other = (Row) obj;
|
||||
return this.id.equals(other.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return this.id.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(Row other) {
|
||||
return this.id.compareTo(other.id);
|
||||
}
|
||||
|
||||
String getAnchor() {
|
||||
return this.snippet.getAnchor() + "." + this.id;
|
||||
}
|
||||
|
||||
abstract void write(Asciidoc asciidoc);
|
||||
|
||||
}
|
||||
@@ -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, "+`");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<String> prefixes;
|
||||
|
||||
private final Map<String, String> overrides;
|
||||
|
||||
Snippet(String anchor, String title, Consumer<Config> config) {
|
||||
Set<String> prefixes = new LinkedHashSet<>();
|
||||
Map<String, String> overrides = new LinkedHashMap<>();
|
||||
if (config != null) {
|
||||
config.accept(new Config() {
|
||||
|
||||
@Override
|
||||
public void accept(String prefix) {
|
||||
prefixes.add(prefix);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(String prefix, String description) {
|
||||
overrides.put(prefix, description);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
this.anchor = anchor;
|
||||
this.title = title;
|
||||
this.prefixes = prefixes;
|
||||
this.overrides = overrides;
|
||||
}
|
||||
|
||||
String getAnchor() {
|
||||
return this.anchor;
|
||||
}
|
||||
|
||||
String getTitle() {
|
||||
return this.title;
|
||||
}
|
||||
|
||||
void forEachPrefix(Consumer<String> action) {
|
||||
this.prefixes.forEach(action);
|
||||
}
|
||||
|
||||
void forEachOverride(BiConsumer<String, String> action) {
|
||||
this.overrides.forEach(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to configure the snippet.
|
||||
*/
|
||||
interface Config {
|
||||
|
||||
/**
|
||||
* Accept the given prefix using the meta-data description.
|
||||
* @param prefix the prefix to accept
|
||||
*/
|
||||
void accept(String prefix);
|
||||
|
||||
/**
|
||||
* Accept the given prefix with a defined description.
|
||||
* @param prefix the prefix to accept
|
||||
* @param description the description to use
|
||||
*/
|
||||
void accept(String prefix, String description);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,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<Snippet> snippets = new ArrayList<>();
|
||||
|
||||
Snippets(FileCollection configurationPropertyMetadata) {
|
||||
this.properties = ConfigurationProperties.fromFiles(configurationPropertyMetadata);
|
||||
}
|
||||
|
||||
void add(String anchor, String title, Consumer<Snippet.Config> config) {
|
||||
this.snippets.add(new Snippet(anchor, title, config));
|
||||
}
|
||||
|
||||
void writeTo(Path outputDirectory) throws IOException {
|
||||
createDirectory(outputDirectory);
|
||||
Set<String> remaining = this.properties.stream().filter((property) -> !property.isDeprecated())
|
||||
.map(ConfigurationProperty::getName).collect(Collectors.toSet());
|
||||
for (Snippet snippet : this.snippets) {
|
||||
Set<String> written = writeSnippet(outputDirectory, snippet, remaining);
|
||||
remaining.removeAll(written);
|
||||
}
|
||||
if (!remaining.isEmpty()) {
|
||||
throw new IllegalStateException(
|
||||
"The following keys were not written to the documentation: " + String.join(", ", remaining));
|
||||
}
|
||||
}
|
||||
|
||||
private Set<String> writeSnippet(Path outputDirectory, Snippet snippet, Set<String> remaining) throws IOException {
|
||||
Table table = new Table();
|
||||
Set<String> added = new HashSet<>();
|
||||
snippet.forEachOverride((prefix, description) -> {
|
||||
CompoundRow row = new CompoundRow(snippet, prefix, description);
|
||||
remaining.stream().filter((candidate) -> candidate.startsWith(prefix)).forEach((name) -> {
|
||||
if (added.add(name)) {
|
||||
row.addProperty(this.properties.get(name));
|
||||
}
|
||||
});
|
||||
table.addRow(row);
|
||||
});
|
||||
snippet.forEachPrefix((prefix) -> {
|
||||
remaining.stream().filter((candidate) -> candidate.startsWith(prefix)).forEach((name) -> {
|
||||
if (added.add(name)) {
|
||||
table.addRow(new SingleRow(snippet, this.properties.get(name)));
|
||||
}
|
||||
});
|
||||
});
|
||||
Asciidoc asciidoc = getAsciidoc(snippet, table);
|
||||
writeAsciidoc(outputDirectory, snippet, asciidoc);
|
||||
return added;
|
||||
}
|
||||
|
||||
private Asciidoc getAsciidoc(Snippet snippet, Table table) {
|
||||
Asciidoc asciidoc = new Asciidoc();
|
||||
// We have to prepend 'appendix.' as a section id here, otherwise the
|
||||
// spring-asciidoctor-extensions:section-id asciidoctor extension complains
|
||||
asciidoc.appendln("[[appendix." + snippet.getAnchor() + "]]");
|
||||
asciidoc.appendln("== ", snippet.getTitle());
|
||||
table.write(asciidoc);
|
||||
return asciidoc;
|
||||
}
|
||||
|
||||
private void writeAsciidoc(Path outputDirectory, Snippet snippet, Asciidoc asciidoc) throws IOException {
|
||||
String[] parts = (snippet.getAnchor()).split("\\.");
|
||||
Path path = outputDirectory;
|
||||
for (int i = 0; i < parts.length; i++) {
|
||||
String name = (i < parts.length - 1) ? parts[i] : parts[i] + ".adoc";
|
||||
path = path.resolve(name);
|
||||
}
|
||||
createDirectory(path.getParent());
|
||||
Files.deleteIfExists(path);
|
||||
try (OutputStream outputStream = Files.newOutputStream(path)) {
|
||||
outputStream.write(asciidoc.toString().getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
|
||||
private void createDirectory(Path path) throws IOException {
|
||||
assertValidOutputDirectory(path);
|
||||
if (!Files.exists(path)) {
|
||||
Files.createDirectory(path);
|
||||
}
|
||||
}
|
||||
|
||||
private void assertValidOutputDirectory(Path path) {
|
||||
if (path == null) {
|
||||
throw new IllegalArgumentException("Directory path should not be null");
|
||||
}
|
||||
if (Files.exists(path) && !Files.isDirectory(path)) {
|
||||
throw new IllegalArgumentException("Path already exists and is not a directory");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<Row> 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("|===");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<Project> {
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<Project> {
|
||||
|
||||
private static final Spec<Task> 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"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -18,16 +18,14 @@ public class JacocoConventionsPlugin implements Plugin<Project> {
|
||||
@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));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Project> {
|
||||
pluginManager.apply(AsciidoctorConventionsPlugin.class);
|
||||
pluginManager.apply(SpringPublishPlugin.class);
|
||||
pluginManager.apply(OptionalDependenciesPlugin.class);
|
||||
pluginManager.apply(IntegrationTestPlugin.class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -33,6 +33,10 @@ test {
|
||||
testLogging.showStandardStreams = true
|
||||
}
|
||||
|
||||
integrationTest {
|
||||
maxHeapSize '2048m'
|
||||
}
|
||||
|
||||
task downloadRabbitConnector {
|
||||
onlyIf {
|
||||
System.getProperty("downloadRabbitConnector") == "true"
|
||||
|
||||
@@ -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}.
|
||||
* <p>
|
||||
* 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<String> 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<String> 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<String, Object> 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
spring:
|
||||
main:
|
||||
banner-mode: log
|
||||
@@ -0,0 +1,13 @@
|
||||
<configuration>
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<root level="WARN">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
</root>
|
||||
<logger name="org.testcontainers" level="ERROR"/>
|
||||
<logger name="com.github.dockerjava" level="ERROR"/>
|
||||
<logger name="org.springframework.pulsar.function" level="INFO"/>
|
||||
</configuration>
|
||||
Reference in New Issue
Block a user