subscriptionUpdate = UPayload.unpack(
- message.getPayload(), message.getAttributes().getPayloadFormat(), Update.class);
-
- // Check if we got the right subscription change notification
- if (subscriptionUpdate.isPresent()) {
- // Check if we have a handler registered for the subscription change notification for the specific
- // topic that triggered the subscription change notification. It is very possible that the client
- // did not register one to begin with (i.e they don't care to receive it)
- mHandlers.computeIfPresent(subscriptionUpdate.get().getTopic(), (topic, handler) -> {
- try {
- handler.handleSubscriptionChange(subscriptionUpdate.get().getTopic(),
- subscriptionUpdate.get().getStatus());
- } catch (Exception e) {
- Logger.getGlobal().info(e.getMessage());
- }
- return handler;
- });
- }
- }
-}
diff --git a/src/main/java/org/eclipse/uprotocol/client/usubscription/v3/RpcClientBasedUSubscriptionClient.java b/src/main/java/org/eclipse/uprotocol/client/usubscription/v3/RpcClientBasedUSubscriptionClient.java
new file mode 100644
index 00000000..0f239c70
--- /dev/null
+++ b/src/main/java/org/eclipse/uprotocol/client/usubscription/v3/RpcClientBasedUSubscriptionClient.java
@@ -0,0 +1,193 @@
+/**
+ * SPDX-FileCopyrightText: 2024 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Apache License Version 2.0 which is available at
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package org.eclipse.uprotocol.client.usubscription.v3;
+
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.CompletionStage;
+
+import org.eclipse.uprotocol.Uoptions;
+import org.eclipse.uprotocol.communication.CallOptions;
+import org.eclipse.uprotocol.communication.RpcClient;
+import org.eclipse.uprotocol.communication.UPayload;
+import org.eclipse.uprotocol.core.usubscription.v3.FetchSubscribersRequest;
+import org.eclipse.uprotocol.core.usubscription.v3.FetchSubscribersResponse;
+import org.eclipse.uprotocol.core.usubscription.v3.FetchSubscriptionsRequest;
+import org.eclipse.uprotocol.core.usubscription.v3.FetchSubscriptionsResponse;
+import org.eclipse.uprotocol.core.usubscription.v3.NotificationsRequest;
+import org.eclipse.uprotocol.core.usubscription.v3.NotificationsResponse;
+import org.eclipse.uprotocol.core.usubscription.v3.SubscriptionRequest;
+import org.eclipse.uprotocol.core.usubscription.v3.SubscriptionResponse;
+import org.eclipse.uprotocol.core.usubscription.v3.USubscriptionProto;
+import org.eclipse.uprotocol.core.usubscription.v3.UnsubscribeRequest;
+import org.eclipse.uprotocol.core.usubscription.v3.UnsubscribeResponse;
+import org.eclipse.uprotocol.v1.UUri;
+
+import com.google.protobuf.Message;
+import com.google.protobuf.Descriptors.ServiceDescriptor;
+
+/**
+ * A USubscription client implementation for invoking operations of a USubscription service.
+ *
+ * The client requires an {@link RpcClient} for performing the remote procedure calls.
+ */
+public class RpcClientBasedUSubscriptionClient implements USubscriptionClient {
+ private static final ServiceDescriptor USUBSCRIPTION_DEFAULT_DESCRIPTOR = USubscriptionProto.getDescriptor()
+ .getServices().get(0);
+
+ // TODO: The following items eventually need to be pulled from generated code
+ private static final int SUBSCRIBE_METHOD_ID = 0x0001;
+ private static final int UNSUBSCRIBE_METHOD_ID = 0x0002;
+ private static final int FETCH_SUBSCRIPTIONS_METHOD_ID = 0x0003;
+ private static final int REGISTER_NOTIFICATIONS_METHOD_ID = 0x0006;
+ private static final int UNREGISTER_NOTIFICATIONS_METHOD_ID = 0x0007;
+ private static final int FETCH_SUBSCRIBERS_METHOD_ID = 0x0008;
+ private static final int NOTIFICATION_TOPIC_ID = 0x8000;
+
+ private final RpcClient rpcClient;
+ private final UUri serviceUri;
+ private final CallOptions callOptions;
+
+ /**
+ * Creates a new client for interacting with the default instance of
+ * the local uSubscription service.
+ *
+ * @param rpcClient The client to use for sending the RPC requests.
+ * @param callOptions The options to use for the RPC calls.
+ * @throws NullPointerException if rpcClient or callOptions are {@code null}.
+ */
+ public RpcClientBasedUSubscriptionClient (RpcClient rpcClient, CallOptions callOptions) {
+ this(rpcClient, callOptions, 0x0000, null);
+ }
+
+ /**
+ * Creates a new client for interacting with a given uSubscription
+ * service instance.
+ *
+ * @param rpcClient The client to use for sending the RPC requests.
+ * @param callOptions The options to use for the RPC calls.
+ * @param subscriptionServiceInstanceId The instance of the subscription service to invoke,
+ * {@code 0x000} to use the default instance.
+ * @param subscriptionServiceAuthority The authority that the subscription service runs on,
+ * or {@code null} if the instance runs on the local authority.
+ * @throws NullPointerException if rpcClient or callOptions are {@code null}.
+ * @throws IllegalArgumentException if the instance ID is invalid.
+ */
+ public RpcClientBasedUSubscriptionClient (
+ RpcClient rpcClient,
+ CallOptions callOptions,
+ int subscriptionServiceInstanceId,
+ String subscriptionServiceAuthority) {
+ Objects.requireNonNull(rpcClient, "RpcClient missing");
+ Objects.requireNonNull(callOptions, "CallOptions missing");
+ if (subscriptionServiceInstanceId < 0 || subscriptionServiceInstanceId >= 0xFFFF) {
+ throw new IllegalArgumentException("Invalid subscription service instance ID");
+ }
+ this.rpcClient = rpcClient;
+ this.callOptions = callOptions;
+ this.serviceUri = getUSubscriptionServiceUri(subscriptionServiceInstanceId, subscriptionServiceAuthority);
+ }
+
+ private static UUri getUSubscriptionServiceUri(int instanceId, String authority) {
+ final var options = USUBSCRIPTION_DEFAULT_DESCRIPTOR.getOptions();
+ var builder = UUri.newBuilder();
+ Optional.ofNullable(authority).ifPresent(builder::setAuthorityName);
+ return builder
+ .setUeId((instanceId << 16) | options.getExtension(Uoptions.serviceId))
+ .setUeVersionMajor(options.getExtension(Uoptions.serviceVersionMajor))
+ .build();
+ }
+
+ private CompletionStage invokeMethod(
+ int methodId,
+ UPayload request,
+ CallOptions options,
+ Class responseType) {
+ Objects.requireNonNull(request, "Request missing");
+ Objects.requireNonNull(options, "CallOptions missing");
+ Objects.requireNonNull(responseType, "Response type missing");
+
+ final var method = UUri.newBuilder(serviceUri).setResourceId(methodId).build();
+ return rpcClient.invokeMethod(method, request, options)
+ .thenApply(responsePayload -> UPayload.unpackOrDefaultInstance(responsePayload, responseType));
+ }
+
+ @Override
+ public UUri getSubscriptionServiceNotificationTopic() {
+ return UUri.newBuilder(serviceUri)
+ .setResourceId(NOTIFICATION_TOPIC_ID)
+ .build();
+ }
+
+ @Override
+ public CompletionStage subscribe(SubscriptionRequest request) {
+ Objects.requireNonNull(request, "Subscribe request missing");
+ return invokeMethod(
+ SUBSCRIBE_METHOD_ID,
+ UPayload.pack(request),
+ callOptions,
+ SubscriptionResponse.class);
+ }
+
+ @Override
+ public CompletionStage unsubscribe(UnsubscribeRequest request) {
+ Objects.requireNonNull(request, "Unsubscribe request missing");
+ return invokeMethod(
+ UNSUBSCRIBE_METHOD_ID,
+ UPayload.pack(request),
+ callOptions,
+ UnsubscribeResponse.class);
+ }
+
+ @Override
+ public CompletionStage fetchSubscribers(FetchSubscribersRequest request) {
+ Objects.requireNonNull(request, "Request missing");
+
+ return invokeMethod(
+ FETCH_SUBSCRIBERS_METHOD_ID,
+ UPayload.pack(request),
+ callOptions,
+ FetchSubscribersResponse.class);
+ }
+
+ @Override
+ public CompletionStage fetchSubscriptions(FetchSubscriptionsRequest request) {
+ Objects.requireNonNull(request, "Request missing");
+
+ return invokeMethod(
+ FETCH_SUBSCRIPTIONS_METHOD_ID,
+ UPayload.pack(request),
+ callOptions,
+ FetchSubscriptionsResponse.class);
+ }
+
+ @Override
+ public CompletionStage registerForNotifications(NotificationsRequest request) {
+ Objects.requireNonNull(request, "Request missing");
+ return invokeMethod(
+ REGISTER_NOTIFICATIONS_METHOD_ID,
+ UPayload.pack(request),
+ callOptions,
+ NotificationsResponse.class);
+ }
+
+ @Override
+ public CompletionStage unregisterForNotifications(NotificationsRequest request) {
+ Objects.requireNonNull(request, "Request missing");
+ return invokeMethod(
+ UNREGISTER_NOTIFICATIONS_METHOD_ID,
+ UPayload.pack(request),
+ callOptions,
+ NotificationsResponse.class);
+ }
+}
diff --git a/src/main/java/org/eclipse/uprotocol/client/usubscription/v3/USubscriptionClient.java b/src/main/java/org/eclipse/uprotocol/client/usubscription/v3/USubscriptionClient.java
index b188ec62..4ba4b301 100644
--- a/src/main/java/org/eclipse/uprotocol/client/usubscription/v3/USubscriptionClient.java
+++ b/src/main/java/org/eclipse/uprotocol/client/usubscription/v3/USubscriptionClient.java
@@ -14,247 +14,105 @@
import java.util.concurrent.CompletionStage;
-import org.eclipse.uprotocol.communication.CallOptions;
+import org.eclipse.uprotocol.communication.Notifier;
import org.eclipse.uprotocol.communication.UStatusException;
+import org.eclipse.uprotocol.core.usubscription.v3.FetchSubscribersRequest;
import org.eclipse.uprotocol.core.usubscription.v3.FetchSubscribersResponse;
import org.eclipse.uprotocol.core.usubscription.v3.FetchSubscriptionsRequest;
import org.eclipse.uprotocol.core.usubscription.v3.FetchSubscriptionsResponse;
+import org.eclipse.uprotocol.core.usubscription.v3.NotificationsRequest;
import org.eclipse.uprotocol.core.usubscription.v3.NotificationsResponse;
import org.eclipse.uprotocol.core.usubscription.v3.SubscribeAttributes;
+import org.eclipse.uprotocol.core.usubscription.v3.SubscriptionRequest;
import org.eclipse.uprotocol.core.usubscription.v3.SubscriptionResponse;
-import org.eclipse.uprotocol.core.usubscription.v3.SubscriptionStatus;
-import org.eclipse.uprotocol.transport.UListener;
-import org.eclipse.uprotocol.v1.UCode;
-import org.eclipse.uprotocol.v1.UStatus;
+import org.eclipse.uprotocol.core.usubscription.v3.UnsubscribeRequest;
+import org.eclipse.uprotocol.core.usubscription.v3.UnsubscribeResponse;
import org.eclipse.uprotocol.v1.UUri;
/**
- * The Client-side interface for communicating with the USubscription service.
+ * The client-side interface for interacting with a USubscription service instance.
+ *
+ * @see
+ * USubscription service specification
*/
public interface USubscriptionClient {
/**
- * Subscribes to a given topic.
- *
- * The API will return a {@link CompletionStage} with the response {@link SubscriptionResponse} or exception
- * with {@link UStatusException} containing the reason for the failure.
- *
- * @param topic The topic to subscribe to.
- * @param listener The listener to be called when a message is received on the topic.
- * @return Returns the CompletionStage with {@link SubscriptionResponse} or exception with the failure.
+ * Gets the topic that the USubscription service instance uses for sending subscription change notifications.
+ *
+ * Clients can use this topic to register a listener for these notifications using
+ * {@link Notifier#registerNotificationListener(UUri, org.eclipse.uprotocol.transport.UListener)}.
+ *
+ * @return The topic.
*/
- default CompletionStage subscribe(UUri topic, UListener listener) {
- return subscribe(topic, listener, CallOptions.DEFAULT);
- }
+ UUri getSubscriptionServiceNotificationTopic();
/**
* Subscribes to a given topic.
- *
- * The API will return a {@link CompletionStage} with the response {@link SubscriptionResponse} or exception
- * with {@link UStatusException} containing the reason for the failure.
- *
- * @param topic The topic to subscribe to.
- * @param listener The listener to be called when a message is received on the topic.
- * @param options The {@link CallOptions} to be used for the subscription.
- * @return Returns the CompletionStage with {@link SubscriptionResponse} or exception with the failure.
+ *
+ * @param request The request to send.
+ * @return The outcome of the operation. The stage will be completed with a {@link UStatusException}
+ * if the request has failed.
+ * @throws NullPointerException if request is {@code null}.
*/
- default CompletionStage subscribe(UUri topic, UListener listener, CallOptions options) {
- return subscribe(topic, listener, options, null);
- }
-
-
- /**
- * Subscribes to a given topic.
- *
- * The API will return a {@link CompletionStage} with the response {@link SubscriptionResponse} or exception
- * with the failure if the subscription was not successful. The optional passed {@link SubscriptionChangeHandler}
- * is used to receive notifications of changes to the subscription status like a transition from
- * {@link SubscriptionStatus.State#SUBSCRIBE_PENDING} to {@link SubscriptionStatus.State#SUBSCRIBED} that
- * occurs when we subscribe to remote topics that the device we are on has not yet a subscriber that has
- * subscribed to said topic.
- *
- * @param topic The topic to subscribe to.
- * @param listener The listener to be called when a messages are received.
- * @param options The {@link CallOptions} to be used for the subscription.
- * @param handler {@link SubscriptionChangeHandler} to handle changes to subscription states.
- * @return Returns the CompletionStage with {@link SubscriptionResponse} or exception with the failure
- * reason as {@link UStatus}. {@link UCode#ALREADY_EXISTS} will be returned if you call this API multiple
- * times passing a different handler.
- */
- CompletionStage subscribe(UUri topic, UListener listener, CallOptions options,
- SubscriptionChangeHandler handler);
-
+ CompletionStage subscribe(SubscriptionRequest request);
/**
* Unsubscribes from a given topic.
- *
- * The subscriber no longer wishes to be subscribed to said topic so we issue a unsubscribe
- * request to the USubscription service. The API will return a {@link CompletionStage} with the
- * {@link UStatus} of the result. If we are unable to unsubscribe to the topic with USubscription
- * service, the listener and handler (if any) will remain registered.
- *
- * @param topic The topic to unsubscribe to.
- * @param listener The listener to be called when a message is received on the topic.
- * @return Returns {@link UStatus} with the result from the unsubscribe request.
+ *
+ * @param request The request to send.
+ * @return The outcome of the operation. The stage will be completed with a {@link UStatusException}
+ * if the request has failed.
+ * @throws NullPointerException if request is {@code null}.
*/
- default CompletionStage unsubscribe(UUri topic, UListener listener) {
- return unsubscribe(topic, listener, CallOptions.DEFAULT);
- }
+ CompletionStage unsubscribe(UnsubscribeRequest request);
/**
- * Unsubscribes from a given topic.
+ * Fetches a list of subscribers that are currently subscribed to a given topic.
*
- * The subscriber no longer wishes to be subscribed to said topic so we issue a unsubscribe
- * request to the USubscription service. The API will return a {@link CompletionStage} with the
- * {@link UStatus} of the result. If we are unable to unsubscribe to the topic with USubscription
- * service, the listener and handler (if any) will remain registered.
- *
- * @param topic The topic to unsubscribe to.
- * @param listener The listener to be called when a message is received on the topic.
- * @param options The {@link CallOptions} to be used for the unsubscribe request.
- * @return Returns {@link UStatus} with the result from the unsubscribe request.
+ * @param request The request to send.
+ * @return The outcome of the operation. The stage will be completed with a {@link UStatusException}
+ * if the request has failed.
+ * @throws NullPointerException if request is {@code null}.
*/
- CompletionStage unsubscribe(UUri topic, UListener listener, CallOptions options);
-
+ CompletionStage fetchSubscribers(FetchSubscribersRequest request);
/**
- * Unregister a listener and removes any registered {@link SubscriptionChangeHandler} for the topic.
- *
- * This method is used to remove handlers/listeners without notifying the uSubscription service
- * so that we can be persistently subscribed even when the uE is not running.
+ * Fetches all subscriptions for a given topic or subscriber.
+ *
+ * API provides more information than {@code #fetchSubscribers(UUri)} in that it also returns
+ * {@link SubscribeAttributes} per subscriber that might be useful to the producer to know.
*
- * @param topic The topic to subscribe to.
- * @param listener The listener to be called when a message is received on the topic.
- * @return Returns {@link UStatus} with the status of the listener unregister request.
+ * @param request The request to send.
+ * @return The outcome of the operation. The stage will be completed with a {@link UStatusException}
+ * if the request has failed.
+ * @throws NullPointerException if request is {@code null}.
*/
- CompletionStage unregisterListener(UUri topic, UListener listener);
-
+ CompletionStage fetchSubscriptions(FetchSubscriptionsRequest request);
/**
- * Register for Subscription Change Notifications.
- *
+ * Registers for notifications about changes to the subscription status for a given topic.
+ *
* This API allows producers to register to receive subscription change notifications for
* topics that they produce only.
*
* NOTE: Subscribers are automatically registered to receive notifications when they call
- * {@code subscribe()} API passing a {@link SubscriptionChangeHandler} so they do not need to
- * call this API.
+ * {@link #subscribe(SubscriptionRequest)}.
*
- * @param topic The topic to register for notifications.
- * @param handler The {@link SubscriptionChangeHandler} to handle the subscription changes.
- * @return {@link CompletionStage} completed successfully if uSubscription service accepts the
- * request to register the caller to be notified of subscription changes, or
- * the CompletionStage completes exceptionally with {@link UStatus} that indicates
- * the failure reason.
+ * @param request The request to send.
+ * @return The outcome of the operation. The stage will be completed with a {@link UStatusException}
+ * if the request has failed.
+ * @throws NullPointerException if request is {@code null}.
*/
- default CompletionStage registerForNotifications(UUri topic,
- SubscriptionChangeHandler handler) {
- return registerForNotifications(topic, handler, CallOptions.DEFAULT);
- }
-
+ CompletionStage registerForNotifications(NotificationsRequest request);
/**
- * Register for Subscription Change Notifications.
- *
- * This API allows producers to register to receive subscription change notifications for
- * topics that they produce only.
- *
- * NOTE: Subscribers are automatically registered to receive notifications when they call
- * {@code subscribe()} API passing a {@link SubscriptionChangeHandler} so they do not need to
- * call this API.
- *
- * @param topic The topic to register for notifications.
- * @param handler The {@link SubscriptionChangeHandler} to handle the subscription changes.
- * @param options The {@link CallOptions} to be used for the request.
- * @return {@link CompletionStage} completed successfully if uSubscription service accepts the
- * request to register the caller to be notified of subscription changes, or
- * the CompletionStage completes exceptionally with {@link UStatus} that indicates
- * the failure reason.
- */
- CompletionStage registerForNotifications(UUri topic,
- SubscriptionChangeHandler handler, CallOptions options);
-
-
- /**
- * Unregister for subscription change notifications.
- *
- * @param topic The topic to unregister for notifications.
- * @return {@link CompletionStage} completed successfully with {@link NotificationsResponse} with
- * the status of the API call to uSubscription service, or completed unsuccessfully with
- * {@link UStatus} with the reason for the failure.
- */
- default CompletionStage unregisterForNotifications(UUri topic) {
- return unregisterForNotifications(topic, CallOptions.DEFAULT);
- }
-
-
- /**
- * Unregister for subscription change notifications.
- *
- * @param topic The topic to unregister for notifications.
- * @param options The {@link CallOptions} to be used for the request.
- * @return {@link CompletionStage} completed successfully with {@link NotificationsResponse} with
- * the status of the API call to uSubscription service, or completed unsuccessfully with
- * {@link UStatus} with the reason for the failure.
- */
- CompletionStage unregisterForNotifications(UUri topic, CallOptions options);
-
-
- /**
- * Fetch the list of subscribers for a given produced topic.
- *
- * @param topic The topic to fetch the subscribers for.
- * @return {@link CompletionStage} completed successfully with {@link FetchSubscribersResponse} with
- * the list of subscribers, or completed unsuccessfully with {@link UStatus} with the reason
- * for the failure.
- */
- default CompletionStage fetchSubscribers(UUri topic) {
- return fetchSubscribers(topic, CallOptions.DEFAULT);
- }
-
-
- /**
- * Fetch the list of subscribers for a given produced topic.
- *
- * @param topic The topic to fetch the subscribers for.
- * @param options The {@link CallOptions} to be used for the request.
- * @return {@link CompletionStage} completed successfully with {@link FetchSubscribersResponse} with
- * the list of subscribers, or completed unsuccessfully with {@link UStatus} with the reason
- * for the failure.
- */
- CompletionStage fetchSubscribers(UUri topic, CallOptions options);
-
-
- /**
- * Fetch list of Subscriptions for a given topic.
- *
- * API provides more information than {@code #fetchSubscribers(UUri)} in that it also returns
- * {@link SubscribeAttributes} per subscriber that might be useful to the producer to know.
- *
- * @param request The request containing the topic to fetch subscriptions for.
- * @return {@link CompletionStage} completed successfully with {@link FetchSubscriptionsResponse} that
- * contains the subscription information per subscriber to the topic or completed unsuccessfully with
- * {@link UStatus} with the reason for the failure. {@link UCode#PERMISSION_DENIED} is returned if the
- * topic ue_id does not equal the callers ue_id.
- */
- default CompletionStage fetchSubscriptions(FetchSubscriptionsRequest request) {
- return fetchSubscriptions(request, CallOptions.DEFAULT);
- }
-
-
- /**
- * Fetch list of Subscriptions for a given topic.
- *
- * API provides more information than {@code fetchSubscribers()} in that it also returns
- * {@link SubscribeAttributes} per subscriber that might be useful to the producer to know.
+ * Cancels a registration for subscription change notifications.
*
- * @param request The request containing the topic to fetch subscriptions for.
- * @param options The {@link CallOptions} to be used for the request.
- * @return {@link CompletionStage} completed successfully with {@link FetchSubscriptionsResponse} that
- * contains the subscription information per subscriber to the topic or completed unsuccessfully with
- * {@link UStatus} with the reason for the failure. {@link UCode#PERMISSION_DENIED} is returned if the
- * topic ue_id does not equal the callers ue_id.
+ * @param request The request to send.
+ * @return The outcome of the operation. The stage will be completed with a {@link UStatusException}
+ * if the request has failed.
+ * @throws NullPointerException if request is {@code null}.
*/
- CompletionStage fetchSubscriptions(FetchSubscriptionsRequest request,
- CallOptions options);
+ CompletionStage unregisterForNotifications(NotificationsRequest request);
}
diff --git a/src/main/java/org/eclipse/uprotocol/communication/AbstractCommunicationLayerClient.java b/src/main/java/org/eclipse/uprotocol/communication/AbstractCommunicationLayerClient.java
new file mode 100644
index 00000000..567a84a5
--- /dev/null
+++ b/src/main/java/org/eclipse/uprotocol/communication/AbstractCommunicationLayerClient.java
@@ -0,0 +1,36 @@
+/**
+ * SPDX-FileCopyrightText: 2025 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Apache License Version 2.0 which is available at
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package org.eclipse.uprotocol.communication;
+
+import java.util.Objects;
+
+import org.eclipse.uprotocol.transport.LocalUriProvider;
+import org.eclipse.uprotocol.transport.UTransport;
+
+public class AbstractCommunicationLayerClient {
+ private final UTransport transport;
+ private final LocalUriProvider uriProvider;
+
+ protected AbstractCommunicationLayerClient(UTransport transport, LocalUriProvider uriProvider) {
+ this.transport = Objects.requireNonNull(transport);
+ this.uriProvider = Objects.requireNonNull(uriProvider);
+ }
+
+ protected UTransport getTransport() {
+ return transport;
+ }
+
+ protected LocalUriProvider getUriProvider() {
+ return uriProvider;
+ }
+}
diff --git a/src/main/java/org/eclipse/uprotocol/communication/CallOptions.java b/src/main/java/org/eclipse/uprotocol/communication/CallOptions.java
index bfe66b91..2c6d5b9b 100644
--- a/src/main/java/org/eclipse/uprotocol/communication/CallOptions.java
+++ b/src/main/java/org/eclipse/uprotocol/communication/CallOptions.java
@@ -13,7 +13,9 @@
package org.eclipse.uprotocol.communication;
import java.util.Objects;
+import java.util.Optional;
+import org.eclipse.uprotocol.transport.builder.UMessageBuilder;
import org.eclipse.uprotocol.v1.UPriority;
/**
@@ -23,7 +25,7 @@ public record CallOptions (Integer timeout, UPriority priority, String token) {
public static final int TIMEOUT_DEFAULT = 10000; // Default timeout of 10 seconds
// Default instance.
- public static final CallOptions DEFAULT = new CallOptions(TIMEOUT_DEFAULT, UPriority.UPRIORITY_CS4, "");
+ public static final CallOptions DEFAULT = new CallOptions(TIMEOUT_DEFAULT, UPriority.UPRIORITY_CS4, null);
/**
* Check to ensure CallOptions is not null.
@@ -31,7 +33,6 @@ public record CallOptions (Integer timeout, UPriority priority, String token) {
public CallOptions {
Objects.requireNonNull(timeout);
Objects.requireNonNull(priority);
- Objects.requireNonNull(token);
}
/**
@@ -41,7 +42,7 @@ public record CallOptions (Integer timeout, UPriority priority, String token) {
* @param priority The priority of the method invocation.
*/
public CallOptions(Integer timeout, UPriority priority) {
- this(timeout, priority, "");
+ this(timeout, priority, null);
}
/**
@@ -50,13 +51,24 @@ public CallOptions(Integer timeout, UPriority priority) {
* @param timeout The timeout for the method invocation.
*/
public CallOptions(Integer timeout) {
- this(timeout, UPriority.UPRIORITY_CS4, "");
+ this(timeout, UPriority.UPRIORITY_CS4, null);
}
/**
* Constructor for CallOptions.
*/
public CallOptions() {
- this(TIMEOUT_DEFAULT, UPriority.UPRIORITY_CS4, "");
+ this(TIMEOUT_DEFAULT, UPriority.UPRIORITY_CS4, null);
+ }
+
+ /**
+ * Adds these call options to a message.
+ *
+ * @param builder The message builder.
+ */
+ public void applyToMessage(UMessageBuilder builder) {
+ Optional.ofNullable(this.priority()).ifPresent(builder::withPriority);
+ Optional.ofNullable(this.timeout()).ifPresent(builder::withTtl);
+ Optional.ofNullable(this.token()).ifPresent(builder::withToken);
}
}
diff --git a/src/main/java/org/eclipse/uprotocol/communication/InMemoryRpcClient.java b/src/main/java/org/eclipse/uprotocol/communication/InMemoryRpcClient.java
index 7f13bb62..e1525e37 100644
--- a/src/main/java/org/eclipse/uprotocol/communication/InMemoryRpcClient.java
+++ b/src/main/java/org/eclipse/uprotocol/communication/InMemoryRpcClient.java
@@ -12,11 +12,17 @@
*/
package org.eclipse.uprotocol.communication;
+import java.util.Map;
import java.util.Objects;
+import java.util.Optional;
import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+
+import org.eclipse.uprotocol.transport.LocalUriProvider;
import org.eclipse.uprotocol.transport.UListener;
import org.eclipse.uprotocol.transport.UTransport;
import org.eclipse.uprotocol.transport.builder.UMessageBuilder;
@@ -39,52 +45,58 @@
* or directly use the {@link UTransport} to send RPC requests and register listeners that
* handle the RPC responses.
*/
-public class InMemoryRpcClient implements RpcClient {
- // The transport to use for sending the RPC requests
- private final UTransport transport;
-
+public class InMemoryRpcClient extends AbstractCommunicationLayerClient implements RpcClient {
// Map to store the futures that needs to be completed when the response comes in
- private final ConcurrentHashMap> mRequests = new ConcurrentHashMap<>();
+ private final Map> mRequests = new ConcurrentHashMap<>();
// Generic listener to handle all RPC response messages
- private final UListener mResponseHandler = this::handleResponses;
+ private final UListener mResponseHandler = this::handleResponse;
+
+ private Consumer unexpectedMessageHandler;
-
/**
- * Constructor for the DefaultRpcClient.
- *
- * @param transport the transport to use for sending the RPC requests
+ * Creates a client for a transport.
+ *
+ * @param transport The transport to use for sending the RPC requests.
+ * @param uriProvider The helper for creating URIs that represent local resources.
+ * @throws NullPointerException if any of the arguments are {@code null}.
+ * @throws CompletionException if registration of the response listener fails.
*/
- public InMemoryRpcClient (UTransport transport) {
- Objects.requireNonNull(transport, UTransport.TRANSPORT_NULL_ERROR);
- this.transport = transport;
-
- transport.registerListener(UriFactory.ANY,
- transport.getSource(), mResponseHandler).toCompletableFuture().join();
- }
+ public InMemoryRpcClient(UTransport transport, LocalUriProvider uriProvider) {
+ super(transport, uriProvider);
+ getTransport().registerListener(
+ UriFactory.ANY,
+ getUriProvider().getSource(),
+ mResponseHandler)
+ .toCompletableFuture().join();
+ }
/**
- * Invoke a method (send an RPC request) and receive the response
- * (the returned {@link CompletionStage} {@link UPayload}.
- *
- * @param methodUri The method URI to be invoked.
- * @param requestPayload The request message to be sent to the server.
- * @param options RPC method invocation call options, see {@link CallOptions}
- * @return Returns the CompletionStage with the response payload or exception with the failure
- * reason as {@link UStatus}.
+ * Sets a handler to be invoked for unexpected inbound messages.
+ *
+ * @param unexpectedResponseHandler A handler to invoke for incoming messages that cannot
+ * be processed, either because they are no RPC response messages or because they contain
+ * an unknown request ID.
*/
+ void setUnexpectedMessageHandler(Consumer handler) {
+ this.unexpectedMessageHandler = handler;
+ }
+
@Override
public CompletionStage invokeMethod(UUri methodUri, UPayload requestPayload, CallOptions options) {
- options = Objects.requireNonNullElse(options, CallOptions.DEFAULT);
- UMessageBuilder builder = UMessageBuilder.request(transport.getSource(), methodUri, options.timeout());
- UMessage request;
-
- if (!options.token().isBlank()) {
- builder.withToken(options.token());
- }
- // Build a request uMessage
- request = builder.build(requestPayload);
+ Objects.requireNonNull(methodUri, "Method URI cannot be null");
+ Objects.requireNonNull(requestPayload, "Request payload cannot be null");
+ Objects.requireNonNull(options, "Call options cannot be null");
+
+ UMessageBuilder builder = UMessageBuilder.request(getUriProvider().getSource(), methodUri, options.timeout());
+ Optional.ofNullable(options.priority()).ifPresent(priority -> builder.withPriority(priority));
+ Optional.ofNullable(options.token())
+ .filter(s -> !s.isBlank())
+ .ifPresent(token -> builder.withToken(token));
+
+ // Build the request message
+ final UMessage request = builder.build(requestPayload);
// Create the response future and store it in mRequests
CompletableFuture responseFuture = new CompletableFuture()
@@ -99,51 +111,48 @@ public CompletionStage invokeMethod(UUri methodUri, UPayload requestPa
});
// Send the request
- CompletionStage status = transport.send(request);
-
- return status.thenApply(s -> {
- if (s.getCode() != UCode.OK) throw new UStatusException(s);
- return s;
- }).thenCompose(s -> responseFuture.thenApply(responseMessage ->
- UPayload.pack(responseMessage.getPayload(), responseMessage.getAttributes().getPayloadFormat())
- ));
+ return getTransport().send(request)
+ .thenCompose(s -> responseFuture)
+ .thenApply(responseMessage -> UPayload.pack(
+ responseMessage.getPayload(),
+ responseMessage.getAttributes().getPayloadFormat())
+ );
}
-
/**
* Close the RPC client and clean up any resources.
*/
public void close() {
mRequests.clear();
- transport.unregisterListener(UriFactory.ANY, transport.getSource(), mResponseHandler);
+ getTransport().unregisterListener(UriFactory.ANY, getUriProvider().getSource(), mResponseHandler);
}
- /**
- * Handle the responses coming back from the server.
- *
- * @param response The response message from the server
- */
- private void handleResponses(UMessage response) {
- // Only handle responses messages, ignore all other messages like notifications
- if (response.getAttributes().getType() != UMessageType.UMESSAGE_TYPE_RESPONSE) {
+ private void handleResponse(UMessage message) {
+ // Only handle responses messages
+ if (message.getAttributes().getType() != UMessageType.UMESSAGE_TYPE_RESPONSE) {
+ Optional.ofNullable(unexpectedMessageHandler).ifPresent(handler -> handler.accept(message));
return;
}
- final UAttributes responseAttributes = response.getAttributes();
+ final UAttributes responseAttributes = message.getAttributes();
// Check if the response is for a request we made, if not then ignore it
final CompletableFuture responseFuture = mRequests.remove(responseAttributes.getReqid());
if (responseFuture == null) {
+ Optional.ofNullable(unexpectedMessageHandler).ifPresent(handler -> handler.accept(message));
return;
}
// Check if the response has a commstatus and if it is not OK then complete the future with an exception
if (responseAttributes.hasCommstatus() && responseAttributes.getCommstatus() != UCode.OK) {
- final UCode code = responseAttributes.getCommstatus();
- responseFuture.completeExceptionally(
- new UStatusException(code, "Communication error [" + code + "]"));
- return;
+ // first, try to extract error details from payload
+ final var exception = UPayload.unpack(message, UStatus.class)
+ .map(UStatusException::new)
+ // fall back to a generic error based on commstatus
+ .orElseGet(() -> new UStatusException(responseAttributes.getCommstatus(), "Communication error"));
+ responseFuture.completeExceptionally(exception);
+ } else {
+ responseFuture.complete(message);
}
- responseFuture.complete(response);
}
}
diff --git a/src/main/java/org/eclipse/uprotocol/communication/InMemoryRpcServer.java b/src/main/java/org/eclipse/uprotocol/communication/InMemoryRpcServer.java
index 557371ea..66402dc5 100644
--- a/src/main/java/org/eclipse/uprotocol/communication/InMemoryRpcServer.java
+++ b/src/main/java/org/eclipse/uprotocol/communication/InMemoryRpcServer.java
@@ -12,21 +12,29 @@
*/
package org.eclipse.uprotocol.communication;
+import java.util.Map;
import java.util.Objects;
+import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Consumer;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.CompletableFuture;
+import org.eclipse.uprotocol.transport.LocalUriProvider;
import org.eclipse.uprotocol.transport.UListener;
import org.eclipse.uprotocol.transport.UTransport;
import org.eclipse.uprotocol.transport.builder.UMessageBuilder;
import org.eclipse.uprotocol.uri.factory.UriFactory;
+import org.eclipse.uprotocol.uri.serializer.UriSerializer;
+import org.eclipse.uprotocol.uri.validator.UriValidator;
import org.eclipse.uprotocol.v1.UAttributes;
import org.eclipse.uprotocol.v1.UCode;
import org.eclipse.uprotocol.v1.UMessage;
import org.eclipse.uprotocol.v1.UMessageType;
import org.eclipse.uprotocol.v1.UStatus;
import org.eclipse.uprotocol.v1.UUri;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/**
@@ -35,152 +43,161 @@
* to register handlers for processing RPC requests from clients. This implementation
* uses an in-memory map to store the request handlers that needs to be invoked when the
* request comes in from the client.
- *
- * *NOTE:* Developers are not required to use these APIs, they can implement their own
- * or directly use the {@link UTransport} to register listeners that handle
- * RPC requests and send RPC responses.
+ *
+ * NOTE: Developers are not required to use these APIs, they can implement their own
+ * or directly use a {@link UTransport} to register listeners that handle
+ * RPC requests and send RPC responses.
*/
-public class InMemoryRpcServer implements RpcServer {
- // The transport to use for sending the RPC requests
- private final UTransport transport;
+public class InMemoryRpcServer extends AbstractCommunicationLayerClient implements RpcServer {
+ private static final Logger LOGGER = LoggerFactory.getLogger(InMemoryRpcServer.class);
+
+ protected static final String REQUEST_HANDLER_ERROR_MESSAGE = "Failed to handle RPC request";
// Map to store the request handlers so we can handle the right request on the server side
- private final ConcurrentHashMap mRequestsHandlers = new ConcurrentHashMap<>();
+ private final Map mRequestsHandlers = new ConcurrentHashMap<>();
// Generic listener to handle all RPC request messages
- private final UListener mRequestHandler = this::handleRequests;
+ private final UListener mRequestHandler = this::handleRequest;
+ private Consumer unexpectedMessageHandler;
+ private Consumer sendResponseErrorHandler;
/**
- * Constructor for the DefaultRpcServer.
- *
- * @param transport the transport to use for sending the RPC requests
+ * Creates a new server for a transport.
+ *
+ * @param transport The transport to use for receiving RPC requests and
+ * sending RPC responses.
+ * @param uriProvider The URI provider to use for generating local resource URIs.
+ * @throws NullPointerException if transport is {@code null}.
*/
- public InMemoryRpcServer (UTransport transport) {
- Objects.requireNonNull(transport, UTransport.TRANSPORT_NULL_ERROR);
- this.transport = transport;
+ public InMemoryRpcServer (UTransport transport, LocalUriProvider uriProvider) {
+ super(transport, uriProvider);
}
-
/**
- * Register a handler that will be invoked when when requests come in from clients for the given method.
+ * Sets the handler to invoke when an unexpected message is received.
*
- * Note: Only one handler is allowed to be registered per method URI.
+ * @param handler The handler to invoke when an unexpected message is received.
+ */
+ void setUnexpectedMessageHandler(Consumer handler) {
+ this.unexpectedMessageHandler = handler;
+ }
+
+ /**
+ * Sets the handler to invoke when sending an RPC response fails.
*
- * @param method Uri for the method to register the listener for.
- * @param handler The handler that will process the request for the client.
- * @return Returns the status of registering the RpcListener.
+ * @param handler The handler to invoke when sending an RPC response fails.
*/
+ void setSendErrorHandler(Consumer handler) {
+ this.sendResponseErrorHandler = handler;
+ }
+
@Override
- public CompletionStage registerRequestHandler(UUri method, RequestHandler handler) {
- if (method == null || handler == null) {
- return CompletableFuture.completedFuture(
- UStatus.newBuilder()
- .setCode(UCode.INVALID_ARGUMENT)
- .setMessage("Method URI or handler missing")
- .build());
+ public CompletionStage registerRequestHandler(UUri originFilter, int resourceId, RequestHandler handler) {
+ Objects.requireNonNull(originFilter, "Origin filter must not be null");
+ Objects.requireNonNull(handler, "Request handler must not be null");
+
+ // create the method URI for where we want to register the listener
+ final var method = UUri.newBuilder(getUriProvider().getSource())
+ .setResourceId(resourceId)
+ .build();
+ if (!UriValidator.isRpcMethod(method)) {
+ return CompletableFuture.failedFuture(new UStatusException(
+ UCode.INVALID_ARGUMENT, "Resource ID must be an RPC method ID"));
}
-
- // Ensure the method URI matches the transport source URI
- if (!method.getAuthorityName().equals(transport.getSource().getAuthorityName()) ||
- method.getUeId() != transport.getSource().getUeId() ||
- method.getUeVersionMajor() != transport.getSource().getUeVersionMajor()) {
- return CompletableFuture.completedFuture(
- UStatus.newBuilder()
- .setCode(UCode.INVALID_ARGUMENT)
- .setMessage("Method URI does not match the transport source URI")
- .build());
- }
- try {
- mRequestsHandlers.compute(method, (key, currentHandler) -> {
- if (currentHandler != null) {
- throw new UStatusException(UCode.ALREADY_EXISTS, "Handler already registered");
- }
-
- UStatus status = transport.registerListener(UriFactory.ANY, method, mRequestHandler)
- .toCompletableFuture().join();
- if (status.getCode() != UCode.OK) {
- throw new UStatusException(status);
- }
- return handler;
- });
- return CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build());
- } catch (UStatusException e) {
- return CompletableFuture.completedFuture(e.getStatus());
+
+ synchronized (mRequestsHandlers) {
+ if (mRequestsHandlers.containsKey(method)) {
+ return CompletableFuture.failedFuture(new UStatusException(
+ UCode.ALREADY_EXISTS, "Handler already registered"));
+ }
+ return getTransport().registerListener(UriFactory.ANY, method, mRequestHandler)
+ .whenComplete((ok, throwable) -> {
+ if (throwable != null) {
+ mRequestsHandlers.remove(method);
+ } else {
+ mRequestsHandlers.put(method, handler);
+ }
+ });
}
}
-
- /**
- * Unregister a handler that will be invoked when when requests come in from clients for the given method.
- *
- * @param method Resolved UUri for where the listener was registered to receive messages from.
- * @param handler The handler for processing requests
- * @return Returns status of registering the RpcListener.
- */
@Override
- public CompletionStage unregisterRequestHandler(UUri method, RequestHandler handler) {
- if (method == null || handler == null) {
- return CompletableFuture.completedFuture(
- UStatus.newBuilder()
- .setCode(UCode.INVALID_ARGUMENT)
- .setMessage("Method URI or handler missing")
- .build());
- }
-
- // Ensure the method URI matches the transport source URI
- if (!method.getAuthorityName().equals(transport.getSource().getAuthorityName()) ||
- method.getUeId() != transport.getSource().getUeId() ||
- method.getUeVersionMajor() != transport.getSource().getUeVersionMajor()) {
- return CompletableFuture.completedFuture(
- UStatus.newBuilder()
- .setCode(UCode.INVALID_ARGUMENT)
- .setMessage("Method URI does not match the transport source URI")
- .build());
+ public CompletionStage unregisterRequestHandler(
+ UUri originFilter,
+ int resourceId,
+ RequestHandler handler) {
+ Objects.requireNonNull(originFilter, "Origin filter must not be null");
+ Objects.requireNonNull(handler, "Request handler must not be null");
+
+ final var method = UUri.newBuilder(getUriProvider().getSource())
+ .setResourceId(resourceId)
+ .build();
+ if (!UriValidator.isRpcMethod(method)) {
+ return CompletableFuture.failedFuture(new UStatusException(
+ UCode.INVALID_ARGUMENT, "Resource ID must be an RPC method ID"));
}
if (mRequestsHandlers.remove(method, handler)) {
- return transport.unregisterListener(UriFactory.ANY, method, mRequestHandler);
+ return getTransport().unregisterListener(UriFactory.ANY, method, mRequestHandler);
+ } else {
+ return CompletableFuture.failedFuture(new UStatusException(
+ UCode.NOT_FOUND, "Handler not found"));
}
-
- return CompletableFuture.completedFuture(
- UStatus.newBuilder().setCode(UCode.NOT_FOUND).setMessage("Handler not found").build());
}
-
/**
* Generic incoming handler to process RPC requests from clients
* @param request The request message from clients
*/
- private void handleRequests(UMessage request) {
+ private void handleRequest(UMessage request) {
+ final UAttributes requestAttributes = request.getAttributes();
+
// Only handle request messages, ignore all other messages like notifications
- if (request.getAttributes().getType() != UMessageType.UMESSAGE_TYPE_REQUEST) {
+ if (requestAttributes.getType() != UMessageType.UMESSAGE_TYPE_REQUEST) {
+ Optional.ofNullable(unexpectedMessageHandler).ifPresent(handler -> handler.accept(request));
return;
}
-
- final UAttributes requestAttributes = request.getAttributes();
-
+
// Check if the request is for one that we have registered a handler for, if not ignore it
- final RequestHandler handler = mRequestsHandlers.get(requestAttributes.getSink());
- if (handler == null) {
+ final var requestHandler = mRequestsHandlers.get(requestAttributes.getSink());
+ if (requestHandler == null) {
+ Optional.ofNullable(unexpectedMessageHandler).ifPresent(handler -> handler.accept(request));
return;
}
UPayload responsePayload;
- UMessageBuilder responseBuilder = UMessageBuilder.response(request.getAttributes());
+ final UMessageBuilder responseBuilder = UMessageBuilder.response(request.getAttributes());
try {
- responsePayload = handler.handleRequest(request);
+ responsePayload = requestHandler.handleRequest(request);
+ } catch (UStatusException e) {
+ responseBuilder.withCommStatus(e.getStatus().getCode());
+ responsePayload = UPayload.pack(e.getStatus());
} catch (Exception e) {
- UCode code = UCode.INTERNAL;
- responsePayload = null;
- if (e instanceof UStatusException statusException) {
- code = statusException.getStatus().getCode();
+ if (LOGGER.isInfoEnabled()) {
+ LOGGER.info("""
+ RPC RequestHandler threw unexpected exception while processing RPC request \
+ [source: {}, sink: {}]: {}""",
+ UriSerializer.serialize(request.getAttributes().getSource()),
+ UriSerializer.serialize(request.getAttributes().getSink()),
+ e.getMessage());
}
- responseBuilder.withCommStatus(code);
+
+ final var status = UStatus.newBuilder()
+ .setCode(UCode.INTERNAL)
+ .setMessage(REQUEST_HANDLER_ERROR_MESSAGE)
+ .build();
+ responseBuilder.withCommStatus(status.getCode());
+ responsePayload = UPayload.pack(status);
}
- // TODO: Handle error sending the response
- transport.send(responseBuilder.build(responsePayload));
+ final var responseMessage = responseBuilder.build(responsePayload);
+ getTransport().send(responseMessage)
+ .whenComplete((ok, t) -> {
+ if (t != null) {
+ Optional.ofNullable(sendResponseErrorHandler).ifPresent(handler -> handler.accept(t));
+ }
+ });
}
}
diff --git a/src/main/java/org/eclipse/uprotocol/communication/InMemorySubscriber.java b/src/main/java/org/eclipse/uprotocol/communication/InMemorySubscriber.java
new file mode 100644
index 00000000..d0362045
--- /dev/null
+++ b/src/main/java/org/eclipse/uprotocol/communication/InMemorySubscriber.java
@@ -0,0 +1,325 @@
+/**
+ * SPDX-FileCopyrightText: 2024 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Apache License Version 2.0 which is available at
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package org.eclipse.uprotocol.communication;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Consumer;
+
+import org.eclipse.uprotocol.client.usubscription.v3.RpcClientBasedUSubscriptionClient;
+import org.eclipse.uprotocol.client.usubscription.v3.USubscriptionClient;
+import org.eclipse.uprotocol.core.usubscription.v3.NotificationsRequest;
+import org.eclipse.uprotocol.core.usubscription.v3.NotificationsResponse;
+import org.eclipse.uprotocol.core.usubscription.v3.SubscriptionRequest;
+import org.eclipse.uprotocol.core.usubscription.v3.SubscriptionResponse;
+import org.eclipse.uprotocol.core.usubscription.v3.UnsubscribeRequest;
+import org.eclipse.uprotocol.core.usubscription.v3.UnsubscribeResponse;
+import org.eclipse.uprotocol.core.usubscription.v3.Update;
+import org.eclipse.uprotocol.transport.LocalUriProvider;
+import org.eclipse.uprotocol.transport.UListener;
+import org.eclipse.uprotocol.transport.UTransport;
+import org.eclipse.uprotocol.uri.serializer.UriSerializer;
+import org.eclipse.uprotocol.v1.UCode;
+import org.eclipse.uprotocol.v1.UMessage;
+import org.eclipse.uprotocol.v1.UMessageType;
+import org.eclipse.uprotocol.v1.UUri;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A Subscriber which keeps all information about registered subscription change handlers in memory.
+ *
+ * The subscriber requires a {@link USubscriptionClient implementation} in order to inform a
+ * USubscription service instance about newly subscribed and unsubscribed topics. It also needs a
+ * {@link Notifier} for receiving notifications about subscription status updates from the USubscription
+ * service. Finally, it needs a {@link UTransport} for receiving events that have been published to
+ * subscribed topics.
+ *
+ * During startup the subscriber uses the Notifier to register a generic {@link UListener} for receiving
+ * notifications from the USubscription service. The listener maintains an in-memory mapping
+ * of subscribed topics to corresponding subscription change handlers.
+ *
+ * When a client {@link #subscribe(UUri, UListener, Optional) subscribes to a topic}, the USubscription
+ * service is informed about the new subscription and a (client provided) subscription change handler is
+ * registered with the listener. When a subscription change notification arrives from the USubscription
+ * service, the corresponding handler is being looked up and invoked.
+ */
+public final class InMemorySubscriber implements Subscriber {
+ private static final Logger LOGGER = LoggerFactory.getLogger(InMemorySubscriber.class);
+
+ private final UTransport transport;
+ private final USubscriptionClient subscriptionClient;
+ private final Notifier notifier;
+
+ // topic URI -> subscription change notification handler
+ private final Map subscriptionChangeHandlers = new ConcurrentHashMap<>();
+
+ // listener for processing subscription change notifications
+ private final UListener subscriptionChangeListener = this::handleSubscriptionChangeNotification;
+
+ private Consumer unexpectedMessageHandler;
+
+ /**
+ * Creates a new USubscription client passing {@link UTransport} and {@link CallOptions}
+ * used to provide additional options for the RPC requests to uSubscription service.
+ *
+ * @param transport The transport to use for sending the notifications
+ * @param uriProvider The URI provider to use for generating local resource URIs.
+ * @param options The call options to use for the RPC requests.
+ * @param subscriptionServiceInstanceId The instance of the subscription service to invoke,
+ * {@code 0x000} to use the default instance.
+ * @param subscriptionServiceAuthority The authority that the subscription service runs on,
+ * or {@code null} if the instance runs on the local authority.
+ */
+ public InMemorySubscriber(
+ UTransport transport,
+ LocalUriProvider uriProvider,
+ CallOptions options,
+ int subscriptionServiceInstanceId,
+ String subscriptionServiceAuthority) {
+ this(
+ transport,
+ new RpcClientBasedUSubscriptionClient(
+ new InMemoryRpcClient(transport, uriProvider),
+ options,
+ subscriptionServiceInstanceId,
+ subscriptionServiceAuthority
+ ),
+ new SimpleNotifier(transport, uriProvider));
+ }
+
+ /**
+ * Creates a new USubscription client.
+ *
+ * Also registers a listener for subscription change notifications from the USubscription service
+ * instance that the given USubscription client is
+ * {@link USubscriptionClient#getSubscriptionServiceNotificationTopic() configured to use}.
+ *
+ * @param transport The transport to use for sending the notifications.
+ * @param subscriptionClient The client to use for interacting with the USubscription service.
+ * @param notifier The notifier to use for registering the notification listener.
+ */
+ public InMemorySubscriber (
+ UTransport transport,
+ USubscriptionClient subscriptionClient,
+ Notifier notifier) {
+ Objects.requireNonNull(transport, "Transport missing");
+ Objects.requireNonNull(subscriptionClient, "SubscriptionClient missing");
+ Objects.requireNonNull(notifier, "Notifier missing");
+ this.transport = transport;
+ this.subscriptionClient = subscriptionClient;
+ this.notifier = notifier;
+
+ // Register listener for receiving subscription change notifications
+ notifier.registerNotificationListener(
+ subscriptionClient.getSubscriptionServiceNotificationTopic(),
+ subscriptionChangeListener)
+ .toCompletableFuture().join();
+ }
+
+ void setUnexpectedMessageHandler(Consumer handler) {
+ this.unexpectedMessageHandler = handler;
+ }
+
+ /**
+ * Closes this client and cleans up resources.
+ */
+ public void close() {
+ subscriptionChangeHandlers.clear();
+ try {
+ notifier.unregisterNotificationListener(
+ subscriptionClient.getSubscriptionServiceNotificationTopic(),
+ subscriptionChangeListener)
+ .toCompletableFuture().join();
+ } catch (CompletionException e) {
+ LOGGER.debug("error while unregistering listener for subscription change notifications", e);
+ }
+ }
+
+ void addSubscriptionChangeHandler(
+ UUri topic,
+ Optional handler) {
+
+ handler.ifPresent(newHandler -> {
+
+ subscriptionChangeHandlers.compute(topic, (k, existingHandler) -> {
+ if (existingHandler != null && existingHandler != newHandler) {
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug(
+ "Subscription state notification handler already registered for topic [{}]",
+ UriSerializer.serialize(topic));
+ }
+ throw new UStatusException(
+ UCode.ALREADY_EXISTS,
+ "Subscription state notification handler already registered");
+ }
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug(
+ "Registering subscription state notification handler for topic [{}]",
+ UriSerializer.serialize(topic));
+ }
+ return newHandler;
+ });
+ });
+ }
+
+ boolean hasSubscriptionChangeHandler(UUri topic) {
+ return subscriptionChangeHandlers.containsKey(topic);
+ }
+
+ @Override
+ public CompletionStage subscribe(
+ UUri topic,
+ UListener listener,
+ Optional handler) {
+ Objects.requireNonNull(topic, "Subscribe topic missing");
+ Objects.requireNonNull(listener, "Request listener missing");
+
+ final var request = SubscriptionRequest.newBuilder().setTopic(topic).build();
+
+ return subscriptionClient.subscribe(request)
+ // add the subscription change handler (if the client provided one) so the client
+ // can be notified of changes to the subscription state.
+ .thenApply(subscriptionResponse -> {
+ addSubscriptionChangeHandler(topic, handler);
+ return subscriptionResponse;
+ })
+ .thenCompose(subscriptionResponse -> {
+ switch (subscriptionResponse.getStatus().getState()) {
+ case SUBSCRIBED:
+ case SUBSCRIBE_PENDING:
+ return transport.registerListener(topic, listener)
+ .thenApply(ok -> subscriptionResponse)
+ .exceptionallyCompose(t -> {
+ // When registering the listener fails, we have ended up in a situation where we
+ // have successfully (logically) subscribed to the topic via the USubscription service
+ // but we have not been able to register the listener with the local transport.
+ // This means that events might start getting forwarded to the local authority which
+ // are not being consumed. Apart from this inefficiency, this does not pose a real
+ // problem and since we return a failed future, the client might be inclined to try
+ // again and (eventually) succeed in registering the listener as well.
+ if (LOGGER.isWarnEnabled()) {
+ LOGGER.warn(
+ "Failed to register listener for topic [{}]: {}",
+ UriSerializer.serialize(topic), t.getMessage());
+ }
+ return CompletableFuture.failedStage(t);
+ });
+ default:
+ // The USubscription service should not return any other subscription state
+ return CompletableFuture.failedStage(new UStatusException(
+ UCode.INTERNAL,
+ "Subscription request resulted in invalid state"));
+ }
+ });
+ }
+
+ @Override
+ public CompletionStage unsubscribe(UUri topic, UListener listener) {
+ Objects.requireNonNull(topic, "Unsubscribe topic missing");
+ Objects.requireNonNull(listener, "listener missing");
+
+ final var request = UnsubscribeRequest.newBuilder().setTopic(topic).build();
+
+ return subscriptionClient.unsubscribe(request)
+ .thenApply(unsubscribeResponse -> {
+ // remove subscription change handler (if one had been registered)
+ subscriptionChangeHandlers.remove(topic);
+ return unsubscribeResponse;
+ })
+ .thenCompose(unsubscribeResponse -> {
+ // remove subscription change handler (if one had been registered)
+ subscriptionChangeHandlers.remove(topic);
+ // When this fails, we have ended up in a situation where we
+ // have successfully (logically) unsubscribed from the topic via the USubscription service
+ // but we have not been able to unregister the listener from the local transport.
+ // This means that events originating from entities connected to a different transport
+ // may no longer get forwarded to the local transport, resulting in the (still registered)
+ // listener not being invoked for these events. We therefore return an error which should
+ // trigger the client to try again and (eventually) succeed in unregistering the listener
+ // as well.
+ return transport.unregisterListener(topic, listener)
+ .whenComplete((ok, throwable) -> {
+ if (throwable != null) {
+ LOGGER.warn("Failed to unregister listener for topic {}: {}", topic, throwable);
+ }
+ })
+ .thenApply(ok -> unsubscribeResponse);
+ });
+ }
+
+ @Override
+ public CompletionStage registerSubscriptionChangeHandler(
+ UUri topic,
+ SubscriptionChangeHandler handler) {
+ Objects.requireNonNull(topic, "Topic missing");
+ Objects.requireNonNull(handler, "Handler missing");
+
+ final var request = NotificationsRequest.newBuilder().setTopic(topic).build();
+ return subscriptionClient.registerForNotifications(request)
+ // Then add the handler so the client can be notified of
+ // changes to the subscription state.
+ .thenApply(response -> {
+ addSubscriptionChangeHandler(topic, Optional.of(handler));
+ return response;
+ });
+ }
+
+ @Override
+ public CompletionStage unregisterSubscriptionChangeHandler(UUri topic) {
+ Objects.requireNonNull(topic, "Topic missing");
+
+ final var request = NotificationsRequest.newBuilder().setTopic(topic).build();
+ return subscriptionClient.unregisterForNotifications(request)
+ .thenApply(response -> {
+ // remove subscription change handler (if one had been registered)
+ subscriptionChangeHandlers.remove(topic);
+ return response;
+ });
+ }
+
+ /**
+ * Handles incoming notifications from the USubscription service.
+ *
+ * @param message The notification message from the USubscription service
+ */
+ private void handleSubscriptionChangeNotification(UMessage message) {
+ // Ignore messages that are not notifications
+ if (message.getAttributes().getType() != UMessageType.UMESSAGE_TYPE_NOTIFICATION) {
+ Optional.ofNullable(unexpectedMessageHandler).ifPresent(handler -> handler.accept(message));
+ return;
+ }
+
+ UPayload.unpack(message, Update.class)
+ // Check if we have a handler registered for the subscription change notification for the specific
+ // topic that triggered the subscription change notification. It is very possible that the client
+ // did not register one to begin with (i.e they don't care to receive it)
+ .ifPresent(subscriptionUpdate -> {
+ final var topic = subscriptionUpdate.getTopic();
+ Optional.ofNullable(subscriptionChangeHandlers.get(topic))
+ .ifPresentOrElse(handler -> {
+ try {
+ handler.handleSubscriptionChange(topic, subscriptionUpdate.getStatus());
+ } catch (Exception e) {
+ LOGGER.info("Error handling subscription update", e);
+ }
+ }, () -> Optional.ofNullable(unexpectedMessageHandler)
+ .ifPresent(handler -> handler.accept(message)));
+ });
+ }
+}
diff --git a/src/main/java/org/eclipse/uprotocol/communication/Notifier.java b/src/main/java/org/eclipse/uprotocol/communication/Notifier.java
index d233d164..89862ae1 100644
--- a/src/main/java/org/eclipse/uprotocol/communication/Notifier.java
+++ b/src/main/java/org/eclipse/uprotocol/communication/Notifier.java
@@ -14,80 +14,109 @@
import java.util.concurrent.CompletionStage;
-import org.eclipse.uprotocol.v1.UStatus;
import org.eclipse.uprotocol.v1.UUri;
import org.eclipse.uprotocol.transport.UListener;
/**
- * Communication Layer (uP-L2) Notification Interface.
- *
- * Notifier is an interface that provides the APIs to send notifications (to a client) or
- * register/unregister listeners to receive the notifications.
+ * A client for sending Notification messages to a uEntity.
+ *
+ * @see
+ * Communication Layer API Specifications
*/
public interface Notifier {
/**
- * Send a notification to a given topic.
- *
- * @param topic The topic to send the notification to.
- * @param destination The destination to send the notification to.
- * @return Returns the {@link UStatus} with the status of the notification.
+ * Sends a notification to a uEntity.
+ *
+ * This default implementation invokes {@link #notify(int, UUri, CallOptions, UPayload)} with the
+ * given resource ID, destination, {@link CallOptions#DEFAULT default options} and an
+ * {@link UPayload#EMPTY empty payload}.
+ *
+ * @param resourceId The (local) resource identifier representing the origin of the notification.
+ * @param destination A URI representing the uEntity that the notification should be sent to.
+ * @return The outcome of the operation. The stage will be failed with a {@link UStatusException}
+ * if the notification could not be sent.
+ * @throws NullPointerException if any of the arguments are {@code null}.
*/
- default CompletionStage notify(UUri topic, UUri destination) {
- return notify(topic, destination, null, null);
+ default CompletionStage notify(int resourceId, UUri destination) {
+ return notify(resourceId, destination, CallOptions.DEFAULT, UPayload.EMPTY);
}
/**
- * Send a notification to a given topic with specific {@link CallOptions}.
- *
- * @param topic The topic to send the notification to.
+ * Sends a notification to a uEntity.
+ *
+ * This default implementation invokes {@link #notify(int, UUri, CallOptions, UPayload)} with the
+ * given resource ID, destination, options and an {@link UPayload#EMPTY empty payload}.
+ *
+ * @param resourceId The (local) resource identifier representing the origin of the notification.
* @param destination The destination to send the notification to.
- * @param options Call options for the notification.
- * @return Returns the {@link UStatus} with the status of the notification.
+ * @param options Options to include in the notification message. {@link CallOptions#DEFAULT} can
+ * be used for default options.
+ * @return The outcome of the operation. The stage will be failed with a {@link UStatusException}
+ * if the notification could not be sent.
+ * @throws NullPointerException if any of the arguments are {@code null}.
*/
- default CompletionStage notify(UUri topic, UUri destination, CallOptions options) {
- return notify(topic, destination, options, null);
+ default CompletionStage notify(int resourceId, UUri destination, CallOptions options) {
+ return notify(resourceId, destination, options, UPayload.EMPTY);
}
/**
- * Send a notification to a given topic passing a payload.
- *
- * @param topic The topic to send the notification to.
+ * Sends a notification to a uEntity.
+ *
+ * This default implementation invokes {@link #notify(int, UUri, CallOptions, UPayload)} with the
+ * given resource ID, destination, payload and {@link CallOptions#DEFAULT default options}.
+ *
+ * @param resourceId The (local) resource identifier representing the origin of the notification.
* @param destination The destination to send the notification to.
- * @param payload The payload to send with the notification.
- * @return Returns the {@link UStatus} with the status of the notification.
+ * @param payload The payload to include in the notification message. {@link UPayload#EMPTY}
+ * can be used if the notification has no payload.
+ * @return The outcome of the operation. The stage will be failed with a {@link UStatusException}
+ * if the notification could not be sent.
+ * @throws NullPointerException if any of the arguments are {@code null}.
*/
- default CompletionStage notify(UUri topic, UUri destination, UPayload payload) {
- return notify(topic, destination, null, payload);
+ default CompletionStage notify(int resourceId, UUri destination, UPayload payload) {
+ return notify(resourceId, destination, CallOptions.DEFAULT, payload);
}
/**
- * Send a notification to a given topic passing a payload and with specific {@link CallOptions}.
- *
- * @param topic The topic to send the notification to.
- * @param destination The destination to send the notification to.
- * @param payload The payload to send with the notification.
- * @param options Call options for the notification.
- * @return Returns the {@link UStatus} with the status of the notification.
+ * Sends a notification to a uEntity.
+ *
+ * @param resourceId The (local) resource identifier representing the origin of the notification.
+ * @param destination A URI representing the uEntity that the notification should be sent to.
+ * @param options Options to include in the notification message. {@link CallOptions#DEFAULT} can
+ * be used for default options.
+ * @param payload The payload to include in the notification message. {@link UPayload#EMPTY}
+ * can be used if the notification has no payload.
+ * @return The outcome of the operation. The stage will be failed with a {@link UStatusException}
+ * if the notification could not be sent.
+ * @throws NullPointerException if any of the arguments are {@code null}.
*/
- CompletionStage notify(UUri topic, UUri destination, CallOptions options, UPayload payload);
+ CompletionStage notify(int resourceId, UUri destination, CallOptions options, UPayload payload);
/**
- * Register a listener for a notification topic.
- *
- * @param topic The topic to register the listener to.
- * @param listener The listener to be called when a message is received on the topic.
- * @return Returns the {@link UStatus} with the status of the listener registration.
+ * Starts listening to a notification topic.
+ *
+ * More than one handler can be registered for the same topic.
+ * The same handler can be registered for multiple topics.
+ *
+ * @param topic The topic to listen to. The topic must not contain any wildcards.
+ * @param listener The handler to invoke for each notification that has been sent on the topic.
+ * @return The outcome of the operation. The stage will be failed with a {@link UStatusException}
+ * if the listener could not be registered.
+ * @throws NullPointerException if any of the arguments are {@code null}.
*/
- CompletionStage registerNotificationListener(UUri topic, UListener listener);
+ CompletionStage registerNotificationListener(UUri topic, UListener listener);
/**
- * Unregister a listener from a notification topic.
- *
- * @param topic The topic to unregister the listener from.
- * @param listener The listener to be unregistered from the topic.
- * @return Returns the {@link UStatus} with the status of the listener that was unregistered.
+ * Unregisters a previously {@link #registerNotificationListener(UUri, UListener) registered handler}
+ * for listening to notifications.
+ *
+ * @param topic The topic that the handler had been registered for.
+ * @param listener The listener to unregister.
+ * @return The outcome of the operation. The stage will be failed with a {@link UStatusException}
+ * if the listener could not be unregistered.
+ * @throws NullPointerException if any of the arguments are {@code null}.
*/
- CompletionStage unregisterNotificationListener(UUri topic, UListener listener);
+ CompletionStage unregisterNotificationListener(UUri topic, UListener listener);
}
diff --git a/src/main/java/org/eclipse/uprotocol/communication/Publisher.java b/src/main/java/org/eclipse/uprotocol/communication/Publisher.java
index d70d8748..07833a40 100644
--- a/src/main/java/org/eclipse/uprotocol/communication/Publisher.java
+++ b/src/main/java/org/eclipse/uprotocol/communication/Publisher.java
@@ -14,57 +14,72 @@
import java.util.concurrent.CompletionStage;
-import org.eclipse.uprotocol.v1.UStatus;
-import org.eclipse.uprotocol.v1.UUri;
-
/**
- * uP-L2 interface and data models for Java.
- *
- * uP-L1 interfaces implements the core uProtocol across various the communication middlewares
- * and programming languages while uP-L2 API are the client-facing APIs that wrap the transport
- * functionality into easy to use, language specific, APIs to do the most common functionality
- * of the protocol (subscribe, publish, notify, invoke a Method, or handle RPC requests).
+ * A client for publishing messages to a topic.
+ *
+ * @see
+ * Communication Layer API Specifications
*/
public interface Publisher {
/**
- * Publish a message to a topic.
- *
- * @param topic The topic to publish to.
- * @return Returns the {@link UStatus} with the status of the publish.
+ * Publishes a message to a topic.
+ *
+ * This default implementation invokes {@link #publish(int, CallOptions, UPayload)} with the
+ * given resource ID, {@link CallOptions#DEFAULT default options} and an
+ * {@link UPayload#EMPTY empty payload}.
+
+ * @param resourceId The (local) resource ID of the topic to publish to.
+ * @return The outcome of the operation. The stage will be failed with a {@link UStatusException}
+ * if the message could not be published.
+ * @throws NullPointerException if any of the arguments are {@code null}.
*/
- default CompletionStage publish(UUri topic) {
- return publish(topic, null, null);
+ default CompletionStage publish(int resourceId) {
+ return publish(resourceId, CallOptions.DEFAULT, UPayload.EMPTY);
}
/**
- * Publish a message to a topic with specific {@link CallOptions}.
+ * Publishes a message to a topic.
+ *
+ * This default implementation invokes {@link #publish(int, CallOptions, UPayload)} with the
+ * given resource ID, options and an {@link UPayload#EMPTY empty payload}.
*
- * @param topic The topic to publish to.
+ * @param resourceId The (local) resource ID of the topic to publish to.
* @param options The {@link CallOptions} for the publish.
- * @return Returns the {@link UStatus} with the status of the publish.
+ * @return The outcome of the operation. The stage will be failed with a {@link UStatusException}
+ * if the message could not be published.
+ * @throws NullPointerException if any of the arguments are {@code null}.
*/
- default CompletionStage publish(UUri topic, CallOptions options) {
- return publish(topic, options, null);
+ default CompletionStage publish(int resourceId, CallOptions options) {
+ return publish(resourceId, options, UPayload.EMPTY);
}
/**
- * Publish a message to a topic passing {@link UPayload} as the payload.
- *
- * @param topic The topic to publish to.
+ * Publishes a message to a topic.
+ *
+ * This default implementation invokes {@link #publish(int, CallOptions, UPayload)} with the
+ * given resource ID, payload and {@link CallOptions#DEFAULT default options}.
+ *
+ * @param resourceId The (local) resource ID of the topic to publish to.
* @param payload The {@link UPayload} to publish.
- * @return Returns the {@link UStatus} with the status of the publish.
+ * @return The outcome of the operation. The stage will be failed with a {@link UStatusException}
+ * if the message could not be published.
+ * @throws NullPointerException if any of the arguments are {@code null}.
*/
- default CompletionStage publish(UUri topic, UPayload payload) {
- return publish(topic, null, payload);
+ default CompletionStage publish(int resourceId, UPayload payload) {
+ return publish(resourceId, CallOptions.DEFAULT, payload);
}
/**
- * Publish a message to a topic passing {@link UPayload} as the payload and with specific {@link CallOptions}.
- *
- * @param topic The topic to publish to.
- * @param options The {@link CallOptions} for the publish.
- * @param payload The {@link UPayload} to publish.
- * @return Returns the {@link UStatus} with the status of the publish.
+ * Publishes a message to a topic.
+ *
+ * @param resourceId The (local) resource ID of the topic to publish to.
+ * @param options Options to include in the published message. {@link CallOptions#DEFAULT} can
+ * be used for default options.
+ * @param payload Payload to include in the published message. {@link UPayload#EMPTY}
+ * can be used if the message has no payload.
+ * @return The outcome of the operation. The stage will be failed with a {@link UStatusException}
+ * if the message could not be published.
+ * @throws NullPointerException if any of the arguments are {@code null}.
*/
- CompletionStage publish(UUri topic, CallOptions options, UPayload payload);
+ CompletionStage publish(int resourceId, CallOptions options, UPayload payload);
}
diff --git a/src/main/java/org/eclipse/uprotocol/communication/RpcClient.java b/src/main/java/org/eclipse/uprotocol/communication/RpcClient.java
index fc4057d5..d3a72249 100644
--- a/src/main/java/org/eclipse/uprotocol/communication/RpcClient.java
+++ b/src/main/java/org/eclipse/uprotocol/communication/RpcClient.java
@@ -14,24 +14,26 @@
import java.util.concurrent.CompletionStage;
import org.eclipse.uprotocol.v1.UUri;
-import org.eclipse.uprotocol.v1.UStatus;
/**
- * Communication Layer (uP-L2) RPC Client Interface.
- *
- * clients use this API to invoke a method (send a request and wait for a reply).
+ * A client for performing Remote Procedure Calls (RPC) on (other) uEntities.
+ *
+ * @see
+ * Communication Layer API specification
*/
public interface RpcClient {
/**
- * API for clients to invoke a method (send an RPC request) and receive the response (the returned
- * {@link CompletionStage} {@link UPayload}.
+ * Invokes a method on a service.
*
- * @param methodUri The method URI to be invoked.
- * @param requestPayload The request message to be sent to the server.
- * @param options RPC method invocation call options, see {@link CallOptions}
- * @return Returns the CompletionStage with the response payload or exception with the failure
- * reason as {@link UStatus}.
+ * @param methodUri The method to be invoked.
+ * @param requestPayload The payload to include in the RPC request message to be sent
+ * to the server. Use {@link UPayload#EMPTY} if no payload is required.
+ * @param options RPC method invocation call options. Use {@link CallOptions#DEFAULT} for default options.
+ * @return The outcome of the method invocation. The stage will either succeed with the
+ * response payload, or it will be failed with a {@link UStatusException} if the method invocation
+ * did not succeed.
+ * @throws NullPointerException if any of the arguments are {@code null}.
*/
CompletionStage invokeMethod(UUri methodUri, UPayload requestPayload, CallOptions options);
}
diff --git a/src/main/java/org/eclipse/uprotocol/communication/RpcMapper.java b/src/main/java/org/eclipse/uprotocol/communication/RpcMapper.java
index c24dd862..3926197b 100644
--- a/src/main/java/org/eclipse/uprotocol/communication/RpcMapper.java
+++ b/src/main/java/org/eclipse/uprotocol/communication/RpcMapper.java
@@ -23,7 +23,10 @@
* RPC Wrapper is an interface that provides static methods to be able to wrap an RPC request with
* an RPC Response (uP-L2). APIs that return Message assumes that the payload is either protobuf serialized
* UPayloadFormat.UPAYLOAD_FORMAT_PROTOBUF_WRAPPED_IN_ANY) or UPAYLOAD_FORMAT_PROTOBUF.
+ *
+ * @deprecated Use the methods for unpacking payload provided by {@link UPayload} instead.
*/
+@Deprecated(forRemoval = true)
public interface RpcMapper {
/**
@@ -70,7 +73,9 @@ static CompletionStage mapResponse(
* @return Returns a CompletionStage containing an RpcResult containing the declared expected
* return type T, or a Status containing any errors.
* @param The declared expected return type of the RPC method.
- */
+ * @deprecated Use the methods for unpacking payload provided by {@link UPayload} instead.
+ */
+ @Deprecated(forRemoval = true)
static CompletionStage> mapResponseToResult(
CompletionStage responseFuture, Class expectedClazz) {
@@ -99,6 +104,4 @@ static CompletionStage> mapResponseToResult(
return RpcResult.failure(exception.getMessage(), exception);
});
}
-
-
}
diff --git a/src/main/java/org/eclipse/uprotocol/communication/RpcResult.java b/src/main/java/org/eclipse/uprotocol/communication/RpcResult.java
index cd06e1a3..1ad747ba 100644
--- a/src/main/java/org/eclipse/uprotocol/communication/RpcResult.java
+++ b/src/main/java/org/eclipse/uprotocol/communication/RpcResult.java
@@ -24,6 +24,7 @@
* or a failure with the UStatus returned by the failed call.
* @param The type of the successful RPC call.
*/
+@Deprecated(forRemoval = true)
public abstract class RpcResult {
private RpcResult() {
@@ -31,8 +32,8 @@ private RpcResult() {
public abstract boolean isSuccess();
public abstract boolean isFailure();
- public abstract T getOrElse(final T defaultValue);
- public abstract T getOrElse(final Supplier defaultValue);
+ public abstract T getOrElse(T defaultValue);
+ public abstract T getOrElse(Supplier defaultValue);
public abstract RpcResult map(Function f);
@@ -44,7 +45,7 @@ private RpcResult() {
public abstract T successValue();
- private static class Success extends RpcResult {
+ private static final class Success extends RpcResult {
private final T value;
@@ -118,7 +119,7 @@ public String toString() {
}
}
- private static class Failure extends RpcResult {
+ private static final class Failure extends RpcResult {
private final UStatus value;
diff --git a/src/main/java/org/eclipse/uprotocol/communication/RpcServer.java b/src/main/java/org/eclipse/uprotocol/communication/RpcServer.java
index bc354470..94bff11b 100644
--- a/src/main/java/org/eclipse/uprotocol/communication/RpcServer.java
+++ b/src/main/java/org/eclipse/uprotocol/communication/RpcServer.java
@@ -14,35 +14,43 @@
import java.util.concurrent.CompletionStage;
-import org.eclipse.uprotocol.v1.UStatus;
+import org.eclipse.uprotocol.uri.factory.UriFactory;
import org.eclipse.uprotocol.v1.UUri;
/**
- * Communication Layer (uP-L2) Rpc Server interface.
- *
- * This interface provides APIs that services can call to register handlers for
- * incoming requests for given methods.
+ * A server for exposing Remote Procedure Call (RPC) endpoints.
+ *
+ * @see
+ * Communication Layer API specification
*/
public interface RpcServer {
/**
- * Register a handler that will be invoked when when requests come in from clients for the given method.
- *
- * Note: Only one handler is allowed to be registered per method URI.
+ * Registers an endpoint for RPC requests.
+ *
+ * Note that only a single endpoint can be registered for a given resource ID.
+ * However, the same request handler can be registered for multiple endpoints.
*
- * @param method Uri for the method to register the listener for.
- * @param handler The handler that will process the request for the client.
- * @return Returns the status of registering the RpcListener.
+ * @param originFilter A pattern defining origin addresses to accept requests from. Use {@link UriFactory#ANY}
+ * to match all origin addresses.
+ * @param resourceId The resource identifier of the (local) method to accept requests for.
+ * @param handler The handler to invoke for each incoming request that originates from a
+ * source matching the origin filter.
+ * @return The outcome of the registration. The stage will be completed with a {@link UStatusException} if
+ * registration has failed.
+ * @throws NullPointerException if any of the parameters is {@code null}.
*/
- CompletionStage registerRequestHandler(UUri method, RequestHandler handler);
-
+ CompletionStage registerRequestHandler(UUri originFilter, int resourceId, RequestHandler handler);
/**
- * Unregister a handler that will be invoked when when requests come in from clients for the given method.
+ * Deregisters a previously {@link #registerRequestHandler(UUri, int, RequestHandler) registered endpoint}.
*
- * @param method Resolved UUri for where the listener was registered to receive messages from.
- * @param handler The handler for processing requests
- * @return Returns status of registering the RpcListener.
+ * @param originFilter The origin pattern that the endpoint had been registered for.
+ * @param resourceId The (local) resource identifier that the endpoint had been registered for.
+ * @param handler The handler to unregister.
+ * @return The outcome of the registration. The stage will be completed with a {@link UStatusException} if
+ * registration has failed.
+ * @throws NullPointerException if any of the parameters is {@code null}.
*/
- CompletionStage unregisterRequestHandler(UUri method, RequestHandler handler);
+ CompletionStage unregisterRequestHandler(UUri originFilter, int resourceId, RequestHandler handler);
}
diff --git a/src/main/java/org/eclipse/uprotocol/communication/SimpleNotifier.java b/src/main/java/org/eclipse/uprotocol/communication/SimpleNotifier.java
index 881d8017..ef425d19 100644
--- a/src/main/java/org/eclipse/uprotocol/communication/SimpleNotifier.java
+++ b/src/main/java/org/eclipse/uprotocol/communication/SimpleNotifier.java
@@ -13,81 +13,61 @@
package org.eclipse.uprotocol.communication;
import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
+import org.eclipse.uprotocol.transport.LocalUriProvider;
import org.eclipse.uprotocol.transport.UListener;
import org.eclipse.uprotocol.transport.UTransport;
import org.eclipse.uprotocol.transport.builder.UMessageBuilder;
-import org.eclipse.uprotocol.v1.UStatus;
+import org.eclipse.uprotocol.uri.validator.UriValidator;
+import org.eclipse.uprotocol.v1.UCode;
import org.eclipse.uprotocol.v1.UUri;
/**
- * The following is an example implementation of the {@link Notifier} interface that
- * wraps the {@link UTransport} for implementing the notification pattern to send
- * notifications and register to receive notification events.
- *
- * *NOTE:* Developers are not required to use these APIs, they can implement their own
- * or directly use the {@link UTransport} to send notifications and register listeners.
+ * A Notifier that uses the uProtocol Transport Layer API to send and receive
+ * notifications to/from (other) uEntities.
+ *
+ * NOTE: Developers are not required to use these APIs, they can implement
+ * their own or directly use the {@link UTransport} to send notifications and register
+ * listeners.
*/
-public class SimpleNotifier implements Notifier {
- // The transport to use for sending the RPC requests
- private final UTransport transport;
+public class SimpleNotifier extends AbstractCommunicationLayerClient implements Notifier {
/**
- * Constructor for the DefaultNotifier.
+ * Creates a new notifier for a transport.
*
- * @param transport the transport to use for sending the notifications
+ * @param transport The transport to use for sending the notifications.
+ * @param uriProvider The helper to use for creating local resource URIs.
*/
- public SimpleNotifier (UTransport transport) {
- Objects.requireNonNull(transport, UTransport.TRANSPORT_NULL_ERROR);
- this.transport = transport;
+ public SimpleNotifier (UTransport transport, LocalUriProvider uriProvider) {
+ super(transport, uriProvider);
}
-
- /**
- * Send a notification to a given topic.
- *
- * @param topic The topic to send the notification to.
- * @param destination The destination to send the notification to.
- * @param options Call options for the notification.
- * @param payload The payload to send with the notification.
- * @return Returns the {@link UStatus} with the status of the notification.
- */
@Override
- public CompletionStage notify(UUri topic, UUri destination, CallOptions options, UPayload payload) {
- UMessageBuilder builder = UMessageBuilder.notification(topic, destination);
- if (options != null) {
- builder.withPriority(options.priority());
- builder.withTtl(options.timeout());
- builder.withToken(options.token());
+ public CompletionStage notify(int resourceId, UUri destination, CallOptions options, UPayload payload) {
+ Objects.requireNonNull(destination);
+ Objects.requireNonNull(options);
+ Objects.requireNonNull(payload);
+ final var topic = getUriProvider().getResource(resourceId);
+ if (!UriValidator.isTopic(topic)) {
+ return CompletableFuture.failedFuture(new UStatusException(
+ UCode.INVALID_ARGUMENT,
+ "Resource ID does not map to a valid topic URI"));
}
- return transport.send((payload == null) ? builder.build() :
- builder.build(payload));
+ UMessageBuilder builder = UMessageBuilder.notification(topic, destination);
+ options.applyToMessage(builder);
+ return getTransport().send(builder.build(payload));
}
- /**
- * Register a listener for a notification topic.
- *
- * @param topic The topic to register the listener to.
- * @param listener The listener to be called when a message is received on the topic.
- * @return Returns the {@link UStatus} with the status of the listener registration.
- */
@Override
- public CompletionStage registerNotificationListener(UUri topic, UListener listener) {
- return transport.registerListener(topic, transport.getSource(), listener);
+ public CompletionStage registerNotificationListener(UUri topic, UListener listener) {
+ return getTransport().registerListener(topic, getUriProvider().getSource(), listener);
}
-
- /**
- * Unregister a listener from a notification topic.
- *
- * @param topic The topic to unregister the listener from.
- * @param listener The listener to be unregistered from the topic.
- * @return Returns the {@link UStatus} with the status of the listener that was unregistered.
- */
@Override
- public CompletionStage unregisterNotificationListener(UUri topic, UListener listener) {
- return transport.unregisterListener(topic, transport.getSource(), listener);
+ public CompletionStage unregisterNotificationListener(UUri topic, UListener listener) {
+ return getTransport().unregisterListener(topic, getUriProvider().getSource(), listener);
}
}
diff --git a/src/main/java/org/eclipse/uprotocol/communication/SimplePublisher.java b/src/main/java/org/eclipse/uprotocol/communication/SimplePublisher.java
index f37e6c05..ac3f427b 100644
--- a/src/main/java/org/eclipse/uprotocol/communication/SimplePublisher.java
+++ b/src/main/java/org/eclipse/uprotocol/communication/SimplePublisher.java
@@ -13,53 +13,45 @@
package org.eclipse.uprotocol.communication;
import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
+import org.eclipse.uprotocol.transport.LocalUriProvider;
import org.eclipse.uprotocol.transport.UTransport;
import org.eclipse.uprotocol.transport.builder.UMessageBuilder;
-import org.eclipse.uprotocol.v1.UStatus;
-import org.eclipse.uprotocol.v1.UUri;
+import org.eclipse.uprotocol.uri.validator.UriValidator;
+import org.eclipse.uprotocol.v1.UCode;
/**
- * The following is an example implementation of the {@link Publisher} interface that
- * wraps the {@link UTransport} for implementing the notification pattern to send
- * notifications.
- *
- * *NOTE:* Developers are not required to use these APIs, they can implement their own
+ * A Publisher that uses the uProtocol Transport Layer API for publishing events to topics.
+ *
+ * NOTE: Developers are not required to use these APIs, they can implement their own
* or directly use the {@link UTransport} to send notifications and register listeners.
*/
-public class SimplePublisher implements Publisher {
- // The transport to use for sending the RPC requests
- private final UTransport transport;
+public class SimplePublisher extends AbstractCommunicationLayerClient implements Publisher {
/**
- * Constructor for the DefaultPublisher.
- *
+ * Creates a new publisher for a transport.
+ *
* @param transport the transport to use for sending the notifications
+ * @param uriProvider the URI provider to use for creating local resource URIs
*/
- public SimplePublisher (UTransport transport) {
- Objects.requireNonNull(transport, UTransport.TRANSPORT_NULL_ERROR);
- this.transport = transport;
+ public SimplePublisher(UTransport transport, LocalUriProvider uriProvider) {
+ super(transport, uriProvider);
}
- /**
- * Publish a message to a topic passing {@link UPayload} as the payload.
- *
- * @param topic The topic to publish to.
- * @param options The {@link CallOptions} for the publish.
- * @param payload The {@link UPayload} to publish.
- * @return {@link UStatus} with the result for sending the published message
- */
@Override
- public CompletionStage publish(UUri topic, CallOptions options, UPayload payload) {
- Objects.requireNonNull(topic, "Publish topic missing");
- UMessageBuilder builder = UMessageBuilder.publish(topic);
- if (options != null) {
- builder.withPriority(options.priority());
- builder.withTtl(options.timeout());
- builder.withToken(options.token());
+ public CompletionStage publish(int resourceId, CallOptions options, UPayload payload) {
+ Objects.requireNonNull(options);
+ Objects.requireNonNull(payload);
+ final var topic = getUriProvider().getResource(resourceId);
+ if (!UriValidator.isTopic(topic)) {
+ return CompletableFuture.failedFuture(new UStatusException(
+ UCode.INVALID_ARGUMENT,
+ "Resource ID does not map to a valid topic URI"));
}
-
- return transport.send(builder.build(payload));
+ UMessageBuilder builder = UMessageBuilder.publish(topic);
+ options.applyToMessage(builder);
+ return getTransport().send(builder.build(payload));
}
}
diff --git a/src/main/java/org/eclipse/uprotocol/communication/Subscriber.java b/src/main/java/org/eclipse/uprotocol/communication/Subscriber.java
new file mode 100644
index 00000000..c11f3726
--- /dev/null
+++ b/src/main/java/org/eclipse/uprotocol/communication/Subscriber.java
@@ -0,0 +1,90 @@
+/**
+ * SPDX-FileCopyrightText: 2025 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Apache License Version 2.0 which is available at
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package org.eclipse.uprotocol.communication;
+
+import java.util.Optional;
+import java.util.concurrent.CompletionStage;
+
+import org.eclipse.uprotocol.core.usubscription.v3.NotificationsResponse;
+import org.eclipse.uprotocol.core.usubscription.v3.SubscriptionResponse;
+import org.eclipse.uprotocol.core.usubscription.v3.SubscriptionStatus;
+import org.eclipse.uprotocol.core.usubscription.v3.UnsubscribeResponse;
+import org.eclipse.uprotocol.transport.UListener;
+import org.eclipse.uprotocol.v1.UUri;
+
+/**
+ * A client for subscribing to topics.
+ *
+ * @see
+ * Communication Layer API Specifications
+ */
+public interface Subscriber {
+ /**
+ * Registers a handler to invoke for messages that have been published to a given topic.
+ *
+ * More than one handler can be registered for the same topic.
+ * The same handler can be registered for multiple topics.
+ *
+ * @param topic The topic to subscribe to. The topic must not contain any wildcards.
+ * @param handler The handler to invoke for each message that has been published to the topic.
+ * @param subscriptionChangeHandler A handler to invoke for any subscription state changes for
+ * the given topic, like a transition from {@link SubscriptionStatus.State#SUBSCRIBE_PENDING} to
+ * {@link SubscriptionStatus.State#SUBSCRIBED} that occurs when the client subscribes to a
+ * remote topic for which no other local subscribers exist yet.
+ * @return The outcome of the operation. The stage will be failed with a {@link UStatusException}
+ * if subscribing to the topic failed.
+ * @throws NullPointerException if any of the arguments are {@code null}.
+ */
+ CompletionStage subscribe(
+ UUri topic,
+ UListener handler,
+ Optional subscriptionChangeHandler);
+
+ /**
+ * Deregisters a previously {@link #subscribe(UUri, UListener, Optional) registered handler}.
+ *
+ * @param topic The topic that the handler had been registered for.
+ * @param handler The handler to unregister.
+ * @return The outcome of the operation. The stage will be failed with a {@link UStatusException}
+ * if unsubscribing from the topic failed.
+ * @throws NullPointerException if any of the arguments are {@code null}.
+ */
+ CompletionStage unsubscribe(UUri topic, UListener handler);
+
+ /**
+ * Registers a handler for receiving subscription change notifications for a topic.
+ *
+ * This method can be used by event producers to get notified about other uEntities'
+ * attempts to subscribe to topics that they publish to.
+ *
+ * @param topic The topic to get subscription change notifications for.
+ * @param handler The handler to invoke for subscription changes.
+ * @return The outcome of the operation. The stage will be failed with a {@link UStatusException}
+ * if unsubscribing from the topic failed.
+ * @throws NullPointerException if any of the arguments are {@code null}.
+ */
+ CompletionStage registerSubscriptionChangeHandler(
+ UUri topic,
+ SubscriptionChangeHandler handler);
+
+ /**
+ * Unregisters a {@link #registerSubscriptionChangeHandler(UUri, SubscriptionChangeHandler) previously registered}
+ * subscription change handler.
+ *
+ * @param topic The topic that the handler had been registered for.
+ * @return The outcome of the operation. The stage will be failed with a {@link UStatusException}
+ * if unsubscribing from the topic failed.
+ * @throws NullPointerException if topic is {@code null}.
+ */
+ CompletionStage unregisterSubscriptionChangeHandler(UUri topic);
+}
diff --git a/src/main/java/org/eclipse/uprotocol/client/usubscription/v3/SubscriptionChangeHandler.java b/src/main/java/org/eclipse/uprotocol/communication/SubscriptionChangeHandler.java
similarity index 95%
rename from src/main/java/org/eclipse/uprotocol/client/usubscription/v3/SubscriptionChangeHandler.java
rename to src/main/java/org/eclipse/uprotocol/communication/SubscriptionChangeHandler.java
index 0b36a9fb..88f7e1c8 100644
--- a/src/main/java/org/eclipse/uprotocol/client/usubscription/v3/SubscriptionChangeHandler.java
+++ b/src/main/java/org/eclipse/uprotocol/communication/SubscriptionChangeHandler.java
@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: Apache-2.0
*/
-package org.eclipse.uprotocol.client.usubscription.v3;
+package org.eclipse.uprotocol.communication;
import org.eclipse.uprotocol.core.usubscription.v3.SubscriptionStatus;
import org.eclipse.uprotocol.v1.UUri;
diff --git a/src/main/java/org/eclipse/uprotocol/communication/UClient.java b/src/main/java/org/eclipse/uprotocol/communication/UClient.java
index c52e5acf..e237d565 100644
--- a/src/main/java/org/eclipse/uprotocol/communication/UClient.java
+++ b/src/main/java/org/eclipse/uprotocol/communication/UClient.java
@@ -15,89 +15,92 @@
import java.util.Objects;
import java.util.concurrent.CompletionStage;
+import org.eclipse.uprotocol.transport.LocalUriProvider;
import org.eclipse.uprotocol.transport.UListener;
import org.eclipse.uprotocol.transport.UTransport;
-import org.eclipse.uprotocol.v1.UStatus;
import org.eclipse.uprotocol.v1.UUri;
/**
- * Default implementation of the communication layer that uses the {@link UTransport}.
+ * A client for Communication Layer APIs.
*/
-public class UClient implements RpcServer, Notifier, Publisher, RpcClient {
-
- // The transport to use for sending the RPC requests
- private final UTransport transport;
-
- private final InMemoryRpcServer rpcServer;
- private final SimplePublisher publisher;
- private final SimpleNotifier notifier;
- private final InMemoryRpcClient rpcClient;
-
- private UClient (UTransport transport) {
- this.transport = transport;
-
- rpcServer = new InMemoryRpcServer(transport);
- publisher = new SimplePublisher(transport);
- notifier = new SimpleNotifier(transport);
- rpcClient = new InMemoryRpcClient(transport);
- }
+public final class UClient implements RpcServer, Notifier, Publisher, RpcClient {
+
+ private final RpcServer rpcServer;
+ private final Publisher publisher;
+ private final Notifier notifier;
+ private final RpcClient rpcClient;
+ /**
+ * Creates a new client.
+ *
+ * @param rpcClient The RPC client to use.
+ * @param rpcServer The RPC server to use.
+ * @param publisher The publisher to use.
+ * @param notifier The notifier to use.
+ * @throws NullPointerException if any of the arguments are {@code null}.
+ */
+ public UClient(RpcClient rpcClient, RpcServer rpcServer, Publisher publisher, Notifier notifier) {
+ this.rpcClient = Objects.requireNonNull(rpcClient);
+ this.rpcServer = Objects.requireNonNull(rpcServer);
+ this.publisher = Objects.requireNonNull(publisher);
+ this.notifier = Objects.requireNonNull(notifier);
+ }
@Override
- public CompletionStage notify(UUri topic, UUri destination, CallOptions options, UPayload payload) {
- return notifier.notify(topic, destination, options, payload);
+ public CompletionStage notify(int resourceId, UUri destination, CallOptions options, UPayload payload) {
+ return notifier.notify(resourceId, destination, options, payload);
}
@Override
- public CompletionStage registerNotificationListener(UUri topic, UListener listener) {
- Objects.requireNonNull(transport, UTransport.TRANSPORT_NULL_ERROR);
+ public CompletionStage registerNotificationListener(UUri topic, UListener listener) {
return notifier.registerNotificationListener(topic, listener);
}
@Override
- public CompletionStage unregisterNotificationListener(UUri topic, UListener listener) {
+ public CompletionStage unregisterNotificationListener(UUri topic, UListener listener) {
return notifier.unregisterNotificationListener(topic, listener);
}
@Override
- public CompletionStage publish(UUri topic, CallOptions options, UPayload payload) {
- return publisher.publish(topic, options, payload);
+ public CompletionStage publish(int resourceId, CallOptions options, UPayload payload) {
+ return publisher.publish(resourceId, options, payload);
}
@Override
- public CompletionStage registerRequestHandler(UUri method, RequestHandler handler) {
- return rpcServer.registerRequestHandler(method, handler);
+ public CompletionStage registerRequestHandler(UUri originFilter, int resourceId, RequestHandler handler) {
+ return rpcServer.registerRequestHandler(originFilter, resourceId, handler);
}
-
@Override
- public CompletionStage unregisterRequestHandler(UUri method, RequestHandler handler) {
- return rpcServer.unregisterRequestHandler(method, handler);
+ public CompletionStage unregisterRequestHandler(UUri originFilter, int resourceId,
+ RequestHandler handler) {
+ return rpcServer.unregisterRequestHandler(originFilter, resourceId, handler);
}
-
@Override
public CompletionStage invokeMethod(UUri methodUri, UPayload requestPayload, CallOptions options) {
return rpcClient.invokeMethod(methodUri, requestPayload, options);
}
-
/**
- * Create a new instance of UPClient.
+ * Creates a new client for a transport implementation.
*
- * @param transport The transport to use for sending the RPC requests
+ * @param transport The transport to use for sending and receiving messages.
+ * @param uriProvider The helper to use for creating local resource URIs.
* @return Returns a new instance of the RPC client
+ * @throws NullPointerException if any of the arguments are {@code null}.
*/
- public static UClient create(UTransport transport) {
- Objects.requireNonNull(transport, UTransport.TRANSPORT_NULL_ERROR);
- return new UClient(transport);
- }
-
-
- public void close() {
- rpcClient.close();
+ public static UClient create(UTransport transport, LocalUriProvider uriProvider) {
+ Objects.requireNonNull(transport);
+ Objects.requireNonNull(uriProvider);
+ return new UClient(
+ new InMemoryRpcClient(transport, uriProvider),
+ new InMemoryRpcServer(transport, uriProvider),
+ new SimplePublisher(transport, uriProvider),
+ new SimpleNotifier(transport, uriProvider)
+ );
}
}
diff --git a/src/main/java/org/eclipse/uprotocol/communication/UPayload.java b/src/main/java/org/eclipse/uprotocol/communication/UPayload.java
index 9cbe278e..d461b038 100644
--- a/src/main/java/org/eclipse/uprotocol/communication/UPayload.java
+++ b/src/main/java/org/eclipse/uprotocol/communication/UPayload.java
@@ -21,6 +21,7 @@
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.Message;
+import org.eclipse.uprotocol.v1.UCode;
import org.eclipse.uprotocol.v1.UMessage;
import org.eclipse.uprotocol.v1.UPayloadFormat;
@@ -106,18 +107,19 @@ public static Optional unpack(UMessage message, Class
return unpack(message.getPayload(), message.getAttributes().getPayloadFormat(), clazz);
}
-
/**
- * Unpack a uPayload into {@link Message}.
+ * Unpacks a protobuf from a message payload into a Java type.
*
* IMPORTANT NOTE: If {@link UPayloadFormat} is not
* {@link UPayloadFormat#UPAYLOAD_FORMAT_PROTOBUF_WRAPPED_IN_ANY},
* there is no guarantee that the parsing to T is correct as we do not have the data schema.
*
- * @param payload the payload to unpack
- * @param clazz the class of the message to unpack
- * @return the unpacked message
+ * @param payload The payload to unpack.
+ * @param clazz The Java type to unpack the payload to.
+ * @return The unpacked type instance.
+ * @deprecated Use {@link #unpackOrDefaultInstance(UPayload, Class)} instead.
*/
+ @Deprecated(forRemoval = true)
public static Optional unpack(UPayload payload, Class clazz) {
if (payload == null) {
return Optional.empty();
@@ -125,20 +127,42 @@ public static Optional unpack(UPayload payload, Class
return unpack(payload.data(), payload.format(), clazz);
}
+ /**
+ * Unpacks a protobuf from a message payload into a Java type.
+ *
+ * @param payload The payload to unpack.
+ * @param expectedType The Java type to unpack the protobuf to.
+ * @return An instance of the expected type. The instance will be the default instance if the
+ * given protobuf is empty and the payload format is {@link UPayloadFormat#UPAYLOAD_FORMAT_PROTOBUF}.
+ *
+ * IMPORTANT NOTE: If format is not
+ * {@link UPayloadFormat#UPAYLOAD_FORMAT_PROTOBUF_WRAPPED_IN_ANY} then there is no guarantee
+ * that the returned instance's fields contain proper values because in the absence of a data schema
+ * it is unclear, if the protobuf actually represents an instance of the expected type.
+ * @throws NullPointerException if any of the arguments are {@code null}.
+ * @throws UStatusException if the protobuf cannot be unpacked to the expected type.
+ */
+ public static T unpackOrDefaultInstance(UPayload payload, Class expectedType) {
+ return unpackOrDefaultInstance(payload.data(), payload.format(), expectedType);
+ }
/**
- * Unpack a uPayload into a {@link Message}.
+ * Unpacks a protobuf into a Java type.
*
* IMPORTANT NOTE: If the format is not {@link UPayloadFormat#UPAYLOAD_FORMAT_PROTOBUF_WRAPPED_IN_ANY},
* there is no guarantee that the parsing to T is correct as we do not have the data schema.
*
- * @param data The serialized UPayload data
- * @param format The serialization format of the payload
- * @param clazz the class of the message to unpack
- * @return the unpacked message
+ * @param data The protobuf to unpack.
+ * @param format The serialization format of the protobuf.
+ * @param clazz The Java type to unpack the protobuf to.
+ * @return The unpacked type instance.
+ * @throws NullPointerException if clazz is {@code null}.
+ * @deprecated Use {@link #unpackOrDefaultInstance(ByteString, UPayloadFormat, Class)} instead.
*/
+ @Deprecated(forRemoval = true)
@SuppressWarnings("unchecked")
public static Optional unpack(ByteString data, UPayloadFormat format, Class clazz) {
+ Objects.requireNonNull(clazz, "clazz must not be null");
format = Objects.requireNonNullElse(format, UPayloadFormat.UPAYLOAD_FORMAT_UNSPECIFIED);
if (data == null || data.isEmpty()) {
return Optional.empty();
@@ -160,4 +184,57 @@ public static Optional unpack(ByteString data, UPayloadFo
return Optional.empty();
}
}
+
+ /**
+ * Unpacks a protobuf to a Java type.
+ *
+ * @param protobuf The protobuf to unpack.
+ * @param format The serialization format of the protobuf.
+ * @param expectedType The Java type to unpack the protobuf to.
+ * @return An instance of the expected type. The instance will be the default instance if the
+ * given protobuf is empty and the payload format is {@link UPayloadFormat#UPAYLOAD_FORMAT_PROTOBUF}.
+ *
+ * IMPORTANT NOTE: If format is not
+ * {@link UPayloadFormat#UPAYLOAD_FORMAT_PROTOBUF_WRAPPED_IN_ANY} then there is no guarantee
+ * that the returned instance's fields contain proper values because in the absence of a data schema
+ * it is unclear, if the protobuf actually represents an instance of the expected type.
+ * @throws NullPointerException if any of the arguments are {@code null}.
+ * @throws UStatusException if the protobuf cannot be unpacked to the expected type.
+ */
+ @SuppressWarnings("unchecked")
+ public static T unpackOrDefaultInstance(
+ ByteString protobuf,
+ UPayloadFormat format,
+ Class expectedType) {
+ Objects.requireNonNull(protobuf, "data must not be null");
+ Objects.requireNonNull(format, "format must not be null");
+ Objects.requireNonNull(expectedType, "expectedType must not be null");
+ switch (format) {
+ case UPAYLOAD_FORMAT_UNSPECIFIED: // Default is WRAPPED_IN_ANY
+ case UPAYLOAD_FORMAT_PROTOBUF_WRAPPED_IN_ANY :
+ try {
+ return Any.parseFrom(protobuf).unpack(expectedType);
+ } catch (InvalidProtocolBufferException e) {
+ throw new UStatusException(UCode.INVALID_ARGUMENT, "Failed to unpack Any", e);
+ }
+
+ case UPAYLOAD_FORMAT_PROTOBUF:
+ T defaultInstance = com.google.protobuf.Internal.getDefaultInstance(expectedType);
+ if (protobuf.isEmpty()) {
+ // this can happen when trying to unpack a proto message that has no fields
+ // and is therefore encoded as an empty byte array
+ return defaultInstance;
+ } else {
+ try {
+ return (T) defaultInstance.getParserForType().parseFrom(protobuf);
+ } catch (InvalidProtocolBufferException e) {
+ throw new UStatusException(UCode.INVALID_ARGUMENT, "Failed to unpack protobuf", e);
+ }
+ }
+
+ default:
+ throw new UStatusException(
+ UCode.INVALID_ARGUMENT, "Unsupported payload format");
+ }
+ }
}
diff --git a/src/main/java/org/eclipse/uprotocol/transport/LocalUriProvider.java b/src/main/java/org/eclipse/uprotocol/transport/LocalUriProvider.java
new file mode 100644
index 00000000..7f99b3fc
--- /dev/null
+++ b/src/main/java/org/eclipse/uprotocol/transport/LocalUriProvider.java
@@ -0,0 +1,46 @@
+/**
+ * SPDX-FileCopyrightText: 2025 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Apache License Version 2.0 which is available at
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package org.eclipse.uprotocol.transport;
+
+import org.eclipse.uprotocol.v1.UUri;
+
+/**
+ * A factory for URIs representing this uEntity's resources.
+ *
+ * Implementations may use arbitrary mechanisms to determine the information that
+ * is necessary for creating URIs, e.g. environment variables, configuration files etc.
+ */
+public interface LocalUriProvider {
+ /**
+ * Gets the authority used for URIs representing this uEntity's resources.
+ *
+ * @return The authority name.
+ */
+ String getAuthority();
+
+ /**
+ * Gets the URI that represents the resource that this uEntity expects
+ * RPC responses and notifications to be sent to.
+ *
+ * @return The source URI.
+ */
+ UUri getSource();
+
+ /**
+ * Gets a URI that represents a given resource of this uEntity.
+ *
+ * @param id The ID of the resource.
+ * @return The resource URI.
+ */
+ UUri getResource(int id);
+}
diff --git a/src/main/java/org/eclipse/uprotocol/transport/StaticUriProvider.java b/src/main/java/org/eclipse/uprotocol/transport/StaticUriProvider.java
new file mode 100644
index 00000000..ff509188
--- /dev/null
+++ b/src/main/java/org/eclipse/uprotocol/transport/StaticUriProvider.java
@@ -0,0 +1,79 @@
+/**
+ * SPDX-FileCopyrightText: 2025 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Apache License Version 2.0 which is available at
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package org.eclipse.uprotocol.transport;
+
+import java.util.Objects;
+
+import org.eclipse.uprotocol.uri.validator.UriValidator;
+import org.eclipse.uprotocol.v1.UUri;
+
+/**
+ * A URI provider that is statically configured with the uEntity's authority, entity ID and version.
+ */
+public final class StaticUriProvider implements LocalUriProvider {
+
+ private final UUri localUri;
+
+ private StaticUriProvider(UUri uuri) {
+ this.localUri = uuri;
+ }
+
+ /**
+ * Creates a new provider for a uEntity with a known local URI.
+ *
+ * @param uuri The local URI of the uEntity.
+ * @return The provider.
+ * @throws NullPointerException if uuri is {@code null}.
+ */
+ public static StaticUriProvider of(UUri uuri) {
+ return of(uuri.getAuthorityName(), uuri.getUeId(), uuri.getUeVersionMajor());
+ }
+
+ /**
+ * Creates a new provider for a uEntity with a known authority, entity ID and version.
+ *
+ * @param authority The authority name of the uEntity.
+ * @param entityId The entity ID of the uEntity.
+ * @param majorVersion The major version of the uEntity.
+ * @return The provider.
+ * @throws IllegalArgumentException if the provided parameters are invalid.
+ * @throws NullPointerException if authority is {@code null}.
+ */
+ public static StaticUriProvider of(String authority, int entityId, int majorVersion) {
+ Objects.requireNonNull(authority);
+ UUri localUri = UUri.newBuilder()
+ .setAuthorityName(authority)
+ .setUeId(entityId)
+ .setUeVersionMajor(majorVersion)
+ .build();
+ UriValidator.validate(localUri);
+ return new StaticUriProvider(localUri);
+ }
+
+ @Override
+ public String getAuthority() {
+ return localUri.getAuthorityName();
+ }
+
+ @Override
+ public UUri getSource() {
+ return localUri;
+ }
+
+ @Override
+ public UUri getResource(int id) {
+ return UUri.newBuilder(localUri)
+ .setResourceId(id)
+ .build();
+ }
+}
diff --git a/src/main/java/org/eclipse/uprotocol/transport/UListener.java b/src/main/java/org/eclipse/uprotocol/transport/UListener.java
index 2da79f87..5912cf2b 100644
--- a/src/main/java/org/eclipse/uprotocol/transport/UListener.java
+++ b/src/main/java/org/eclipse/uprotocol/transport/UListener.java
@@ -15,15 +15,24 @@
import org.eclipse.uprotocol.v1.UMessage;
/**
- * For any implementation that defines some kind of callback or function that
- * will be called to handle incoming messages.
+ * A handler for processing uProtocol messages.
+ *
+ * Implementations contain the details for what should occur when a message is received.
+ *
+ * @see
+ * uProtocol Transport Layer specification
*/
+/// for details. */
public interface UListener {
/**
- * Method called to handle/process messages.
- *
- * @param message Message received.
+ * Performs some action on receipt of a message.
+ *
+ * This function is expected to return almost immediately. If it does not, it could potentially
+ * block processing of succeeding messages. Long-running operations for processing a message should
+ * therefore be run on a separate thread.
+ *
+ * @param message The message to process.
*/
void onReceive(UMessage message);
}
diff --git a/src/main/java/org/eclipse/uprotocol/transport/UTransport.java b/src/main/java/org/eclipse/uprotocol/transport/UTransport.java
index 1ee08000..84f721ad 100644
--- a/src/main/java/org/eclipse/uprotocol/transport/UTransport.java
+++ b/src/main/java/org/eclipse/uprotocol/transport/UTransport.java
@@ -13,132 +13,100 @@
package org.eclipse.uprotocol.transport;
import java.util.concurrent.CompletionStage;
-import java.util.concurrent.CompletableFuture;
+import org.eclipse.uprotocol.communication.UStatusException;
import org.eclipse.uprotocol.uri.factory.UriFactory;
-import org.eclipse.uprotocol.v1.UCode;
+import org.eclipse.uprotocol.v1.UAttributes;
import org.eclipse.uprotocol.v1.UMessage;
-import org.eclipse.uprotocol.v1.UStatus;
import org.eclipse.uprotocol.v1.UUri;
/**
* UTransport is the uP-L1 interface that provides a common API for uE developers to send and receive messages.
+ *
* UTransport implementations contain the details for connecting to the underlying transport technology and
- * sending UMessage using the configured technology. For more information please refer to
- * https://github.com/eclipse-uprotocol/up-spec/blob/main/up-l1/README.adoc.
+ * sending UMessage using the configured technology.
+ *
+ * @see
+ * uProtocol Transport Layer specification
*/
public interface UTransport {
-
- /**
- * Error message for null transport.
- */
- String TRANSPORT_NULL_ERROR = "Transport cannot be null";
-
/**
- * Send a message over the transport.
- *
- * @param message the {@link UMessage} to be sent.
- * @return Returns {@link UStatus} with {@link UCode} set to the status code
- * (successful or failure).
+ * Sends a message using this transport's message exchange mechanism.
+ *
+ * @param message The message to send. The type, source and sink properties of the
+ * {@link UAttributes} contained in the message determine the addressing semantics.
+ * @return The outcome of the operation. The stage will be completed with a {@link UStatusException} if
+ * the message could not be sent.
*/
- CompletionStage send(UMessage message);
-
+ CompletionStage send(UMessage message);
/**
- * Register {@code UListener} for {@code UUri} source filters to be called when
- * a message is received.
- *
- * @param sourceFilter The UAttributes::source address pattern that the message
- * to receive needs to match.
- * @param listener The {@code UListener} that will be execute when the
- * message is
- * received on the given {@code UUri}.
- * @return Returns {@link UStatus} with {@link UCode#OK} if the listener is
- * registered correctly, otherwise it returns with the appropriate failure.
+ * Registers a listener to be called for messages.
+ *
+ * The listener will be invoked for each message that matches the given source filter pattern
+ * according to the rules defined by the
+ * UUri
+ * specification.
+ *
+ * This default implementation invokes {@link #registerListener(UUri, UUri, UListener)} with the
+ * given source filter and a sink filter of {@link UriFactory#ANY}.
+ *
+ * @param sourceFilter The source address pattern that messages need to match.
+ * @param listener The listener to invoke. The listener can be unregistered again
+ * using {@link #unregisterListener(UUri, UUri, UListener)}.
+ * @return The outcome of the operation. The stage will be completed with a {@link UStatusException} if
+ * the listener could not be registered.
*/
- default CompletionStage registerListener(UUri sourceFilter, UListener listener) {
+ default CompletionStage registerListener(UUri sourceFilter, UListener listener) {
return registerListener(sourceFilter, UriFactory.ANY, listener);
}
-
/**
- * Register {@code UListener} for {@code UUri} source and sink filters to be
- * called when a message is received.
- *
- * @param sourceFilter The UAttributes::source address pattern that the message
- * to receive needs to match.
- * @param sinkFilter The UAttributes::sink address pattern that the message to
- * receive needs to match.
- * @param listener The {@code UListener} that will be execute when the
- * message is
- * received on the given {@code UUri}.
- * @return Returns {@link UStatus} with {@link UCode#OK} if the listener is
- * registered
- * correctly, otherwise it returns with the appropriate failure.
+ * Registers a listener to be called for messages.
+ *
+ * The listener will be invoked for each message that matches the given source and sink filter patterns
+ * according to the rules defined by the
+ * UUri
+ * specification.
+ *
+ * @param sourceFilter The source address pattern that messages need to match.
+ * @param sinkFilter The sink address pattern that messages need to match.
+ * @param listener The listener to invoke. The listener can be unregistered again
+ * using {@link #unregisterListener(UUri, UUri, UListener)}.
+ * @return The outcome of the operation. The stage will be completed with a {@link UStatusException} if
+ * the listener could not be registered.
*/
- CompletionStage registerListener(UUri sourceFilter, UUri sinkFilter, UListener listener);
-
+ CompletionStage registerListener(UUri sourceFilter, UUri sinkFilter, UListener listener);
/**
- * Unregister {@code UListener} for {@code UUri} source filters. Messages
- * arriving on this topic will no longer be processed by this listener.
- *
- * @param sourceFilter The UAttributes::source address pattern that the message
- * to receive needs to match.
- * @param listener The {@code UListener} that will no longer want to be
- * registered to receive
- * messages.
- * @return Returns {@link UStatus} with {@link UCode#OK} if the listener is
- * unregistered
- * correctly, otherwise it returns with the appropriate failure.
+ * Unregisters a message listener.
+ *
+ * The listener will no longer be called for any (matching) messages after this function has
+ * returned successfully.
+ *
+ * This default implementation invokes {@link #unregisterListener(UUri, UUri, UListener)} with the
+ * given source filter and a sink filter of {@link UriFactory#ANY}.
+ *
+ * @param sourceFilter The source address pattern that the listener had been registered for.
+ * @param listener The listener to unregister.
+ * @return The outcome of the operation. The stage will be completed with a {@link UStatusException} if
+ * the listener could not be unregistered.
*/
- default CompletionStage unregisterListener(UUri sourceFilter, UListener listener) {
+ default CompletionStage unregisterListener(UUri sourceFilter, UListener listener) {
return unregisterListener(sourceFilter, UriFactory.ANY, listener);
}
-
- /**
- * Unregister {@code UListener} for {@code UUri} source and sink filters.
- * Messages arriving on this topic will no longer be processed by this listener.
- *
- * @param sourceFilter The UAttributes::source address pattern that the message
- * to receive needs to match.
- * @param sinkFilter The UAttributes::sink address pattern that the message to
- * receive needs to match.
- * @param listener The {@code UListener} that will no longer want to be
- * registered to receive
- * messages.
- * @return Returns {@link UStatus} with {@link UCode#OK} if the listener is
- * unregistered
- * correctly, otherwise it returns with the appropriate failure.
- */
- CompletionStage unregisterListener(UUri sourceFilter, UUri sinkFilter, UListener listener);
-
-
- /**
- * Return the source address of the uE.
- * The Source address is passed to the constructor of a given transport
- *
- * @return UUri containing the source address
- */
- UUri getSource();
-
-
- /**
- * Open the connection to the transport that will trigger any registered listeners
- * to be registered.
- *
- * @return Returns {@link UStatus} with {@link UCode#OK} if the connection is
- * opened correctly, otherwise it returns with the appropriate failure.
- */
- default CompletionStage open() {
- return CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build());
- }
-
-
/**
- * Close the connection to the transport that will trigger any registered listeners
- * to be unregistered.
+ * Unregisters a message listener.
+ *
+ * The listener will no longer be called for any (matching) messages after this function has
+ * returned successfully.
+ *
+ * @param sourceFilter The source address pattern that the listener had been registered for.
+ * @param sinkFilter The sink address pattern that the listener had been registered for.
+ * @param listener The listener to unregister.
+ * @return The outcome of the operation. The stage will be completed with a {@link UStatusException} if
+ * the listener could not be unregistered.
*/
- default void close() { }
+ CompletionStage unregisterListener(UUri sourceFilter, UUri sinkFilter, UListener listener);
}
diff --git a/src/main/java/org/eclipse/uprotocol/uri/factory/UriFactory.java b/src/main/java/org/eclipse/uprotocol/uri/factory/UriFactory.java
index e72ff62a..7b80c587 100644
--- a/src/main/java/org/eclipse/uprotocol/uri/factory/UriFactory.java
+++ b/src/main/java/org/eclipse/uprotocol/uri/factory/UriFactory.java
@@ -19,13 +19,27 @@
import com.google.protobuf.Descriptors.ServiceDescriptor;
/**
- * URI Factory that builds URIs from protos.
+ * A factory for uProtocol URIs.
*/
-public interface UriFactory {
- String WILDCARD_AUTHORITY = "*";
- int WILDCARD_ENTITY_ID = 0xFFFF;
- int WILDCARD_ENTITY_VERSION = 0xFF;
- int WILDCARD_RESOURCE_ID = 0xFFFF;
+public final class UriFactory {
+
+ public static final String WILDCARD_AUTHORITY = "*";
+ public static final int WILDCARD_ENTITY_ID = 0xFFFF;
+ public static final int WILDCARD_ENTITY_VERSION = 0xFF;
+ public static final int WILDCARD_RESOURCE_ID = 0xFFFF;
+
+ /**
+ * A uProtocol pattern URI that matches all UUris.
+ */
+ public static final UUri ANY = UUri.newBuilder()
+ .setAuthorityName(WILDCARD_AUTHORITY)
+ .setUeId(WILDCARD_ENTITY_ID)
+ .setUeVersionMajor(WILDCARD_ENTITY_VERSION)
+ .setResourceId(WILDCARD_RESOURCE_ID).build();
+
+ private UriFactory() {
+ // Prevent instantiation
+ }
/**
* Builds a UEntity for an protobuf generated code Service Descriptor.
@@ -34,7 +48,7 @@ public interface UriFactory {
* @param resourceId The resource id.
* @return Returns a UEntity for an protobuf generated code Service Descriptor.
*/
- static UUri fromProto(ServiceDescriptor descriptor, int resourceId) {
+ public static UUri fromProto(ServiceDescriptor descriptor, int resourceId) {
return fromProto(descriptor, resourceId, null);
}
@@ -46,7 +60,7 @@ static UUri fromProto(ServiceDescriptor descriptor, int resourceId) {
* @param authorityName The authority name.
* @return Returns a UEntity for an protobuf generated code Service Descriptor.
*/
- static UUri fromProto(ServiceDescriptor descriptor, int resourceId, String authorityName) {
+ public static UUri fromProto(ServiceDescriptor descriptor, int resourceId, String authorityName) {
if (descriptor == null) {
return UUri.getDefaultInstance();
}
@@ -63,14 +77,4 @@ static UUri fromProto(ServiceDescriptor descriptor, int resourceId, String autho
}
return builder.build();
}
-
-
- /**
- * A uProtocol pattern URI that matches all UUris.
- */
- UUri ANY = UUri.newBuilder()
- .setAuthorityName(WILDCARD_AUTHORITY)
- .setUeId(WILDCARD_ENTITY_ID)
- .setUeVersionMajor(WILDCARD_ENTITY_VERSION)
- .setResourceId(WILDCARD_RESOURCE_ID).build();
}
diff --git a/src/main/java/org/eclipse/uprotocol/uri/validator/UriValidator.java b/src/main/java/org/eclipse/uprotocol/uri/validator/UriValidator.java
index 69450711..e62da205 100644
--- a/src/main/java/org/eclipse/uprotocol/uri/validator/UriValidator.java
+++ b/src/main/java/org/eclipse/uprotocol/uri/validator/UriValidator.java
@@ -30,6 +30,35 @@ public interface UriValidator {
*/
int DEFAULT_RESOURCE_ID = 0;
+ /**
+ * Validates a UUri against the uProtocol specification.
+ *
+ * @param uuri The UUri to validate.
+ * @throws NullPointerException if the UUri is null.
+ * @throws IllegalArgumentException if the UUri does not comply with the UUri specification.
+ */
+ static void validate(UUri uuri) {
+ if (uuri == null) {
+ throw new NullPointerException("URI cannot be null");
+ }
+
+ if (uuri.getAuthorityName().length() > 128) {
+ throw new IllegalArgumentException("Authority name exceeds maximum length of 128 characters");
+ }
+
+ // no need to check uEntity ID which is of Java primitive type (signed) int but actually represents
+ // an unsigned 32 bit integer, thus any value is valid
+
+ if ((uuri.getUeVersionMajor() & 0xFFFF_FF00) != 0) {
+ throw new IllegalArgumentException("uEntity version major must be in range [0, 0x%X]"
+ .formatted(UriFactory.WILDCARD_ENTITY_VERSION));
+ }
+
+ if ((uuri.getResourceId() & 0xFFFF_0000) != 0) {
+ throw new IllegalArgumentException("uEntity resource ID must be in range [0, 0x%X]"
+ .formatted(UriFactory.WILDCARD_RESOURCE_ID));
+ }
+ }
/**
* Indicates that this URI is an empty as it does not contain authority, entity,
diff --git a/src/test/java/org/eclipse/uprotocol/client/usubscription/v3/InMemoryUSubscriptionClientTest.java b/src/test/java/org/eclipse/uprotocol/client/usubscription/v3/InMemoryUSubscriptionClientTest.java
index e89c4878..e69de29b 100644
--- a/src/test/java/org/eclipse/uprotocol/client/usubscription/v3/InMemoryUSubscriptionClientTest.java
+++ b/src/test/java/org/eclipse/uprotocol/client/usubscription/v3/InMemoryUSubscriptionClientTest.java
@@ -1,1171 +0,0 @@
-/**
- * SPDX-FileCopyrightText: 2024 Contributors to the Eclipse Foundation
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information regarding copyright ownership.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Apache License Version 2.0 which is available at
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * SPDX-License-Identifier: Apache-2.0
- */
-package org.eclipse.uprotocol.client.usubscription.v3;
-
-import java.util.concurrent.BrokenBarrierException;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.CompletionException;
-import java.util.concurrent.CompletionStage;
-import java.util.concurrent.Executors;
-import java.util.concurrent.CyclicBarrier;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Test;
-
-import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import org.mockito.Mock;
-import org.junit.jupiter.api.extension.ExtendWith;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-import static org.mockito.Mockito.mock;
-
-import org.mockito.junit.jupiter.MockitoExtension;
-import org.eclipse.uprotocol.communication.CallOptions;
-import org.eclipse.uprotocol.communication.InMemoryRpcClient;
-import org.eclipse.uprotocol.communication.SimpleNotifier;
-import org.eclipse.uprotocol.communication.UPayload;
-import org.eclipse.uprotocol.communication.UStatusException;
-import org.eclipse.uprotocol.core.usubscription.v3.FetchSubscribersResponse;
-import org.eclipse.uprotocol.core.usubscription.v3.FetchSubscriptionsRequest;
-import org.eclipse.uprotocol.core.usubscription.v3.FetchSubscriptionsResponse;
-import org.eclipse.uprotocol.core.usubscription.v3.NotificationsResponse;
-import org.eclipse.uprotocol.core.usubscription.v3.SubscriptionResponse;
-import org.eclipse.uprotocol.core.usubscription.v3.SubscriptionStatus;
-import org.eclipse.uprotocol.core.usubscription.v3.UnsubscribeResponse;
-import org.eclipse.uprotocol.core.usubscription.v3.Update;
-import org.eclipse.uprotocol.transport.UListener;
-import org.eclipse.uprotocol.transport.UTransport;
-import org.eclipse.uprotocol.transport.builder.UMessageBuilder;
-import org.eclipse.uprotocol.v1.UCode;
-import org.eclipse.uprotocol.v1.UMessage;
-import org.eclipse.uprotocol.v1.UStatus;
-import org.eclipse.uprotocol.v1.UUri;
-
-@ExtendWith(MockitoExtension.class)
-public class InMemoryUSubscriptionClientTest {
-
- @Mock
- private UTransport transport;
-
- @Mock
- private InMemoryRpcClient rpcClient;
-
- @Mock
- private SimpleNotifier notifier;
-
- @Mock
- private SubscriptionChangeHandler subscriptionChangeHandler;
-
- private final UUri topic = UUri.newBuilder()
- .setAuthorityName("hartley")
- .setUeId(3)
- .setUeVersionMajor(1)
- .setResourceId(0x8000)
- .build();
-
- private final UUri source = UUri.newBuilder()
- .setAuthorityName("Hartley")
- .setUeId(4)
- .setUeVersionMajor(1)
- .build();
- private final UListener listener = new UListener() {
- @Override
- public void onReceive(UMessage message) {
- // Do nothing
- }
- };
-
-
- @BeforeEach
- public void setup() {
- rpcClient = mock(InMemoryRpcClient.class);
- notifier = mock(SimpleNotifier.class);
- transport = mock(UTransport.class);
- }
-
-
- @Test
- @DisplayName("Testing creation of InMemoryUSubscriptionClient passing only the transport")
- public void testCreationOfInMemoryUSubscriptionClientPassingOnlyTheTransport() {
- when(transport.getSource()).thenReturn(source);
-
- when(transport.registerListener(any(UUri.class), any(UUri.class), any(UListener.class)))
- .thenReturn(CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()));
-
- assertDoesNotThrow(() -> {
- new InMemoryUSubscriptionClient(transport);
- });
-
- verify(transport, times(2)).getSource();
- verify(transport, times(2)).registerListener(any(), any(), any());
- }
-
-
- @Test
- @DisplayName("Testing creation of InMemoryUSubscriptionClient passing null for the transport")
- public void testCreationOfInMemoryUSubscriptionClientPassingNullForTheTransport() {
- assertThrows(NullPointerException.class, () -> {
- new InMemoryUSubscriptionClient(null);
- });
- }
-
-
- @Test
- @DisplayName("Testing simple mock of RpcClient and notifier happy path")
- public void testSimpleMockOfRpcClientAndNotifier() {
-
- final SubscriptionResponse response = SubscriptionResponse.newBuilder()
- .setTopic(topic)
- .setStatus(SubscriptionStatus.newBuilder().setState(SubscriptionStatus.State.SUBSCRIBED).build())
- .build();
-
- when(transport.registerListener(any(UUri.class), any(UListener.class)))
- .thenReturn(CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()));
-
- when(rpcClient.invokeMethod(any(UUri.class), any(UPayload.class), any(CallOptions.class)))
- .thenReturn(CompletableFuture.completedFuture(UPayload.pack(response)));
-
- when(notifier.registerNotificationListener(any(UUri.class), any(UListener.class)))
- .thenReturn(CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()));
-
-
- InMemoryUSubscriptionClient subscriber = new InMemoryUSubscriptionClient(transport, rpcClient, notifier);
- assertNotNull(subscriber);
-
- assertDoesNotThrow(() -> {
- assertEquals(subscriber.subscribe(topic, listener).toCompletableFuture().get().getStatus().getState(),
- SubscriptionStatus.State.SUBSCRIBED);
- });
-
- verify(rpcClient, times(1)).invokeMethod(any(), any(), any());
- verify(notifier, times(1)).registerNotificationListener(any(), any());
- verify(transport, times(1)).registerListener(any(), any());
- }
-
-
- @Test
- @DisplayName("Testing simple mock of RpcClient when usubscription returned SUBSCRIBE_PENDING")
- public void testSimpleMockOfRpcClientAndNotifierReturnedSubscribePending() {
-
- final SubscriptionResponse response = SubscriptionResponse.newBuilder()
- .setTopic(topic)
- .setStatus(SubscriptionStatus.newBuilder().setState(SubscriptionStatus.State.SUBSCRIBE_PENDING).build())
- .build();
-
- when(transport.registerListener(any(UUri.class), any(UListener.class)))
- .thenReturn(CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()));
-
- when(rpcClient.invokeMethod(any(UUri.class), any(UPayload.class), any(CallOptions.class)))
- .thenReturn(CompletableFuture.completedFuture(UPayload.pack(response)));
-
- when(notifier.registerNotificationListener(any(UUri.class), any(UListener.class)))
- .thenReturn(CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()));
-
- InMemoryUSubscriptionClient subscriber = new InMemoryUSubscriptionClient(transport, rpcClient, notifier);
- assertNotNull(subscriber);
-
- assertDoesNotThrow(() -> {
- assertEquals(subscriber.subscribe(topic, listener).toCompletableFuture().get().getStatus().getState(),
- SubscriptionStatus.State.SUBSCRIBE_PENDING);
- });
-
- verify(rpcClient, times(1)).invokeMethod(any(), any(), any());
- verify(notifier, times(1)).registerNotificationListener(any(), any());
- verify(transport, times(1)).registerListener(any(), any());
- }
-
-
- @Test
- @DisplayName("Testing simple mock of RpcClient and notifier when usubscription returned UNSUBSCRIBED")
- public void testSimpleMockWhenSubscriptionServiceReturnsUnsubscribed() {
-
- final SubscriptionResponse response = SubscriptionResponse.newBuilder()
- .setTopic(topic)
- .setStatus(SubscriptionStatus.newBuilder().setState(SubscriptionStatus.State.UNSUBSCRIBED).build())
- .build();
-
- when(rpcClient.invokeMethod(any(UUri.class), any(UPayload.class), any(CallOptions.class)))
- .thenReturn(CompletableFuture.completedFuture(UPayload.pack(response)));
-
- when(notifier.registerNotificationListener(any(UUri.class), any(UListener.class)))
- .thenReturn(CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()));
-
-
- InMemoryUSubscriptionClient subscriber = new InMemoryUSubscriptionClient(transport, rpcClient, notifier);
- assertNotNull(subscriber);
-
- assertDoesNotThrow(() -> {
- assertEquals(subscriber.subscribe(topic, listener).toCompletableFuture().get().getStatus().getState(),
- SubscriptionStatus.State.UNSUBSCRIBED);
- });
-
- verify(rpcClient, times(1)).invokeMethod(any(), any(), any());
- verify(notifier, times(1)).registerNotificationListener(any(), any());
- verify(transport, never()).registerListener(any(), any());
- }
-
-
-
- @Test
- @DisplayName("Test subscribe using mock RpcClient and SimplerNotifier when invokemethod return an exception")
- void testSubscribeUsingMockRpcClientAndSimplerNotifierWhenInvokemethodReturnAnException() {
- when(rpcClient.invokeMethod(any(UUri.class), any(UPayload.class), any(CallOptions.class)))
- .thenReturn(CompletableFuture.failedFuture(
- new UStatusException(UCode.PERMISSION_DENIED, "Not permitted")));
-
- when(notifier.registerNotificationListener(any(UUri.class), any(UListener.class)))
- .thenReturn(CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()));
-
- InMemoryUSubscriptionClient subscriber = new InMemoryUSubscriptionClient(transport, rpcClient, notifier);
- assertNotNull(subscriber);
-
- assertThrows(CompletionException.class, () -> {
- CompletionStage response = subscriber.subscribe(topic, listener);
- assertTrue(response.toCompletableFuture().isCompletedExceptionally());
- response.handle((r, e) -> {
- e = e.getCause();
- assertTrue(e instanceof UStatusException);
- assertEquals(((UStatusException) e).getCode(), UCode.PERMISSION_DENIED);
- return null;
- });
- response.toCompletableFuture().join();
- });
-
- verify(rpcClient, times(1)).invokeMethod(any(), any(), any());
- verify(notifier, times(1)).registerNotificationListener(any(), any());
- verify(transport, times(0)).registerListener(any(), any());
- }
-
-
- @Test
- @DisplayName("Test subscribe using mock RpcClient and SimplerNotifier when" +
- "we pass a subscription change notification handler")
- void testSubscribeWhenWePassASubscriptionChangeNotificationHandler() {
- when(transport.registerListener(any(UUri.class), any(UListener.class)))
- .thenReturn(CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()));
-
- when(rpcClient.invokeMethod(any(UUri.class), any(UPayload.class), any(CallOptions.class)))
- .thenReturn(CompletableFuture.completedFuture(UPayload.pack(SubscriptionResponse.newBuilder()
- .setTopic(topic)
- .setStatus(SubscriptionStatus.newBuilder().setState(SubscriptionStatus.State.SUBSCRIBED).build())
- .build())));
-
- when(notifier.registerNotificationListener(any(UUri.class), any(UListener.class)))
- .thenReturn(CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()));
-
- InMemoryUSubscriptionClient subscriber = new InMemoryUSubscriptionClient(transport, rpcClient, notifier);
- assertNotNull(subscriber);
-
- assertDoesNotThrow(() -> {
- assertEquals(subscriber.subscribe(topic, listener, CallOptions.DEFAULT, subscriptionChangeHandler)
- .toCompletableFuture().get().getStatus().getState(), SubscriptionStatus.State.SUBSCRIBED);
- });
-
- verify(rpcClient, times(1)).invokeMethod(any(), any(), any());
- verify(notifier, times(1)).registerNotificationListener(any(), any());
- verify(transport, times(1)).registerListener(any(), any());
- }
-
-
- @Test
- @DisplayName("Test subscribe to the same topic twice passing the same parameters")
- void testSubscribeWhenWeTryToSubscribeToTheSameTopicTwice() {
- when(transport.registerListener(any(UUri.class), any(UListener.class)))
- .thenReturn(CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()));
-
- when(rpcClient.invokeMethod(any(UUri.class), any(UPayload.class), any(CallOptions.class)))
- .thenReturn(CompletableFuture.completedFuture(UPayload.pack(SubscriptionResponse.newBuilder()
- .setTopic(topic)
- .setStatus(SubscriptionStatus.newBuilder().setState(SubscriptionStatus.State.SUBSCRIBED).build())
- .build())));
-
- when(notifier.registerNotificationListener(any(UUri.class), any(UListener.class)))
- .thenReturn(CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()));
-
- InMemoryUSubscriptionClient subscriber = new InMemoryUSubscriptionClient(transport, rpcClient, notifier);
- assertNotNull(subscriber);
-
- assertDoesNotThrow(() -> {
- assertEquals(subscriber.subscribe(topic, listener, CallOptions.DEFAULT, subscriptionChangeHandler)
- .toCompletableFuture().get().getStatus().getState(), SubscriptionStatus.State.SUBSCRIBED);
- });
-
- assertDoesNotThrow(() -> {
- assertEquals(subscriber.subscribe(topic, listener, CallOptions.DEFAULT, subscriptionChangeHandler)
- .toCompletableFuture().get().getStatus().getState(), SubscriptionStatus.State.SUBSCRIBED);
- });
-
- verify(rpcClient, times(2)).invokeMethod(any(), any(), any());
- verify(notifier, times(1)).registerNotificationListener(any(), any());
- verify(transport, times(2)).registerListener(any(), any());
- }
-
-
- @Test
- @DisplayName("Test subscribe to the same topic twice passing different SubscriptionChangeHandlers")
- void testSubscribeToTheSameTopicTwicePassingDifferentSubscriptionChangeHandlers() {
- when(transport.registerListener(any(UUri.class), any(UListener.class)))
- .thenReturn(CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()));
-
- when(rpcClient.invokeMethod(any(UUri.class), any(UPayload.class), any(CallOptions.class)))
- .thenReturn(CompletableFuture.completedFuture(UPayload.pack(SubscriptionResponse.newBuilder()
- .setTopic(topic)
- .setStatus(SubscriptionStatus.newBuilder().setState(SubscriptionStatus.State.SUBSCRIBE_PENDING).build())
- .build())));
-
- when(notifier.registerNotificationListener(any(UUri.class), any(UListener.class)))
- .thenReturn(CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()));
-
- InMemoryUSubscriptionClient subscriber = new InMemoryUSubscriptionClient(transport, rpcClient, notifier);
- assertNotNull(subscriber);
-
- assertDoesNotThrow(() -> {
- assertEquals(subscriber.subscribe(topic, listener, CallOptions.DEFAULT, subscriptionChangeHandler)
- .toCompletableFuture().get().getStatus().getState(), SubscriptionStatus.State.SUBSCRIBE_PENDING);
- });
-
- assertThrows( CompletionException.class, () -> {
- CompletionStage response = subscriber.subscribe(
- topic,
- listener,
- CallOptions.DEFAULT,
- mock(SubscriptionChangeHandler.class));
-
- assertTrue(response.toCompletableFuture().isCompletedExceptionally());
- response.handle((r, e) -> {
- e = e.getCause();
- assertTrue(e instanceof UStatusException);
- assertEquals(((UStatusException) e).getCode(), UCode.ALREADY_EXISTS);
- return null;
- });
- response.toCompletableFuture().join();
- });
-
- verify(rpcClient, times(2)).invokeMethod(any(), any(), any());
- verify(notifier, times(1)).registerNotificationListener(any(), any());
- verify(transport, times(2)).registerListener(any(), any());
- }
-
-
- @Test
- @DisplayName("Test unsubscribe using mock RpcClient and SimplerNotifier")
- void testUnsubscribeUsingMockRpcClientAndSimplerNotifier() {
- when(notifier.registerNotificationListener(any(UUri.class), any(UListener.class)))
- .thenReturn(CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()));
-
- when(transport.unregisterListener(any(UUri.class), any(UListener.class)))
- .thenReturn(CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()));
-
- when(rpcClient.invokeMethod(any(UUri.class), any(UPayload.class), any(CallOptions.class)))
- .thenReturn(CompletableFuture.completedFuture(UPayload.pack(UnsubscribeResponse.getDefaultInstance())));
-
- when(notifier.unregisterNotificationListener(any(UUri.class), any(UListener.class)))
- .thenReturn(CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()));
-
- InMemoryUSubscriptionClient subscriber = new InMemoryUSubscriptionClient(transport, rpcClient, notifier);
- assertNotNull(subscriber);
-
- assertDoesNotThrow(() -> {
- assertEquals(subscriber.unsubscribe(topic, listener).toCompletableFuture().get().getCode(), UCode.OK);
- });
-
- subscriber.close();
-
- verify(rpcClient, times(1)).invokeMethod(any(), any(), any());
- verify(notifier, times(1)).unregisterNotificationListener(any(), any());
- verify(notifier, times(1)).registerNotificationListener(any(), any());
- verify(transport, times(1)).unregisterListener(any(), any());
- }
-
-
- @Test
- @DisplayName("Test unsubscribe using when invokemethod return an exception")
- void testUnsubscribeWhenInvokemethodReturnAnException() {
- when(notifier.registerNotificationListener(any(UUri.class), any(UListener.class)))
- .thenReturn(CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()));
-
- when(rpcClient.invokeMethod(any(UUri.class), any(UPayload.class), any(CallOptions.class)))
- .thenReturn(CompletableFuture.failedStage(new UStatusException(UCode.CANCELLED, "Operation cancelled")));
-
- when(notifier.unregisterNotificationListener(any(UUri.class), any(UListener.class)))
- .thenReturn(CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()));
-
- InMemoryUSubscriptionClient subscriber = new InMemoryUSubscriptionClient(transport, rpcClient, notifier);
- assertNotNull(subscriber);
-
- assertDoesNotThrow(() -> {
- CompletionStage response = subscriber.unsubscribe(topic, listener);
- assertNotNull(response);
- assertFalse(response.toCompletableFuture().isCompletedExceptionally());
- assertEquals(response.toCompletableFuture().get().getCode(), UCode.CANCELLED);
- });
-
- subscriber.close();
-
- verify(rpcClient, times(1)).invokeMethod(any(), any(), any());
- verify(notifier, times(1)).unregisterNotificationListener(any(), any());
- verify(notifier, times(1)).registerNotificationListener(any(), any());
- verify(transport, times(0)).unregisterListener(any(), any());
- }
-
-
- @Test
- @DisplayName("Test unsubscribe when invokemethod returned OK but we failed to unregister the listener")
- void testUnsubscribeWhenInvokemethodReturnedOkButWeFailedToUnregisterTheListener() {
- when(notifier.registerNotificationListener(any(UUri.class), any(UListener.class)))
- .thenReturn(CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()));
-
- when(rpcClient.invokeMethod(any(UUri.class), any(UPayload.class), any(CallOptions.class)))
- .thenReturn(CompletableFuture.completedFuture(UPayload.pack(SubscriptionResponse.newBuilder()
- .setTopic(topic)
- .setStatus(SubscriptionStatus.newBuilder().setState(SubscriptionStatus.State.SUBSCRIBE_PENDING).build())
- .build())));
-
- when(transport.unregisterListener(any(UUri.class), any(UListener.class)))
- .thenReturn(CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.ABORTED).build()));
-
- when(notifier.unregisterNotificationListener(any(UUri.class), any(UListener.class)))
- .thenReturn(CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()));
-
- InMemoryUSubscriptionClient subscriber = new InMemoryUSubscriptionClient(transport, rpcClient, notifier);
- assertNotNull(subscriber);
-
- assertDoesNotThrow(() -> {
- CompletionStage response = subscriber.unsubscribe(topic, listener);
- assertNotNull(response);
- assertFalse(response.toCompletableFuture().isCompletedExceptionally());
- assertEquals(response.toCompletableFuture().get().getCode(), UCode.ABORTED);
- });
-
- subscriber.close();
-
- verify(rpcClient, times(1)).invokeMethod(any(), any(), any());
- verify(notifier, times(1)).unregisterNotificationListener(any(), any());
- verify(notifier, times(1)).registerNotificationListener(any(), any());
- verify(transport, times(1)).unregisterListener(any(), any());
- }
-
-
- @Test
- @DisplayName("Test handling going from SUBSCRIBE_PENDING to SUBSCRIBED state")
- void testHandlingGoingFromSubscribePendingToSubscribedState()
- throws InterruptedException, BrokenBarrierException {
-
- // Create a CyclicBarrier with a count of 2 so we synchronize the subscription with the
- // changes in states for the usubscription service
- CyclicBarrier barrier = new CyclicBarrier(2);
-
- when(transport.registerListener(any(UUri.class), any(UListener.class)))
- .thenReturn(CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()));
-
- when(rpcClient.invokeMethod(any(UUri.class), any(UPayload.class), any(CallOptions.class)))
- .thenReturn(CompletableFuture.completedFuture(UPayload.pack(SubscriptionResponse.newBuilder()
- .setTopic(topic)
- .setStatus(SubscriptionStatus.newBuilder().setState(SubscriptionStatus.State.SUBSCRIBE_PENDING).build())
- .build())));
-
- // Fake sending the subscription change notification
- when(notifier.registerNotificationListener(any(UUri.class), any(UListener.class)))
- .thenAnswer(invocation -> {
- Executors.newSingleThreadExecutor().execute(new Runnable() {
- @Override
- public void run() {
- try {
- barrier.await();
- } catch (Exception e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- Update update = Update.newBuilder().setTopic(topic).setStatus(SubscriptionStatus.newBuilder()
- .setState(SubscriptionStatus.State.SUBSCRIBED).build()).build();
- UMessage message = UMessageBuilder.notification(topic, source).build(UPayload.pack(update));
- invocation.getArgument(1, UListener.class).onReceive(message);
- }
- });
- return CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build());
- });
-
-
- InMemoryUSubscriptionClient subscriber = new InMemoryUSubscriptionClient(transport, rpcClient, notifier);
- assertNotNull(subscriber);
-
- SubscriptionChangeHandler handler = new SubscriptionChangeHandler() {
- @Override
- public void handleSubscriptionChange(UUri topic, SubscriptionStatus status) {
- assertEquals(status.getState(), SubscriptionStatus.State.SUBSCRIBED);
- }
- };
- assertDoesNotThrow(() -> {
- assertEquals(subscriber.subscribe(topic, listener, CallOptions.DEFAULT, handler)
- .toCompletableFuture().get().getStatus().getState(), SubscriptionStatus.State.SUBSCRIBE_PENDING);
- });
-
- // Wait for a specific time (500ms)
- Thread.sleep(100);
-
- // Release the barrier by calling await() again
- barrier.await();
-
- verify(rpcClient, times(1)).invokeMethod(any(), any(), any());
- verify(notifier, times(1)).registerNotificationListener(any(), any());
- verify(transport, times(1)).registerListener(any(), any());
- }
-
- @Test
- @DisplayName("Test handling going from SUBSCRIBE_PENDING to SUBSCRIBED state but handler throws exception")
- void testHandlingGoingFromSubscribePendingToSubscribedStateButHandlerThrowsException()
- throws InterruptedException, BrokenBarrierException {
-
- // Create a CyclicBarrier with a count of 2 so we synchronize the subscription with the
- // changes in states for the usubscription service
- CyclicBarrier barrier = new CyclicBarrier(2);
-
- when(transport.registerListener(any(UUri.class), any(UListener.class)))
- .thenReturn(CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()));
-
- when(rpcClient.invokeMethod(any(UUri.class), any(UPayload.class), any(CallOptions.class)))
- .thenReturn(CompletableFuture.completedFuture(UPayload.pack(SubscriptionResponse.newBuilder()
- .setTopic(topic)
- .setStatus(SubscriptionStatus.newBuilder().setState(SubscriptionStatus.State.SUBSCRIBE_PENDING).build())
- .build())));
-
- // Fake sending the subscription change notification
- when(notifier.registerNotificationListener(any(UUri.class), any(UListener.class)))
- .thenAnswer(invocation -> {
- Executors.newSingleThreadExecutor().execute(new Runnable() {
- @Override
- public void run() {
- try {
- barrier.await();
- } catch (Exception e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- Update update = Update.newBuilder().setTopic(topic).setStatus(SubscriptionStatus.newBuilder()
- .setState(SubscriptionStatus.State.SUBSCRIBED).build()).build();
- UMessage message = UMessageBuilder.notification(topic, source).build(UPayload.pack(update));
- invocation.getArgument(1, UListener.class).onReceive(message);
- }
- });
- return CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build());
- });
-
-
- InMemoryUSubscriptionClient subscriber = new InMemoryUSubscriptionClient(transport, rpcClient, notifier);
- assertNotNull(subscriber);
-
- SubscriptionChangeHandler handler = new SubscriptionChangeHandler() {
- @Override
- public void handleSubscriptionChange(UUri topic, SubscriptionStatus status) {
- throw new UnsupportedOperationException("Throwing exception in the handler");
- }
- };
- assertDoesNotThrow(() -> {
- assertEquals(subscriber.subscribe(topic, listener, CallOptions.DEFAULT, handler)
- .toCompletableFuture().get().getStatus().getState(), SubscriptionStatus.State.SUBSCRIBE_PENDING);
- });
-
- // Wait for a specific time (500ms)
- Thread.sleep(100);
-
- // Release the barrier by calling await() again
- barrier.await();
-
- verify(rpcClient, times(1)).invokeMethod(any(), any(), any());
- verify(notifier, times(1)).registerNotificationListener(any(), any());
- verify(transport, times(1)).registerListener(any(), any());
- }
-
-
- @Test
- @DisplayName("Test notification handling when we pass the wrong message type to the handler")
- void testNotificationHandlingWhenWePassTheWrongMessageTypeToTheHandler()
- throws InterruptedException, BrokenBarrierException {
-
- // Create a CyclicBarrier with a count of 2 so we synchronize the subscription with the
- // changes in states for the usubscription service
- CyclicBarrier barrier = new CyclicBarrier(2);
-
- when(transport.registerListener(any(UUri.class), any(UListener.class)))
- .thenReturn(CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()));
-
- when(rpcClient.invokeMethod(any(UUri.class), any(UPayload.class), any(CallOptions.class)))
- .thenReturn(CompletableFuture.completedFuture(UPayload.packToAny(SubscriptionResponse.newBuilder()
- .setTopic(topic)
- .setStatus(SubscriptionStatus.newBuilder().setState(SubscriptionStatus.State.SUBSCRIBE_PENDING).build())
- .build())));
-
- // Fake sending the subscription change notification
- when(notifier.registerNotificationListener(any(UUri.class), any(UListener.class)))
- .thenAnswer(invocation -> {
- Executors.newSingleThreadExecutor().execute(new Runnable() {
- @Override
- public void run() {
- try {
- barrier.await();
- } catch (Exception e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- Update update = Update.newBuilder().setTopic(topic).setStatus(SubscriptionStatus.newBuilder()
- .setState(SubscriptionStatus.State.SUBSCRIBED).build()).build();
- UMessage message = UMessageBuilder.publish(topic).build(UPayload.pack(update));
- invocation.getArgument(1, UListener.class).onReceive(message);
- }
- });
- return CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build());
- });
-
-
- InMemoryUSubscriptionClient subscriber = new InMemoryUSubscriptionClient(transport, rpcClient, notifier);
- assertNotNull(subscriber);
-
- SubscriptionChangeHandler handler = new SubscriptionChangeHandler() {
- @Override
- public void handleSubscriptionChange(UUri topic, SubscriptionStatus status) {
- throw new UnsupportedOperationException("This handler should not be called");
- }
- };
- assertDoesNotThrow(() -> {
- assertEquals(subscriber.subscribe(topic, listener, CallOptions.DEFAULT, handler)
- .toCompletableFuture().get().getStatus().getState(), SubscriptionStatus.State.SUBSCRIBE_PENDING);
- });
-
- // Wait for a specific time (500ms)
- Thread.sleep(100);
-
- // Release the barrier by calling await() again
- barrier.await();
-
- verify(rpcClient, times(1)).invokeMethod(any(), any(), any());
- verify(notifier, times(1)).registerNotificationListener(any(), any());
- verify(transport, times(1)).registerListener(any(), any());
- }
-
-
- @Test
- @DisplayName("Test notification handling when we get something other than the Update message")
- void testNotificationHandlingWhenWeGetSomethingOtherThanTheUpdateMessage()
- throws InterruptedException, BrokenBarrierException {
-
- // Create a CyclicBarrier with a count of 2 so we synchronize the subscription with the
- // changes in states for the usubscription service
- CyclicBarrier barrier = new CyclicBarrier(2);
-
- when(transport.registerListener(any(UUri.class), any(UListener.class)))
- .thenReturn(CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()));
-
- when(rpcClient.invokeMethod(any(UUri.class), any(UPayload.class), any(CallOptions.class)))
- .thenReturn(CompletableFuture.completedFuture(UPayload.pack(SubscriptionResponse.newBuilder()
- .setTopic(topic)
- .setStatus(SubscriptionStatus.newBuilder().setState(SubscriptionStatus.State.SUBSCRIBE_PENDING).build())
- .build())));
-
- // Fake sending the subscription change notification
- when(notifier.registerNotificationListener(any(UUri.class), any(UListener.class)))
- .thenAnswer(invocation -> {
- Executors.newSingleThreadExecutor().execute(new Runnable() {
- @Override
- public void run() {
- try {
- barrier.await();
- } catch (Exception e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- UMessage message = UMessageBuilder.notification(topic, source)
- .build(UPayload.packToAny(source));
- invocation.getArgument(1, UListener.class).onReceive(message);
- }
- });
- return CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build());
- });
-
-
- InMemoryUSubscriptionClient subscriber = new InMemoryUSubscriptionClient(transport, rpcClient, notifier);
- assertNotNull(subscriber);
-
- SubscriptionChangeHandler handler = new SubscriptionChangeHandler() {
- @Override
- public void handleSubscriptionChange(UUri topic, SubscriptionStatus status) {
- throw new UnsupportedOperationException("This handler should not be called");
- }
- };
- assertDoesNotThrow(() -> {
- assertEquals(subscriber.subscribe(topic, listener, CallOptions.DEFAULT, handler)
- .toCompletableFuture().get().getStatus().getState(), SubscriptionStatus.State.SUBSCRIBE_PENDING);
- });
-
- // Wait for a specific time (500ms)
- Thread.sleep(100);
-
- // Release the barrier by calling await() again
- barrier.await();
-
- verify(rpcClient, times(1)).invokeMethod(any(), any(), any());
- verify(notifier, times(1)).registerNotificationListener(any(), any());
- verify(transport, times(1)).registerListener(any(), any());
- }
-
-
- @Test
- @DisplayName("Test notification handling when we didn't register a notification handler")
- void testNotificationHandlingWhenWeDidntRegisterANotificationHandler()
- throws InterruptedException, BrokenBarrierException {
-
- // Create a CyclicBarrier with a count of 2 so we synchronize the subscription with the
- // changes in states for the usubscription service
- CyclicBarrier barrier = new CyclicBarrier(2);
-
- when(transport.registerListener(any(UUri.class), any(UListener.class)))
- .thenReturn(CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()));
-
- when(rpcClient.invokeMethod(any(UUri.class), any(UPayload.class), any(CallOptions.class)))
- .thenReturn(CompletableFuture.completedFuture(UPayload.pack(SubscriptionResponse.newBuilder()
- .setTopic(topic)
- .setStatus(SubscriptionStatus.newBuilder().setState(SubscriptionStatus.State.SUBSCRIBE_PENDING).build())
- .build())));
-
- // Fake sending the subscription change notification
- when(notifier.registerNotificationListener(any(UUri.class), any(UListener.class)))
- .thenAnswer(invocation -> {
- Executors.newSingleThreadExecutor().execute(new Runnable() {
- @Override
- public void run() {
- try {
- barrier.await();
- } catch (Exception e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- Update update = Update.newBuilder().setTopic(topic).setStatus(SubscriptionStatus.newBuilder()
- .setState(SubscriptionStatus.State.SUBSCRIBED).build()).build();
- UMessage message = UMessageBuilder.notification(topic, source).build(UPayload.pack(update));
- invocation.getArgument(1, UListener.class).onReceive(message);
- }
- });
- return CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build());
- });
-
-
- InMemoryUSubscriptionClient subscriber = new InMemoryUSubscriptionClient(transport, rpcClient, notifier);
- assertNotNull(subscriber);
-
- assertDoesNotThrow(() -> {
- assertEquals(subscriber.subscribe(topic, listener)
- .toCompletableFuture().get().getStatus().getState(), SubscriptionStatus.State.SUBSCRIBE_PENDING);
- });
-
- // Wait for a specific time (100ms)
- Thread.sleep(100);
-
- // Release the barrier by calling await() again
- barrier.await();
-
- verify(rpcClient, times(1)).invokeMethod(any(), any(), any());
- verify(notifier, times(1)).registerNotificationListener(any(), any());
- verify(transport, times(1)).registerListener(any(), any());
- }
-
-
- @Test
- @DisplayName("Test registerNotification() api when passed a null topic")
- void testRegisterNotificationApiWhenPassedANullTopic() {
- when(notifier.registerNotificationListener(any(UUri.class), any(UListener.class)))
- .thenReturn(CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()));
-
- InMemoryUSubscriptionClient subscriber = new InMemoryUSubscriptionClient(transport, rpcClient, notifier);
- assertNotNull(subscriber);
-
- assertThrows(NullPointerException.class, () -> {
- subscriber.registerForNotifications(null, subscriptionChangeHandler);
- });
-
- verify(notifier, times(1)).registerNotificationListener(any(), any());
- }
-
-
- @Test
- @DisplayName("Test registerNotification() api when passed a null handler")
- void testRegisterNotificationApiWhenPassedANullHandler() {
- when(notifier.registerNotificationListener(any(UUri.class), any(UListener.class)))
- .thenReturn(CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()));
-
- InMemoryUSubscriptionClient subscriber = new InMemoryUSubscriptionClient(transport, rpcClient, notifier);
- assertNotNull(subscriber);
-
- assertThrows(NullPointerException.class, () -> {
- subscriber.registerForNotifications(topic, null);
- });
-
- verify(notifier, times(1)).registerNotificationListener(any(), any());
- }
-
- @Test
- @DisplayName("Test unregisterListener() api for the happy path")
- void testUnregisterListenerApiForTheHappyPath() {
- when(notifier.registerNotificationListener(any(UUri.class), any(UListener.class)))
- .thenReturn(CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()));
-
- when(transport.registerListener(any(UUri.class), any(UListener.class)))
- .thenReturn(CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()));
-
- when(transport.unregisterListener(any(UUri.class), any(UListener.class)))
- .thenReturn(CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()));
-
- when(rpcClient.invokeMethod(any(UUri.class), any(UPayload.class), any(CallOptions.class)))
- .thenReturn(CompletableFuture.completedFuture(UPayload.pack(SubscriptionResponse.newBuilder()
- .setTopic(topic)
- .setStatus(SubscriptionStatus.newBuilder().setState(SubscriptionStatus.State.SUBSCRIBED).build())
- .build())));
-
- InMemoryUSubscriptionClient subscriber = new InMemoryUSubscriptionClient(transport, rpcClient, notifier);
- assertNotNull(subscriber);
-
- assertDoesNotThrow(() -> {
- assertEquals(subscriber.subscribe(topic, listener).toCompletableFuture().get().getStatus().getState(),
- SubscriptionStatus.State.SUBSCRIBED);
- assertEquals(subscriber.unregisterListener(topic, listener).toCompletableFuture().get().getCode(),
- UCode.OK);
- });
-
- verify(transport, times(1)).unregisterListener(any(), any());
- }
-
-
- @Test
- @DisplayName("Test registerNotification() api when passed a valid topic and handler")
- void testRegisterNotificationApiWhenPassedAValidTopicAndHandler() {
- when(notifier.registerNotificationListener(any(UUri.class), any(UListener.class)))
- .thenReturn(CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()));
-
- when(transport.getSource()).thenReturn(source);
-
- when(rpcClient.invokeMethod(any(UUri.class), any(UPayload.class), any(CallOptions.class)))
- .thenReturn(CompletableFuture.completedFuture(UPayload.pack(NotificationsResponse.getDefaultInstance())));
-
- InMemoryUSubscriptionClient subscriber = new InMemoryUSubscriptionClient(transport, rpcClient, notifier);
- assertNotNull(subscriber);
-
- UUri topic = UUri.newBuilder(transport.getSource()).setResourceId(0x8000).build();
-
- assertDoesNotThrow(() -> subscriber.registerForNotifications(topic, subscriptionChangeHandler)
- .toCompletableFuture().get());
- verify(notifier, times(1)).registerNotificationListener(any(), any());
- }
-
-
- @Test
- @DisplayName("Test registerNotification() api when invokeMethod() throws an exception")
- void testRegisterNotificationApiWhenInvokeMethodThrowsAnException() {
- when(notifier.registerNotificationListener(any(UUri.class), any(UListener.class)))
- .thenReturn(CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()));
-
- when(transport.getSource()).thenReturn(source);
-
- when(rpcClient.invokeMethod(any(UUri.class), any(UPayload.class), any(CallOptions.class)))
- .thenReturn(CompletableFuture.failedFuture(
- new UStatusException(UCode.PERMISSION_DENIED, "Not permitted")));
-
- InMemoryUSubscriptionClient subscriber = new InMemoryUSubscriptionClient(transport, rpcClient, notifier);
- assertNotNull(subscriber);
-
- UUri topic = UUri.newBuilder(transport.getSource()).setResourceId(0x8000).build();
-
- assertDoesNotThrow(() -> {
- var response = subscriber.registerForNotifications(topic, subscriptionChangeHandler);
- assertTrue(response.toCompletableFuture().isCompletedExceptionally());
- response.handle((r, e) -> {
- e = e.getCause();
- assertTrue(e instanceof UStatusException);
- assertEquals(((UStatusException) e).getCode(), UCode.PERMISSION_DENIED);
- return null;
- }).toCompletableFuture().get();
- });
- }
-
-
- @Test
- @DisplayName("Test registerNotification() calling the API twice passing the same topic and handler")
- void testRegisterNotificationApiCallingTheApiTwicePassingTheSameTopicAndHandler() {
- when(notifier.registerNotificationListener(any(UUri.class), any(UListener.class)))
- .thenReturn(CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()));
-
- when(transport.getSource()).thenReturn(source);
-
- when(rpcClient.invokeMethod(any(UUri.class), any(UPayload.class), any(CallOptions.class)))
- .thenReturn(CompletableFuture.completedFuture(UPayload.pack(NotificationsResponse.getDefaultInstance())));
-
- InMemoryUSubscriptionClient subscriber = new InMemoryUSubscriptionClient(transport, rpcClient, notifier);
- assertNotNull(subscriber);
-
- UUri topic = UUri.newBuilder(transport.getSource()).setResourceId(0x8000).build();
-
- assertDoesNotThrow(() -> {
- assertTrue(NotificationsResponse.getDefaultInstance().equals(
- subscriber.registerForNotifications(topic, subscriptionChangeHandler).toCompletableFuture().get()));
- });
-
- assertDoesNotThrow(() -> {
- assertTrue(NotificationsResponse.getDefaultInstance().equals(
- subscriber.registerForNotifications(topic, subscriptionChangeHandler).toCompletableFuture().get()));
- });
-
- verify(notifier, times(1)).registerNotificationListener(any(), any());
- }
-
-
- @Test
- @DisplayName("Test registerNotification() calling the API twice passing the same topic but different handlers")
- void testRegisterNotificationApiCallingTheApiTwicePassingTheSameTopicButDifferentHandlers() {
- when(notifier.registerNotificationListener(any(UUri.class), any(UListener.class)))
- .thenReturn(CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()));
-
- when(transport.getSource()).thenReturn(source);
-
- when(rpcClient.invokeMethod(any(UUri.class), any(UPayload.class), any(CallOptions.class)))
- .thenReturn(CompletableFuture.completedFuture(UPayload.pack(NotificationsResponse.getDefaultInstance())));
-
- InMemoryUSubscriptionClient subscriber = new InMemoryUSubscriptionClient(transport, rpcClient, notifier);
- assertNotNull(subscriber);
-
- UUri topic = UUri.newBuilder(transport.getSource()).setResourceId(0x8000).build();
-
- assertDoesNotThrow(() -> {
- assertTrue(NotificationsResponse.getDefaultInstance().equals(
- subscriber.registerForNotifications(topic, subscriptionChangeHandler).toCompletableFuture().get()));
- });
-
- assertDoesNotThrow(() -> {
- CompletionStage response = subscriber.registerForNotifications(
- topic,
- mock(SubscriptionChangeHandler.class));
- assertTrue(response.toCompletableFuture().isCompletedExceptionally());
-
- response.handle((r, e) -> {
- assertNotNull(e);
- e = e.getCause();
- assertTrue(e instanceof UStatusException);
- assertEquals(((UStatusException) e).getCode(), UCode.ALREADY_EXISTS);
- return null;
- }).toCompletableFuture().join();
- });
-
- verify(notifier, times(1)).registerNotificationListener(any(), any());
- }
-
-
- @Test
- @DisplayName("Test unregisterNotification() api for the happy path")
- void testUnregisterNotificationApiForTheHappyPath() {
- when(notifier.registerNotificationListener(any(UUri.class), any(UListener.class)))
- .thenReturn(CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()));
-
- when(transport.getSource()).thenReturn(source);
-
- when(rpcClient.invokeMethod(any(UUri.class), any(UPayload.class), any(CallOptions.class)))
- .thenReturn(CompletableFuture.completedFuture(UPayload.pack(NotificationsResponse.getDefaultInstance())));
-
- InMemoryUSubscriptionClient subscriber = new InMemoryUSubscriptionClient(transport, rpcClient, notifier);
- assertNotNull(subscriber);
-
- UUri topic = UUri.newBuilder(transport.getSource()).setResourceId(0x8000).build();
-
- assertDoesNotThrow(() -> {
- subscriber.registerForNotifications(topic, subscriptionChangeHandler).toCompletableFuture().get();
- subscriber.unregisterForNotifications(topic).toCompletableFuture().get();
- });
-
- verify(notifier, times(1)).registerNotificationListener(any(), any());
- }
-
-
- @Test
- @DisplayName("Test unregisterNotification() api when passed a null topic")
- void testUnregisterNotificationApiWhenPassedANullTopic() {
- when(notifier.registerNotificationListener(any(UUri.class), any(UListener.class)))
- .thenReturn(CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()));
-
- InMemoryUSubscriptionClient subscriber = new InMemoryUSubscriptionClient(transport, rpcClient, notifier);
- assertNotNull(subscriber);
-
- assertThrows(NullPointerException.class, () -> {
- subscriber.unregisterForNotifications(null);
- });
-
- verify(notifier, times(1)).registerNotificationListener(any(), any());
- }
-
-
- @Test
- @DisplayName("Test unregisterNotification() api when passed a null handler")
- void testUnregisterNotificationApiWhenPassedANullHandler() {
- when(notifier.registerNotificationListener(any(UUri.class), any(UListener.class)))
- .thenReturn(CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()));
-
- InMemoryUSubscriptionClient subscriber = new InMemoryUSubscriptionClient(transport, rpcClient, notifier);
- assertNotNull(subscriber);
-
- assertThrows(NullPointerException.class, () -> {
- subscriber.unregisterForNotifications(topic, null);
- });
-
- verify(notifier, times(1)).registerNotificationListener(any(), any());
- }
-
-
- @Test
- @DisplayName("Test calling unregisterNotification() api when we never registered the notification below")
- void testCallingUnregisterNotificationApiWhenWeNeverRegisteredTheNotificationBelow() {
- when(notifier.registerNotificationListener(any(UUri.class), any(UListener.class)))
- .thenReturn(CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()));
-
- when(rpcClient.invokeMethod(any(UUri.class), any(UPayload.class), any(CallOptions.class)))
- .thenReturn(CompletableFuture.failedFuture(new UStatusException(UCode.NOT_FOUND, "Not found")));
-
- InMemoryUSubscriptionClient subscriber = new InMemoryUSubscriptionClient(transport, rpcClient, notifier);
- assertNotNull(subscriber);
-
- UUri topic = UUri.newBuilder(source).setResourceId(0x8000).build();
-
- assertDoesNotThrow(() -> {
- CompletionStage response = subscriber.unregisterForNotifications(topic);
- assertTrue(response.toCompletableFuture().isCompletedExceptionally());
-
- response.handle((r, e) -> {
- assertNotNull(e);
- e = e.getCause();
- assertTrue(e instanceof UStatusException);
- assertEquals(((UStatusException) e).getCode(), UCode.NOT_FOUND);
- return null;
- }).toCompletableFuture().join();
- });
-
- verify(transport, never()).getSource();
- verify(notifier, times(1)).registerNotificationListener(any(), any());
- }
-
-
- @Test
- @DisplayName("Test fetchSubscribers() when pssing null topic")
- void testFetchSubscribersWhenPassingNullTopic() {
- when(notifier.registerNotificationListener(any(UUri.class), any(UListener.class)))
- .thenReturn(CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()));
-
- InMemoryUSubscriptionClient subscriber = new InMemoryUSubscriptionClient(transport, rpcClient, notifier);
- assertNotNull(subscriber);
-
- assertThrows(NullPointerException.class, () -> {
- subscriber.fetchSubscribers(null).toCompletableFuture().get();
- });
- }
-
-
- @Test
- @DisplayName("Test fetchSubscribers() when passing a valid topic")
- void testFetchSubscribersWhenPassingAValidTopic() {
- when(notifier.registerNotificationListener(any(UUri.class), any(UListener.class)))
- .thenReturn(CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()));
-
- when(rpcClient.invokeMethod(any(UUri.class), any(UPayload.class), any(CallOptions.class)))
- .thenReturn(CompletableFuture.completedFuture(
- UPayload.pack(FetchSubscribersResponse.getDefaultInstance())));
-
- InMemoryUSubscriptionClient subscriber = new InMemoryUSubscriptionClient(transport, rpcClient, notifier);
- assertNotNull(subscriber);
-
- assertDoesNotThrow(() -> {
- assertTrue(FetchSubscribersResponse.getDefaultInstance().equals(
- subscriber.fetchSubscribers(topic).toCompletableFuture().get()));
- });
-
- verify(notifier, times(1)).registerNotificationListener(any(), any());
- }
-
-
- @Test
- @DisplayName("Test fetchSubscribers() when passing when invokeMethod returns NOT_PERMITTED")
- void testFetchSubscribersWhenPassingWhenInvokeMethodReturnsNotPermitted() {
- when(notifier.registerNotificationListener(any(UUri.class), any(UListener.class)))
- .thenReturn(CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()));
-
- when(rpcClient.invokeMethod(any(UUri.class), any(UPayload.class), any(CallOptions.class)))
- .thenReturn(CompletableFuture.failedFuture(
- new UStatusException(UCode.PERMISSION_DENIED, "Not permitted")));
-
- InMemoryUSubscriptionClient subscriber = new InMemoryUSubscriptionClient(transport, rpcClient, notifier);
- assertNotNull(subscriber);
-
- assertDoesNotThrow(() -> {
- CompletionStage response = subscriber.fetchSubscribers(topic);
- assertTrue(response.toCompletableFuture().isCompletedExceptionally());
- response.handle((r, e) -> {
- e = e.getCause();
- assertTrue(e instanceof UStatusException);
- assertEquals(((UStatusException) e).getCode(), UCode.PERMISSION_DENIED);
- return null;
- }).toCompletableFuture().join();
- });
-
- verify(notifier, times(1)).registerNotificationListener(any(), any());
- }
-
-
- @Test
- @DisplayName("Test fetchSubscriptions() passing null FetchSubscriptionRequest")
- void testFetchSubscriptionsPassingNullFetchSubscriptionRequest() {
- when(notifier.registerNotificationListener(any(UUri.class), any(UListener.class)))
- .thenReturn(CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()));
-
- InMemoryUSubscriptionClient subscriber = new InMemoryUSubscriptionClient(transport, rpcClient, notifier);
- assertNotNull(subscriber);
-
- assertThrows(NullPointerException.class, () -> {
- subscriber.fetchSubscriptions(null).toCompletableFuture().get();
- });
-
- verify(notifier, times(1)).registerNotificationListener(any(), any());
- }
-
-
- @Test
- @DisplayName("Test fetchSubscriptions() passing a valid FetchSubscriptionRequest")
- void testFetchSubscriptionsPassingAValidFetchSubscriptionRequest() {
- when(notifier.registerNotificationListener(any(UUri.class), any(UListener.class)))
- .thenReturn(CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()));
-
- when(rpcClient.invokeMethod(any(UUri.class), any(UPayload.class), any(CallOptions.class)))
- .thenReturn(CompletableFuture.completedFuture(
- UPayload.pack(FetchSubscriptionsResponse.getDefaultInstance())));
-
- InMemoryUSubscriptionClient subscriber = new InMemoryUSubscriptionClient(transport, rpcClient, notifier);
- assertNotNull(subscriber);
-
- FetchSubscriptionsRequest request = FetchSubscriptionsRequest.newBuilder().setTopic(topic).build();
-
- assertDoesNotThrow(() -> {
- assertTrue(FetchSubscriptionsResponse.getDefaultInstance().equals(
- subscriber.fetchSubscriptions(request).toCompletableFuture().get()));
- });
-
- verify(notifier, times(1)).registerNotificationListener(any(), any());
- }
-}
diff --git a/src/test/java/org/eclipse/uprotocol/client/usubscription/v3/RpcClientBasedUSubscriptionClientTest.java b/src/test/java/org/eclipse/uprotocol/client/usubscription/v3/RpcClientBasedUSubscriptionClientTest.java
new file mode 100644
index 00000000..f57651ee
--- /dev/null
+++ b/src/test/java/org/eclipse/uprotocol/client/usubscription/v3/RpcClientBasedUSubscriptionClientTest.java
@@ -0,0 +1,151 @@
+/**
+ * SPDX-FileCopyrightText: 2025 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Apache License Version 2.0 which is available at
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package org.eclipse.uprotocol.client.usubscription.v3;
+
+import java.util.concurrent.CompletableFuture;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.mock;
+
+import org.eclipse.uprotocol.communication.CallOptions;
+import org.eclipse.uprotocol.communication.InMemoryRpcClient;
+import org.eclipse.uprotocol.communication.RpcClient;
+import org.eclipse.uprotocol.communication.UPayload;
+import org.eclipse.uprotocol.core.usubscription.v3.FetchSubscribersRequest;
+import org.eclipse.uprotocol.core.usubscription.v3.FetchSubscribersResponse;
+import org.eclipse.uprotocol.core.usubscription.v3.FetchSubscriptionsRequest;
+import org.eclipse.uprotocol.core.usubscription.v3.FetchSubscriptionsResponse;
+import org.eclipse.uprotocol.core.usubscription.v3.NotificationsRequest;
+import org.eclipse.uprotocol.core.usubscription.v3.NotificationsResponse;
+import org.eclipse.uprotocol.core.usubscription.v3.SubscriptionRequest;
+import org.eclipse.uprotocol.core.usubscription.v3.SubscriptionResponse;
+import org.eclipse.uprotocol.core.usubscription.v3.UnsubscribeRequest;
+import org.eclipse.uprotocol.core.usubscription.v3.UnsubscribeResponse;
+import org.eclipse.uprotocol.v1.UUri;
+
+class RpcClientBasedUSubscriptionClientTest {
+
+ private static final UUri TOPIC = UUri.newBuilder()
+ .setAuthorityName("hartley")
+ .setUeId(3)
+ .setUeVersionMajor(1)
+ .setResourceId(0x8000)
+ .build();
+
+ private RpcClient rpcClient;
+ private CallOptions callOptions;
+ private USubscriptionClient subscriptionClient;
+
+ @BeforeEach
+ void setup() {
+ rpcClient = mock(InMemoryRpcClient.class);
+ callOptions = CallOptions.DEFAULT;
+ subscriptionClient = new RpcClientBasedUSubscriptionClient(rpcClient, callOptions);
+ }
+
+ @Test
+ @DisplayName("Test constructors require RpcClient")
+ void testConstructorsRequireRpcClient() {
+ assertThrows(
+ NullPointerException.class,
+ () -> new RpcClientBasedUSubscriptionClient(null, CallOptions.DEFAULT));
+ assertThrows(
+ NullPointerException.class,
+ () -> new RpcClientBasedUSubscriptionClient(null, CallOptions.DEFAULT, 0x000, "my-vehicle"));
+ }
+
+ @Test
+ @DisplayName("Test constructor rejects invalid instance ID")
+ void testConstructorRejectsInvalidInstanceId() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> new RpcClientBasedUSubscriptionClient(rpcClient, CallOptions.DEFAULT, -1, "local"));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> new RpcClientBasedUSubscriptionClient(rpcClient, CallOptions.DEFAULT, 0xFFFF, "local"));
+ }
+
+ @Test
+ void testSubscribeInvokesRpcClient() {
+ var request = SubscriptionRequest.newBuilder().setTopic(TOPIC).build();
+ var response = SubscriptionResponse.getDefaultInstance();
+ when(rpcClient.invokeMethod(any(UUri.class), any(UPayload.class), any(CallOptions.class)))
+ .thenReturn(CompletableFuture.completedFuture(UPayload.pack(response)));
+ var actualResponse = subscriptionClient.subscribe(request).toCompletableFuture().join();
+ verify(rpcClient).invokeMethod(any(UUri.class), eq(UPayload.pack(request)), eq(callOptions));
+ assertEquals(response, actualResponse);
+ }
+
+ @Test
+ void testUnsubscribeInvokesRpcClient() {
+ var request = UnsubscribeRequest.newBuilder().setTopic(TOPIC).build();
+ var response = UnsubscribeResponse.getDefaultInstance();
+ when(rpcClient.invokeMethod(any(UUri.class), any(UPayload.class), any(CallOptions.class)))
+ .thenReturn(CompletableFuture.completedFuture(UPayload.pack(response)));
+ var actualResponse = subscriptionClient.unsubscribe(request).toCompletableFuture().join();
+ verify(rpcClient).invokeMethod(any(UUri.class), eq(UPayload.pack(request)), eq(callOptions));
+ assertEquals(response, actualResponse);
+ }
+
+ @Test
+ void testFetchSubscribersInvokesRpcClient() {
+ var request = FetchSubscribersRequest.newBuilder().setTopic(TOPIC).build();
+ var response = FetchSubscribersResponse.getDefaultInstance();
+ when(rpcClient.invokeMethod(any(UUri.class), any(UPayload.class), any(CallOptions.class)))
+ .thenReturn(CompletableFuture.completedFuture(UPayload.pack(response)));
+ var actualResponse = subscriptionClient.fetchSubscribers(request).toCompletableFuture().join();
+ verify(rpcClient).invokeMethod(any(UUri.class), eq(UPayload.pack(request)), eq(callOptions));
+ assertEquals(response, actualResponse);
+ }
+
+ @Test
+ void testFetchSubscriptionsInvokesRpcClient() {
+ var request = FetchSubscriptionsRequest.newBuilder().setTopic(TOPIC).build();
+ var response = FetchSubscriptionsResponse.getDefaultInstance();
+ when(rpcClient.invokeMethod(any(UUri.class), any(UPayload.class), any(CallOptions.class)))
+ .thenReturn(CompletableFuture.completedFuture(UPayload.pack(response)));
+ var actualResponse = subscriptionClient.fetchSubscriptions(request).toCompletableFuture().join();
+ verify(rpcClient).invokeMethod(any(UUri.class), eq(UPayload.pack(request)), eq(callOptions));
+ assertEquals(response, actualResponse);
+ }
+
+ @Test
+ void testRegisterForNotificationsInvokesRpcClient() {
+ var request = NotificationsRequest.newBuilder().setTopic(TOPIC).build();
+ var response = NotificationsResponse.getDefaultInstance();
+ when(rpcClient.invokeMethod(any(UUri.class), any(UPayload.class), any(CallOptions.class)))
+ .thenReturn(CompletableFuture.completedFuture(UPayload.pack(response)));
+ var actualResponse = subscriptionClient.registerForNotifications(request).toCompletableFuture().join();
+ verify(rpcClient).invokeMethod(any(UUri.class), eq(UPayload.pack(request)), eq(callOptions));
+ assertEquals(response, actualResponse);
+ }
+
+ @Test
+ void testUnregisterForNotificationsInvokesRpcClient() {
+ var request = NotificationsRequest.newBuilder().setTopic(TOPIC).build();
+ var response = NotificationsResponse.getDefaultInstance();
+ when(rpcClient.invokeMethod(any(UUri.class), any(UPayload.class), any(CallOptions.class)))
+ .thenReturn(CompletableFuture.completedFuture(UPayload.pack(response)));
+ var actualResponse = subscriptionClient.unregisterForNotifications(request).toCompletableFuture().join();
+ verify(rpcClient).invokeMethod(any(UUri.class), eq(UPayload.pack(request)), eq(callOptions));
+ assertEquals(response, actualResponse);
+ }
+}
diff --git a/src/test/java/org/eclipse/uprotocol/communication/CallOptionsTest.java b/src/test/java/org/eclipse/uprotocol/communication/CallOptionsTest.java
index b71bdb42..274e5c2e 100644
--- a/src/test/java/org/eclipse/uprotocol/communication/CallOptionsTest.java
+++ b/src/test/java/org/eclipse/uprotocol/communication/CallOptionsTest.java
@@ -12,6 +12,8 @@
*/
package org.eclipse.uprotocol.communication;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -22,27 +24,27 @@
import org.junit.jupiter.api.Test;
-public class CallOptionsTest {
+class CallOptionsTest {
@Test
@DisplayName("Test building a null CallOptions that is equal to the default")
- public void testBuildNullCallOptions() {
+ void testBuildNullCallOptions() {
CallOptions options = new CallOptions();
assertTrue(options.equals(CallOptions.DEFAULT));
}
@Test
@DisplayName("Test building a CallOptions with a timeout")
- public void testBuildCallOptionsWithTimeout() {
+ void testBuildCallOptionsWithTimeout() {
CallOptions options = new CallOptions(1000);
assertEquals(1000, options.timeout());
assertEquals(UPriority.UPRIORITY_CS4, options.priority());
- assertTrue(options.token().isEmpty());
+ assertNull(options.token());
}
@Test
@DisplayName("Test building a CallOptions with a priority")
- public void testBuildCallOptionsWithPriority() {
+ void testBuildCallOptionsWithPriority() {
CallOptions options = new CallOptions(1000, UPriority.UPRIORITY_CS4);
assertEquals(UPriority.UPRIORITY_CS4, options.priority());
}
@@ -50,7 +52,7 @@ public void testBuildCallOptionsWithPriority() {
@Test
@DisplayName("Test building a CallOptions with all parameters")
- public void testBuildCallOptionsWithAllParameters() {
+ void testBuildCallOptionsWithAllParameters() {
CallOptions options = new CallOptions(1000, UPriority.UPRIORITY_CS4, "token");
assertEquals(1000, options.timeout());
assertEquals(UPriority.UPRIORITY_CS4, options.priority());
@@ -59,28 +61,28 @@ public void testBuildCallOptionsWithAllParameters() {
@Test
@DisplayName("Test building a CallOptions with a blank token")
- public void testBuildCallOptionsWithBlankToken() {
+ void testBuildCallOptionsWithBlankToken() {
CallOptions options = new CallOptions(1000, UPriority.UPRIORITY_CS4, "");
assertTrue(options.token().isEmpty());
}
@Test
@DisplayName("Test isEquals when passed parameter is not equals")
- public void testIsEqualsWithNull() {
+ void testIsEqualsWithNull() {
CallOptions options = new CallOptions(1000, UPriority.UPRIORITY_CS4, "token");
- assertFalse(options.equals(null));
+ assertNotNull(options);
}
@Test
@DisplayName("Test isEquals when passed parameter is equals")
- public void testIsEqualsWithSameObject() {
+ void testIsEqualsWithSameObject() {
CallOptions options = new CallOptions(1000, UPriority.UPRIORITY_CS4, "token");
assertTrue(options.equals(options));
}
@Test
@DisplayName("Test isEquals when timeout is not the same")
- public void testIsEqualsWithDifferentParameters() {
+ void testIsEqualsWithDifferentParameters() {
CallOptions options = new CallOptions(1001, UPriority.UPRIORITY_CS3, "token");
CallOptions otherOptions = new CallOptions(1000, UPriority.UPRIORITY_CS3, "token");
assertFalse(options.equals(otherOptions));
@@ -89,7 +91,7 @@ public void testIsEqualsWithDifferentParameters() {
@Test
@DisplayName("Test isEquals when priority is not the same")
- public void testIsEqualsWithDifferentParametersPriority() {
+ void testIsEqualsWithDifferentParametersPriority() {
CallOptions options = new CallOptions(1000, UPriority.UPRIORITY_CS4, "token");
CallOptions otherOptions = new CallOptions(1000, UPriority.UPRIORITY_CS3, "token");
assertFalse(options.equals(otherOptions));
@@ -98,7 +100,7 @@ public void testIsEqualsWithDifferentParametersPriority() {
@Test
@DisplayName("Test isEquals when token is not the same")
- public void testIsEqualsWithDifferentParametersToken() {
+ void testIsEqualsWithDifferentParametersToken() {
CallOptions options = new CallOptions(1000, UPriority.UPRIORITY_CS3, "Mytoken");
CallOptions otherOptions = new CallOptions(1000, UPriority.UPRIORITY_CS3, "token");
assertFalse(options.equals(otherOptions));
@@ -108,7 +110,7 @@ public void testIsEqualsWithDifferentParametersToken() {
@SuppressWarnings("unlikely-arg-type")
@Test
@DisplayName("Test equals when object passed is not the same type as CallOptions")
- public void testIsEqualsWithDifferentType() {
+ void testIsEqualsWithDifferentType() {
CallOptions options = new CallOptions(1000, UPriority.UPRIORITY_CS4, "token");
UUri uri = UUri.getDefaultInstance();
assertFalse(options.equals(uri));
diff --git a/src/test/java/org/eclipse/uprotocol/communication/CommunicationLayerClientTestBase.java b/src/test/java/org/eclipse/uprotocol/communication/CommunicationLayerClientTestBase.java
new file mode 100644
index 00000000..b494c362
--- /dev/null
+++ b/src/test/java/org/eclipse/uprotocol/communication/CommunicationLayerClientTestBase.java
@@ -0,0 +1,68 @@
+/**
+ * SPDX-FileCopyrightText: 2024 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Apache License Version 2.0 which is available at
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package org.eclipse.uprotocol.communication;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+
+import java.util.concurrent.CompletableFuture;
+
+import org.eclipse.uprotocol.transport.LocalUriProvider;
+import org.eclipse.uprotocol.transport.StaticUriProvider;
+import org.eclipse.uprotocol.transport.UListener;
+import org.eclipse.uprotocol.transport.UTransport;
+import org.eclipse.uprotocol.v1.UMessage;
+import org.eclipse.uprotocol.v1.UUri;
+import org.junit.jupiter.api.BeforeEach;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+class CommunicationLayerClientTestBase {
+ protected static final UUri TRANSPORT_SOURCE = UUri.newBuilder()
+ .setAuthorityName("my-vehicle")
+ .setUeId(0xa1)
+ .setUeVersionMajor(0x01)
+ .setResourceId(0x0000)
+ .build();
+ protected static final UUri TOPIC_URI = UUri.newBuilder(TRANSPORT_SOURCE)
+ .setResourceId(0xa100)
+ .build();
+ protected static final UUri DESTINATION_URI = UUri.newBuilder()
+ .setAuthorityName("other-vehicle")
+ .setUeId(0x2bbbb)
+ .setUeVersionMajor(0x02)
+ .setResourceId(0x0000)
+ .build();
+ protected static final UUri METHOD_URI = UUri.newBuilder(TRANSPORT_SOURCE)
+ .setResourceId(0x00a)
+ .build();
+
+ protected UTransport transport;
+ protected LocalUriProvider uriProvider;
+ protected ArgumentCaptor responseListener;
+ protected ArgumentCaptor requestMessage;
+
+ @BeforeEach
+ void setUpTransport() {
+ transport = mock(UTransport.class);
+ Mockito.lenient().when(transport.registerListener(any(UUri.class), any(UUri.class), any(UListener.class)))
+ .thenReturn(CompletableFuture.completedFuture(null));
+ Mockito.lenient().when(transport.unregisterListener(any(UUri.class), any(UUri.class), any(UListener.class)))
+ .thenReturn(CompletableFuture.completedFuture(null));
+ Mockito.lenient().when(transport.send(any(UMessage.class)))
+ .thenReturn(CompletableFuture.completedFuture(null));
+ uriProvider = StaticUriProvider.of(TRANSPORT_SOURCE);
+ responseListener = ArgumentCaptor.forClass(UListener.class);
+ requestMessage = ArgumentCaptor.forClass(UMessage.class);
+ }
+}
diff --git a/src/test/java/org/eclipse/uprotocol/communication/InMemoryRpcClientTest.java b/src/test/java/org/eclipse/uprotocol/communication/InMemoryRpcClientTest.java
index fba3d129..1d7d5cad 100644
--- a/src/test/java/org/eclipse/uprotocol/communication/InMemoryRpcClientTest.java
+++ b/src/test/java/org/eclipse/uprotocol/communication/InMemoryRpcClientTest.java
@@ -12,212 +12,209 @@
*/
package org.eclipse.uprotocol.communication;
+import static org.junit.Assert.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import java.util.Optional;
-import java.util.concurrent.CompletionStage;
import java.util.concurrent.ExecutionException;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
import java.util.concurrent.CompletableFuture;
-import org.eclipse.uprotocol.transport.UTransport;
+
import org.eclipse.uprotocol.transport.builder.UMessageBuilder;
+import org.eclipse.uprotocol.uri.factory.UriFactory;
import org.eclipse.uprotocol.uuid.factory.UuidFactory;
-import org.eclipse.uprotocol.v1.UAttributes;
import org.eclipse.uprotocol.v1.UCode;
import org.eclipse.uprotocol.v1.UMessage;
+import org.eclipse.uprotocol.v1.UPayloadFormat;
import org.eclipse.uprotocol.v1.UPriority;
-import org.eclipse.uprotocol.v1.UStatus;
import org.eclipse.uprotocol.v1.UUri;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
-
-public class InMemoryRpcClientTest {
- @Test
- @DisplayName("Test calling invokeMethod passing UPayload")
- public void testInvokeMethodWithPayload() {
- UPayload payload = UPayload.packToAny(UUri.newBuilder().build());
- RpcClient rpcClient = new InMemoryRpcClient(new TestUTransport());
- final CompletionStage response = rpcClient.invokeMethod(createMethodUri(), payload, null);
- assertNotNull(response);
- assertDoesNotThrow(() -> {
- UPayload payload1 = response.toCompletableFuture().get();
- assertTrue(payload.equals(payload1));
- });
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import com.google.common.truth.Truth;
+import com.google.protobuf.ByteString;
+
+class InMemoryRpcClientTest extends CommunicationLayerClientTestBase {
+
+ private static void assertMessageHasOptions(CallOptions options, UMessage message) {
+ Optional.ofNullable(options.timeout())
+ .ifPresent(timeout -> assertEquals(timeout, message.getAttributes().getTtl()));
+ Optional.ofNullable(options.priority())
+ .ifPresent(priority -> assertEquals(priority, message.getAttributes().getPriority()));
+ Optional.ofNullable(options.token())
+ .ifPresent(token -> assertEquals(token, message.getAttributes().getToken()));
}
- @Test
- @DisplayName("Test calling invokeMethod passing a UPaylod and calloptions")
- public void testInvokeMethodWithPayloadAndCallOptions() {
- UPayload payload = UPayload.packToAny(UUri.newBuilder().build());
- CallOptions options = new CallOptions(1000, UPriority.UPRIORITY_CS5);
- RpcClient rpcClient = new InMemoryRpcClient(new TestUTransport());
- CompletionStage response = rpcClient.invokeMethod(createMethodUri(), payload, options);
- assertNotNull(response);
- assertDoesNotThrow(() -> {
- UPayload result = response.toCompletableFuture().get();
- assertTrue(result.equals(payload));
- });
- assertFalse(response.toCompletableFuture().isCompletedExceptionally());
+ private static Stream callOptionsAndPayloadProvider() {
+ return Stream.of(
+ Arguments.of(CallOptions.DEFAULT, UPayload.EMPTY, UCode.OK),
+ Arguments.of(
+ new CallOptions(3000, UPriority.UPRIORITY_CS4, null),
+ UPayload.packToAny(UUri.newBuilder().build()),
+ UCode.OK),
+ Arguments.of(
+ new CallOptions(4000, UPriority.UPRIORITY_CS5, ""),
+ UPayload.pack(UUri.newBuilder().build()),
+ UCode.OK),
+ Arguments.of(
+ new CallOptions(5000, UPriority.UPRIORITY_CS6, "my-token"),
+ UPayload.pack(ByteString.copyFromUtf8("hello"), UPayloadFormat.UPAYLOAD_FORMAT_TEXT),
+ (UCode) null)
+ );
}
- @Test
- @DisplayName("Test calling invokeMethod passing a Null UPayload")
- public void testInvokeMethodWithNullPayload() {
- RpcClient rpcClient = new InMemoryRpcClient(new TestUTransport());
- CompletionStage response = rpcClient.invokeMethod(createMethodUri(), null, CallOptions.DEFAULT);
- assertNotNull(response);
- assertDoesNotThrow(() -> {
- UPayload payload = response.toCompletableFuture().get();
- assertEquals(payload, UPayload.EMPTY);
- });
- assertFalse(response.toCompletableFuture().isCompletedExceptionally());
+ @ParameterizedTest(name = "Test successful RPC, sending and receiving a payload: {index} - {arguments}")
+ @MethodSource("callOptionsAndPayloadProvider")
+ void testInvokeMethodWithPayloadSucceeds(CallOptions options, UPayload payload, UCode responseStatus) {
+ RpcClient rpcClient = new InMemoryRpcClient(transport, uriProvider);
+
+ var response = rpcClient.invokeMethod(METHOD_URI, payload, options);
+ verify(transport).registerListener(any(UUri.class), any(UUri.class), responseListener.capture());
+ verify(transport).send(requestMessage.capture());
+ assertEquals(payload.data(), requestMessage.getValue().getPayload());
+ assertMessageHasOptions(options, requestMessage.getValue());
+
+ var requestAttributes = requestMessage.getValue().getAttributes();
+ var responseMessageBuilder = UMessageBuilder.response(requestAttributes);
+ Optional.ofNullable(responseStatus)
+ .ifPresent(status -> responseMessageBuilder.withCommStatus(status));
+ var responseMessage = responseMessageBuilder.build(payload);
+ responseListener.getValue().onReceive(responseMessage);
+
+ var receivedPayload = assertDoesNotThrow(() -> response.toCompletableFuture().get());
+ assertEquals(payload, receivedPayload);
}
@Test
- @DisplayName("Test calling invokeMethod with TimeoutUTransport that will timeout the request")
- public void testInvokeMethodWithTimeoutTransport() {
+ @DisplayName("Test running into timeout when invoking method")
+ void testInvokeMethodFailsForTimeout() {
final UPayload payload = UPayload.packToAny(UUri.newBuilder().build());
final CallOptions options = new CallOptions(100, UPriority.UPRIORITY_CS5, "token");
- RpcClient rpcClient = new InMemoryRpcClient(new TimeoutUTransport());
- final CompletionStage response = rpcClient.invokeMethod(createMethodUri(), payload, options);
-
- Exception exception = assertThrows(java.util.concurrent.ExecutionException.class,
- response.toCompletableFuture()::get);
- assertEquals(exception.getMessage(),
- "org.eclipse.uprotocol.communication.UStatusException: Request timed out");
- assertEquals(((UStatus) (((UStatusException) exception.getCause())).getStatus()).getCode(),
- UCode.DEADLINE_EXCEEDED);
- }
-
- @Test
- @DisplayName("Test calling close for DefaultRpcClient when there are multiple response listeners registered")
- public void testCloseWithMultipleListeners() {
- InMemoryRpcClient rpcClient = new InMemoryRpcClient(new TestUTransport());
- UPayload payload = UPayload.packToAny(UUri.newBuilder().build());
- CompletionStage response = rpcClient.invokeMethod(createMethodUri(), payload, null);
- assertNotNull(response);
- CompletionStage response2 = rpcClient.invokeMethod(createMethodUri(), payload, null);
- assertNotNull(response2);
- rpcClient.close();
+ RpcClient rpcClient = new InMemoryRpcClient(transport, uriProvider);
+ var exception = assertThrows(ExecutionException.class, () -> {
+ rpcClient.invokeMethod(METHOD_URI, payload, options).toCompletableFuture().get();
+ });
+ Truth.assertThat(exception).hasCauseThat().isInstanceOf(UStatusException.class);
+ assertEquals(
+ UCode.DEADLINE_EXCEEDED,
+ ((UStatusException) exception.getCause()).getStatus().getCode()
+ );
}
@Test
- @DisplayName("Test calling invokeMethod when we use the CommStatusTransport")
- public void testInvokeMethodWithCommStatusTransport() {
- RpcClient rpcClient = new InMemoryRpcClient(new CommStatusTransport());
- UPayload payload = UPayload.packToAny(UUri.newBuilder().build());
- CompletionStage response = rpcClient.invokeMethod(createMethodUri(), payload, null);
-
- Exception exception = assertThrows(java.util.concurrent.ExecutionException.class,
- response.toCompletableFuture()::get);
- assertTrue(response.toCompletableFuture().isCompletedExceptionally());
- assertEquals(exception.getMessage(),
- "org.eclipse.uprotocol.communication.UStatusException: Communication error [FAILED_PRECONDITION]");
-
- assertEquals(((UStatus) (((UStatusException) exception.getCause())).getStatus()).getCode(),
- UCode.FAILED_PRECONDITION);
+ @DisplayName("Test invoking method fails for transport error")
+ void testInvokeMethodFailsForTransportError() {
+ when(transport.send(any(UMessage.class)))
+ .thenReturn(CompletableFuture.failedFuture(
+ new UStatusException(UCode.UNAVAILABLE, "transport not ready")));
+ RpcClient rpcClient = new InMemoryRpcClient(transport, uriProvider);
+ var exception = assertThrows(ExecutionException.class, () -> {
+ rpcClient.invokeMethod(METHOD_URI, UPayload.EMPTY, CallOptions.DEFAULT).toCompletableFuture().get();
+ });
+ verify(transport).send(requestMessage.capture());
+ assertEquals(UPayload.EMPTY.data(), requestMessage.getValue().getPayload());
+ assertMessageHasOptions(CallOptions.DEFAULT, requestMessage.getValue());
+
+ Truth.assertThat(exception).hasCauseThat().isInstanceOf(UStatusException.class);
+ assertEquals(
+ UCode.UNAVAILABLE,
+ ((UStatusException) exception.getCause()).getStatus().getCode()
+ );
}
@Test
- @DisplayName("Test calling invokeMethod when we use the ErrorUTransport that fails the transport send()")
- public void testInvokeMethodWithErrorTransport() {
- UTransport transport = new TestUTransport() {
- @Override
- public CompletionStage send(UMessage message) {
- return CompletableFuture.completedFuture(
- UStatus.newBuilder().setCode(UCode.FAILED_PRECONDITION).build());
- }
- };
-
- RpcClient rpcClient = new InMemoryRpcClient(transport);
- UPayload payload = UPayload.packToAny(UUri.newBuilder().build());
- CompletionStage response = rpcClient.invokeMethod(createMethodUri(), payload, null);
-
- Exception exception = assertThrows(java.util.concurrent.ExecutionException.class,
- response.toCompletableFuture()::get);
- assertTrue(response.toCompletableFuture().isCompletedExceptionally());
- assertEquals(exception.getMessage(),
- "org.eclipse.uprotocol.communication.UStatusException: ");
- assertEquals(((UStatus) (((UStatusException) exception.getCause())).getStatus()).getCode(),
- UCode.FAILED_PRECONDITION);
+ @DisplayName("Test unsuccessful RPC, with service returning error")
+ void testInvokeMethodFailsForErroneousServiceInvocation() {
+ RpcClient rpcClient = new InMemoryRpcClient(transport, uriProvider);
+ var response = rpcClient.invokeMethod(METHOD_URI, UPayload.EMPTY, CallOptions.DEFAULT);
+ verify(transport).registerListener(any(UUri.class), any(UUri.class), responseListener.capture());
+ verify(transport).send(requestMessage.capture());
+ assertEquals(UPayload.EMPTY.data(), requestMessage.getValue().getPayload());
+ assertMessageHasOptions(CallOptions.DEFAULT, requestMessage.getValue());
+
+ var requestAttributes = requestMessage.getValue().getAttributes();
+ var responseMessage = UMessageBuilder.response(requestAttributes)
+ .withCommStatus(UCode.RESOURCE_EXHAUSTED)
+ .build();
+ responseListener.getValue().onReceive(responseMessage);
+
+ var exception = assertThrows(ExecutionException.class, () -> response.toCompletableFuture().get());
+ Truth.assertThat(exception).hasCauseThat().isInstanceOf(UStatusException.class);
+ assertEquals(
+ UCode.RESOURCE_EXHAUSTED,
+ ((UStatusException) exception.getCause()).getStatus().getCode()
+ );
}
-
- @Test
- @DisplayName("Test calling handleResponse when it gets a response for an unknown request")
- public void testHandleResponseForUnknownRequest() {
- UTransport transport = new TestUTransport() {
- @Override
- public UMessage buildResponse(UMessage request) {
- UAttributes attributes = UAttributes.newBuilder(request.getAttributes())
- .setId(UuidFactory.Factories.UPROTOCOL.factory().create()).build();
- return UMessageBuilder.response(attributes).build();
- }
- };
-
- InMemoryRpcClient rpcClient = new InMemoryRpcClient(transport);
-
- CallOptions options = new CallOptions(10, UPriority.UPRIORITY_CS5);
- CompletionStage response = rpcClient.invokeMethod(createMethodUri(), null, options);
- assertNotNull(response);
- assertThrows(ExecutionException.class, () -> {
- UPayload payload = response.toCompletableFuture().get();
- assertEquals(payload, UPayload.EMPTY);
- });
- assertTrue(response.toCompletableFuture().isCompletedExceptionally());
+ static Stream unexpectedMessageHandlerProvider() {
+ return Stream.of(
+ Arguments.of((Consumer) null),
+ Arguments.of(mock(Consumer.class))
+ );
}
-
- @Test
- @DisplayName("Test calling handleResponse when it gets a message that is not a response")
- public void testHandleResponseForNonResponseMessage() {
- UTransport transport = new TestUTransport() {
- @Override
- public UMessage buildResponse(UMessage request) {
- UUri topic = UUri.newBuilder(getSource()).setResourceId(0x8000).build();
- return UMessageBuilder.publish(topic).build();
- }
- };
-
- InMemoryRpcClient rpcClient = new InMemoryRpcClient(transport);
-
- CallOptions options = new CallOptions(10, UPriority.UPRIORITY_CS5);
- CompletionStage response = rpcClient.invokeMethod(createMethodUri(), null, options);
- assertNotNull(response);
- assertThrows(ExecutionException.class, () -> {
- UPayload payload = response.toCompletableFuture().get();
- assertEquals(payload, UPayload.EMPTY);
- });
- assertTrue(response.toCompletableFuture().isCompletedExceptionally());
+ @ParameterizedTest(name = "Test client handles unexpected incoming messages: {index} - {arguments}")
+ @MethodSource("unexpectedMessageHandlerProvider")
+ void testHandleUnexpectedResponse(Consumer unexpectedMessageHandler) {
+ var rpcClient = new InMemoryRpcClient(transport, uriProvider);
+ Optional.ofNullable(unexpectedMessageHandler).ifPresent(rpcClient::setUnexpectedMessageHandler);
+ verify(transport).registerListener(any(UUri.class), any(UUri.class), responseListener.capture());
+
+ // send an arbitrary request
+ rpcClient.invokeMethod(METHOD_URI, UPayload.EMPTY, CallOptions.DEFAULT);
+ verify(transport).send(requestMessage.capture());
+
+ // create unsolicited response message
+ final var reqId = UuidFactory.Factories.UPROTOCOL.factory().create();
+ assertNotEquals(reqId, requestMessage.getValue().getAttributes().getId());
+ var responseMessage = UMessageBuilder.response(
+ METHOD_URI,
+ TRANSPORT_SOURCE,
+ reqId)
+ .build();
+ responseListener.getValue().onReceive(responseMessage);
+
+ if (unexpectedMessageHandler != null) {
+ // assert that the unexpected response is handled correctly
+ verify(unexpectedMessageHandler).accept(responseMessage);
+ }
+
+ // create unsolicited notification message
+ var notificationMessage = UMessageBuilder.notification(
+ UUri.newBuilder()
+ .setAuthorityName("hartley")
+ .setUeId(10)
+ .setUeVersionMajor(1)
+ .setResourceId(0x9100)
+ .build(),
+ TRANSPORT_SOURCE)
+ .build();
+ responseListener.getValue().onReceive(notificationMessage);
+
+ if (unexpectedMessageHandler != null) {
+ // assert that the unexpected notification is handled correctly
+ verify(unexpectedMessageHandler).accept(notificationMessage);
+ }
}
@Test
- @DisplayName("Test calling invokeMethod when we set comm status to UCode.OK")
- public void testInvokeMethodWithCommStatusUCodeOKTransport() {
- RpcClient rpcClient = new InMemoryRpcClient(new CommStatusOkTransport());
- UPayload payload = UPayload.packToAny(UUri.newBuilder().build());
- CompletionStage response = rpcClient.invokeMethod(createMethodUri(), payload, null);
- assertFalse(response.toCompletableFuture().isCompletedExceptionally());
- assertDoesNotThrow(() -> {
- Optional unpackedStatus = UPayload.unpack(response.toCompletableFuture().get(), UStatus.class);
- assertTrue(unpackedStatus.isPresent());
- assertEquals(UCode.OK, unpackedStatus.get().getCode());
- assertEquals("No Communication Error", unpackedStatus.get().getMessage());
- });
- }
-
-
-
- private UUri createMethodUri() {
- return UUri.newBuilder()
- .setAuthorityName("hartley")
- .setUeId(10)
- .setUeVersionMajor(1)
- .setResourceId(3).build();
+ @DisplayName("Test close() unregisters the response listener from the transport")
+ void testCloseUnregistersResponseListenerFromTransport() {
+ InMemoryRpcClient rpcClient = new InMemoryRpcClient(transport, uriProvider);
+ verify(transport).registerListener(eq(UriFactory.ANY), eq(TRANSPORT_SOURCE), responseListener.capture());
+ rpcClient.close();
+ verify(transport).unregisterListener(eq(UriFactory.ANY), eq(TRANSPORT_SOURCE), eq(responseListener.getValue()));
}
}
diff --git a/src/test/java/org/eclipse/uprotocol/communication/InMemoryRpcServerTest.java b/src/test/java/org/eclipse/uprotocol/communication/InMemoryRpcServerTest.java
index e10fbe01..a8313c58 100644
--- a/src/test/java/org/eclipse/uprotocol/communication/InMemoryRpcServerTest.java
+++ b/src/test/java/org/eclipse/uprotocol/communication/InMemoryRpcServerTest.java
@@ -12,466 +12,351 @@
*/
package org.eclipse.uprotocol.communication;
-import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.Assert.assertThrows;
import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import java.util.concurrent.CompletionStage;
-import org.eclipse.uprotocol.transport.UTransport;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
+
+import org.eclipse.uprotocol.transport.UListener;
import org.eclipse.uprotocol.transport.builder.UMessageBuilder;
+import org.eclipse.uprotocol.uri.factory.UriFactory;
+import org.eclipse.uprotocol.v1.UAttributes;
import org.eclipse.uprotocol.v1.UCode;
import org.eclipse.uprotocol.v1.UMessage;
+import org.eclipse.uprotocol.v1.UMessageType;
+import org.eclipse.uprotocol.v1.UPayloadFormat;
import org.eclipse.uprotocol.v1.UStatus;
import org.eclipse.uprotocol.v1.UUri;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
-
-public class InMemoryRpcServerTest {
- @Test
- @DisplayName("Test registering and unregister a request listener")
- public void testRegisteringRequestListener() {
- RequestHandler handler = new RequestHandler() {
- @Override
- public UPayload handleRequest(UMessage request) {
- return UPayload.EMPTY;
- }
- };
- UUri method = createMethodUri();
- RpcServer server = new InMemoryRpcServer(new TestUTransport());
- final CompletionStage result = server.registerRequestHandler(method, handler);
- assertFalse(result.toCompletableFuture().isCompletedExceptionally());
- assertDoesNotThrow(() -> assertEquals(result.toCompletableFuture().get().getCode(), UCode.OK));
-
- // second time should return an error
- final CompletionStage result2 = server.unregisterRequestHandler(method, handler);
- assertFalse(result2.toCompletableFuture().isCompletedExceptionally());
- assertDoesNotThrow(() -> assertEquals(result2.toCompletableFuture().get().getCode(), UCode.OK));
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import com.google.protobuf.ByteString;
+
+@ExtendWith(MockitoExtension.class)
+class InMemoryRpcServerTest extends CommunicationLayerClientTestBase {
+
+ @Mock
+ private RequestHandler handler;
+
+ private static Stream invalidArgsForRegisterRequestHandler() {
+ return Stream.of(
+ Arguments.of(UriFactory.ANY, 0, null, NullPointerException.class),
+ Arguments.of(null, 0, mock(RequestHandler.class), NullPointerException.class),
+ Arguments.of(UriFactory.ANY, 0x0000, mock(RequestHandler.class), CompletionException.class),
+ Arguments.of(UriFactory.ANY, 0x8000, mock(RequestHandler.class), CompletionException.class)
+ );
}
- @Test
- @DisplayName("Test registering twice the same request handler for the same method")
- public void testRegisteringTwiceTheSameRequestHandler() {
- RequestHandler handler = new RequestHandler() {
- @Override
- public UPayload handleRequest(UMessage request) {
- return UPayload.EMPTY;
- }
- };
- RpcServer server = new InMemoryRpcServer(new TestUTransport());
- assertFalse(server.registerRequestHandler(createMethodUri(), handler)
- .toCompletableFuture().isCompletedExceptionally());
-
- CompletionStage result = server.registerRequestHandler(createMethodUri(), handler);
- assertFalse(result.toCompletableFuture().isCompletedExceptionally());
- assertDoesNotThrow(() -> {
- assertEquals(result.toCompletableFuture().get().getCode(), UCode.ALREADY_EXISTS);
- });
+ @ParameterizedTest(name = "Test registerRequestHandler fails for invalid arguments: {index} => {arguments}")
+ @MethodSource("invalidArgsForRegisterRequestHandler")
+ void testRegisterRequestHandlerFailsForNullParameters(
+ UUri uri,
+ int methodId,
+ RequestHandler handler,
+ Class extends Throwable> expectedException) {
+ RpcServer server = new InMemoryRpcServer(transport, uriProvider);
+ assertThrows(expectedException, () -> server.registerRequestHandler(uri, methodId, handler)
+ .toCompletableFuture().join());
}
- @Test
- @DisplayName("Test unregistering a request handler that wasn't registered already")
- public void testUnregisteringNonRegisteredRequestHandler() {
- RequestHandler handler = new RequestHandler() {
- @Override
- public UPayload handleRequest(UMessage request) {
- throw new UnsupportedOperationException("Unimplemented method 'handleRequest'");
- }
- };
- RpcServer server = new InMemoryRpcServer(new TestUTransport());
- CompletionStage result = server.unregisterRequestHandler(createMethodUri(), handler);
- assertFalse(result.toCompletableFuture().isCompletedExceptionally());
- assertDoesNotThrow(() -> {
- assertEquals(result.toCompletableFuture().get().getCode(), UCode.NOT_FOUND);
- });
+ @ParameterizedTest(name = "Test unregisterRequestHandler fails for invalid arguments: {index} => {arguments}")
+ @MethodSource("invalidArgsForRegisterRequestHandler")
+ void testUnregisterRequestHandlerFailsForNullParameters(
+ UUri uri,
+ int methodId,
+ RequestHandler handler,
+ Class extends Throwable> expectedException) {
+ RpcServer server = new InMemoryRpcServer(transport, uriProvider);
+ assertThrows(expectedException, () -> server.unregisterRequestHandler(uri, methodId, handler)
+ .toCompletableFuture().join());
}
@Test
- @DisplayName("Test register a request handler where authority does not match the transport source authority")
- public void testRegisteringRequestListenerWithWrongAuthority() {
- RequestHandler handler = new RequestHandler() {
- @Override
- public UPayload handleRequest(UMessage request) {
- return UPayload.EMPTY;
- }
- };
- RpcServer server = new InMemoryRpcServer(new TestUTransport());
- UUri method = UUri.newBuilder()
- .setAuthorityName("Steven")
- .setUeId(4)
- .setUeVersionMajor(1)
- .setResourceId(3).build();
- CompletionStage status = server.registerRequestHandler(method, handler);
- assertFalse(status.toCompletableFuture().isCompletedExceptionally());
- assertDoesNotThrow(() -> {
- UStatus result = status.toCompletableFuture().get();
- assertEquals(result.getCode(), UCode.INVALID_ARGUMENT);
- assertEquals(result.getMessage(), "Method URI does not match the transport source URI");
- });
+ @DisplayName("Test registering and unregistering a request listener")
+ void testRegisterRequestListenerSucceeds() {
+ final RpcServer server = new InMemoryRpcServer(transport, uriProvider);
+ final var originFilter = UriFactory.ANY;
+ server.registerRequestHandler(
+ originFilter,
+ METHOD_URI.getResourceId(),
+ handler)
+ .toCompletableFuture()
+ .join();
+ verify(transport).registerListener(
+ eq(originFilter),
+ eq(METHOD_URI),
+ any(UListener.class));
+
+ server.unregisterRequestHandler(
+ originFilter,
+ METHOD_URI.getResourceId(),
+ handler)
+ .toCompletableFuture()
+ .join();
+ verify(transport).unregisterListener(
+ eq(originFilter),
+ eq(METHOD_URI),
+ any(UListener.class));
}
@Test
- @DisplayName("Test register a request handler where ue_id does not match the transport source ue)_id")
- public void testRegisteringRequestListenerWithWrongUeId() {
- RequestHandler handler = new RequestHandler() {
- @Override
- public UPayload handleRequest(UMessage request) {
- return UPayload.EMPTY;
- }
- };
- RpcServer server = new InMemoryRpcServer(new TestUTransport());
- UUri method = UUri.newBuilder()
- .setAuthorityName("Hartley")
- .setUeId(5)
- .setUeVersionMajor(1)
- .setResourceId(3).build();
-
- CompletionStage status = server.registerRequestHandler(method, handler);
- assertFalse(status.toCompletableFuture().isCompletedExceptionally());
- assertDoesNotThrow(() -> {
- UStatus result = status.toCompletableFuture().get();
- assertEquals(result.getCode(), UCode.INVALID_ARGUMENT);
- assertEquals(result.getMessage(), "Method URI does not match the transport source URI");
- });
+ @DisplayName("Test registering the same request handler twice for the same endpoint")
+ void testRegisteringTwiceTheSameRequestHandler() {
+ final RpcServer server = new InMemoryRpcServer(transport, uriProvider);
+
+ final var originFilter = UriFactory.ANY;
+ server.registerRequestHandler(
+ originFilter,
+ METHOD_URI.getResourceId(),
+ handler)
+ .toCompletableFuture()
+ .join();
+ verify(transport, times(1)).registerListener(
+ eq(originFilter),
+ eq(METHOD_URI),
+ any(UListener.class));
+
+ var exception = assertThrows(CompletionException.class, () -> server.registerRequestHandler(
+ originFilter,
+ METHOD_URI.getResourceId(),
+ handler)
+ .toCompletableFuture()
+ .join());
+ assertEquals(UCode.ALREADY_EXISTS, ((UStatusException) exception.getCause()).getCode());
}
@Test
- @DisplayName("Test register request handler where ue_version_major does not " +
- "match the transport source ue_version_major")
- public void testRegisteringRequestListenerWithWrongUeVersionMajor() {
- RequestHandler handler = new RequestHandler() {
- @Override
- public UPayload handleRequest(UMessage request) {
- return UPayload.EMPTY;
- }
- };
- RpcServer server = new InMemoryRpcServer(new TestUTransport());
- UUri method = UUri.newBuilder()
- .setAuthorityName("Hartley")
- .setUeId(4)
- .setUeVersionMajor(2)
- .setResourceId(3).build();
-
- CompletionStage status = server.registerRequestHandler(method, handler);
- assertFalse(status.toCompletableFuture().isCompletedExceptionally());
- assertDoesNotThrow(() -> {
- UStatus result = status.toCompletableFuture().get();
- assertEquals(result.getCode(), UCode.INVALID_ARGUMENT);
- assertEquals(result.getMessage(), "Method URI does not match the transport source URI");
- });
-
+ @DisplayName("Test unregistering a request handler that wasn't registered already")
+ void testUnregisterRequestHandlerFailsForUnknownHandler() {
+ final RpcServer server = new InMemoryRpcServer(transport, uriProvider);
+
+ var exception = assertThrows(
+ CompletionException.class,
+ () -> server.unregisterRequestHandler(UriFactory.ANY, 1, handler)
+ .toCompletableFuture().join());
+ assertEquals(UCode.NOT_FOUND, ((UStatusException) exception.getCause()).getCode());
+ verify(transport, never()).unregisterListener(
+ any(UUri.class),
+ any(UUri.class),
+ any(UListener.class));
}
@Test
- @DisplayName("Test unregister requesthandler where authority not match the transport source URI")
- public void testUnregisteringRequestHandlerWithWrongAuthority() {
- RequestHandler handler = new RequestHandler() {
- @Override
- public UPayload handleRequest(UMessage request) {
- return UPayload.EMPTY;
- }
- };
- RpcServer server = new InMemoryRpcServer(new TestUTransport());
- UUri method = UUri.newBuilder()
- .setAuthorityName("Steven")
- .setUeId(4)
- .setUeVersionMajor(1)
- .setResourceId(3).build();
-
- CompletionStage status = server.unregisterRequestHandler(method, handler);
-
- assertFalse(status.toCompletableFuture().isCompletedExceptionally());
- assertDoesNotThrow(() -> {
- UStatus result = status.toCompletableFuture().get();
- assertEquals(result.getCode(), UCode.INVALID_ARGUMENT);
- assertEquals(result.getMessage(), "Method URI does not match the transport source URI");
- });
+ @DisplayName("Test registering a request handler with unavailable transport fails")
+ void testRegisteringRequestListenerFailsIfTransportIsUnavailable() {
+ when(transport.registerListener(any(UUri.class), any(UUri.class), any(UListener.class)))
+ .thenReturn(CompletableFuture.failedFuture(new UStatusException(UCode.UNAVAILABLE, "unavailable")));
+ RpcServer server = new InMemoryRpcServer(transport, uriProvider);
+
+ var exception = assertThrows(
+ CompletionException.class,
+ () -> server.registerRequestHandler(UriFactory.ANY, METHOD_URI.getResourceId(), handler)
+ .toCompletableFuture().join());
+ assertEquals(UCode.UNAVAILABLE, ((UStatusException) exception.getCause()).getCode());
+ verify(transport, times(1)).registerListener(
+ eq(UriFactory.ANY),
+ eq(METHOD_URI),
+ any(UListener.class));
}
- @Test
- @DisplayName("Test unregister request handler where ue_id does not match the transport source URI")
- public void testUnregisteringRequestHandlerWithWrongUeId() {
- RequestHandler handler = new RequestHandler() {
- @Override
- public UPayload handleRequest(UMessage request) {
- return UPayload.EMPTY;
- }
- };
- RpcServer server = new InMemoryRpcServer(new TestUTransport());
- UUri method = UUri.newBuilder()
- .setAuthorityName("Hartley")
- .setUeId(5)
- .setUeVersionMajor(1)
- .setResourceId(3).build();
-
- CompletionStage result = server.unregisterRequestHandler(method, handler);
- assertFalse(result.toCompletableFuture().isCompletedExceptionally());
- assertDoesNotThrow(() -> {
- UStatus status = result.toCompletableFuture().get();
- assertEquals(status.getCode(), UCode.INVALID_ARGUMENT);
- assertEquals(status.getMessage(), "Method URI does not match the transport source URI");
- });
+ private static Stream requestHandlerExceptionProvider() {
+ return Stream.of(
+ Arguments.of(
+ new UStatusException(UStatus.newBuilder()
+ .setCode(UCode.PERMISSION_DENIED)
+ .setMessage("Client is not authorized to invoke this operation")
+ .build()),
+ UCode.PERMISSION_DENIED,
+ "Client is not authorized to invoke this operation"),
+ Arguments.of(
+ new IllegalStateException("service not ready (yet)"),
+ UCode.INTERNAL,
+ InMemoryRpcServer.REQUEST_HANDLER_ERROR_MESSAGE)
+ );
}
- @Test
- @DisplayName("Test unregister request handler where ue_version_major does not match the transport source URI")
- public void testUnregisteringRequestHandlerWithWrongUeVersionMajor() {
- RequestHandler handler = new RequestHandler() {
- @Override
- public UPayload handleRequest(UMessage request) {
- return UPayload.EMPTY;
- }
- };
- RpcServer server = new InMemoryRpcServer(new TestUTransport());
- UUri method = UUri.newBuilder()
- .setAuthorityName("Hartley")
- .setUeId(4)
- .setUeVersionMajor(2)
- .setResourceId(3).build();
-
- CompletionStage result = server.unregisterRequestHandler(method, handler);
- assertFalse(result.toCompletableFuture().isCompletedExceptionally());
- assertDoesNotThrow(() -> {
- UStatus status = result.toCompletableFuture().get();
- assertEquals(status.getCode(), UCode.INVALID_ARGUMENT);
- assertEquals(status.getMessage(), "Method URI does not match the transport source URI");
- });
- }
+ @ParameterizedTest(name = "Test request handler throws exception: {index} => {arguments}")
+ @MethodSource("requestHandlerExceptionProvider")
+ void testHandleRequestHandlesException(Exception thrownException, UCode expectedCode, String expectedMessage) {
+ when(handler.handleRequest(any(UMessage.class))).thenThrow(thrownException);
+ RpcServer server = new InMemoryRpcServer(transport, uriProvider);
+ server.registerRequestHandler(UriFactory.ANY, METHOD_URI.getResourceId(), handler)
+ .toCompletableFuture().join();
+ final ArgumentCaptor requestListener = ArgumentCaptor.forClass(UListener.class);
+ verify(transport).registerListener(eq(UriFactory.ANY), eq(METHOD_URI), requestListener.capture());
- @Test
- @DisplayName("Test register a request handler when we use the ErrorUTransport that returns an error")
- public void testRegisteringRequestListenerWithErrorTransport() {
- RequestHandler handler = new RequestHandler() {
- @Override
- public UPayload handleRequest(UMessage request) {
- return UPayload.EMPTY;
- }
- };
- RpcServer server = new InMemoryRpcServer(new ErrorUTransport());
- CompletionStage result = server.registerRequestHandler(createMethodUri(), handler);
- assertFalse(result.toCompletableFuture().isCompletedExceptionally());
- assertDoesNotThrow(() -> {
- UStatus status = result.toCompletableFuture().get();
- assertEquals(status.getCode(), UCode.FAILED_PRECONDITION);
- });
- }
+ final var request = UMessageBuilder.request(uriProvider.getSource(), METHOD_URI, 5000).build();
+ requestListener.getValue().onReceive(request);
+ verify(handler).handleRequest(request);
- @Test
- @DisplayName("Test handleRequests when we have 2 RpcServers and the request is not for the second instance" +
- "this is to test that we pull from mRequestHandlers and remove returns nothing")
- public void testHandlerequests() {
- // test transport that will trigger the handleRequest()
- UTransport transport = new EchoUTransport();
- RequestHandler handler = new RequestHandler() {
- @Override
- public UPayload handleRequest(UMessage request) {
- throw new UnsupportedOperationException("this should not be called");
- }
- };
-
- RpcServer server = new InMemoryRpcServer(transport);
-
- UUri method = createMethodUri();
- UUri method2 = UUri.newBuilder(method).setResourceId(69).build();
-
- server.registerRequestHandler(method, handler)
- .thenApplyAsync(status -> {
- UMessage request = UMessageBuilder.request(transport.getSource(), method, 1000).build();
- assertDoesNotThrow(() -> {
- return transport.send(request).toCompletableFuture().get();
- });
- return status;
- })
- .thenApplyAsync(status -> {
- assertDoesNotThrow(() -> {
- return server.registerRequestHandler(method2, handler).toCompletableFuture().get();
- });
- return status;
- });
- }
+ final ArgumentCaptor responseMessage = ArgumentCaptor.forClass(UMessage.class);
+ verify(transport).send(responseMessage.capture());
- @Test
- @DisplayName("Test handleRequests the handler triggered an exception")
- public void testHandlerequestsException() {
- // test transport that will trigger the handleRequest()
- UTransport transport = new EchoUTransport();
- RequestHandler handler = new RequestHandler() {
- @Override
- public UPayload handleRequest(UMessage request) {
- throw new UStatusException(UStatus.newBuilder().setCode(UCode.FAILED_PRECONDITION)
- .setMessage("Steven it failed!").build());
- }
- };
-
- RpcServer server = new InMemoryRpcServer(transport);
-
- UUri method = createMethodUri();
-
- server.registerRequestHandler(method, handler).thenApply(status -> {
- UMessage request = UMessageBuilder.request(transport.getSource(), method, 1000).build();
- CompletionStage result = transport.send(request);
- assertFalse(result.toCompletableFuture().isCompletedExceptionally());
- assertDoesNotThrow(() -> {
- assertEquals(result.toCompletableFuture().get().getCode(), UCode.FAILED_PRECONDITION);
- });
- return status;
- });
+ assertEquals(expectedCode, responseMessage.getValue().getAttributes().getCommstatus());
+ final var status = UPayload.unpack(responseMessage.getValue(), UStatus.class);
+ assertEquals(expectedCode, status.get().getCode());
+ assertEquals(expectedMessage, status.get().getMessage());
}
- @Test
- @DisplayName("Test handleRequests the handler triggered an unknown exception")
- public void testHandlerequestsUnknownException() {
- // test transport that will trigger the handleRequest()
- UTransport transport = new EchoUTransport();
- RequestHandler handler = new RequestHandler() {
- @Override
- public UPayload handleRequest(UMessage request) {
- throw new UnsupportedOperationException("Steven it failed!");
- }
- };
-
- RpcServer server = new InMemoryRpcServer(transport);
-
- UUri method = createMethodUri();
-
- server.registerRequestHandler(method, handler).thenAcceptAsync(status -> {
- // fake sending a request message that will trigger the handler to be called but since it is
- // not for the same method as the one registered, it should be ignored and the handler not called
- UMessage request = UMessageBuilder.request(transport.getSource(), method, 1000).build();
-
- CompletionStage result = transport.send(request);
- assertFalse(result.toCompletableFuture().isCompletedExceptionally());
- assertDoesNotThrow(() -> {
- assertEquals(result.toCompletableFuture().get().getCode(), UCode.INTERNAL);
- assertEquals(result.toCompletableFuture().get().getMessage(), "Steven it failed!");
- });
-
- });
+ private static Stream errorHandlersProvider() {
+ return Stream.of(
+ Arguments.of(
+ mock(Consumer.class),
+ mock(Consumer.class),
+ new UStatusException(UCode.UNAVAILABLE, "unavailable")
+ ),
+ Arguments.of(
+ null,
+ null,
+ new UStatusException(UCode.DEADLINE_EXCEEDED, "message expired")
+ ),
+ Arguments.of(
+ null,
+ null,
+ new IllegalStateException("not ready")
+ )
+ );
}
- @Test
- @DisplayName("Test handleRequests when we receive a request for a method that we do not have a registered handler")
- public void testHandlerequestsNoHandler() {
- // test transport that will trigger the handleRequest()
- UTransport transport = new EchoUTransport();
- RequestHandler handler = new RequestHandler() {
- @Override
- public UPayload handleRequest(UMessage request) {
- throw new UnsupportedOperationException("this should not be called");
- }
- };
-
- RpcServer server = new InMemoryRpcServer(transport);
- UUri method = createMethodUri();
- UUri method2 = UUri.newBuilder(method).setResourceId(69).build();
-
- assertDoesNotThrow(() -> {
- server.registerRequestHandler(method, handler).thenApplyAsync(result -> {
- assertEquals(result.getCode(), UCode.OK);
- UMessage request = UMessageBuilder.request(transport.getSource(), method2, 1000).build();
- assertDoesNotThrow(() -> {
- UStatus status = transport.send(request).toCompletableFuture().get();
- assertEquals(status.getCode(), UCode.NOT_FOUND);
- });
- return result;
- });
- });
+ @ParameterizedTest(name = "Test request handler reports send error: {index} => {arguments}")
+ @MethodSource("errorHandlersProvider")
+ void testRequestHandlerReportsSendError(
+ Consumer sendResponseErrorHandler,
+ Consumer unexpectedMessageHandler,
+ Exception sendError) {
+
+ final var request = UMessageBuilder.request(uriProvider.getSource(), METHOD_URI, 5000)
+ .build(UPayload.pack(ByteString.copyFromUtf8("Hello"), UPayloadFormat.UPAYLOAD_FORMAT_TEXT));
+ final var responsePayload = UPayload.pack(
+ ByteString.copyFromUtf8("Hello again"),
+ UPayloadFormat.UPAYLOAD_FORMAT_TEXT);
+ when(handler.handleRequest(any(UMessage.class))).thenReturn(responsePayload);
+ when(transport.send(any(UMessage.class)))
+ .thenReturn(CompletableFuture.failedFuture(sendError));
+
+ var server = new InMemoryRpcServer(transport, uriProvider);
+ Optional.ofNullable(sendResponseErrorHandler).ifPresent(server::setSendErrorHandler);
+ Optional.ofNullable(unexpectedMessageHandler).ifPresent(server::setUnexpectedMessageHandler);
+ server.registerRequestHandler(UriFactory.ANY, METHOD_URI.getResourceId(), handler)
+ .toCompletableFuture().join();
+ final ArgumentCaptor requestListener = ArgumentCaptor.forClass(UListener.class);
+ verify(transport).registerListener(eq(UriFactory.ANY), eq(METHOD_URI), requestListener.capture());
+
+ requestListener.getValue().onReceive(request);
+ verify(handler).handleRequest(request);
+
+ final ArgumentCaptor responseMessage = ArgumentCaptor.forClass(UMessage.class);
+ verify(transport).send(responseMessage.capture());
+ if (sendResponseErrorHandler != null) {
+ verify(sendResponseErrorHandler).accept(sendError);
+ }
+ // Default handler just logs the error, so nothing to verify
}
- @Test
- @DisplayName("Test handling a request where the handler returns a payload and completes successfully")
- public void testHandlerequestsWithPayload() {
- // test transport that will trigger the handleRequest()
- UTransport transport = new EchoUTransport();
-
- RequestHandler handler = new RequestHandler() {
- @Override
- public UPayload handleRequest(UMessage request) {
- return UPayload.EMPTY;
- }
- };
-
- RpcServer server = new InMemoryRpcServer(transport);
-
- UUri method = createMethodUri();
-
- server.registerRequestHandler(method, handler).thenAccept(status -> {
- UMessage request = UMessageBuilder.request(transport.getSource(), method, 1000).build();
- CompletionStage result = transport.send(request);
- assertFalse(result.toCompletableFuture().isCompletedExceptionally());
- assertDoesNotThrow(() -> {
- assertEquals(result.toCompletableFuture().get().getCode(), UCode.OK);
- });
- });
+ static Stream unexpectedMessageHandlerProvider() {
+ return Stream.of(
+ Arguments.of((Consumer) null),
+ Arguments.of(mock(Consumer.class))
+ );
}
- @Test
- @DisplayName("Test registerRequestHandler when passed parameters are null")
- public void testRegisterrequesthandlerNullParameters() {
- RequestHandler handler = new RequestHandler() {
- @Override
- public UPayload handleRequest(UMessage request) {
- return UPayload.EMPTY;
- }
- };
-
- RpcServer server = new InMemoryRpcServer(new TestUTransport());
- assertDoesNotThrow(() -> {
- UStatus status = server.registerRequestHandler(null, null).toCompletableFuture().get();
- assertEquals(status.getCode(), UCode.INVALID_ARGUMENT);
- assertEquals(status.getMessage(), "Method URI or handler missing");
- });
-
- assertDoesNotThrow(() -> {
- UStatus status = server.registerRequestHandler(createMethodUri(), null).toCompletableFuture().get();
- assertEquals(status.getCode(), UCode.INVALID_ARGUMENT);
- assertEquals(status.getMessage(), "Method URI or handler missing");
- });
-
- assertDoesNotThrow(() -> {
- UStatus status = server.registerRequestHandler(null, handler).toCompletableFuture().get();
- assertEquals(status.getCode(), UCode.INVALID_ARGUMENT);
- assertEquals(status.getMessage(), "Method URI or handler missing");
- });
+ @ParameterizedTest
+ @MethodSource("unexpectedMessageHandlerProvider")
+ void testRequestHandlerIgnoresRequestsWithoutHandler(Consumer unexpectedMessageHandler) {
+ final var unsolicitedRequest = UMessageBuilder.request(uriProvider.getSource(), METHOD_URI, 5000).build();
+
+ var server = new InMemoryRpcServer(transport, uriProvider);
+ Optional.ofNullable(unexpectedMessageHandler).ifPresent(server::setUnexpectedMessageHandler);
+ server.registerRequestHandler(UriFactory.ANY, METHOD_URI.getResourceId(), handler)
+ .toCompletableFuture().join();
+ final ArgumentCaptor requestListener = ArgumentCaptor.forClass(UListener.class);
+ verify(transport).registerListener(eq(UriFactory.ANY), eq(METHOD_URI), requestListener.capture());
+
+ server.unregisterRequestHandler(UriFactory.ANY, METHOD_URI.getResourceId(), handler)
+ .toCompletableFuture().join();
+
+ requestListener.getValue().onReceive(unsolicitedRequest);
+ verify(handler, never()).handleRequest(any(UMessage.class));
+ verify(transport, never()).send(any(UMessage.class));
+ if (unexpectedMessageHandler != null) {
+ verify(unexpectedMessageHandler).accept(unsolicitedRequest);
+ }
}
@Test
- @DisplayName("Test unregisterRequestHandler when passed parameters are null")
- public void testUnregisterrequesthandlerNullParameters() {
- RequestHandler handler = new RequestHandler() {
- @Override
- public UPayload handleRequest(UMessage request) {
- return UPayload.EMPTY;
- }
- };
-
- RpcServer server = new InMemoryRpcServer(new TestUTransport());
- assertDoesNotThrow(() -> {
- UStatus status = server.unregisterRequestHandler(null, null).toCompletableFuture().get();
- assertEquals(status.getCode(), UCode.INVALID_ARGUMENT);
- assertEquals(status.getMessage(), "Method URI or handler missing");
- });
-
- assertDoesNotThrow(() -> {
- UStatus status = server.unregisterRequestHandler(createMethodUri(), null).toCompletableFuture().get();
- assertEquals(status.getCode(), UCode.INVALID_ARGUMENT);
- assertEquals(status.getMessage(), "Method URI or handler missing");
- });
-
- assertDoesNotThrow(() -> {
- UStatus status = server.unregisterRequestHandler(null, handler).toCompletableFuture().get();
- assertEquals(status.getCode(), UCode.INVALID_ARGUMENT);
- assertEquals(status.getMessage(), "Method URI or handler missing");
- });
+ void testHandleRequestIgnoresNonRequestMessages() {
+ final var invalidNotification = UMessage.newBuilder()
+ .setAttributes(UAttributes.newBuilder()
+ .setType(UMessageType.UMESSAGE_TYPE_NOTIFICATION)
+ .setSource(TRANSPORT_SOURCE)
+ .setSink(METHOD_URI)
+ .build())
+ .build();
+
+ @SuppressWarnings("unchecked")
+ final Consumer sendResponseErrorHandler = mock(Consumer.class);
+ @SuppressWarnings("unchecked")
+ final Consumer