diff --git a/README.adoc b/README.adoc index df4de9b5..b6e2ee97 100644 --- a/README.adoc +++ b/README.adoc @@ -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]. diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 82e02112..1c4317e7 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -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" diff --git a/buildSrc/src/main/groovy/io/spring/gradle/convention/IntegrationTestPlugin.groovy b/buildSrc/src/main/groovy/io/spring/gradle/convention/IntegrationTestPlugin.groovy deleted file mode 100644 index 3a919f84..00000000 --- a/buildSrc/src/main/groovy/io/spring/gradle/convention/IntegrationTestPlugin.groovy +++ /dev/null @@ -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. - * - * - * @author Rob Winch - * @author Chris Bono - */ -class IntegrationTestPlugin implements Plugin { - - @Override - void apply(Project project) { - project.plugins.withType(JavaPlugin.class) { - applyJava(project) - } - } - - private applyJava(Project project) { - if(!project.file('src/integration-test/').exists()) { - // ensure we don't add if no tests - return - } - project.configurations { - integrationTestCompile { - extendsFrom testImplementation - } - integrationTestRuntime { - extendsFrom integrationTestCompile, testRuntimeClasspath, testRuntimeOnly - } - integrationTestCompileClasspath { - extendsFrom integrationTestCompile - canBeResolved = true - } - integrationTestRuntimeClasspath { - extendsFrom integrationTestRuntime - canBeResolved = true - } - } - - project.sourceSets { - integrationTest { - java.srcDir project.file('src/integration-test/java') - resources.srcDir project.file('src/integration-test/resources') - compileClasspath = project.sourceSets.main.output + project.sourceSets.test.output + project.configurations.integrationTestCompileClasspath - runtimeClasspath = output + compileClasspath + project.configurations.integrationTestRuntimeClasspath - } - } - - Task integrationTestTask = project.tasks.create("integrationTest", Test) { - description = 'Runs integration tests.' - group = 'verification' - - testClassesDirs = project.sourceSets.integrationTest.output.classesDirs - classpath = project.sourceSets.integrationTest.runtimeClasspath - - 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 ] - } - } - } -} diff --git a/buildSrc/src/main/groovy/spring-pulsar.integration-test-conventions.gradle b/buildSrc/src/main/groovy/spring-pulsar.integration-test-conventions.gradle new file mode 100644 index 00000000..b5b82a0b --- /dev/null +++ b/buildSrc/src/main/groovy/spring-pulsar.integration-test-conventions.gradle @@ -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 diff --git a/buildSrc/src/main/java/org/springframework/pulsar/gradle/SpringDocsModulePlugin.java b/buildSrc/src/main/java/org/springframework/pulsar/gradle/SpringDocsModulePlugin.java index 5a46a653..11885677 100644 --- a/buildSrc/src/main/java/org/springframework/pulsar/gradle/SpringDocsModulePlugin.java +++ b/buildSrc/src/main/java/org/springframework/pulsar/gradle/SpringDocsModulePlugin.java @@ -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 { pluginManager.apply(AsciidoctorConventionsPlugin.class); pluginManager.apply(SpringPublishPlugin.class); pluginManager.apply(OptionalDependenciesPlugin.class); - pluginManager.apply(IntegrationTestPlugin.class); } } diff --git a/buildSrc/src/main/java/org/springframework/pulsar/gradle/docs/configprops/Asciidoc.java b/buildSrc/src/main/java/org/springframework/pulsar/gradle/docs/configprops/Asciidoc.java deleted file mode 100644 index 581c8f8a..00000000 --- a/buildSrc/src/main/java/org/springframework/pulsar/gradle/docs/configprops/Asciidoc.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2012-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.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(); - } - -} diff --git a/buildSrc/src/main/java/org/springframework/pulsar/gradle/docs/configprops/CheckAdditionalSpringConfigurationMetadata.java b/buildSrc/src/main/java/org/springframework/pulsar/gradle/docs/configprops/CheckAdditionalSpringConfigurationMetadata.java deleted file mode 100644 index 191ce382..00000000 --- a/buildSrc/src/main/java/org/springframework/pulsar/gradle/docs/configprops/CheckAdditionalSpringConfigurationMetadata.java +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright 2012-2022 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.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 json = objectMapper.readValue(file, Map.class); - check("groups", json, analysis); - check("properties", json, analysis); - check("hints", json, analysis); - } - return report; - } - - @SuppressWarnings("unchecked") - private void check(String key, Map json, Analysis analysis) { - List> groups = (List>) json.get(key); - List names = groups.stream().map((group) -> (String) group.get("name")).collect(Collectors.toList()); - List sortedNames = sortedCopy(names); - for (int i = 0; i < names.size(); i++) { - String actual = names.get(i); - String expected = sortedNames.get(i); - if (!actual.equals(expected)) { - analysis.problems.add("Wrong order at $." + key + "[" + i + "].name - expected '" + expected - + "' but found '" + actual + "'"); - } - } - } - - private List sortedCopy(Collection original) { - List copy = new ArrayList<>(original); - Collections.sort(copy); - return copy; - } - - private static final class Report implements Iterable { - - private final List analyses = new ArrayList<>(); - - private Analysis analysis(Path path) { - Analysis analysis = new Analysis(path); - this.analyses.add(analysis); - return analysis; - } - - private boolean hasProblems() { - for (Analysis analysis : this.analyses) { - if (!analysis.problems.isEmpty()) { - return true; - } - } - return false; - } - - @Override - public Iterator iterator() { - List lines = new ArrayList<>(); - for (Analysis analysis : this.analyses) { - lines.add(analysis.source.toString()); - lines.add(""); - if (analysis.problems.isEmpty()) { - lines.add("No problems found."); - } - else { - lines.addAll(analysis.problems); - } - lines.add(""); - } - return lines.iterator(); - } - - } - - private static final class Analysis { - - private final List problems = new ArrayList<>(); - - private final Path source; - - private Analysis(Path source) { - this.source = source; - } - - } - -} diff --git a/buildSrc/src/main/java/org/springframework/pulsar/gradle/docs/configprops/CompoundRow.java b/buildSrc/src/main/java/org/springframework/pulsar/gradle/docs/configprops/CompoundRow.java deleted file mode 100644 index 0f33f308..00000000 --- a/buildSrc/src/main/java/org/springframework/pulsar/gradle/docs/configprops/CompoundRow.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2012-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.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 propertyNames; - - private final String description; - - CompoundRow(Snippet snippet, String prefix, String description) { - super(snippet, prefix); - this.description = description; - this.propertyNames = new TreeSet<>(); - } - - void addProperty(ConfigurationProperty property) { - this.propertyNames.add(property.getDisplayName()); - } - - @Override - void write(Asciidoc asciidoc) { - asciidoc.append("|"); - asciidoc.append("[[" + getAnchor() + "]]"); - asciidoc.append("<<" + getAnchor() + ","); - this.propertyNames.forEach(asciidoc::appendWithHardLineBreaks); - asciidoc.appendln(">>"); - asciidoc.appendln("|+++", this.description, "+++"); - asciidoc.appendln("|"); - } - -} diff --git a/buildSrc/src/main/java/org/springframework/pulsar/gradle/docs/configprops/ConfigurationProperties.java b/buildSrc/src/main/java/org/springframework/pulsar/gradle/docs/configprops/ConfigurationProperties.java deleted file mode 100644 index 0a539b1f..00000000 --- a/buildSrc/src/main/java/org/springframework/pulsar/gradle/docs/configprops/ConfigurationProperties.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2012-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.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 byName; - - private ConfigurationProperties(List properties) { - Map byName = new LinkedHashMap<>(); - for (ConfigurationProperty property : properties) { - byName.put(property.getName(), property); - } - this.byName = Collections.unmodifiableMap(byName); - } - - ConfigurationProperty get(String propertyName) { - return this.byName.get(propertyName); - } - - Stream stream() { - return this.byName.values().stream(); - } - - @SuppressWarnings("unchecked") - static ConfigurationProperties fromFiles(Iterable files) { - try { - ObjectMapper objectMapper = new ObjectMapper(); - List properties = new ArrayList<>(); - for (File file : files) { - Map json = objectMapper.readValue(file, Map.class); - for (Map property : (List>) json.get("properties")) { - properties.add(ConfigurationProperty.fromJsonProperties(property)); - } - } - return new ConfigurationProperties(properties); - } - catch (IOException ex) { - throw new RuntimeException("Failed to load configuration metadata", ex); - } - } - -} diff --git a/buildSrc/src/main/java/org/springframework/pulsar/gradle/docs/configprops/ConfigurationPropertiesPlugin.java b/buildSrc/src/main/java/org/springframework/pulsar/gradle/docs/configprops/ConfigurationPropertiesPlugin.java deleted file mode 100644 index 84fd067c..00000000 --- a/buildSrc/src/main/java/org/springframework/pulsar/gradle/docs/configprops/ConfigurationPropertiesPlugin.java +++ /dev/null @@ -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: - * - *
    - *
  • Adding a dependency on the configuration properties annotation processor. - *
  • Configuring the additional metadata locations annotation processor compiler - * argument. - *
  • Adding the outputs of the processResources task as inputs of the compileJava task - * to ensure that the additional metadata is available when the annotation processor runs. - *
  • Registering a {@link CheckAdditionalSpringConfigurationMetadata} task and - * configuring the {@code check} task to depend upon it. - *
  • Defining an artifact for the resulting configuration property metadata so that it - * can be consumed by downstream projects. - *
