Removes all non-core components from main branch (#390)

* Prunes all non-core components

Remove modules:
- spring-pulsar-spring-boot-starter
- spring-pulsar-reactive-spring-boot-starter
- spring-pulsar-spring-boot-autoconfigure
- spring-pulsar-spring-cloud-stream-binder

- Remove config props plugins
- Update docs to point to Spring Boot config props
- Move intTest into spring-pulsar and spring-pulsar-reactive

* Replace refs from `spring-projects-experimental` to `spring-projects`

* Update spring-pulsar-spring-boot-starter to 0.2.1-SNAPSHOT
This commit is contained in:
Chris Bono
2023-04-11 09:25:42 -05:00
committed by GitHub
parent 9e00c2be7c
commit a4caa2fa2b
101 changed files with 182 additions and 10836 deletions

View File

@@ -85,21 +85,9 @@ Provides reference docs and handles aggregating javadocs.
=== spring-pulsar-reactive
Provides the API to access Apache Pulsar using a Reactive client.
=== spring-pulsar-reactive-spring-boot-starter
Provides a dependency descriptor that can be included in your application to easily start using Spring Pulsar in Reactive and imperative fashions.
=== spring-pulsar-sample-apps
Provides sample applications to illustrate Spring Pulsar functionality as well as provide ability for quick manual verification during development.
=== spring-pulsar-spring-boot-autoconfigure
Provides Spring Boot auto-configuration for Spring Pulsar.
=== spring-pulsar-spring-boot-starter
Provides a dependency descriptor that can be included in your application to easily start using Spring Pulsar in an imperative fashion.
=== spring-pulsar-spring-cloud-stream-binder
Provides a Spring Cloud Stream Binder implementation for Apache Pulsar.
== License
Spring Pulsar is Open Source software released under the https://www.apache.org/licenses/LICENSE-2.0.html[Apache 2.0 license].

View File

@@ -1,5 +1,6 @@
plugins {
id "java-gradle-plugin"
id 'groovy-gradle-plugin'
id "java"
id "groovy"
}
@@ -69,10 +70,6 @@ tasks.named('test', Test).configure {
gradlePlugin {
plugins {
configurationPropertiesPlugin {
id = "org.springframework.pulsar.configuration-properties"
implementationClass = 'org.springframework.pulsar.gradle.docs.configprops.ConfigurationPropertiesPlugin'
}
jacocoConventionsPlugin {
id = "org.springframework.pulsar.jacoco"
implementationClass = "org.springframework.pulsar.gradle.JacocoConventionsPlugin"
@@ -106,10 +103,6 @@ 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"

View File

@@ -1,116 +0,0 @@
/*
* 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
shouldRunAfter 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 ]
}
}
}
}

View File

@@ -0,0 +1,50 @@
/*
* 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.
*/
configurations {
intTestImplementation {
extendsFrom testImplementation
}
intTestRuntime {
extendsFrom intTestImplementation, testRuntimeClasspath, testRuntimeOnly
}
intTestCompileClasspath {
extendsFrom intTestImplementation
canBeResolved = true
}
intTestRuntimeClasspath {
extendsFrom intTestRuntime
canBeResolved = true
}
}
sourceSets {
intTest {
compileClasspath += sourceSets.main.output + project.configurations.intTestCompileClasspath
runtimeClasspath += sourceSets.main.output + project.configurations.intTestRuntimeClasspath
}
}
tasks.register('integrationTest', Test) {
description = 'Runs integration tests.'
group = 'verification'
testClassesDirs = sourceSets.intTest.output.classesDirs
classpath = sourceSets.intTest.runtimeClasspath
shouldRunAfter test
}
check.dependsOn integrationTest

View File

@@ -16,7 +16,6 @@
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;
@@ -45,6 +44,5 @@ public class SpringDocsModulePlugin implements Plugin<Project> {
pluginManager.apply(AsciidoctorConventionsPlugin.class);
pluginManager.apply(SpringPublishPlugin.class);
pluginManager.apply(OptionalDependenciesPlugin.class);
pluginManager.apply(IntegrationTestPlugin.class);
}
}

View File

@@ -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.pulsar.gradle.docs.configprops;
/**
* 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();
}
}

View File

@@ -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.pulsar.gradle.docs.configprops;
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;
}
}
}

View File

@@ -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.pulsar.gradle.docs.configprops;
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("|");
}
}

View File

@@ -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.pulsar.gradle.docs.configprops;
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);
}
}
}

View File

@@ -1,126 +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.pulsar.gradle.docs.configprops;
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
* @author Chris Bono
*/
public class ConfigurationPropertiesPlugin implements Plugin<Project> {
// TODO extend the one in boot and delete most of this
/**
* 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().create(
"org.springframework.boot: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));
}
}

View File

@@ -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.pulsar.gradle.docs.configprops;
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);
}
}

View File

@@ -1,93 +0,0 @@
/*
* Copyright 2012-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.gradle.docs.configprops;
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;
/**
* {@link Task} used to document auto-configuration classes.
*
* @author Andy Wilkinson
* @author Phillip Webb
* @author Chris Bono
* @author Alexander Preuß
* @author Soby Chacko
*/
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.pulsar-client", "Pulsar Client Properties", (c) -> c.accept("spring.pulsar.client"));
snippets.add("application-properties.pulsar-producer", "Pulsar Producer Properties", (c) -> {
c.accept("spring.pulsar.producer");
c.accept("spring.pulsar.template");
});
snippets.add("application-properties.pulsar-consumer", "Pulsar Consumer Properties", (c) -> {
c.accept("spring.pulsar.consumer");
c.accept("spring.pulsar.listener");
});
snippets.add("application-properties.pulsar-reader", "Pulsar Reader Properties", (c) -> {
c.accept("spring.pulsar.reader");
});
snippets.add("application-properties.pulsar-defaults", "Pulsar Defaults Properties", (c) -> c.accept("spring.pulsar.defaults"));
snippets.add("application-properties.pulsar-function", "Pulsar Function Properties", (c) -> c.accept("spring.pulsar.function"));
snippets.add("application-properties.pulsar-administration", "Pulsar Administration Properties", (c) -> c.accept("spring.pulsar.administration"));
snippets.add("application-properties.pulsar-reactive-sender", "Pulsar Reactive Sender Properties", (c) -> c.accept("spring.pulsar.reactive.sender"));
snippets.add("application-properties.pulsar-reactive-consumer", "Pulsar Reactive Consumer Properties", (c) -> {
c.accept("spring.pulsar.reactive.consumer");
c.accept("spring.pulsar.reactive.listener");
});
snippets.add("application-properties.pulsar-reactive-reader", "Pulsar Reactive Reader Properties", (c) -> c.accept("spring.pulsar.reactive.reader"));
snippets.add("application-properties.pulsar-binder", "Pulsar Binder Properties", (c) -> c.accept("spring.cloud.stream.pulsar.binder"));
snippets.add("application-properties.pulsar-bindings", "Pulsar Binding Properties", (c) -> c.accept("spring.cloud.stream.pulsar.bindings"));
snippets.writeTo(this.outputDir.toPath());
}
}

View File

@@ -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.pulsar.gradle.docs.configprops;
/**
* 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);
}

View File

@@ -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.pulsar.gradle.docs.configprops;
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("<", "&lt;").replace(">", "&gt;");
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, "+`");
}
}
}

View File

@@ -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.pulsar.gradle.docs.configprops;
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);
}
}

View File

@@ -1,181 +0,0 @@
/*
* Copyright 2012-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.gradle.docs.configprops;
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
* @author Chris Bono
*/
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((p) -> p.startsWith(prefix) &&
!DocsOnlyConfigurationProperty.isDocsOnlyProp(p)).forEach((name) -> {
if (added.add(name)) {
table.addRow(new SingleRow(snippet, this.properties.get(name)));
// Handle any "docs only" props
if (name.equals("spring.cloud.stream.pulsar.bindings")) {
// TODO if sourceType is Map<K,V> then look for docs only props
this.properties.stream()
.filter((p) -> DocsOnlyConfigurationProperty.isDocsOnlyPropUnderPrefix(p, name))
.filter((p) -> added.add(p.getName()))
.map(DocsOnlyConfigurationProperty::fromConfigProp)
.forEach((p) -> table.addRow(new SingleRow(snippet, p)));
}
}
}));
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");
}
}
static class DocsOnlyConfigurationProperty extends ConfigurationProperty {
private static final String DOCS_ONLY_TOKEN = ".for-docs-only";
private final String displayName;
private DocsOnlyConfigurationProperty(String name, String displayName, String type,
Object defaultValue, String description, boolean deprecated) {
super(name, type, defaultValue, description, deprecated);
this.displayName = displayName;
}
@Override
String getDisplayName() {
return this.displayName;
}
static DocsOnlyConfigurationProperty fromConfigProp(ConfigurationProperty prop) {
var propNewName = prop.getName().replace(DOCS_ONLY_TOKEN, ".z");
var propDisplayName = prop.getName().replace(DOCS_ONLY_TOKEN, ".*");
return new DocsOnlyConfigurationProperty(
propNewName,
propDisplayName,
prop.getType(),
prop.getDefaultValue(),
prop.getDescription(),
prop.isDeprecated());
}
static boolean isDocsOnlyProp(String propName) {
return propName.contains(DOCS_ONLY_TOKEN);
}
static boolean isDocsOnlyPropUnderPrefix(ConfigurationProperty configProp, String prefix) {
return configProp.getName().startsWith(prefix + DOCS_ONLY_TOKEN);
}
}
}

View File

@@ -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.pulsar.gradle.docs.configprops;
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("|===");
}
}

View File

@@ -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.pulsar.gradle.docs.configprops;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link CompoundRow}.
*
* @author Brian Clozel
*/
class CompoundRowTests {
private static final String NEWLINE = System.lineSeparator();
private static final Snippet SNIPPET = new Snippet("my", "title", null);
@Test
void simpleProperty() {
CompoundRow row = new CompoundRow(SNIPPET, "spring.test", "This is a description.");
row.addProperty(new ConfigurationProperty("spring.test.first", "java.lang.String"));
row.addProperty(new ConfigurationProperty("spring.test.second", "java.lang.String"));
row.addProperty(new ConfigurationProperty("spring.test.third", "java.lang.String"));
Asciidoc asciidoc = new Asciidoc();
row.write(asciidoc);
assertThat(asciidoc.toString()).isEqualTo("|[[my.spring.test]]<<my.spring.test,`+spring.test.first+` +"
+ NEWLINE + "`+spring.test.second+` +" + NEWLINE + "`+spring.test.third+` +" + NEWLINE + ">>" + NEWLINE
+ "|+++This is a description.+++" + NEWLINE + "|" + NEWLINE);
}
}

View File

@@ -1,40 +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.pulsar.gradle.docs.configprops;
import java.io.File;
import java.util.Arrays;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ConfigurationProperties}
*
* @author Andy Wilkinson
*/
class ConfigurationPropertiesTests {
@Test
void whenJsonHasAnIntegerDefaultValueThenItRemainsAnIntegerWhenRead() {
ConfigurationProperties properties = ConfigurationProperties
.fromFiles(Arrays.asList(new File("src/test/resources/spring-configuration-metadata.json")));
assertThat(properties.get("example.counter").getDefaultValue()).isEqualTo(0);
}
}

View File

@@ -1,114 +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.pulsar.gradle.docs.configprops;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link SingleRow}.
*
* @author Brian Clozel
*/
class SingleRowTests {
private static final String NEWLINE = System.lineSeparator();
private static final Snippet SNIPPET = new Snippet("my", "title", null);
@Test
void simpleProperty() {
ConfigurationProperty property = new ConfigurationProperty("spring.test.prop", "java.lang.String", "something",
"This is a description.", false);
SingleRow row = new SingleRow(SNIPPET, property);
Asciidoc asciidoc = new Asciidoc();
row.write(asciidoc);
assertThat(asciidoc.toString()).isEqualTo("|[[my.spring.test.prop]]<<my.spring.test.prop,`+spring.test.prop+`>>"
+ NEWLINE + "|+++This is a description.+++" + NEWLINE + "|`+something+`" + NEWLINE);
}
@Test
void noDefaultValue() {
ConfigurationProperty property = new ConfigurationProperty("spring.test.prop", "java.lang.String", null,
"This is a description.", false);
SingleRow row = new SingleRow(SNIPPET, property);
Asciidoc asciidoc = new Asciidoc();
row.write(asciidoc);
assertThat(asciidoc.toString()).isEqualTo("|[[my.spring.test.prop]]<<my.spring.test.prop,`+spring.test.prop+`>>"
+ NEWLINE + "|+++This is a description.+++" + NEWLINE + "|" + NEWLINE);
}
@Test
void defaultValueWithPipes() {
ConfigurationProperty property = new ConfigurationProperty("spring.test.prop", "java.lang.String",
"first|second", "This is a description.", false);
SingleRow row = new SingleRow(SNIPPET, property);
Asciidoc asciidoc = new Asciidoc();
row.write(asciidoc);
assertThat(asciidoc.toString()).isEqualTo("|[[my.spring.test.prop]]<<my.spring.test.prop,`+spring.test.prop+`>>"
+ NEWLINE + "|+++This is a description.+++" + NEWLINE + "|`+first\\|second+`" + NEWLINE);
}
@Test
void defaultValueWithBackslash() {
ConfigurationProperty property = new ConfigurationProperty("spring.test.prop", "java.lang.String",
"first\\second", "This is a description.", false);
SingleRow row = new SingleRow(SNIPPET, property);
Asciidoc asciidoc = new Asciidoc();
row.write(asciidoc);
assertThat(asciidoc.toString()).isEqualTo("|[[my.spring.test.prop]]<<my.spring.test.prop,`+spring.test.prop+`>>"
+ NEWLINE + "|+++This is a description.+++" + NEWLINE + "|`+first\\\\second+`" + NEWLINE);
}
@Test
void descriptionWithPipe() {
ConfigurationProperty property = new ConfigurationProperty("spring.test.prop", "java.lang.String", null,
"This is a description with a | pipe.", false);
SingleRow row = new SingleRow(SNIPPET, property);
Asciidoc asciidoc = new Asciidoc();
row.write(asciidoc);
assertThat(asciidoc.toString()).isEqualTo("|[[my.spring.test.prop]]<<my.spring.test.prop,`+spring.test.prop+`>>"
+ NEWLINE + "|+++This is a description with a \\| pipe.+++" + NEWLINE + "|" + NEWLINE);
}
@Test
void mapProperty() {
ConfigurationProperty property = new ConfigurationProperty("spring.test.prop",
"java.util.Map<java.lang.String,java.lang.String>", null, "This is a description.", false);
SingleRow row = new SingleRow(SNIPPET, property);
Asciidoc asciidoc = new Asciidoc();
row.write(asciidoc);
assertThat(asciidoc.toString())
.isEqualTo("|[[my.spring.test.prop]]<<my.spring.test.prop,`+spring.test.prop.*+`>>" + NEWLINE
+ "|+++This is a description.+++" + NEWLINE + "|" + NEWLINE);
}
@Test
void listProperty() {
String[] defaultValue = new String[] { "first", "second", "third" };
ConfigurationProperty property = new ConfigurationProperty("spring.test.prop",
"java.util.List<java.lang.String>", defaultValue, "This is a description.", false);
SingleRow row = new SingleRow(SNIPPET, property);
Asciidoc asciidoc = new Asciidoc();
row.write(asciidoc);
assertThat(asciidoc.toString()).isEqualTo("|[[my.spring.test.prop]]<<my.spring.test.prop,`+spring.test.prop+`>>"
+ NEWLINE + "|+++This is a description.+++" + NEWLINE + "|`+first," + NEWLINE + "second," + NEWLINE
+ "third+`" + NEWLINE);
}
}

View File

@@ -1,58 +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.pulsar.gradle.docs.configprops;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link Table}.
*
* @author Brian Clozel
*/
class TableTests {
private static final String NEWLINE = System.lineSeparator();
private static final Snippet SNIPPET = new Snippet("my", "title", null);
@Test
void simpleTable() {
Table table = new Table();
table.addRow(new SingleRow(SNIPPET, new ConfigurationProperty("spring.test.prop", "java.lang.String",
"something", "This is a description.", false)));
table.addRow(new SingleRow(SNIPPET, new ConfigurationProperty("spring.test.other", "java.lang.String",
"other value", "This is another description.", false)));
Asciidoc asciidoc = new Asciidoc();
table.write(asciidoc);
// @formatter:off
assertThat(asciidoc.toString()).isEqualTo(
"[cols=\"4,3,3\", options=\"header\"]" + NEWLINE +
"|===" + NEWLINE +
"|Name|Description|Default Value" + NEWLINE + NEWLINE +
"|[[my.spring.test.other]]<<my.spring.test.other,`+spring.test.other+`>>" + NEWLINE +
"|+++This is another description.+++" + NEWLINE +
"|`+other value+`" + NEWLINE + NEWLINE +
"|[[my.spring.test.prop]]<<my.spring.test.prop,`+spring.test.prop+`>>" + NEWLINE +
"|+++This is a description.+++" + NEWLINE +
"|`+something+`" + NEWLINE + NEWLINE +
"|===" + NEWLINE);
// @formatter:on
}
}

View File

@@ -1,9 +0,0 @@
{
"properties": [
{
"name": "example.counter",
"type": "java.lang.Integer",
"defaultValue": 0
}
]
}

View File

@@ -28,9 +28,6 @@ include 'spring-pulsar-cache-provider'
include 'spring-pulsar-cache-provider-caffeine'
include 'spring-pulsar-reactive'
include 'spring-pulsar-dependencies'
include 'spring-pulsar-spring-boot-autoconfigure'
include 'spring-pulsar-spring-boot-starter'
include 'spring-pulsar-reactive-spring-boot-starter'
include 'spring-pulsar-sample-apps:sample-app1'
include 'spring-pulsar-sample-apps:sample-app2'
include 'spring-pulsar-sample-apps:sample-pulsar-functions:sample-signup-app'
@@ -39,5 +36,4 @@ include 'spring-pulsar-sample-apps:sample-reactive'
include 'spring-pulsar-sample-apps:sample-pulsar-binder'
include 'spring-pulsar-sample-apps:sample-pulsar-reader'
include 'spring-pulsar-docs'
include 'spring-pulsar-spring-cloud-stream-binder'
include 'spring-pulsar-test'

View File

@@ -13,6 +13,8 @@ ext {
pulsarVersion = '2.11.0'
pulsarClientReactiveVersion = '0.2.0'
springBootVersion = '3.0.5'
springPulsarStarterVersion = '0.2.1-SNAPSHOT'
springPulsarBinderVersion = '0.2.0'
springCloudStreamVersion = '4.0.1'
}
@@ -27,5 +29,8 @@ dependencies {
api "org.apache.pulsar:pulsar-client-reactive-producer-cache-caffeine:$pulsarClientReactiveVersion"
api "org.springframework.cloud:spring-cloud-stream:$springCloudStreamVersion"
api "org.springframework.cloud:spring-cloud-stream-test-support:$springCloudStreamVersion"
api "org.springframework.pulsar:spring-pulsar-spring-boot-starter:$springPulsarStarterVersion"
api "org.springframework.pulsar:spring-pulsar-reactive-spring-boot-starter:$springPulsarStarterVersion"
api "org.springframework.pulsar:spring-pulsar-spring-cloud-stream-binder:$springPulsarBinderVersion"
}
}

View File

