Skip to content

Commit 496458d

Browse files
authored
Fixes NullPointerException when using connectionString and build client. (Azure#32086)
* Do not validate eventHubName until client is built. * Add tests. * Update changelog.
1 parent d4f02c5 commit 496458d

File tree

3 files changed

+97
-25
lines changed

3 files changed

+97
-25
lines changed

sdk/eventhubs/azure-messaging-eventhubs/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
### Bugs Fixed
1212

13+
- Fixed ability to pass in namespace connection string in EventHubClientBuilder. ([#29536](https://github.com/Azure/azure-sdk-for-java/issues/29536))
1314
- Added retry for createBatch API as this API also makes network calls similar to its companion send API.
1415

1516
### Other Changes

sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubClientBuilder.java

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
import java.util.UUID;
5454
import java.util.concurrent.atomic.AtomicBoolean;
5555
import java.util.concurrent.atomic.AtomicInteger;
56+
import java.util.function.Supplier;
5657
import java.util.regex.Pattern;
5758

5859
import static com.azure.messaging.eventhubs.implementation.ClientConstants.CONNECTION_ID_KEY;
@@ -221,7 +222,7 @@ public EventHubClientBuilder() {
221222
}
222223

223224
/**
224-
* Sets the credential information given a connection string to the Event Hub instance.
225+
* Sets the credential information given a connection string to the Event Hub instance or the Event Hubs namespace.
225226
*
226227
* <p>
227228
* If the connection string is copied from the Event Hubs namespace, it will likely not contain the name to the
@@ -234,20 +235,34 @@ public EventHubClientBuilder() {
234235
* from that Event Hub will result in a connection string that contains the name.
235236
* </p>
236237
*
237-
* @param connectionString The connection string to use for connecting to the Event Hub instance. It is expected
238-
* that the Event Hub name and the shared access key properties are contained in this connection string.
238+
* @param connectionString The connection string to use for connecting to the Event Hub instance or Event Hubs
239+
* instance. It is expected that the Event Hub name and the shared access key properties are contained in this
240+
* connection string.
239241
*
240242
* @return The updated {@link EventHubClientBuilder} object.
241-
* @throws IllegalArgumentException if {@code connectionString} is null or empty. Or, the {@code
242-
* connectionString} does not contain the "EntityPath" key, which is the name of the Event Hub instance.
243+
* @throws IllegalArgumentException if {@code connectionString} is null or empty. If {@code fullyQualifiedNamespace}
244+
* in the connection string is null.
245+
* @throws NullPointerException if a credential could not be extracted
243246
* @throws AzureException If the shared access signature token credential could not be created using the
244247
* connection string.
245248
*/
246249
@Override
247250
public EventHubClientBuilder connectionString(String connectionString) {
248-
ConnectionStringProperties properties = new ConnectionStringProperties(connectionString);
249-
TokenCredential tokenCredential = getTokenCredential(properties);
250-
return credential(properties.getEndpoint().getHost(), properties.getEntityPath(), tokenCredential);
251+
final ConnectionStringProperties properties = new ConnectionStringProperties(connectionString);
252+
253+
this.fullyQualifiedNamespace = Objects.requireNonNull(properties.getEndpoint().getHost(),
254+
"'fullyQualifiedNamespace' cannot be null.");
255+
this.credentials = getTokenCredential(properties);
256+
257+
if (CoreUtils.isNullOrEmpty(fullyQualifiedNamespace)) {
258+
throw LOGGER.logExceptionAsError(new IllegalArgumentException("'host' cannot be an empty string."));
259+
}
260+
261+
if (!CoreUtils.isNullOrEmpty(properties.getEntityPath())) {
262+
this.eventHubName = properties.getEntityPath();
263+
}
264+
265+
return this;
251266
}
252267

253268
private TokenCredential getTokenCredential(ConnectionStringProperties properties) {
@@ -405,13 +420,6 @@ public EventHubClientBuilder eventHubName(String eventHubName) {
405420
return this;
406421
}
407422

408-
private String getEventHubName() {
409-
if (CoreUtils.isNullOrEmpty(eventHubName)) {
410-
throw LOGGER.logExceptionAsError(new IllegalArgumentException("'eventHubName' cannot be an empty string."));
411-
}
412-
return eventHubName;
413-
}
414-
415423
/**
416424
* Toggles the builder to use the same connection for producers or consumers that are built from this instance. By
417425
* default, a new connection is constructed and used created for each Event Hub consumer or producer created.
@@ -714,7 +722,7 @@ EventHubClientBuilder verifyMode(SslDomain.VerifyMode verifyMode) {
714722
* @throws IllegalArgumentException If shared connection is not used and the credentials have not been set using
715723
* either {@link #connectionString(String)} or {@link #credential(String, String, TokenCredential)}. Also, if
716724
* {@link #consumerGroup(String)} have not been set. And if a proxy is specified but the transport type is not
717-
* {@link AmqpTransportType#AMQP_WEB_SOCKETS web sockets}.
725+
* {@link AmqpTransportType#AMQP_WEB_SOCKETS web sockets}. Or, if the {@code eventHubName} has not been set.
718726
*/
719727
public EventHubConsumerAsyncClient buildAsyncConsumerClient() {
720728
if (CoreUtils.isNullOrEmpty(consumerGroup)) {
@@ -733,7 +741,7 @@ public EventHubConsumerAsyncClient buildAsyncConsumerClient() {
733741
* @throws IllegalArgumentException If shared connection is not used and the credentials have not been set using
734742
* either {@link #connectionString(String)} or {@link #credential(String, String, TokenCredential)}. Also, if
735743
* {@link #consumerGroup(String)} have not been set. And if a proxy is specified but the transport type is not
736-
* {@link AmqpTransportType#AMQP_WEB_SOCKETS web sockets}.
744+
* {@link AmqpTransportType#AMQP_WEB_SOCKETS web sockets}. Or, if the {@code eventHubName} has not been set.
737745
*/
738746
public EventHubConsumerClient buildConsumerClient() {
739747
return buildClient().createConsumer(consumerGroup, prefetchCount);
@@ -747,6 +755,7 @@ public EventHubConsumerClient buildConsumerClient() {
747755
* @throws IllegalArgumentException If shared connection is not used and the credentials have not been set using
748756
* either {@link #connectionString(String)} or {@link #credential(String, String, TokenCredential)}. Or, if a
749757
* proxy is specified but the transport type is not {@link AmqpTransportType#AMQP_WEB_SOCKETS web sockets}.
758+
* Or, if the {@code eventHubName} has not been set.
750759
*/
751760
public EventHubProducerAsyncClient buildAsyncProducerClient() {
752761
return buildAsyncClient().createProducer();
@@ -760,6 +769,7 @@ public EventHubProducerAsyncClient buildAsyncProducerClient() {
760769
* @throws IllegalArgumentException If shared connection is not used and the credentials have not been set using
761770
* either {@link #connectionString(String)} or {@link #credential(String, String, TokenCredential)}. Or, if a
762771
* proxy is specified but the transport type is not {@link AmqpTransportType#AMQP_WEB_SOCKETS web sockets}.
772+
* Or, if the {@code eventHubName} has not been set.
763773
*/
764774
public EventHubProducerClient buildProducerClient() {
765775
return buildClient().createProducer();
@@ -786,7 +796,8 @@ public EventHubProducerClient buildProducerClient() {
786796
* @return A new {@link EventHubAsyncClient} instance with all the configured options.
787797
* @throws IllegalArgumentException if the credentials have not been set using either {@link
788798
* #connectionString(String)} or {@link #credential(String, String, TokenCredential)}. Or, if a proxy is
789-
* specified but the transport type is not {@link AmqpTransportType#AMQP_WEB_SOCKETS web sockets}.
799+
* specified but the transport type is not {@link AmqpTransportType#AMQP_WEB_SOCKETS web sockets}. Or, if the
800+
* {@code eventHubName} has not been set.
790801
*/
791802
EventHubAsyncClient buildAsyncClient() {
792803
if (retryOptions == null) {
@@ -898,6 +909,13 @@ Meter createMeter() {
898909

899910
private EventHubConnectionProcessor buildConnectionProcessor(MessageSerializer messageSerializer, Meter meter) {
900911
final ConnectionOptions connectionOptions = getConnectionOptions();
912+
final Supplier<String> getEventHubName = () -> {
913+
if (CoreUtils.isNullOrEmpty(eventHubName)) {
914+
throw LOGGER.logExceptionAsError(new IllegalArgumentException("'eventHubName' cannot be an empty string."));
915+
}
916+
return eventHubName;
917+
};
918+
901919
final Flux<EventHubAmqpConnection> connectionFlux = Flux.create(sink -> {
902920
sink.onRequest(request -> {
903921

@@ -921,15 +939,15 @@ private EventHubConnectionProcessor buildConnectionProcessor(MessageSerializer m
921939
final ReactorHandlerProvider handlerProvider = new ReactorHandlerProvider(provider, meter);
922940

923941
final EventHubAmqpConnection connection = new EventHubReactorAmqpConnection(connectionId,
924-
connectionOptions, getEventHubName(), provider, handlerProvider, tokenManagerProvider,
942+
connectionOptions, getEventHubName.get(), provider, handlerProvider, tokenManagerProvider,
925943
messageSerializer);
926944

927945
sink.next(connection);
928946
});
929947
});
930948

931949
return connectionFlux.subscribeWith(new EventHubConnectionProcessor(
932-
connectionOptions.getFullyQualifiedNamespace(), getEventHubName(), connectionOptions.getRetry()));
950+
connectionOptions.getFullyQualifiedNamespace(), getEventHubName.get(), connectionOptions.getRetry()));
933951
}
934952

935953
private ConnectionOptions getConnectionOptions() {

sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubClientBuilderTest.java

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@
2424
import java.util.Locale;
2525
import java.util.stream.Stream;
2626

27+
import static org.junit.jupiter.api.Assertions.assertEquals;
2728
import static org.junit.jupiter.api.Assertions.assertNotNull;
2829
import static org.junit.jupiter.api.Assertions.assertThrows;
30+
import static org.junit.jupiter.api.Assertions.assertTrue;
2931

3032
public class EventHubClientBuilderTest {
3133
private static final String NAMESPACE_NAME = "dummyNamespaceName";
@@ -106,8 +108,6 @@ public void testConnectionStringWithSas() {
106108
.connectionString(connectionStringWithNoEntityPath, "eh-name"));
107109
assertNotNull(new EventHubClientBuilder()
108110
.connectionString(connectionStringWithEntityPath));
109-
assertThrows(NullPointerException.class, () -> new EventHubClientBuilder()
110-
.connectionString(connectionStringWithNoEntityPath));
111111
assertThrows(IllegalArgumentException.class, () -> new EventHubClientBuilder()
112112
.connectionString(connectionStringWithEntityPath, "eh-name-mismatch"));
113113
}
@@ -181,22 +181,33 @@ public void testConnectionWithAzureSasCredential() {
181181

182182
@Test
183183
public void testCreatesClientWithTokenCredential() {
184-
new EventHubClientBuilder()
184+
EventHubClient eventHubClient = new EventHubClientBuilder()
185185
.credential(TOKEN_CREDENTIAL)
186186
.fullyQualifiedNamespace(NAMESPACE_NAME)
187187
.eventHubName(EVENT_HUB_NAME)
188188
.buildClient();
189-
new EventHubClientBuilder()
189+
EventHubProducerClient eventHubProducerClient = new EventHubClientBuilder()
190190
.credential(TOKEN_CREDENTIAL)
191191
.fullyQualifiedNamespace(NAMESPACE_NAME)
192192
.eventHubName(EVENT_HUB_NAME)
193193
.buildProducerClient();
194-
new EventHubClientBuilder()
194+
EventHubConsumerClient eventHubConsumerClient = new EventHubClientBuilder()
195195
.credential(TOKEN_CREDENTIAL)
196196
.fullyQualifiedNamespace(NAMESPACE_NAME)
197197
.eventHubName(EVENT_HUB_NAME)
198198
.consumerGroup("foo")
199199
.buildConsumerClient();
200+
201+
// Assert
202+
assertNotNull(eventHubClient);
203+
assertNotNull(eventHubProducerClient);
204+
assertNotNull(eventHubConsumerClient);
205+
206+
assertEquals(EVENT_HUB_NAME, eventHubProducerClient.getEventHubName());
207+
assertEquals(NAMESPACE_NAME, eventHubProducerClient.getFullyQualifiedNamespace());
208+
209+
assertEquals(EVENT_HUB_NAME, eventHubConsumerClient.getEventHubName());
210+
assertEquals(NAMESPACE_NAME, eventHubConsumerClient.getFullyQualifiedNamespace());
200211
}
201212

202213
@Test
@@ -233,6 +244,48 @@ public void testThrowsIfAttemptsToCreateClientWithTokenCredentialWithoutEventHub
233244
.buildConsumerClient());
234245
}
235246

247+
/**
248+
* Verifies that we can pass an Event Hub namespace connection string and event hub name to create a client.
249+
*/
250+
@Test
251+
public void namespaceConnectionStringAndName() {
252+
// Arrange
253+
final String namespaceConnectionString = String.format("Endpoint=%s;SharedAccessKeyName=%s;SharedAccessKey=%s",
254+
ENDPOINT, SHARED_ACCESS_KEY_NAME, SHARED_ACCESS_KEY);
255+
final String fullyQualifiedDomainName = NAMESPACE_NAME + ENDPOINT_SUFFIX;
256+
257+
// Act
258+
final EventHubProducerAsyncClient client = new EventHubClientBuilder()
259+
.connectionString(namespaceConnectionString)
260+
.eventHubName(EVENT_HUB_NAME)
261+
.buildAsyncProducerClient();
262+
263+
// Assert
264+
assertTrue(fullyQualifiedDomainName.equalsIgnoreCase(client.getFullyQualifiedNamespace()),
265+
String.format("Expected: %s. Actual: %s%n", fullyQualifiedDomainName,
266+
client.getFullyQualifiedNamespace()));
267+
268+
assertEquals(EVENT_HUB_NAME, client.getEventHubName());
269+
}
270+
271+
/**
272+
* Verifies that an exception is thrown when we try to construct a client without setting the event hub name.
273+
*/
274+
@Test
275+
public void namespaceConnectionStringThrowsNoEventHubName() {
276+
// Arrange
277+
final String namespaceConnectionString = String.format("Endpoint=%s;SharedAccessKeyName=%s;SharedAccessKey=%s",
278+
ENDPOINT, SHARED_ACCESS_KEY_NAME, SHARED_ACCESS_KEY);
279+
280+
// Act & Assert
281+
assertThrows(IllegalArgumentException.class, () -> new EventHubClientBuilder()
282+
.connectionString(namespaceConnectionString)
283+
.buildAsyncProducerClient());
284+
assertThrows(IllegalArgumentException.class, () -> new EventHubClientBuilder()
285+
.connectionString(namespaceConnectionString)
286+
.buildAsyncConsumerClient());
287+
}
288+
236289
private static Stream<Arguments> getProxyConfigurations() {
237290
return Stream.of(
238291
Arguments.of("http://localhost:8080"),

0 commit comments

Comments
 (0)