- * - * @author Andy Wilkinson - * @author Chris Bono - */ -public class ConfigurationPropertiesPlugin implements Plugin { - - // 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 checkConfigurationMetadata = project.getTasks() - .register(CHECK_ADDITIONAL_SPRING_CONFIGURATION_METADATA_TASK_NAME, - CheckAdditionalSpringConfigurationMetadata.class); - checkConfigurationMetadata.configure((check) -> { - SourceSet mainSourceSet = project.getExtensions().getByType(JavaPluginExtension.class).getSourceSets() - .getByName(SourceSet.MAIN_SOURCE_SET_NAME); - check.setSource(mainSourceSet.getResources()); - check.include("META-INF/additional-spring-configuration-metadata.json"); - check.getReportLocation().set(project.getLayout().getBuildDirectory() - .file("reports/additional-spring-configuration-metadata/check.txt")); - }); - project.getTasks().named(LifecycleBasePlugin.CHECK_TASK_NAME) - .configure((check) -> check.dependsOn(checkConfigurationMetadata)); - } - -} diff --git a/buildSrc/src/main/java/org/springframework/pulsar/gradle/docs/configprops/ConfigurationProperty.java b/buildSrc/src/main/java/org/springframework/pulsar/gradle/docs/configprops/ConfigurationProperty.java deleted file mode 100644 index 91cd51c9..00000000 --- a/buildSrc/src/main/java/org/springframework/pulsar/gradle/docs/configprops/ConfigurationProperty.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2012-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.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 property) { - String name = (String) property.get("name"); - String type = (String) property.get("type"); - Object defaultValue = property.get("defaultValue"); - String description = (String) property.get("description"); - boolean deprecated = property.containsKey("deprecated"); - return new ConfigurationProperty(name, type, defaultValue, description, deprecated); - } - -} diff --git a/buildSrc/src/main/java/org/springframework/pulsar/gradle/docs/configprops/DocumentConfigurationProperties.java b/buildSrc/src/main/java/org/springframework/pulsar/gradle/docs/configprops/DocumentConfigurationProperties.java deleted file mode 100644 index cde989ce..00000000 --- a/buildSrc/src/main/java/org/springframework/pulsar/gradle/docs/configprops/DocumentConfigurationProperties.java +++ /dev/null @@ -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()); - } -} diff --git a/buildSrc/src/main/java/org/springframework/pulsar/gradle/docs/configprops/Row.java b/buildSrc/src/main/java/org/springframework/pulsar/gradle/docs/configprops/Row.java deleted file mode 100644 index 285d7967..00000000 --- a/buildSrc/src/main/java/org/springframework/pulsar/gradle/docs/configprops/Row.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2012-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.pulsar.gradle.docs.configprops; - -/** - * Abstract class for rows in {@link Table}. - * - * @author Brian Clozel - * @author Phillip Webb - */ -abstract class Row implements Comparable { - - private final Snippet snippet; - - private final String id; - - protected Row(Snippet snippet, String id) { - this.snippet = snippet; - this.id = id; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - Row other = (Row) obj; - return this.id.equals(other.id); - } - - @Override - public int hashCode() { - return this.id.hashCode(); - } - - @Override - public int compareTo(Row other) { - return this.id.compareTo(other.id); - } - - String getAnchor() { - return this.snippet.getAnchor() + "." + this.id; - } - - abstract void write(Asciidoc asciidoc); - -} diff --git a/buildSrc/src/main/java/org/springframework/pulsar/gradle/docs/configprops/SingleRow.java b/buildSrc/src/main/java/org/springframework/pulsar/gradle/docs/configprops/SingleRow.java deleted file mode 100644 index 56ee6e55..00000000 --- a/buildSrc/src/main/java/org/springframework/pulsar/gradle/docs/configprops/SingleRow.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2012-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.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("<", "<").replace(">", ">"); - builder.appendln("|+++", cleanedDescription, "+++"); - } - } - - private void writeDefaultValue(Asciidoc builder) { - String defaultValue = (this.defaultValue != null) ? this.defaultValue : ""; - if (defaultValue.isEmpty()) { - builder.appendln("|"); - } - else { - defaultValue = defaultValue.replace("\\", "\\\\").replace("|", "\\|"); - builder.appendln("|`+", defaultValue, "+`"); - } - } - -} diff --git a/buildSrc/src/main/java/org/springframework/pulsar/gradle/docs/configprops/Snippet.java b/buildSrc/src/main/java/org/springframework/pulsar/gradle/docs/configprops/Snippet.java deleted file mode 100644 index dff8adf8..00000000 --- a/buildSrc/src/main/java/org/springframework/pulsar/gradle/docs/configprops/Snippet.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright 2012-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.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 prefixes; - - private final Map overrides; - - Snippet(String anchor, String title, Consumer config) { - Set prefixes = new LinkedHashSet<>(); - Map overrides = new LinkedHashMap<>(); - if (config != null) { - config.accept(new Config() { - - @Override - public void accept(String prefix) { - prefixes.add(prefix); - } - - @Override - public void accept(String prefix, String description) { - overrides.put(prefix, description); - } - - }); - } - this.anchor = anchor; - this.title = title; - this.prefixes = prefixes; - this.overrides = overrides; - } - - String getAnchor() { - return this.anchor; - } - - String getTitle() { - return this.title; - } - - void forEachPrefix(Consumer action) { - this.prefixes.forEach(action); - } - - void forEachOverride(BiConsumer action) { - this.overrides.forEach(action); - } - - /** - * Callback to configure the snippet. - */ - interface Config { - - /** - * Accept the given prefix using the meta-data description. - * @param prefix the prefix to accept - */ - void accept(String prefix); - - /** - * Accept the given prefix with a defined description. - * @param prefix the prefix to accept - * @param description the description to use - */ - void accept(String prefix, String description); - - } - -} diff --git a/buildSrc/src/main/java/org/springframework/pulsar/gradle/docs/configprops/Snippets.java b/buildSrc/src/main/java/org/springframework/pulsar/gradle/docs/configprops/Snippets.java deleted file mode 100644 index 1f6f13ca..00000000 --- a/buildSrc/src/main/java/org/springframework/pulsar/gradle/docs/configprops/Snippets.java +++ /dev/null @@ -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 snippets = new ArrayList<>(); - - Snippets(FileCollection configurationPropertyMetadata) { - this.properties = ConfigurationProperties.fromFiles(configurationPropertyMetadata); - } - - void add(String anchor, String title, Consumer config) { - this.snippets.add(new Snippet(anchor, title, config)); - } - - void writeTo(Path outputDirectory) throws IOException { - createDirectory(outputDirectory); - Set remaining = this.properties.stream().filter((property) -> !property.isDeprecated()) - .map(ConfigurationProperty::getName).collect(Collectors.toSet()); - for (Snippet snippet : this.snippets) { - Set written = writeSnippet(outputDirectory, snippet, remaining); - remaining.removeAll(written); - } - if (!remaining.isEmpty()) { - throw new IllegalStateException( - "The following keys were not written to the documentation: " + String.join(", ", remaining)); - } - } - - private Set writeSnippet(Path outputDirectory, Snippet snippet, Set remaining) throws IOException { - Table table = new Table(); - Set added = new HashSet<>(); - snippet.forEachOverride((prefix, description) -> { - CompoundRow row = new CompoundRow(snippet, prefix, description); - remaining.stream().filter((candidate) -> candidate.startsWith(prefix)).forEach((name) -> { - if (added.add(name)) { - row.addProperty(this.properties.get(name)); - } - }); - table.addRow(row); - }); - - snippet.forEachPrefix((prefix) -> - remaining.stream().filter((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 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); - } - - } -} diff --git a/buildSrc/src/main/java/org/springframework/pulsar/gradle/docs/configprops/Table.java b/buildSrc/src/main/java/org/springframework/pulsar/gradle/docs/configprops/Table.java deleted file mode 100644 index 7f58b483..00000000 --- a/buildSrc/src/main/java/org/springframework/pulsar/gradle/docs/configprops/Table.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2012-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.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 rows = new TreeSet<>(); - - void addRow(Row row) { - this.rows.add(row); - } - - void write(Asciidoc asciidoc) { - asciidoc.appendln("[cols=\"4,3,3\", options=\"header\"]"); - asciidoc.appendln("|==="); - asciidoc.appendln("|Name|Description|Default Value"); - asciidoc.appendln(); - this.rows.forEach((entry) -> { - entry.write(asciidoc); - asciidoc.appendln(); - }); - asciidoc.appendln("|==="); - } - -} diff --git a/buildSrc/src/test/java/org/springframework/pulsar/gradle/docs/configprops/CompoundRowTests.java b/buildSrc/src/test/java/org/springframework/pulsar/gradle/docs/configprops/CompoundRowTests.java deleted file mode 100644 index cd5237f4..00000000 --- a/buildSrc/src/test/java/org/springframework/pulsar/gradle/docs/configprops/CompoundRowTests.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2012-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.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]]<>" + NEWLINE - + "|+++This is a description.+++" + NEWLINE + "|" + NEWLINE); - } - -} diff --git a/buildSrc/src/test/java/org/springframework/pulsar/gradle/docs/configprops/ConfigurationPropertiesTests.java b/buildSrc/src/test/java/org/springframework/pulsar/gradle/docs/configprops/ConfigurationPropertiesTests.java deleted file mode 100644 index 5948aa20..00000000 --- a/buildSrc/src/test/java/org/springframework/pulsar/gradle/docs/configprops/ConfigurationPropertiesTests.java +++ /dev/null @@ -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); - } - -} diff --git a/buildSrc/src/test/java/org/springframework/pulsar/gradle/docs/configprops/SingleRowTests.java b/buildSrc/src/test/java/org/springframework/pulsar/gradle/docs/configprops/SingleRowTests.java deleted file mode 100644 index 78cc8f13..00000000 --- a/buildSrc/src/test/java/org/springframework/pulsar/gradle/docs/configprops/SingleRowTests.java +++ /dev/null @@ -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]]<>" - + 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]]<>" - + 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]]<>" - + 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]]<>" - + 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]]<>" - + NEWLINE + "|+++This is a description with a \\| pipe.+++" + NEWLINE + "|" + NEWLINE); - } - - @Test - void mapProperty() { - ConfigurationProperty property = new ConfigurationProperty("spring.test.prop", - "java.util.Map", 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]]<>" + 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", 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]]<>" - + NEWLINE + "|+++This is a description.+++" + NEWLINE + "|`+first," + NEWLINE + "second," + NEWLINE - + "third+`" + NEWLINE); - } - -} diff --git a/buildSrc/src/test/java/org/springframework/pulsar/gradle/docs/configprops/TableTests.java b/buildSrc/src/test/java/org/springframework/pulsar/gradle/docs/configprops/TableTests.java deleted file mode 100644 index 6866aea6..00000000 --- a/buildSrc/src/test/java/org/springframework/pulsar/gradle/docs/configprops/TableTests.java +++ /dev/null @@ -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]]<>" + NEWLINE + - "|+++This is another description.+++" + NEWLINE + - "|`+other value+`" + NEWLINE + NEWLINE + - "|[[my.spring.test.prop]]<>" + NEWLINE + - "|+++This is a description.+++" + NEWLINE + - "|`+something+`" + NEWLINE + NEWLINE + - "|===" + NEWLINE); - // @formatter:on - } - -} diff --git a/buildSrc/src/test/resources/spring-configuration-metadata.json b/buildSrc/src/test/resources/spring-configuration-metadata.json deleted file mode 100644 index e975b1e3..00000000 --- a/buildSrc/src/test/resources/spring-configuration-metadata.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "properties": [ - { - "name": "example.counter", - "type": "java.lang.Integer", - "defaultValue": 0 - } - ] -} diff --git a/settings.gradle b/settings.gradle index a660bfc1..8f95b94e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -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' diff --git a/spring-pulsar-dependencies/build.gradle b/spring-pulsar-dependencies/build.gradle index c6b3ede1..1d71dabe 100644 --- a/spring-pulsar-dependencies/build.gradle +++ b/spring-pulsar-dependencies/build.gradle @@ -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" } } diff --git a/spring-pulsar-docs/build.gradle b/spring-pulsar-docs/build.gradle index 0941e2c2..c5a28d12 100644 --- a/spring-pulsar-docs/build.gradle +++ b/spring-pulsar-docs/build.gradle @@ -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 excludedProjects = ['spring-pulsar-sample-apps:sample-app1', - 'spring-pulsar-sample-apps:sample-app2', - 'spring-pulsar-sample-apps:sample-reactive'] Set 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" } diff --git a/spring-pulsar-docs/src/main/asciidoc/application-properties.adoc b/spring-pulsar-docs/src/main/asciidoc/application-properties.adoc deleted file mode 100644 index a398d476..00000000 --- a/spring-pulsar-docs/src/main/asciidoc/application-properties.adoc +++ /dev/null @@ -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[] diff --git a/spring-pulsar-docs/src/main/asciidoc/attributes-variables.adoc b/spring-pulsar-docs/src/main/asciidoc/attributes-variables.adoc new file mode 100644 index 00000000..3f15dbac --- /dev/null +++ b/spring-pulsar-docs/src/main/asciidoc/attributes-variables.adoc @@ -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 diff --git a/spring-pulsar-docs/src/main/asciidoc/attributes.adoc b/spring-pulsar-docs/src/main/asciidoc/attributes.adoc index d84eb246..54b093b4 100644 --- a/spring-pulsar-docs/src/main/asciidoc/attributes.adoc +++ b/spring-pulsar-docs/src/main/asciidoc/attributes.adoc @@ -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[] diff --git a/spring-pulsar-docs/src/main/asciidoc/index.adoc b/spring-pulsar-docs/src/main/asciidoc/index.adoc index 591c7875..9c5795dd 100644 --- a/spring-pulsar-docs/src/main/asciidoc/index.adoc +++ b/spring-pulsar-docs/src/main/asciidoc/index.adoc @@ -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] diff --git a/spring-pulsar-docs/src/main/asciidoc/pulsar-admin.adoc b/spring-pulsar-docs/src/main/asciidoc/pulsar-admin.adoc index 21d12076..a345b0e3 100644 --- a/spring-pulsar-docs/src/main/asciidoc/pulsar-admin.adoc +++ b/spring-pulsar-docs/src/main/asciidoc/pulsar-admin.adoc @@ -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)://:`. There are many application properties available to configure the client. -See the <> for application properties prefixed with `spring.pulsar.administration`. +See the {spring-boot-pulsar-config-props}[`spring.pulsar.administration.*`] application properties. [[pulsar-admin-authentication]] === Authentication diff --git a/spring-pulsar-docs/src/main/asciidoc/pulsar-binder.adoc b/spring-pulsar-docs/src/main/asciidoc/pulsar-binder.adoc index 420dd6bd..aba4ea6b 100644 --- a/spring-pulsar-docs/src/main/asciidoc/pulsar-binder.adoc +++ b/spring-pulsar-docs/src/main/asciidoc/pulsar-binder.adoc @@ -18,7 +18,7 @@ We need to include the following dependency on your application to use Apache Pu org.springframework.pulsar spring-pulsar-spring-cloud-stream-binder - {spring-pulsar-version} + {spring-pulsar-binder-version} ---- @@ -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}' } ---- diff --git a/spring-pulsar-docs/src/main/asciidoc/pulsar-function.adoc b/spring-pulsar-docs/src/main/asciidoc/pulsar-function.adoc index 8d6aec07..7d2af5ad 100644 --- a/spring-pulsar-docs/src/main/asciidoc/pulsar-function.adoc +++ b/spring-pulsar-docs/src/main/asciidoc/pulsar-function.adoc @@ -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 <> for available client options (including authentication). Other available application properties can be found in the <> prefixed by `spring.pulsar.function`. +However, because it leverages the already configured `PulsarAdministration`, see <> 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. diff --git a/spring-pulsar-docs/src/main/asciidoc/pulsar-header.adoc b/spring-pulsar-docs/src/main/asciidoc/pulsar-header.adoc index 3f1fff93..5b1a0f4d 100644 --- a/spring-pulsar-docs/src/main/asciidoc/pulsar-header.adoc +++ b/spring-pulsar-docs/src/main/asciidoc/pulsar-header.adoc @@ -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. diff --git a/spring-pulsar-docs/src/main/asciidoc/pulsar.adoc b/spring-pulsar-docs/src/main/asciidoc/pulsar.adoc index 7ff14442..1199e113 100644 --- a/spring-pulsar-docs/src/main/asciidoc/pulsar.adoc +++ b/spring-pulsar-docs/src/main/asciidoc/pulsar.adoc @@ -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 <> 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 <>. +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 <> 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 <>. +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 <> 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 <> 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 diff --git a/spring-pulsar-docs/src/main/asciidoc/quick-tour.adoc b/spring-pulsar-docs/src/main/asciidoc/quick-tour.adoc index 4e2bfbd6..24f9467e 100644 --- a/spring-pulsar-docs/src/main/asciidoc/quick-tour.adoc +++ b/spring-pulsar-docs/src/main/asciidoc/quick-tour.adoc @@ -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 org.springframework.pulsar spring-pulsar-spring-boot-starter - {spring-pulsar-version} + {spring-pulsar-starter-version} ---- @@ -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}' } ---- diff --git a/spring-pulsar-docs/src/main/asciidoc/reactive-pulsar.adoc b/spring-pulsar-docs/src/main/asciidoc/reactive-pulsar.adoc index 4b34c005..ae6f32fb 100644 --- a/spring-pulsar-docs/src/main/asciidoc/reactive-pulsar.adoc +++ b/spring-pulsar-docs/src/main/asciidoc/reactive-pulsar.adoc @@ -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 <> 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 <> 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 <> 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 <> 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> listen2(Flux> 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 <> 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 diff --git a/spring-pulsar-docs/src/main/asciidoc/reactive-quick-tour.adoc b/spring-pulsar-docs/src/main/asciidoc/reactive-quick-tour.adoc index c2f2db70..7320222e 100644 --- a/spring-pulsar-docs/src/main/asciidoc/reactive-quick-tour.adoc +++ b/spring-pulsar-docs/src/main/asciidoc/reactive-quick-tour.adoc @@ -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 org.springframework.pulsar spring-pulsar-reactive-spring-boot-starter - {spring-pulsar-version} + {spring-pulsar-starter-version} ---- @@ -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}' } ---- diff --git a/spring-pulsar-reactive-spring-boot-starter/build.gradle b/spring-pulsar-reactive-spring-boot-starter/build.gradle deleted file mode 100644 index 859c7c75..00000000 --- a/spring-pulsar-reactive-spring-boot-starter/build.gradle +++ /dev/null @@ -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' -} diff --git a/spring-pulsar-reactive/build.gradle b/spring-pulsar-reactive/build.gradle index 326f06cc..b68e14ef 100644 --- a/spring-pulsar-reactive/build.gradle +++ b/spring-pulsar-reactive/build.gradle @@ -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' +} diff --git a/spring-pulsar-spring-boot-autoconfigure/src/test/java/org/springframework/pulsar/autoconfigure/ReactivePulsarListenerTests.java b/spring-pulsar-reactive/src/intTest/java/org/springframework/pulsar/autoconfigure/ReactivePulsarListenerIntegrationTests.java similarity index 98% rename from spring-pulsar-spring-boot-autoconfigure/src/test/java/org/springframework/pulsar/autoconfigure/ReactivePulsarListenerTests.java rename to spring-pulsar-reactive/src/intTest/java/org/springframework/pulsar/autoconfigure/ReactivePulsarListenerIntegrationTests.java index adfdda3f..60e930a5 100644 --- a/spring-pulsar-spring-boot-autoconfigure/src/test/java/org/springframework/pulsar/autoconfigure/ReactivePulsarListenerTests.java +++ b/spring-pulsar-reactive/src/intTest/java/org/springframework/pulsar/autoconfigure/ReactivePulsarListenerIntegrationTests.java @@ -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); diff --git a/spring-pulsar-spring-boot-autoconfigure/src/integration-test/resources/application.yml b/spring-pulsar-reactive/src/intTest/resources/application.yml similarity index 100% rename from spring-pulsar-spring-boot-autoconfigure/src/integration-test/resources/application.yml rename to spring-pulsar-reactive/src/intTest/resources/application.yml diff --git a/spring-pulsar-spring-boot-autoconfigure/src/integration-test/resources/logback-test.xml b/spring-pulsar-reactive/src/intTest/resources/logback-test.xml similarity index 100% rename from spring-pulsar-spring-boot-autoconfigure/src/integration-test/resources/logback-test.xml rename to spring-pulsar-reactive/src/intTest/resources/logback-test.xml diff --git a/spring-pulsar-sample-apps/sample-app1/build.gradle b/spring-pulsar-sample-apps/sample-app1/build.gradle index 2bce28a3..7dc4bf62 100644 --- a/spring-pulsar-sample-apps/sample-app1/build.gradle +++ b/spring-pulsar-sample-apps/sample-app1/build.gradle @@ -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 diff --git a/spring-pulsar-sample-apps/sample-app2/build.gradle b/spring-pulsar-sample-apps/sample-app2/build.gradle index d708f21c..3b1c8f9e 100644 --- a/spring-pulsar-sample-apps/sample-app2/build.gradle +++ b/spring-pulsar-sample-apps/sample-app2/build.gradle @@ -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 diff --git a/spring-pulsar-sample-apps/sample-pulsar-binder/build.gradle b/spring-pulsar-sample-apps/sample-pulsar-binder/build.gradle index b941508d..66861151 100644 --- a/spring-pulsar-sample-apps/sample-pulsar-binder/build.gradle +++ b/spring-pulsar-sample-apps/sample-pulsar-binder/build.gradle @@ -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 { diff --git a/spring-pulsar-sample-apps/sample-pulsar-functions/sample-signup-app/build.gradle b/spring-pulsar-sample-apps/sample-pulsar-functions/sample-signup-app/build.gradle index 2f00d66a..81b13cd9 100644 --- a/spring-pulsar-sample-apps/sample-pulsar-functions/sample-signup-app/build.gradle +++ b/spring-pulsar-sample-apps/sample-pulsar-functions/sample-signup-app/build.gradle @@ -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' diff --git a/spring-pulsar-sample-apps/sample-pulsar-reader/build.gradle b/spring-pulsar-sample-apps/sample-pulsar-reader/build.gradle index 184e16a7..8a218877 100644 --- a/spring-pulsar-sample-apps/sample-pulsar-reader/build.gradle +++ b/spring-pulsar-sample-apps/sample-pulsar-reader/build.gradle @@ -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' } diff --git a/spring-pulsar-sample-apps/sample-reactive/build.gradle b/spring-pulsar-sample-apps/sample-reactive/build.gradle index 84558f1f..080ca547 100644 --- a/spring-pulsar-sample-apps/sample-reactive/build.gradle +++ b/spring-pulsar-sample-apps/sample-reactive/build.gradle @@ -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' } diff --git a/spring-pulsar-spring-boot-autoconfigure/build.gradle b/spring-pulsar-spring-boot-autoconfigure/build.gradle deleted file mode 100644 index 2ed2f534..00000000 --- a/spring-pulsar-spring-boot-autoconfigure/build.gradle +++ /dev/null @@ -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 -} diff --git a/spring-pulsar-spring-boot-autoconfigure/src/main/java/org/springframework/pulsar/autoconfigure/AuthParameterUtils.java b/spring-pulsar-spring-boot-autoconfigure/src/main/java/org/springframework/pulsar/autoconfigure/AuthParameterUtils.java deleted file mode 100644 index 2e8132ec..00000000 --- a/spring-pulsar-spring-boot-autoconfigure/src/main/java/org/springframework/pulsar/autoconfigure/AuthParameterUtils.java +++ /dev/null @@ -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 convertWellKnownLowerCaseKeysToCamelCase(Map params) { - return params.entrySet().stream().collect( - Collectors.toMap(entry -> WellKnownAuthParameters.toCamelCaseKey(entry.getKey()), Map.Entry::getValue)); - } - - private static Map convertKebabCaseKeysToCamelCase(Map params) { - return params.entrySet().stream() - .collect(Collectors.toMap(entry -> convertKebabCaseToCamelCase(entry.getKey()), Map.Entry::getValue)); - } - - static String maybeConvertToEncodedParamString(Map 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); - } - } - -} diff --git a/spring-pulsar-spring-boot-autoconfigure/src/main/java/org/springframework/pulsar/autoconfigure/ConsumerConfigProperties.java b/spring-pulsar-spring-boot-autoconfigure/src/main/java/org/springframework/pulsar/autoconfigure/ConsumerConfigProperties.java deleted file mode 100644 index 2551a972..00000000 --- a/spring-pulsar-spring-boot-autoconfigure/src/main/java/org/springframework/pulsar/autoconfigure/ConsumerConfigProperties.java +++ /dev/null @@ -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 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 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 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 getTopics() { - return this.topics; - } - - public void setTopics(Set 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 getSubscriptionProperties() { - return this.subscriptionProperties; - } - - public void setSubscriptionProperties(Map 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 getProperties() { - return this.properties; - } - - public void setProperties(SortedMap 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 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; - } - -} diff --git a/spring-pulsar-spring-boot-autoconfigure/src/main/java/org/springframework/pulsar/autoconfigure/ProducerConfigProperties.java b/spring-pulsar-spring-boot-autoconfigure/src/main/java/org/springframework/pulsar/autoconfigure/ProducerConfigProperties.java deleted file mode 100644 index e1355b84..00000000 --- a/spring-pulsar-spring-boot-autoconfigure/src/main/java/org/springframework/pulsar/autoconfigure/ProducerConfigProperties.java +++ /dev/null @@ -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 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 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 getEncryptionKeys() { - return this.encryptionKeys; - } - - public void setEncryptionKeys(Set 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 getProperties() { - return this.properties; - } - - public void setProperties(Map properties) { - this.properties = properties; - } - - public Cache getCache() { - return this.cache; - } - - public Map 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; - } - -} diff --git a/spring-pulsar-spring-boot-autoconfigure/src/main/java/org/springframework/pulsar/autoconfigure/PulsarAnnotationDrivenConfiguration.java b/spring-pulsar-spring-boot-autoconfigure/src/main/java/org/springframework/pulsar/autoconfigure/PulsarAnnotationDrivenConfiguration.java deleted file mode 100644 index a278f7f2..00000000 --- a/spring-pulsar-spring-boot-autoconfigure/src/main/java/org/springframework/pulsar/autoconfigure/PulsarAnnotationDrivenConfiguration.java +++ /dev/null @@ -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> consumerFactoryProvider, - ObjectProvider observationRegistryProvider, - ObjectProvider 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> 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 { - - } - -} diff --git a/spring-pulsar-spring-boot-autoconfigure/src/main/java/org/springframework/pulsar/autoconfigure/PulsarAutoConfiguration.java b/spring-pulsar-spring-boot-autoconfigure/src/main/java/org/springframework/pulsar/autoconfigure/PulsarAutoConfiguration.java deleted file mode 100644 index 1a5af4f9..00000000 --- a/spring-pulsar-spring-boot-autoconfigure/src/main/java/org/springframework/pulsar/autoconfigure/PulsarAutoConfiguration.java +++ /dev/null @@ -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 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) { - 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 pulsarFunctions, ObjectProvider pulsarSinks, - ObjectProvider 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()); - } - -} diff --git a/spring-pulsar-spring-boot-autoconfigure/src/main/java/org/springframework/pulsar/autoconfigure/PulsarProperties.java b/spring-pulsar-spring-boot-autoconfigure/src/main/java/org/springframework/pulsar/autoconfigure/PulsarProperties.java deleted file mode 100644 index 914a8a61..00000000 --- a/spring-pulsar-spring-boot-autoconfigure/src/main/java/org/springframework/pulsar/autoconfigure/PulsarProperties.java +++ /dev/null @@ -1,1390 +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 java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; - -import org.apache.pulsar.client.api.ProxyProtocol; -import org.apache.pulsar.common.schema.SchemaType; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; -import org.springframework.boot.context.properties.PropertyMapper; -import org.springframework.lang.Nullable; -import org.springframework.pulsar.listener.AckMode; -import org.springframework.util.CollectionUtils; -import org.springframework.util.StringUtils; -import org.springframework.util.unit.DataSize; - -/** - * Configuration properties for Spring for Apache Pulsar. - *

- * Users should refer to Pulsar documentation for complete descriptions of these - * properties. - * - * @author Soby Chacko - * @author Alexander Preuß - * @author Christophe Bornet - * @author Chris Bono - */ -@ConfigurationProperties(prefix = "spring.pulsar") -public class PulsarProperties { - - @NestedConfigurationProperty - private final ConsumerConfigProperties consumer = new ConsumerConfigProperties(); - - private final Client client = new Client(); - - private final Function function = new Function(); - - private final Listener listener = new Listener(); - - @NestedConfigurationProperty - private final ProducerConfigProperties producer = new ProducerConfigProperties(); - - private final Template template = new Template(); - - private final Admin admin = new Admin(); - - private final Reader reader = new Reader(); - - private final Defaults defaults = new Defaults(); - - public ConsumerConfigProperties getConsumer() { - return this.consumer; - } - - public Client getClient() { - return this.client; - } - - public Listener getListener() { - return this.listener; - } - - public Function getFunction() { - return this.function; - } - - public ProducerConfigProperties getProducer() { - return this.producer; - } - - public Template getTemplate() { - return this.template; - } - - public Admin getAdministration() { - return this.admin; - } - - public Reader getReader() { - return this.reader; - } - - public Defaults getDefaults() { - return this.defaults; - } - - public Map buildConsumerProperties() { - return new HashMap<>(this.consumer.buildProperties()); - } - - public Map buildClientProperties() { - return new HashMap<>(this.client.buildProperties()); - } - - public Map buildProducerProperties() { - return new HashMap<>(this.producer.buildProperties()); - } - - public Map buildAdminProperties() { - return new HashMap<>(this.admin.buildProperties()); - } - - public Map buildReaderProperties() { - return new HashMap<>(this.reader.buildProperties()); - } - - public static class Template { - - /** - * Whether to record observations for send operations when the Observations API is - * available. - */ - private Boolean observationsEnabled = true; - - public Boolean isObservationsEnabled() { - return this.observationsEnabled; - } - - public void setObservationsEnabled(Boolean observationsEnabled) { - this.observationsEnabled = observationsEnabled; - } - - } - - public static class Cache { - - /** Time period to expire unused entries in the cache. */ - private Duration expireAfterAccess = Duration.ofMinutes(1); - - /** Maximum size of cache (entries). */ - private Long maximumSize = 1000L; - - /** Initial size of cache. */ - private Integer initialCapacity = 50; - - public Duration getExpireAfterAccess() { - return this.expireAfterAccess; - } - - public void setExpireAfterAccess(Duration expireAfterAccess) { - this.expireAfterAccess = expireAfterAccess; - } - - public Long getMaximumSize() { - return this.maximumSize; - } - - public void setMaximumSize(Long maximumSize) { - this.maximumSize = maximumSize; - } - - public Integer getInitialCapacity() { - return this.initialCapacity; - } - - public void setInitialCapacity(Integer initialCapacity) { - this.initialCapacity = initialCapacity; - } - - } - - public static class Client { - - /** - * Pulsar service URL in the format - * '(pulsar|pulsar+ssl)://<host>:<port>'. - */ - private String serviceUrl = "pulsar://localhost:6650"; - - /** - * Listener name for lookup. Clients can use listenerName to choose one of the - * listeners as the service URL to create a connection to the broker. To use this, - * "advertisedListeners" must be enabled on the broker. - */ - private String listenerName; - - /** - * Fully qualified class name of the authentication plugin. - */ - private String authPluginClassName; - - /** - * Authentication parameter(s) as a JSON encoded string. - */ - private String authParams; - - /** - * Authentication parameter(s) as a map of parameter names to parameter values. - */ - private Map authentication; - - /** - * Client operation timeout. - */ - private Duration operationTimeout = Duration.ofSeconds(30); - - /** - * Client lookup timeout. - */ - private Duration lookupTimeout = Duration.ofMillis(-1); - - /** - * Number of threads to be used for handling connections to brokers. - */ - private Integer numIoThreads = 1; - - /** - * Number of threads to be used for message listeners. The listener thread pool is - * shared across all the consumers and readers that are using a "listener" model - * to get messages. For a given consumer, the listener will always be invoked from - * the same thread, to ensure ordering. - */ - private Integer numListenerThreads = 1; - - /** - * Maximum number of connections that the client will open to a single broker. - */ - private Integer numConnectionsPerBroker = 1; - - /** - * Whether to use TCP no-delay flag on the connection, to disable Nagle algorithm. - */ - private Boolean useTcpNoDelay = true; - - /** - * Whether to use TLS encryption on the connection. - */ - private Boolean useTls = false; - - /** - * Whether the hostname is validated when the proxy creates a TLS connection with - * brokers. - */ - private Boolean tlsHostnameVerificationEnable = false; - - /** - * Path to the trusted TLS certificate file. - */ - private String tlsTrustCertsFilePath; - - /** - * Whether the client accepts untrusted TLS certificates from the broker. - */ - private Boolean tlsAllowInsecureConnection = false; - - /** - * Enable KeyStore instead of PEM type configuration if TLS is enabled. - */ - private Boolean useKeyStoreTls = false; - - /** - * Name of the security provider used for SSL connections. - */ - private String sslProvider; - - /** - * File format of the trust store file. - */ - private String tlsTrustStoreType; - - /** - * Location of the trust store file. - */ - private String tlsTrustStorePath; - - /** - * Store password for the key store file. - */ - private String tlsTrustStorePassword; - - /** - * Comma-separated list of cipher suites. This is a named combination of - * authentication, encryption, MAC and key exchange algorithm used to negotiate - * the security settings for a network connection using TLS or SSL network - * protocol. By default, all the available cipher suites are supported. - */ - private Set tlsCiphers; - - /** - * Comma-separated list of SSL protocols used to generate the SSLContext. Allowed - * values in recent JVMs are TLS, TLSv1.3, TLSv1.2 and TLSv1.1. - */ - private Set tlsProtocols; - - /** - * Interval between each stat info. - */ - private Duration statsInterval = Duration.ofSeconds(60); - - /** - * Number of concurrent lookup-requests allowed to send on each broker-connection - * to prevent overload on broker. - */ - private Integer maxConcurrentLookupRequest = 5000; - - /** - * Number of max lookup-requests allowed on each broker-connection to prevent - * overload on broker. - */ - private Integer maxLookupRequest = 50000; - - /** - * Maximum number of times a lookup-request to a broker will be redirected. - */ - private Integer maxLookupRedirects = 20; - - /** - * Maximum number of broker-rejected requests in a certain timeframe, after which - * the current connection is closed and a new connection is created by the client. - */ - private Integer maxNumberOfRejectedRequestPerConnection = 50; - - /** - * Keep alive interval for broker-client connection. - */ - private Duration keepAliveInterval = Duration.ofSeconds(30); - - /** - * Duration to wait for a connection to a broker to be established. - */ - private Duration connectionTimeout = Duration.ofSeconds(10); - - /** - * Maximum duration for completing a request. - */ - private Duration requestTimeout = Duration.ofMinutes(1); - - /** - * Initial backoff interval. - */ - private Duration initialBackoffInterval = Duration.ofMillis(100); - - /** - * Maximum backoff interval. - */ - private Duration maxBackoffInterval = Duration.ofSeconds(30); - - /** - * Enables spin-waiting on executors and IO threads in order to reduce latency - * during context switches. - */ - private Boolean enableBusyWait = false; - - /** - * Limit of direct memory that will be allocated by the client. - */ - private DataSize memoryLimit = DataSize.ofMegabytes(64); - - /** - * URL of proxy service. proxyServiceUrl and proxyProtocol must be mutually - * inclusive. - */ - private String proxyServiceUrl; - - /** - * Protocol of proxy service. proxyServiceUrl and proxyProtocol must be mutually - * inclusive. - */ - private ProxyProtocol proxyProtocol; - - /** - * Enables transactions. To use this, start the transactionCoordinatorClient with - * the pulsar client. - */ - private Boolean enableTransaction = false; - - /** - * DNS lookup bind address. - */ - private String dnsLookupBindAddress; - - /** - * DNS lookup bind port. - */ - private Integer dnsLookupBindPort = 0; - - /** - * SOCKS5 proxy address. - */ - private String socks5ProxyAddress; - - /** - * SOCKS5 proxy username. - */ - private String socks5ProxyUsername; - - /** - * SOCKS5 proxy password. - */ - private String socks5ProxyPassword; - - public String getServiceUrl() { - return this.serviceUrl; - } - - public void setServiceUrl(String serviceUrl) { - this.serviceUrl = serviceUrl; - } - - public String getListenerName() { - return this.listenerName; - } - - public void setListenerName(String listenerName) { - this.listenerName = listenerName; - } - - public String getAuthPluginClassName() { - return this.authPluginClassName; - } - - public void setAuthPluginClassName(String authPluginClassName) { - this.authPluginClassName = authPluginClassName; - } - - public String getAuthParams() { - return this.authParams; - } - - public void setAuthParams(String authParams) { - this.authParams = authParams; - } - - public Map getAuthentication() { - return this.authentication; - } - - public void setAuthentication(Map authentication) { - this.authentication = authentication; - } - - public Duration getOperationTimeout() { - return this.operationTimeout; - } - - public void setOperationTimeout(Duration operationTimeout) { - this.operationTimeout = operationTimeout; - } - - public Duration getLookupTimeout() { - return this.lookupTimeout; - } - - public void setLookupTimeout(Duration lookupTimeout) { - this.lookupTimeout = lookupTimeout; - } - - public Integer getNumIoThreads() { - return this.numIoThreads; - } - - public void setNumIoThreads(Integer numIoThreads) { - this.numIoThreads = numIoThreads; - } - - public Integer getNumListenerThreads() { - return this.numListenerThreads; - } - - public void setNumListenerThreads(Integer numListenerThreads) { - this.numListenerThreads = numListenerThreads; - } - - public Integer getNumConnectionsPerBroker() { - return this.numConnectionsPerBroker; - } - - public void setNumConnectionsPerBroker(Integer numConnectionsPerBroker) { - this.numConnectionsPerBroker = numConnectionsPerBroker; - } - - public Boolean getUseTcpNoDelay() { - return this.useTcpNoDelay; - } - - public void setUseTcpNoDelay(Boolean useTcpNoDelay) { - this.useTcpNoDelay = useTcpNoDelay; - } - - public Boolean getUseTls() { - return this.useTls; - } - - public void setUseTls(Boolean useTls) { - this.useTls = useTls; - } - - public Boolean getTlsHostnameVerificationEnable() { - return this.tlsHostnameVerificationEnable; - } - - public void setTlsHostnameVerificationEnable(Boolean tlsHostnameVerificationEnable) { - this.tlsHostnameVerificationEnable = tlsHostnameVerificationEnable; - } - - public String getTlsTrustCertsFilePath() { - return this.tlsTrustCertsFilePath; - } - - public void setTlsTrustCertsFilePath(String tlsTrustCertsFilePath) { - this.tlsTrustCertsFilePath = tlsTrustCertsFilePath; - } - - public Boolean getTlsAllowInsecureConnection() { - return this.tlsAllowInsecureConnection; - } - - public void setTlsAllowInsecureConnection(Boolean tlsAllowInsecureConnection) { - this.tlsAllowInsecureConnection = tlsAllowInsecureConnection; - } - - public Boolean getUseKeyStoreTls() { - return this.useKeyStoreTls; - } - - public void setUseKeyStoreTls(Boolean useKeyStoreTls) { - this.useKeyStoreTls = useKeyStoreTls; - } - - public String getSslProvider() { - return this.sslProvider; - } - - public void setSslProvider(String sslProvider) { - this.sslProvider = sslProvider; - } - - public String getTlsTrustStoreType() { - return this.tlsTrustStoreType; - } - - public void setTlsTrustStoreType(String tlsTrustStoreType) { - this.tlsTrustStoreType = tlsTrustStoreType; - } - - public String getTlsTrustStorePath() { - return this.tlsTrustStorePath; - } - - public void setTlsTrustStorePath(String tlsTrustStorePath) { - this.tlsTrustStorePath = tlsTrustStorePath; - } - - public String getTlsTrustStorePassword() { - return this.tlsTrustStorePassword; - } - - public void setTlsTrustStorePassword(String tlsTrustStorePassword) { - this.tlsTrustStorePassword = tlsTrustStorePassword; - } - - public Set getTlsCiphers() { - return this.tlsCiphers; - } - - public void setTlsCiphers(Set tlsCiphers) { - this.tlsCiphers = tlsCiphers; - } - - public Set getTlsProtocols() { - return this.tlsProtocols; - } - - public void setTlsProtocols(Set tlsProtocols) { - this.tlsProtocols = tlsProtocols; - } - - public Duration getStatsInterval() { - return this.statsInterval; - } - - public void setStatsInterval(Duration statsInterval) { - this.statsInterval = statsInterval; - } - - public Integer getMaxConcurrentLookupRequest() { - return this.maxConcurrentLookupRequest; - } - - public void setMaxConcurrentLookupRequest(Integer maxConcurrentLookupRequest) { - this.maxConcurrentLookupRequest = maxConcurrentLookupRequest; - } - - public Integer getMaxLookupRequest() { - return this.maxLookupRequest; - } - - public void setMaxLookupRequest(Integer maxLookupRequest) { - this.maxLookupRequest = maxLookupRequest; - } - - public Integer getMaxLookupRedirects() { - return this.maxLookupRedirects; - } - - public void setMaxLookupRedirects(Integer maxLookupRedirects) { - this.maxLookupRedirects = maxLookupRedirects; - } - - public Integer getMaxNumberOfRejectedRequestPerConnection() { - return this.maxNumberOfRejectedRequestPerConnection; - } - - public void setMaxNumberOfRejectedRequestPerConnection(Integer maxNumberOfRejectedRequestPerConnection) { - this.maxNumberOfRejectedRequestPerConnection = maxNumberOfRejectedRequestPerConnection; - } - - public Duration getKeepAliveInterval() { - return this.keepAliveInterval; - } - - public void setKeepAliveInterval(Duration keepAliveInterval) { - this.keepAliveInterval = keepAliveInterval; - } - - public Duration getConnectionTimeout() { - return this.connectionTimeout; - } - - public void setConnectionTimeout(Duration connectionTimeout) { - this.connectionTimeout = connectionTimeout; - } - - public Duration getRequestTimeout() { - return this.requestTimeout; - } - - public void setRequestTimeout(Duration requestTimeout) { - this.requestTimeout = requestTimeout; - } - - public Duration getInitialBackoffInterval() { - return this.initialBackoffInterval; - } - - public void setInitialBackoffInterval(Duration initialBackoffInterval) { - this.initialBackoffInterval = initialBackoffInterval; - } - - public Duration getMaxBackoffInterval() { - return this.maxBackoffInterval; - } - - public void setMaxBackoffInterval(Duration maxBackoffInterval) { - this.maxBackoffInterval = maxBackoffInterval; - } - - public Boolean getEnableBusyWait() { - return this.enableBusyWait; - } - - public void setEnableBusyWait(Boolean enableBusyWait) { - this.enableBusyWait = enableBusyWait; - } - - public DataSize getMemoryLimit() { - return this.memoryLimit; - } - - public void setMemoryLimit(DataSize memoryLimit) { - this.memoryLimit = memoryLimit; - } - - public String getProxyServiceUrl() { - return this.proxyServiceUrl; - } - - public void setProxyServiceUrl(String proxyServiceUrl) { - this.proxyServiceUrl = proxyServiceUrl; - } - - public ProxyProtocol getProxyProtocol() { - return this.proxyProtocol; - } - - public void setProxyProtocol(ProxyProtocol proxyProtocol) { - this.proxyProtocol = proxyProtocol; - } - - public Boolean getEnableTransaction() { - return this.enableTransaction; - } - - public void setEnableTransaction(Boolean enableTransaction) { - this.enableTransaction = enableTransaction; - } - - public String getDnsLookupBindAddress() { - return this.dnsLookupBindAddress; - } - - public void setDnsLookupBindAddress(String dnsLookupBindAddress) { - this.dnsLookupBindAddress = dnsLookupBindAddress; - } - - public Integer getDnsLookupBindPort() { - return this.dnsLookupBindPort; - } - - public void setDnsLookupBindPort(Integer dnsLookupBindPort) { - this.dnsLookupBindPort = dnsLookupBindPort; - } - - public String getSocks5ProxyAddress() { - return this.socks5ProxyAddress; - } - - public void setSocks5ProxyAddress(String socks5ProxyAddress) { - this.socks5ProxyAddress = socks5ProxyAddress; - } - - public String getSocks5ProxyUsername() { - return this.socks5ProxyUsername; - } - - public void setSocks5ProxyUsername(String socks5ProxyUsername) { - this.socks5ProxyUsername = socks5ProxyUsername; - } - - public String getSocks5ProxyPassword() { - return this.socks5ProxyPassword; - } - - public void setSocks5ProxyPassword(String socks5ProxyPassword) { - this.socks5ProxyPassword = socks5ProxyPassword; - } - - public Map buildProperties() { - if (StringUtils.hasText(this.getAuthParams()) && !CollectionUtils.isEmpty(this.getAuthentication())) { - throw new IllegalArgumentException( - "Cannot set both spring.pulsar.client.authParams and spring.pulsar.client.authentication.*"); - } - - PulsarProperties.Properties properties = new Properties(); - - PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); - map.from(this::getServiceUrl).to(properties.in("serviceUrl")); - map.from(this::getListenerName).to(properties.in("listenerName")); - map.from(this::getAuthPluginClassName).to(properties.in("authPluginClassName")); - map.from(this::getAuthParams).to(properties.in("authParams")); - map.from(this::getAuthentication).as(AuthParameterUtils::maybeConvertToEncodedParamString) - .to(properties.in("authParams")); - map.from(this::getOperationTimeout).as(Duration::toMillis).to(properties.in("operationTimeoutMs")); - map.from(this::getLookupTimeout).as(Duration::toMillis).to(properties.in("lookupTimeoutMs")); - map.from(this::getNumIoThreads).to(properties.in("numIoThreads")); - map.from(this::getNumListenerThreads).to(properties.in("numListenerThreads")); - map.from(this::getNumConnectionsPerBroker).to(properties.in("connectionsPerBroker")); - map.from(this::getUseTcpNoDelay).to(properties.in("useTcpNoDelay")); - map.from(this::getUseTls).to(properties.in("useTls")); - map.from(this::getTlsHostnameVerificationEnable).to(properties.in("tlsHostnameVerificationEnable")); - map.from(this::getTlsTrustCertsFilePath).to(properties.in("tlsTrustCertsFilePath")); - map.from(this::getTlsAllowInsecureConnection).to(properties.in("tlsAllowInsecureConnection")); - map.from(this::getUseKeyStoreTls).to(properties.in("useKeyStoreTls")); - map.from(this::getSslProvider).to(properties.in("sslProvider")); - map.from(this::getTlsTrustStoreType).to(properties.in("tlsTrustStoreType")); - map.from(this::getTlsTrustStorePath).to(properties.in("tlsTrustStorePath")); - map.from(this::getTlsTrustStorePassword).to(properties.in("tlsTrustStorePassword")); - map.from(this::getTlsCiphers).to(properties.in("tlsCiphers")); - map.from(this::getTlsProtocols).to(properties.in("tlsProtocols")); - map.from(this::getStatsInterval).as(Duration::toSeconds).to(properties.in("statsIntervalSeconds")); - map.from(this::getMaxConcurrentLookupRequest).to(properties.in("concurrentLookupRequest")); - map.from(this::getMaxLookupRequest).to(properties.in("maxLookupRequest")); - map.from(this::getMaxLookupRedirects).to(properties.in("maxLookupRedirects")); - map.from(this::getMaxNumberOfRejectedRequestPerConnection) - .to(properties.in("maxNumberOfRejectedRequestPerConnection")); - map.from(this::getKeepAliveInterval).asInt(Duration::toSeconds) - .to(properties.in("keepAliveIntervalSeconds")); - map.from(this::getConnectionTimeout).asInt(Duration::toMillis).to(properties.in("connectionTimeoutMs")); - map.from(this::getRequestTimeout).asInt(Duration::toMillis).to(properties.in("requestTimeoutMs")); - map.from(this::getInitialBackoffInterval).as(Duration::toNanos) - .to(properties.in("initialBackoffIntervalNanos")); - map.from(this::getMaxBackoffInterval).as(Duration::toNanos).to(properties.in("maxBackoffIntervalNanos")); - map.from(this::getEnableBusyWait).to(properties.in("enableBusyWait")); - map.from(this::getMemoryLimit).as(DataSize::toBytes).to(properties.in("memoryLimitBytes")); - map.from(this::getProxyServiceUrl).to(properties.in("proxyServiceUrl")); - map.from(this::getProxyProtocol).to(properties.in("proxyProtocol")); - map.from(this::getEnableTransaction).to(properties.in("enableTransaction")); - map.from(this::getDnsLookupBindAddress).to(properties.in("dnsLookupBindAddress")); - map.from(this::getDnsLookupBindPort).to(properties.in("dnsLookupBindPort")); - map.from(this::getSocks5ProxyAddress).to(properties.in("socks5ProxyAddress")); - map.from(this::getSocks5ProxyUsername).to(properties.in("socks5ProxyUsername")); - map.from(this::getSocks5ProxyPassword).to(properties.in("socks5ProxyPassword")); - - return properties; - } - - } - - public static class Function { - - /** - * Whether to stop processing further function creates/updates when a failure - * occurs. - */ - private Boolean failFast = Boolean.TRUE; - - /** - * Whether to throw an exception if any failure is encountered during server - * startup while creating/updating functions. - */ - private Boolean propagateFailures = Boolean.TRUE; - - /** - * Whether to throw an exception if any failure is encountered during server - * shutdown while enforcing stop policy on functions. - */ - private Boolean propagateStopFailures = Boolean.FALSE; - - public Boolean getFailFast() { - return this.failFast; - } - - public void setFailFast(Boolean failFast) { - this.failFast = failFast; - } - - public Boolean getPropagateFailures() { - return this.propagateFailures; - } - - public void setPropagateFailures(Boolean propagateFailures) { - this.propagateFailures = propagateFailures; - } - - public Boolean getPropagateStopFailures() { - return this.propagateStopFailures; - } - - public void setPropagateStopFailures(Boolean propagateStopFailures) { - this.propagateStopFailures = propagateStopFailures; - } - - } - - public static class Listener { - - /** - * AckMode for acknowledgements. Allowed values are RECORD, BATCH, MANUAL. - */ - private AckMode ackMode; - - /** - * SchemaType of the consumed messages. - */ - private SchemaType schemaType; - - /** - * Max number of messages in a single batch request. - */ - private Integer maxNumMessages = -1; - - /** - * Max size in a single batch request. - */ - private DataSize maxNumBytes = DataSize.ofMegabytes(10); - - /** - * Duration to wait for enough message to fill a batch request before timing out. - */ - private Duration batchTimeout = Duration.ofMillis(100); - - /** - * Whether to record observations for receive operations when the Observations API - * is available. - */ - private Boolean observationsEnabled = true; - - public AckMode getAckMode() { - return this.ackMode; - } - - public void setAckMode(AckMode ackMode) { - this.ackMode = ackMode; - } - - public SchemaType getSchemaType() { - return this.schemaType; - } - - public void setSchemaType(SchemaType schemaType) { - this.schemaType = schemaType; - } - - public Integer getMaxNumMessages() { - return this.maxNumMessages; - } - - public void setMaxNumMessages(Integer maxNumMessages) { - this.maxNumMessages = maxNumMessages; - } - - public DataSize getMaxNumBytes() { - return this.maxNumBytes; - } - - public void setMaxNumBytes(DataSize maxNumBytes) { - this.maxNumBytes = maxNumBytes; - } - - public Duration getBatchTimeout() { - return this.batchTimeout; - } - - public void setBatchTimeout(Duration batchTimeout) { - this.batchTimeout = batchTimeout; - } - - public Boolean isObservationsEnabled() { - return this.observationsEnabled; - } - - public void setObservationsEnabled(Boolean observationsEnabled) { - this.observationsEnabled = observationsEnabled; - } - - } - - public static class Admin { - - /** - * Pulsar web URL for the admin endpoint in the format - * '(http|https)://<host>:<port>'. - */ - private String serviceUrl = "http://localhost:8080"; - - /** - * Fully qualified class name of the authentication plugin. - */ - private String authPluginClassName; - - /** - * Authentication parameter(s) as a JSON encoded string. - */ - private String authParams; - - /** - * Authentication parameter(s) as a map of parameter names to parameter values. - */ - private Map authentication; - - /** - * Path to the trusted TLS certificate file. - */ - private String tlsTrustCertsFilePath; - - /** - * Whether the client accepts untrusted TLS certificates from the broker. - */ - private Boolean tlsAllowInsecureConnection = false; - - /** - * Whether the hostname is validated when the proxy creates a TLS connection with - * brokers. - */ - private Boolean tlsHostnameVerificationEnable = false; - - /** - * Enable KeyStore instead of PEM type configuration if TLS is enabled. - */ - private Boolean useKeyStoreTls = false; - - /** - * Name of the security provider used for SSL connections. - */ - private String sslProvider; - - /** - * File format of the trust store file. - */ - private String tlsTrustStoreType; - - /** - * Location of the trust store file. - */ - private String tlsTrustStorePath; - - /** - * Store password for the key store file. - */ - private String tlsTrustStorePassword; - - /** - * List of cipher suites. This is a named combination of authentication, - * encryption, MAC and key exchange algorithm used to negotiate the security - * settings for a network connection using TLS or SSL network protocol. By - * default, all the available cipher suites are supported. - */ - private Set tlsCiphers; - - /** - * List of SSL protocols used to generate the SSLContext. Allowed values in recent - * JVMs are TLS, TLSv1.3, TLSv1.2 and TLSv1.1. - */ - private Set tlsProtocols; - - /** - * Duration to wait for a connection to server to be established. - */ - private Duration connectionTimeout = Duration.ofMinutes(1); - - /** - * Server response read time out for any request. - */ - private Duration readTimeout = Duration.ofMinutes(1); - - /** - * Server request time out for any request. - */ - private Duration requestTimeout = Duration.ofMinutes(5); - - /** - * Certificates auto refresh time if Pulsar admin uses tls authentication. - */ - private Duration autoCertRefreshTime = Duration.ofMinutes(5); - - public String getServiceUrl() { - return this.serviceUrl; - } - - public void setServiceUrl(String serviceUrl) { - this.serviceUrl = serviceUrl; - } - - public String getAuthPluginClassName() { - return this.authPluginClassName; - } - - public void setAuthPluginClassName(String authPluginClassName) { - this.authPluginClassName = authPluginClassName; - } - - public String getAuthParams() { - return this.authParams; - } - - public void setAuthParams(String authParams) { - this.authParams = authParams; - } - - public Map getAuthentication() { - return this.authentication; - } - - public void setAuthentication(Map authentication) { - this.authentication = authentication; - } - - public String getTlsTrustCertsFilePath() { - return this.tlsTrustCertsFilePath; - } - - public void setTlsTrustCertsFilePath(String tlsTrustCertsFilePath) { - this.tlsTrustCertsFilePath = tlsTrustCertsFilePath; - } - - public Boolean isTlsAllowInsecureConnection() { - return this.tlsAllowInsecureConnection; - } - - public void setTlsAllowInsecureConnection(Boolean tlsAllowInsecureConnection) { - this.tlsAllowInsecureConnection = tlsAllowInsecureConnection; - } - - public Boolean isTlsHostnameVerificationEnable() { - return this.tlsHostnameVerificationEnable; - } - - public void setTlsHostnameVerificationEnable(Boolean tlsHostnameVerificationEnable) { - this.tlsHostnameVerificationEnable = tlsHostnameVerificationEnable; - } - - public Boolean isUseKeyStoreTls() { - return this.useKeyStoreTls; - } - - public void setUseKeyStoreTls(Boolean useKeyStoreTls) { - this.useKeyStoreTls = useKeyStoreTls; - } - - public String getSslProvider() { - return this.sslProvider; - } - - public void setSslProvider(String sslProvider) { - this.sslProvider = sslProvider; - } - - public String getTlsTrustStoreType() { - return this.tlsTrustStoreType; - } - - public void setTlsTrustStoreType(String tlsTrustStoreType) { - this.tlsTrustStoreType = tlsTrustStoreType; - } - - public String getTlsTrustStorePath() { - return this.tlsTrustStorePath; - } - - public void setTlsTrustStorePath(String tlsTrustStorePath) { - this.tlsTrustStorePath = tlsTrustStorePath; - } - - public String getTlsTrustStorePassword() { - return this.tlsTrustStorePassword; - } - - public void setTlsTrustStorePassword(String tlsTrustStorePassword) { - this.tlsTrustStorePassword = tlsTrustStorePassword; - } - - public Set getTlsCiphers() { - return this.tlsCiphers; - } - - public void setTlsCiphers(Set tlsCiphers) { - this.tlsCiphers = tlsCiphers; - } - - public Set getTlsProtocols() { - return this.tlsProtocols; - } - - public void setTlsProtocols(Set tlsProtocols) { - this.tlsProtocols = tlsProtocols; - } - - public Duration getConnectionTimeout() { - return this.connectionTimeout; - } - - public void setConnectionTimeout(Duration connectionTimeout) { - this.connectionTimeout = connectionTimeout; - } - - public Duration getReadTimeout() { - return this.readTimeout; - } - - public void setReadTimeout(Duration readTimeout) { - this.readTimeout = readTimeout; - } - - public Duration getRequestTimeout() { - return this.requestTimeout; - } - - public void setRequestTimeout(Duration requestTimeout) { - this.requestTimeout = requestTimeout; - } - - public Duration getAutoCertRefreshTime() { - return this.autoCertRefreshTime; - } - - public void setAutoCertRefreshTime(Duration autoCertRefreshTime) { - this.autoCertRefreshTime = autoCertRefreshTime; - } - - public Map buildProperties() { - if (StringUtils.hasText(this.getAuthParams()) && !CollectionUtils.isEmpty(this.getAuthentication())) { - throw new IllegalArgumentException( - "Cannot set both spring.pulsar.administration.authParams and spring.pulsar.administration.authentication.*"); - } - PulsarProperties.Properties properties = new Properties(); - - PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); - map.from(this::getServiceUrl).to(properties.in("serviceUrl")); - map.from(this::getAuthPluginClassName).to(properties.in("authPluginClassName")); - map.from(this::getAuthParams).to(properties.in("authParams")); - map.from(this::getAuthentication).as(AuthParameterUtils::maybeConvertToEncodedParamString) - .to(properties.in("authParams")); - map.from(this::getTlsTrustCertsFilePath).to(properties.in("tlsTrustCertsFilePath")); - map.from(this::isTlsAllowInsecureConnection).to(properties.in("tlsAllowInsecureConnection")); - map.from(this::isTlsHostnameVerificationEnable).to(properties.in("tlsHostnameVerificationEnable")); - map.from(this::isUseKeyStoreTls).to(properties.in("useKeyStoreTls")); - map.from(this::getSslProvider).to(properties.in("sslProvider")); - map.from(this::getTlsTrustStoreType).to(properties.in("tlsTrustStoreType")); - map.from(this::getTlsTrustStorePath).to(properties.in("tlsTrustStorePath")); - map.from(this::getTlsTrustStorePassword).to(properties.in("tlsTrustStorePassword")); - map.from(this::getTlsCiphers).to(properties.in("tlsCiphers")); - map.from(this::getTlsProtocols).to(properties.in("tlsProtocols")); - map.from(this::getConnectionTimeout).asInt(Duration::toMillis).to(properties.in("connectionTimeoutMs")); - map.from(this::getReadTimeout).asInt(Duration::toMillis).to(properties.in("readTimeoutMs")); - map.from(this::getRequestTimeout).asInt(Duration::toMillis).to(properties.in("requestTimeoutMs")); - map.from(this::getAutoCertRefreshTime).asInt(Duration::toSeconds) - .to(properties.in("autoCertRefreshSeconds")); - - return properties; - } - - } - - public static class Reader { - - /** - * Topic names. - */ - private List topicNames; - - /** - * Size of a consumer's receiver queue. - */ - private Integer receiverQueueSize; - - /** - * Reader name. - */ - private String readerName; - - /** - * Subscription name. - */ - private String subscriptionName; - - /** - * Prefix of subscription role. - */ - private String subscriptionRolePrefix; - - /** - * Whether to read messages from a compacted topic rather than a full message - * backlog of a topic. - */ - private Boolean readCompacted; - - /** - * Whether the first message to be returned is the one specified by messageId. - */ - private Boolean resetIncludeHead; - - public List getTopicNames() { - return this.topicNames; - } - - public void setTopicNames(List topicNames) { - this.topicNames = topicNames; - } - - public Integer getReceiverQueueSize() { - return this.receiverQueueSize; - } - - public void setReceiverQueueSize(Integer receiverQueueSize) { - this.receiverQueueSize = receiverQueueSize; - } - - public String getReaderName() { - return this.readerName; - } - - public void setReaderName(String readerName) { - this.readerName = readerName; - } - - public String getSubscriptionName() { - return this.subscriptionName; - } - - public void setSubscriptionName(String subscriptionName) { - this.subscriptionName = subscriptionName; - } - - public String getSubscriptionRolePrefix() { - return this.subscriptionRolePrefix; - } - - public void setSubscriptionRolePrefix(String subscriptionRolePrefix) { - this.subscriptionRolePrefix = subscriptionRolePrefix; - } - - public Boolean getReadCompacted() { - return this.readCompacted; - } - - public void setReadCompacted(Boolean readCompacted) { - this.readCompacted = readCompacted; - } - - public Boolean getResetIncludeHead() { - return this.resetIncludeHead; - } - - public void setResetIncludeHead(Boolean resetIncludeHead) { - this.resetIncludeHead = resetIncludeHead; - } - - public Map buildProperties() { - - PulsarProperties.Properties properties = new Properties(); - - PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); - - map.from(this::getTopicNames).to(properties.in("topicNames")); - map.from(this::getReceiverQueueSize).to(properties.in("receiverQueueSize")); - map.from(this::getReaderName).to(properties.in("readerName")); - map.from(this::getSubscriptionName).to(properties.in("subscriptionName")); - map.from(this::getSubscriptionRolePrefix).to(properties.in("subscriptionRolePrefix")); - map.from(this::getReadCompacted).to(properties.in("readCompacted")); - map.from(this::getResetIncludeHead).to(properties.in("resetIncludeHead")); - - return properties; - } - - } - - public static class Defaults { - - /** - * List of mappings from message type to topic name and schema info to use as a - * defaults when a topic name and/or schema is not explicitly specified when - * producing or consuming messages of the mapped type. - */ - private List typeMappings = new ArrayList<>(); - - public List getTypeMappings() { - return this.typeMappings; - } - - public void setTypeMappings(List typeMappings) { - this.typeMappings = typeMappings; - } - - } - - /** - * A mapping from message type to topic and/or schema info to use (at least one of - * {@code topicName} or {@code schemaInfo} must be specified. - * @param messageType the message type - * @param topicName the topic name - * @param schemaInfo the schema info - */ - public record TypeMapping(Class messageType, @Nullable String topicName, @Nullable SchemaInfo schemaInfo) { - public TypeMapping { - Objects.requireNonNull(messageType, "messageType must not be null"); - if (topicName == null && schemaInfo == null) { - throw new IllegalArgumentException("At least one of topicName or schemaInfo must not be null"); - } - } - } - - /** - * Represents a schema - holds enough information to construct an actual schema - * instance. - * @param schemaType schema type - * @param messageKeyType message key type (required for key value type) - */ - public record SchemaInfo(SchemaType schemaType, @Nullable Class messageKeyType) { - public SchemaInfo { - Objects.requireNonNull(schemaType, "schemaType must not be null"); - if (schemaType == SchemaType.NONE) { - throw new IllegalArgumentException("schemaType NONE not supported"); - } - if (schemaType != SchemaType.KEY_VALUE && messageKeyType != null) { - throw new IllegalArgumentException("messageKeyType can only be set when schemaType is KEY_VALUE"); - } - } - } - - static class Properties extends HashMap { - - java.util.function.Consumer in(String key) { - return (value) -> put(key, value); - } - - } - -} diff --git a/spring-pulsar-spring-boot-autoconfigure/src/main/java/org/springframework/pulsar/autoconfigure/PulsarReactiveAnnotationDrivenConfiguration.java b/spring-pulsar-spring-boot-autoconfigure/src/main/java/org/springframework/pulsar/autoconfigure/PulsarReactiveAnnotationDrivenConfiguration.java deleted file mode 100644 index 015c2df2..00000000 --- a/spring-pulsar-spring-boot-autoconfigure/src/main/java/org/springframework/pulsar/autoconfigure/PulsarReactiveAnnotationDrivenConfiguration.java +++ /dev/null @@ -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> consumerFactoryProvider, - SchemaResolver schemaResolver, TopicResolver topicResolver) { - - ReactivePulsarContainerProperties 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 { - - } - -} diff --git a/spring-pulsar-spring-boot-autoconfigure/src/main/java/org/springframework/pulsar/autoconfigure/PulsarReactiveAutoConfiguration.java b/spring-pulsar-spring-boot-autoconfigure/src/main/java/org/springframework/pulsar/autoconfigure/PulsarReactiveAutoConfiguration.java deleted file mode 100644 index 73d1fca1..00000000 --- a/spring-pulsar-spring-boot-autoconfigure/src/main/java/org/springframework/pulsar/autoconfigure/PulsarReactiveAutoConfiguration.java +++ /dev/null @@ -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 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) { - return producerCacheProvider.stream().findFirst().map(AdaptedReactivePulsarClientFactory::createCache) - .orElseGet(AdaptedReactivePulsarClientFactory::createCache); - } - - @Bean - @ConditionalOnMissingBean - public ReactivePulsarSenderFactory reactivePulsarSenderFactory(ReactivePulsarClient pulsarReactivePulsarClient, - ObjectProvider 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); - } - -} diff --git a/spring-pulsar-spring-boot-autoconfigure/src/main/java/org/springframework/pulsar/autoconfigure/PulsarReactiveProperties.java b/spring-pulsar-spring-boot-autoconfigure/src/main/java/org/springframework/pulsar/autoconfigure/PulsarReactiveProperties.java deleted file mode 100644 index 50d3cbf7..00000000 --- a/spring-pulsar-spring-boot-autoconfigure/src/main/java/org/springframework/pulsar/autoconfigure/PulsarReactiveProperties.java +++ /dev/null @@ -1,1118 +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 java.util.HashMap; -import java.util.HashSet; -import java.util.List; -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.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.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.common.schema.SchemaType; -import org.apache.pulsar.reactive.client.api.ImmutableReactiveMessageConsumerSpec; -import org.apache.pulsar.reactive.client.api.ImmutableReactiveMessageReaderSpec; -import org.apache.pulsar.reactive.client.api.ImmutableReactiveMessageSenderSpec; -import org.apache.pulsar.reactive.client.api.MutableReactiveMessageConsumerSpec; -import org.apache.pulsar.reactive.client.api.MutableReactiveMessageReaderSpec; -import org.apache.pulsar.reactive.client.api.MutableReactiveMessageSenderSpec; -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.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; -import org.springframework.boot.context.properties.PropertyMapper; -import org.springframework.lang.Nullable; -import org.springframework.util.unit.DataSize; - -import reactor.core.scheduler.Schedulers; - -/** - * Configuration properties for Spring for the Apache Pulsar reactive client. - *

- * Users should refer to Pulsar reactive client documentation for complete descriptions of - * these properties. - * - * @author Christophe Bornet - */ -@ConfigurationProperties(prefix = "spring.pulsar.reactive") -public class PulsarReactiveProperties { - - private final Sender sender = new Sender(); - - private final Consumer consumer = new Consumer(); - - private final Reader reader = new Reader(); - - private final Listener listener = new Listener(); - - public Sender getSender() { - return this.sender; - } - - public Consumer getConsumer() { - return this.consumer; - } - - public Reader getReader() { - return this.reader; - } - - public Listener getListener() { - return this.listener; - } - - public ReactiveMessageSenderSpec buildReactiveMessageSenderSpec() { - return this.sender.buildReactiveMessageSenderSpec(); - } - - public ReactiveMessageReaderSpec buildReactiveMessageReaderSpec() { - return this.reader.buildReactiveMessageReaderSpec(); - } - - public ReactiveMessageConsumerSpec buildReactiveMessageConsumerSpec() { - return this.consumer.buildReactiveMessageConsumerSpec(); - } - - public static class Sender { - - /** - * 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); - - /** - * 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); - - private Integer roundRobinRouterBatchingPartitionSwitchFrequency; - - /** - * 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 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 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 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 getRoundRobinRouterBatchingPartitionSwitchFrequency() { - return this.roundRobinRouterBatchingPartitionSwitchFrequency; - } - - public void setRoundRobinRouterBatchingPartitionSwitchFrequency( - Integer roundRobinRouterBatchingPartitionSwitchFrequency) { - this.roundRobinRouterBatchingPartitionSwitchFrequency = roundRobinRouterBatchingPartitionSwitchFrequency; - } - - 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 getEncryptionKeys() { - return this.encryptionKeys; - } - - public void setEncryptionKeys(Set 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 getProperties() { - return this.properties; - } - - public void setProperties(Map properties) { - this.properties = properties; - } - - public Cache getCache() { - return this.cache; - } - - public ReactiveMessageSenderSpec buildReactiveMessageSenderSpec() { - PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); - - MutableReactiveMessageSenderSpec spec = new MutableReactiveMessageSenderSpec(); - - map.from(this::getTopicName).to(spec::setTopicName); - map.from(this::getProducerName).to(spec::setProducerName); - map.from(this::getSendTimeout).to(spec::setSendTimeout); - map.from(this::getMaxPendingMessages).to(spec::setMaxPendingMessages); - map.from(this::getMaxPendingMessagesAcrossPartitions).to(spec::setMaxPendingMessagesAcrossPartitions); - map.from(this::getMessageRoutingMode).to(spec::setMessageRoutingMode); - map.from(this::getHashingScheme).to(spec::setHashingScheme); - map.from(this::getCryptoFailureAction).to(spec::setCryptoFailureAction); - map.from(this::getBatchingMaxPublishDelay).to(spec::setBatchingMaxPublishDelay); - map.from(this::getRoundRobinRouterBatchingPartitionSwitchFrequency) - .to(spec::setRoundRobinRouterBatchingPartitionSwitchFrequency); - map.from(this::getBatchingMaxMessages).to(spec::setBatchingMaxMessages); - map.from(this::getBatchingMaxBytes).asInt(DataSize::toBytes).to(spec::setBatchingMaxBytes); - map.from(this::getBatchingEnabled).to(spec::setBatchingEnabled); - map.from(this::getChunkingEnabled).to(spec::setChunkingEnabled); - map.from(this::getEncryptionKeys).to(spec::setEncryptionKeys); - map.from(this::getCompressionType).to(spec::setCompressionType); - map.from(this::getInitialSequenceId).to(spec::setInitialSequenceId); - map.from(this::getAutoUpdatePartitions).to(spec::setAutoUpdatePartitions); - map.from(this::getAutoUpdatePartitionsInterval).to(spec::setAutoUpdatePartitionsInterval); - map.from(this::getMultiSchema).to(spec::setMultiSchema); - map.from(this::getProducerAccessMode).to(spec::setAccessMode); - map.from(this::getLazyStartPartitionedProducers).to(spec::setLazyStartPartitionedProducers); - map.from(this::getProperties).to(spec::setProperties); - - return new ImmutableReactiveMessageSenderSpec(spec); - } - - } - - public static class Reader { - - private String[] topicNames; - - private String readerName; - - private String subscriptionName; - - private String generatedSubscriptionNamePrefix; - - private Integer receiverQueueSize; - - private Boolean readCompacted; - - private Range[] keyHashRanges; - - private ConsumerCryptoFailureAction cryptoFailureAction; - - public String[] getTopicNames() { - return this.topicNames; - } - - public void setTopicNames(String[] topicNames) { - this.topicNames = topicNames; - } - - public String getReaderName() { - return this.readerName; - } - - public void setReaderName(String readerName) { - this.readerName = readerName; - } - - public String getSubscriptionName() { - return this.subscriptionName; - } - - public void setSubscriptionName(String subscriptionName) { - this.subscriptionName = subscriptionName; - } - - public String getGeneratedSubscriptionNamePrefix() { - return this.generatedSubscriptionNamePrefix; - } - - public void setGeneratedSubscriptionNamePrefix(String generatedSubscriptionNamePrefix) { - this.generatedSubscriptionNamePrefix = generatedSubscriptionNamePrefix; - } - - public Integer getReceiverQueueSize() { - return this.receiverQueueSize; - } - - public void setReceiverQueueSize(Integer receiverQueueSize) { - this.receiverQueueSize = receiverQueueSize; - } - - public Boolean getReadCompacted() { - return this.readCompacted; - } - - public void setReadCompacted(Boolean readCompacted) { - this.readCompacted = readCompacted; - } - - public Range[] getKeyHashRanges() { - return this.keyHashRanges; - } - - public void setKeyHashRanges(Range[] keyHashRanges) { - this.keyHashRanges = keyHashRanges; - } - - public ConsumerCryptoFailureAction getCryptoFailureAction() { - return this.cryptoFailureAction; - } - - public void setCryptoFailureAction(ConsumerCryptoFailureAction cryptoFailureAction) { - this.cryptoFailureAction = cryptoFailureAction; - } - - public ReactiveMessageReaderSpec buildReactiveMessageReaderSpec() { - PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); - - MutableReactiveMessageReaderSpec spec = new MutableReactiveMessageReaderSpec(); - - map.from(this::getTopicNames).as(List::of).to(spec::setTopicNames); - map.from(this::getReaderName).to(spec::setReaderName); - map.from(this::getSubscriptionName).to(spec::setSubscriptionName); - map.from(this::getGeneratedSubscriptionNamePrefix).to(spec::setGeneratedSubscriptionNamePrefix); - map.from(this::getReceiverQueueSize).to(spec::setReceiverQueueSize); - map.from(this::getReadCompacted).to(spec::setReadCompacted); - map.from(this::getKeyHashRanges).as(List::of).to(spec::setKeyHashRanges); - map.from(this::getCryptoFailureAction).to(spec::setCryptoFailureAction); - - return new ImmutableReactiveMessageReaderSpec(spec); - } - - } - - public static class Consumer { - - /** - * Comma-separated list of topics the consumer subscribes to. - */ - private 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 SortedMap subscriptionProperties = new TreeMap<>(); - - /** - * 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); - - /** - * When set to true, ignores the acknowledge operation completion and makes it - * asynchronous from the message consuming processing to improve performance by - * allowing the acknowledges and message processing to interleave. Defaults to - * true. - */ - private Boolean acknowledgeAsynchronously = true; - - /** - * Type of acknowledge scheduler. - */ - private SchedulerType acknowledgeSchedulerType; - - /** - * Delay before re-delivering messages that have failed to be processed. - */ - private Duration negativeAckRedeliveryDelay = Duration.ofMinutes(1); - - /** - * Configuration for the dead letter queue. - */ - @NestedConfigurationProperty - private DeadLetterPolicy deadLetterPolicy; - - /** - * Whether the retry letter topic is enabled. - */ - private Boolean retryLetterTopicEnable = false; - - /** - * 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 ackTimeoutTickTime = 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 properties = new TreeMap<>(); - - /** - * Whether to read messages from the compacted topic rather than the full message - * backlog. - */ - private Boolean readCompacted = false; - - /** - * Whether batch index acknowledgement is enabled. - */ - private Boolean batchIndexAckEnabled = false; - - /** - * Position where to initialize a newly created subscription. - */ - private SubscriptionInitialPosition subscriptionInitialPosition = SubscriptionInitialPosition.Latest; - - /** - * Auto-discovery period for topics when topic pattern is used. - */ - private Duration topicsPatternAutoDiscoveryPeriod = Duration.ofMinutes(1); - - /** - * Determines which topics the consumer should be subscribed to when using pattern - * subscriptions. - */ - private RegexSubscriptionMode topicsPatternSubscriptionMode = RegexSubscriptionMode.PersistentOnly; - - /** - * Whether the consumer auto-subscribes for partition increase. This is only for - * partitioned consumers. - */ - private Boolean autoUpdatePartitions = true; - - private Duration autoUpdatePartitionsInterval = Duration.ofMinutes(1); - - /** - * Whether to replicate subscription state. - */ - private Boolean replicateSubscriptionState = 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 in milliseconds. - */ - private Duration expireTimeOfIncompleteChunkedMessage = Duration.ofMinutes(1); - - public String[] getTopics() { - return this.topics; - } - - public void setTopics(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 SubscriptionType getSubscriptionType() { - return this.subscriptionType; - } - - public void setSubscriptionType(SubscriptionType subscriptionType) { - this.subscriptionType = subscriptionType; - } - - public SortedMap getSubscriptionProperties() { - return this.subscriptionProperties; - } - - public void setSubscriptionProperties(SortedMap subscriptionProperties) { - this.subscriptionProperties = subscriptionProperties; - } - - public SubscriptionMode getSubscriptionMode() { - return this.subscriptionMode; - } - - public void setSubscriptionMode(SubscriptionMode subscriptionMode) { - this.subscriptionMode = subscriptionMode; - } - - 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 Boolean getAcknowledgeAsynchronously() { - return this.acknowledgeAsynchronously; - } - - public void setAcknowledgeAsynchronously(Boolean acknowledgeAsynchronously) { - this.acknowledgeAsynchronously = acknowledgeAsynchronously; - } - - public SchedulerType getAcknowledgeSchedulerType() { - return this.acknowledgeSchedulerType; - } - - public void setAcknowledgeSchedulerType(SchedulerType acknowledgeSchedulerType) { - this.acknowledgeSchedulerType = acknowledgeSchedulerType; - } - - public Duration getNegativeAckRedeliveryDelay() { - return this.negativeAckRedeliveryDelay; - } - - public void setNegativeAckRedeliveryDelay(Duration negativeAckRedeliveryDelay) { - this.negativeAckRedeliveryDelay = negativeAckRedeliveryDelay; - } - - public DeadLetterPolicy getDeadLetterPolicy() { - return this.deadLetterPolicy; - } - - public void setDeadLetterPolicy(DeadLetterPolicy deadLetterPolicy) { - this.deadLetterPolicy = deadLetterPolicy; - } - - public Boolean getRetryLetterTopicEnable() { - return this.retryLetterTopicEnable; - } - - public void setRetryLetterTopicEnable(Boolean retryLetterTopicEnable) { - this.retryLetterTopicEnable = retryLetterTopicEnable; - } - - 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 getAckTimeoutTickTime() { - return this.ackTimeoutTickTime; - } - - public void setAckTimeoutTickTime(Duration ackTimeoutTickTime) { - this.ackTimeoutTickTime = ackTimeoutTickTime; - } - - 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 getProperties() { - return this.properties; - } - - public void setProperties(SortedMap properties) { - this.properties = properties; - } - - public Boolean getReadCompacted() { - return this.readCompacted; - } - - public void setReadCompacted(Boolean readCompacted) { - this.readCompacted = readCompacted; - } - - public Boolean getBatchIndexAckEnabled() { - return this.batchIndexAckEnabled; - } - - public void setBatchIndexAckEnabled(Boolean batchIndexAckEnabled) { - this.batchIndexAckEnabled = batchIndexAckEnabled; - } - - public SubscriptionInitialPosition getSubscriptionInitialPosition() { - return this.subscriptionInitialPosition; - } - - public void setSubscriptionInitialPosition(SubscriptionInitialPosition subscriptionInitialPosition) { - this.subscriptionInitialPosition = subscriptionInitialPosition; - } - - public Duration getTopicsPatternAutoDiscoveryPeriod() { - return this.topicsPatternAutoDiscoveryPeriod; - } - - public void setTopicsPatternAutoDiscoveryPeriod(Duration topicsPatternAutoDiscoveryPeriod) { - this.topicsPatternAutoDiscoveryPeriod = topicsPatternAutoDiscoveryPeriod; - } - - public RegexSubscriptionMode getTopicsPatternSubscriptionMode() { - return this.topicsPatternSubscriptionMode; - } - - public void setTopicsPatternSubscriptionMode(RegexSubscriptionMode topicsPatternSubscriptionMode) { - this.topicsPatternSubscriptionMode = topicsPatternSubscriptionMode; - } - - 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 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 ReactiveMessageConsumerSpec buildReactiveMessageConsumerSpec() { - - MutableReactiveMessageConsumerSpec spec = new MutableReactiveMessageConsumerSpec(); - - PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); - - map.from(this::getTopics).as(List::of).to(spec::setTopicNames); - map.from(this::getTopicsPattern).to(spec::setTopicsPattern); - map.from(this::getSubscriptionName).to(spec::setSubscriptionName); - map.from(this::getSubscriptionType).to(spec::setSubscriptionType); - map.from(this::getSubscriptionProperties).to(spec::setSubscriptionProperties); - map.from(this::getSubscriptionMode).to(spec::setSubscriptionMode); - map.from(this::getReceiverQueueSize).to(spec::setReceiverQueueSize); - map.from(this::getAcknowledgementsGroupTime).to(spec::setAcknowledgementsGroupTime); - map.from(this::getAcknowledgeAsynchronously).to(spec::setAcknowledgeAsynchronously); - map.from(this::getAcknowledgeSchedulerType).as((scheduler) -> switch (scheduler) { - case boundedElastic -> Schedulers.boundedElastic(); - case parallel -> Schedulers.parallel(); - case single -> Schedulers.single(); - case immediate -> Schedulers.immediate(); - }).to(spec::setAcknowledgeScheduler); - map.from(this::getNegativeAckRedeliveryDelay).to(spec::setNegativeAckRedeliveryDelay); - map.from(this::getDeadLetterPolicy).to(spec::setDeadLetterPolicy); - map.from(this::getRetryLetterTopicEnable).to(spec::setRetryLetterTopicEnable); - map.from(this::getMaxTotalReceiverQueueSizeAcrossPartitions) - .to(spec::setMaxTotalReceiverQueueSizeAcrossPartitions); - map.from(this::getConsumerName).to(spec::setConsumerName); - map.from(this::getAckTimeout).to(spec::setAckTimeout); - map.from(this::getAckTimeoutTickTime).to(spec::setAckTimeoutTickTime); - map.from(this::getPriorityLevel).to(spec::setPriorityLevel); - map.from(this::getCryptoFailureAction).to(spec::setCryptoFailureAction); - map.from(this::getProperties).to(spec::setProperties); - map.from(this::getReadCompacted).to(spec::setReadCompacted); - map.from(this::getBatchIndexAckEnabled).to(spec::setBatchIndexAckEnabled); - map.from(this::getSubscriptionInitialPosition).to(spec::setSubscriptionInitialPosition); - map.from(this::getTopicsPatternAutoDiscoveryPeriod).to(spec::setTopicsPatternAutoDiscoveryPeriod); - map.from(this::getTopicsPatternSubscriptionMode).to(spec::setTopicsPatternSubscriptionMode); - map.from(this::getAutoUpdatePartitions).to(spec::setAutoUpdatePartitions); - map.from(this::getAutoUpdatePartitionsInterval).to(spec::setAutoUpdatePartitionsInterval); - map.from(this::getReplicateSubscriptionState).to(spec::setReplicateSubscriptionState); - map.from(this::getAutoAckOldestChunkedMessageOnQueueFull) - .to(spec::setAutoAckOldestChunkedMessageOnQueueFull); - map.from(this::getMaxPendingChunkedMessage).to(spec::setMaxPendingChunkedMessage); - map.from(this::getExpireTimeOfIncompleteChunkedMessage).to(spec::setExpireTimeOfIncompleteChunkedMessage); - return new ImmutableReactiveMessageConsumerSpec(spec); - } - - } - - public enum SchedulerType { - - /** - * The reactor.core.scheduler.BoundedElasticScheduler. - */ - boundedElastic, - - /** - * The reactor.core.scheduler.ParallelScheduler. - */ - parallel, - - /** - * The reactor.core.scheduler.SingleScheduler. - */ - single, - - /** - * The reactor.core.scheduler.ImmediateScheduler. - */ - immediate - - } - - public static class Cache { - - /** Time period to expire unused entries in the cache. */ - private Duration expireAfterAccess = Duration.ofMinutes(1); - - /** Maximum size of cache (entries). */ - private Long maximumSize = 1000L; - - /** Initial size of cache. */ - private Integer initialCapacity = 50; - - public Duration getExpireAfterAccess() { - return this.expireAfterAccess; - } - - public void setExpireAfterAccess(Duration expireAfterAccess) { - this.expireAfterAccess = expireAfterAccess; - } - - public Long getMaximumSize() { - return this.maximumSize; - } - - public void setMaximumSize(Long maximumSize) { - this.maximumSize = maximumSize; - } - - public Integer getInitialCapacity() { - return this.initialCapacity; - } - - public void setInitialCapacity(Integer initialCapacity) { - this.initialCapacity = initialCapacity; - } - - } - - public static class Listener { - - /** - * SchemaType of the consumed messages. - */ - private SchemaType schemaType; - - /** - * Duration to wait before the message handling times out. - */ - private Duration handlingTimeout = Duration.ofMinutes(2); - - /** - * Whether per-key message ordering should be maintained when concurrent - * processing is used. - */ - private Boolean useKeyOrderedProcessing = false; - - public SchemaType getSchemaType() { - return this.schemaType; - } - - public void setSchemaType(SchemaType schemaType) { - this.schemaType = schemaType; - } - - public Duration getHandlingTimeout() { - return this.handlingTimeout; - } - - public void setHandlingTimeout(Duration handlingTimeout) { - this.handlingTimeout = handlingTimeout; - } - - public Boolean getUseKeyOrderedProcessing() { - return this.useKeyOrderedProcessing; - } - - public void setUseKeyOrderedProcessing(Boolean useKeyOrderedProcessing) { - this.useKeyOrderedProcessing = useKeyOrderedProcessing; - } - - } - -} diff --git a/spring-pulsar-spring-boot-autoconfigure/src/main/java/org/springframework/pulsar/autoconfigure/WellKnownAuthParameters.java b/spring-pulsar-spring-boot-autoconfigure/src/main/java/org/springframework/pulsar/autoconfigure/WellKnownAuthParameters.java deleted file mode 100644 index d8d409af..00000000 --- a/spring-pulsar-spring-boot-autoconfigure/src/main/java/org/springframework/pulsar/autoconfigure/WellKnownAuthParameters.java +++ /dev/null @@ -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 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); - } - -} diff --git a/spring-pulsar-spring-boot-autoconfigure/src/main/java/org/springframework/pulsar/autoconfigure/package-info.java b/spring-pulsar-spring-boot-autoconfigure/src/main/java/org/springframework/pulsar/autoconfigure/package-info.java deleted file mode 100644 index 89928137..00000000 --- a/spring-pulsar-spring-boot-autoconfigure/src/main/java/org/springframework/pulsar/autoconfigure/package-info.java +++ /dev/null @@ -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; diff --git a/spring-pulsar-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-pulsar-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json deleted file mode 100644 index 6fe034ef..00000000 --- a/spring-pulsar-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ /dev/null @@ -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": [] -} diff --git a/spring-pulsar-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-pulsar-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports deleted file mode 100644 index 93aa9162..00000000 --- a/spring-pulsar-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ /dev/null @@ -1,2 +0,0 @@ -org.springframework.pulsar.autoconfigure.PulsarAutoConfiguration -org.springframework.pulsar.autoconfigure.PulsarReactiveAutoConfiguration diff --git a/spring-pulsar-spring-boot-autoconfigure/src/test/java/org/springframework/pulsar/autoconfigure/AuthParameterUtilsTests.java b/spring-pulsar-spring-boot-autoconfigure/src/test/java/org/springframework/pulsar/autoconfigure/AuthParameterUtilsTests.java deleted file mode 100644 index d43865fc..00000000 --- a/spring-pulsar-spring-boot-autoconfigure/src/test/java/org/springframework/pulsar/autoconfigure/AuthParameterUtilsTests.java +++ /dev/null @@ -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 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 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"))); - } - -} diff --git a/spring-pulsar-spring-boot-autoconfigure/src/test/java/org/springframework/pulsar/autoconfigure/PulsarAutoConfigurationTests.java b/spring-pulsar-spring-boot-autoconfigure/src/test/java/org/springframework/pulsar/autoconfigure/PulsarAutoConfigurationTests.java deleted file mode 100644 index 1951e298..00000000 --- a/spring-pulsar-spring-boot-autoconfigure/src/test/java/org/springframework/pulsar/autoconfigure/PulsarAutoConfigurationTests.java +++ /dev/null @@ -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 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 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 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 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::getContainerProperties) - .hasFieldOrPropertyWithValue("schemaResolver", schemaResolver) - .hasFieldOrPropertyWithValue("topicResolver", topicResolver))); - } - - @Test - void customPulsarListenerAnnotationBeanPostProcessorIsRespected() { - PulsarListenerAnnotationBeanPostProcessor 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 properties = assertThat(context).hasNotFailed() - .getBean(ConcurrentPulsarListenerContainerFactory.class) - .extracting(ConcurrentPulsarListenerContainerFactory::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; - } - - } - -} diff --git a/spring-pulsar-spring-boot-autoconfigure/src/test/java/org/springframework/pulsar/autoconfigure/PulsarPropertiesTests.java b/spring-pulsar-spring-boot-autoconfigure/src/test/java/org/springframework/pulsar/autoconfigure/PulsarPropertiesTests.java deleted file mode 100644 index 61242166..00000000 --- a/spring-pulsar-spring-boot-autoconfigure/src/test/java/org/springframework/pulsar/autoconfigure/PulsarPropertiesTests.java +++ /dev/null @@ -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 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 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 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 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 clientProps = properties.buildClientProperties(); - - assertThat(clientProps).containsEntry("authPluginClassName", authPluginClassName) - .containsEntry("authParams", authParamsStr); - } - - @Test - void authenticationUsingAuthenticationMap() { - Map 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 clientProps = properties.buildClientProperties(); - assertThat(clientProps).containsEntry("authPluginClassName", authPluginClassName) - .containsEntry("authParams", authParamsStr); - } - - @Test - void authenticationNotAllowedUsingBothAuthParamsStringAndAuthenticationMap() { - Map 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 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 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 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 adminProps = properties.buildAdminProperties(); - assertThat(adminProps).containsEntry("authPluginClassName", authPluginClassName).containsEntry("authParams", - authParamsStr); - } - - @Test - void authenticationUsingAuthenticationMap() { - Map 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 adminProps = properties.buildAdminProperties(); - assertThat(adminProps).containsEntry("authPluginClassName", authPluginClassName).containsEntry("authParams", - authParamsStr); - } - - @Test - void authenticationNotAllowedUsingBothAuthParamsStringAndAuthenticationMap() { - Map 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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); - } - - } - -} diff --git a/spring-pulsar-spring-boot-autoconfigure/src/test/java/org/springframework/pulsar/autoconfigure/PulsarReactiveAutoConfigurationTests.java b/spring-pulsar-spring-boot-autoconfigure/src/test/java/org/springframework/pulsar/autoconfigure/PulsarReactiveAutoConfigurationTests.java deleted file mode 100644 index d1416a6c..00000000 --- a/spring-pulsar-spring-boot-autoconfigure/src/test/java/org/springframework/pulsar/autoconfigure/PulsarReactiveAutoConfigurationTests.java +++ /dev/null @@ -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 }) - void customBeanIsRespected(Class 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, 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 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 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, 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, 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, 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, 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> properties = assertThat(context) - .hasNotFailed().getBean(DefaultReactivePulsarListenerContainerFactory.class) - .extracting(DefaultReactivePulsarListenerContainerFactory::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 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 assertCaffeineProducerCacheProvider( - AssertableApplicationContext context) { - return assertThat(context).hasNotFailed().hasSingleBean(ProducerCacheProvider.class) - .hasSingleBean(ReactiveMessageSenderCache.class).getBean(ProducerCacheProvider.class) - .isExactlyInstanceOf(CaffeineProducerCacheProvider.class); - } - - } - -} diff --git a/spring-pulsar-spring-boot-autoconfigure/src/test/java/org/springframework/pulsar/autoconfigure/PulsarReactivePropertiesTests.java b/spring-pulsar-spring-boot-autoconfigure/src/test/java/org/springframework/pulsar/autoconfigure/PulsarReactivePropertiesTests.java deleted file mode 100644 index aa4838fe..00000000 --- a/spring-pulsar-spring-boot-autoconfigure/src/test/java/org/springframework/pulsar/autoconfigure/PulsarReactivePropertiesTests.java +++ /dev/null @@ -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 map) { - ConfigurationPropertySource source = new MapConfigurationPropertySource(map); - new Binder(source).bind("spring.pulsar.reactive", Bindable.ofInstance(this.properties)); - } - - @Nested - class SenderPropertiesTests { - - @Test - void senderPropsToSenderSpec() { - Map 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 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 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); - } - - } - -} diff --git a/spring-pulsar-spring-boot-autoconfigure/src/test/java/org/springframework/pulsar/autoconfigure/SpringPulsarBootAppSanityTests.java b/spring-pulsar-spring-boot-autoconfigure/src/test/java/org/springframework/pulsar/autoconfigure/SpringPulsarBootAppSanityTests.java deleted file mode 100644 index 3e202e6d..00000000 --- a/spring-pulsar-spring-boot-autoconfigure/src/test/java/org/springframework/pulsar/autoconfigure/SpringPulsarBootAppSanityTests.java +++ /dev/null @@ -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) { - 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> pulsarTemplateProvider; - - @RestController - class TestWebController { - - @GetMapping("/hello") - String sayHello() throws PulsarClientException { - - PulsarTemplate pulsarTemplate = pulsarTemplateProvider.getIfAvailable(); - if (pulsarTemplate == null) { - return "NOPE! Not hello world"; - } - MessageId msgId = pulsarTemplate.send("spbast-hello-topic", "hello"); - return "Hello World -> " + msgId; - } - - } - - } - -} diff --git a/spring-pulsar-spring-boot-autoconfigure/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/spring-pulsar-spring-boot-autoconfigure/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker deleted file mode 100644 index 1f0955d4..00000000 --- a/spring-pulsar-spring-boot-autoconfigure/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker +++ /dev/null @@ -1 +0,0 @@ -mock-maker-inline diff --git a/spring-pulsar-spring-boot-starter/build.gradle b/spring-pulsar-spring-boot-starter/build.gradle deleted file mode 100644 index 422c14de..00000000 --- a/spring-pulsar-spring-boot-starter/build.gradle +++ /dev/null @@ -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' -} diff --git a/spring-pulsar-spring-cloud-stream-binder/build.gradle b/spring-pulsar-spring-cloud-stream-binder/build.gradle deleted file mode 100644 index 3e05cdd7..00000000 --- a/spring-pulsar-spring-cloud-stream-binder/build.gradle +++ /dev/null @@ -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 -} - diff --git a/spring-pulsar-spring-cloud-stream-binder/src/main/java/org/springframework/pulsar/spring/cloud/stream/binder/PulsarBinderHeaderMapper.java b/spring-pulsar-spring-cloud-stream-binder/src/main/java/org/springframework/pulsar/spring/cloud/stream/binder/PulsarBinderHeaderMapper.java deleted file mode 100644 index ddcf426f..00000000 --- a/spring-pulsar-spring-cloud-stream-binder/src/main/java/org/springframework/pulsar/spring/cloud/stream/binder/PulsarBinderHeaderMapper.java +++ /dev/null @@ -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 toPulsarHeaders(MessageHeaders springHeaders) { - Map 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; - } - -} diff --git a/spring-pulsar-spring-cloud-stream-binder/src/main/java/org/springframework/pulsar/spring/cloud/stream/binder/PulsarBinderUtils.java b/spring-pulsar-spring-cloud-stream-binder/src/main/java/org/springframework/pulsar/spring/cloud/stream/binder/PulsarBinderUtils.java deleted file mode 100644 index cef8b8c1..00000000 --- a/spring-pulsar-spring-cloud-stream-binder/src/main/java/org/springframework/pulsar/spring/cloud/stream/binder/PulsarBinderUtils.java +++ /dev/null @@ -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). - *