@@ -10,15 +10,12 @@ ext {
}
configurations {
configurationProperties
observationDocs
}
dependencies {
api project (':spring-pulsar')
api 'org.springframework.boot:spring-boot-starter'
configurationProperties(project(path: ":spring-pulsar-spring-boot-autoconfigure", configuration: "configurationPropertiesMetadata"))
configurationProperties(project(path: ":spring-pulsar-spring-cloud-stream-binder", configuration: "configurationPropertiesMetadata"))
observationDocs "io.micrometer:micrometer-docs-generator:$micrometerDocsVersion"
}
@@ -44,12 +41,9 @@ task aggregatedJavadoc(type: Javadoc) {
title = "${rootProject.description} ${version} API"
project.rootProject.gradle.projectsEvaluated {
Set<String> excludedProjects = ['spring-pulsar-sample-apps:sample-app1',
'spring-pulsar-sample-apps:sample-app2',
'spring-pulsar-sample-apps:sample-reactive']
Set<Project> publishedProjects = rootProject.subprojects.findAll { it != project}
.findAll { it.plugins.hasPlugin(JavaPlugin) && it.plugins.hasPlugin(MavenPublishPlugin) }
.findAll { !excludedProjects.contains(it.name) }
.findAll { !it.name.startsWith('sample-') && !it.name.equals('spring-pulsar-test') }
dependsOn publishedProjects.javadoc
source publishedProjects.javadoc.source
classpath = project.files(publishedProjects.javadoc.classpath)
@@ -73,11 +67,6 @@ task aggregatedJavadoc(type: Javadoc) {
}
}
task documentConfigurationProperties(type: org.springframework.pulsar.gradle.docs.configprops.DocumentConfigurationProperties) {
configurationPropertyMetadata = configurations.configurationProperties
outputDir = file("${buildDir}/docs/generated/")
}
def observationsInputDir = file("${rootDir}/spring-pulsar/src/main/java/org/springframework/pulsar/observation").absolutePath
def observationsOutputDir = file("${buildDir}/docs/generated/observation/").absolutePath
@@ -97,9 +86,11 @@ tasks.withType(org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask) {
jvmArgs "--add-opens", "java.base/sun.nio.ch=ALL-UNNAMED", "--add-opens", "java.base/java.io=ALL-UNNAMED"
}
doFirst {
attributes "spring-pulsar-version": project.version,
"spring-boot-version": project(':spring-pulsar-dependencies').springBootVersion ?: 'current',
"spring-cloud-stream-version": project(':spring-pulsar-dependencies').springCloudStreamVersion ?: 'current'
attributes "spring-boot-version": project(':spring-pulsar-dependencies').springBootVersion ?: 'current',
"spring-cloud-stream-version": project(':spring-pulsar-dependencies').springCloudStreamVersion ?: 'current',
"spring-pulsar-version": project.version,
"spring-pulsar-binder-version": project(':spring-pulsar-dependencies').springPulsarBinderVersion ?: 'current',
"spring-pulsar-starter-version": project(':spring-pulsar-dependencies').springPulsarStarterVersion ?: 'current'
}
}
@@ -122,7 +113,7 @@ task asciidoctorMultipage(type: org.asciidoctor.gradle.jvm.AsciidoctorTask) {
}
syncDocumentationSourceForAsciidoctor {
dependsOn documentConfigurationProperties, generateObservabilityDocs
dependsOn generateObservabilityDocs
from("${buildDir}/docs/generated") {
into "asciidoc"
}
@@ -132,7 +123,7 @@ syncDocumentationSourceForAsciidoctor {
}
syncDocumentationSourceForAsciidoctorMultipage {
dependsOn documentConfigurationProperties, generateObservabilityDocs
dependsOn generateObservabilityDocs
from("${buildDir}/docs/generated") {
into "asciidoc"
}
@@ -142,7 +133,7 @@ syncDocumentationSourceForAsciidoctorMultipage {
}
syncDocumentationSourceForAsciidoctorPdf {
dependsOn documentConfigurationProperties, generateObservabilityDocs
dependsOn generateObservabilityDocs
from("${buildDir}/docs/generated") {
into "asciidoc"
}

View File

@@ -1,37 +0,0 @@
[appendix]
[[appendix.application-properties]]
= Application Properties
include::attributes.adoc[]
:sectnums!:
You can specify various properties inside your `application.properties` file, inside your `application.yml` file, or as command line switches.
This appendix provides a list of Spring Pulsar properties and references to the underlying classes that consume them.
TIP: Spring Boot provides various conversion mechanisms with advanced value formatting.
See {spring-boot-docs}/#features.external-config.typesafe-configuration-properties.conversion[the properties conversion section] for more detail.
include::application-properties/pulsar-client.adoc[]
include::application-properties/pulsar-producer.adoc[]
include::application-properties/pulsar-consumer.adoc[]
include::application-properties/pulsar-reader.adoc[]
include::application-properties/pulsar-defaults.adoc[]
include::application-properties/pulsar-function.adoc[]
include::application-properties/pulsar-administration.adoc[]
include::application-properties/pulsar-reactive-sender.adoc[]
include::application-properties/pulsar-reactive-consumer.adoc[]
include::application-properties/pulsar-reactive-reader.adoc[]
include::application-properties/pulsar-binder.adoc[]
include::application-properties/pulsar-bindings.adoc[]

View File

@@ -0,0 +1,16 @@
:spring-boot-version: current
:spring-cloud-stream-version: current
:spring-pulsar-version: current
:spring-pulsar-binder-version: current
:spring-pulsar-starter-version: current
:github: https://github.com/spring-projects/spring-pulsar
:javadocs: https://docs.spring.io/spring-pulsar/docs/{spring-pulsar-version}/api
:spring-boot-docs: https://docs.spring.io/spring-boot/docs/{spring-boot-version}/reference/htmlsingle
:spring-boot-pulsar-config-props: {spring-boot-docs}/#application-properties.integration
:spring-cloud-stream-docs: https://docs.spring.io/spring-cloud-stream/docs/{spring-cloud-stream-version}/reference/html/
:spring-cloud-function: https://spring.io/projects/spring-cloud-function
:apache-pulsar-docs: https://pulsar.apache.org/docs/2.11.x
:apache-pulsar-io-docs: {apache-pulsar-docs}/io-connectors
:apache-pulsar-function-docs: {apache-pulsar-docs}/functions-overview

View File

@@ -6,16 +6,4 @@
:sectnums:
:sectnumlevels: 3
:spring-pulsar-version: current
:spring-boot-version: current
:spring-cloud-stream-version: current
:github: https://github.com/spring-projects/spring-pulsar
:javadocs: https://docs.spring.io/spring-pulsar/docs/{spring-pulsar-version}/api
:spring-boot-docs: https://docs.spring.io/spring-boot/docs/{spring-boot-version}/reference/htmlsingle
:spring-cloud-stream-docs: https://docs.spring.io/spring-cloud-stream/docs/{spring-cloud-stream-version}/reference/html/
:spring-cloud-function: https://spring.io/projects/spring-cloud-function
:apache-pulsar-docs: https://pulsar.apache.org/docs/2.11.x
:apache-pulsar-io-docs: {apache-pulsar-docs}/io-connectors
:apache-pulsar-function-docs: {apache-pulsar-docs}/functions-overview
include::attributes-variables.adoc[]

View File

@@ -42,8 +42,6 @@ In addition to this reference documentation, we recommend a number of other reso
[[appendix]]
== Appendices
include::application-properties.adoc[leveloffset=+2]
include::non-ga-versions.adoc[leveloffset=+2]
include::native-image.adoc[leveloffset=+2]

View File

@@ -13,7 +13,7 @@ By default, the application tries to connect to a local Pulsar instance at `\htt
This can be adjusted by setting the `spring.pulsar.administration.service-url` property to a different value in the form `(http|https)://<host>:<port>`.
There are many application properties available to configure the client.
See the <<application-properties.adoc#appendix.application-properties.pulsar-administration,Appendix>> for application properties prefixed with `spring.pulsar.administration`.
See the {spring-boot-pulsar-config-props}[`spring.pulsar.administration.*`] application properties.
[[pulsar-admin-authentication]]
=== Authentication

View File

@@ -18,7 +18,7 @@ We need to include the following dependency on your application to use Apache Pu
<dependency>
<groupId>org.springframework.pulsar</groupId>
<artifactId>spring-pulsar-spring-cloud-stream-binder</artifactId>
<version>{spring-pulsar-version}</version>
<version>{spring-pulsar-binder-version}</version>
</dependency>
</dependencies>
----
@@ -27,7 +27,7 @@ We need to include the following dependency on your application to use Apache Pu
.Gradle
----
dependencies {
implementation 'org.springframework.pulsar:spring-pulsar-spring-cloud-stream-binder:{spring-pulsar-version}'
implementation 'org.springframework.pulsar:spring-pulsar-spring-cloud-stream-binder:{spring-pulsar-binder-version}'
}
----

View File

@@ -24,7 +24,8 @@ The framework provides the `PulsarFunctionAdministration` component to manage Pu
When you use the Pulsar Spring Boot starter, you get the `PulsarFunctionAdministration` auto-configured.
By default, the application tries to connect to a local Pulsar instance at `http://localhost:8080`.
However, because it leverages the already configured `PulsarAdministration`, see <<pulsar-admin.adoc#pulsar-admin-client,Pulsar Admin Client>> for available client options (including authentication). Other available application properties can be found in the <<application-properties.adoc#appendix.application-properties.pulsar-function,Appendix>> prefixed by `spring.pulsar.function`.
However, because it leverages the already configured `PulsarAdministration`, see <<pulsar-admin.adoc#pulsar-admin-client,Pulsar Admin Client>> for available client options (including authentication).
Additional configuration options are available with the {spring-boot-pulsar-config-props}[`spring.pulsar.function.*`] application properties.
== Automatic Function Management
On application startup, the framework finds all `PulsarFunction`, `PulsarSink`, and `PulsarSource` beans in the application context.

View File

@@ -1,8 +1,10 @@
include::attributes-variables.adoc[]
=== Pulsar Headers
Pulsar does not have a first-class "`header`" concept but instead provides a map for custom user properties as well as methods to access the message metadata typically stored in a message header (eg. `id` and `event-time`).
As such, the terms "`Pulsar message header`" and "`Pulsar message metadata`" are used interchangeably.
The list of available message metadata (headers) can be found in https://github.com/spring-projects/spring-pulsar/blob/main/spring-pulsar/src/main/java/org/springframework/pulsar/support/PulsarHeaders.java[PulsarHeaders.java].
The list of available message metadata (headers) can be found in {github}/blob/main/spring-pulsar/src/main/java/org/springframework/pulsar/support/PulsarHeaders.java[PulsarHeaders.java].
=== Spring Headers
Spring Messaging provides first-class "`header`" support via its `MessageHeaders` abstraction.

View File

@@ -2,6 +2,14 @@
= Using Spring for Apache Pulsar
include::attributes.adoc[]
== Preface
NOTE: We recommend using a Spring-Boot-First approach for Spring for Apache Pulsar-based applications, as that simplifies things tremendously.
To do so, you can add the `spring-pulsar-spring-boot-starter` module as a dependency.
NOTE: The majority of this reference expects the reader to be using the starter and gives most directions for configuration with that in mind.
However, an effort is made to call out when instructions are specific to the Spring Boot starter usage.
include::quick-tour.adoc[leveloffset=+1]
[[pulsar-client]]
@@ -14,8 +22,10 @@ This can be adjusted by setting the `spring.pulsar.client.service-url` property
TIP: The value must be a valid {apache-pulsar-docs}/client-libraries-java/#connection-urls[Pulsar Protocol] URL
There are many application properties available to configure the client.
See the <<application-properties.adoc#appendix.application-properties.pulsar-client,Appendix>> for more detail.
You can further configure the client by specifying any of the {spring-boot-pulsar-config-props}[`spring.pulsar.client.*`] application properties.
NOTE: If you are not using the starter, you will need to configure and register the `PulsarClientFactoryBean` yourself.
It has a constructor that accepts a map of Pulsar https://pulsar.apache.org/docs/2.11.x/client-libraries-java/#client[native properties].
[[client-authentication]]
=== Authentication
@@ -97,8 +107,8 @@ include::schema-info/schema-info-template.adoc[leveloffset=+1]
[[pulsar-producer-factory]]
=== Pulsar Producer Factory
The `PulsarTemplate` relies on a `PulsarProducerFactory` to actually create the underlying producer. Spring Boot auto-configuration also provides this producer factory. Additionally, you can configure the factory by specifying any of the available producer-centric application properties.
See the <<application-properties.adoc#appendix.application-properties.pulsar-producer,Appendix>>.
The `PulsarTemplate` relies on a `PulsarProducerFactory` to actually create the underlying producer.
Spring Boot auto-configuration also provides this producer factory which you can further configure by specifying any of the {spring-boot-pulsar-config-props}[`spring.pulsar.producer.*`] application properties.
NOTE: If topic information is not specified when using the producer factory APIs directly, the same <<topic-resolution-process-imperative,topic resolution process>> used by the `PulsarTemplate` is used with the one exception that the "Message type default" step is **omitted**.
@@ -106,8 +116,7 @@ NOTE: If topic information is not specified when using the producer factory APIs
==== Pulsar Producer Caching
Each underlying Pulsar producer consumes resources. To improve performance and avoid continual creation of producers, the producer factory caches the producers that it creates. They are cached in an LRU fashion and evicted when they have not been used within a configured time period. The link:{github}/blob/8e33ac0b122bc0e75df299919c956cacabcc9809/spring-pulsar/src/main/java/org/springframework/pulsar/core/CachingPulsarProducerFactory.java#L159[cache key] is composed of just enough information to ensure that callers are returned the same producer on subsequent creation requests.
Additionally, you can configure the cache settings by specifying any of the `spring.pulsar.producer.cache` prefixed application properties.
See the <<application-properties.adoc#appendix.application-properties.pulsar-producer,Appendix>>.
Additionally, you can configure the cache settings by specifying any of the {spring-boot-pulsar-config-props}[`spring.pulsar.producer.cache.*`] application properties.
=== Intercept Messages on the Producer
Adding a `ProducerInterceptor` lets you intercept and mutate messages received by the producer before they are published to the brokers.
@@ -135,6 +144,9 @@ ProducerInterceptor secondInterceptor() {
----
====
NOTE: If you are not using the starter, you will need to configure and register the aforementioned components yourself.
== Message Consumption
[[pulsar-listener]]
@@ -145,8 +157,7 @@ To use `PulsarListener`, you need to use the `@EnablePulsar` annotation.
When you use Spring Boot support, it automatically enables this annotation and configures all the components necessary for `PulsarListener`, such as the message listener infrastructure (which is responsible for creating the Pulsar consumer).
`PulsarMessageListenerContainer` uses a `PulsarConsumerFactory` to create and manage the Pulsar consumer.
This consumer factory is also auto-configured through Spring Boot.
See the <<application-properties.adoc#appendix.application-properties.pulsar-consumer,Appendix>> for Pulsar consumer properties.
Spring Boot auto-configuration also provides this consumer factory which you can further configure by specifying any of the {spring-boot-pulsar-config-props}[`spring.pulsar.consumer.*`] application properties.
Let us revisit the `PulsarListener` code snippet we saw in the quick-tour section:
@@ -529,7 +540,7 @@ Since it already received the data in batches by using the Consumer's `batchRece
[[pulsar-headers]]
=== Pulsar Headers
The Pulsar message metadata can be consumed as Spring message headers.
The list of available headers can be found in https://github.com/spring-projects/spring-pulsar/blob/main/spring-pulsar/src/main/java/org/springframework/pulsar/support/PulsarHeaders.java[PulsarHeaders.java].
The list of available headers can be found in {github}/blob/main/spring-pulsar/src/main/java/org/springframework/pulsar/support/PulsarHeaders.java[PulsarHeaders.java].
==== Accessing in Single Record based Consumer
@@ -1176,7 +1187,7 @@ TIP: The id parameter passed to `getListenerContainer` is the container id - whi
=== Pulsar Reader Support
The framework provides support for using {apache-pulsar-docs}/concepts-clients/#reader-interface[Pulsar Reader] via the `PulsarReaderFactory`.
Spring Boot provides this reader factory which can be configured with any of the <<application-properties.adoc#appendix.application-properties.pulsar-reader,`spring.pulsar.reader`>> prefixed application properties.
Spring Boot provides this reader factory which you can further configure by specifying any of the {spring-boot-pulsar-config-props}[`spring.pulsar.reader.*`] application properties.
==== PulsarReader Annotation

View File

@@ -5,10 +5,7 @@ include::attributes.adoc[]
We will take a quick tour of Spring for Apache Pulsar by showing a sample Spring Boot application that produces and consumes.
This is a complete application and does not require any additional configuration, as long as you have a Pulsar cluster running on the default location - `localhost:6650`.
NOTE: We recommend using a Spring-Boot-First approach for Spring for Apache Pulsar-based application, as that simplifies things tremendously. To do so, you can add the `spring-pulsar-spring-boot-starter` module as a dependency.
== Dependencies
Spring Boot applications need only the `spring-pulsar-spring-boot-starter` dependency. The following listings show how to define the dependency for Maven and Gradle, respectively:
[source,xml,indent=0,subs="verbatim,attributes",role="primary"]
@@ -18,7 +15,7 @@ Spring Boot applications need only the `spring-pulsar-spring-boot-starter` depen
<dependency>
<groupId>org.springframework.pulsar</groupId>
<artifactId>spring-pulsar-spring-boot-starter</artifactId>
<version>{spring-pulsar-version}</version>
<version>{spring-pulsar-starter-version}</version>
</dependency>
</dependencies>
----
@@ -27,7 +24,7 @@ Spring Boot applications need only the `spring-pulsar-spring-boot-starter` depen
.Gradle
----
dependencies {
implementation 'org.springframework.pulsar:spring-pulsar-spring-boot-starter:{spring-pulsar-version}'
implementation 'org.springframework.pulsar:spring-pulsar-spring-boot-starter:{spring-pulsar-starter-version}'
}
----

View File

@@ -20,6 +20,14 @@ However, the following is not yet supported:
* Accessing Pulsar headers via `@Header` in streaming mode
* Observations
== Preface
NOTE: We recommend using a Spring-Boot-First approach for Spring for Apache Pulsar-based applications, as that simplifies things tremendously.
To do so, you can add the `spring-pulsar-reactive-spring-boot-starter` module as a dependency.
NOTE: The majority of this reference expects the reader to be using the starter and gives most directions for configuration with that in mind.
However, an effort is made to call out when instructions are specific to the Spring Boot starter usage.
include::reactive-quick-tour.adoc[leveloffset=+1]
== Design
@@ -44,7 +52,7 @@ This can be adjusted by setting the `spring.pulsar.client.service-url` property
TIP: The value must be a valid {apache-pulsar-docs}/client-libraries-java/#connection-urls[Pulsar Protocol] URL
There are many other application properties (inherited from the adapted imperative client) available to configure.
See the <<application-properties.adoc#appendix.application-properties.pulsar-client,Appendix>> for properties prefixed with `spring.pulsar.client`.
See the {spring-boot-pulsar-config-props}[`spring.pulsar.client.*`] application properties.
[[reactive-client-authentication]]
=== Authentication
@@ -113,7 +121,7 @@ include::schema-info/schema-info-template.adoc[leveloffset=+1]
=== ReactivePulsarSenderFactory
The `ReactivePulsarTemplate` relies on a `ReactivePulsarSenderFactory` to actually create the underlying sender.
Spring Boot provides this sender factory which can be configured with any of the <<application-properties.adoc#appendix.application-properties.pulsar-reactive-sender,`spring.pulsar.reactive.sender`>> prefixed application properties.
Spring Boot provides this sender factory which can be configured with any of the {spring-boot-pulsar-config-props}[`spring.pulsar.reactive.sender.*`] application properties.
NOTE: If topic information is not specified when using the sender factory APIs directly, the same <<topic-resolution-process-reactive,topic resolution process>> used by the `ReactivePulsarTemplate` is used with the one exception that the "Message type default" step is **omitted**.
@@ -122,7 +130,7 @@ Each underlying Pulsar producer consumes resources.
To improve performance and avoid continual creation of producers, the `ReactiveMessageSenderCache` in the underlying Apache Pulsar Reactive client caches the producers that it creates.
They are cached in an LRU fashion and evicted when they have not been used within a configured time period.
You can configure the cache settings by specifying any of the <<application-properties.adoc#appendix.application-properties.pulsar-reactive-sender,`spring.pulsar.reactive.sender.cache`>> prefixed application properties.
You can configure the cache settings by specifying any of the {spring-boot-pulsar-config-props}[`spring.pulsar.reactive.sender.cache.*`] application properties.
[[reactive-message-consumption]]
== Message Consumption
@@ -262,7 +270,7 @@ Flux<MessageResult<Void>> listen2(Flux<org.springframework.messaging.Message<Foo
==== Configuration - Application Properties
The listener ultimately relies on `ReactivePulsarConsumerFactory` to create and manage the underlying Pulsar consumer.
Spring Boot provides this consumer factory which can be configured with any of the <<application-properties.adoc#appendix.application-properties.pulsar-reactive-consumer,`spring.pulsar.reactive.consumer`>> prefixed application properties.
Spring Boot provides this consumer factory which can be configured with any of the {spring-boot-pulsar-config-props}[`spring.pulsar.reactive.consumer.*`] application-properties.
[[reactive-consumer-customizer]]
==== Consumer Customization
@@ -355,7 +363,7 @@ In contrast to imperative concurrency that can not currently be used with `Exclu
[[reactive-pulsar-headers]]
=== Pulsar Headers
The Pulsar message metadata can be consumed as Spring message headers.
The list of available headers can be found in https://github.com/spring-projects/spring-pulsar/blob/main/spring-pulsar/src/main/java/org/springframework/pulsar/support/PulsarHeaders.java[PulsarHeaders.java].
The list of available headers can be found in {github}/blob/main/spring-pulsar/src/main/java/org/springframework/pulsar/support/PulsarHeaders.java[PulsarHeaders.java].
==== Accessing In OneByOne Listener
The following example shows how you can access Pulsar Headers when using a one-by-one message listener:
@@ -500,7 +508,7 @@ The easy way to solve this is to provide a DLQ topic name always.
=== Pulsar Reader Support
The framework provides support for using {apache-pulsar-docs}/concepts-clients/#reader-interface[Pulsar Reader] in a Reactive fashion via the `ReactivePulsarReaderFactory`.
Spring Boot provides this reader factory which can be configured with any of the <<application-properties.adoc#appendix.application-properties.pulsar-reactive-reader,`spring.pulsar.reactive.reader`>> prefixed application properties.
Spring Boot provides this reader factory which can be configured with any of the {spring-boot-pulsar-config-props}[`spring.pulsar.reactive.reader.*`] application properties.
[[topic-resolution-process-reactive]]
== Topic Resolution

View File

@@ -5,8 +5,6 @@ include::attributes.adoc[]
We will take a quick tour of the Reactive support in Spring for Apache Pulsar by showing a sample Spring Boot application that produces and consumes in a Reactive fashion.
This is a complete application and does not require any additional configuration, as long as you have a Pulsar cluster running on the default location - `localhost:6650`.
NOTE: We recommend using a Spring-Boot-First approach for Spring for Apache Pulsar-based applications, as that simplifies things tremendously. To do so, you can add the `spring-pulsar-reactive-spring-boot-starter` module as a dependency.
== Dependencies
Spring Boot applications need only the `spring-pulsar-reactive-spring-boot-starter` dependency. The following listings show how to define the dependency for Maven and Gradle, respectively:
@@ -18,7 +16,7 @@ Spring Boot applications need only the `spring-pulsar-reactive-spring-boot-start
<dependency>
<groupId>org.springframework.pulsar</groupId>
<artifactId>spring-pulsar-reactive-spring-boot-starter</artifactId>
<version>{spring-pulsar-version}</version>
<version>{spring-pulsar-starter-version}</version>
</dependency>
</dependencies>
----
@@ -26,7 +24,7 @@ Spring Boot applications need only the `spring-pulsar-reactive-spring-boot-start
.Gradle
----
dependencies {
implementation 'org.springframework.pulsar:spring-pulsar-reactive-spring-boot-starter:{spring-pulsar-version}'
implementation 'org.springframework.pulsar:spring-pulsar-reactive-spring-boot-starter:{spring-pulsar-starter-version}'
}
----

View File

@@ -1,13 +0,0 @@
plugins {
id 'org.springframework.pulsar.spring-module'
}
description = 'Spring Pulsar Reactive Spring Boot Starter'
dependencies {
api project (':spring-pulsar')
api project (':spring-pulsar-reactive')
api project (':spring-pulsar-spring-boot-autoconfigure')
api 'org.apache.pulsar:pulsar-client-reactive-producer-cache-caffeine'
api 'org.springframework.boot:spring-boot-starter'
}

View File

@@ -1,5 +1,6 @@
plugins {
id 'org.springframework.pulsar.spring-module'
id 'spring-pulsar.integration-test-conventions'
}
description = 'Spring Pulsar Reactive Support'
@@ -36,8 +37,16 @@ dependencies {
testImplementation 'org.springframework:spring-test'
testImplementation 'org.testcontainers:junit-jupiter'
testImplementation 'org.testcontainers:pulsar'
intTestImplementation 'org.springframework.pulsar:spring-pulsar-reactive-spring-boot-starter'
intTestImplementation 'org.testcontainers:junit-jupiter'
intTestImplementation 'org.testcontainers:pulsar'
}
test {
testLogging.showStandardStreams = true
}
integrationTest {
maxHeapSize '2048m'
}

View File

@@ -50,12 +50,12 @@ import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* Tests for {@link ReactivePulsarListener}.
* Integration tests for {@link ReactivePulsarListener}.
*
* @author Christophe Bornet
* @author Chris Bono
*/
class ReactivePulsarListenerTests implements PulsarTestContainerSupport {
class ReactivePulsarListenerIntegrationTests implements PulsarTestContainerSupport {
private static final CountDownLatch LATCH1 = new CountDownLatch(1);

View File

@@ -6,7 +6,7 @@ plugins {
description = 'Spring Pulsar Sample Application (Send and Receive)'
dependencies {
implementation project(':spring-pulsar-spring-boot-starter')
implementation 'org.springframework.pulsar:spring-pulsar-spring-boot-starter'
implementation 'com.google.code.findbugs:jsr305'
// observability

View File

@@ -6,7 +6,7 @@ plugins {
description = 'Spring Pulsar Sample Applications (Custom Routing)'
dependencies {
implementation project(':spring-pulsar-spring-boot-starter')
implementation 'org.springframework.pulsar:spring-pulsar-spring-boot-starter'
implementation 'com.google.code.findbugs:jsr305'
// observability

View File

@@ -6,8 +6,8 @@ plugins {
description = 'Spring Cloud Stream Binder for Pulsar Sample Application'
dependencies {
implementation project(':spring-pulsar-spring-cloud-stream-binder')
implementation project(':spring-pulsar-spring-boot-starter')
implementation 'org.springframework.pulsar:spring-pulsar-spring-cloud-stream-binder'
implementation 'org.springframework.pulsar:spring-pulsar-spring-boot-starter'
}
bootRun {

View File

@@ -7,7 +7,7 @@ group = 'org.springframework.pulsar.sample'
description = 'Sample Signup App (Pulsar Functions)'
dependencies {
implementation project(':spring-pulsar-spring-boot-starter')
implementation 'org.springframework.pulsar:spring-pulsar-spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-amqp'
implementation 'org.springframework.boot:spring-boot-starter-data-cassandra'
implementation 'com.devskiller:jfairy:0.6.5'

View File

@@ -6,7 +6,7 @@ plugins {
description = 'Spring Pulsar Sample Application (Send and Receive)'
dependencies {
implementation project(':spring-pulsar-spring-boot-starter')
implementation 'org.springframework.pulsar:spring-pulsar-spring-boot-starter'
implementation 'com.google.code.findbugs:jsr305'
}

View File

@@ -6,7 +6,7 @@ plugins {
description = 'Reactive Spring Pulsar Sample Application'
dependencies {
implementation project(':spring-pulsar-reactive-spring-boot-starter')
implementation 'org.springframework.pulsar:spring-pulsar-reactive-spring-boot-starter'
implementation 'com.google.code.findbugs:jsr305'
}

View File

@@ -1,60 +0,0 @@
plugins {
id 'org.springframework.pulsar.spring-module'
id 'org.springframework.pulsar.configuration-properties'
id "de.undercouch.download" version "5.3.0"
}
description = 'Spring Pulsar Spring Boot Auto-configuration'
dependencies {
annotationProcessor 'org.springframework.boot:spring-boot-autoconfigure-processor'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
optional project (':spring-pulsar')
optional project (':spring-pulsar-reactive')
optional 'org.apache.pulsar:pulsar-client-reactive-producer-cache-caffeine'
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'com.google.code.findbugs:jsr305'
testImplementation project(':spring-pulsar-test')
testRuntimeOnly 'org.apache.logging.log4j:log4j-core'
testRuntimeOnly 'org.apache.logging.log4j:log4j-jcl'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.testcontainers:junit-jupiter'
testImplementation 'org.testcontainers:pulsar'
// used by PulsarFunctionTests
testImplementation 'org.testcontainers:rabbitmq'
testImplementation 'org.springframework.boot:spring-boot-starter-amqp'
}
test {
testLogging.showStandardStreams = true
}
integrationTest {
maxHeapSize '2048m'
}
task downloadRabbitConnector {
onlyIf {
System.getProperty("downloadRabbitConnector") == "true"
}
doLast {
try {
download.run {
println "Downloading Rabbit connector to 'src/test/resources/connectors/' (one time only if not already downloaded)..."
src 'https://archive.apache.org/dist/pulsar/pulsar-2.11.0/connectors/pulsar-io-rabbitmq-2.11.0.nar'
dest "$buildDir/../src/test/resources/connectors/pulsar-io-rabbitmq-2.11.0.nar"
overwrite false
}
} catch (Exception ex) {
println "Failed to download rabbit connector: $ex"
}
}
}
project.afterEvaluate {
compileTestJava.dependsOn downloadRabbitConnector
}

View File

@@ -1,72 +0,0 @@
/*
* Copyright 2022-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 java.util.Map;
import java.util.TreeMap;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.apache.pulsar.common.util.ObjectMapperFactory;
import org.springframework.util.CollectionUtils;
/**
* Utility methods for Pulsar authentication parameters.
*
* @author Alexander Preuß
*/
final class AuthParameterUtils {
private static final Pattern KEBAB_CASE_PATTERN = Pattern.compile("-(.)");
private AuthParameterUtils() {
}
private static String convertKebabCaseToCamelCase(String kebabString) {
return KEBAB_CASE_PATTERN.matcher(kebabString).replaceAll(mr -> mr.group(1).toUpperCase());
}
private static Map<String, String> convertWellKnownLowerCaseKeysToCamelCase(Map<String, String> params) {
return params.entrySet().stream().collect(
Collectors.toMap(entry -> WellKnownAuthParameters.toCamelCaseKey(entry.getKey()), Map.Entry::getValue));
}
private static Map<String, String> convertKebabCaseKeysToCamelCase(Map<String, String> params) {
return params.entrySet().stream()
.collect(Collectors.toMap(entry -> convertKebabCaseToCamelCase(entry.getKey()), Map.Entry::getValue));
}
static String maybeConvertToEncodedParamString(Map<String, String> params) {
if (CollectionUtils.isEmpty(params)) {
return null;
}
// env vars are bound like this ISSUER_ID -> issuerid, have to be camel-cased to
// work
params = convertWellKnownLowerCaseKeysToCamelCase(params);
params = convertKebabCaseKeysToCamelCase(params);
params = new TreeMap<>(params); // sort keys for testing and readability
try {
return ObjectMapperFactory.create().writeValueAsString(params);
}
catch (Exception e) {
throw new RuntimeException("Could not convert parameters to encoded string", e);
}
}
}

View File

@@ -1,533 +0,0 @@
/*
* 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 java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.regex.Pattern;
import org.apache.pulsar.client.api.ConsumerCryptoFailureAction;
import org.apache.pulsar.client.api.DeadLetterPolicy;
import org.apache.pulsar.client.api.MessageId;
import org.apache.pulsar.client.api.RegexSubscriptionMode;
import org.apache.pulsar.client.api.SubscriptionInitialPosition;
import org.apache.pulsar.client.api.SubscriptionMode;
import org.apache.pulsar.client.api.SubscriptionType;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.lang.Nullable;
import org.springframework.pulsar.autoconfigure.PulsarProperties.Properties;
/**
* Configuration properties used to specify Pulsar consumers.
*
* @author Chris Bono
*/
public class ConsumerConfigProperties {
/**
* Comma-separated list of topics the consumer subscribes to.
*/
private Set<String> topics;
/**
* Pattern for topics the consumer subscribes to.
*/
private Pattern topicsPattern;
/**
* Subscription name for the consumer.
*/
private String subscriptionName;
/**
* Subscription type to be used when subscribing to a topic.
*/
private SubscriptionType subscriptionType = SubscriptionType.Exclusive;
/**
* Map of properties to add to the subscription.
*/
private Map<String, String> subscriptionProperties = new HashMap<>();
/**
* Subscription mode to be used when subscribing to the topic.
*/
private SubscriptionMode subscriptionMode = SubscriptionMode.Durable;
/**
* Number of messages that can be accumulated before the consumer calls "receive".
*/
private Integer receiverQueueSize = 1000;
/**
* Time to group acknowledgements before sending them to the broker.
*/
private Duration acknowledgementsGroupTime = Duration.ofMillis(100);
/**
* Delay before re-delivering messages that have failed to be processed.
*/
private Duration negativeAckRedeliveryDelay = Duration.ofMinutes(1);
/**
* Maximum number of messages that a consumer can be pushed at once from a broker
* across all partitions.
*/
private Integer maxTotalReceiverQueueSizeAcrossPartitions = 50000;
/**
* Consumer name to identify a particular consumer from the topic stats.
*/
private String consumerName;
/**
* Timeout for unacked messages to be redelivered.
*/
private Duration ackTimeout = Duration.ZERO;
/**
* Precision for the ack timeout messages tracker.
*/
private Duration tickDuration = Duration.ofSeconds(1);
/**
* Priority level for shared subscription consumers.
*/
private Integer priorityLevel = 0;
/**
* Action the consumer will take in case of decryption failure.
*/
private ConsumerCryptoFailureAction cryptoFailureAction = ConsumerCryptoFailureAction.FAIL;
/**
* Map of properties to add to the consumer.
*/
private SortedMap<String, String> properties = new TreeMap<>();
/**
* Whether to read messages from the compacted topic rather than the full message
* backlog.
*/
private Boolean readCompacted = false;
/**
* Position where to initialize a newly created subscription.
*/
private SubscriptionInitialPosition subscriptionInitialPosition = SubscriptionInitialPosition.Latest;
/**
* Auto-discovery period for topics when topic pattern is used in minutes.
*/
private Integer patternAutoDiscoveryPeriod = 1;
/**
* Determines which topics the consumer should be subscribed to when using pattern
* subscriptions.
*/
private RegexSubscriptionMode regexSubscriptionMode = RegexSubscriptionMode.PersistentOnly;
/**
* Dead letter policy to use.
*/
@Nullable
@NestedConfigurationProperty
private DeadLetterPolicy deadLetterPolicy;
/**
* Whether to auto retry messages.
*/
private Boolean retryEnable = false;
/**
* Whether the consumer auto-subscribes for partition increase. This is only for
* partitioned consumers.
*/
private Boolean autoUpdatePartitions = true;
/**
* Interval of partitions discovery updates.
*/
private Duration autoUpdatePartitionsInterval = Duration.ofMinutes(1);
/**
* Whether to replicate subscription state.
*/
private Boolean replicateSubscriptionState = false;
/**
* Whether to include the given position of any reset operation like
* {@link org.apache.pulsar.client.api.Consumer#seek(long) or
* {@link ConsumerConfigProperties#seek(MessageId)}}.
*/
private Boolean resetIncludeHead = false;
/**
* Whether the batch index acknowledgment is enabled.
*/
private Boolean batchIndexAckEnabled = false;
/**
* Whether an acknowledgement receipt is enabled.
*/
private Boolean ackReceiptEnabled = false;
/**
* Whether pooling of messages and the underlying data buffers is enabled.
*/
private Boolean poolMessages = false;
/**
* Whether to start the consumer in a paused state.
*/
private Boolean startPaused = false;
/**
* Whether to automatically drop outstanding un-acked messages if the queue is full.
*/
private Boolean autoAckOldestChunkedMessageOnQueueFull = true;
/**
* Maximum number of chunked messages to be kept in memory.
*/
private Integer maxPendingChunkedMessage = 10;
/**
* Time to expire incomplete chunks if the consumer won't be able to receive all
* chunks before.
*/
private Duration expireTimeOfIncompleteChunkedMessage = Duration.ofMinutes(1);
public Set<String> getTopics() {
return this.topics;
}
public void setTopics(Set<String> topics) {
this.topics = topics;
}
public Pattern getTopicsPattern() {
return this.topicsPattern;
}
public void setTopicsPattern(Pattern topicsPattern) {
this.topicsPattern = topicsPattern;
}
public String getSubscriptionName() {
return this.subscriptionName;
}
public void setSubscriptionName(String subscriptionName) {
this.subscriptionName = subscriptionName;
}
public Map<String, String> getSubscriptionProperties() {
return this.subscriptionProperties;
}
public void setSubscriptionProperties(Map<String, String> subscriptionProperties) {
this.subscriptionProperties = subscriptionProperties;
}
public SubscriptionMode getSubscriptionMode() {
return this.subscriptionMode;
}
public void setSubscriptionMode(SubscriptionMode subscriptionMode) {
this.subscriptionMode = subscriptionMode;
}
public SubscriptionType getSubscriptionType() {
return this.subscriptionType;
}
public void setSubscriptionType(SubscriptionType subscriptionType) {
this.subscriptionType = subscriptionType;
}
public Integer getReceiverQueueSize() {
return this.receiverQueueSize;
}
public void setReceiverQueueSize(Integer receiverQueueSize) {
this.receiverQueueSize = receiverQueueSize;
}
public Duration getAcknowledgementsGroupTime() {
return this.acknowledgementsGroupTime;
}
public void setAcknowledgementsGroupTime(Duration acknowledgementsGroupTime) {
this.acknowledgementsGroupTime = acknowledgementsGroupTime;
}
public Duration getNegativeAckRedeliveryDelay() {
return this.negativeAckRedeliveryDelay;
}
public void setNegativeAckRedeliveryDelay(Duration negativeAckRedeliveryDelay) {
this.negativeAckRedeliveryDelay = negativeAckRedeliveryDelay;
}
public Integer getMaxTotalReceiverQueueSizeAcrossPartitions() {
return this.maxTotalReceiverQueueSizeAcrossPartitions;
}
public void setMaxTotalReceiverQueueSizeAcrossPartitions(Integer maxTotalReceiverQueueSizeAcrossPartitions) {
this.maxTotalReceiverQueueSizeAcrossPartitions = maxTotalReceiverQueueSizeAcrossPartitions;
}
public String getConsumerName() {
return this.consumerName;
}
public void setConsumerName(String consumerName) {
this.consumerName = consumerName;
}
public Duration getAckTimeout() {
return this.ackTimeout;
}
public void setAckTimeout(Duration ackTimeout) {
this.ackTimeout = ackTimeout;
}
public Duration getTickDuration() {
return this.tickDuration;
}
public void setTickDuration(Duration tickDuration) {
this.tickDuration = tickDuration;
}
public Integer getPriorityLevel() {
return this.priorityLevel;
}
public void setPriorityLevel(Integer priorityLevel) {
this.priorityLevel = priorityLevel;
}
public ConsumerCryptoFailureAction getCryptoFailureAction() {
return this.cryptoFailureAction;
}
public void setCryptoFailureAction(ConsumerCryptoFailureAction cryptoFailureAction) {
this.cryptoFailureAction = cryptoFailureAction;
}
public SortedMap<String, String> getProperties() {
return this.properties;
}
public void setProperties(SortedMap<String, String> properties) {
this.properties = properties;
}
public Boolean getReadCompacted() {
return this.readCompacted;
}
public void setReadCompacted(Boolean readCompacted) {
this.readCompacted = readCompacted;
}
public SubscriptionInitialPosition getSubscriptionInitialPosition() {
return this.subscriptionInitialPosition;
}
public void setSubscriptionInitialPosition(SubscriptionInitialPosition subscriptionInitialPosition) {
this.subscriptionInitialPosition = subscriptionInitialPosition;
}
public Integer getPatternAutoDiscoveryPeriod() {
return this.patternAutoDiscoveryPeriod;
}
public void setPatternAutoDiscoveryPeriod(Integer patternAutoDiscoveryPeriod) {
this.patternAutoDiscoveryPeriod = patternAutoDiscoveryPeriod;
}
public RegexSubscriptionMode getRegexSubscriptionMode() {
return this.regexSubscriptionMode;
}
public void setRegexSubscriptionMode(RegexSubscriptionMode regexSubscriptionMode) {
this.regexSubscriptionMode = regexSubscriptionMode;
}
@Nullable
public DeadLetterPolicy getDeadLetterPolicy() {
return this.deadLetterPolicy;
}
public void setDeadLetterPolicy(@Nullable DeadLetterPolicy deadLetterPolicy) {
this.deadLetterPolicy = deadLetterPolicy;
}
public Boolean getRetryEnable() {
return this.retryEnable;
}
public void setRetryEnable(Boolean retryEnable) {
this.retryEnable = retryEnable;
}
public Boolean getAutoUpdatePartitions() {
return this.autoUpdatePartitions;
}
public void setAutoUpdatePartitions(Boolean autoUpdatePartitions) {
this.autoUpdatePartitions = autoUpdatePartitions;
}
public Duration getAutoUpdatePartitionsInterval() {
return this.autoUpdatePartitionsInterval;
}
public void setAutoUpdatePartitionsInterval(Duration autoUpdatePartitionsInterval) {
this.autoUpdatePartitionsInterval = autoUpdatePartitionsInterval;
}
public Boolean getReplicateSubscriptionState() {
return this.replicateSubscriptionState;
}
public void setReplicateSubscriptionState(Boolean replicateSubscriptionState) {
this.replicateSubscriptionState = replicateSubscriptionState;
}
public Boolean getResetIncludeHead() {
return this.resetIncludeHead;
}
public void setResetIncludeHead(Boolean resetIncludeHead) {
this.resetIncludeHead = resetIncludeHead;
}
public Boolean getBatchIndexAckEnabled() {
return this.batchIndexAckEnabled;
}
public void setBatchIndexAckEnabled(Boolean batchIndexAckEnabled) {
this.batchIndexAckEnabled = batchIndexAckEnabled;
}
public Boolean getAckReceiptEnabled() {
return this.ackReceiptEnabled;
}
public void setAckReceiptEnabled(Boolean ackReceiptEnabled) {
this.ackReceiptEnabled = ackReceiptEnabled;
}
public Boolean getPoolMessages() {
return this.poolMessages;
}
public void setPoolMessages(Boolean poolMessages) {
this.poolMessages = poolMessages;
}
public Boolean getStartPaused() {
return this.startPaused;
}
public void setStartPaused(Boolean startPaused) {
this.startPaused = startPaused;
}
public Boolean getAutoAckOldestChunkedMessageOnQueueFull() {
return this.autoAckOldestChunkedMessageOnQueueFull;
}
public void setAutoAckOldestChunkedMessageOnQueueFull(Boolean autoAckOldestChunkedMessageOnQueueFull) {
this.autoAckOldestChunkedMessageOnQueueFull = autoAckOldestChunkedMessageOnQueueFull;
}
public Integer getMaxPendingChunkedMessage() {
return this.maxPendingChunkedMessage;
}
public void setMaxPendingChunkedMessage(Integer maxPendingChunkedMessage) {
this.maxPendingChunkedMessage = maxPendingChunkedMessage;
}
public Duration getExpireTimeOfIncompleteChunkedMessage() {
return this.expireTimeOfIncompleteChunkedMessage;
}
public void setExpireTimeOfIncompleteChunkedMessage(Duration expireTimeOfIncompleteChunkedMessage) {
this.expireTimeOfIncompleteChunkedMessage = expireTimeOfIncompleteChunkedMessage;
}
public Map<String, Object> buildProperties() {
PulsarProperties.Properties properties = new Properties();
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(this::getTopics).to(properties.in("topicNames"));
map.from(this::getTopicsPattern).to(properties.in("topicsPattern"));
map.from(this::getSubscriptionName).to(properties.in("subscriptionName"));
map.from(this::getSubscriptionType).to(properties.in("subscriptionType"));
map.from(this::getSubscriptionProperties).to(properties.in("subscriptionProperties"));
map.from(this::getSubscriptionMode).to(properties.in("subscriptionMode"));
map.from(this::getReceiverQueueSize).to(properties.in("receiverQueueSize"));
map.from(this::getAcknowledgementsGroupTime).as(it -> it.toNanos() / 1000)
.to(properties.in("acknowledgementsGroupTimeMicros"));
map.from(this::getNegativeAckRedeliveryDelay).as(it -> it.toNanos() / 1000)
.to(properties.in("negativeAckRedeliveryDelayMicros"));
map.from(this::getMaxTotalReceiverQueueSizeAcrossPartitions)
.to(properties.in("maxTotalReceiverQueueSizeAcrossPartitions"));
map.from(this::getConsumerName).to(properties.in("consumerName"));
map.from(this::getAckTimeout).as(Duration::toMillis).to(properties.in("ackTimeoutMillis"));
map.from(this::getTickDuration).as(Duration::toMillis).to(properties.in("tickDurationMillis"));
map.from(this::getPriorityLevel).to(properties.in("priorityLevel"));
map.from(this::getCryptoFailureAction).to(properties.in("cryptoFailureAction"));
map.from(this::getProperties).to(properties.in("properties"));
map.from(this::getReadCompacted).to(properties.in("readCompacted"));
map.from(this::getSubscriptionInitialPosition).to(properties.in("subscriptionInitialPosition"));
map.from(this::getPatternAutoDiscoveryPeriod).to(properties.in("patternAutoDiscoveryPeriod"));
map.from(this::getRegexSubscriptionMode).to(properties.in("regexSubscriptionMode"));
map.from(this::getDeadLetterPolicy).to(properties.in("deadLetterPolicy"));
map.from(this::getRetryEnable).to(properties.in("retryEnable"));
map.from(this::getAutoUpdatePartitions).to(properties.in("autoUpdatePartitions"));
map.from(this::getAutoUpdatePartitionsInterval).as(Duration::toSeconds)
.to(properties.in("autoUpdatePartitionsIntervalSeconds"));
map.from(this::getReplicateSubscriptionState).to(properties.in("replicateSubscriptionState"));
map.from(this::getResetIncludeHead).to(properties.in("resetIncludeHead"));
map.from(this::getBatchIndexAckEnabled).to(properties.in("batchIndexAckEnabled"));
map.from(this::getAckReceiptEnabled).to(properties.in("ackReceiptEnabled"));
map.from(this::getPoolMessages).to(properties.in("poolMessages"));
map.from(this::getStartPaused).to(properties.in("startPaused"));
map.from(this::getAutoAckOldestChunkedMessageOnQueueFull)
.to(properties.in("autoAckOldestChunkedMessageOnQueueFull"));
map.from(this::getMaxPendingChunkedMessage).to(properties.in("maxPendingChunkedMessage"));
map.from(this::getExpireTimeOfIncompleteChunkedMessage).as(Duration::toMillis)
.to(properties.in("expireTimeOfIncompleteChunkedMessageMillis"));
return properties;
}
}

View File

@@ -1,404 +0,0 @@
/*
* 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 java.time.Duration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.apache.pulsar.client.api.CompressionType;
import org.apache.pulsar.client.api.HashingScheme;
import org.apache.pulsar.client.api.MessageRoutingMode;
import org.apache.pulsar.client.api.ProducerAccessMode;
import org.apache.pulsar.client.api.ProducerCryptoFailureAction;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.lang.Nullable;
import org.springframework.pulsar.autoconfigure.PulsarProperties.Cache;
import org.springframework.pulsar.autoconfigure.PulsarProperties.Properties;
import org.springframework.util.unit.DataSize;
/**
* Configuration properties used to specify Pulsar producers.
*
* @author Chris Bono
*/
public class ProducerConfigProperties {
/**
* Topic the producer will publish to.
*/
private String topicName;
/**
* Name for the producer. If not assigned, a unique name is generated.
*/
private String producerName;
/**
* Time before a message has to be acknowledged by the broker.
*/
private Duration sendTimeout = Duration.ofSeconds(30);
/**
* Whether the "send" and "sendAsync" methods should block if the outgoing message
* queue is full.
*/
private Boolean blockIfQueueFull = false;
/**
* Maximum number of pending messages for the producer.
*/
private Integer maxPendingMessages = 1000;
/**
* Maximum number of pending messages across all the partitions.
*/
private Integer maxPendingMessagesAcrossPartitions = 50000;
/**
* Message routing mode for a partitioned producer.
*/
private MessageRoutingMode messageRoutingMode = MessageRoutingMode.RoundRobinPartition;
/**
* Message hashing scheme to choose the partition to which the message is published.
*/
private HashingScheme hashingScheme = HashingScheme.JavaStringHash;
/**
* Action the producer will take in case of encryption failure.
*/
private ProducerCryptoFailureAction cryptoFailureAction = ProducerCryptoFailureAction.FAIL;
/**
* Time period within which the messages sent will be batched.
*/
private Duration batchingMaxPublishDelay = Duration.ofMillis(1);
/**
* Partition switch frequency while batching of messages is enabled and using
* round-robin routing mode for non-keyed message.
*/
private Integer batchingPartitionSwitchFrequencyByPublishDelay = 10;
/**
* Maximum number of messages to be batched.
*/
private Integer batchingMaxMessages = 1000;
/**
* Maximum number of bytes permitted in a batch.
*/
private DataSize batchingMaxBytes = DataSize.ofKilobytes(128);
/**
* Whether to automatically batch messages.
*/
private Boolean batchingEnabled = true;
/**
* Whether to split large-size messages into multiple chunks.
*/
private Boolean chunkingEnabled = false;
/**
* Names of the public encryption keys to use when encrypting data.
*/
private Set<String> encryptionKeys = new HashSet<>();
/**
* Message compression type.
*/
private CompressionType compressionType;
/**
* Baseline for the sequence ids for messages published by the producer.
*/
@Nullable
private Long initialSequenceId;
/**
* Whether partitioned producer automatically discover new partitions at runtime.
*/
private Boolean autoUpdatePartitions = true;
/**
* Interval of partitions discovery updates.
*/
private Duration autoUpdatePartitionsInterval = Duration.ofMinutes(1);
/**
* Whether the multiple schema mode is enabled.
*/
private Boolean multiSchema = true;
/**
* Type of access to the topic the producer requires.
*/
private ProducerAccessMode producerAccessMode = ProducerAccessMode.Shared;
/**
* Whether producers in Shared mode register and connect immediately to the owner
* broker of each partition or start lazily on demand.
*/
private Boolean lazyStartPartitionedProducers = false;
/**
* Map of properties to add to the producer.
*/
private Map<String, String> properties = new HashMap<>();
private final Cache cache = new Cache();
public String getTopicName() {
return this.topicName;
}
public void setTopicName(String topicName) {
this.topicName = topicName;
}
public String getProducerName() {
return this.producerName;
}
public void setProducerName(String producerName) {
this.producerName = producerName;
}
public Duration getSendTimeout() {
return this.sendTimeout;
}
public void setSendTimeout(Duration sendTimeout) {
this.sendTimeout = sendTimeout;
}
public Boolean getBlockIfQueueFull() {
return this.blockIfQueueFull;
}
public void setBlockIfQueueFull(Boolean blockIfQueueFull) {
this.blockIfQueueFull = blockIfQueueFull;
}
public Integer getMaxPendingMessages() {
return this.maxPendingMessages;
}
public void setMaxPendingMessages(Integer maxPendingMessages) {
this.maxPendingMessages = maxPendingMessages;
}
public Integer getMaxPendingMessagesAcrossPartitions() {
return this.maxPendingMessagesAcrossPartitions;
}
public void setMaxPendingMessagesAcrossPartitions(Integer maxPendingMessagesAcrossPartitions) {
this.maxPendingMessagesAcrossPartitions = maxPendingMessagesAcrossPartitions;
}
public MessageRoutingMode getMessageRoutingMode() {
return this.messageRoutingMode;
}
public void setMessageRoutingMode(MessageRoutingMode messageRoutingMode) {
this.messageRoutingMode = messageRoutingMode;
}
public HashingScheme getHashingScheme() {
return this.hashingScheme;
}
public void setHashingScheme(HashingScheme hashingScheme) {
this.hashingScheme = hashingScheme;
}
public ProducerCryptoFailureAction getCryptoFailureAction() {
return this.cryptoFailureAction;
}
public void setCryptoFailureAction(ProducerCryptoFailureAction cryptoFailureAction) {
this.cryptoFailureAction = cryptoFailureAction;
}
public Duration getBatchingMaxPublishDelay() {
return this.batchingMaxPublishDelay;
}
public void setBatchingMaxPublishDelay(Duration batchingMaxPublishDelay) {
this.batchingMaxPublishDelay = batchingMaxPublishDelay;
}
public Integer getBatchingPartitionSwitchFrequencyByPublishDelay() {
return this.batchingPartitionSwitchFrequencyByPublishDelay;
}
public void setBatchingPartitionSwitchFrequencyByPublishDelay(
Integer batchingPartitionSwitchFrequencyByPublishDelay) {
this.batchingPartitionSwitchFrequencyByPublishDelay = batchingPartitionSwitchFrequencyByPublishDelay;
}
public Integer getBatchingMaxMessages() {
return this.batchingMaxMessages;
}
public void setBatchingMaxMessages(Integer batchingMaxMessages) {
this.batchingMaxMessages = batchingMaxMessages;
}
public DataSize getBatchingMaxBytes() {
return this.batchingMaxBytes;
}
public void setBatchingMaxBytes(DataSize batchingMaxBytes) {
this.batchingMaxBytes = batchingMaxBytes;
}
public Boolean getBatchingEnabled() {
return this.batchingEnabled;
}
public void setBatchingEnabled(Boolean batchingEnabled) {
this.batchingEnabled = batchingEnabled;
}
public Boolean getChunkingEnabled() {
return this.chunkingEnabled;
}
public void setChunkingEnabled(Boolean chunkingEnabled) {
this.chunkingEnabled = chunkingEnabled;
}
public Set<String> getEncryptionKeys() {
return this.encryptionKeys;
}
public void setEncryptionKeys(Set<String> encryptionKeys) {
this.encryptionKeys = encryptionKeys;
}
public CompressionType getCompressionType() {
return this.compressionType;
}
public void setCompressionType(CompressionType compressionType) {
this.compressionType = compressionType;
}
@Nullable
public Long getInitialSequenceId() {
return this.initialSequenceId;
}
public void setInitialSequenceId(@Nullable Long initialSequenceId) {
this.initialSequenceId = initialSequenceId;
}
public Boolean getAutoUpdatePartitions() {
return this.autoUpdatePartitions;
}
public void setAutoUpdatePartitions(Boolean autoUpdatePartitions) {
this.autoUpdatePartitions = autoUpdatePartitions;
}
public Duration getAutoUpdatePartitionsInterval() {
return this.autoUpdatePartitionsInterval;
}
public void setAutoUpdatePartitionsInterval(Duration autoUpdatePartitionsInterval) {
this.autoUpdatePartitionsInterval = autoUpdatePartitionsInterval;
}
public Boolean getMultiSchema() {
return this.multiSchema;
}
public void setMultiSchema(Boolean multiSchema) {
this.multiSchema = multiSchema;
}
public ProducerAccessMode getProducerAccessMode() {
return this.producerAccessMode;
}
public void setProducerAccessMode(ProducerAccessMode producerAccessMode) {
this.producerAccessMode = producerAccessMode;
}
public Boolean getLazyStartPartitionedProducers() {
return this.lazyStartPartitionedProducers;
}
public void setLazyStartPartitionedProducers(Boolean lazyStartPartitionedProducers) {
this.lazyStartPartitionedProducers = lazyStartPartitionedProducers;
}
public Map<String, String> getProperties() {
return this.properties;
}
public void setProperties(Map<String, String> properties) {
this.properties = properties;
}
public Cache getCache() {
return this.cache;
}
public Map<String, Object> buildProperties() {
PulsarProperties.Properties properties = new Properties();
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(this::getTopicName).to(properties.in("topicName"));
map.from(this::getProducerName).to(properties.in("producerName"));
map.from(this::getSendTimeout).asInt(Duration::toMillis).to(properties.in("sendTimeoutMs"));
map.from(this::getBlockIfQueueFull).to(properties.in("blockIfQueueFull"));
map.from(this::getMaxPendingMessages).to(properties.in("maxPendingMessages"));
map.from(this::getMaxPendingMessagesAcrossPartitions).to(properties.in("maxPendingMessagesAcrossPartitions"));
map.from(this::getMessageRoutingMode).to(properties.in("messageRoutingMode"));
map.from(this::getHashingScheme).to(properties.in("hashingScheme"));
map.from(this::getCryptoFailureAction).to(properties.in("cryptoFailureAction"));
map.from(this::getBatchingMaxPublishDelay).as(it -> it.toNanos() / 1000)
.to(properties.in("batchingMaxPublishDelayMicros"));
map.from(this::getBatchingPartitionSwitchFrequencyByPublishDelay)
.to(properties.in("batchingPartitionSwitchFrequencyByPublishDelay"));
map.from(this::getBatchingMaxMessages).to(properties.in("batchingMaxMessages"));
map.from(this::getBatchingMaxBytes).asInt(DataSize::toBytes).to(properties.in("batchingMaxBytes"));
map.from(this::getBatchingEnabled).to(properties.in("batchingEnabled"));
map.from(this::getChunkingEnabled).to(properties.in("chunkingEnabled"));
map.from(this::getEncryptionKeys).to(properties.in("encryptionKeys"));
map.from(this::getCompressionType).to(properties.in("compressionType"));
map.from(this::getInitialSequenceId).to(properties.in("initialSequenceId"));
map.from(this::getAutoUpdatePartitions).to(properties.in("autoUpdatePartitions"));
map.from(this::getAutoUpdatePartitionsInterval).as(Duration::toSeconds)
.to(properties.in("autoUpdatePartitionsIntervalSeconds"));
map.from(this::getMultiSchema).to(properties.in("multiSchema"));
map.from(this::getProducerAccessMode).to(properties.in("accessMode"));
map.from(this::getLazyStartPartitionedProducers).to(properties.in("lazyStartPartitionedProducers"));
map.from(this::getProperties).to(properties.in("properties"));
return properties;
}
}

View File

@@ -1,108 +0,0 @@
/*
* Copyright 2022-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 java.time.Duration;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.pulsar.annotation.EnablePulsar;
import org.springframework.pulsar.config.ConcurrentPulsarListenerContainerFactory;
import org.springframework.pulsar.config.DefaultPulsarReaderContainerFactory;
import org.springframework.pulsar.config.PulsarAnnotationSupportBeanNames;
import org.springframework.pulsar.core.PulsarConsumerFactory;
import org.springframework.pulsar.core.PulsarReaderFactory;
import org.springframework.pulsar.core.SchemaResolver;
import org.springframework.pulsar.core.TopicResolver;
import org.springframework.pulsar.listener.PulsarContainerProperties;
import org.springframework.pulsar.observation.PulsarListenerObservationConvention;
import org.springframework.pulsar.reader.PulsarReaderContainerProperties;
import org.springframework.util.unit.DataSize;
import io.micrometer.observation.ObservationRegistry;
/**
* Configuration for Pulsar annotation-driven support.
*
* @author Soby Chacko
* @author Chris Bono
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(EnablePulsar.class)
public class PulsarAnnotationDrivenConfiguration {
private final PulsarProperties pulsarProperties;
public PulsarAnnotationDrivenConfiguration(PulsarProperties pulsarProperties) {
this.pulsarProperties = pulsarProperties;
}
@Bean
@ConditionalOnMissingBean(name = "pulsarListenerContainerFactory")
ConcurrentPulsarListenerContainerFactory<?> pulsarListenerContainerFactory(
ObjectProvider<PulsarConsumerFactory<Object>> consumerFactoryProvider,
ObjectProvider<ObservationRegistry> observationRegistryProvider,
ObjectProvider<PulsarListenerObservationConvention> observationConventionProvider,
SchemaResolver schemaResolver, TopicResolver topicResolver) {
PulsarContainerProperties containerProperties = new PulsarContainerProperties();
containerProperties.setSchemaResolver(schemaResolver);
containerProperties.setTopicResolver(topicResolver);
containerProperties.setSubscriptionType(this.pulsarProperties.getConsumer().getSubscriptionType());
containerProperties.setObservationEnabled(this.pulsarProperties.getListener().isObservationsEnabled());
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
PulsarProperties.Listener listenerProperties = this.pulsarProperties.getListener();
map.from(listenerProperties::getSchemaType).to(containerProperties::setSchemaType);
map.from(listenerProperties::getAckMode).to(containerProperties::setAckMode);
map.from(listenerProperties::getBatchTimeout).asInt(Duration::toMillis)
.to(containerProperties::setBatchTimeoutMillis);
map.from(listenerProperties::getMaxNumBytes).asInt(DataSize::toBytes).to(containerProperties::setMaxNumBytes);
map.from(listenerProperties::getMaxNumMessages).to(containerProperties::setMaxNumMessages);
return new ConcurrentPulsarListenerContainerFactory<>(consumerFactoryProvider.getIfAvailable(),
containerProperties);
}
@Bean
@ConditionalOnMissingBean(name = "pulsarReaderContainerFactory")
DefaultPulsarReaderContainerFactory<?> pulsarReaderContainerFactory(
ObjectProvider<PulsarReaderFactory<Object>> readerFactoryProvider, SchemaResolver schemaResolver) {
PulsarReaderContainerProperties containerProperties = new PulsarReaderContainerProperties();
containerProperties.setSchemaResolver(schemaResolver);
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
PulsarProperties.Reader readerProperties = this.pulsarProperties.getReader();
map.from(readerProperties::getTopicNames).to(containerProperties::setTopics);
return new DefaultPulsarReaderContainerFactory<>(readerFactoryProvider.getIfAvailable(), containerProperties);
}
@Configuration(proxyBeanMethods = false)
@EnablePulsar
@ConditionalOnMissingBean(name = { PulsarAnnotationSupportBeanNames.PULSAR_LISTENER_ANNOTATION_PROCESSOR_BEAN_NAME,
PulsarAnnotationSupportBeanNames.PULSAR_READER_ANNOTATION_PROCESSOR_BEAN_NAME })
static class EnablePulsarConfiguration {
}
}

View File

@@ -1,163 +0,0 @@
/*
* Copyright 2022-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 java.util.Optional;
import org.apache.pulsar.client.api.PulsarClient;
import org.apache.pulsar.client.api.interceptor.ProducerInterceptor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.pulsar.config.PulsarClientFactoryBean;
import org.springframework.pulsar.core.CachingPulsarProducerFactory;
import org.springframework.pulsar.core.DefaultPulsarConsumerFactory;
import org.springframework.pulsar.core.DefaultPulsarProducerFactory;
import org.springframework.pulsar.core.DefaultPulsarReaderFactory;
import org.springframework.pulsar.core.DefaultSchemaResolver;
import org.springframework.pulsar.core.DefaultTopicResolver;
import org.springframework.pulsar.core.PulsarAdministration;
import org.springframework.pulsar.core.PulsarConsumerFactory;
import org.springframework.pulsar.core.PulsarProducerFactory;
import org.springframework.pulsar.core.PulsarReaderFactory;
import org.springframework.pulsar.core.PulsarTemplate;
import org.springframework.pulsar.core.SchemaResolver;
import org.springframework.pulsar.core.SchemaResolver.SchemaResolverCustomizer;
import org.springframework.pulsar.core.TopicResolver;
import org.springframework.pulsar.function.PulsarFunction;
import org.springframework.pulsar.function.PulsarFunctionAdministration;
import org.springframework.pulsar.function.PulsarSink;
import org.springframework.pulsar.function.PulsarSource;
/**
* {@link EnableAutoConfiguration Auto-configuration} for Apache Pulsar.
*
* @author Soby Chacko
* @author Chris Bono
* @author Alexander Preuß
*/
@AutoConfiguration
@ConditionalOnClass(PulsarTemplate.class)
@EnableConfigurationProperties(PulsarProperties.class)
@Import({ PulsarAnnotationDrivenConfiguration.class })
public class PulsarAutoConfiguration {
private final PulsarProperties properties;
public PulsarAutoConfiguration(PulsarProperties properties) {
this.properties = properties;
}
@Bean
@ConditionalOnMissingBean
public PulsarClientFactoryBean pulsarClientFactoryBean() {
return new PulsarClientFactoryBean(this.properties.buildClientProperties());
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "spring.pulsar.producer.cache.enabled", havingValue = "false")
public PulsarProducerFactory<?> pulsarProducerFactory(PulsarClient pulsarClient, TopicResolver topicResolver) {
return new DefaultPulsarProducerFactory<>(pulsarClient, this.properties.buildProducerProperties(),
topicResolver);
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "spring.pulsar.producer.cache.enabled", havingValue = "true", matchIfMissing = true)
public PulsarProducerFactory<?> cachingPulsarProducerFactory(PulsarClient pulsarClient,
TopicResolver topicResolver) {
return new CachingPulsarProducerFactory<>(pulsarClient, this.properties.buildProducerProperties(),
topicResolver, this.properties.getProducer().getCache().getExpireAfterAccess(),
this.properties.getProducer().getCache().getMaximumSize(),
this.properties.getProducer().getCache().getInitialCapacity());
}
@Bean
@ConditionalOnMissingBean
public PulsarTemplate<?> pulsarTemplate(PulsarProducerFactory<?> pulsarProducerFactory,
ObjectProvider<ProducerInterceptor> interceptorsProvider, SchemaResolver schemaResolver,
TopicResolver topicResolver) {
return new PulsarTemplate<>(pulsarProducerFactory, interceptorsProvider.orderedStream().toList(),
schemaResolver, topicResolver, this.properties.getTemplate().isObservationsEnabled());
}
@Bean
@ConditionalOnMissingBean(SchemaResolver.class)
public DefaultSchemaResolver schemaResolver(PulsarProperties pulsarProperties,
Optional<SchemaResolverCustomizer<DefaultSchemaResolver>> schemaResolverCustomizer) {
var schemaResolver = new DefaultSchemaResolver();
if (pulsarProperties.getDefaults().getTypeMappings() != null) {
pulsarProperties.getDefaults().getTypeMappings().stream().filter((tm) -> tm.schemaInfo() != null)
.forEach((tm) -> {
var schema = schemaResolver.resolveSchema(tm.schemaInfo().schemaType(), tm.messageType(),
tm.schemaInfo().messageKeyType()).orElseThrow();
schemaResolver.addCustomSchemaMapping(tm.messageType(), schema);
});
}
schemaResolverCustomizer.ifPresent((customizer) -> customizer.customize(schemaResolver));
return schemaResolver;
}
@Bean
@ConditionalOnMissingBean(TopicResolver.class)
public DefaultTopicResolver topicResolver(PulsarProperties pulsarProperties) {
var topicResolver = new DefaultTopicResolver();
if (pulsarProperties.getDefaults().getTypeMappings() != null) {
pulsarProperties.getDefaults().getTypeMappings().stream().filter((tm) -> tm.topicName() != null)
.forEach((tm) -> topicResolver.addCustomTopicMapping(tm.messageType(), tm.topicName()));
}
return topicResolver;
}
@Bean
@ConditionalOnMissingBean
public PulsarConsumerFactory<?> pulsarConsumerFactory(PulsarClient pulsarClient) {
return new DefaultPulsarConsumerFactory<>(pulsarClient, this.properties.buildConsumerProperties());
}
@Bean
@ConditionalOnMissingBean
public PulsarAdministration pulsarAdministration() {
return new PulsarAdministration(this.properties.buildAdminProperties());
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "spring.pulsar.function.enabled", havingValue = "true", matchIfMissing = true)
public PulsarFunctionAdministration pulsarFunctionAdministration(PulsarAdministration pulsarAdministration,
ObjectProvider<PulsarFunction> pulsarFunctions, ObjectProvider<PulsarSink> pulsarSinks,
ObjectProvider<PulsarSource> pulsarSources) {
return new PulsarFunctionAdministration(pulsarAdministration, pulsarFunctions, pulsarSinks, pulsarSources,
this.properties.getFunction().getFailFast(), this.properties.getFunction().getPropagateFailures(),
this.properties.getFunction().getPropagateStopFailures());
}
@Bean
@ConditionalOnMissingBean
public PulsarReaderFactory<?> pulsarReaderFactory(PulsarClient pulsarClient) {
return new DefaultPulsarReaderFactory<>(pulsarClient, this.properties.buildReaderProperties());
}
}

View File

@@ -1,77 +0,0 @@
/*
* Copyright 2022-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 org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.pulsar.config.PulsarAnnotationSupportBeanNames;
import org.springframework.pulsar.core.SchemaResolver;
import org.springframework.pulsar.core.TopicResolver;
import org.springframework.pulsar.reactive.config.DefaultReactivePulsarListenerContainerFactory;
import org.springframework.pulsar.reactive.config.annotation.EnableReactivePulsar;
import org.springframework.pulsar.reactive.core.ReactivePulsarConsumerFactory;
import org.springframework.pulsar.reactive.listener.ReactivePulsarContainerProperties;
/**
* Configuration for Reactive Pulsar annotation-driven support.
*
* @author Christophe Bornet
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(EnableReactivePulsar.class)
public class PulsarReactiveAnnotationDrivenConfiguration {
private final PulsarReactiveProperties properties;
public PulsarReactiveAnnotationDrivenConfiguration(PulsarReactiveProperties properties) {
this.properties = properties;
}
@Bean
@ConditionalOnMissingBean(name = "reactivePulsarListenerContainerFactory")
DefaultReactivePulsarListenerContainerFactory<?> reactivePulsarListenerContainerFactory(
ObjectProvider<ReactivePulsarConsumerFactory<Object>> consumerFactoryProvider,
SchemaResolver schemaResolver, TopicResolver topicResolver) {
ReactivePulsarContainerProperties<Object> containerProperties = new ReactivePulsarContainerProperties<>();
containerProperties.setSchemaResolver(schemaResolver);
containerProperties.setTopicResolver(topicResolver);
containerProperties.setSubscriptionType(this.properties.getConsumer().getSubscriptionType());
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
PulsarReactiveProperties.Listener listenerProperties = this.properties.getListener();
map.from(listenerProperties::getSchemaType).to(containerProperties::setSchemaType);
map.from(listenerProperties::getHandlingTimeout).to(containerProperties::setHandlingTimeout);
map.from(listenerProperties::getUseKeyOrderedProcessing).to(containerProperties::setUseKeyOrderedProcessing);
return new DefaultReactivePulsarListenerContainerFactory<>(consumerFactoryProvider.getIfAvailable(),
containerProperties);
}
@Configuration(proxyBeanMethods = false)
@EnableReactivePulsar
@ConditionalOnMissingBean(
name = PulsarAnnotationSupportBeanNames.REACTIVE_PULSAR_LISTENER_ANNOTATION_PROCESSOR_BEAN_NAME)
static class EnableReactivePulsarConfiguration {
}
}

View File

@@ -1,123 +0,0 @@
/*
* Copyright 2022-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 org.apache.pulsar.client.api.PulsarClient;
import org.apache.pulsar.reactive.client.adapter.AdaptedReactivePulsarClientFactory;
import org.apache.pulsar.reactive.client.adapter.ProducerCacheProvider;
import org.apache.pulsar.reactive.client.api.ReactiveMessageSenderCache;
import org.apache.pulsar.reactive.client.api.ReactivePulsarClient;
import org.apache.pulsar.reactive.client.producercache.CaffeineProducerCacheProvider;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.pulsar.core.SchemaResolver;
import org.springframework.pulsar.core.TopicResolver;
import org.springframework.pulsar.reactive.core.DefaultReactivePulsarConsumerFactory;
import org.springframework.pulsar.reactive.core.DefaultReactivePulsarReaderFactory;
import org.springframework.pulsar.reactive.core.DefaultReactivePulsarSenderFactory;
import org.springframework.pulsar.reactive.core.ReactivePulsarConsumerFactory;
import org.springframework.pulsar.reactive.core.ReactivePulsarReaderFactory;
import org.springframework.pulsar.reactive.core.ReactivePulsarSenderFactory;
import org.springframework.pulsar.reactive.core.ReactivePulsarTemplate;
import com.github.benmanes.caffeine.cache.Caffeine;
/**
* {@link EnableAutoConfiguration Auto-configuration} for Apache Pulsar.
*
* @author Chris Bono
* @author Christophe Bornet
*/
@AutoConfiguration(after = PulsarAutoConfiguration.class)
@ConditionalOnClass({ ReactivePulsarTemplate.class, ReactivePulsarClient.class })
@EnableConfigurationProperties(PulsarReactiveProperties.class)
@Import({ PulsarReactiveAnnotationDrivenConfiguration.class })
public class PulsarReactiveAutoConfiguration {
private final PulsarReactiveProperties properties;
public PulsarReactiveAutoConfiguration(PulsarReactiveProperties properties) {
this.properties = properties;
}
@Bean
@ConditionalOnMissingBean
public ReactivePulsarClient pulsarReactivePulsarClient(PulsarClient pulsarClient) {
return AdaptedReactivePulsarClientFactory.create(pulsarClient);
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnClass(CaffeineProducerCacheProvider.class)
@ConditionalOnProperty(name = "spring.pulsar.reactive.sender.cache.enabled", havingValue = "true",
matchIfMissing = true)
public ProducerCacheProvider pulsarProducerCacheProvider() {
PulsarReactiveProperties.Cache cache = this.properties.getSender().getCache();
Caffeine<Object, Object> caffeine = Caffeine.newBuilder().expireAfterAccess(cache.getExpireAfterAccess())
.maximumSize(cache.getMaximumSize()).initialCapacity(cache.getInitialCapacity());
return new CaffeineProducerCacheProvider(caffeine);
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "spring.pulsar.reactive.sender.cache.enabled", havingValue = "true",
matchIfMissing = true)
public ReactiveMessageSenderCache pulsarReactiveMessageSenderCache(
ObjectProvider<ProducerCacheProvider> producerCacheProvider) {
return producerCacheProvider.stream().findFirst().map(AdaptedReactivePulsarClientFactory::createCache)
.orElseGet(AdaptedReactivePulsarClientFactory::createCache);
}
@Bean
@ConditionalOnMissingBean
public ReactivePulsarSenderFactory<?> reactivePulsarSenderFactory(ReactivePulsarClient pulsarReactivePulsarClient,
ObjectProvider<ReactiveMessageSenderCache> cache, TopicResolver topicResolver) {
return new DefaultReactivePulsarSenderFactory<>(pulsarReactivePulsarClient,
this.properties.buildReactiveMessageSenderSpec(), cache.getIfAvailable(), topicResolver);
}
@Bean
@ConditionalOnMissingBean
public ReactivePulsarConsumerFactory<?> reactivePulsarConsumerFactory(
ReactivePulsarClient pulsarReactivePulsarClient) {
return new DefaultReactivePulsarConsumerFactory<>(pulsarReactivePulsarClient,
this.properties.buildReactiveMessageConsumerSpec());
}
@Bean
@ConditionalOnMissingBean
public ReactivePulsarReaderFactory<?> reactivePulsarReaderFactory(ReactivePulsarClient pulsarReactivePulsarClient) {
return new DefaultReactivePulsarReaderFactory<>(pulsarReactivePulsarClient,
this.properties.buildReactiveMessageReaderSpec());
}
@Bean
@ConditionalOnMissingBean
public ReactivePulsarTemplate<?> pulsarReactiveTemplate(ReactivePulsarSenderFactory<?> reactivePulsarSenderFactory,
SchemaResolver schemaResolver, TopicResolver topicResolver) {
return new ReactivePulsarTemplate<>(reactivePulsarSenderFactory, schemaResolver, topicResolver);
}
}

View File

@@ -1,106 +0,0 @@
/*
* Copyright 2022-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 java.util.Arrays;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* Utility class to map Pulsar auth parameters to well-known keys.
*
* @author Alexander Preuß
*/
enum WellKnownAuthParameters {
TENANT_DOMAIN("tenantDomain"),
TENANT_SERVICE("tenantService"),
PROVIDER_DOMAIN("providerDomain"),
PRIVATE_KEY("privateKey"),
PRIVATE_KEY_PATH("privateKeyPath"),
KEY_ID("keyId"),
AUTO_PREFETCH_ENABLED("autoPrefetchEnabled"),
ATHENZ_CONF_PATH("athenzConfPath"),
PRINCIPAL_HEADER("principalHeader"),
ROLE_HEADER("roleHeader"),
ZTS_URL("ztsUrl"),
USER_ID("userId"),
PASSWORD("password"),
KEY_STORE_TYPE("keyStoreType"),
KEY_STORE_PATH("keyStorePath"),
KEY_STORE_PASSWORD("keyStorePassword"),
TYPE("type"),
ISSUER_URL("issuerUrl"),
AUDIENCE("audience"),
SCOPE("scope"),
SASL_JAAS_CLIENT_SECTION_NAME("saslJaasClientSectionName"),
SERVER_TYPE("serverType"),
TLS_CERT_FILE("tlsCertFile"),
TLS_KEY_FILE("tlsKeyFile"),
TOKEN("token");
private static final Map<String, String> LOWER_CASE_TO_CAMEL_CASE = Arrays.stream(values())
.map(WellKnownAuthParameters::getCamelCaseKey)
.collect(Collectors.toMap(String::toLowerCase, Function.identity()));
private final String camelCaseKey;
WellKnownAuthParameters(String camelCaseKey) {
this.camelCaseKey = camelCaseKey;
}
String getCamelCaseKey() {
return this.camelCaseKey;
}
/**
* Returns the camel-cased version a Pulsar auth parameter or the given key in case it
* is not part of the well-known ones.
* @param lowerCaseKey the lower-cased auth parameter
* @return the camel-cased auth parameter, or the lowerCaseKey if the parameter is not
* found.
*/
public static String toCamelCaseKey(String lowerCaseKey) {
return LOWER_CASE_TO_CAMEL_CASE.getOrDefault(lowerCaseKey, lowerCaseKey);
}
}

View File

@@ -1,11 +0,0 @@
/**
* Package containing the Spring Boot
* {@link org.springframework.boot.autoconfigure.AutoConfiguration} for the Spring for
* Apache Pulsar framework.
*/
@NonNullApi
@NonNullFields
package org.springframework.pulsar.autoconfigure;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

View File

@@ -1,24 +0,0 @@
{
"groups": [],
"properties": [
{
"name": "spring.pulsar.function.enabled",
"type": "java.lang.Boolean",
"description": "Whether to enable function support.",
"defaultValue": true
},
{
"name": "spring.pulsar.producer.cache.enabled",
"type": "java.lang.Boolean",
"description": "Whether to enable caching in the PulsarProducerFactory.",
"defaultValue": true
},
{
"name": "spring.pulsar.reactive.sender.cache.enabled",
"type": "java.lang.Boolean",
"description": "Whether to enable caching in the ReactivePulsarSenderFactory.",
"defaultValue": true
}
],
"hints": []
}

View File

@@ -1,2 +0,0 @@
org.springframework.pulsar.autoconfigure.PulsarAutoConfiguration
org.springframework.pulsar.autoconfigure.PulsarReactiveAutoConfiguration

View File

@@ -1,65 +0,0 @@
/*
* Copyright 2022-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.junit.jupiter.params.provider.Arguments.arguments;
import java.util.Collections;
import java.util.Map;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
/**
* Tests for {@link AuthParameterUtils}.
*
* @author Alexander Preuß
*/
public class AuthParameterUtilsTests {
@ParameterizedTest(name = "{0}")
@MethodSource("encodedParamStringConversionProvider")
void encodedParamStringConversion(String testName, Map<String, String> authParamsMap) {
String encodedAuthParamString = AuthParameterUtils.maybeConvertToEncodedParamString(authParamsMap);
if (authParamsMap == null || authParamsMap.isEmpty()) {
assertThat(encodedAuthParamString).isNull();
}
else {
assertThat(encodedAuthParamString).isEqualTo("{\"audience\":\"urn:sn:pulsar:abc:xyz\","
+ "\"issuerUrl\":\"https://auth.server.cloud\",\"privateKey\":\"file://Users/xyz/key.json\"}");
}
}
private static Stream<Arguments> encodedParamStringConversionProvider() {
return Stream.of(arguments("null", null), arguments("empty", Collections.emptyMap()),
arguments("camelCase",
Map.of("issuerUrl", "https://auth.server.cloud", "privateKey", "file://Users/xyz/key.json",
"audience", "urn:sn:pulsar:abc:xyz")),
arguments("kebabCase",
Map.of("issuer-url", "https://auth.server.cloud", "private-key", "file://Users/xyz/key.json",
"audience", "urn:sn:pulsar:abc:xyz")),
arguments("lowerCase",
Map.of("issuerurl", "https://auth.server.cloud", "privatekey", "file://Users/xyz/key.json",
"audience", "urn:sn:pulsar:abc:xyz")),
arguments("mixed", Map.of("issuerurl", "https://auth.server.cloud", "private-key",
"file://Users/xyz/key.json", "audience", "urn:sn:pulsar:abc:xyz")));
}
}

View File

@@ -1,570 +0,0 @@
/*
* Copyright 2022-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.entry;
import static org.mockito.Mockito.mock;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.apache.pulsar.client.api.PulsarClient;
import org.apache.pulsar.client.api.Schema;
import org.apache.pulsar.client.api.SubscriptionInitialPosition;
import org.apache.pulsar.client.api.SubscriptionType;
import org.apache.pulsar.client.api.interceptor.ProducerInterceptor;
import org.apache.pulsar.common.schema.KeyValueEncodingType;
import org.apache.pulsar.common.schema.SchemaType;
import org.assertj.core.api.AbstractObjectAssert;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.pulsar.annotation.EnablePulsar;
import org.springframework.pulsar.annotation.PulsarBootstrapConfiguration;
import org.springframework.pulsar.annotation.PulsarListenerAnnotationBeanPostProcessor;
import org.springframework.pulsar.config.ConcurrentPulsarListenerContainerFactory;
import org.springframework.pulsar.config.PulsarClientFactoryBean;
import org.springframework.pulsar.config.PulsarListenerContainerFactory;
import org.springframework.pulsar.config.PulsarListenerEndpointRegistry;
import org.springframework.pulsar.core.CachingPulsarProducerFactory;
import org.springframework.pulsar.core.DefaultPulsarProducerFactory;
import org.springframework.pulsar.core.DefaultPulsarReaderFactory;
import org.springframework.pulsar.core.DefaultSchemaResolver;
import org.springframework.pulsar.core.DefaultTopicResolver;
import org.springframework.pulsar.core.PulsarAdministration;
import org.springframework.pulsar.core.PulsarConsumerFactory;
import org.springframework.pulsar.core.PulsarProducerFactory;
import org.springframework.pulsar.core.PulsarReaderFactory;
import org.springframework.pulsar.core.PulsarTemplate;
import org.springframework.pulsar.core.SchemaResolver;
import org.springframework.pulsar.core.SchemaResolver.SchemaResolverCustomizer;
import org.springframework.pulsar.core.TopicResolver;
import org.springframework.pulsar.function.PulsarFunctionAdministration;
import org.springframework.pulsar.listener.AckMode;
import org.springframework.pulsar.listener.PulsarContainerProperties;
import com.github.benmanes.caffeine.cache.Caffeine;
/**
* Autoconfiguration tests for {@link PulsarAutoConfiguration}.
*
* @author Chris Bono
* @author Alexander Preuß
* @author Soby Chacko
*/
@SuppressWarnings("unchecked")
class PulsarAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(PulsarAutoConfiguration.class));
@Test
void autoConfigurationSkippedWhenPulsarTemplateNotOnClasspath() {
this.contextRunner.withClassLoader(new FilteredClassLoader(PulsarTemplate.class))
.run((context) -> assertThat(context).hasNotFailed().doesNotHaveBean(PulsarAutoConfiguration.class));
}
@Test
void annotationDrivenConfigurationSkippedWhenEnablePulsarAnnotationNotOnClasspath() {
this.contextRunner.withClassLoader(new FilteredClassLoader(EnablePulsar.class))
.run((context) -> assertThat(context).hasNotFailed()
.doesNotHaveBean(PulsarAnnotationDrivenConfiguration.class));
}
@Test
void bootstrapConfigurationSkippedWhenCustomPulsarListenerAnnotationProcessorDefined() {
this.contextRunner
.withBean("org.springframework.pulsar.config.internalPulsarListenerAnnotationProcessor", String.class,
() -> "someFauxBean")
.run((context) -> assertThat(context).hasNotFailed()
.doesNotHaveBean(PulsarBootstrapConfiguration.class));
}
@Test
void defaultBeansAreAutoConfigured() {
this.contextRunner.run((context) -> assertThat(context).hasNotFailed()
.hasSingleBean(PulsarClientFactoryBean.class).hasSingleBean(PulsarProducerFactory.class)
.hasSingleBean(PulsarTemplate.class).hasSingleBean(PulsarConsumerFactory.class)
.hasSingleBean(ConcurrentPulsarListenerContainerFactory.class)
.hasSingleBean(PulsarListenerAnnotationBeanPostProcessor.class)
.hasSingleBean(PulsarListenerEndpointRegistry.class).hasSingleBean(PulsarAdministration.class)
.hasSingleBean(DefaultSchemaResolver.class).hasSingleBean(DefaultTopicResolver.class));
}
@Test
void customPulsarClientFactoryBeanIsRespected() {
PulsarClientFactoryBean clientFactoryBean = new PulsarClientFactoryBean(
new PulsarProperties().buildClientProperties());
this.contextRunner
.withBean("customPulsarClientFactoryBean", PulsarClientFactoryBean.class, () -> clientFactoryBean)
.run((context) -> assertThat(context)
.getBean("&customPulsarClientFactoryBean", PulsarClientFactoryBean.class)
.isSameAs(clientFactoryBean));
}
@Test
void customSchemaResolverIsRespected() {
SchemaResolver customSchemaResolver = mock(SchemaResolver.class);
this.contextRunner.withBean("customSchemaResolver", SchemaResolver.class, () -> customSchemaResolver)
.run((context) -> assertThat(context).hasNotFailed().getBean(SchemaResolver.class)
.isSameAs(customSchemaResolver));
}
@Test
void defaultSchemaResolverCanBeCustomized() {
record Foo() {
}
SchemaResolverCustomizer<DefaultSchemaResolver> customizer = (sr) -> sr.addCustomSchemaMapping(Foo.class,
Schema.STRING);
this.contextRunner.withBean("schemaResolverCustomizer", SchemaResolverCustomizer.class, () -> customizer)
.run((context) -> assertThat(context).hasNotFailed().getBean(DefaultSchemaResolver.class)
.extracting(DefaultSchemaResolver::getCustomSchemaMappings, InstanceOfAssertFactories.MAP)
.containsEntry(Foo.class, Schema.STRING));
}
@Test
void customTopicResolverIsRespected() {
TopicResolver customTopicResolver = mock(TopicResolver.class);
this.contextRunner.withBean("customTopicResolver", TopicResolver.class, () -> customTopicResolver)
.run((context) -> assertThat(context).hasNotFailed().getBean(TopicResolver.class)
.isSameAs(customTopicResolver));
}
@Test
void customPulsarProducerFactoryIsRespected() {
PulsarProducerFactory<String> producerFactory = mock(PulsarProducerFactory.class);
this.contextRunner.withBean("customPulsarProducerFactory", PulsarProducerFactory.class, () -> producerFactory)
.run((context) -> assertThat(context).hasNotFailed().getBean(PulsarProducerFactory.class)
.isSameAs(producerFactory));
}
@Test
void customPulsarTemplateIsRespected() {
PulsarTemplate<String> template = mock(PulsarTemplate.class);
this.contextRunner.withBean("customPulsarTemplate", PulsarTemplate.class, () -> template)
.run((context) -> assertThat(context).hasNotFailed().getBean(PulsarTemplate.class).isSameAs(template));
}
@Test
void beansAreInjectedInPulsarTemplate() {
PulsarProducerFactory<?> producerFactory = mock(PulsarProducerFactory.class);
SchemaResolver schemaResolver = mock(SchemaResolver.class);
TopicResolver topicResolver = mock(TopicResolver.class);
this.contextRunner.withBean("customPulsarProducerFactory", PulsarProducerFactory.class, () -> producerFactory)
.withBean("schemaResolver", SchemaResolver.class, () -> schemaResolver)
.withBean("topicResolver", TopicResolver.class, () -> topicResolver)
.run((context -> assertThat(context).hasNotFailed().getBean(PulsarTemplate.class)
.hasFieldOrPropertyWithValue("producerFactory", producerFactory)
.hasFieldOrPropertyWithValue("schemaResolver", schemaResolver)
.hasFieldOrPropertyWithValue("topicResolver", topicResolver)));
}
@Test
void customPulsarConsumerFactoryIsRespected() {
PulsarConsumerFactory<String> consumerFactory = mock(PulsarConsumerFactory.class);
this.contextRunner.withBean("customPulsarConsumerFactory", PulsarConsumerFactory.class, () -> consumerFactory)
.run((context) -> assertThat(context).hasNotFailed().getBean(PulsarConsumerFactory.class)
.isSameAs(consumerFactory));
}
@Test
void pulsarConsumerFactoryWithEnumPropertyValue() {
this.contextRunner.withPropertyValues("spring.pulsar.consumer.subscription-initial-position=earliest")
.run((context -> assertThat(context).hasNotFailed().getBean(PulsarConsumerFactory.class)
.extracting("consumerConfig").hasFieldOrPropertyWithValue("subscriptionInitialPosition",
SubscriptionInitialPosition.Earliest)));
}
@Test
void customPulsarListenerContainerFactoryIsRespected() {
PulsarListenerContainerFactory listenerContainerFactory = mock(PulsarListenerContainerFactory.class);
this.contextRunner
.withBean("pulsarListenerContainerFactory", PulsarListenerContainerFactory.class,
() -> listenerContainerFactory)
.run((context) -> assertThat(context).hasNotFailed().getBean(PulsarListenerContainerFactory.class)
.isSameAs(listenerContainerFactory));
}
@Test
void beansAreInjectedInPulsarListenerContainerFactory() {
PulsarConsumerFactory<?> consumerFactory = mock(PulsarConsumerFactory.class);
SchemaResolver schemaResolver = mock(SchemaResolver.class);
TopicResolver topicResolver = mock(TopicResolver.class);
this.contextRunner.withBean("pulsarConsumerFactory", PulsarConsumerFactory.class, () -> consumerFactory)
.withBean("schemaResolver", SchemaResolver.class, () -> schemaResolver)
.withBean("topicResolver", TopicResolver.class, () -> topicResolver)
.run((context -> assertThat(context).hasNotFailed()
.getBean(ConcurrentPulsarListenerContainerFactory.class)
.hasFieldOrPropertyWithValue("consumerFactory", consumerFactory)
.extracting(ConcurrentPulsarListenerContainerFactory<Object>::getContainerProperties)
.hasFieldOrPropertyWithValue("schemaResolver", schemaResolver)
.hasFieldOrPropertyWithValue("topicResolver", topicResolver)));
}
@Test
void customPulsarListenerAnnotationBeanPostProcessorIsRespected() {
PulsarListenerAnnotationBeanPostProcessor<String> listenerAnnotationBeanPostProcessor = mock(
PulsarListenerAnnotationBeanPostProcessor.class);
this.contextRunner
.withBean("org.springframework.pulsar.config.internalPulsarListenerAnnotationProcessor",
PulsarListenerAnnotationBeanPostProcessor.class, () -> listenerAnnotationBeanPostProcessor)
.run((context) -> assertThat(context).hasNotFailed()
.getBean(PulsarListenerAnnotationBeanPostProcessor.class)
.isSameAs(listenerAnnotationBeanPostProcessor));
}
@Test
void customPulsarAdministrationIsRespected() {
PulsarAdministration pulsarAdministration = mock(PulsarAdministration.class);
this.contextRunner
.withBean("customPulsarAdministration", PulsarAdministration.class, () -> pulsarAdministration)
.run((context) -> assertThat(context).hasNotFailed().getBean(PulsarAdministration.class)
.isSameAs(pulsarAdministration));
}
@Test
void customProducerInterceptorIsUsedInPulsarTemplate() {
ProducerInterceptor interceptor = mock(ProducerInterceptor.class);
this.contextRunner.withBean("customProducerInterceptor", ProducerInterceptor.class, () -> interceptor)
.run((context -> assertThat(context).hasNotFailed().getBean(PulsarTemplate.class)
.extracting("interceptors")
.asInstanceOf(InstanceOfAssertFactories.list(ProducerInterceptor.class))
.contains(interceptor)));
}
@Test
void customProducerInterceptorsOrderedProperly() {
this.contextRunner.withUserConfiguration(InterceptorTestConfiguration.class)
.run((context -> assertThat(context).hasNotFailed().getBean(PulsarTemplate.class)
.extracting("interceptors")
.asInstanceOf(InstanceOfAssertFactories.list(ProducerInterceptor.class))
.containsExactly(InterceptorTestConfiguration.interceptorBar,
InterceptorTestConfiguration.interceptorFoo)));
}
@Test
void listenerPropertiesAreHonored() {
contextRunner
.withPropertyValues("spring.pulsar.listener.ack-mode=manual", "spring.pulsar.listener.schema-type=avro",
"spring.pulsar.listener.max-num-messages=10", "spring.pulsar.listener.max-num-bytes=101B",
"spring.pulsar.listener.batch-timeout=50ms", "spring.pulsar.consumer.subscription-type=shared")
.run((context -> {
AbstractObjectAssert<?, PulsarContainerProperties> properties = assertThat(context).hasNotFailed()
.getBean(ConcurrentPulsarListenerContainerFactory.class)
.extracting(ConcurrentPulsarListenerContainerFactory<Object>::getContainerProperties);
properties.extracting(PulsarContainerProperties::getAckMode).isEqualTo(AckMode.MANUAL);
properties.extracting(PulsarContainerProperties::getSchemaType).isEqualTo(SchemaType.AVRO);
properties.extracting(PulsarContainerProperties::getMaxNumMessages).isEqualTo(10);
properties.extracting(PulsarContainerProperties::getMaxNumBytes).isEqualTo(101);
properties.extracting(PulsarContainerProperties::getBatchTimeoutMillis).isEqualTo(50);
properties.extracting(PulsarContainerProperties::getSubscriptionType)
.isEqualTo(SubscriptionType.Shared);
}));
}
@Nested
class DefaultsTypeMappingsTests {
@Test
void topicMappingsAreAddedToTopicResolver() {
contextRunner
.withPropertyValues(
"spring.pulsar.defaults.type-mappings[0].message-type=%s".formatted(Foo.class.getName()),
"spring.pulsar.defaults.type-mappings[0].topic-name=foo-topic",
"spring.pulsar.defaults.type-mappings[1].message-type=%s".formatted(String.class.getName()),
"spring.pulsar.defaults.type-mappings[1].topic-name=string-topic")
.run((context -> assertThat(context).hasNotFailed().getBean(TopicResolver.class)
.asInstanceOf(InstanceOfAssertFactories.type(DefaultTopicResolver.class))
.extracting(DefaultTopicResolver::getCustomTopicMappings, InstanceOfAssertFactories.MAP)
.containsOnly(entry(Foo.class, "foo-topic"), entry(String.class, "string-topic"))));
}
@Test
void schemaMappingForPrimitiveIsAddedToSchemaResolver() {
contextRunner
.withPropertyValues(
"spring.pulsar.defaults.type-mappings[0].message-type=%s".formatted(Foo.class.getName()),
"spring.pulsar.defaults.type-mappings[0].schema-info.schema-type=STRING")
.run((context -> assertThat(context).hasNotFailed().getBean(SchemaResolver.class)
.asInstanceOf(InstanceOfAssertFactories.type(DefaultSchemaResolver.class))
.extracting(DefaultSchemaResolver::getCustomSchemaMappings, InstanceOfAssertFactories.MAP)
.containsOnly(entry(Foo.class, Schema.STRING))));
}
@Test
void schemaMappingForStructIsAddedToSchemaResolver() {
contextRunner
.withPropertyValues(
"spring.pulsar.defaults.type-mappings[0].message-type=%s".formatted(Foo.class.getName()),
"spring.pulsar.defaults.type-mappings[0].schema-info.schema-type=JSON")
.run((context -> assertThat(context).hasNotFailed().getBean(SchemaResolver.class)
.asInstanceOf(InstanceOfAssertFactories.type(DefaultSchemaResolver.class))
.extracting(DefaultSchemaResolver::getCustomSchemaMappings,
InstanceOfAssertFactories.map(Class.class, Schema.class))
.hasEntrySatisfying(Foo.class,
(schema) -> assertSchemaEquals(schema, Schema.JSON(Foo.class)))));
}
@Test
void schemaMappingForKeyValueIsAddedToSchemaResolver() {
contextRunner
.withPropertyValues(
"spring.pulsar.defaults.type-mappings[0].message-type=%s".formatted(Foo.class.getName()),
"spring.pulsar.defaults.type-mappings[0].schema-info.schema-type=%s"
.formatted(SchemaType.KEY_VALUE.name()),
"spring.pulsar.defaults.type-mappings[0].schema-info.message-key-type=%s"
.formatted(String.class.getName()))
.run((context -> assertThat(context).hasNotFailed().getBean(SchemaResolver.class)
.asInstanceOf(InstanceOfAssertFactories.type(DefaultSchemaResolver.class))
.extracting(DefaultSchemaResolver::getCustomSchemaMappings,
InstanceOfAssertFactories.map(Class.class, Schema.class))
.hasEntrySatisfying(Foo.class, (schema) -> assertSchemaEquals(schema, Schema
.KeyValue(Schema.STRING, Schema.JSON(Foo.class), KeyValueEncodingType.INLINE)))));
}
private void assertSchemaEquals(Schema<?> left, Schema<?> right) {
assertThat(left.getSchemaInfo()).isEqualTo(right.getSchemaInfo());
}
record Foo() {
}
}
@Nested
class ClientAutoConfigurationTests {
@Test
void authParamMapConvertedToEncodedParamString() {
contextRunner.withPropertyValues(
"spring.pulsar.client.auth-plugin-class-name=org.apache.pulsar.client.impl.auth.AuthenticationBasic",
"spring.pulsar.client.authentication.userId=username",
"spring.pulsar.client.authentication.password=topsecret")
.run((context -> assertThat(context).hasNotFailed().getBean(PulsarClientFactoryBean.class)
.extracting("config", InstanceOfAssertFactories.map(String.class, Object.class))
.doesNotContainKey("authParamMap").doesNotContainKey("userId").doesNotContainKey("password")
.containsEntry("authParams", "{\"password\":\"topsecret\",\"userId\":\"username\"}")));
}
}
@Nested
class FunctionAutoConfigurationTests {
@Test
void functionSupportEnabledByDefault() {
// NOTE: hasNoNullFieldsOrProperties() ensures object providers set
contextRunner.run(context -> assertThat(context).hasNotFailed().getBean(PulsarFunctionAdministration.class)
.hasFieldOrPropertyWithValue("failFast", Boolean.TRUE)
.hasFieldOrPropertyWithValue("propagateFailures", Boolean.TRUE)
.hasFieldOrPropertyWithValue("propagateStopFailures", Boolean.FALSE).hasNoNullFieldsOrProperties()
.extracting("pulsarAdministration").isSameAs(context.getBean(PulsarAdministration.class)));
}
@Test
void functionSupportCanBeConfigured() {
contextRunner
.withPropertyValues("spring.pulsar.function.fail-fast=false",
"spring.pulsar.function.propagate-failures=false",
"spring.pulsar.function.propagate-stop-failures=true")
.run(context -> assertThat(context).hasNotFailed().getBean(PulsarFunctionAdministration.class)
.hasFieldOrPropertyWithValue("failFast", Boolean.FALSE)
.hasFieldOrPropertyWithValue("propagateFailures", Boolean.FALSE)
.hasFieldOrPropertyWithValue("propagateStopFailures", Boolean.TRUE));
}
@Test
void functionSupportCanBeDisabled() {
contextRunner.withPropertyValues("spring.pulsar.function.enabled=false").run(
context -> assertThat(context).hasNotFailed().doesNotHaveBean(PulsarFunctionAdministration.class));
}
@Test
void customFunctionAdminIsRespected() {
PulsarFunctionAdministration customFunctionAdmin = mock(PulsarFunctionAdministration.class);
contextRunner.withBean(PulsarFunctionAdministration.class, () -> customFunctionAdmin)
.run(context -> assertThat(context).hasNotFailed().getBean(PulsarFunctionAdministration.class)
.isSameAs(customFunctionAdmin));
}
}
@Nested
class ObservationAutoConfigurationTests {
@Test
void templateObservationsEnabledByDefault() {
contextRunner.run((context -> assertThat(context).getBean(PulsarTemplate.class)
.hasFieldOrPropertyWithValue("observationEnabled", true)));
}
@Test
void templateObservationsEnabledExplicitly() {
contextRunner.withPropertyValues("spring.pulsar.template.observations-enabled=true")
.run((context -> assertThat(context).getBean(PulsarTemplate.class)
.hasFieldOrPropertyWithValue("observationEnabled", true)));
}
@Test
void templateObservationsCanBeDisabled() {
contextRunner.withPropertyValues("spring.pulsar.template.observations-enabled=false")
.run((context -> assertThat(context).getBean(PulsarTemplate.class)
.hasFieldOrPropertyWithValue("observationEnabled", false)));
}
@Test
void listenerObservationsEnabledByDefault() {
contextRunner.run((context -> assertThat(context).getBean(ConcurrentPulsarListenerContainerFactory.class)
.hasFieldOrPropertyWithValue("containerProperties.observationEnabled", true)));
}
@Test
void listenerObservationsEnabledExplicitly() {
contextRunner.withPropertyValues("spring.pulsar.listener.observations-enabled=true")
.run((context -> assertThat(context).getBean(ConcurrentPulsarListenerContainerFactory.class)
.hasFieldOrPropertyWithValue("containerProperties.observationEnabled", true)));
}
@Test
void listenerObservationsCanBeDisabled() {
contextRunner.withPropertyValues("spring.pulsar.listener.observations-enabled=false")
.run((context -> assertThat(context).getBean(ConcurrentPulsarListenerContainerFactory.class)
.hasFieldOrPropertyWithValue("containerProperties.observationEnabled", false)));
}
}
@Nested
class ProducerFactoryAutoConfigurationTests {
@Test
void cachingProducerFactoryEnabledByDefault() {
contextRunner.run((context) -> assertHasProducerFactoryOfType(CachingPulsarProducerFactory.class, context));
}
@Test
void nonCachingProducerFactoryCanBeEnabled() {
contextRunner.withPropertyValues("spring.pulsar.producer.cache.enabled=false")
.run((context -> assertHasProducerFactoryOfType(DefaultPulsarProducerFactory.class, context)));
}
@Test
void cachingProducerFactoryCanBeEnabled() {
contextRunner.withPropertyValues("spring.pulsar.producer.cache.enabled=true")
.run((context -> assertHasProducerFactoryOfType(CachingPulsarProducerFactory.class, context)));
}
@Test
void cachingEnabledAndCaffeineNotOnClasspath() {
contextRunner.withClassLoader(new FilteredClassLoader(Caffeine.class))
.withPropertyValues("spring.pulsar.producer.cache.enabled=true")
.run((context -> assertHasProducerFactoryOfType(CachingPulsarProducerFactory.class, context)));
}
@Test
void cachingProducerFactoryCanBeConfigured() {
contextRunner
.withPropertyValues("spring.pulsar.producer.cache.expire-after-access=100s",
"spring.pulsar.producer.cache.maximum-size=5150",
"spring.pulsar.producer.cache.initial-capacity=200")
.run((context -> assertThat(context).hasNotFailed().getBean(PulsarProducerFactory.class)
.extracting("producerCache.cache.cache").hasFieldOrPropertyWithValue("maximum", 5150L)
.hasFieldOrPropertyWithValue("expiresAfterAccessNanos", TimeUnit.SECONDS.toNanos(100))));
}
@Test
void beansAreInjectedInNonCachingProducerFactory() {
contextRunner.withPropertyValues("spring.pulsar.producer.cache.enabled=false")
.run((context -> assertThat(context).hasNotFailed().getBean(DefaultPulsarProducerFactory.class)
.hasFieldOrPropertyWithValue("pulsarClient", context.getBean(PulsarClient.class))
.hasFieldOrPropertyWithValue("topicResolver", context.getBean(TopicResolver.class))));
}
@Test
void beansAreInjectedInCachingProducerFactory() {
contextRunner.withPropertyValues("spring.pulsar.producer.cache.enabled=true")
.run((context -> assertThat(context).hasNotFailed().getBean(CachingPulsarProducerFactory.class)
.hasFieldOrPropertyWithValue("pulsarClient", context.getBean(PulsarClient.class))
.hasFieldOrPropertyWithValue("topicResolver", context.getBean(TopicResolver.class))));
}
private void assertHasProducerFactoryOfType(Class<?> producerFactoryType,
AssertableApplicationContext context) {
assertThat(context).hasNotFailed().hasSingleBean(PulsarProducerFactory.class)
.getBean(PulsarProducerFactory.class).isExactlyInstanceOf(producerFactoryType);
}
}
@Nested
class ReaderFactoryAutoConfigurationTests {
@Test
void readerFactoryIsAutoConfiguredByDefault() {
contextRunner.run((context) -> assertThat(context).hasNotFailed().hasSingleBean(PulsarReaderFactory.class)
.getBean(PulsarReaderFactory.class).isExactlyInstanceOf(DefaultPulsarReaderFactory.class));
}
@Test
void readerFactoryCanBeConfigured() {
contextRunner.withPropertyValues("spring.pulsar.reader.topic-names=foo",
"spring.pulsar.reader.receiver-queue-size=200", "spring.pulsar.reader.reader-name=test-reader",
"spring.pulsar.reader.subscription-name=test-subscription",
"spring.pulsar.reader.subscription-role-prefix=test-prefix",
"spring.pulsar.reader.read-compacted=true", "spring.pulsar.reader.reset-include-head=true")
.run((context -> assertThat(context).hasNotFailed().getBean(PulsarReaderFactory.class)
.extracting("readerConfig").hasFieldOrPropertyWithValue("topicNames", List.of("foo"))
.hasFieldOrPropertyWithValue("receiverQueueSize", 200)
.hasFieldOrPropertyWithValue("readerName", "test-reader")
.hasFieldOrPropertyWithValue("subscriptionName", "test-subscription")
.hasFieldOrPropertyWithValue("subscriptionRolePrefix", "test-prefix")
.hasFieldOrPropertyWithValue("readCompacted", true)
.hasFieldOrPropertyWithValue("resetIncludeHead", true)));
}
}
@Configuration(proxyBeanMethods = false)
static class InterceptorTestConfiguration {
static ProducerInterceptor interceptorFoo = mock(ProducerInterceptor.class);
static ProducerInterceptor interceptorBar = mock(ProducerInterceptor.class);
@Bean
@Order(200)
ProducerInterceptor interceptorFoo() {
return interceptorFoo;
}
@Bean
@Order(100)
ProducerInterceptor interceptorBar() {
return interceptorBar;
}
}
}

View File

@@ -1,606 +0,0 @@
/*
* Copyright 2022-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.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.assertj.core.api.Assertions.assertThatRuntimeException;
import java.util.HashMap;
import java.util.Map;
import org.apache.pulsar.client.admin.PulsarAdmin;
import org.apache.pulsar.client.api.CompressionType;
import org.apache.pulsar.client.api.ConsumerCryptoFailureAction;
import org.apache.pulsar.client.api.DeadLetterPolicy;
import org.apache.pulsar.client.api.HashingScheme;
import org.apache.pulsar.client.api.MessageRoutingMode;
import org.apache.pulsar.client.api.ProducerAccessMode;
import org.apache.pulsar.client.api.ProducerCryptoFailureAction;
import org.apache.pulsar.client.api.ProxyProtocol;
import org.apache.pulsar.client.api.PulsarClient;
import org.apache.pulsar.client.api.RegexSubscriptionMode;
import org.apache.pulsar.client.api.SubscriptionInitialPosition;
import org.apache.pulsar.client.api.SubscriptionMode;
import org.apache.pulsar.client.api.SubscriptionType;
import org.apache.pulsar.client.impl.conf.ConfigurationDataUtils;
import org.apache.pulsar.client.impl.conf.ConsumerConfigurationData;
import org.apache.pulsar.client.impl.conf.ProducerConfigurationData;
import org.apache.pulsar.client.impl.conf.ReaderConfigurationData;
import org.apache.pulsar.common.schema.SchemaType;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.boot.context.properties.bind.BindException;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
import org.springframework.pulsar.autoconfigure.PulsarProperties.SchemaInfo;
import org.springframework.pulsar.autoconfigure.PulsarProperties.TypeMapping;
/**
* Unit tests for {@link PulsarProperties}.
*
* @author Chris Bono
* @author Christophe Bornet
* @author Soby Chacko
*/
public class PulsarPropertiesTests {
private final PulsarProperties properties = new PulsarProperties();
private void bind(Map<String, String> map) {
ConfigurationPropertySource source = new MapConfigurationPropertySource(map);
new Binder(source).bind("spring.pulsar", Bindable.ofInstance(this.properties));
}
@Nested
class ClientPropertiesTests {
private final String authPluginClassName = "org.apache.pulsar.client.impl.auth.AuthenticationToken";
private final String authParamsStr = "{\"token\":\"1234\"}";
private final String authToken = "1234";
@Test
void clientProperties() {
Map<String, String> props = new HashMap<>();
props.put("spring.pulsar.client.service-url", "my-service-url");
props.put("spring.pulsar.client.listener-name", "my-listener");
props.put("spring.pulsar.client.operation-timeout", "1s");
props.put("spring.pulsar.client.lookup-timeout", "2s");
props.put("spring.pulsar.client.num-io-threads", "3");
props.put("spring.pulsar.client.num-listener-threads", "4");
props.put("spring.pulsar.client.num-connections-per-broker", "5");
props.put("spring.pulsar.client.use-tcp-no-delay", "false");
props.put("spring.pulsar.client.use-tls", "true");
props.put("spring.pulsar.client.tls-hostname-verification-enable", "true");
props.put("spring.pulsar.client.tls-trust-certs-file-path", "my-trust-certs-file-path");
props.put("spring.pulsar.client.tls-allow-insecure-connection", "true");
props.put("spring.pulsar.client.use-key-store-tls", "true");
props.put("spring.pulsar.client.ssl-provider", "my-ssl-provider");
props.put("spring.pulsar.client.tls-trust-store-type", "my-trust-store-type");
props.put("spring.pulsar.client.tls-trust-store-path", "my-trust-store-path");
props.put("spring.pulsar.client.tls-trust-store-password", "my-trust-store-password");
props.put("spring.pulsar.client.tls-ciphers[0]", "my-tls-cipher");
props.put("spring.pulsar.client.tls-protocols[0]", "my-tls-protocol");
props.put("spring.pulsar.client.stats-interval", "6s");
props.put("spring.pulsar.client.max-concurrent-lookup-request", "7");
props.put("spring.pulsar.client.max-lookup-request", "8");
props.put("spring.pulsar.client.max-lookup-redirects", "9");
props.put("spring.pulsar.client.max-number-of-rejected-request-per-connection", "10");
props.put("spring.pulsar.client.keep-alive-interval", "11s");
props.put("spring.pulsar.client.connection-timeout", "12s");
props.put("spring.pulsar.client.request-timeout", "13s");
props.put("spring.pulsar.client.initial-backoff-interval", "14s");
props.put("spring.pulsar.client.max-backoff-interval", "15s");
props.put("spring.pulsar.client.enable-busy-wait", "true");
props.put("spring.pulsar.client.memory-limit", "16B");
props.put("spring.pulsar.client.proxy-service-url", "my-proxy-service-url");
props.put("spring.pulsar.client.proxy-protocol", "sni");
props.put("spring.pulsar.client.enable-transaction", "true");
props.put("spring.pulsar.client.dns-lookup-bind-address", "my-dns-lookup-bind-address");
props.put("spring.pulsar.client.dns-lookup-bind-port", "17");
props.put("spring.pulsar.client.socks5-proxy-address", "my-socks5-proxy-address");
props.put("spring.pulsar.client.socks5-proxy-username", "my-socks5-proxy-username");
props.put("spring.pulsar.client.socks5-proxy-password", "my-socks5-proxy-password");
bind(props);
Map<String, Object> clientProps = properties.buildClientProperties();
// Verify that the props can be loaded in a ClientBuilder
assertThatNoException().isThrownBy(() -> PulsarClient.builder().loadConf(clientProps));
assertThat(clientProps).containsEntry("serviceUrl", "my-service-url")
.containsEntry("listenerName", "my-listener").containsEntry("operationTimeoutMs", 1_000L)
.containsEntry("lookupTimeoutMs", 2_000L).containsEntry("numIoThreads", 3)
.containsEntry("numListenerThreads", 4).containsEntry("connectionsPerBroker", 5)
.containsEntry("useTcpNoDelay", false).containsEntry("useTls", true)
.containsEntry("tlsHostnameVerificationEnable", true)
.containsEntry("tlsTrustCertsFilePath", "my-trust-certs-file-path")
.containsEntry("tlsAllowInsecureConnection", true).containsEntry("useKeyStoreTls", true)
.containsEntry("sslProvider", "my-ssl-provider")
.containsEntry("tlsTrustStoreType", "my-trust-store-type")
.containsEntry("tlsTrustStorePath", "my-trust-store-path")
.containsEntry("tlsTrustStorePassword", "my-trust-store-password")
.hasEntrySatisfying("tlsCiphers",
ciphers -> assertThat(ciphers)
.asInstanceOf(InstanceOfAssertFactories.collection(String.class))
.containsExactly("my-tls-cipher"))
.hasEntrySatisfying("tlsProtocols",
protocols -> assertThat(protocols)
.asInstanceOf(InstanceOfAssertFactories.collection(String.class))
.containsExactly("my-tls-protocol"))
.containsEntry("statsIntervalSeconds", 6L).containsEntry("concurrentLookupRequest", 7)
.containsEntry("maxLookupRequest", 8).containsEntry("maxLookupRedirects", 9)
.containsEntry("maxNumberOfRejectedRequestPerConnection", 10)
.containsEntry("keepAliveIntervalSeconds", 11).containsEntry("connectionTimeoutMs", 12_000)
.containsEntry("requestTimeoutMs", 13_000)
.containsEntry("initialBackoffIntervalNanos", 14_000_000_000L)
.containsEntry("maxBackoffIntervalNanos", 15_000_000_000L).containsEntry("enableBusyWait", true)
.containsEntry("memoryLimitBytes", 16L).containsEntry("proxyServiceUrl", "my-proxy-service-url")
.containsEntry("proxyProtocol", ProxyProtocol.SNI).containsEntry("enableTransaction", true)
.containsEntry("dnsLookupBindAddress", "my-dns-lookup-bind-address")
.containsEntry("dnsLookupBindPort", 17)
.containsEntry("socks5ProxyAddress", "my-socks5-proxy-address")
.containsEntry("socks5ProxyUsername", "my-socks5-proxy-username")
.containsEntry("socks5ProxyPassword", "my-socks5-proxy-password");
}
@Test
void authenticationUsingAuthParamsString() {
Map<String, String> props = new HashMap<>();
props.put("spring.pulsar.client.auth-plugin-class-name",
"org.apache.pulsar.client.impl.auth.AuthenticationToken");
props.put("spring.pulsar.client.auth-params", authParamsStr);
bind(props);
assertThat(properties.getClient().getAuthParams()).isEqualTo(authParamsStr);
assertThat(properties.getClient().getAuthPluginClassName()).isEqualTo(authPluginClassName);
Map<String, Object> clientProps = properties.buildClientProperties();
assertThat(clientProps).containsEntry("authPluginClassName", authPluginClassName)
.containsEntry("authParams", authParamsStr);
}
@Test
void authenticationUsingAuthenticationMap() {
Map<String, String> props = new HashMap<>();
props.put("spring.pulsar.client.auth-plugin-class-name", authPluginClassName);
props.put("spring.pulsar.client.authentication.token", authToken);
bind(props);
assertThat(properties.getClient().getAuthentication()).containsEntry("token", authToken);
assertThat(properties.getClient().getAuthPluginClassName()).isEqualTo(authPluginClassName);
Map<String, Object> clientProps = properties.buildClientProperties();
assertThat(clientProps).containsEntry("authPluginClassName", authPluginClassName)
.containsEntry("authParams", authParamsStr);
}
@Test
void authenticationNotAllowedUsingBothAuthParamsStringAndAuthenticationMap() {
Map<String, String> props = new HashMap<>();
props.put("spring.pulsar.client.auth-plugin-class-name", authPluginClassName);
props.put("spring.pulsar.client.auth-params", authParamsStr);
props.put("spring.pulsar.client.authentication.token", authToken);
bind(props);
assertThatIllegalArgumentException().isThrownBy(properties::buildClientProperties).withMessageContaining(
"Cannot set both spring.pulsar.client.authParams and spring.pulsar.client.authentication.*");
}
}
@Nested
class AdminPropertiesTests {
private final String authPluginClassName = "org.apache.pulsar.client.impl.auth.AuthenticationToken";
private final String authParamsStr = "{\"token\":\"1234\"}";
private final String authToken = "1234";
@Test
void adminProperties() {
Map<String, String> props = new HashMap<>();
props.put("spring.pulsar.administration.service-url", "my-service-url");
props.put("spring.pulsar.administration.connection-timeout", "12s");
props.put("spring.pulsar.administration.read-timeout", "13s");
props.put("spring.pulsar.administration.request-timeout", "14s");
props.put("spring.pulsar.administration.auto-cert-refresh-time", "15s");
props.put("spring.pulsar.administration.tls-hostname-verification-enable", "true");
props.put("spring.pulsar.administration.tls-trust-certs-file-path", "my-trust-certs-file-path");
props.put("spring.pulsar.administration.tls-allow-insecure-connection", "true");
props.put("spring.pulsar.administration.use-key-store-tls", "true");
props.put("spring.pulsar.administration.ssl-provider", "my-ssl-provider");
props.put("spring.pulsar.administration.tls-trust-store-type", "my-trust-store-type");
props.put("spring.pulsar.administration.tls-trust-store-path", "my-trust-store-path");
props.put("spring.pulsar.administration.tls-trust-store-password", "my-trust-store-password");
props.put("spring.pulsar.administration.tls-ciphers[0]", "my-tls-cipher");
props.put("spring.pulsar.administration.tls-protocols[0]", "my-tls-protocol");
bind(props);
Map<String, Object> adminProps = properties.buildAdminProperties();
// Verify that the props can NOT be loaded directly via a ClientBuilder due to
// the
// unknown readTimeout and autoCertRefreshTime properties
assertThatRuntimeException().isThrownBy(() -> PulsarAdmin.builder().loadConf(adminProps)).havingCause()
.withMessageContaining("Unrecognized field \"autoCertRefreshSeconds\"");
assertThat(adminProps).containsEntry("serviceUrl", "my-service-url")
.containsEntry("connectionTimeoutMs", 12_000).containsEntry("readTimeoutMs", 13_000)
.containsEntry("requestTimeoutMs", 14_000).containsEntry("autoCertRefreshSeconds", 15)
.containsEntry("tlsHostnameVerificationEnable", true)
.containsEntry("tlsTrustCertsFilePath", "my-trust-certs-file-path")
.containsEntry("tlsAllowInsecureConnection", true).containsEntry("useKeyStoreTls", true)
.containsEntry("sslProvider", "my-ssl-provider")
.containsEntry("tlsTrustStoreType", "my-trust-store-type")
.containsEntry("tlsTrustStorePath", "my-trust-store-path")
.containsEntry("tlsTrustStorePassword", "my-trust-store-password")
.hasEntrySatisfying("tlsCiphers",
ciphers -> assertThat(ciphers)
.asInstanceOf(InstanceOfAssertFactories.collection(String.class))
.containsExactly("my-tls-cipher"))
.hasEntrySatisfying("tlsProtocols",
protocols -> assertThat(protocols)
.asInstanceOf(InstanceOfAssertFactories.collection(String.class))
.containsExactly("my-tls-protocol"));
}
@Test
void authenticationUsingAuthParamsString() {
Map<String, String> props = new HashMap<>();
props.put("spring.pulsar.administration.auth-plugin-class-name",
"org.apache.pulsar.client.impl.auth.AuthenticationToken");
props.put("spring.pulsar.administration.auth-params", authParamsStr);
bind(props);
assertThat(properties.getAdministration().getAuthParams()).isEqualTo(authParamsStr);
assertThat(properties.getAdministration().getAuthPluginClassName()).isEqualTo(authPluginClassName);
Map<String, Object> adminProps = properties.buildAdminProperties();
assertThat(adminProps).containsEntry("authPluginClassName", authPluginClassName).containsEntry("authParams",
authParamsStr);
}
@Test
void authenticationUsingAuthenticationMap() {
Map<String, String> props = new HashMap<>();
props.put("spring.pulsar.administration.auth-plugin-class-name", authPluginClassName);
props.put("spring.pulsar.administration.authentication.token", authToken);
bind(props);
assertThat(properties.getAdministration().getAuthentication()).containsEntry("token", authToken);
assertThat(properties.getAdministration().getAuthPluginClassName()).isEqualTo(authPluginClassName);
Map<String, Object> adminProps = properties.buildAdminProperties();
assertThat(adminProps).containsEntry("authPluginClassName", authPluginClassName).containsEntry("authParams",
authParamsStr);
}
@Test
void authenticationNotAllowedUsingBothAuthParamsStringAndAuthenticationMap() {
Map<String, String> props = new HashMap<>();
props.put("spring.pulsar.administration.auth-plugin-class-name", authPluginClassName);
props.put("spring.pulsar.administration.auth-params", authParamsStr);
props.put("spring.pulsar.administration.authentication.token", authToken);
bind(props);
assertThatIllegalArgumentException().isThrownBy(properties::buildAdminProperties).withMessageContaining(
"Cannot set both spring.pulsar.administration.authParams and spring.pulsar.administration.authentication.*");
}
}
@Nested
class DefaultsTypeMappingsPropertiesTests {
@Test
void emptyByDefault() {
assertThat(properties.getDefaults().getTypeMappings()).isEmpty();
}
@Test
void withTopicsOnly() {
Map<String, String> props = new HashMap<>();
props.put("spring.pulsar.defaults.type-mappings[0].message-type", Foo.class.getName());
props.put("spring.pulsar.defaults.type-mappings[0].topic-name", "foo-topic");
props.put("spring.pulsar.defaults.type-mappings[1].message-type", String.class.getName());
props.put("spring.pulsar.defaults.type-mappings[1].topic-name", "string-topic");
bind(props);
assertThat(properties.getDefaults().getTypeMappings()).containsExactly(
new TypeMapping(Foo.class, "foo-topic", null), new TypeMapping(String.class, "string-topic", null));
}
@Test
void withSchemaOnly() {
Map<String, String> props = new HashMap<>();
props.put("spring.pulsar.defaults.type-mappings[0].message-type", Foo.class.getName());
props.put("spring.pulsar.defaults.type-mappings[0].schema-info.schema-type", "JSON");
bind(props);
assertThat(properties.getDefaults().getTypeMappings())
.containsExactly(new TypeMapping(Foo.class, null, new SchemaInfo(SchemaType.JSON, null)));
}
@Test
void withTopicAndSchema() {
Map<String, String> props = new HashMap<>();
props.put("spring.pulsar.defaults.type-mappings[0].message-type", Foo.class.getName());
props.put("spring.pulsar.defaults.type-mappings[0].topic-name", "foo-topic");
props.put("spring.pulsar.defaults.type-mappings[0].schema-info.schema-type", "JSON");
bind(props);
assertThat(properties.getDefaults().getTypeMappings())
.containsExactly(new TypeMapping(Foo.class, "foo-topic", new SchemaInfo(SchemaType.JSON, null)));
}
@Test
void withKeyValueSchema() {
Map<String, String> props = new HashMap<>();
props.put("spring.pulsar.defaults.type-mappings[0].message-type", Foo.class.getName());
props.put("spring.pulsar.defaults.type-mappings[0].schema-info.schema-type", "KEY_VALUE");
props.put("spring.pulsar.defaults.type-mappings[0].schema-info.message-key-type", String.class.getName());
bind(props);
assertThat(properties.getDefaults().getTypeMappings()).containsExactly(
new TypeMapping(Foo.class, null, new SchemaInfo(SchemaType.KEY_VALUE, String.class)));
}
@Test
void schemaTypeRequired() {
Map<String, String> props = new HashMap<>();
props.put("spring.pulsar.defaults.type-mappings[0].message-type", Foo.class.getName());
props.put("spring.pulsar.defaults.type-mappings[0].schema-info.message-key-type", String.class.getName());
assertThatExceptionOfType(BindException.class).isThrownBy(() -> bind(props)).havingRootCause()
.withMessageContaining("schemaType must not be null");
}
@Test
void schemaTypeNoneNotAllowed() {
Map<String, String> props = new HashMap<>();
props.put("spring.pulsar.defaults.type-mappings[0].message-type", Foo.class.getName());
props.put("spring.pulsar.defaults.type-mappings[0].schema-info.schema-type", "NONE");
assertThatExceptionOfType(BindException.class).isThrownBy(() -> bind(props)).havingRootCause()
.withMessageContaining("schemaType NONE not supported");
}
@Test
void messageKeyTypeOnlyAllowedForKeyValueSchemaType() {
Map<String, String> props = new HashMap<>();
props.put("spring.pulsar.defaults.type-mappings[0].message-type", Foo.class.getName());
props.put("spring.pulsar.defaults.type-mappings[0].schema-info.schema-type", "JSON");
props.put("spring.pulsar.defaults.type-mappings[0].schema-info.message-key-type", String.class.getName());
assertThatExceptionOfType(BindException.class).isThrownBy(() -> bind(props)).havingRootCause()
.withMessageContaining("messageKeyType can only be set when schemaType is KEY_VALUE");
}
record Foo(String value) {
}
}
@Nested
class ProducerPropertiesTests {
@Test
void producerProperties() {
Map<String, String> props = new HashMap<>();
props.put("spring.pulsar.producer.topic-name", "my-topic");
props.put("spring.pulsar.producer.producer-name", "my-producer");
props.put("spring.pulsar.producer.send-timeout", "2s");
props.put("spring.pulsar.producer.block-if-queue-full", "true");
props.put("spring.pulsar.producer.max-pending-messages", "3");
props.put("spring.pulsar.producer.max-pending-messages-across-partitions", "4");
props.put("spring.pulsar.producer.message-routing-mode", "custompartition");
props.put("spring.pulsar.producer.hashing-scheme", "murmur3_32hash");
props.put("spring.pulsar.producer.crypto-failure-action", "send");
props.put("spring.pulsar.producer.batching-max-publish-delay", "5s");
props.put("spring.pulsar.producer.batching-partition-switch-frequency-by-publish-delay", "6");
props.put("spring.pulsar.producer.batching-max-messages", "7");
props.put("spring.pulsar.producer.batching-max-bytes", "8");
props.put("spring.pulsar.producer.batching-enabled", "false");
props.put("spring.pulsar.producer.chunking-enabled", "true");
props.put("spring.pulsar.producer.encryption-keys[0]", "my-key");
props.put("spring.pulsar.producer.compression-type", "lz4");
props.put("spring.pulsar.producer.initial-sequence-id", "9");
props.put("spring.pulsar.producer.producer-access-mode", "exclusive");
props.put("spring.pulsar.producer.lazy-start=partitioned-producers", "true");
props.put("spring.pulsar.producer.properties[my-prop]", "my-prop-value");
bind(props);
Map<String, Object> producerProps = properties.buildProducerProperties();
// Verify that the props can be loaded in a ProducerBuilder
assertThatNoException().isThrownBy(() -> ConfigurationDataUtils.loadData(producerProps,
new ProducerConfigurationData(), ProducerConfigurationData.class));
assertThat(producerProps).containsEntry("topicName", "my-topic")
.containsEntry("producerName", "my-producer").containsEntry("sendTimeoutMs", 2_000)
.containsEntry("blockIfQueueFull", true).containsEntry("maxPendingMessages", 3)
.containsEntry("maxPendingMessagesAcrossPartitions", 4)
.containsEntry("messageRoutingMode", MessageRoutingMode.CustomPartition)
.containsEntry("hashingScheme", HashingScheme.Murmur3_32Hash)
.containsEntry("cryptoFailureAction", ProducerCryptoFailureAction.SEND)
.containsEntry("batchingMaxPublishDelayMicros", 5_000_000L)
.containsEntry("batchingPartitionSwitchFrequencyByPublishDelay", 6)
.containsEntry("batchingMaxMessages", 7).containsEntry("batchingMaxBytes", 8)
.containsEntry("batchingEnabled", false).containsEntry("chunkingEnabled", true)
.hasEntrySatisfying("encryptionKeys",
keys -> assertThat(keys).asInstanceOf(InstanceOfAssertFactories.collection(String.class))
.containsExactly("my-key"))
.containsEntry("compressionType", CompressionType.LZ4).containsEntry("initialSequenceId", 9L)
.containsEntry("accessMode", ProducerAccessMode.Exclusive)
.containsEntry("lazyStartPartitionedProducers", true).hasEntrySatisfying("properties",
properties -> assertThat(properties)
.asInstanceOf(InstanceOfAssertFactories.map(String.class, String.class))
.containsEntry("my-prop", "my-prop-value"));
}
}
@Nested
class ConsumerPropertiesTests {
@Test
void consumerProperties() {
Map<String, String> props = new HashMap<>();
props.put("spring.pulsar.consumer.topics[0]", "my-topic");
props.put("spring.pulsar.consumer.topics-pattern", "my-pattern");
props.put("spring.pulsar.consumer.subscription-name", "my-subscription");
props.put("spring.pulsar.consumer.subscription-type", "shared");
props.put("spring.pulsar.consumer.subscription-properties[my-sub-prop]", "my-sub-prop-value");
props.put("spring.pulsar.consumer.subscription-mode", "nondurable");
props.put("spring.pulsar.consumer.receiver-queue-size", "1");
props.put("spring.pulsar.consumer.acknowledgements-group-time", "2s");
props.put("spring.pulsar.consumer.negative-ack-redelivery-delay", "3s");
props.put("spring.pulsar.consumer.max-total-receiver-queue-size-across-partitions", "5");
props.put("spring.pulsar.consumer.consumer-name", "my-consumer");
props.put("spring.pulsar.consumer.ack-timeout", "6s");
props.put("spring.pulsar.consumer.tick-duration", "7s");
props.put("spring.pulsar.consumer.priority-level", "8");
props.put("spring.pulsar.consumer.crypto-failure-action", "discard");
props.put("spring.pulsar.consumer.properties[my-prop]", "my-prop-value");
props.put("spring.pulsar.consumer.read-compacted", "true");
props.put("spring.pulsar.consumer.subscription-initial-position", "earliest");
props.put("spring.pulsar.consumer.pattern-auto-discovery-period", "9");
props.put("spring.pulsar.consumer.regex-subscription-mode", "all-topics");
props.put("spring.pulsar.consumer.dead-letter-policy.max-redeliver-count", "4");
props.put("spring.pulsar.consumer.dead-letter-policy.retry-letter-topic", "my-retry-topic");
props.put("spring.pulsar.consumer.dead-letter-policy.dead-letter-topic", "my-dlt-topic");
props.put("spring.pulsar.consumer.dead-letter-policy.initial-subscription-name", "my-initial-subscription");
props.put("spring.pulsar.consumer.retry-enable", "true");
props.put("spring.pulsar.consumer.auto-update-partitions", "false");
props.put("spring.pulsar.consumer.auto-update-partitions-interval", "10s");
props.put("spring.pulsar.consumer.replicate-subscription-state", "true");
props.put("spring.pulsar.consumer.reset-include-head", "true");
props.put("spring.pulsar.consumer.batch-index-ack-enabled", "true");
props.put("spring.pulsar.consumer.ack-receipt-enabled", "true");
props.put("spring.pulsar.consumer.pool-messages", "true");
props.put("spring.pulsar.consumer.start-paused", "true");
props.put("spring.pulsar.consumer.auto-ack-oldest-chunked-message-on-queue-full", "false");
props.put("spring.pulsar.consumer.max-pending-chunked-message", "11");
props.put("spring.pulsar.consumer.expire-time-of-incomplete-chunked-message", "12s");
bind(props);
Map<String, Object> consumerProps = properties.buildConsumerProperties();
// Verify that the props can be loaded in a ConsumerBuilder
assertThatNoException().isThrownBy(() -> ConfigurationDataUtils.loadData(consumerProps,
new ConsumerConfigurationData<>(), ConsumerConfigurationData.class));
assertThat(consumerProps)
.hasEntrySatisfying("topicNames",
topics -> assertThat(topics)
.asInstanceOf(InstanceOfAssertFactories.collection(String.class))
.containsExactly("my-topic"))
.hasEntrySatisfying("topicsPattern", p -> assertThat(p.toString()).isEqualTo("my-pattern"))
.containsEntry("subscriptionName", "my-subscription")
.containsEntry("subscriptionType", SubscriptionType.Shared)
.hasEntrySatisfying("subscriptionProperties",
properties -> assertThat(properties)
.asInstanceOf(InstanceOfAssertFactories.map(String.class, String.class))
.containsEntry("my-sub-prop", "my-sub-prop-value"))
.containsEntry("subscriptionMode", SubscriptionMode.NonDurable)
.containsEntry("receiverQueueSize", 1).containsEntry("acknowledgementsGroupTimeMicros", 2_000_000L)
.containsEntry("negativeAckRedeliveryDelayMicros", 3_000_000L)
.containsEntry("maxTotalReceiverQueueSizeAcrossPartitions", 5)
.containsEntry("consumerName", "my-consumer").containsEntry("ackTimeoutMillis", 6_000L)
.containsEntry("tickDurationMillis", 7_000L).containsEntry("priorityLevel", 8)
.containsEntry("cryptoFailureAction", ConsumerCryptoFailureAction.DISCARD)
.hasEntrySatisfying("properties",
properties -> assertThat(properties)
.asInstanceOf(InstanceOfAssertFactories.map(String.class, String.class))
.containsEntry("my-prop", "my-prop-value"))
.containsEntry("readCompacted", true)
.containsEntry("subscriptionInitialPosition", SubscriptionInitialPosition.Earliest)
.containsEntry("patternAutoDiscoveryPeriod", 9)
.containsEntry("regexSubscriptionMode", RegexSubscriptionMode.AllTopics)
.hasEntrySatisfying("deadLetterPolicy", dlp -> {
DeadLetterPolicy deadLetterPolicy = (DeadLetterPolicy) dlp;
assertThat(deadLetterPolicy.getMaxRedeliverCount()).isEqualTo(4);
assertThat(deadLetterPolicy.getRetryLetterTopic()).isEqualTo("my-retry-topic");
assertThat(deadLetterPolicy.getDeadLetterTopic()).isEqualTo("my-dlt-topic");
assertThat(deadLetterPolicy.getInitialSubscriptionName()).isEqualTo("my-initial-subscription");
}).containsEntry("retryEnable", true).containsEntry("autoUpdatePartitions", false)
.containsEntry("autoUpdatePartitionsIntervalSeconds", 10L)
.containsEntry("replicateSubscriptionState", true).containsEntry("resetIncludeHead", true)
.containsEntry("batchIndexAckEnabled", true).containsEntry("ackReceiptEnabled", true)
.containsEntry("poolMessages", true).containsEntry("startPaused", true)
.containsEntry("autoAckOldestChunkedMessageOnQueueFull", false)
.containsEntry("maxPendingChunkedMessage", 11)
.containsEntry("expireTimeOfIncompleteChunkedMessageMillis", 12_000L);
}
}
@Nested
class FunctionPropertiesTests {
@Test
void functionProperties() {
Map<String, String> props = new HashMap<>();
bind(props);
// check defaults
assertThat(properties.getFunction().getFailFast()).isTrue();
assertThat(properties.getFunction().getPropagateFailures()).isTrue();
assertThat(properties.getFunction().getPropagateStopFailures()).isFalse();
// set values and verify
props.put("spring.pulsar.function.fail-fast", "false");
props.put("spring.pulsar.function.propagate-failures", "false");
props.put("spring.pulsar.function.propagate-stop-failures", "true");
bind(props);
assertThat(properties.getFunction().getFailFast()).isFalse();
assertThat(properties.getFunction().getPropagateFailures()).isFalse();
assertThat(properties.getFunction().getPropagateStopFailures()).isTrue();
}
}
@Nested
class ReaderPropertiesTests {
@Test
void readerProperties() {
Map<String, String> props = new HashMap<>();
props.put("spring.pulsar.reader.topic-names", "my-topic");
props.put("spring.pulsar.reader.receiver-queue-size", "100");
props.put("spring.pulsar.reader.reader-name", "my-reader");
props.put("spring.pulsar.reader.subscription-name", "my-subscription");
props.put("spring.pulsar.reader.subscription-role-prefix", "sub-role");
props.put("spring.pulsar.reader.read-compacted", "true");
props.put("spring.pulsar.reader.reset-include-head", "true");
bind(props);
Map<String, Object> readerProps = properties.buildReaderProperties();
// Verify that the props can be loaded in a ReaderBuilder
assertThatNoException().isThrownBy(() -> ConfigurationDataUtils.loadData(readerProps,
new ReaderConfigurationData<>(), ReaderConfigurationData.class));
assertThat(readerProps)
.hasEntrySatisfying("topicNames",
topics -> assertThat(topics).asInstanceOf(InstanceOfAssertFactories.list(String.class))
.containsExactly("my-topic"))
.containsEntry("receiverQueueSize", 100).containsEntry("readerName", "my-reader")
.containsEntry("subscriptionName", "my-subscription")
.containsEntry("subscriptionRolePrefix", "sub-role").containsEntry("readCompacted", true)
.containsEntry("resetIncludeHead", true);
}
}
}

View File

@@ -1,350 +0,0 @@
/*
* Copyright 2022-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.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import java.time.Duration;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import org.apache.pulsar.client.api.PulsarClient;
import org.apache.pulsar.client.api.SubscriptionType;
import org.apache.pulsar.common.schema.SchemaType;
import org.apache.pulsar.reactive.client.adapter.AdaptedReactivePulsarClientFactory;
import org.apache.pulsar.reactive.client.adapter.ProducerCacheProvider;
import org.apache.pulsar.reactive.client.api.ReactiveMessageConsumerSpec;
import org.apache.pulsar.reactive.client.api.ReactiveMessageReaderSpec;
import org.apache.pulsar.reactive.client.api.ReactiveMessageSenderCache;
import org.apache.pulsar.reactive.client.api.ReactiveMessageSenderSpec;
import org.apache.pulsar.reactive.client.api.ReactivePulsarClient;
import org.apache.pulsar.reactive.client.producercache.CaffeineProducerCacheProvider;
import org.assertj.core.api.AbstractObjectAssert;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.pulsar.config.PulsarClientFactoryBean;
import org.springframework.pulsar.core.SchemaResolver;
import org.springframework.pulsar.core.TopicResolver;
import org.springframework.pulsar.reactive.config.DefaultReactivePulsarListenerContainerFactory;
import org.springframework.pulsar.reactive.config.ReactivePulsarListenerContainerFactory;
import org.springframework.pulsar.reactive.config.ReactivePulsarListenerEndpointRegistry;
import org.springframework.pulsar.reactive.config.annotation.EnableReactivePulsar;
import org.springframework.pulsar.reactive.config.annotation.ReactivePulsarBootstrapConfiguration;
import org.springframework.pulsar.reactive.config.annotation.ReactivePulsarListenerAnnotationBeanPostProcessor;
import org.springframework.pulsar.reactive.core.DefaultReactivePulsarConsumerFactory;
import org.springframework.pulsar.reactive.core.DefaultReactivePulsarReaderFactory;
import org.springframework.pulsar.reactive.core.DefaultReactivePulsarSenderFactory;
import org.springframework.pulsar.reactive.core.ReactivePulsarConsumerFactory;
import org.springframework.pulsar.reactive.core.ReactivePulsarReaderFactory;
import org.springframework.pulsar.reactive.core.ReactivePulsarSenderFactory;
import org.springframework.pulsar.reactive.core.ReactivePulsarTemplate;
import org.springframework.pulsar.reactive.listener.ReactivePulsarContainerProperties;
/**
* Autoconfiguration tests for {@link PulsarReactiveAutoConfiguration}.
*
* @author Christophe Bornet
* @author Chris Bono
*/
@SuppressWarnings("unchecked")
class PulsarReactiveAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(PulsarAutoConfiguration.class))
.withConfiguration(AutoConfigurations.of(PulsarReactiveAutoConfiguration.class));
@Test
void autoConfigurationSkippedWhenReactivePulsarClientNotOnClasspath() {
this.contextRunner.withClassLoader(new FilteredClassLoader(ReactivePulsarClient.class)).run(
(context) -> assertThat(context).hasNotFailed().doesNotHaveBean(PulsarReactiveAutoConfiguration.class));
}
@Test
void autoConfigurationSkippedWhenReactivePulsarTemplateNotOnClasspath() {
this.contextRunner.withClassLoader(new FilteredClassLoader(ReactivePulsarTemplate.class)).run(
(context) -> assertThat(context).hasNotFailed().doesNotHaveBean(PulsarReactiveAutoConfiguration.class));
}
@Test
void annotationDrivenConfigurationSkippedWhenEnablePulsarAnnotationNotOnClasspath() {
this.contextRunner.withClassLoader(new FilteredClassLoader(EnableReactivePulsar.class))
.run((context) -> assertThat(context).hasNotFailed()
.doesNotHaveBean(PulsarReactiveAnnotationDrivenConfiguration.class));
}
@Test
void bootstrapConfigurationSkippedWhenCustomReactivePulsarListenerAnnotationProcessorDefined() {
this.contextRunner
.withBean("org.springframework.pulsar.config.internalReactivePulsarListenerAnnotationProcessor",
String.class, () -> "someFauxBean")
.run((context) -> assertThat(context).hasNotFailed()
.doesNotHaveBean(ReactivePulsarBootstrapConfiguration.class));
}
@Test
void defaultBeansAreAutoConfigured() {
this.contextRunner.run((context) -> assertThat(context).hasNotFailed()
.hasSingleBean(ReactivePulsarTemplate.class).hasSingleBean(ReactivePulsarClient.class)
.hasSingleBean(ProducerCacheProvider.class).hasSingleBean(ReactiveMessageSenderCache.class)
.hasSingleBean(ReactivePulsarSenderFactory.class).hasSingleBean(ReactivePulsarTemplate.class)
.hasSingleBean(DefaultReactivePulsarListenerContainerFactory.class)
.hasSingleBean(ReactivePulsarListenerAnnotationBeanPostProcessor.class)
.hasSingleBean(ReactivePulsarListenerEndpointRegistry.class));
}
@ParameterizedTest
@ValueSource(classes = { ReactivePulsarClient.class, ProducerCacheProvider.class, ReactiveMessageSenderCache.class,
ReactivePulsarSenderFactory.class, ReactivePulsarConsumerFactory.class, ReactivePulsarReaderFactory.class,
ReactivePulsarTemplate.class })
<T> void customBeanIsRespected(Class<T> beanClass) {
T bean = mock(beanClass);
this.contextRunner.withBean(beanClass.getName(), beanClass, () -> bean)
.run((context) -> assertThat(context).hasNotFailed().getBean(beanClass).isSameAs(bean));
}
@SuppressWarnings("rawtypes")
@Test
void beansAreInjectedInReactivePulsarListenerContainerFactory() {
ReactivePulsarConsumerFactory<?> consumerFactory = mock(ReactivePulsarConsumerFactory.class);
SchemaResolver schemaResolver = mock(SchemaResolver.class);
this.contextRunner
.withBean("customReactivePulsarConsumerFactory", ReactivePulsarConsumerFactory.class,
() -> consumerFactory)
.withBean("schemaResolver", SchemaResolver.class, () -> schemaResolver).run((context -> {
AbstractObjectAssert<? extends AbstractObjectAssert<?, DefaultReactivePulsarListenerContainerFactory>, DefaultReactivePulsarListenerContainerFactory> containerFactory = assertThat(
context).hasNotFailed().getBean(DefaultReactivePulsarListenerContainerFactory.class);
containerFactory.extracting("consumerFactory").isSameAs(consumerFactory);
containerFactory.extracting(DefaultReactivePulsarListenerContainerFactory::getContainerProperties)
.extracting(ReactivePulsarContainerProperties::getSchemaResolver).isSameAs(schemaResolver);
}));
}
@Test
void customReactivePulsarListenerContainerFactoryIsRespected() {
ReactivePulsarListenerContainerFactory<String> listenerContainerFactory = mock(
ReactivePulsarListenerContainerFactory.class);
this.contextRunner
.withBean("reactivePulsarListenerContainerFactory", ReactivePulsarListenerContainerFactory.class,
() -> listenerContainerFactory)
.run((context) -> assertThat(context).hasNotFailed()
.getBean(ReactivePulsarListenerContainerFactory.class).isSameAs(listenerContainerFactory));
}
@Test
void customReactivePulsarListenerAnnotationBeanPostProcessorIsRespected() {
ReactivePulsarListenerAnnotationBeanPostProcessor<String> listenerAnnotationBeanPostProcessor = mock(
ReactivePulsarListenerAnnotationBeanPostProcessor.class);
this.contextRunner
.withBean("org.springframework.pulsar.config.internalReactivePulsarListenerAnnotationProcessor",
ReactivePulsarListenerAnnotationBeanPostProcessor.class,
() -> listenerAnnotationBeanPostProcessor)
.run((context) -> assertThat(context).hasNotFailed()
.getBean(ReactivePulsarListenerAnnotationBeanPostProcessor.class)
.isSameAs(listenerAnnotationBeanPostProcessor));
}
@Test
@SuppressWarnings("rawtypes")
void beansAreInjectedInReactivePulsarTemplate() {
ReactivePulsarSenderFactory<?> senderFactory = mock(ReactivePulsarSenderFactory.class);
SchemaResolver schemaResolver = mock(SchemaResolver.class);
this.contextRunner
.withBean("customReactivePulsarSenderFactory", ReactivePulsarSenderFactory.class, () -> senderFactory)
.withBean("schemaResolver", SchemaResolver.class, () -> schemaResolver).run((context -> {
AbstractObjectAssert<? extends AbstractObjectAssert<?, ReactivePulsarTemplate>, ReactivePulsarTemplate> template = assertThat(
context).hasNotFailed().getBean(ReactivePulsarTemplate.class);
template.extracting("reactiveMessageSenderFactory").isSameAs(senderFactory);
template.extracting("schemaResolver").isSameAs(schemaResolver);
}));
}
@Test
@SuppressWarnings("rawtypes")
void beansAreInjectedInReactivePulsarSenderFactory() throws Exception {
ReactivePulsarClient client = mock(ReactivePulsarClient.class);
try (ReactiveMessageSenderCache cache = mock(ReactiveMessageSenderCache.class)) {
this.contextRunner.withPropertyValues("spring.pulsar.reactive.sender.topic-name=test-topic")
.withBean("customReactivePulsarClient", ReactivePulsarClient.class, () -> client)
.withBean("customReactiveMessageSenderCache", ReactiveMessageSenderCache.class, () -> cache)
.run((context -> {
AbstractObjectAssert<? extends AbstractObjectAssert<?, DefaultReactivePulsarSenderFactory>, DefaultReactivePulsarSenderFactory> senderFactory = assertThat(
context).hasNotFailed().getBean(DefaultReactivePulsarSenderFactory.class);
senderFactory.extracting(DefaultReactivePulsarSenderFactory::getReactiveMessageSenderSpec)
.extracting(ReactiveMessageSenderSpec::getTopicName).isEqualTo("test-topic");
senderFactory.extracting("reactivePulsarClient",
InstanceOfAssertFactories.type(ReactivePulsarClient.class)).isSameAs(client);
senderFactory
.extracting("reactiveMessageSenderCache",
InstanceOfAssertFactories.type(ReactiveMessageSenderCache.class))
.isSameAs(cache);
senderFactory.extracting("topicResolver", InstanceOfAssertFactories.type(TopicResolver.class))
.isSameAs(context.getBean(TopicResolver.class));
}));
}
}
@Test
@SuppressWarnings("rawtypes")
void beansAreInjectedInReactivePulsarConsumerFactory() {
ReactivePulsarClient client = mock(ReactivePulsarClient.class);
this.contextRunner.withPropertyValues("spring.pulsar.reactive.consumer.consumer-name=test-consumer")
.withBean("customReactivePulsarClient", ReactivePulsarClient.class, () -> client).run((context -> {
AbstractObjectAssert<? extends AbstractObjectAssert<?, DefaultReactivePulsarConsumerFactory>, DefaultReactivePulsarConsumerFactory> senderFactory = assertThat(
context).hasNotFailed().getBean(DefaultReactivePulsarConsumerFactory.class);
senderFactory
.extracting("consumerSpec",
InstanceOfAssertFactories.type(ReactiveMessageConsumerSpec.class))
.extracting(ReactiveMessageConsumerSpec::getConsumerName).isEqualTo("test-consumer");
senderFactory.extracting("reactivePulsarClient",
InstanceOfAssertFactories.type(ReactivePulsarClient.class)).isSameAs(client);
}));
}
@Test
@SuppressWarnings("rawtypes")
void beansAreInjectedInReactivePulsarReaderFactory() {
ReactivePulsarClient client = mock(ReactivePulsarClient.class);
this.contextRunner.withPropertyValues("spring.pulsar.reactive.reader.reader-name=test-reader")
.withBean("customReactivePulsarClient", ReactivePulsarClient.class, () -> client).run((context -> {
AbstractObjectAssert<? extends AbstractObjectAssert<?, DefaultReactivePulsarReaderFactory>, DefaultReactivePulsarReaderFactory> senderFactory = assertThat(
context).hasNotFailed().getBean(DefaultReactivePulsarReaderFactory.class);
senderFactory
.extracting("readerSpec", InstanceOfAssertFactories.type(ReactiveMessageReaderSpec.class))
.extracting(ReactiveMessageReaderSpec::getReaderName).isEqualTo("test-reader");
senderFactory.extracting("reactivePulsarClient",
InstanceOfAssertFactories.type(ReactivePulsarClient.class)).isSameAs(client);
}));
}
@Test
void beansAreInjectedInReactiveMessageSenderCache() throws Exception {
try (ProducerCacheProvider provider = mock(ProducerCacheProvider.class)) {
this.contextRunner.withBean("customProducerCacheProvider", ProducerCacheProvider.class, () -> provider)
.run((context -> {
var senderFactory = assertThat(context).hasNotFailed()
.getBean(ReactiveMessageSenderCache.class);
senderFactory.extracting("cacheProvider")
.asInstanceOf(InstanceOfAssertFactories.type(ProducerCacheProvider.class))
.isSameAs(provider);
}));
}
}
@Test
@SuppressWarnings("rawtypes")
void beansAreInjectedInReactivePulsarClient() throws Exception {
try (PulsarClient client = mock(PulsarClient.class)) {
PulsarClientFactoryBean factoryBean = new PulsarClientFactoryBean(Collections.emptyMap()) {
@Override
protected PulsarClient createInstance() {
return client;
}
};
this.contextRunner.withBean("customPulsarClient", PulsarClientFactoryBean.class, () -> factoryBean)
.run((context -> assertThat(context).hasNotFailed().getBean(ReactivePulsarClient.class)
.extracting("reactivePulsarResourceAdapter")
.extracting("pulsarClientSupplier", InstanceOfAssertFactories.type(Supplier.class))
.extracting(Supplier::get).isSameAs(client)));
}
}
@Test
void reactiveListenerPropertiesAreHonored() {
contextRunner.withPropertyValues("spring.pulsar.reactive.listener.schema-type=avro",
"spring.pulsar.reactive.listener.handling-timeout=10s",
"spring.pulsar.reactive.listener.use-key-ordered-processing=true",
"spring.pulsar.reactive.consumer.subscription-type=shared").run((context -> {
AbstractObjectAssert<?, ReactivePulsarContainerProperties<?>> properties = assertThat(context)
.hasNotFailed().getBean(DefaultReactivePulsarListenerContainerFactory.class)
.extracting(DefaultReactivePulsarListenerContainerFactory<Object>::getContainerProperties);
properties.extracting(ReactivePulsarContainerProperties::getSchemaType).isEqualTo(SchemaType.AVRO);
properties.extracting(ReactivePulsarContainerProperties::getHandlingTimeout)
.isEqualTo(Duration.ofSeconds(10));
properties.extracting(ReactivePulsarContainerProperties::isUseKeyOrderedProcessing).isEqualTo(true);
properties.extracting(ReactivePulsarContainerProperties::getSubscriptionType)
.isEqualTo(SubscriptionType.Shared);
}));
}
@Nested
class SenderCacheAutoConfigurationTests {
@Test
void caffeineCacheUsedByDefault() {
contextRunner.run(this::assertCaffeineProducerCacheProvider);
}
@Test
void caffeineCacheCanBeConfigured() {
contextRunner
.withPropertyValues("spring.pulsar.reactive.sender.cache.expire-after-access=100s",
"spring.pulsar.reactive.sender.cache.maximum-size=5150",
"spring.pulsar.reactive.sender.cache.initial-capacity=200")
.run((context) -> assertCaffeineProducerCacheProvider(context).extracting("cache")
.extracting("cache").hasFieldOrPropertyWithValue("maximum", 5150L)
.hasFieldOrPropertyWithValue("expiresAfterAccessNanos", TimeUnit.SECONDS.toNanos(100)));
}
@Test
void defaultClientCacheIsUsedIfCaffeineProducerCacheProviderNotOnClasspath() {
ReactiveMessageSenderCache cache = AdaptedReactivePulsarClientFactory.createCache();
try (MockedStatic<AdaptedReactivePulsarClientFactory> mockedClientFactory = Mockito
.mockStatic(AdaptedReactivePulsarClientFactory.class)) {
mockedClientFactory.when(AdaptedReactivePulsarClientFactory::createCache).thenReturn(cache);
mockedClientFactory.when(() -> AdaptedReactivePulsarClientFactory.create(any(PulsarClient.class)))
.thenReturn(mock(ReactivePulsarClient.class));
contextRunner.withClassLoader(new FilteredClassLoader(CaffeineProducerCacheProvider.class))
.run((context) -> assertThat(context).hasNotFailed()
.doesNotHaveBean(ProducerCacheProvider.class)
.hasSingleBean(ReactiveMessageSenderCache.class)
.getBean(ReactiveMessageSenderCache.class).isSameAs(cache));
mockedClientFactory.verify(AdaptedReactivePulsarClientFactory::createCache);
}
}
@Test
void cacheCanBeDisabled() {
contextRunner.withPropertyValues("spring.pulsar.reactive.sender.cache.enabled=false")
.run((context -> assertThat(context).hasNotFailed().doesNotHaveBean(ProducerCacheProvider.class)
.doesNotHaveBean(ReactiveMessageSenderCache.class)));
}
private AbstractObjectAssert<?, ProducerCacheProvider> assertCaffeineProducerCacheProvider(
AssertableApplicationContext context) {
return assertThat(context).hasNotFailed().hasSingleBean(ProducerCacheProvider.class)
.hasSingleBean(ReactiveMessageSenderCache.class).getBean(ProducerCacheProvider.class)
.isExactlyInstanceOf(CaffeineProducerCacheProvider.class);
}
}
}

View File

@@ -1,256 +0,0 @@
/*
* Copyright 2022-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 java.time.Duration;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.apache.pulsar.client.api.CompressionType;
import org.apache.pulsar.client.api.ConsumerCryptoFailureAction;
import org.apache.pulsar.client.api.HashingScheme;
import org.apache.pulsar.client.api.MessageRoutingMode;
import org.apache.pulsar.client.api.ProducerAccessMode;
import org.apache.pulsar.client.api.ProducerCryptoFailureAction;
import org.apache.pulsar.client.api.Range;
import org.apache.pulsar.client.api.RegexSubscriptionMode;
import org.apache.pulsar.client.api.SubscriptionInitialPosition;
import org.apache.pulsar.client.api.SubscriptionMode;
import org.apache.pulsar.client.api.SubscriptionType;
import org.apache.pulsar.reactive.client.api.ReactiveMessageConsumerSpec;
import org.apache.pulsar.reactive.client.api.ReactiveMessageReaderSpec;
import org.apache.pulsar.reactive.client.api.ReactiveMessageSenderSpec;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.EnumSource.Mode;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
import org.springframework.pulsar.autoconfigure.PulsarReactiveProperties.SchedulerType;
import reactor.core.scheduler.Schedulers;
/**
* Unit tests for {@link PulsarReactiveProperties}.
*
* @author Christophe Bornet
*/
public class PulsarReactivePropertiesTests {
private final PulsarReactiveProperties properties = new PulsarReactiveProperties();
private void bind(String name, String value) {
bind(Collections.singletonMap(name, value));
}
private void bind(Map<String, String> map) {
ConfigurationPropertySource source = new MapConfigurationPropertySource(map);
new Binder(source).bind("spring.pulsar.reactive", Bindable.ofInstance(this.properties));
}
@Nested
class SenderPropertiesTests {
@Test
void senderPropsToSenderSpec() {
Map<String, String> props = new HashMap<>();
props.put("spring.pulsar.reactive.sender.topic-name", "my-topic");
props.put("spring.pulsar.reactive.sender.producer-name", "my-producer");
props.put("spring.pulsar.reactive.sender.send-timeout", "2s");
props.put("spring.pulsar.reactive.sender.max-pending-messages", "3");
props.put("spring.pulsar.reactive.sender.max-pending-messages-across-partitions", "4");
props.put("spring.pulsar.reactive.sender.message-routing-mode", "custompartition");
props.put("spring.pulsar.reactive.sender.hashing-scheme", "murmur3_32hash");
props.put("spring.pulsar.reactive.sender.crypto-failure-action", "send");
props.put("spring.pulsar.reactive.sender.batching-max-publish-delay", "5s");
props.put("spring.pulsar.reactive.sender.round-robin-router-batching-partition-switch-frequency", "6");
props.put("spring.pulsar.reactive.sender.batching-max-messages", "7");
props.put("spring.pulsar.reactive.sender.batching-max-bytes", "8");
props.put("spring.pulsar.reactive.sender.batching-enabled", "false");
props.put("spring.pulsar.reactive.sender.chunking-enabled", "true");
props.put("spring.pulsar.reactive.sender.encryption-keys[0]", "my-key");
props.put("spring.pulsar.reactive.sender.compression-type", "lz4");
props.put("spring.pulsar.reactive.sender.initial-sequence-id", "9");
props.put("spring.pulsar.reactive.sender.producer-access-mode", "exclusive");
props.put("spring.pulsar.reactive.sender.lazy-start=partitioned-producers", "true");
props.put("spring.pulsar.reactive.sender.properties[my-prop]", "my-prop-value");
bind(props);
ReactiveMessageSenderSpec senderSpec = properties.buildReactiveMessageSenderSpec();
assertThat(senderSpec.getTopicName()).isEqualTo("my-topic");
assertThat(senderSpec.getProducerName()).isEqualTo("my-producer");
assertThat(senderSpec.getSendTimeout()).isEqualTo(Duration.ofSeconds(2));
assertThat(senderSpec.getMaxPendingMessages()).isEqualTo(3);
assertThat(senderSpec.getMaxPendingMessagesAcrossPartitions()).isEqualTo(4);
assertThat(senderSpec.getMessageRoutingMode()).isEqualTo(MessageRoutingMode.CustomPartition);
assertThat(senderSpec.getHashingScheme()).isEqualTo(HashingScheme.Murmur3_32Hash);
assertThat(senderSpec.getCryptoFailureAction()).isEqualTo(ProducerCryptoFailureAction.SEND);
assertThat(senderSpec.getBatchingMaxPublishDelay()).isEqualTo(Duration.ofSeconds(5));
assertThat(senderSpec.getRoundRobinRouterBatchingPartitionSwitchFrequency()).isEqualTo(6);
assertThat(senderSpec.getBatchingMaxMessages()).isEqualTo(7);
assertThat(senderSpec.getBatchingMaxBytes()).isEqualTo(8);
assertThat(senderSpec.getBatchingEnabled()).isEqualTo(false);
assertThat(senderSpec.getChunkingEnabled()).isEqualTo(true);
assertThat(senderSpec.getEncryptionKeys()).containsExactly("my-key");
assertThat(senderSpec.getCompressionType()).isEqualTo(CompressionType.LZ4);
assertThat(senderSpec.getInitialSequenceId()).isEqualTo(9);
assertThat(senderSpec.getAccessMode()).isEqualTo(ProducerAccessMode.Exclusive);
assertThat(senderSpec.getLazyStartPartitionedProducers()).isTrue();
assertThat(senderSpec.getProperties()).hasSize(1).containsEntry("my-prop", "my-prop-value");
}
}
@Nested
class ConsumerPropertiesTests {
@Test
void consumerPropsToConsumerSpec() {
Map<String, String> props = new HashMap<>();
props.put("spring.pulsar.reactive.consumer.topics[0]", "my-topic");
props.put("spring.pulsar.reactive.consumer.topics-pattern", "my-pattern");
props.put("spring.pulsar.reactive.consumer.subscription-name", "my-subscription");
props.put("spring.pulsar.reactive.consumer.subscription-type", "shared");
props.put("spring.pulsar.reactive.consumer.subscription-mode", "nondurable");
props.put("spring.pulsar.reactive.consumer.subscription-properties[my-sub-prop]", "my-sub-prop-value");
props.put("spring.pulsar.reactive.consumer.receiver-queue-size", "1");
props.put("spring.pulsar.reactive.consumer.acknowledgements-group-time", "2s");
props.put("spring.pulsar.reactive.consumer.acknowledge-asynchronously", "false");
props.put("spring.pulsar.reactive.consumer.negative-ack-redelivery-delay", "3s");
props.put("spring.pulsar.reactive.consumer.dead-letter-policy.max-redeliver-count", "4");
props.put("spring.pulsar.reactive.consumer.dead-letter-policy.retry-letter-topic", "my-retry-topic");
props.put("spring.pulsar.reactive.consumer.dead-letter-policy.dead-letter-topic", "my-dlt-topic");
props.put("spring.pulsar.reactive.consumer.dead-letter-policy.initial-subscription-name",
"my-initial-subscription");
props.put("spring.pulsar.reactive.consumer.max-total-receiver-queue-size-across-partitions", "5");
props.put("spring.pulsar.reactive.consumer.consumer-name", "my-consumer");
props.put("spring.pulsar.reactive.consumer.ack-timeout", "6s");
props.put("spring.pulsar.reactive.consumer.ack-timeout-tick-time", "7s");
props.put("spring.pulsar.reactive.consumer.priority-level", "8");
props.put("spring.pulsar.reactive.consumer.crypto-failure-action", "discard");
props.put("spring.pulsar.reactive.consumer.properties[my-prop]", "my-prop-value");
props.put("spring.pulsar.reactive.consumer.read-compacted", "true");
props.put("spring.pulsar.reactive.consumer.batch-index-ack-enabled", "true");
props.put("spring.pulsar.reactive.consumer.subscription-initial-position", "earliest");
props.put("spring.pulsar.reactive.consumer.topics-pattern-auto-discovery-period", "9s");
props.put("spring.pulsar.reactive.consumer.topics-pattern-subscription-mode", "alltopics");
props.put("spring.pulsar.reactive.consumer.auto-update-partitions", "false");
props.put("spring.pulsar.reactive.consumer.auto-update-partitions-interval", "10s");
props.put("spring.pulsar.reactive.consumer.replicate-subscription-state", "true");
props.put("spring.pulsar.reactive.consumer.auto-ack-oldest-chunked-message-on-queue-full", "false");
props.put("spring.pulsar.reactive.consumer.max-pending-chunked-message", "11");
props.put("spring.pulsar.reactive.consumer.expire-time-of-incomplete-chunked-message", "12s");
bind(props);
ReactiveMessageConsumerSpec consumerSpec = properties.buildReactiveMessageConsumerSpec();
assertThat(consumerSpec.getTopicNames()).containsExactly("my-topic");
assertThat(consumerSpec.getTopicsPattern().toString()).isEqualTo("my-pattern");
assertThat(consumerSpec.getSubscriptionName()).isEqualTo("my-subscription");
assertThat(consumerSpec.getSubscriptionType()).isEqualTo(SubscriptionType.Shared);
assertThat(consumerSpec.getSubscriptionMode()).isEqualTo(SubscriptionMode.NonDurable);
assertThat(consumerSpec.getSubscriptionProperties()).hasSize(1).containsEntry("my-sub-prop",
"my-sub-prop-value");
assertThat(consumerSpec.getReceiverQueueSize()).isEqualTo(1);
assertThat(consumerSpec.getAcknowledgementsGroupTime()).isEqualTo(Duration.ofSeconds(2));
assertThat(consumerSpec.getAcknowledgeAsynchronously()).isFalse();
assertThat(consumerSpec.getNegativeAckRedeliveryDelay()).isEqualTo(Duration.ofSeconds(3));
assertThat(consumerSpec.getDeadLetterPolicy().getMaxRedeliverCount()).isEqualTo(4);
assertThat(consumerSpec.getDeadLetterPolicy().getRetryLetterTopic()).isEqualTo("my-retry-topic");
assertThat(consumerSpec.getDeadLetterPolicy().getDeadLetterTopic()).isEqualTo("my-dlt-topic");
assertThat(consumerSpec.getDeadLetterPolicy().getInitialSubscriptionName())
.isEqualTo("my-initial-subscription");
assertThat(consumerSpec.getMaxTotalReceiverQueueSizeAcrossPartitions()).isEqualTo(5);
assertThat(consumerSpec.getConsumerName()).isEqualTo("my-consumer");
assertThat(consumerSpec.getAckTimeout()).isEqualTo(Duration.ofSeconds(6));
assertThat(consumerSpec.getAckTimeoutTickTime()).isEqualTo(Duration.ofSeconds(7));
assertThat(consumerSpec.getPriorityLevel()).isEqualTo(8);
assertThat(consumerSpec.getCryptoFailureAction()).isEqualTo(ConsumerCryptoFailureAction.DISCARD);
assertThat(consumerSpec.getProperties()).hasSize(1).containsEntry("my-prop", "my-prop-value");
assertThat(consumerSpec.getReadCompacted()).isTrue();
assertThat(consumerSpec.getBatchIndexAckEnabled()).isTrue();
assertThat(consumerSpec.getSubscriptionInitialPosition()).isEqualTo(SubscriptionInitialPosition.Earliest);
assertThat(consumerSpec.getTopicsPatternAutoDiscoveryPeriod()).isEqualTo(Duration.ofSeconds(9));
assertThat(consumerSpec.getTopicsPatternSubscriptionMode()).isEqualTo(RegexSubscriptionMode.AllTopics);
assertThat(consumerSpec.getAutoUpdatePartitions()).isFalse();
assertThat(consumerSpec.getAutoUpdatePartitionsInterval()).isEqualTo(Duration.ofSeconds(10));
assertThat(consumerSpec.getReplicateSubscriptionState()).isTrue();
assertThat(consumerSpec.getAutoAckOldestChunkedMessageOnQueueFull()).isFalse();
assertThat(consumerSpec.getMaxPendingChunkedMessage()).isEqualTo(11);
assertThat(consumerSpec.getExpireTimeOfIncompleteChunkedMessage()).isEqualTo(Duration.ofSeconds(12));
}
@ParameterizedTest
@EnumSource(value = SchedulerType.class, names = "immediate", mode = Mode.EXCLUDE)
void acknowledgeScheduler(SchedulerType acknowledgeSchedulerType) {
bind("spring.pulsar.reactive.consumer.acknowledge-scheduler-type", acknowledgeSchedulerType.name());
ReactiveMessageConsumerSpec consumerSpec = properties.buildReactiveMessageConsumerSpec();
assertThat(consumerSpec.getAcknowledgeScheduler().toString())
.isEqualTo("Schedulers.%s()".formatted(acknowledgeSchedulerType));
}
@Test
void acknowledgeSchedulerImmediate() {
bind("spring.pulsar.reactive.consumer.acknowledge-scheduler-type", "immediate");
ReactiveMessageConsumerSpec consumerSpec = properties.buildReactiveMessageConsumerSpec();
assertThat(consumerSpec.getAcknowledgeScheduler()).isSameAs(Schedulers.immediate());
}
}
@Nested
class ReaderPropertiesTests {
@Test
void readerPropsToReaderSpec() {
Map<String, String> props = new HashMap<>();
props.put("spring.pulsar.reactive.reader.topic-names[0]", "my-topic");
props.put("spring.pulsar.reactive.reader.reader-name", "my-reader");
props.put("spring.pulsar.reactive.reader.subscription-name", "my-subscription");
props.put("spring.pulsar.reactive.reader.generated-subscription-name-prefix", "my-prefix");
props.put("spring.pulsar.reactive.reader.receiver-queue-size", "1");
props.put("spring.pulsar.reactive.reader.read-compacted", "true");
props.put("spring.pulsar.reactive.reader.key-hash-ranges[0].start", "2");
props.put("spring.pulsar.reactive.reader.key-hash-ranges[0].end", "3");
props.put("spring.pulsar.reactive.reader.crypto-failure-action", "discard");
bind(props);
ReactiveMessageReaderSpec readerSpec = properties.buildReactiveMessageReaderSpec();
assertThat(readerSpec.getTopicNames()).containsExactly("my-topic");
assertThat(readerSpec.getReaderName()).isEqualTo("my-reader");
assertThat(readerSpec.getSubscriptionName()).isEqualTo("my-subscription");
assertThat(readerSpec.getGeneratedSubscriptionNamePrefix()).isEqualTo("my-prefix");
assertThat(readerSpec.getReceiverQueueSize()).isEqualTo(1);
assertThat(readerSpec.getReadCompacted()).isTrue();
assertThat(readerSpec.getKeyHashRanges()).containsExactly(Range.of(2, 3));
assertThat(readerSpec.getCryptoFailureAction()).isEqualTo(ConsumerCryptoFailureAction.DISCARD);
}
}
}

View File

@@ -1,91 +0,0 @@
/*
* Copyright 2022-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 org.apache.pulsar.client.api.MessageId;
import org.apache.pulsar.client.api.PulsarClientException;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.pulsar.autoconfigure.SpringPulsarBootAppSanityTests.SpringPulsarBootTestApp;
import org.springframework.pulsar.core.PulsarTemplate;
import org.springframework.pulsar.test.support.PulsarTestContainerSupport;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Sanity tests to ensure that {@code Spring Pulsar} can be auto-configured into a Spring
* Boot application.
*
* @author Chris Bono
*/
@SpringBootTest(classes = SpringPulsarBootTestApp.class, webEnvironment = WebEnvironment.RANDOM_PORT)
class SpringPulsarBootAppSanityTests implements PulsarTestContainerSupport {
@DynamicPropertySource
static void pulsarProperties(DynamicPropertyRegistry registry) {
registry.add("spring.pulsar.client.service-url", PulsarTestContainerSupport::getPulsarBrokerUrl);
}
@Test
void appStartsWithAutoConfiguredSpringPulsarComponents(
@Autowired ObjectProvider<PulsarTemplate<String>> pulsarTemplate) {
assertThat(pulsarTemplate.getIfAvailable()).isNotNull();
}
@Test
void templateCanBeAccessedDuringWebRequest(@Autowired TestRestTemplate restTemplate) {
String body = restTemplate.getForObject("/hello", String.class);
assertThat(body).startsWith("Hello World -> ");
}
@SpringBootConfiguration
@EnableAutoConfiguration
static class SpringPulsarBootTestApp {
@Autowired
private ObjectProvider<PulsarTemplate<String>> pulsarTemplateProvider;
@RestController
class TestWebController {
@GetMapping("/hello")
String sayHello() throws PulsarClientException {
PulsarTemplate<String> pulsarTemplate = pulsarTemplateProvider.getIfAvailable();
if (pulsarTemplate == null) {
return "NOPE! Not hello world";
}
MessageId msgId = pulsarTemplate.send("spbast-hello-topic", "hello");
return "Hello World -> " + msgId;
}
}
}
}

View File

@@ -1,11 +0,0 @@
plugins {
id 'org.springframework.pulsar.spring-module'
}
description = 'Spring Pulsar Spring Boot Starter'
dependencies {
api project (':spring-pulsar')
api project (':spring-pulsar-spring-boot-autoconfigure')
api 'org.springframework.boot:spring-boot-starter'
}

View File

@@ -1,31 +0,0 @@
plugins {
id 'org.springframework.pulsar.spring-module'
id 'org.springframework.pulsar.configuration-properties'
}
description = 'Spring Cloud Stream Binder for Apache Pulsar'
dependencies {
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
implementation project(':spring-pulsar-spring-boot-starter')
api('org.springframework.cloud:spring-cloud-stream') {
exclude group: 'javax.activation', module: 'javax.activation-api'
exclude group: 'javax.annotation', module: 'javax.annotation-api'
}
testImplementation project(':spring-pulsar-test')
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation('org.springframework.cloud:spring-cloud-stream-test-support') {
exclude group: 'javax.activation', module: 'javax.activation-api'
exclude group: 'javax.annotation', module: 'javax.annotation-api'
}
testImplementation 'org.awaitility:awaitility'
testImplementation 'org.testcontainers:junit-jupiter'
testImplementation 'org.testcontainers:pulsar'
}
test {
testLogging.showStandardStreams = true
}

View File

@@ -1,69 +0,0 @@
/*
* Copyright 2018-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.pulsar.spring.cloud.stream.binder;
import java.util.Map;
import org.apache.pulsar.client.api.Message;
import org.springframework.cloud.stream.binder.BinderHeaders;
import org.springframework.integration.IntegrationMessageHeaderAccessor;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.pulsar.support.header.PulsarHeaderMapper;
/**
* A delegating {@code PulsarHeaderMapper} that ensures the delegate mapper never includes
* internal binder specific headers during outbound mapping.
*
* @author Chris Bono
*/
class PulsarBinderHeaderMapper implements PulsarHeaderMapper {
private final PulsarHeaderMapper delegate;
/**
* Construct a mapper with the specified delegate.
* @param delegate the delegate mapper
*/
PulsarBinderHeaderMapper(PulsarHeaderMapper delegate) {
this.delegate = delegate;
}
@Override
public Map<String, String> toPulsarHeaders(MessageHeaders springHeaders) {
Map<String, String> pulsarHeaders = this.delegate.toPulsarHeaders(springHeaders);
pulsarHeaders.remove(MessageHeaders.ID);
pulsarHeaders.remove(MessageHeaders.TIMESTAMP);
pulsarHeaders.remove(IntegrationMessageHeaderAccessor.DELIVERY_ATTEMPT);
pulsarHeaders.remove(BinderHeaders.NATIVE_HEADERS_PRESENT);
return pulsarHeaders;
}
@Override
public MessageHeaders toSpringHeaders(Message<?> pulsarMessage) {
var springHeaders = this.delegate.toSpringHeaders(pulsarMessage);
if (!springHeaders.isEmpty()) {
MessageHeaderAccessor mutableHeaders = new MessageHeaderAccessor();
mutableHeaders.copyHeaders(springHeaders);
mutableHeaders.setHeader(BinderHeaders.NATIVE_HEADERS_PRESENT, Boolean.TRUE);
springHeaders = mutableHeaders.getMessageHeaders();
}
return springHeaders;
}
}

View File

@@ -1,103 +0,0 @@
/*
* Copyright 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.spring.cloud.stream.binder;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import org.springframework.cloud.stream.provisioning.ConsumerDestination;
import org.springframework.core.log.LogAccessor;
import org.springframework.pulsar.spring.cloud.stream.binder.properties.PulsarConsumerProperties;
import org.springframework.util.StringUtils;
/**
* Binder utility methods.
*
* @author Soby Chacko
* @author Chris Bono
*/
final class PulsarBinderUtils {
private static final LogAccessor LOGGER = new LogAccessor(PulsarBinderUtils.class);
private static final String SUBSCRIPTION_NAME_FORMAT_STR = "%s-anon-subscription-%s";
private PulsarBinderUtils() {
}
/**
* Gets the subscription name to use for the binder.
* @param consumerProps the pulsar consumer props
* @param consumerDestination the destination being subscribed to
* @return the subscription name from the consumer properties or a generated name in
* the format {@link #SUBSCRIPTION_NAME_FORMAT_STR} when the name is not set on the
* consumer properties
*/
static String subscriptionName(PulsarConsumerProperties consumerProps, ConsumerDestination consumerDestination) {
if (StringUtils.hasText(consumerProps.getSubscriptionName())) {
return consumerProps.getSubscriptionName();
}
return SUBSCRIPTION_NAME_FORMAT_STR.formatted(consumerDestination.getName(), UUID.randomUUID());
}
/**
* Merges properties defined at the binder and binding level (binding properties
* override binder properties).
* <p>
* <b>NOTE:</b> Properties whose value is not different from the default value in the
* {@code baseProps} are not included in the merged result.
* @param baseProps the map of base level properties (eg. 'spring.pulsar.consumer.*')
* @param binderProps the map of binder level properties (eg.
* 'spring.cloud.stream.pulsar.binder.consumer.*')
* @param bindingProps the map of binding level properties (eg.
* 'spring.cloud.stream.pulsar.bindings.myBinding-in-0.consumer.*')
* @return map of merged binder and binding properties including only properties whose
* value has changed from the same property in the base properties
*/
static Map<String, Object> mergePropertiesWithPrecedence(Map<String, Object> baseProps,
Map<String, Object> binderProps, Map<String, Object> bindingProps) {
Objects.requireNonNull(baseProps, "baseProps must be specified");
Objects.requireNonNull(binderProps, "binderProps must be specified");
Objects.requireNonNull(bindingProps, "bindingProps must be specified");
Map<String, Object> newOrModifiedBinderProps = extractNewOrModifiedProperties(binderProps, baseProps);
LOGGER.trace(() -> "New or modified binder props: %s".formatted(newOrModifiedBinderProps));
Map<String, Object> newOrModifiedBindingProps = extractNewOrModifiedProperties(bindingProps, baseProps);
LOGGER.trace(() -> "New or modified binding props: %s".formatted(newOrModifiedBindingProps));
Map<String, Object> mergedProps = new HashMap<>(newOrModifiedBinderProps);
mergedProps.putAll(newOrModifiedBindingProps);
LOGGER.trace(() -> "Final merged props: %s".formatted(mergedProps));
return mergedProps;
}
private static Map<String, Object> extractNewOrModifiedProperties(Map<String, Object> candidateProps,
Map<String, Object> baseProps) {
Map<String, Object> newOrModifiedProps = new HashMap<>();
candidateProps.forEach((propName, propValue) -> {
if (!baseProps.containsKey(propName) || (!Objects.equals(propValue, baseProps.get(propName)))) {
newOrModifiedProps.put(propName, propValue);
}
});
return newOrModifiedProps;
}
}

View File

@@ -1,316 +0,0 @@
/*
* Copyright 2022-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.spring.cloud.stream.binder;
import java.util.Optional;
import java.util.Set;
import org.apache.pulsar.client.api.PulsarClientException;
import org.apache.pulsar.client.api.Schema;
import org.apache.pulsar.common.schema.SchemaType;
import org.springframework.cloud.stream.binder.AbstractMessageChannelBinder;
import org.springframework.cloud.stream.binder.Binder;
import org.springframework.cloud.stream.binder.BinderSpecificPropertiesProvider;
import org.springframework.cloud.stream.binder.ExtendedConsumerProperties;
import org.springframework.cloud.stream.binder.ExtendedProducerProperties;
import org.springframework.cloud.stream.binder.ExtendedPropertiesBinder;
import org.springframework.cloud.stream.binder.HeaderMode;
import org.springframework.cloud.stream.provisioning.ConsumerDestination;
import org.springframework.cloud.stream.provisioning.ProducerDestination;
import org.springframework.integration.core.MessageProducer;
import org.springframework.integration.endpoint.MessageProducerSupport;
import org.springframework.integration.handler.AbstractMessageProducingHandler;
import org.springframework.integration.support.management.ManageableLifecycle;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.pulsar.autoconfigure.ConsumerConfigProperties;
import org.springframework.pulsar.autoconfigure.ProducerConfigProperties;
import org.springframework.pulsar.core.ProducerBuilderConfigurationUtil;
import org.springframework.pulsar.core.ProducerBuilderCustomizer;
import org.springframework.pulsar.core.PulsarConsumerFactory;
import org.springframework.pulsar.core.PulsarTemplate;
import org.springframework.pulsar.core.SchemaResolver;
import org.springframework.pulsar.core.TypedMessageBuilderCustomizer;
import org.springframework.pulsar.listener.AbstractPulsarMessageListenerContainer;
import org.springframework.pulsar.listener.DefaultPulsarMessageListenerContainer;
import org.springframework.pulsar.listener.PulsarContainerProperties;
import org.springframework.pulsar.listener.PulsarRecordMessageListener;
import org.springframework.pulsar.spring.cloud.stream.binder.properties.PulsarBinderConfigurationProperties;
import org.springframework.pulsar.spring.cloud.stream.binder.properties.PulsarConsumerProperties;
import org.springframework.pulsar.spring.cloud.stream.binder.properties.PulsarExtendedBindingProperties;
import org.springframework.pulsar.spring.cloud.stream.binder.properties.PulsarProducerProperties;
import org.springframework.pulsar.spring.cloud.stream.binder.provisioning.PulsarTopicProvisioner;
import org.springframework.pulsar.support.header.PulsarHeaderMapper;
/**
* {@link Binder} implementation for Apache Pulsar.
*
* @author Soby Chacko
* @author Chris Bono
*/
public class PulsarMessageChannelBinder extends
AbstractMessageChannelBinder<ExtendedConsumerProperties<PulsarConsumerProperties>, ExtendedProducerProperties<PulsarProducerProperties>, PulsarTopicProvisioner>
implements ExtendedPropertiesBinder<MessageChannel, PulsarConsumerProperties, PulsarProducerProperties> {
private final PulsarTemplate<Object> pulsarTemplate;
private final PulsarConsumerFactory<?> pulsarConsumerFactory;
private final PulsarBinderConfigurationProperties binderConfigProps;
private final SchemaResolver schemaResolver;
private final PulsarHeaderMapper headerMapper;
private PulsarExtendedBindingProperties extendedBindingProperties = new PulsarExtendedBindingProperties();
public PulsarMessageChannelBinder(PulsarTopicProvisioner provisioningProvider,
PulsarTemplate<Object> pulsarTemplate, PulsarConsumerFactory<?> pulsarConsumerFactory,
PulsarBinderConfigurationProperties binderConfigProps, SchemaResolver schemaResolver,
PulsarHeaderMapper headerMapper) {
super(null, provisioningProvider);
this.pulsarTemplate = pulsarTemplate;
this.pulsarConsumerFactory = pulsarConsumerFactory;
this.binderConfigProps = binderConfigProps;
this.schemaResolver = schemaResolver;
this.headerMapper = headerMapper;
}
@Override
protected MessageHandler createProducerMessageHandler(ProducerDestination destination,
ExtendedProducerProperties<PulsarProducerProperties> producerProperties, MessageChannel errorChannel) {
final Schema<Object> schema;
if (producerProperties.isUseNativeEncoding()) {
var schemaType = Optional.ofNullable(producerProperties.getExtension().getSchemaType())
.orElse(SchemaType.NONE);
schema = this.schemaResolver
.resolveSchema(schemaType, producerProperties.getExtension().getMessageType(),
producerProperties.getExtension().getMessageKeyType())
.orElseThrow(() -> "Could not determine producer schema for " + destination.getName());
}
else {
schema = null;
}
var baseProducerProps = new ProducerConfigProperties().buildProperties();
var binderProducerProps = this.binderConfigProps.getProducer().buildProperties();
var bindingProducerProps = producerProperties.getExtension().buildProperties();
var mergedProducerProps = PulsarBinderUtils.mergePropertiesWithPrecedence(baseProducerProps,
binderProducerProps, bindingProducerProps);
var handler = new PulsarProducerConfigurationMessageHandler(this.pulsarTemplate, schema, destination.getName(),
(builder) -> ProducerBuilderConfigurationUtil.loadConf(builder, mergedProducerProps),
determineOutboundHeaderMapper(producerProperties));
handler.setApplicationContext(getApplicationContext());
handler.setBeanFactory(getBeanFactory());
return handler;
}
@Nullable
private PulsarBinderHeaderMapper determineOutboundHeaderMapper(
ExtendedProducerProperties<PulsarProducerProperties> extProducerProps) {
if (HeaderMode.none.equals(extProducerProps.getHeaderMode())) {
return null;
}
return new PulsarBinderHeaderMapper(this.headerMapper);
}
@Override
protected MessageProducer createConsumerEndpoint(ConsumerDestination destination, String group,
ExtendedConsumerProperties<PulsarConsumerProperties> properties) {
var containerProperties = new PulsarContainerProperties();
containerProperties.setTopics(Set.of(destination.getName()));
var inboundHeaderMapper = determineInboundHeaderMapper(properties);
var messageDrivenChannelAdapter = new PulsarMessageDrivenChannelAdapter();
containerProperties.setMessageListener((PulsarRecordMessageListener<?>) (consumer, pulsarMsg) -> {
var springMessage = (inboundHeaderMapper != null)
? MessageBuilder.createMessage(pulsarMsg.getValue(), inboundHeaderMapper.toSpringHeaders(pulsarMsg))
: MessageBuilder.withPayload(pulsarMsg.getValue()).build();
messageDrivenChannelAdapter.send(springMessage);
});
if (properties.isUseNativeDecoding()) {
var schemaType = Optional.ofNullable(properties.getExtension().getSchemaType()).orElse(SchemaType.NONE);
var schema = this.schemaResolver
.resolveSchema(schemaType, properties.getExtension().getMessageType(),
properties.getExtension().getMessageKeyType())
.orElseThrow(() -> "Could not determine consumer schema for " + destination.getName());
containerProperties.setSchema(schema);
}
else {
containerProperties.setSchema(Schema.BYTES);
}
var subscriptionName = PulsarBinderUtils.subscriptionName(properties.getExtension(), destination);
containerProperties.setSubscriptionName(subscriptionName);
var baseConsumerProps = new ConsumerConfigProperties().buildProperties();
var binderConsumerProps = this.binderConfigProps.getConsumer().buildProperties();
var bindingConsumerProps = properties.getExtension().buildProperties();
var mergedConsumerProps = PulsarBinderUtils.mergePropertiesWithPrecedence(baseConsumerProps,
binderConsumerProps, bindingConsumerProps);
containerProperties.getPulsarConsumerProperties().putAll(mergedConsumerProps);
containerProperties.updateContainerProperties();
var container = new DefaultPulsarMessageListenerContainer<>(this.pulsarConsumerFactory, containerProperties);
messageDrivenChannelAdapter.setMessageListenerContainer(container);
return messageDrivenChannelAdapter;
}
@Nullable
private PulsarBinderHeaderMapper determineInboundHeaderMapper(
ExtendedConsumerProperties<PulsarConsumerProperties> extConsumerProps) {
if (HeaderMode.none.equals(extConsumerProps.getHeaderMode())) {
return null;
}
return new PulsarBinderHeaderMapper(this.headerMapper);
}
@Override
public PulsarConsumerProperties getExtendedConsumerProperties(String channelName) {
return this.extendedBindingProperties.getExtendedConsumerProperties(channelName);
}
@Override
public PulsarProducerProperties getExtendedProducerProperties(String channelName) {
return this.extendedBindingProperties.getExtendedProducerProperties(channelName);
}
@Override
public String getDefaultsPrefix() {
return null;
}
@Override
public Class<? extends BinderSpecificPropertiesProvider> getExtendedPropertiesEntryClass() {
return null;
}
public PulsarExtendedBindingProperties getExtendedBindingProperties() {
return this.extendedBindingProperties;
}
public void setExtendedBindingProperties(PulsarExtendedBindingProperties extendedBindingProperties) {
this.extendedBindingProperties = extendedBindingProperties;
}
static class PulsarMessageDrivenChannelAdapter extends MessageProducerSupport {
AbstractPulsarMessageListenerContainer<?> messageListenerContainer;
public void send(Message<?> message) {
sendMessage(message);
}
@Override
protected void doStart() {
this.messageListenerContainer.start();
}
@Override
protected void doStop() {
this.messageListenerContainer.stop();
}
public void setMessageListenerContainer(AbstractPulsarMessageListenerContainer<?> messageListenerContainer) {
this.messageListenerContainer = messageListenerContainer;
}
}
static class PulsarProducerConfigurationMessageHandler extends AbstractMessageProducingHandler
implements ManageableLifecycle {
private final PulsarTemplate<Object> pulsarTemplate;
private final Schema<Object> schema;
private final String destination;
private final ProducerBuilderCustomizer<Object> layeredProducerPropsCustomizer;
private final PulsarHeaderMapper headerMapper;
private boolean running = true;
PulsarProducerConfigurationMessageHandler(PulsarTemplate<Object> pulsarTemplate, Schema<Object> schema,
String destination, ProducerBuilderCustomizer<Object> layeredProducerPropsCustomizer,
PulsarHeaderMapper headerMapper) {
this.pulsarTemplate = pulsarTemplate;
this.schema = schema;
this.destination = destination;
this.layeredProducerPropsCustomizer = layeredProducerPropsCustomizer;
this.headerMapper = headerMapper;
}
@Override
public void start() {
try {
super.onInit();
}
catch (Exception ex) {
this.logger.error(ex, "Initialization errors: ");
throw new RuntimeException(ex);
}
}
@Override
public void stop() {
// TODO - should we close the underlyiung producer?
this.running = false;
}
@Override
public boolean isRunning() {
return this.running;
}
@Override
protected void handleMessageInternal(Message<?> message) {
try {
// @formatter:off
this.pulsarTemplate.newMessage(message.getPayload())
.withTopic(this.destination)
.withSchema(this.schema)
.withProducerCustomizer(this.layeredProducerPropsCustomizer)
.withMessageCustomizer(this.applySpringHeadersAsPulsarProperties(message.getHeaders()))
.sendAsync();
// @formatter:on
}
catch (PulsarClientException ex) {
logger.trace(ex, "Failed to send message to destination: " + this.destination);
}
}
private TypedMessageBuilderCustomizer<Object> applySpringHeadersAsPulsarProperties(MessageHeaders headers) {
return (mb) -> {
if (this.headerMapper != null) {
this.headerMapper.toPulsarHeaders(headers).forEach(mb::property);
}
};
}
}
}

View File

@@ -1,75 +0,0 @@
/*
* Copyright 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.spring.cloud.stream.binder.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.stream.binder.Binder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.pulsar.autoconfigure.PulsarProperties;
import org.springframework.pulsar.core.PulsarAdministration;
import org.springframework.pulsar.core.PulsarConsumerFactory;
import org.springframework.pulsar.core.PulsarTemplate;
import org.springframework.pulsar.core.SchemaResolver;
import org.springframework.pulsar.spring.cloud.stream.binder.PulsarMessageChannelBinder;
import org.springframework.pulsar.spring.cloud.stream.binder.properties.PulsarBinderConfigurationProperties;
import org.springframework.pulsar.spring.cloud.stream.binder.properties.PulsarExtendedBindingProperties;
import org.springframework.pulsar.spring.cloud.stream.binder.provisioning.PulsarTopicProvisioner;
import org.springframework.pulsar.support.header.JacksonUtils;
import org.springframework.pulsar.support.header.JsonPulsarHeaderMapper;
import org.springframework.pulsar.support.header.PulsarHeaderMapper;
import org.springframework.pulsar.support.header.ToStringPulsarHeaderMapper;
/**
* Pulsar binder {@link Configuration}.
*
* @author Soby Chacko
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(Binder.class)
@EnableConfigurationProperties({ PulsarProperties.class, PulsarExtendedBindingProperties.class,
PulsarBinderConfigurationProperties.class })
public class PulsarBinderConfiguration {
@Bean
public PulsarTopicProvisioner pulsarTopicProvisioner(PulsarAdministration pulsarAdministration,
PulsarBinderConfigurationProperties pulsarBinderConfigurationProperties) {
return new PulsarTopicProvisioner(pulsarAdministration, pulsarBinderConfigurationProperties);
}
@Bean
@ConditionalOnMissingBean
public PulsarHeaderMapper pulsarHeaderMapper() {
if (JacksonUtils.isJacksonPresent()) {
return JsonPulsarHeaderMapper.builder().build();
}
return new ToStringPulsarHeaderMapper();
}
@Bean
public PulsarMessageChannelBinder pulsarMessageChannelBinder(PulsarTopicProvisioner pulsarTopicProvisioner,
PulsarTemplate<Object> pulsarTemplate, PulsarConsumerFactory<byte[]> pulsarConsumerFactory,
PulsarBinderConfigurationProperties binderConfigProps, PulsarExtendedBindingProperties bindingConfigProps,
SchemaResolver schemaResolver, PulsarHeaderMapper headerMapper) {
PulsarMessageChannelBinder pulsarMessageChannelBinder = new PulsarMessageChannelBinder(pulsarTopicProvisioner,
pulsarTemplate, pulsarConsumerFactory, binderConfigProps, schemaResolver, headerMapper);
pulsarMessageChannelBinder.setExtendedBindingProperties(bindingConfigProps);
return pulsarMessageChannelBinder;
}
}

View File

@@ -1,9 +0,0 @@
/**
* Package containing Spring Cloud Stream binder classes for Apache Pulsar.
*/
@NonNullApi
@NonNullFields
package org.springframework.pulsar.spring.cloud.stream.binder;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

View File

@@ -1,71 +0,0 @@
/*
* Copyright 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.spring.cloud.stream.binder.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import org.springframework.lang.Nullable;
import org.springframework.pulsar.autoconfigure.ConsumerConfigProperties;
import org.springframework.pulsar.autoconfigure.ProducerConfigProperties;
/**
* {@link ConfigurationProperties @ConfigurationProperties} for the Pulsar binder.
* <p>
* These properties are applied at the binder level (to all bindings).
*
* @author Soby Chacko
* @author Chris Bono
*/
@ConfigurationProperties(prefix = "spring.cloud.stream.pulsar.binder")
public class PulsarBinderConfigurationProperties {
/**
* Pulsar consumer specific binder-level properties (applied to all bindings).
*/
@NestedConfigurationProperty
private final ConsumerConfigProperties consumer = new ConsumerConfigProperties();
/**
* Pulsar producer specific binder-level properties (applied to all bindings).
*/
@NestedConfigurationProperty
private final ProducerConfigProperties producer = new ProducerConfigProperties();
/**
* Number of topic partitions.
*/
@Nullable
private Integer partitionCount;
public ConsumerConfigProperties getConsumer() {
return this.consumer;
}
public ProducerConfigProperties getProducer() {
return this.producer;
}
@Nullable
public Integer getPartitionCount() {
return this.partitionCount;
}
public void setPartitionCount(Integer partitionCount) {
this.partitionCount = partitionCount;
}
}

View File

@@ -1,71 +0,0 @@
/*
* Copyright 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.spring.cloud.stream.binder.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import org.springframework.cloud.stream.binder.BinderSpecificPropertiesProvider;
/**
* Container for Pulsar specific extended producer and consumer binding properties.
* <p>
* These properties are applied to individual bindings and will override any binder-level
* setting.
*
* <p>
* <em>NOTE:</em> This class is only referenced as a value in the
* {@link PulsarExtendedBindingProperties#getBindings() bindings map} and therefore, by
* default is not included in the generated configuration metadata. To get around this
* limitation it is annotated with {@code @ConfigurationProperties}. However, that is the
* only reason it is annotated and is not intended to be used directly.
*
* @author Soby Chacko
* @author Chris Bono
*/
@SuppressWarnings("ConfigurationProperties")
@ConfigurationProperties("spring.cloud.stream.pulsar.bindings.for-docs-only")
public class PulsarBindingProperties implements BinderSpecificPropertiesProvider {
/**
* Pulsar consumer specific binding properties.
*/
@NestedConfigurationProperty
private PulsarConsumerProperties consumer = new PulsarConsumerProperties();
/**
* Pulsar producer specific binding properties.
*/
@NestedConfigurationProperty
private PulsarProducerProperties producer = new PulsarProducerProperties();
public PulsarConsumerProperties getConsumer() {
return this.consumer;
}
public void setConsumer(PulsarConsumerProperties consumer) {
this.consumer = consumer;
}
public PulsarProducerProperties getProducer() {
return this.producer;
}
public void setProducer(PulsarProducerProperties producer) {
this.producer = producer;
}
}

View File

@@ -1,93 +0,0 @@
/*
* Copyright 2022-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.spring.cloud.stream.binder.properties;
import org.apache.pulsar.common.schema.SchemaType;
import org.springframework.lang.Nullable;
import org.springframework.pulsar.autoconfigure.ConsumerConfigProperties;
/**
* Pulsar consumer properties used by the binder.
*
* @author Soby Chacko
* @author Chris Bono
*/
public class PulsarConsumerProperties extends ConsumerConfigProperties {
/**
* Pulsar {@link SchemaType} for this binding.
*/
@Nullable
private SchemaType schemaType;
/**
* Pulsar message type for this binding.
*/
@Nullable
private Class<?> messageType;
/**
* Pulsar message key type for this binding (only used when schema type is
* {@code }KEY_VALUE}).
*/
@Nullable
private Class<?> messageKeyType;
/**
* Number of topic partitions.
*/
@Nullable
private Integer partitionCount;
@Nullable
public SchemaType getSchemaType() {
return this.schemaType;
}
public void setSchemaType(@Nullable SchemaType schemaType) {
this.schemaType = schemaType;
}
@Nullable
public Class<?> getMessageType() {
return this.messageType;
}
public void setMessageType(@Nullable Class<?> messageType) {
this.messageType = messageType;
}
@Nullable
public Class<?> getMessageKeyType() {
return this.messageKeyType;
}
public void setMessageKeyType(@Nullable Class<?> messageKeyType) {
this.messageKeyType = messageKeyType;
}
@Nullable
public Integer getPartitionCount() {
return this.partitionCount;
}
public void setPartitionCount(Integer partitionCount) {
this.partitionCount = partitionCount;
}
}

View File

@@ -1,59 +0,0 @@
/*
* Copyright 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.spring.cloud.stream.binder.properties;
import java.util.Map;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.stream.binder.AbstractExtendedBindingProperties;
import org.springframework.cloud.stream.binder.BinderSpecificPropertiesProvider;
/**
* {@link ConfigurationProperties @ConfigurationProperties} for Pulsar binder specific
* extensions to the common binding properties.
* <p>
* These properties are applied to individual bindings and will override any binder-level
* settings.
*
* @author Soby Chacko
* @author Chris Bono
*/
@ConfigurationProperties("spring.cloud.stream.pulsar")
public class PulsarExtendedBindingProperties extends
AbstractExtendedBindingProperties<PulsarConsumerProperties, PulsarProducerProperties, PulsarBindingProperties> {
private static final String DEFAULTS_PREFIX = "spring.cloud.stream.pulsar.default";
@Override
public String getDefaultsPrefix() {
return DEFAULTS_PREFIX;
}
/**
* Properties per individual binding name (e.g. 'mySink-in-0').
*/
@Override
public Map<String, PulsarBindingProperties> getBindings() {
return this.doGetBindings();
}
@Override
public Class<? extends BinderSpecificPropertiesProvider> getExtendedPropertiesEntryClass() {
return PulsarBindingProperties.class;
}
}

View File

@@ -1,93 +0,0 @@
/*
* Copyright 2022-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.spring.cloud.stream.binder.properties;
import org.apache.pulsar.common.schema.SchemaType;
import org.springframework.lang.Nullable;
import org.springframework.pulsar.autoconfigure.ProducerConfigProperties;
/**
* Pulsar producer properties used by the binder.
*
* @author Soby Chacko
* @author Chris Bono
*/
public class PulsarProducerProperties extends ProducerConfigProperties {
/**
* Pulsar {@link SchemaType} for this binding.
*/
@Nullable
private SchemaType schemaType;
/**
* Pulsar message type for this binding.
*/
@Nullable
private Class<?> messageType;
/**
* Pulsar message key type for this binding (only used when schema type is
* {@code }KEY_VALUE}).
*/
@Nullable
private Class<?> messageKeyType;
/**
* Number of topic partitions.
*/
@Nullable
private Integer partitionCount;
@Nullable
public SchemaType getSchemaType() {
return this.schemaType;
}
public void setSchemaType(@Nullable SchemaType schemaType) {
this.schemaType = schemaType;
}
@Nullable
public Class<?> getMessageType() {
return this.messageType;
}
public void setMessageType(@Nullable Class<?> messageType) {
this.messageType = messageType;
}
@Nullable
public Class<?> getMessageKeyType() {
return this.messageKeyType;
}
public void setMessageKeyType(@Nullable Class<?> messageKeyType) {
this.messageKeyType = messageKeyType;
}
@Nullable
public Integer getPartitionCount() {
return this.partitionCount;
}
public void setPartitionCount(Integer partitionCount) {
this.partitionCount = partitionCount;
}
}

View File

@@ -1,9 +0,0 @@
/**
* Package containing Spring Cloud Stream binder properties classes for Apache Pulsar.
*/
@NonNullApi
@NonNullFields
package org.springframework.pulsar.spring.cloud.stream.binder.properties;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

View File

@@ -1,94 +0,0 @@
/*
* Copyright 2022-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.spring.cloud.stream.binder.provisioning;
import org.springframework.cloud.stream.binder.ExtendedConsumerProperties;
import org.springframework.cloud.stream.binder.ExtendedProducerProperties;
import org.springframework.cloud.stream.provisioning.ConsumerDestination;
import org.springframework.cloud.stream.provisioning.ProducerDestination;
import org.springframework.cloud.stream.provisioning.ProvisioningException;
import org.springframework.cloud.stream.provisioning.ProvisioningProvider;
import org.springframework.lang.Nullable;
import org.springframework.pulsar.core.PulsarAdministration;
import org.springframework.pulsar.core.PulsarTopic;
import org.springframework.pulsar.spring.cloud.stream.binder.properties.PulsarBinderConfigurationProperties;
import org.springframework.pulsar.spring.cloud.stream.binder.properties.PulsarConsumerProperties;
import org.springframework.pulsar.spring.cloud.stream.binder.properties.PulsarProducerProperties;
/**
* Pulsar topic provisioner.
*
* @author Soby Chacko
*/
public class PulsarTopicProvisioner implements
ProvisioningProvider<ExtendedConsumerProperties<PulsarConsumerProperties>, ExtendedProducerProperties<PulsarProducerProperties>> {
private final PulsarAdministration pulsarAdministration;
private final PulsarBinderConfigurationProperties pulsarBinderConfigurationProperties;
public PulsarTopicProvisioner(PulsarAdministration pulsarAdministration,
PulsarBinderConfigurationProperties pulsarBinderConfigurationProperties) {
this.pulsarAdministration = pulsarAdministration;
this.pulsarBinderConfigurationProperties = pulsarBinderConfigurationProperties;
}
@Override
public ProducerDestination provisionProducerDestination(String name,
ExtendedProducerProperties<PulsarProducerProperties> pulsarProducerProperties)
throws ProvisioningException {
Integer partitionCountFromBinding = pulsarProducerProperties.getExtension().getPartitionCount();
var partitionCount = getPartitionCount(partitionCountFromBinding);
var pulsarTopic = PulsarTopic.builder(name).numberOfPartitions(partitionCount).build();
this.pulsarAdministration.createOrModifyTopics(pulsarTopic);
return new PulsarDestination(pulsarTopic.topicName(), pulsarTopic.numberOfPartitions());
}
private int getPartitionCount(@Nullable Integer partitionCountConfig) {
var partitionCount = this.pulsarBinderConfigurationProperties.getPartitionCount();
if (partitionCountConfig != null && partitionCountConfig > 0) {
partitionCount = partitionCountConfig;
}
return partitionCount == null ? 0 : partitionCount;
}
@Override
public ConsumerDestination provisionConsumerDestination(String name, String group,
ExtendedConsumerProperties<PulsarConsumerProperties> pulsarConsumerProperties)
throws ProvisioningException {
var partitionCountFromBinding = pulsarConsumerProperties.getExtension().getPartitionCount();
var partitionCount = getPartitionCount(partitionCountFromBinding);
var pulsarTopic = PulsarTopic.builder(name).numberOfPartitions(partitionCount).build();
this.pulsarAdministration.createOrModifyTopics(pulsarTopic);
return new PulsarDestination(pulsarTopic.topicName(), pulsarTopic.numberOfPartitions());
}
private record PulsarDestination(String destinationName,
Integer partitions) implements ProducerDestination, ConsumerDestination {
@Override
public String getName() {
return this.destinationName;
}
@Override
public String getNameForPartition(int partition) {
return this.destinationName;
}
}
}

View File

@@ -1,9 +0,0 @@
/**
* Package containing Spring Cloud Stream binder provisioning classes for Apache Pulsar.
*/
@NonNullApi
@NonNullFields
package org.springframework.pulsar.spring.cloud.stream.binder.provisioning;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

View File

@@ -1,10 +0,0 @@
{
"groups": [],
"properties": [
{
"name": "spring.cloud.stream.pulsar.bindings",
"description": "Properties per individual binding name (e.g. 'mySink-in-0'). Replace the '*' ' with the name of your binding."
}
],
"hints": []
}

View File

@@ -1,2 +0,0 @@
pulsar:\
org.springframework.pulsar.spring.cloud.stream.binder.config.PulsarBinderConfiguration

View File

@@ -1,48 +0,0 @@
/*
* Copyright 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.spring.cloud.stream.binder;
import org.springframework.cloud.stream.binder.AbstractPollableConsumerTestBinder;
import org.springframework.cloud.stream.binder.ExtendedConsumerProperties;
import org.springframework.cloud.stream.binder.ExtendedProducerProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.pulsar.spring.cloud.stream.binder.properties.PulsarConsumerProperties;
import org.springframework.pulsar.spring.cloud.stream.binder.properties.PulsarProducerProperties;
/**
* Base class for {@link PulsarTestBinder}.
*
* @author Soby Chacko
*/
public abstract class AbstractPulsarTestBinder extends
AbstractPollableConsumerTestBinder<PulsarMessageChannelBinder, ExtendedConsumerProperties<PulsarConsumerProperties>, ExtendedProducerProperties<PulsarProducerProperties>> {
private ApplicationContext applicationContext;
@Override
public void cleanup() {
}
protected final void setApplicationContext(ApplicationContext context) {
this.applicationContext = context;
}
public ApplicationContext getApplicationContext() {
return this.applicationContext;
}
}

View File

@@ -1,121 +0,0 @@
/*
* Copyright 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.spring.cloud.stream.binder;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatNoException;
import java.util.HashMap;
import java.util.Map;
import org.apache.pulsar.client.api.ProducerAccessMode;
import org.apache.pulsar.client.api.SubscriptionMode;
import org.apache.pulsar.client.impl.conf.ConfigurationDataUtils;
import org.apache.pulsar.client.impl.conf.ConsumerConfigurationData;
import org.apache.pulsar.client.impl.conf.ProducerConfigurationData;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
import org.springframework.pulsar.spring.cloud.stream.binder.properties.PulsarBinderConfigurationProperties;
/**
* Tests for {@link PulsarBinderConfigurationProperties}.
*
* @author Chris Bono
*/
public class PulsarBinderConfigurationPropertiesTests {
private final PulsarBinderConfigurationProperties properties = new PulsarBinderConfigurationProperties();
private void bind(Map<String, String> map) {
ConfigurationPropertySource source = new MapConfigurationPropertySource(map);
new Binder(source).bind("spring.cloud.stream.pulsar.binder", Bindable.ofInstance(this.properties));
}
@Test
void partitionCountProperty() {
assertThat(properties.getPartitionCount()).isNull();
bind(Map.of("spring.cloud.stream.pulsar.binder.partition-count", "5150"));
assertThat(properties.getPartitionCount()).isEqualTo(5150);
}
@Test
void producerProperties() {
// Only spot check a few values (PulsarPropertiesTests does the heavy lifting)
Map<String, String> props = new HashMap<>();
props.put("spring.cloud.stream.pulsar.binder.producer.topic-name", "my-topic");
props.put("spring.cloud.stream.pulsar.binder.producer.send-timeout", "2s");
props.put("spring.cloud.stream.pulsar.binder.producer.max-pending-messages", "3");
props.put("spring.cloud.stream.pulsar.binder.producer.producer-access-mode", "exclusive");
props.put("spring.cloud.stream.pulsar.binder.producer.properties[my-prop]", "my-prop-value");
bind(props);
Map<String, Object> producerProps = properties.getProducer().buildProperties();
// Verify that the props can be loaded in a ProducerBuilder
assertThatNoException().isThrownBy(() -> ConfigurationDataUtils.loadData(producerProps,
new ProducerConfigurationData(), ProducerConfigurationData.class));
// @formatter:off
assertThat(producerProps)
.containsEntry("topicName", "my-topic")
.containsEntry("sendTimeoutMs", 2_000)
.containsEntry("maxPendingMessages", 3)
.containsEntry("accessMode", ProducerAccessMode.Exclusive)
.hasEntrySatisfying("properties", properties ->
assertThat(properties)
.asInstanceOf(InstanceOfAssertFactories.map(String.class, String.class))
.containsEntry("my-prop", "my-prop-value"));
// @formatter:on
}
@Test
void consumerProperties() {
// Only spot check a few values (PulsarPropertiesTests does the heavy lifting)
Map<String, String> props = new HashMap<>();
props.put("spring.cloud.stream.pulsar.binder.consumer.topics[0]", "my-topic");
props.put("spring.cloud.stream.pulsar.binder.consumer.subscription-properties[my-sub-prop]",
"my-sub-prop-value");
props.put("spring.cloud.stream.pulsar.binder.consumer.subscription-mode", "nondurable");
props.put("spring.cloud.stream.pulsar.binder.consumer.receiver-queue-size", "1");
bind(props);
Map<String, Object> consumerProps = properties.getConsumer().buildProperties();
// Verify that the props can be loaded in a ConsumerBuilder
assertThatNoException().isThrownBy(() -> ConfigurationDataUtils.loadData(consumerProps,
new ConsumerConfigurationData<>(), ConsumerConfigurationData.class));
// @formatter:off
assertThat(consumerProps)
.hasEntrySatisfying("topicNames",
topics -> assertThat(topics).asInstanceOf(InstanceOfAssertFactories.collection(String.class))
.containsExactly("my-topic"))
.hasEntrySatisfying("subscriptionProperties",
properties -> assertThat(properties)
.asInstanceOf(InstanceOfAssertFactories.map(String.class, String.class))
.containsEntry("my-sub-prop", "my-sub-prop-value"))
.containsEntry("subscriptionMode", SubscriptionMode.NonDurable)
.containsEntry("receiverQueueSize", 1);
// @formatter:on
}
}

View File

@@ -1,115 +0,0 @@
/*
* Copyright 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.spring.cloud.stream.binder;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.AssertionsForClassTypes.entry;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import java.util.HashMap;
import java.util.Map;
import org.apache.pulsar.client.api.Message;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.cloud.stream.binder.BinderHeaders;
import org.springframework.integration.IntegrationMessageHeaderAccessor;
import org.springframework.messaging.MessageHeaders;
import org.springframework.pulsar.support.header.PulsarHeaderMapper;
/**
* Tests for {@link PulsarBinderHeaderMapper}.
*
* @author Chris Bono
*/
@ExtendWith(MockitoExtension.class)
class PulsarBinderHeaderMapperTests {
@Mock
private PulsarHeaderMapper delegateMapper;
@InjectMocks
private PulsarBinderHeaderMapper binderHeaderMapper;
@Nested
class ToPulsarHeadersOutboundTests {
@Test
void delegateReturnsEmptyHeaders() {
var delegatePulsarHeaders = new HashMap<String, String>();
when(delegateMapper.toPulsarHeaders(any(MessageHeaders.class))).thenReturn(delegatePulsarHeaders);
var springHeaders = mock(MessageHeaders.class);
var pulsarHeaders = binderHeaderMapper.toPulsarHeaders(springHeaders);
verify(delegateMapper).toPulsarHeaders(springHeaders);
assertThat(pulsarHeaders).isEmpty();
}
@Test
void neverHeadersRemovedFromDelegateHeaders() {
var delegatePulsarHeaders = new HashMap<String, String>();
delegatePulsarHeaders.put(MessageHeaders.ID, "5150");
delegatePulsarHeaders.put(MessageHeaders.TIMESTAMP, "12345");
delegatePulsarHeaders.put(IntegrationMessageHeaderAccessor.DELIVERY_ATTEMPT, "5");
delegatePulsarHeaders.put(BinderHeaders.NATIVE_HEADERS_PRESENT, "true");
delegatePulsarHeaders.put("foo", "bar");
when(delegateMapper.toPulsarHeaders(any(MessageHeaders.class))).thenReturn(delegatePulsarHeaders);
var springHeaders = mock(MessageHeaders.class);
var pulsarHeaders = binderHeaderMapper.toPulsarHeaders(springHeaders);
verify(delegateMapper).toPulsarHeaders(springHeaders);
assertThat(pulsarHeaders).containsOnly(entry("foo", "bar"));
}
}
@Nested
class ToSpringHeadersInboundTests {
@Test
void delegateReturnsEmptyHeaders() {
var emptyDelegateHeaders = mock(MessageHeaders.class);
when(emptyDelegateHeaders.isEmpty()).thenReturn(true);
when(delegateMapper.toSpringHeaders(any(Message.class))).thenReturn(emptyDelegateHeaders);
var springHeaders = binderHeaderMapper.toSpringHeaders(mock(Message.class));
assertThat(springHeaders).isSameAs(emptyDelegateHeaders);
verify(springHeaders).isEmpty();
verifyNoMoreInteractions(springHeaders);
}
@Test
void nativeHeadersIndicatorAddedToDelegateHeaders() {
var delegateSpringHeaders = new MessageHeaders(Map.of("foo", "bar"));
when(delegateMapper.toSpringHeaders(any(Message.class))).thenReturn(delegateSpringHeaders);
var pulsarMessage = mock(Message.class);
var springHeaders = binderHeaderMapper.toSpringHeaders(pulsarMessage);
verify(delegateMapper).toSpringHeaders(pulsarMessage);
assertThat(springHeaders).containsEntry("foo", "bar").containsEntry(BinderHeaders.NATIVE_HEADERS_PRESENT,
Boolean.TRUE);
}
}
}

View File

@@ -1,885 +0,0 @@
/*
* Copyright 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.spring.cloud.stream.binder;
import static org.assertj.core.api.Assertions.assertThat;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Supplier;
import org.apache.pulsar.client.api.Producer;
import org.apache.pulsar.client.api.PulsarClient;
import org.apache.pulsar.client.api.PulsarClientException;
import org.apache.pulsar.client.api.Schema;
import org.apache.pulsar.client.impl.schema.JSONSchema;
import org.apache.pulsar.common.schema.KeyValue;
import org.awaitility.Awaitility;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.pulsar.autoconfigure.PulsarProperties;
import org.springframework.pulsar.core.ConsumerBuilderCustomizer;
import org.springframework.pulsar.core.DefaultPulsarConsumerFactory;
import org.springframework.pulsar.core.DefaultPulsarProducerFactory;
import org.springframework.pulsar.core.DefaultSchemaResolver;
import org.springframework.pulsar.core.ProducerBuilderCustomizer;
import org.springframework.pulsar.core.PulsarConsumerFactory;
import org.springframework.pulsar.core.PulsarProducerFactory;
import org.springframework.pulsar.core.SchemaResolver.SchemaResolverCustomizer;
import org.springframework.pulsar.core.TopicResolver;
import org.springframework.pulsar.support.header.PulsarHeaderMapper;
import org.springframework.pulsar.support.header.ToStringPulsarHeaderMapper;
import org.springframework.pulsar.test.support.PulsarTestContainerSupport;
/**
* Integration tests for {@link PulsarBinderIntegrationTests}.
*
* @author Soby Chacko
* @author Chris Bono
*/
@ExtendWith(OutputCaptureExtension.class)
@SuppressWarnings("JUnitMalformedDeclaration")
class PulsarBinderIntegrationTests implements PulsarTestContainerSupport {
private static final int AWAIT_DURATION = 10;
@Test
void binderAndBindingPropsAreAppliedAndRespected(CapturedOutput output) {
SpringApplication app = new SpringApplication(BinderAndBindingPropsTestConfig.class);
app.setWebApplicationType(WebApplicationType.NONE);
try (ConfigurableApplicationContext context = app.run(
"--spring.pulsar.client.service-url=" + PulsarTestContainerSupport.getPulsarBrokerUrl(),
"--spring.pulsar.administration.service-url=" + PulsarTestContainerSupport.getHttpServiceUrl(),
"--spring.cloud.function.definition=textSupplier;textLogger",
"--spring.cloud.stream.bindings.textLogger-in-0.destination=textSupplier-out-0",
"--spring.pulsar.producer.producer-name=textSupplierProducer-fromBase",
"--spring.cloud.stream.pulsar.binder.producer.producer-name=textSupplierProducer-fromBinder",
"--spring.cloud.stream.pulsar.bindings.textSupplier-out-0.producer.producer-name=textSupplierProducer-fromBinding",
"--spring.cloud.stream.pulsar.binder.producer.max-pending-messages=1100",
"--spring.pulsar.producer.block-if-queue-full=true",
"--spring.cloud.stream.pulsar.binder.consumer.subscription-name=textLoggerSub-fromBinder",
"--spring.cloud.stream.pulsar.binder.consumer.consumer-name=textLogger-fromBinder",
"--spring.cloud.stream.pulsar.bindings.textLogger-in-0.consumer.consumer-name=textLogger-fromBinding")) {
Awaitility.await().atMost(Duration.ofSeconds(AWAIT_DURATION))
.until(() -> output.toString().contains("Hello binder: test-basic-scenario"));
// now verify the properties were set onto producer and consumer as expected
TrackingProducerFactory producerFactory = context.getBean(TrackingProducerFactory.class);
assertThat(producerFactory.producersCreated).isNotEmpty().element(0)
.hasFieldOrPropertyWithValue("producerName", "textSupplierProducer-fromBinding")
.hasFieldOrPropertyWithValue("conf.maxPendingMessages", 1100)
.hasFieldOrPropertyWithValue("conf.blockIfQueueFull", true);
TrackingConsumerFactory consumerFactory = context.getBean(TrackingConsumerFactory.class);
assertThat(consumerFactory.consumersCreated).isNotEmpty().element(0)
.hasFieldOrPropertyWithValue("consumerName", "textLogger-fromBinding")
.hasFieldOrPropertyWithValue("conf.subscriptionName", "textLoggerSub-fromBinder");
}
}
@Nested
class DefaultEncoding {
@Test
void primitiveTypeString(CapturedOutput output) {
SpringApplication app = new SpringApplication(PrimitiveTextConfig.class);
app.setWebApplicationType(WebApplicationType.NONE);
try (ConfigurableApplicationContext ignored = app.run(
"--spring.pulsar.client.service-url=" + PulsarTestContainerSupport.getPulsarBrokerUrl(),
"--spring.pulsar.administration.service-url=" + PulsarTestContainerSupport.getHttpServiceUrl(),
"--spring.cloud.function.definition=textSupplier;textLogger",
"--spring.cloud.stream.bindings.textLogger-in-0.destination=textSupplier-out-0",
"--spring.cloud.stream.pulsar.bindings.textLogger-in-0.consumer.subscription-name=pbit-text-sub1")) {
Awaitility.await().atMost(Duration.ofSeconds(AWAIT_DURATION))
.until(() -> output.toString().contains("Hello binder: test-basic-scenario"));
}
}
@Test
void primitiveTypeFloat(CapturedOutput output) {
SpringApplication app = new SpringApplication(PrimitiveFloatConfig.class);
app.setWebApplicationType(WebApplicationType.NONE);
try (ConfigurableApplicationContext ignored = app.run(
"--spring.pulsar.client.service-url=" + PulsarTestContainerSupport.getPulsarBrokerUrl(),
"--spring.pulsar.administration.service-url=" + PulsarTestContainerSupport.getHttpServiceUrl(),
"--spring.cloud.function.definition=piSupplier;piLogger",
"--spring.cloud.stream.bindings.piSupplier-out-0.destination=pi-stream",
"--spring.cloud.stream.bindings.piLogger-in-0.destination=pi-stream",
"--spring.cloud.stream.pulsar.bindings.piLogger-in-0.consumer.subscription-name=pbit-float-sub1")) {
Awaitility.await().atMost(Duration.ofSeconds(AWAIT_DURATION))
.until(() -> output.toString().contains("Hello binder: 3.14"));
}
}
}
@Nested
class NativeEncoding {
@Test
void primitiveTypeFloat(CapturedOutput output) {
SpringApplication app = new SpringApplication(PrimitiveFloatConfig.class);
app.setWebApplicationType(WebApplicationType.NONE);
try (ConfigurableApplicationContext ignored = app.run(
"--spring.pulsar.client.service-url=" + PulsarTestContainerSupport.getPulsarBrokerUrl(),
"--spring.pulsar.administration.service-url=" + PulsarTestContainerSupport.getHttpServiceUrl(),
"--spring.cloud.function.definition=piSupplier;piLogger",
"--spring.cloud.stream.bindings.piLogger-in-0.destination=piSupplier-out-0",
"--spring.cloud.stream.bindings.piSupplier-out-0.producer.use-native-encoding=true",
"--spring.cloud.stream.pulsar.bindings.piSupplier-out-0.producer.schema-type=FLOAT",
"--spring.cloud.stream.bindings.piLogger-in-0.consumer.use-native-decoding=true",
"--spring.cloud.stream.pulsar.bindings.piLogger-in-0.consumer.schema-type=FLOAT",
"--spring.cloud.stream.pulsar.bindings.piLogger-in-0.consumer.subscription-name=pbit-float-sub2")) {
Awaitility.await().atMost(Duration.ofSeconds(AWAIT_DURATION))
.until(() -> output.toString().contains("Hello binder: 3.14"));
}
}
@Test
void jsonTypeFooWithSchemaType(CapturedOutput output) {
SpringApplication app = new SpringApplication(JsonFooConfig.class);
app.setWebApplicationType(WebApplicationType.NONE);
try (ConfigurableApplicationContext ignored = app.run(
"--spring.pulsar.client.service-url=" + PulsarTestContainerSupport.getPulsarBrokerUrl(),
"--spring.pulsar.administration.service-url=" + PulsarTestContainerSupport.getHttpServiceUrl(),
"--spring.cloud.function.definition=fooSupplier;fooLogger",
"--spring.cloud.stream.bindings.fooSupplier-out-0.destination=foo-stream-1",
"--spring.cloud.stream.bindings.fooLogger-in-0.destination=foo-stream-1",
"--spring.cloud.stream.bindings.fooSupplier-out-0.producer.use-native-encoding=true",
"--spring.cloud.stream.pulsar.bindings.fooSupplier-out-0.producer.schema-type=JSON",
"--spring.cloud.stream.pulsar.bindings.fooSupplier-out-0.producer.message-type="
+ Foo.class.getName(),
"--spring.cloud.stream.bindings.fooLogger-in-0.consumer.use-native-decoding=true",
"--spring.cloud.stream.pulsar.bindings.fooLogger-in-0.consumer.schema-type=JSON",
"--spring.cloud.stream.pulsar.bindings.fooLogger-in-0.consumer.message-type=" + Foo.class.getName(),
"--spring.cloud.stream.pulsar.bindings.fooLogger-in-0.consumer.subscription-name=pbit-foo-sub1")) {
Awaitility.await().atMost(Duration.ofSeconds(AWAIT_DURATION))
.until(() -> output.toString().contains("Hello binder: Foo[value=5150]"));
}
}
@Test
void jsonTypeFooWithoutSchemaTypeDefaultsToJsonSchema(CapturedOutput output) {
SpringApplication app = new SpringApplication(JsonFooConfig.class);
app.setWebApplicationType(WebApplicationType.NONE);
try (ConfigurableApplicationContext ignored = app.run(
"--spring.pulsar.client.service-url=" + PulsarTestContainerSupport.getPulsarBrokerUrl(),
"--spring.pulsar.administration.service-url=" + PulsarTestContainerSupport.getHttpServiceUrl(),
"--spring.cloud.function.definition=fooSupplier;fooLogger",
"--spring.cloud.stream.bindings.fooSupplier-out-0.destination=foo-stream-2",
"--spring.cloud.stream.bindings.fooLogger-in-0.destination=foo-stream-2",
"--spring.cloud.stream.bindings.fooSupplier-out-0.producer.use-native-encoding=true",
"--spring.cloud.stream.pulsar.bindings.fooSupplier-out-0.producer.message-type="
+ Foo.class.getName(),
"--spring.cloud.stream.bindings.fooLogger-in-0.consumer.use-native-decoding=true",
"--spring.cloud.stream.pulsar.bindings.fooLogger-in-0.consumer.message-type=" + Foo.class.getName(),
"--spring.cloud.stream.pulsar.bindings.fooLogger-in-0.consumer.subscription-name=pbit-foo-sub2")) {
Awaitility.await().atMost(Duration.ofSeconds(AWAIT_DURATION))
.until(() -> output.toString().contains("Hello binder: Foo[value=5150]"));
}
}
@Test
void avroTypeUserWithSchemaType(CapturedOutput output) {
SpringApplication app = new SpringApplication(AvroUserConfig.class);
app.setWebApplicationType(WebApplicationType.NONE);
try (ConfigurableApplicationContext ignored = app.run(
"--spring.pulsar.client.service-url=" + PulsarTestContainerSupport.getPulsarBrokerUrl(),
"--spring.pulsar.administration.service-url=" + PulsarTestContainerSupport.getHttpServiceUrl(),
"--spring.cloud.function.definition=userSupplier;userLogger",
"--spring.cloud.stream.bindings.userSupplier-out-0.destination=user-stream-1",
"--spring.cloud.stream.bindings.userLogger-in-0.destination=user-stream-1",
"--spring.cloud.stream.bindings.userSupplier-out-0.producer.use-native-encoding=true",
"--spring.cloud.stream.pulsar.bindings.userSupplier-out-0.producer.schema-type=AVRO",
"--spring.cloud.stream.pulsar.bindings.userSupplier-out-0.producer.message-type="
+ User.class.getName(),
"--spring.cloud.stream.bindings.userLogger-in-0.consumer.use-native-decoding=true",
"--spring.cloud.stream.pulsar.bindings.userLogger-in-0.consumer.schema-type=AVRO",
"--spring.cloud.stream.pulsar.bindings.userLogger-in-0.consumer.message-type="
+ User.class.getName(),
"--spring.cloud.stream.pulsar.bindings.userLogger-in-0.consumer.subscription-name=pbit-user-sub1")) {
Awaitility.await().atMost(Duration.ofSeconds(AWAIT_DURATION))
.until(() -> output.toString().contains("Hello binder: User{name='user21', age=21}"));
}
}
@Test
void avroTypeUserWithoutSchemaTypeWithCustomMappingsViaProps(CapturedOutput output) {
SpringApplication app = new SpringApplication(AvroUserConfig.class);
app.setWebApplicationType(WebApplicationType.NONE);
try (ConfigurableApplicationContext ignored = app.run(
"--spring.pulsar.client.service-url=" + PulsarTestContainerSupport.getPulsarBrokerUrl(),
"--spring.pulsar.administration.service-url=" + PulsarTestContainerSupport.getHttpServiceUrl(),
"--spring.cloud.function.definition=userSupplier;userLogger",
"--spring.cloud.stream.bindings.userSupplier-out-0.destination=user-stream-2",
"--spring.cloud.stream.bindings.userLogger-in-0.destination=user-stream-2",
"--spring.cloud.stream.bindings.userSupplier-out-0.producer.use-native-encoding=true",
"--spring.cloud.stream.pulsar.bindings.userSupplier-out-0.producer.message-type="
+ User.class.getName(),
"--spring.cloud.stream.bindings.userLogger-in-0.consumer.use-native-decoding=true",
"--spring.cloud.stream.pulsar.bindings.userLogger-in-0.consumer.message-type="
+ User.class.getName(),
"--spring.cloud.stream.pulsar.bindings.userLogger-in-0.consumer.subscription-name=pbit-user-sub2",
"--spring.pulsar.defaults.type-mappings[0].message-type=%s".formatted(User.class.getName()),
"--spring.pulsar.defaults.type-mappings[0].schema-info.schema-type=AVRO")) {
Awaitility.await().atMost(Duration.ofSeconds(AWAIT_DURATION))
.until(() -> output.toString().contains("Hello binder: User{name='user21', age=21}"));
}
}
@Test
void avroTypeUserWithoutSchemaTypeWithCustomMappingsViaCustomizer(CapturedOutput output) {
SpringApplication app = new SpringApplication(AvroUserConfigCustomMappings.class);
app.setWebApplicationType(WebApplicationType.NONE);
try (ConfigurableApplicationContext ignored = app.run(
"--spring.pulsar.client.service-url=" + PulsarTestContainerSupport.getPulsarBrokerUrl(),
"--spring.pulsar.administration.service-url=" + PulsarTestContainerSupport.getHttpServiceUrl(),
"--spring.cloud.function.definition=userSupplier;userLogger",
"--spring.cloud.stream.bindings.userSupplier-out-0.destination=user-stream-3",
"--spring.cloud.stream.bindings.userLogger-in-0.destination=user-stream-3",
"--spring.cloud.stream.bindings.userSupplier-out-0.producer.use-native-encoding=true",
"--spring.cloud.stream.pulsar.bindings.userSupplier-out-0.producer.message-type="
+ User.class.getName(),
"--spring.cloud.stream.bindings.userLogger-in-0.consumer.use-native-decoding=true",
"--spring.cloud.stream.pulsar.bindings.userLogger-in-0.consumer.message-type="
+ User.class.getName(),
"--spring.cloud.stream.pulsar.bindings.userLogger-in-0.consumer.subscription-name=pbit-user-sub3")) {
Awaitility.await().atMost(Duration.ofSeconds(AWAIT_DURATION))
.until(() -> output.toString().contains("Hello binder: User{name='user21', age=21}"));
}
}
@Test
void keyValueAvroTypeWithSchemaTypeAndCustomTypeMappingsViaProps(CapturedOutput output) {
SpringApplication app = new SpringApplication(KeyValueAvroUserConfig.class);
app.setWebApplicationType(WebApplicationType.NONE);
try (ConfigurableApplicationContext ignored = app.run(
"--spring.pulsar.client.service-url=" + PulsarTestContainerSupport.getPulsarBrokerUrl(),
"--spring.pulsar.administration.service-url=" + PulsarTestContainerSupport.getHttpServiceUrl(),
"--spring.cloud.function.definition=userSupplier;userLogger",
"--spring.cloud.stream.bindings.userSupplier-out-0.destination=kv-stream-1",
"--spring.cloud.stream.bindings.userLogger-in-0.destination=kv-stream-1",
"--spring.cloud.stream.bindings.userSupplier-out-0.producer.use-native-encoding=true",
"--spring.cloud.stream.pulsar.bindings.userSupplier-out-0.producer.schema-type=KEY_VALUE",
"--spring.cloud.stream.pulsar.bindings.userSupplier-out-0.producer.message-type="
+ User.class.getName(),
"--spring.cloud.stream.pulsar.bindings.userSupplier-out-0.producer.message-key-type="
+ String.class.getName(),
"--spring.cloud.stream.bindings.userLogger-in-0.consumer.use-native-decoding=true",
"--spring.cloud.stream.pulsar.bindings.userLogger-in-0.consumer.schema-type=KEY_VALUE",
"--spring.cloud.stream.pulsar.bindings.userLogger-in-0.consumer.message-type="
+ User.class.getName(),
"--spring.cloud.stream.pulsar.bindings.userLogger-in-0.consumer.message-key-type="
+ String.class.getName(),
"--spring.cloud.stream.pulsar.bindings.userLogger-in-0.consumer.subscription-name=pbit-kv-sub1",
"--spring.pulsar.defaults.type-mappings[0].message-type=%s".formatted(User.class.getName()),
"--spring.pulsar.defaults.type-mappings[0].schema-info.schema-type=AVRO")) {
Awaitility.await().atMost(Duration.ofSeconds(AWAIT_DURATION))
.until(() -> output.toString().contains("Hello binder: 21->User{name='user21', age=21}"));
}
}
@Test
void keyValueAvroTypeWithoutSchemaTypeAndCustomTypeMappingsViaProps(CapturedOutput output) {
SpringApplication app = new SpringApplication(KeyValueAvroUserConfig.class);
app.setWebApplicationType(WebApplicationType.NONE);
try (ConfigurableApplicationContext ignored = app.run(
"--spring.pulsar.client.service-url=" + PulsarTestContainerSupport.getPulsarBrokerUrl(),
"--spring.pulsar.administration.service-url=" + PulsarTestContainerSupport.getHttpServiceUrl(),
"--spring.cloud.function.definition=userSupplier;userLogger",
"--spring.cloud.stream.bindings.userSupplier-out-0.destination=kv-stream-2",
"--spring.cloud.stream.bindings.userLogger-in-0.destination=kv-stream-2",
"--spring.cloud.stream.bindings.userSupplier-out-0.producer.use-native-encoding=true",
"--spring.cloud.stream.pulsar.bindings.userSupplier-out-0.producer.message-type="
+ User.class.getName(),
"--spring.cloud.stream.pulsar.bindings.userSupplier-out-0.producer.message-key-type="
+ String.class.getName(),
"--spring.cloud.stream.bindings.userLogger-in-0.consumer.use-native-decoding=true",
"--spring.cloud.stream.pulsar.bindings.userLogger-in-0.consumer.message-type="
+ User.class.getName(),
"--spring.cloud.stream.pulsar.bindings.userLogger-in-0.consumer.message-key-type="
+ String.class.getName(),
"--spring.cloud.stream.pulsar.bindings.userLogger-in-0.consumer.subscription-name=pbit-kv-sub2",
"--spring.pulsar.defaults.type-mappings[0].message-type=%s".formatted(User.class.getName()),
"--spring.pulsar.defaults.type-mappings[0].schema-info.schema-type=AVRO")) {
Awaitility.await().atMost(Duration.ofSeconds(AWAIT_DURATION))
.until(() -> output.toString().contains("Hello binder: 21->User{name='user21', age=21}"));
}
}
@Test
void keyValueAvroTypeWithSchemaTypeAndCustomTypeMappingsViaCustomizer(CapturedOutput output) {
SpringApplication app = new SpringApplication(KeyValueAvroUserConfigCustomMappings.class);
app.setWebApplicationType(WebApplicationType.NONE);
try (ConfigurableApplicationContext ignored = app.run(
"--spring.pulsar.client.service-url=" + PulsarTestContainerSupport.getPulsarBrokerUrl(),
"--spring.pulsar.administration.service-url=" + PulsarTestContainerSupport.getHttpServiceUrl(),
"--spring.cloud.function.definition=userSupplier;userLogger",
"--spring.cloud.stream.bindings.userSupplier-out-0.destination=kv-stream-3",
"--spring.cloud.stream.bindings.userLogger-in-0.destination=kv-stream-3",
"--spring.cloud.stream.bindings.userSupplier-out-0.producer.use-native-encoding=true",
"--spring.cloud.stream.pulsar.bindings.userSupplier-out-0.producer.schema-type=KEY_VALUE",
"--spring.cloud.stream.pulsar.bindings.userSupplier-out-0.producer.message-type="
+ User.class.getName(),
"--spring.cloud.stream.pulsar.bindings.userSupplier-out-0.producer.message-key-type="
+ String.class.getName(),
"--spring.cloud.stream.bindings.userLogger-in-0.consumer.use-native-decoding=true",
"--spring.cloud.stream.pulsar.bindings.userLogger-in-0.consumer.schema-type=KEY_VALUE",
"--spring.cloud.stream.pulsar.bindings.userLogger-in-0.consumer.message-type="
+ User.class.getName(),
"--spring.cloud.stream.pulsar.bindings.userLogger-in-0.consumer.message-key-type="
+ String.class.getName(),
"--spring.cloud.stream.pulsar.bindings.userLogger-in-0.consumer.subscription-name=pbit-kv-sub3")) {
Awaitility.await().atMost(Duration.ofSeconds(AWAIT_DURATION))
.until(() -> output.toString().contains("Hello binder: 21->User{name='user21', age=21}"));
}
}
@Test
void keyValueJsonTypeWithoutSchemaTypeAndWithoutCustomTypeMappings(CapturedOutput output) {
SpringApplication app = new SpringApplication(KeyValueJsonFooConfig.class);
app.setWebApplicationType(WebApplicationType.NONE);
try (ConfigurableApplicationContext ignored = app.run(
"--spring.pulsar.client.service-url=" + PulsarTestContainerSupport.getPulsarBrokerUrl(),
"--spring.pulsar.administration.service-url=" + PulsarTestContainerSupport.getHttpServiceUrl(),
"--spring.cloud.function.definition=fooSupplier;fooLogger",
"--spring.cloud.stream.bindings.fooSupplier-out-0.destination=kv-stream-4",
"--spring.cloud.stream.bindings.fooLogger-in-0.destination=kv-stream-4",
"--spring.cloud.stream.bindings.fooSupplier-out-0.producer.use-native-encoding=true",
"--spring.cloud.stream.pulsar.bindings.fooSupplier-out-0.producer.message-type="
+ Foo.class.getName(),
"--spring.cloud.stream.pulsar.bindings.fooSupplier-out-0.producer.message-key-type="
+ String.class.getName(),
"--spring.cloud.stream.bindings.fooLogger-in-0.consumer.use-native-decoding=true",
"--spring.cloud.stream.pulsar.bindings.fooLogger-in-0.consumer.message-type=" + Foo.class.getName(),
"--spring.cloud.stream.pulsar.bindings.fooLogger-in-0.consumer.message-key-type="
+ String.class.getName(),
"--spring.cloud.stream.pulsar.bindings.fooLogger-in-0.consumer.subscription-name=pbit-kv-sub4")) {
Awaitility.await().atMost(Duration.ofSeconds(AWAIT_DURATION))
.until(() -> output.toString().contains("Hello binder: 5150->Foo[value=5150]"));
}
}
}
@Nested
class CustomMessageHeaders {
@Test
void headersPropagatedSendAndReceive(CapturedOutput output) {
SpringApplication app = new SpringApplication(CustomSimpleHeadersConfig.class);
app.setWebApplicationType(WebApplicationType.NONE);
try (ConfigurableApplicationContext ignored = app.run(
"--spring.pulsar.client.service-url=" + PulsarTestContainerSupport.getPulsarBrokerUrl(),
"--spring.pulsar.administration.service-url=" + PulsarTestContainerSupport.getHttpServiceUrl(),
"--spring.cloud.function.definition=springMessageSupplier;springMessageLogger",
"--spring.cloud.stream.bindings.springMessageSupplier-out-0.destination=cmh-1",
"--spring.cloud.stream.bindings.springMessageLogger-in-0.destination=cmh-1",
"--spring.cloud.stream.pulsar.bindings.springMessageLogger-in-0.consumer.subscription-name=pbit-cmh1-sub1")) {
// Wait for a few of the messages to flow through (check for index = 5)
Awaitility.await().atMost(Duration.ofSeconds(AWAIT_DURATION)).until(
() -> output.toString().contains("Hello binder: test-headers-msg-5 w/ custom-id: 5150-5"));
}
}
@Test
void complexHeadersAreEncodedAndPropagated(CapturedOutput output) {
SpringApplication app = new SpringApplication(CustomComplexHeadersConfig.class);
app.setWebApplicationType(WebApplicationType.NONE);
try (ConfigurableApplicationContext ignored = app.run(
"--spring.pulsar.client.service-url=" + PulsarTestContainerSupport.getPulsarBrokerUrl(),
"--spring.pulsar.administration.service-url=" + PulsarTestContainerSupport.getHttpServiceUrl(),
"--spring.cloud.function.definition=springMessageSupplier;springMessageLogger",
"--spring.cloud.stream.bindings.springMessageSupplier-out-0.destination=cmh-2",
"--spring.cloud.stream.bindings.springMessageLogger-in-0.destination=cmh-2",
"--spring.cloud.stream.pulsar.bindings.springMessageLogger-in-0.consumer.subscription-name=pbit-cmh2-sub1")) {
// Wait for a few of the messages to flow through (check for index = 5)
Awaitility.await().atMost(Duration.ofSeconds(AWAIT_DURATION)).until(() -> output.toString()
.contains("Hello binder: test-headers-msg-5 w/ custom-id: FooHeader[value=5150-5]"));
}
}
@Test
void producerHeaderModeNone(CapturedOutput output) {
SpringApplication app = new SpringApplication(CustomComplexHeadersConfig.class);
app.setWebApplicationType(WebApplicationType.NONE);
try (ConfigurableApplicationContext ignored = app.run(
"--spring.pulsar.client.service-url=" + PulsarTestContainerSupport.getPulsarBrokerUrl(),
"--spring.pulsar.administration.service-url=" + PulsarTestContainerSupport.getHttpServiceUrl(),
"--spring.cloud.function.definition=springMessageSupplier;springMessageLogger",
"--spring.cloud.stream.bindings.springMessageSupplier-out-0.destination=cmh-3",
"--spring.cloud.stream.bindings.springMessageSupplier-out-0.producer.header-mode=none",
"--spring.cloud.stream.bindings.springMessageLogger-in-0.destination=cmh-3",
"--spring.cloud.stream.pulsar.bindings.springMessageLogger-in-0.consumer.subscription-name=pbit-cmh3-sub1")) {
// Wait for a few of the messages to flow through (check for index = 5)
Awaitility.await().atMost(Duration.ofSeconds(AWAIT_DURATION))
.until(() -> output.toString().contains("Hello binder: test-headers-msg-5 w/ custom-id: null"));
}
}
@Test
void consumerHeaderModeNone(CapturedOutput output) {
SpringApplication app = new SpringApplication(CustomComplexHeadersConfig.class);
app.setWebApplicationType(WebApplicationType.NONE);
try (ConfigurableApplicationContext ignored = app.run(
"--spring.pulsar.client.service-url=" + PulsarTestContainerSupport.getPulsarBrokerUrl(),
"--spring.pulsar.administration.service-url=" + PulsarTestContainerSupport.getHttpServiceUrl(),
"--spring.cloud.function.definition=springMessageSupplier;springMessageLogger",
"--spring.cloud.stream.bindings.springMessageSupplier-out-0.destination=cmh-4",
"--spring.cloud.stream.bindings.springMessageLogger-in-0.destination=cmh-4",
"--spring.cloud.stream.bindings.springMessageLogger-in-0.consumer.header-mode=none",
"--spring.cloud.stream.pulsar.bindings.springMessageLogger-in-0.consumer.subscription-name=pbit-cmh4-sub1")) {
// Wait for a few of the messages to flow through (check for index = 5)
Awaitility.await().atMost(Duration.ofSeconds(AWAIT_DURATION))
.until(() -> output.toString().contains("Hello binder: test-headers-msg-5 w/ custom-id: null"));
}
}
@Test
void customHeaderMapperRespected(CapturedOutput output) {
SpringApplication app = new SpringApplication(CustomHeaderMapperConfig.class);
app.setWebApplicationType(WebApplicationType.NONE);
try (ConfigurableApplicationContext ignored = app.run(
"--spring.pulsar.client.service-url=" + PulsarTestContainerSupport.getPulsarBrokerUrl(),
"--spring.pulsar.administration.service-url=" + PulsarTestContainerSupport.getHttpServiceUrl(),
"--spring.cloud.function.definition=springMessageSupplier;springMessageLogger",
"--spring.cloud.stream.bindings.springMessageSupplier-out-0.destination=cmh-5",
"--spring.cloud.stream.bindings.springMessageLogger-in-0.destination=cmh-5",
"--spring.cloud.stream.pulsar.bindings.springMessageLogger-in-0.consumer.subscription-name=pbit-cmh5-sub1")) {
// Wait for a few of the messages to flow through (check for index = 5)
Awaitility.await().atMost(Duration.ofSeconds(AWAIT_DURATION)).until(() -> output.toString()
.contains("Hello binder: test-headers-msg-5 w/ custom-id: tsh->tph->FooHeader[value=5150-5]"));
}
}
@EnableAutoConfiguration
@SpringBootConfiguration
static class CustomSimpleHeadersConfig {
private final Logger logger = LoggerFactory.getLogger(getClass());
private int msgCount = 0;
@Bean
public Supplier<Message<String>> springMessageSupplier() {
return () -> {
msgCount++;
return MessageBuilder.withPayload("test-headers-msg-" + msgCount)
.setHeader("custom-id", "5150-" + msgCount).build();
};
}
@Bean
public Consumer<Message<String>> springMessageLogger() {
return s -> this.logger.info("Hello binder: {} w/ custom-id: {}", s.getPayload(),
s.getHeaders().get("custom-id"));
}
}
@EnableAutoConfiguration
@SpringBootConfiguration
static class CustomComplexHeadersConfig {
private final Logger logger = LoggerFactory.getLogger(getClass());
private int msgCount = 0;
@Bean
public Supplier<Message<String>> springMessageSupplier() {
return () -> {
msgCount++;
return MessageBuilder.withPayload("test-headers-msg-" + msgCount)
.setHeader("custom-id", new FooHeader("5150-" + msgCount)).build();
};
}
@Bean
public Consumer<Message<String>> springMessageLogger() {
return s -> {
var header = s.getHeaders().get("custom-id");
if (header != null) {
assertThat(header).isInstanceOf(FooHeader.class);
}
this.logger.info("Hello binder: {} w/ custom-id: {}", s.getPayload(), header);
};
}
record FooHeader(String value) {
}
}
@EnableAutoConfiguration
@SpringBootConfiguration
static class CustomHeaderMapperConfig {
private final Logger logger = LoggerFactory.getLogger(getClass());
private int msgCount = 0;
@Bean
public PulsarHeaderMapper extendedToStringHeaderMapper() {
return new ToStringPulsarHeaderMapper(List.of("custom-id"), List.of("foo", "custom-id")) {
@Override
public Map<String, String> toPulsarHeaders(MessageHeaders springHeaders) {
Map<String, String> pulsarHeaders = super.toPulsarHeaders(springHeaders);
// foo and custom-id are allowed and expected
assertThat(pulsarHeaders).containsKeys("foo", "custom-id");
return pulsarHeaders;
}
@Override
public MessageHeaders toSpringHeaders(org.apache.pulsar.client.api.Message<?> pulsarMessage) {
MessageHeaders springHeaders = super.toSpringHeaders(pulsarMessage);
// foo not allowed, custom-id allowed
assertThat(springHeaders).doesNotContainKey("foo").containsKey("custom-id");
return springHeaders;
}
@Override
protected String toPulsarHeaderValue(String name, Object value, Object context) {
return "tph->" + super.toPulsarHeaderValue(name, value, context);
}
@Override
protected Object toSpringHeaderValue(String headerName, String rawHeader, Object context) {
return "tsh->" + super.toSpringHeaderValue(headerName, rawHeader, context);
}
};
}
@Bean
public Supplier<Message<String>> springMessageSupplier() {
return () -> {
msgCount++;
return MessageBuilder.withPayload("test-headers-msg-" + msgCount)
.setHeader("foo", "bar-" + msgCount)
.setHeader("custom-id", new FooHeader("5150-" + msgCount)).build();
};
}
@Bean
public Consumer<Message<String>> springMessageLogger() {
return s -> {
var header = s.getHeaders().get("custom-id");
if (header != null) {
assertThat(header).isInstanceOf(String.class);
}
var fooHeader = s.getHeaders().get("foo");
assertThat(fooHeader).isNull();
this.logger.info("Hello binder: {} w/ custom-id: {}", s.getPayload(), header);
};
}
record FooHeader(String value) {
}
}
}
@EnableAutoConfiguration
@SpringBootConfiguration
static class PrimitiveTextConfig {
private final Logger logger = LoggerFactory.getLogger(getClass());
@Bean
public Supplier<String> textSupplier() {
return () -> "test-basic-scenario";
}
@Bean
public Consumer<String> textLogger() {
return s -> this.logger.info("Hello binder: " + s);
}
}
@EnableAutoConfiguration
@SpringBootConfiguration
@Import(PrimitiveTextConfig.class)
static class BinderAndBindingPropsTestConfig {
@Bean
public PulsarProducerFactory<?> pulsarProducerFactory(PulsarClient pulsarClient,
PulsarProperties pulsarProperties, TopicResolver topicResolver) {
return new TrackingProducerFactory(pulsarClient, pulsarProperties.buildProducerProperties(), topicResolver);
}
@Bean
public PulsarConsumerFactory<?> pulsarConsumerFactory(PulsarClient pulsarClient,
PulsarProperties pulsarProperties) {
return new TrackingConsumerFactory(pulsarClient, pulsarProperties.buildConsumerProperties());
}
}
static class TrackingProducerFactory extends DefaultPulsarProducerFactory<String> {
List<Producer<String>> producersCreated = new ArrayList<>();
TrackingProducerFactory(PulsarClient pulsarClient, Map<String, Object> config, TopicResolver topicResolver) {
super(pulsarClient, config, topicResolver);
}
@Override
protected Producer<String> doCreateProducer(Schema<String> schema, @Nullable String topic,
@Nullable Collection<String> encryptionKeys,
@Nullable List<ProducerBuilderCustomizer<String>> producerBuilderCustomizers)
throws PulsarClientException {
Producer<String> producer = super.doCreateProducer(schema, topic, encryptionKeys,
producerBuilderCustomizers);
producersCreated.add(producer);
return producer;
}
}
static class TrackingConsumerFactory extends DefaultPulsarConsumerFactory<String> {
List<org.apache.pulsar.client.api.Consumer<String>> consumersCreated = new ArrayList<>();
TrackingConsumerFactory(PulsarClient pulsarClient, Map<String, Object> consumerConfig) {
super(pulsarClient, consumerConfig);
}
@Override
public org.apache.pulsar.client.api.Consumer<String> createConsumer(Schema<String> schema,
@Nullable Collection<String> topics, @Nullable String subscriptionName,
@Nullable Map<String, String> metadataProperties,
@Nullable List<ConsumerBuilderCustomizer<String>> consumerBuilderCustomizers)
throws PulsarClientException {
org.apache.pulsar.client.api.Consumer<String> consumer = super.createConsumer(schema, topics,
subscriptionName, metadataProperties, consumerBuilderCustomizers);
consumersCreated.add(consumer);
return consumer;
}
}
@EnableAutoConfiguration
@SpringBootConfiguration
static class PrimitiveFloatConfig {
private final Logger logger = LoggerFactory.getLogger(getClass());
@Bean
public Supplier<Float> piSupplier() {
return () -> 3.14f;
}
@Bean
public Consumer<Float> piLogger() {
return f -> this.logger.info("Hello binder: " + f);
}
}
@EnableAutoConfiguration
@SpringBootConfiguration
static class JsonFooConfig {
private final Logger logger = LoggerFactory.getLogger(getClass());
@Bean
public Supplier<Foo> fooSupplier() {
return () -> new Foo("5150");
}
@Bean
public Consumer<Foo> fooLogger() {
return f -> this.logger.info("Hello binder: " + f);
}
}
@EnableAutoConfiguration
@SpringBootConfiguration
@Import(JsonFooConfig.class)
static class JsonFooWithCustomMappingConfig {
@Bean
public SchemaResolverCustomizer<DefaultSchemaResolver> customMappings() {
return (resolver) -> resolver.addCustomSchemaMapping(Foo.class, JSONSchema.of(Foo.class));
}
}
@EnableAutoConfiguration
@SpringBootConfiguration
static class AvroUserConfig {
private final Logger logger = LoggerFactory.getLogger(getClass());
@Bean
public Supplier<User> userSupplier() {
return () -> new User("user21", 21);
}
@Bean
public Consumer<User> userLogger() {
return f -> this.logger.info("Hello binder: " + f);
}
}
@EnableAutoConfiguration
@SpringBootConfiguration
@Import(AvroUserConfig.class)
static class AvroUserConfigCustomMappings {
@Bean
public SchemaResolverCustomizer<DefaultSchemaResolver> customMappings() {
return (resolver) -> resolver.addCustomSchemaMapping(User.class, Schema.AVRO(User.class));
}
}
@EnableAutoConfiguration
@SpringBootConfiguration
static class KeyValueAvroUserConfig {
private final Logger logger = LoggerFactory.getLogger(getClass());
@Bean
public Supplier<KeyValue<String, User>> userSupplier() {
return () -> new KeyValue<>("21", new User("user21", 21));
}
@Bean
public Consumer<KeyValue<String, User>> userLogger() {
return f -> this.logger.info("Hello binder: " + f.getKey() + "->" + f.getValue());
}
}
@EnableAutoConfiguration
@SpringBootConfiguration
@Import(KeyValueAvroUserConfig.class)
static class KeyValueAvroUserConfigCustomMappings {
@Bean
public SchemaResolverCustomizer<DefaultSchemaResolver> customMappings() {
return (resolver) -> resolver.addCustomSchemaMapping(User.class, Schema.AVRO(User.class));
}
}
@EnableAutoConfiguration
@SpringBootConfiguration
static class KeyValueJsonFooConfig {
private final Logger logger = LoggerFactory.getLogger(getClass());
@Bean
public Supplier<KeyValue<String, Foo>> fooSupplier() {
return () -> new KeyValue<>("5150", new Foo("5150"));
}
@Bean
public Consumer<KeyValue<String, Foo>> fooLogger() {
return f -> this.logger.info("Hello binder: " + f.getKey() + "->" + f.getValue());
}
}
record Foo(String value) {
}
/**
* Do not convert this to a Record as Avro does not seem to work well w/ records.
*/
static class User {
private String name;
private int age;
User() {
}
User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
User user = (User) o;
return age == user.age && Objects.equals(name, user.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public String toString() {
return "User{" + "name='" + name + '\'' + ", age=" + age + '}';
}
}
}

View File

@@ -1,256 +0,0 @@
/*
* Copyright 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.spring.cloud.stream.binder;
import static org.assertj.core.api.Assertions.assertThat;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.pulsar.client.api.PulsarClient;
import org.apache.pulsar.client.api.PulsarClientException;
import org.apache.pulsar.client.api.SubscriptionInitialPosition;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.springframework.cloud.stream.binder.Binder;
import org.springframework.cloud.stream.binder.Binding;
import org.springframework.cloud.stream.binder.ExtendedConsumerProperties;
import org.springframework.cloud.stream.binder.ExtendedProducerProperties;
import org.springframework.cloud.stream.binder.PartitionCapableBinderTests;
import org.springframework.cloud.stream.binder.Spy;
import org.springframework.cloud.stream.config.BindingProperties;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHeaders;
import org.springframework.pulsar.core.DefaultPulsarConsumerFactory;
import org.springframework.pulsar.core.DefaultPulsarProducerFactory;
import org.springframework.pulsar.core.DefaultSchemaResolver;
import org.springframework.pulsar.core.PulsarAdministration;
import org.springframework.pulsar.core.PulsarTemplate;
import org.springframework.pulsar.spring.cloud.stream.binder.properties.PulsarBinderConfigurationProperties;
import org.springframework.pulsar.spring.cloud.stream.binder.properties.PulsarConsumerProperties;
import org.springframework.pulsar.spring.cloud.stream.binder.properties.PulsarProducerProperties;
import org.springframework.pulsar.spring.cloud.stream.binder.provisioning.PulsarTopicProvisioner;
import org.springframework.pulsar.support.header.JsonPulsarHeaderMapper;
import org.springframework.pulsar.test.support.PulsarTestContainerSupport;
import org.springframework.util.Assert;
import org.springframework.util.MimeTypeUtils;
/**
* Tests for {@link PulsarMessageChannelBinder}.
*
* @author Soby Chacko
*/
public class PulsarBinderTests extends
PartitionCapableBinderTests<AbstractPulsarTestBinder, ExtendedConsumerProperties<PulsarConsumerProperties>, ExtendedProducerProperties<PulsarProducerProperties>>
implements PulsarTestContainerSupport {
private PulsarTestBinder binder;
@Nullable
protected PulsarClient pulsarClient;
@BeforeEach
void createPulsarClient() throws PulsarClientException {
pulsarClient = PulsarClient.builder().serviceUrl(PulsarTestContainerSupport.getPulsarBrokerUrl()).build();
}
@AfterEach
void closePulsarClient() throws PulsarClientException {
if (pulsarClient != null && !pulsarClient.isClosed()) {
pulsarClient.close();
}
}
@Override
protected boolean usesExplicitRouting() {
return false;
}
@Override
protected String getClassUnderTestName() {
return PulsarMessageChannelBinder.class.getSimpleName();
}
@Override
protected PulsarTestBinder getBinder() {
var pulsarAdministration = new PulsarAdministration(
Map.of("serviceUrl", PulsarTestContainerSupport.getHttpServiceUrl()));
var configProps = new PulsarBinderConfigurationProperties();
var provisioner = new PulsarTopicProvisioner(pulsarAdministration, configProps);
var producerFactory = new DefaultPulsarProducerFactory<>(pulsarClient, Collections.emptyMap());
var pulsarTemplate = new PulsarTemplate<>(producerFactory);
var config = Map.<String, Object>of("subscriptionInitialPosition", SubscriptionInitialPosition.Earliest);
var consumerFactory = new DefaultPulsarConsumerFactory<>(pulsarClient, config);
if (this.binder == null) {
this.binder = new PulsarTestBinder(provisioner, pulsarTemplate, consumerFactory, configProps,
new DefaultSchemaResolver(), JsonPulsarHeaderMapper.builder().build());
}
return this.binder;
}
@Override
protected ExtendedConsumerProperties<PulsarConsumerProperties> createConsumerProperties() {
final ExtendedConsumerProperties<PulsarConsumerProperties> pulsarConsumerProperties = new ExtendedConsumerProperties<>(
new PulsarConsumerProperties());
return pulsarConsumerProperties;
}
@Override
public Spy spyOn(String name) {
return null;
}
private ExtendedProducerProperties<PulsarProducerProperties> createProducerProperties() {
return this.createProducerProperties(null);
}
@Override
protected ExtendedProducerProperties<PulsarProducerProperties> createProducerProperties(TestInfo testInto) {
return new ExtendedProducerProperties<>(new PulsarProducerProperties());
}
@Override
protected void binderBindUnbindLatency() throws InterruptedException {
Thread.sleep(500);
}
@Test
@Override
@SuppressWarnings({ "unchecked", "rawtypes" })
public void testSendAndReceive(TestInfo testInfo) throws Exception {
Binder binder = getBinder();
BindingProperties outputBindingProperties = createProducerBindingProperties(createProducerProperties());
DirectChannel moduleOutputChannel = createBindableChannel("output", outputBindingProperties);
ExtendedConsumerProperties<PulsarConsumerProperties> consumerProperties = createConsumerProperties();
DirectChannel moduleInputChannel = createBindableChannel("input",
createConsumerBindingProperties(consumerProperties));
Binding<MessageChannel> producerBinding = binder.bindProducer("foo.bar", moduleOutputChannel,
outputBindingProperties.getProducer());
Binding<MessageChannel> consumerBinding = binder.bindConsumer("foo.bar", null, moduleInputChannel,
consumerProperties);
Message<?> message = org.springframework.integration.support.MessageBuilder
.withPayload("foo".getBytes(StandardCharsets.UTF_8)).build();
// Let the consumer actually bind to the producer before sending a msg
binderBindUnbindLatency();
moduleOutputChannel.send(message);
CountDownLatch latch = new CountDownLatch(1);
AtomicReference<Message<byte[]>> inboundMessageRef = new AtomicReference<>();
moduleInputChannel.subscribe(message1 -> {
try {
inboundMessageRef.set((Message<byte[]>) message1);
}
finally {
latch.countDown();
}
});
Assert.isTrue(latch.await(5, TimeUnit.SECONDS), "Failed to receive message");
assertThat(inboundMessageRef.get()).isNotNull();
assertThat(new String(inboundMessageRef.get().getPayload(), StandardCharsets.UTF_8)).isEqualTo("foo");
producerBinding.unbind();
consumerBinding.unbind();
}
@Test
@Override
@SuppressWarnings({ "unchecked", "rawtypes" })
public void testAnonymousGroup(TestInfo testInfo) throws Exception {
Binder binder = getBinder();
BindingProperties producerBindingProperties = createProducerBindingProperties(
createProducerProperties(testInfo));
DirectChannel output = createBindableChannel("output", producerBindingProperties);
Binding<MessageChannel> producerBinding = binder.bindProducer(
String.format("defaultGroup%s0", getDestinationNameDelimiter()), output,
producerBindingProperties.getProducer());
QueueChannel input1 = new QueueChannel();
Binding<MessageChannel> binding1 = binder.bindConsumer(
String.format("defaultGroup%s0", getDestinationNameDelimiter()), null, input1,
createConsumerProperties());
QueueChannel input2 = new QueueChannel();
Binding<MessageChannel> binding2 = binder.bindConsumer(
String.format("defaultGroup%s0", getDestinationNameDelimiter()), null, input2,
createConsumerProperties());
String testPayload1 = "foo-" + UUID.randomUUID();
output.send(MessageBuilder.withPayload(testPayload1)
.setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN).build());
Message<byte[]> receivedMessage1 = (Message<byte[]>) receive(input1);
assertThat(receivedMessage1).isNotNull();
assertThat(new String(receivedMessage1.getPayload())).isEqualTo(testPayload1);
Message<byte[]> receivedMessage2 = (Message<byte[]>) receive(input2);
assertThat(receivedMessage2).isNotNull();
assertThat(new String(receivedMessage2.getPayload())).isEqualTo(testPayload1);
binding2.unbind();
String testPayload2 = "foo-" + UUID.randomUUID();
output.send(MessageBuilder.withPayload(testPayload2)
.setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN).build());
binding2 = binder.bindConsumer(String.format("defaultGroup%s0", getDestinationNameDelimiter()), null, input2,
createConsumerProperties());
String testPayload3 = "foo-" + UUID.randomUUID();
output.send(MessageBuilder.withPayload(testPayload3)
.setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN).build());
receivedMessage1 = (Message<byte[]>) receive(input1);
assertThat(receivedMessage1).isNotNull();
assertThat(new String(receivedMessage1.getPayload())).isEqualTo(testPayload2);
receivedMessage1 = (Message<byte[]>) receive(input1);
assertThat(receivedMessage1).isNotNull();
assertThat(new String(receivedMessage1.getPayload())).isEqualTo(testPayload3);
receivedMessage2 = (Message<byte[]>) receive(input2);
assertThat(receivedMessage2).isNotNull();
assertThat(new String(receivedMessage2.getPayload())).isEqualTo(testPayload1);
producerBinding.unbind();
binding1.unbind();
binding2.unbind();
}
@Test
@Override
@Disabled
public void testPartitionedModuleSpEL(TestInfo testInfo) {
// This use-case needs to be further evaluated for Pulsar binder.
}
}

View File

@@ -1,133 +0,0 @@
/*
* Copyright 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.spring.cloud.stream.binder;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.params.provider.Arguments.arguments;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.Collections;
import java.util.Map;
import java.util.stream.Stream;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.cloud.stream.provisioning.ConsumerDestination;
import org.springframework.pulsar.spring.cloud.stream.binder.properties.PulsarConsumerProperties;
/**
* Unit tests for {@link PulsarBinderUtils}.
*
* @author Soby Chacko
* @author Chris Bono
*/
public class PulsarBinderUtilsTests {
@Nested
class SubscriptionNameTests {
@Test
void respectsValueWhenSetAsProperty() {
var consumerDestination = mock(ConsumerDestination.class);
var pulsarConsumerProperties = mock(PulsarConsumerProperties.class);
when(pulsarConsumerProperties.getSubscriptionName()).thenReturn("my-sub");
assertThat(PulsarBinderUtils.subscriptionName(pulsarConsumerProperties, consumerDestination))
.isEqualTo("my-sub");
}
@Test
void generatesValueWhenNotSetAsProperty() {
var consumerDestination = mock(ConsumerDestination.class);
var pulsarConsumerProperties = mock(PulsarConsumerProperties.class);
when(pulsarConsumerProperties.getSubscriptionName()).thenReturn(null);
when(consumerDestination.getName()).thenReturn("my-topic");
assertThat(PulsarBinderUtils.subscriptionName(pulsarConsumerProperties, consumerDestination))
.startsWith("my-topic-anon-subscription-");
}
}
@Nested
class MergedPropertiesTests {
@ParameterizedTest(name = "{0}")
@MethodSource("mergePropertiesTestProvider")
void mergePropertiesTest(String testName, Map<String, Object> baseProps, Map<String, Object> binderProps,
Map<String, Object> bindingProps, Map<String, Object> expectedMergedProps) {
assertThat(PulsarBinderUtils.mergePropertiesWithPrecedence(baseProps, binderProps, bindingProps))
.containsExactlyInAnyOrderEntriesOf(expectedMergedProps);
}
// @formatter:off
static Stream<Arguments> mergePropertiesTestProvider() {
return Stream.of(
arguments("binderLevelContainsSamePropAsBaseWithDiffValue",
Map.of("foo", "foo-base"),
Map.of("foo", "foo-binder"),
Collections.emptyMap(),
Map.of("foo", "foo-binder")),
arguments("binderLevelContainsNewPropNotInBase",
Collections.emptyMap(),
Map.of("foo", "foo-binder"),
Collections.emptyMap(),
Map.of("foo", "foo-binder")),
arguments("binderLevelContainsSamePropAsBaseWithSameValue",
Map.of("foo", "foo-base"),
Map.of("foo", "foo-base"),
Collections.emptyMap(),
Collections.emptyMap()),
arguments("bindingLevelContainsSamePropAsBaseWithDiffValue",
Map.of("foo", "foo-base"),
Collections.emptyMap(),
Map.of("foo", "foo-binding"),
Map.of("foo", "foo-binding")),
arguments("bindingLevelContainsNewPropNotInBase",
Collections.emptyMap(),
Map.of("foo", "foo-binding"),
Collections.emptyMap(),
Map.of("foo", "foo-binding")),
arguments("bindingLevelContainsSamePropAsBaseWithSameValue",
Map.of("foo", "foo-base"),
Collections.emptyMap(),
Map.of("foo", "foo-base"),
Collections.emptyMap()),
arguments("bindingOverridesBinder",
Map.of("bar", "bar-base"),
Map.of("foo", "foo-binder"),
Map.of("foo", "foo-binding"),
Map.of("foo", "foo-binding")),
arguments("binderOverridesBaseAndBindingOverridesBinder",
Map.of("foo", "foo-base"),
Map.of("foo", "foo-binder"),
Map.of("foo", "foo-binding"),
Map.of("foo", "foo-binding")),
arguments("onlyBaseProps",
Map.of("foo", "foo-base"),
Collections.emptyMap(),
Collections.emptyMap(),
Collections.emptyMap()));
}
// @formatter:on
}
}

View File

@@ -1,137 +0,0 @@
/*
* Copyright 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.spring.cloud.stream.binder;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatNoException;
import java.util.HashMap;
import java.util.Map;
import org.apache.pulsar.client.api.ProducerAccessMode;
import org.apache.pulsar.client.api.SubscriptionMode;
import org.apache.pulsar.client.api.SubscriptionType;
import org.apache.pulsar.client.impl.conf.ConfigurationDataUtils;
import org.apache.pulsar.client.impl.conf.ConsumerConfigurationData;
import org.apache.pulsar.client.impl.conf.ProducerConfigurationData;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
import org.springframework.pulsar.listener.PulsarContainerProperties;
import org.springframework.pulsar.spring.cloud.stream.binder.properties.PulsarExtendedBindingProperties;
/**
* Tests for {@link PulsarExtendedBindingProperties}.
*
* @author Chris Bono
*/
public class PulsarExtendedBindingPropertiesTests {
private final PulsarExtendedBindingProperties properties = new PulsarExtendedBindingProperties();
private void bind(Map<String, String> map) {
ConfigurationPropertySource source = new MapConfigurationPropertySource(map);
new Binder(source).bind("spring.cloud.stream.pulsar", Bindable.ofInstance(this.properties));
}
@Test
void producerProperties() {
// Only spot check a few values (PulsarPropertiesTests does the heavy lifting)
Map<String, String> props = new HashMap<>();
props.put("spring.cloud.stream.pulsar.bindings.my-foo.producer.topic-name", "my-topic");
props.put("spring.cloud.stream.pulsar.bindings.my-foo.producer.send-timeout", "2s");
props.put("spring.cloud.stream.pulsar.bindings.my-foo.producer.max-pending-messages", "3");
props.put("spring.cloud.stream.pulsar.bindings.my-foo.producer.producer-access-mode", "exclusive");
props.put("spring.cloud.stream.pulsar.bindings.my-foo.producer.properties[my-prop]", "my-prop-value");
bind(props);
assertThat(properties.getBindings()).containsOnlyKeys("my-foo");
Map<String, Object> producerProps = properties.getExtendedProducerProperties("my-foo").buildProperties();
// Verify that the props can be loaded in a ProducerBuilder
assertThatNoException().isThrownBy(() -> ConfigurationDataUtils.loadData(producerProps,
new ProducerConfigurationData(), ProducerConfigurationData.class));
// @formatter:off
assertThat(producerProps)
.containsEntry("topicName", "my-topic")
.containsEntry("sendTimeoutMs", 2_000)
.containsEntry("maxPendingMessages", 3)
.containsEntry("accessMode", ProducerAccessMode.Exclusive)
.hasEntrySatisfying("properties", properties ->
assertThat(properties)
.asInstanceOf(InstanceOfAssertFactories.map(String.class, String.class))
.containsEntry("my-prop", "my-prop-value"));
// @formatter:on
}
@Test
void consumerProperties() {
// Only spot check a few values (PulsarPropertiesTests does the heavy lifting)
Map<String, String> props = new HashMap<>();
props.put("spring.cloud.stream.pulsar.bindings.my-foo.consumer.topics[0]", "my-topic");
props.put("spring.cloud.stream.pulsar.bindings.my-foo.consumer.subscription-properties[my-sub-prop]",
"my-sub-prop-value");
props.put("spring.cloud.stream.pulsar.bindings.my-foo.consumer.subscription-mode", "nondurable");
props.put("spring.cloud.stream.pulsar.bindings.my-foo.consumer.receiver-queue-size", "1");
bind(props);
assertThat(properties.getBindings()).containsOnlyKeys("my-foo");
Map<String, Object> consumerProps = properties.getExtendedConsumerProperties("my-foo").buildProperties();
// Verify that the props can be loaded in a ConsumerBuilder
assertThatNoException().isThrownBy(() -> ConfigurationDataUtils.loadData(consumerProps,
new ConsumerConfigurationData<>(), ConsumerConfigurationData.class));
// @formatter:off
assertThat(consumerProps)
.hasEntrySatisfying("topicNames",
topics -> assertThat(topics).asInstanceOf(InstanceOfAssertFactories.collection(String.class))
.containsExactly("my-topic"))
.hasEntrySatisfying("subscriptionProperties",
properties -> assertThat(properties)
.asInstanceOf(InstanceOfAssertFactories.map(String.class, String.class))
.containsEntry("my-sub-prop", "my-sub-prop-value"))
.containsEntry("subscriptionMode", SubscriptionMode.NonDurable)
.containsEntry("receiverQueueSize", 1);
// @formatter:on
}
@Test
void extendedBindingsArePropagatedToContainerProperties() {
Map<String, String> props = new HashMap<>();
props.put("spring.cloud.stream.pulsar.bindings.my-foo.consumer.subscription-name", "my-foo-sbscription");
props.put("spring.cloud.stream.pulsar.bindings.my-foo.consumer.subscription-type", "Shared");
bind(props);
var bindingConsumerProps = properties.getExtendedConsumerProperties("my-foo").buildProperties();
PulsarContainerProperties pulsarContainerProperties = new PulsarContainerProperties();
pulsarContainerProperties.getPulsarConsumerProperties().putAll(bindingConsumerProps);
assertThat(pulsarContainerProperties.getSubscriptionName()).isNull();
assertThat(pulsarContainerProperties.getSubscriptionType()).isEqualTo(SubscriptionType.Exclusive);
pulsarContainerProperties.updateContainerProperties();
assertThat(pulsarContainerProperties.getSubscriptionName()).isEqualTo("my-foo-sbscription");
assertThat(pulsarContainerProperties.getSubscriptionType()).isEqualTo(SubscriptionType.Shared);
}
}

View File

@@ -1,63 +0,0 @@
/*
* Copyright 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.spring.cloud.stream.binder;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.config.EnableIntegration;
import org.springframework.pulsar.core.PulsarConsumerFactory;
import org.springframework.pulsar.core.PulsarTemplate;
import org.springframework.pulsar.core.SchemaResolver;
import org.springframework.pulsar.spring.cloud.stream.binder.properties.PulsarBinderConfigurationProperties;
import org.springframework.pulsar.spring.cloud.stream.binder.provisioning.PulsarTopicProvisioner;
import org.springframework.pulsar.support.header.PulsarHeaderMapper;
/**
* Test binder to exercise producer/consumer bindings in
* {@link PulsarMessageChannelBinder}.
*
* @author Soby Chacko
* @author Chris Bono
*/
public class PulsarTestBinder extends AbstractPulsarTestBinder {
@SuppressWarnings({ "unchecked" })
PulsarTestBinder(PulsarTopicProvisioner pulsarTopicProvisioner, PulsarTemplate<?> pulsarTemplate,
PulsarConsumerFactory<?> pulsarConsumerFactory, PulsarBinderConfigurationProperties binderConfigProps,
SchemaResolver schemaResolver, PulsarHeaderMapper headerMapper) {
try {
var binder = new PulsarMessageChannelBinder(pulsarTopicProvisioner, (PulsarTemplate<Object>) pulsarTemplate,
pulsarConsumerFactory, binderConfigProps, schemaResolver, headerMapper);
var context = new AnnotationConfigApplicationContext(Config.class);
setApplicationContext(context);
binder.setApplicationContext(context);
binder.afterPropertiesSet();
this.setPollableConsumerBinder(binder);
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
@Configuration
@EnableIntegration
static class Config {
}
}

View File

@@ -1,136 +0,0 @@
/*
* Copyright 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.spring.cloud.stream.binder;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.cloud.stream.binder.ExtendedConsumerProperties;
import org.springframework.cloud.stream.binder.ExtendedProducerProperties;
import org.springframework.cloud.stream.provisioning.ConsumerDestination;
import org.springframework.cloud.stream.provisioning.ProducerDestination;
import org.springframework.pulsar.core.PulsarAdministration;
import org.springframework.pulsar.core.PulsarTopic;
import org.springframework.pulsar.spring.cloud.stream.binder.properties.PulsarBinderConfigurationProperties;
import org.springframework.pulsar.spring.cloud.stream.binder.properties.PulsarConsumerProperties;
import org.springframework.pulsar.spring.cloud.stream.binder.properties.PulsarProducerProperties;
import org.springframework.pulsar.spring.cloud.stream.binder.provisioning.PulsarTopicProvisioner;
/**
* @author Soby Chacko
*/
public class PulsarTopicProvisionerTests {
@Test
void provisionThroughProducerBindingWithDefaultPartitioning() {
PulsarBinderConfigurationProperties pulsarBinderConfigurationProperties = new PulsarBinderConfigurationProperties();
PulsarAdministration pulsarAdministration = mock(PulsarAdministration.class);
PulsarTopicProvisioner pulsarTopicProvisioner = new PulsarTopicProvisioner(pulsarAdministration,
pulsarBinderConfigurationProperties);
ExtendedProducerProperties<PulsarProducerProperties> properties = new ExtendedProducerProperties<>(
new PulsarProducerProperties());
ProducerDestination producerDestination = pulsarTopicProvisioner.provisionProducerDestination("foo",
properties);
verifyAndAssert(pulsarAdministration, producerDestination.getName(), "foo", 0);
}
private static void verifyAndAssert(PulsarAdministration pulsarAdministration, String actualProducerDestination,
String expectedProducerDestination, int expectedPartitionCount) {
ArgumentCaptor<PulsarTopic> pulsarTopicArgumentCaptor = ArgumentCaptor.forClass(PulsarTopic.class);
verify(pulsarAdministration, times(1)).createOrModifyTopics(pulsarTopicArgumentCaptor.capture());
assertThat(actualProducerDestination).isEqualTo(expectedProducerDestination);
PulsarTopic pulsarTopic = pulsarTopicArgumentCaptor.getValue();
assertThat(pulsarTopic.topicName()).isEqualTo(expectedProducerDestination);
assertThat(pulsarTopic.numberOfPartitions()).isEqualTo(expectedPartitionCount);
}
@Test
void provisionThroughConsumerBindingWithDefaultPartitioning() {
PulsarBinderConfigurationProperties pulsarBinderConfigurationProperties = new PulsarBinderConfigurationProperties();
PulsarAdministration pulsarAdministration = mock(PulsarAdministration.class);
PulsarTopicProvisioner pulsarTopicProvisioner = new PulsarTopicProvisioner(pulsarAdministration,
pulsarBinderConfigurationProperties);
ExtendedConsumerProperties<PulsarConsumerProperties> properties = new ExtendedConsumerProperties<>(
new PulsarConsumerProperties());
ConsumerDestination consumerDestination = pulsarTopicProvisioner.provisionConsumerDestination("bar", "",
properties);
verifyAndAssert(pulsarAdministration, consumerDestination.getName(), "bar", 0);
}
@Test
void provisioningOnProducerBindingWithPartitionsSetAtTheBinderProperties() {
PulsarBinderConfigurationProperties pulsarBinderConfigurationProperties = new PulsarBinderConfigurationProperties();
pulsarBinderConfigurationProperties.setPartitionCount(4);
PulsarAdministration pulsarAdministration = mock(PulsarAdministration.class);
PulsarTopicProvisioner pulsarTopicProvisioner = new PulsarTopicProvisioner(pulsarAdministration,
pulsarBinderConfigurationProperties);
ExtendedProducerProperties<PulsarProducerProperties> properties = new ExtendedProducerProperties<>(
new PulsarProducerProperties());
ProducerDestination producerDestination = pulsarTopicProvisioner.provisionProducerDestination("foo",
properties);
verifyAndAssert(pulsarAdministration, producerDestination.getName(), "foo", 4);
}
@Test
void provisioningOnProducerBindingWithPartitionsSetAtTheBindingProperties() {
PulsarBinderConfigurationProperties pulsarBinderConfigurationProperties = new PulsarBinderConfigurationProperties();
PulsarAdministration pulsarAdministration = mock(PulsarAdministration.class);
PulsarTopicProvisioner pulsarTopicProvisioner = new PulsarTopicProvisioner(pulsarAdministration,
pulsarBinderConfigurationProperties);
ExtendedProducerProperties<PulsarProducerProperties> properties = new ExtendedProducerProperties<>(
new PulsarProducerProperties());
properties.getExtension().setPartitionCount(4);
ProducerDestination producerDestination = pulsarTopicProvisioner.provisionProducerDestination("foo",
properties);
verifyAndAssert(pulsarAdministration, producerDestination.getName(), "foo", 4);
}
@Test
void provisionThroughConsumerBindingWithPartitionsSetAtTheBinderProperties() {
PulsarBinderConfigurationProperties pulsarBinderConfigurationProperties = new PulsarBinderConfigurationProperties();
pulsarBinderConfigurationProperties.setPartitionCount(4);
PulsarAdministration pulsarAdministration = mock(PulsarAdministration.class);
PulsarTopicProvisioner pulsarTopicProvisioner = new PulsarTopicProvisioner(pulsarAdministration,
pulsarBinderConfigurationProperties);
ExtendedConsumerProperties<PulsarConsumerProperties> properties = new ExtendedConsumerProperties<>(
new PulsarConsumerProperties());
ConsumerDestination consumerDestination = pulsarTopicProvisioner.provisionConsumerDestination("bar", "",
properties);
verifyAndAssert(pulsarAdministration, consumerDestination.getName(), "bar", 4);
}
@Test
void provisionThroughConsumerBindingWithPartitionsSetAtTheBindingProperties() {
PulsarBinderConfigurationProperties pulsarBinderConfigurationProperties = new PulsarBinderConfigurationProperties();
PulsarAdministration pulsarAdministration = mock(PulsarAdministration.class);
PulsarTopicProvisioner pulsarTopicProvisioner = new PulsarTopicProvisioner(pulsarAdministration,
pulsarBinderConfigurationProperties);
PulsarConsumerProperties pulsarConsumerProperties = new PulsarConsumerProperties();
pulsarConsumerProperties.setPartitionCount(4);
ExtendedConsumerProperties<PulsarConsumerProperties> properties = new ExtendedConsumerProperties<>(
pulsarConsumerProperties);
ConsumerDestination consumerDestination = pulsarTopicProvisioner.provisionConsumerDestination("bar", "",
properties);
verifyAndAssert(pulsarAdministration, consumerDestination.getName(), "bar", 4);
}
}

View File

@@ -1,13 +0,0 @@
<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" level="INFO"/>
</configuration>

View File

@@ -1,5 +1,7 @@
plugins {
id 'org.springframework.pulsar.spring-module'
id "de.undercouch.download" version "5.3.0"
id 'spring-pulsar.integration-test-conventions'
}
description = 'Spring Pulsar Support'
@@ -43,8 +45,39 @@ dependencies {
testImplementation 'org.springframework:spring-test'
testImplementation 'org.springframework.boot:spring-boot-test'
intTestImplementation 'org.springframework.boot:spring-boot-starter-amqp'
intTestImplementation 'org.springframework.pulsar:spring-pulsar-spring-boot-starter'
intTestImplementation 'org.testcontainers:junit-jupiter'
intTestImplementation 'org.testcontainers:pulsar'
intTestImplementation 'org.testcontainers:rabbitmq'
}
test {
testLogging.showStandardStreams = true
}
integrationTest {
maxHeapSize '2048m'
}
task downloadRabbitConnector {
onlyIf {
System.getProperty("downloadRabbitConnector") == "true"
}
doLast {
try {
download.run {
println "Downloading Rabbit connector to 'src/intTest/resources/connectors/' (one time only if not already downloaded)..."
src 'https://archive.apache.org/dist/pulsar/pulsar-2.10.2/connectors/pulsar-io-rabbitmq-2.10.2.nar'
dest "$buildDir/../src/intTest/resources/connectors/pulsar-io-rabbitmq-2.10.2.nar"
overwrite false
}
} catch (Exception ex) {
println "Failed to download rabbit connector: $ex"
}
}
}
project.afterEvaluate {
compileTestJava.dependsOn downloadRabbitConnector
}

View File

@@ -41,12 +41,12 @@ import org.springframework.pulsar.core.TopicResolver;
import org.springframework.pulsar.test.support.PulsarTestContainerSupport;
/**
* Tests for {@link PulsarListener}.
* Integration tests for {@link PulsarListener}.
*
* @author Soby Chacko
* @author Chris Bono
*/
class PulsarListenerTests implements PulsarTestContainerSupport {
class PulsarListenerIntegrationTests implements PulsarTestContainerSupport {
private static final CountDownLatch LATCH_1 = new CountDownLatch(1);

Some files were not shown because too many files have changed in this diff Show More