Skip to content

Commit 089fef0

Browse files
onobcphilwebb
authored andcommitted
Add Pulsar ConnectionDetails support
Add `ConnectionDetails` support for Apache Pulsar and provide adapters for Docker Compose and Testcontainers. See gh-37197
1 parent db73e07 commit 089fef0

File tree

22 files changed

+549
-14
lines changed

22 files changed

+549
-14
lines changed

spring-boot-project/spring-boot-autoconfigure/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,4 +277,5 @@ tasks.named("checkSpringConfigurationMetadata").configure {
277277

278278
test {
279279
jvmArgs += "--add-opens=java.base/java.net=ALL-UNNAMED"
280+
jvmArgs += "--add-opens=java.base/sun.net=ALL-UNNAMED"
280281
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2012-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.autoconfigure.pulsar;
18+
19+
/**
20+
* Adapts {@link PulsarProperties} to {@link PulsarConnectionDetails}.
21+
*
22+
* @author Chris Bono
23+
*/
24+
class PropertiesPulsarConnectionDetails implements PulsarConnectionDetails {
25+
26+
private final PulsarProperties pulsarProperties;
27+
28+
PropertiesPulsarConnectionDetails(PulsarProperties pulsarProperties) {
29+
this.pulsarProperties = pulsarProperties;
30+
}
31+
32+
@Override
33+
public String getPulsarBrokerUrl() {
34+
return this.pulsarProperties.getClient().getServiceUrl();
35+
}
36+
37+
@Override
38+
public String getPulsarAdminUrl() {
39+
return this.pulsarProperties.getAdmin().getServiceUrl();
40+
}
41+
42+
}

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarConfiguration.java

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,17 +71,31 @@ class PulsarConfiguration {
7171
this.propertiesMapper = new PulsarPropertiesMapper(properties);
7272
}
7373

74+
@Bean
75+
@ConditionalOnMissingBean(PulsarConnectionDetails.class)
76+
PropertiesPulsarConnectionDetails pulsarConnectionDetails() {
77+
return new PropertiesPulsarConnectionDetails(this.properties);
78+
}
79+
7480
@Bean
7581
@ConditionalOnMissingBean(PulsarClientFactory.class)
76-
DefaultPulsarClientFactory pulsarClientFactory(ObjectProvider<PulsarClientBuilderCustomizer> customizersProvider) {
82+
DefaultPulsarClientFactory pulsarClientFactory(PulsarConnectionDetails connectionDetails,
83+
ObjectProvider<PulsarClientBuilderCustomizer> customizersProvider) {
7784
List<PulsarClientBuilderCustomizer> allCustomizers = new ArrayList<>();
7885
allCustomizers.add(this.propertiesMapper::customizeClientBuilder);
86+
allCustomizers.add((clientBuilder) -> this.applyConnectionDetails(connectionDetails, clientBuilder));
7987
allCustomizers.addAll(customizersProvider.orderedStream().toList());
8088
DefaultPulsarClientFactory clientFactory = new DefaultPulsarClientFactory(
8189
(clientBuilder) -> applyClientBuilderCustomizers(allCustomizers, clientBuilder));
8290
return clientFactory;
8391
}
8492

93+
private void applyConnectionDetails(PulsarConnectionDetails connectionDetails, ClientBuilder clientBuilder) {
94+
if (connectionDetails.getPulsarBrokerUrl() != null) {
95+
clientBuilder.serviceUrl(connectionDetails.getPulsarBrokerUrl());
96+
}
97+
}
98+
8599
private void applyClientBuilderCustomizers(List<PulsarClientBuilderCustomizer> customizers,
86100
ClientBuilder clientBuilder) {
87101
customizers.forEach((customizer) -> customizer.customize(clientBuilder));
@@ -95,14 +109,21 @@ PulsarClient pulsarClient(PulsarClientFactory clientFactory) throws PulsarClient
95109

96110
@Bean
97111
@ConditionalOnMissingBean
98-
PulsarAdministration pulsarAdministration(
112+
PulsarAdministration pulsarAdministration(PulsarConnectionDetails connectionDetails,
99113
ObjectProvider<PulsarAdminBuilderCustomizer> pulsarAdminBuilderCustomizers) {
100114
List<PulsarAdminBuilderCustomizer> allCustomizers = new ArrayList<>();
101115
allCustomizers.add(this.propertiesMapper::customizeAdminBuilder);
116+
allCustomizers.add((adminBuilder) -> this.applyConnectionDetails(connectionDetails, adminBuilder));
102117
allCustomizers.addAll(pulsarAdminBuilderCustomizers.orderedStream().toList());
103118
return new PulsarAdministration((adminBuilder) -> applyAdminBuilderCustomizers(allCustomizers, adminBuilder));
104119
}
105120

121+
private void applyConnectionDetails(PulsarConnectionDetails connectionDetails, PulsarAdminBuilder adminBuilder) {
122+
if (connectionDetails.getPulsarAdminUrl() != null) {
123+
adminBuilder.serviceHttpUrl(connectionDetails.getPulsarAdminUrl());
124+
}
125+
}
126+
106127
private void applyAdminBuilderCustomizers(List<PulsarAdminBuilderCustomizer> customizers,
107128
PulsarAdminBuilder adminBuilder) {
108129
customizers.forEach((customizer) -> customizer.customize(adminBuilder));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2012-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.autoconfigure.pulsar;
18+
19+
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
20+
21+
/**
22+
* Details required to establish a connection to a Pulsar service.
23+
*
24+
* @author Chris Bono
25+
* @since 3.2.0
26+
*/
27+
public interface PulsarConnectionDetails extends ConnectionDetails {
28+
29+
/**
30+
* Returns the Pulsar service URL for the broker.
31+
* @return the Pulsar service URL for the broker
32+
*/
33+
String getPulsarBrokerUrl();
34+
35+
/**
36+
* Returns the Pulsar web URL for the admin endpoint.
37+
* @return the Pulsar web URL for the admin endpoint
38+
*/
39+
String getPulsarAdminUrl();
40+
41+
}

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapper.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ void customizeClientBuilder(ClientBuilder clientBuilder) {
5353
PulsarProperties.Client properties = this.properties.getClient();
5454
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
5555
map.from(properties::getServiceUrl).to(clientBuilder::serviceUrl);
56+
5657
map.from(properties::getConnectionTimeout).to(timeoutProperty(clientBuilder::connectionTimeout));
5758
map.from(properties::getOperationTimeout).to(timeoutProperty(clientBuilder::operationTimeout));
5859
map.from(properties::getLookupTimeout).to(timeoutProperty(clientBuilder::lookupTimeout));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright 2012-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.autoconfigure.pulsar;
18+
19+
import org.junit.jupiter.api.Test;
20+
21+
import static org.assertj.core.api.Assertions.assertThat;
22+
23+
/**
24+
* Tests for {@link PropertiesPulsarConnectionDetails}.
25+
*
26+
* @author Chris Bono
27+
*/
28+
class PropertiesPulsarConnectionDetailsTests {
29+
30+
@Test
31+
void pulsarBrokerUrlIsObtainedFromPulsarProperties() {
32+
var pulsarProps = new PulsarProperties();
33+
pulsarProps.getClient().setServiceUrl("foo");
34+
var connectionDetails = new PropertiesPulsarConnectionDetails(pulsarProps);
35+
assertThat(connectionDetails.getPulsarBrokerUrl()).isEqualTo("foo");
36+
}
37+
38+
@Test
39+
void pulsarAdminUrlIsObtainedFromPulsarProperties() {
40+
var pulsarProps = new PulsarProperties();
41+
pulsarProps.getAdmin().setServiceUrl("foo");
42+
var connectionDetails = new PropertiesPulsarConnectionDetails(pulsarProps);
43+
assertThat(connectionDetails.getPulsarAdminUrl()).isEqualTo("foo");
44+
}
45+
46+
}

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfigurationTests.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ void whenCustomPulsarReaderAnnotationProcessorDefinedAutoConfigurationIsSkipped(
114114
@Test
115115
void autoConfiguresBeans() {
116116
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(PulsarConfiguration.class)
117+
.hasSingleBean(PulsarConnectionDetails.class)
117118
.hasSingleBean(DefaultPulsarClientFactory.class)
118119
.hasSingleBean(PulsarClient.class)
119120
.hasSingleBean(PulsarAdministration.class)

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarConfigurationTests.java

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151

5252
import static org.assertj.core.api.Assertions.assertThat;
5353
import static org.assertj.core.api.Assertions.entry;
54+
import static org.mockito.BDDMockito.given;
5455
import static org.mockito.Mockito.mock;
5556

5657
/**
@@ -67,6 +68,15 @@ class PulsarConfigurationTests {
6768
.withConfiguration(AutoConfigurations.of(PulsarConfiguration.class))
6869
.withBean(PulsarClient.class, () -> mock(PulsarClient.class));
6970

71+
@Test
72+
void whenHasUserDefinedConnectionDetailsBeanDoesNotAutoConfigureBean() {
73+
PulsarConnectionDetails customConnectionDetails = mock(PulsarConnectionDetails.class);
74+
this.contextRunner
75+
.withBean("customPulsarConnectionDetails", PulsarConnectionDetails.class, () -> customConnectionDetails)
76+
.run((context) -> assertThat(context).getBean(PulsarConnectionDetails.class)
77+
.isSameAs(customConnectionDetails));
78+
}
79+
7080
@Nested
7181
class ClientTests {
7282

@@ -86,17 +96,36 @@ void whenHasUserDefinedClientBeanDoesNotAutoConfigureBean() {
8696
.run((context) -> assertThat(context).getBean(PulsarClient.class).isSameAs(customClient));
8797
}
8898

99+
@Test
100+
void whenConnectionDetailsAreNullTheyAreNotApplied() {
101+
PulsarConnectionDetails connectionDetails = mock(PulsarConnectionDetails.class);
102+
given(connectionDetails.getPulsarBrokerUrl()).willReturn(null);
103+
PulsarConfigurationTests.this.contextRunner.withBean(PulsarConnectionDetails.class, () -> connectionDetails)
104+
.withPropertyValues("spring.pulsar.client.service-url=fromPropsCustomizer")
105+
.run((context) -> {
106+
DefaultPulsarClientFactory clientFactory = context.getBean(DefaultPulsarClientFactory.class);
107+
Customizers<PulsarClientBuilderCustomizer, ClientBuilder> customizers = Customizers
108+
.of(ClientBuilder.class, PulsarClientBuilderCustomizer::customize);
109+
assertThat(customizers.fromField(clientFactory, "customizer"))
110+
.callsInOrder(ClientBuilder::serviceUrl, "fromPropsCustomizer");
111+
});
112+
}
113+
89114
@Test
90115
void whenHasUserDefinedCustomizersAppliesInCorrectOrder() {
116+
PulsarConnectionDetails connectionDetails = mock(PulsarConnectionDetails.class);
117+
given(connectionDetails.getPulsarBrokerUrl()).willReturn("fromConnectionDetailsCustomizer");
91118
PulsarConfigurationTests.this.contextRunner
92119
.withUserConfiguration(PulsarClientBuilderCustomizersConfig.class)
120+
.withBean(PulsarConnectionDetails.class, () -> connectionDetails)
93121
.withPropertyValues("spring.pulsar.client.service-url=fromPropsCustomizer")
94122
.run((context) -> {
95123
DefaultPulsarClientFactory clientFactory = context.getBean(DefaultPulsarClientFactory.class);
96124
Customizers<PulsarClientBuilderCustomizer, ClientBuilder> customizers = Customizers
97125
.of(ClientBuilder.class, PulsarClientBuilderCustomizer::customize);
98126
assertThat(customizers.fromField(clientFactory, "customizer")).callsInOrder(
99-
ClientBuilder::serviceUrl, "fromPropsCustomizer", "fromCustomizer1", "fromCustomizer2");
127+
ClientBuilder::serviceUrl, "fromPropsCustomizer", "fromConnectionDetailsCustomizer",
128+
"fromCustomizer1", "fromCustomizer2");
100129
});
101130
}
102131

@@ -133,17 +162,35 @@ void whenHasUserDefinedBeanDoesNotAutoConfigureBean() {
133162
.isSameAs(pulsarAdministration));
134163
}
135164

165+
@Test
166+
void whenConnectionDetailsAreNullTheyAreNotApplied() {
167+
PulsarConnectionDetails connectionDetails = mock(PulsarConnectionDetails.class);
168+
given(connectionDetails.getPulsarAdminUrl()).willReturn(null);
169+
PulsarConfigurationTests.this.contextRunner.withBean(PulsarConnectionDetails.class, () -> connectionDetails)
170+
.withPropertyValues("spring.pulsar.admin.service-url=fromPropsCustomizer")
171+
.run((context) -> {
172+
PulsarAdministration pulsarAdmin = context.getBean(PulsarAdministration.class);
173+
Customizers<PulsarAdminBuilderCustomizer, PulsarAdminBuilder> customizers = Customizers
174+
.of(PulsarAdminBuilder.class, PulsarAdminBuilderCustomizer::customize);
175+
assertThat(customizers.fromField(pulsarAdmin, "adminCustomizers"))
176+
.callsInOrder(PulsarAdminBuilder::serviceHttpUrl, "fromPropsCustomizer");
177+
});
178+
}
179+
136180
@Test
137181
void whenHasUserDefinedCustomizersAppliesInCorrectOrder() {
182+
PulsarConnectionDetails connectionDetails = mock(PulsarConnectionDetails.class);
183+
given(connectionDetails.getPulsarAdminUrl()).willReturn("fromConnectionDetailsCustomizer");
138184
this.contextRunner.withUserConfiguration(PulsarAdminBuilderCustomizersConfig.class)
185+
.withBean(PulsarConnectionDetails.class, () -> connectionDetails)
139186
.withPropertyValues("spring.pulsar.admin.service-url=fromPropsCustomizer")
140187
.run((context) -> {
141188
PulsarAdministration pulsarAdmin = context.getBean(PulsarAdministration.class);
142189
Customizers<PulsarAdminBuilderCustomizer, PulsarAdminBuilder> customizers = Customizers
143190
.of(PulsarAdminBuilder.class, PulsarAdminBuilderCustomizer::customize);
144191
assertThat(customizers.fromField(pulsarAdmin, "adminCustomizers")).callsInOrder(
145-
PulsarAdminBuilder::serviceHttpUrl, "fromPropsCustomizer", "fromCustomizer1",
146-
"fromCustomizer2");
192+
PulsarAdminBuilder::serviceHttpUrl, "fromPropsCustomizer",
193+
"fromConnectionDetailsCustomizer", "fromCustomizer1", "fromCustomizer2");
147194
});
148195
}
149196

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright 2012-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.docker.compose.service.connection.pulsar;
18+
19+
import org.springframework.boot.autoconfigure.pulsar.PulsarConnectionDetails;
20+
import org.springframework.boot.docker.compose.core.RunningService;
21+
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory;
22+
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource;
23+
24+
/**
25+
* {@link DockerComposeConnectionDetailsFactory} to create {@link PulsarConnectionDetails}
26+
* for a {@code pulsar} service.
27+
*
28+
* @author Chris Bono
29+
*/
30+
class PulsarDockerComposeConnectionDetailsFactory
31+
extends DockerComposeConnectionDetailsFactory<PulsarConnectionDetails> {
32+
33+
private static final int PULSAR_BROKER_PORT = 6650;
34+
35+
private static final int PULSAR_ADMIN_PORT = 8080;
36+
37+
PulsarDockerComposeConnectionDetailsFactory() {
38+
super("apachepulsar/pulsar");
39+
}
40+
41+
@Override
42+
protected PulsarConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) {
43+
return new PulsarDockerComposeConnectionDetails(source.getRunningService());
44+
}
45+
46+
/**
47+
* {@link PulsarConnectionDetails} backed by a {@code pulsar} {@link RunningService}.
48+
*/
49+
static class PulsarDockerComposeConnectionDetails extends DockerComposeConnectionDetails
50+
implements PulsarConnectionDetails {
51+
52+
private final String brokerUrl;
53+
54+
private final String adminUrl;
55+
56+
PulsarDockerComposeConnectionDetails(RunningService service) {
57+
super(service);
58+
this.brokerUrl = "pulsar://%s:%s".formatted(service.host(), service.ports().get(PULSAR_BROKER_PORT));
59+
this.adminUrl = "http://%s:%s".formatted(service.host(), service.ports().get(PULSAR_ADMIN_PORT));
60+
}
61+
62+
@Override
63+
public String getPulsarBrokerUrl() {
64+
return this.brokerUrl;
65+
}
66+
67+
@Override
68+
public String getPulsarAdminUrl() {
69+
return this.adminUrl;
70+
}
71+
72+
}
73+
74+
}

0 commit comments

Comments
 (0)