- * NOTE: 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 mergePropertiesWithPrecedence(Map baseProps, - Map binderProps, Map bindingProps) { - Objects.requireNonNull(baseProps, "baseProps must be specified"); - Objects.requireNonNull(binderProps, "binderProps must be specified"); - Objects.requireNonNull(bindingProps, "bindingProps must be specified"); - - Map newOrModifiedBinderProps = extractNewOrModifiedProperties(binderProps, baseProps); - LOGGER.trace(() -> "New or modified binder props: %s".formatted(newOrModifiedBinderProps)); - - Map newOrModifiedBindingProps = extractNewOrModifiedProperties(bindingProps, baseProps); - LOGGER.trace(() -> "New or modified binding props: %s".formatted(newOrModifiedBindingProps)); - - Map mergedProps = new HashMap<>(newOrModifiedBinderProps); - mergedProps.putAll(newOrModifiedBindingProps); - LOGGER.trace(() -> "Final merged props: %s".formatted(mergedProps)); - - return mergedProps; - } - - private static Map extractNewOrModifiedProperties(Map candidateProps, - Map baseProps) { - Map newOrModifiedProps = new HashMap<>(); - candidateProps.forEach((propName, propValue) -> { - if (!baseProps.containsKey(propName) || (!Objects.equals(propValue, baseProps.get(propName)))) { - newOrModifiedProps.put(propName, propValue); - } - }); - return newOrModifiedProps; - } - -} diff --git a/spring-pulsar-spring-cloud-stream-binder/src/main/java/org/springframework/pulsar/spring/cloud/stream/binder/PulsarMessageChannelBinder.java b/spring-pulsar-spring-cloud-stream-binder/src/main/java/org/springframework/pulsar/spring/cloud/stream/binder/PulsarMessageChannelBinder.java deleted file mode 100644 index 288c733f..00000000 --- a/spring-pulsar-spring-cloud-stream-binder/src/main/java/org/springframework/pulsar/spring/cloud/stream/binder/PulsarMessageChannelBinder.java +++ /dev/null @@ -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, ExtendedProducerProperties, PulsarTopicProvisioner> - implements ExtendedPropertiesBinder { - - private final PulsarTemplate 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 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 producerProperties, MessageChannel errorChannel) { - final Schema 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 extProducerProps) { - if (HeaderMode.none.equals(extProducerProps.getHeaderMode())) { - return null; - } - return new PulsarBinderHeaderMapper(this.headerMapper); - } - - @Override - protected MessageProducer createConsumerEndpoint(ConsumerDestination destination, String group, - ExtendedConsumerProperties 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 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 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 pulsarTemplate; - - private final Schema schema; - - private final String destination; - - private final ProducerBuilderCustomizer layeredProducerPropsCustomizer; - - private final PulsarHeaderMapper headerMapper; - - private boolean running = true; - - PulsarProducerConfigurationMessageHandler(PulsarTemplate pulsarTemplate, Schema schema, - String destination, ProducerBuilderCustomizer 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 applySpringHeadersAsPulsarProperties(MessageHeaders headers) { - return (mb) -> { - if (this.headerMapper != null) { - this.headerMapper.toPulsarHeaders(headers).forEach(mb::property); - } - }; - } - - } - -} diff --git a/spring-pulsar-spring-cloud-stream-binder/src/main/java/org/springframework/pulsar/spring/cloud/stream/binder/config/PulsarBinderConfiguration.java b/spring-pulsar-spring-cloud-stream-binder/src/main/java/org/springframework/pulsar/spring/cloud/stream/binder/config/PulsarBinderConfiguration.java deleted file mode 100644 index 1845e871..00000000 --- a/spring-pulsar-spring-cloud-stream-binder/src/main/java/org/springframework/pulsar/spring/cloud/stream/binder/config/PulsarBinderConfiguration.java +++ /dev/null @@ -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 pulsarTemplate, PulsarConsumerFactory pulsarConsumerFactory, - PulsarBinderConfigurationProperties binderConfigProps, PulsarExtendedBindingProperties bindingConfigProps, - SchemaResolver schemaResolver, PulsarHeaderMapper headerMapper) { - PulsarMessageChannelBinder pulsarMessageChannelBinder = new PulsarMessageChannelBinder(pulsarTopicProvisioner, - pulsarTemplate, pulsarConsumerFactory, binderConfigProps, schemaResolver, headerMapper); - pulsarMessageChannelBinder.setExtendedBindingProperties(bindingConfigProps); - return pulsarMessageChannelBinder; - } - -} diff --git a/spring-pulsar-spring-cloud-stream-binder/src/main/java/org/springframework/pulsar/spring/cloud/stream/binder/package-info.java b/spring-pulsar-spring-cloud-stream-binder/src/main/java/org/springframework/pulsar/spring/cloud/stream/binder/package-info.java deleted file mode 100644 index 0b0aa4e1..00000000 --- a/spring-pulsar-spring-cloud-stream-binder/src/main/java/org/springframework/pulsar/spring/cloud/stream/binder/package-info.java +++ /dev/null @@ -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; diff --git a/spring-pulsar-spring-cloud-stream-binder/src/main/java/org/springframework/pulsar/spring/cloud/stream/binder/properties/PulsarBinderConfigurationProperties.java b/spring-pulsar-spring-cloud-stream-binder/src/main/java/org/springframework/pulsar/spring/cloud/stream/binder/properties/PulsarBinderConfigurationProperties.java deleted file mode 100644 index b7b5f53e..00000000 --- a/spring-pulsar-spring-cloud-stream-binder/src/main/java/org/springframework/pulsar/spring/cloud/stream/binder/properties/PulsarBinderConfigurationProperties.java +++ /dev/null @@ -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. - *

- * 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; - } - -} diff --git a/spring-pulsar-spring-cloud-stream-binder/src/main/java/org/springframework/pulsar/spring/cloud/stream/binder/properties/PulsarBindingProperties.java b/spring-pulsar-spring-cloud-stream-binder/src/main/java/org/springframework/pulsar/spring/cloud/stream/binder/properties/PulsarBindingProperties.java deleted file mode 100644 index d4e6e224..00000000 --- a/spring-pulsar-spring-cloud-stream-binder/src/main/java/org/springframework/pulsar/spring/cloud/stream/binder/properties/PulsarBindingProperties.java +++ /dev/null @@ -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. - *

- * These properties are applied to individual bindings and will override any binder-level - * setting. - * - *

- * NOTE: 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; - } - -} diff --git a/spring-pulsar-spring-cloud-stream-binder/src/main/java/org/springframework/pulsar/spring/cloud/stream/binder/properties/PulsarConsumerProperties.java b/spring-pulsar-spring-cloud-stream-binder/src/main/java/org/springframework/pulsar/spring/cloud/stream/binder/properties/PulsarConsumerProperties.java deleted file mode 100644 index d360c31c..00000000 --- a/spring-pulsar-spring-cloud-stream-binder/src/main/java/org/springframework/pulsar/spring/cloud/stream/binder/properties/PulsarConsumerProperties.java +++ /dev/null @@ -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; - } - -} diff --git a/spring-pulsar-spring-cloud-stream-binder/src/main/java/org/springframework/pulsar/spring/cloud/stream/binder/properties/PulsarExtendedBindingProperties.java b/spring-pulsar-spring-cloud-stream-binder/src/main/java/org/springframework/pulsar/spring/cloud/stream/binder/properties/PulsarExtendedBindingProperties.java deleted file mode 100644 index 64efe70a..00000000 --- a/spring-pulsar-spring-cloud-stream-binder/src/main/java/org/springframework/pulsar/spring/cloud/stream/binder/properties/PulsarExtendedBindingProperties.java +++ /dev/null @@ -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. - *

- * 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 { - - 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 getBindings() { - return this.doGetBindings(); - } - - @Override - public Class getExtendedPropertiesEntryClass() { - return PulsarBindingProperties.class; - } - -} diff --git a/spring-pulsar-spring-cloud-stream-binder/src/main/java/org/springframework/pulsar/spring/cloud/stream/binder/properties/PulsarProducerProperties.java b/spring-pulsar-spring-cloud-stream-binder/src/main/java/org/springframework/pulsar/spring/cloud/stream/binder/properties/PulsarProducerProperties.java deleted file mode 100644 index d5537d7a..00000000 --- a/spring-pulsar-spring-cloud-stream-binder/src/main/java/org/springframework/pulsar/spring/cloud/stream/binder/properties/PulsarProducerProperties.java +++ /dev/null @@ -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; - } - -} diff --git a/spring-pulsar-spring-cloud-stream-binder/src/main/java/org/springframework/pulsar/spring/cloud/stream/binder/properties/package-info.java b/spring-pulsar-spring-cloud-stream-binder/src/main/java/org/springframework/pulsar/spring/cloud/stream/binder/properties/package-info.java deleted file mode 100644 index 7d964d12..00000000 --- a/spring-pulsar-spring-cloud-stream-binder/src/main/java/org/springframework/pulsar/spring/cloud/stream/binder/properties/package-info.java +++ /dev/null @@ -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; diff --git a/spring-pulsar-spring-cloud-stream-binder/src/main/java/org/springframework/pulsar/spring/cloud/stream/binder/provisioning/PulsarTopicProvisioner.java b/spring-pulsar-spring-cloud-stream-binder/src/main/java/org/springframework/pulsar/spring/cloud/stream/binder/provisioning/PulsarTopicProvisioner.java deleted file mode 100644 index b4fee3d7..00000000 --- a/spring-pulsar-spring-cloud-stream-binder/src/main/java/org/springframework/pulsar/spring/cloud/stream/binder/provisioning/PulsarTopicProvisioner.java +++ /dev/null @@ -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, ExtendedProducerProperties> { - - 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) - 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) - 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; - } - } - -} diff --git a/spring-pulsar-spring-cloud-stream-binder/src/main/java/org/springframework/pulsar/spring/cloud/stream/binder/provisioning/package-info.java b/spring-pulsar-spring-cloud-stream-binder/src/main/java/org/springframework/pulsar/spring/cloud/stream/binder/provisioning/package-info.java deleted file mode 100644 index 1ecfd7b5..00000000 --- a/spring-pulsar-spring-cloud-stream-binder/src/main/java/org/springframework/pulsar/spring/cloud/stream/binder/provisioning/package-info.java +++ /dev/null @@ -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; diff --git a/spring-pulsar-spring-cloud-stream-binder/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-pulsar-spring-cloud-stream-binder/src/main/resources/META-INF/additional-spring-configuration-metadata.json deleted file mode 100644 index 13cd48dc..00000000 --- a/spring-pulsar-spring-cloud-stream-binder/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ /dev/null @@ -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": [] -} diff --git a/spring-pulsar-spring-cloud-stream-binder/src/main/resources/META-INF/spring.binders b/spring-pulsar-spring-cloud-stream-binder/src/main/resources/META-INF/spring.binders deleted file mode 100644 index 27b3b29b..00000000 --- a/spring-pulsar-spring-cloud-stream-binder/src/main/resources/META-INF/spring.binders +++ /dev/null @@ -1,2 +0,0 @@ -pulsar:\ -org.springframework.pulsar.spring.cloud.stream.binder.config.PulsarBinderConfiguration diff --git a/spring-pulsar-spring-cloud-stream-binder/src/test/java/org/springframework/pulsar/spring/cloud/stream/binder/AbstractPulsarTestBinder.java b/spring-pulsar-spring-cloud-stream-binder/src/test/java/org/springframework/pulsar/spring/cloud/stream/binder/AbstractPulsarTestBinder.java deleted file mode 100644 index 47f3ba19..00000000 --- a/spring-pulsar-spring-cloud-stream-binder/src/test/java/org/springframework/pulsar/spring/cloud/stream/binder/AbstractPulsarTestBinder.java +++ /dev/null @@ -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, ExtendedProducerProperties> { - - private ApplicationContext applicationContext; - - @Override - public void cleanup() { - } - - protected final void setApplicationContext(ApplicationContext context) { - this.applicationContext = context; - } - - public ApplicationContext getApplicationContext() { - return this.applicationContext; - } - -} diff --git a/spring-pulsar-spring-cloud-stream-binder/src/test/java/org/springframework/pulsar/spring/cloud/stream/binder/PulsarBinderConfigurationPropertiesTests.java b/spring-pulsar-spring-cloud-stream-binder/src/test/java/org/springframework/pulsar/spring/cloud/stream/binder/PulsarBinderConfigurationPropertiesTests.java deleted file mode 100644 index a51750d4..00000000 --- a/spring-pulsar-spring-cloud-stream-binder/src/test/java/org/springframework/pulsar/spring/cloud/stream/binder/PulsarBinderConfigurationPropertiesTests.java +++ /dev/null @@ -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 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 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 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 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 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 - } - -} diff --git a/spring-pulsar-spring-cloud-stream-binder/src/test/java/org/springframework/pulsar/spring/cloud/stream/binder/PulsarBinderHeaderMapperTests.java b/spring-pulsar-spring-cloud-stream-binder/src/test/java/org/springframework/pulsar/spring/cloud/stream/binder/PulsarBinderHeaderMapperTests.java deleted file mode 100644 index c05ea2b4..00000000 --- a/spring-pulsar-spring-cloud-stream-binder/src/test/java/org/springframework/pulsar/spring/cloud/stream/binder/PulsarBinderHeaderMapperTests.java +++ /dev/null @@ -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(); - 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(); - 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); - - } - - } - -} diff --git a/spring-pulsar-spring-cloud-stream-binder/src/test/java/org/springframework/pulsar/spring/cloud/stream/binder/PulsarBinderIntegrationTests.java b/spring-pulsar-spring-cloud-stream-binder/src/test/java/org/springframework/pulsar/spring/cloud/stream/binder/PulsarBinderIntegrationTests.java deleted file mode 100644 index a4242985..00000000 --- a/spring-pulsar-spring-cloud-stream-binder/src/test/java/org/springframework/pulsar/spring/cloud/stream/binder/PulsarBinderIntegrationTests.java +++ /dev/null @@ -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> springMessageSupplier() { - return () -> { - msgCount++; - return MessageBuilder.withPayload("test-headers-msg-" + msgCount) - .setHeader("custom-id", "5150-" + msgCount).build(); - }; - } - - @Bean - public Consumer> 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> springMessageSupplier() { - return () -> { - msgCount++; - return MessageBuilder.withPayload("test-headers-msg-" + msgCount) - .setHeader("custom-id", new FooHeader("5150-" + msgCount)).build(); - }; - } - - @Bean - public Consumer> 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 toPulsarHeaders(MessageHeaders springHeaders) { - Map 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> 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> 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 textSupplier() { - return () -> "test-basic-scenario"; - } - - @Bean - public Consumer 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 { - - List> producersCreated = new ArrayList<>(); - - TrackingProducerFactory(PulsarClient pulsarClient, Map config, TopicResolver topicResolver) { - super(pulsarClient, config, topicResolver); - } - - @Override - protected Producer doCreateProducer(Schema schema, @Nullable String topic, - @Nullable Collection encryptionKeys, - @Nullable List> producerBuilderCustomizers) - throws PulsarClientException { - Producer producer = super.doCreateProducer(schema, topic, encryptionKeys, - producerBuilderCustomizers); - producersCreated.add(producer); - return producer; - } - - } - - static class TrackingConsumerFactory extends DefaultPulsarConsumerFactory { - - List> consumersCreated = new ArrayList<>(); - - TrackingConsumerFactory(PulsarClient pulsarClient, Map consumerConfig) { - super(pulsarClient, consumerConfig); - } - - @Override - public org.apache.pulsar.client.api.Consumer createConsumer(Schema schema, - @Nullable Collection topics, @Nullable String subscriptionName, - @Nullable Map metadataProperties, - @Nullable List> consumerBuilderCustomizers) - throws PulsarClientException { - org.apache.pulsar.client.api.Consumer 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 piSupplier() { - return () -> 3.14f; - } - - @Bean - public Consumer piLogger() { - return f -> this.logger.info("Hello binder: " + f); - } - - } - - @EnableAutoConfiguration - @SpringBootConfiguration - static class JsonFooConfig { - - private final Logger logger = LoggerFactory.getLogger(getClass()); - - @Bean - public Supplier fooSupplier() { - return () -> new Foo("5150"); - } - - @Bean - public Consumer fooLogger() { - return f -> this.logger.info("Hello binder: " + f); - } - - } - - @EnableAutoConfiguration - @SpringBootConfiguration - @Import(JsonFooConfig.class) - static class JsonFooWithCustomMappingConfig { - - @Bean - public SchemaResolverCustomizer 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 userSupplier() { - return () -> new User("user21", 21); - } - - @Bean - public Consumer userLogger() { - return f -> this.logger.info("Hello binder: " + f); - } - - } - - @EnableAutoConfiguration - @SpringBootConfiguration - @Import(AvroUserConfig.class) - static class AvroUserConfigCustomMappings { - - @Bean - public SchemaResolverCustomizer 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> userSupplier() { - return () -> new KeyValue<>("21", new User("user21", 21)); - } - - @Bean - public Consumer> userLogger() { - return f -> this.logger.info("Hello binder: " + f.getKey() + "->" + f.getValue()); - } - - } - - @EnableAutoConfiguration - @SpringBootConfiguration - @Import(KeyValueAvroUserConfig.class) - static class KeyValueAvroUserConfigCustomMappings { - - @Bean - public SchemaResolverCustomizer 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> fooSupplier() { - return () -> new KeyValue<>("5150", new Foo("5150")); - } - - @Bean - public Consumer> 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 + '}'; - } - - } - -} diff --git a/spring-pulsar-spring-cloud-stream-binder/src/test/java/org/springframework/pulsar/spring/cloud/stream/binder/PulsarBinderTests.java b/spring-pulsar-spring-cloud-stream-binder/src/test/java/org/springframework/pulsar/spring/cloud/stream/binder/PulsarBinderTests.java deleted file mode 100644 index 630ddfb2..00000000 --- a/spring-pulsar-spring-cloud-stream-binder/src/test/java/org/springframework/pulsar/spring/cloud/stream/binder/PulsarBinderTests.java +++ /dev/null @@ -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, ExtendedProducerProperties> - 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.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 createConsumerProperties() { - final ExtendedConsumerProperties pulsarConsumerProperties = new ExtendedConsumerProperties<>( - new PulsarConsumerProperties()); - return pulsarConsumerProperties; - } - - @Override - public Spy spyOn(String name) { - return null; - } - - private ExtendedProducerProperties createProducerProperties() { - return this.createProducerProperties(null); - } - - @Override - protected ExtendedProducerProperties 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 consumerProperties = createConsumerProperties(); - DirectChannel moduleInputChannel = createBindableChannel("input", - createConsumerBindingProperties(consumerProperties)); - - Binding producerBinding = binder.bindProducer("foo.bar", moduleOutputChannel, - outputBindingProperties.getProducer()); - Binding 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> inboundMessageRef = new AtomicReference<>(); - moduleInputChannel.subscribe(message1 -> { - try { - inboundMessageRef.set((Message) 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 producerBinding = binder.bindProducer( - String.format("defaultGroup%s0", getDestinationNameDelimiter()), output, - producerBindingProperties.getProducer()); - - QueueChannel input1 = new QueueChannel(); - Binding binding1 = binder.bindConsumer( - String.format("defaultGroup%s0", getDestinationNameDelimiter()), null, input1, - createConsumerProperties()); - - QueueChannel input2 = new QueueChannel(); - Binding 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 receivedMessage1 = (Message) receive(input1); - assertThat(receivedMessage1).isNotNull(); - assertThat(new String(receivedMessage1.getPayload())).isEqualTo(testPayload1); - - Message receivedMessage2 = (Message) 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) receive(input1); - assertThat(receivedMessage1).isNotNull(); - assertThat(new String(receivedMessage1.getPayload())).isEqualTo(testPayload2); - receivedMessage1 = (Message) receive(input1); - assertThat(receivedMessage1).isNotNull(); - assertThat(new String(receivedMessage1.getPayload())).isEqualTo(testPayload3); - - receivedMessage2 = (Message) 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. - } - -} diff --git a/spring-pulsar-spring-cloud-stream-binder/src/test/java/org/springframework/pulsar/spring/cloud/stream/binder/PulsarBinderUtilsTests.java b/spring-pulsar-spring-cloud-stream-binder/src/test/java/org/springframework/pulsar/spring/cloud/stream/binder/PulsarBinderUtilsTests.java deleted file mode 100644 index 8155a3be..00000000 --- a/spring-pulsar-spring-cloud-stream-binder/src/test/java/org/springframework/pulsar/spring/cloud/stream/binder/PulsarBinderUtilsTests.java +++ /dev/null @@ -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 baseProps, Map binderProps, - Map bindingProps, Map expectedMergedProps) { - assertThat(PulsarBinderUtils.mergePropertiesWithPrecedence(baseProps, binderProps, bindingProps)) - .containsExactlyInAnyOrderEntriesOf(expectedMergedProps); - } - - // @formatter:off - static Stream 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 - - } - -} diff --git a/spring-pulsar-spring-cloud-stream-binder/src/test/java/org/springframework/pulsar/spring/cloud/stream/binder/PulsarExtendedBindingPropertiesTests.java b/spring-pulsar-spring-cloud-stream-binder/src/test/java/org/springframework/pulsar/spring/cloud/stream/binder/PulsarExtendedBindingPropertiesTests.java deleted file mode 100644 index 3a2e56a5..00000000 --- a/spring-pulsar-spring-cloud-stream-binder/src/test/java/org/springframework/pulsar/spring/cloud/stream/binder/PulsarExtendedBindingPropertiesTests.java +++ /dev/null @@ -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 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 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 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 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 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 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); - } - -} diff --git a/spring-pulsar-spring-cloud-stream-binder/src/test/java/org/springframework/pulsar/spring/cloud/stream/binder/PulsarTestBinder.java b/spring-pulsar-spring-cloud-stream-binder/src/test/java/org/springframework/pulsar/spring/cloud/stream/binder/PulsarTestBinder.java deleted file mode 100644 index 7b01b20f..00000000 --- a/spring-pulsar-spring-cloud-stream-binder/src/test/java/org/springframework/pulsar/spring/cloud/stream/binder/PulsarTestBinder.java +++ /dev/null @@ -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) 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 { - - } - -} diff --git a/spring-pulsar-spring-cloud-stream-binder/src/test/java/org/springframework/pulsar/spring/cloud/stream/binder/PulsarTopicProvisionerTests.java b/spring-pulsar-spring-cloud-stream-binder/src/test/java/org/springframework/pulsar/spring/cloud/stream/binder/PulsarTopicProvisionerTests.java deleted file mode 100644 index 3ab8b5a9..00000000 --- a/spring-pulsar-spring-cloud-stream-binder/src/test/java/org/springframework/pulsar/spring/cloud/stream/binder/PulsarTopicProvisionerTests.java +++ /dev/null @@ -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 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 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 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 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 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 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 properties = new ExtendedConsumerProperties<>( - pulsarConsumerProperties); - ConsumerDestination consumerDestination = pulsarTopicProvisioner.provisionConsumerDestination("bar", "", - properties); - verifyAndAssert(pulsarAdministration, consumerDestination.getName(), "bar", 4); - } - -} diff --git a/spring-pulsar-spring-cloud-stream-binder/src/test/resources/logback-test.xml b/spring-pulsar-spring-cloud-stream-binder/src/test/resources/logback-test.xml deleted file mode 100644 index e142997d..00000000 --- a/spring-pulsar-spring-cloud-stream-binder/src/test/resources/logback-test.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n - - - - - - - - - diff --git a/spring-pulsar/build.gradle b/spring-pulsar/build.gradle index 2097dbb9..5d95a52a 100644 --- a/spring-pulsar/build.gradle +++ b/spring-pulsar/build.gradle @@ -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 +} diff --git a/spring-pulsar-spring-boot-autoconfigure/src/integration-test/java/org/springframework/pulsar/autoconfigure/PulsarFunctionAdministrationIntegrationTests.java b/spring-pulsar/src/intTest/java/org/springframework/pulsar/autoconfigure/PulsarFunctionAdministrationIntegrationTests.java similarity index 100% rename from spring-pulsar-spring-boot-autoconfigure/src/integration-test/java/org/springframework/pulsar/autoconfigure/PulsarFunctionAdministrationIntegrationTests.java rename to spring-pulsar/src/intTest/java/org/springframework/pulsar/autoconfigure/PulsarFunctionAdministrationIntegrationTests.java diff --git a/spring-pulsar-spring-boot-autoconfigure/src/test/java/org/springframework/pulsar/autoconfigure/PulsarListenerTests.java b/spring-pulsar/src/intTest/java/org/springframework/pulsar/autoconfigure/PulsarListenerIntegrationTests.java similarity index 98% rename from spring-pulsar-spring-boot-autoconfigure/src/test/java/org/springframework/pulsar/autoconfigure/PulsarListenerTests.java rename to spring-pulsar/src/intTest/java/org/springframework/pulsar/autoconfigure/PulsarListenerIntegrationTests.java index e71d6659..0b5265b1 100644 --- a/spring-pulsar-spring-boot-autoconfigure/src/test/java/org/springframework/pulsar/autoconfigure/PulsarListenerTests.java +++ b/spring-pulsar/src/intTest/java/org/springframework/pulsar/autoconfigure/PulsarListenerIntegrationTests.java @@ -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); diff --git a/spring-pulsar-spring-boot-autoconfigure/src/test/resources/application.yml b/spring-pulsar/src/intTest/resources/application.yml similarity index 100% rename from spring-pulsar-spring-boot-autoconfigure/src/test/resources/application.yml rename to spring-pulsar/src/intTest/resources/application.yml diff --git a/spring-pulsar-spring-boot-autoconfigure/src/test/resources/logback-test.xml b/spring-pulsar/src/intTest/resources/logback-test.xml similarity index 100% rename from spring-pulsar-spring-boot-autoconfigure/src/test/resources/logback-test.xml rename to spring-pulsar/src/intTest/resources/logback-test.xml