diff --git a/checkstyle.xml b/checkstyle.xml index 0cf45859..19c90b92 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -33,7 +33,9 @@ - + + + diff --git a/pom.xml b/pom.xml index 8ecb6c66..f8730b66 100644 --- a/pom.xml +++ b/pom.xml @@ -89,11 +89,23 @@ pom import + + org.slf4j + slf4j-bom + 2.0.17 + pom + import + + + org.slf4j + slf4j-api + + com.google.protobuf protobuf-java @@ -149,6 +161,18 @@ [1.4.4,) test + + ch.qos.logback + logback-classic + [1.5,) + test + + + + org.slf4j + jul-to-slf4j + test + diff --git a/src/main/java/org/eclipse/uprotocol/client/usubscription/v3/InMemoryUSubscriptionClient.java b/src/main/java/org/eclipse/uprotocol/client/usubscription/v3/InMemoryUSubscriptionClient.java deleted file mode 100644 index 0bfce6e9..00000000 --- a/src/main/java/org/eclipse/uprotocol/client/usubscription/v3/InMemoryUSubscriptionClient.java +++ /dev/null @@ -1,394 +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.Objects; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.ConcurrentHashMap; -import java.util.logging.Logger; - -import org.eclipse.uprotocol.communication.CallOptions; -import org.eclipse.uprotocol.communication.InMemoryRpcClient; -import org.eclipse.uprotocol.communication.Notifier; -import org.eclipse.uprotocol.communication.RpcClient; -import org.eclipse.uprotocol.communication.RpcMapper; -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.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.core.usubscription.v3.USubscriptionProto; -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.UListener; -import org.eclipse.uprotocol.transport.UTransport; -import org.eclipse.uprotocol.uri.factory.UriFactory; -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 com.google.protobuf.Descriptors.ServiceDescriptor; - -/** - * Implementation of USubscriptionClient that caches state information within the object - * and used for single tenant applications (ex. in-vehicle). The implementation uses {@link InMemoryRpcClient} - * that also stores RPC corelation information within the objects - */ -public class InMemoryUSubscriptionClient implements USubscriptionClient { - private final UTransport transport; - private final RpcClient rpcClient; - private final Notifier notifier; - - private static final ServiceDescriptor USUBSCRIPTION = USubscriptionProto.getDescriptor().getServices().get(0); - - // TODO: The following items eventually need to be pulled from generated code - private static final UUri SUBSCRIBE_METHOD = UriFactory.fromProto(USUBSCRIPTION, 1); - private static final UUri UNSUBSCRIBE_METHOD = UriFactory.fromProto(USUBSCRIPTION, 2); - private static final UUri FETCH_SUBSCRIBERS_METHOD = UriFactory.fromProto(USUBSCRIPTION, 8); - private static final UUri FETCH_SUBSCRIPTIONS_METHOD = UriFactory.fromProto(USUBSCRIPTION, 3); - private static final UUri REGISTER_NOTIFICATIONS_METHOD = UriFactory.fromProto(USUBSCRIPTION, 6); - private static final UUri UNREGISTER_NOTIFICATIONS_METHOD = UriFactory.fromProto(USUBSCRIPTION, 7); - - private static final UUri NOTIFICATION_TOPIC = UriFactory.fromProto(USUBSCRIPTION, 0x8000); - - - // Map to store subscription change notification handlers - private final ConcurrentHashMap mHandlers = new ConcurrentHashMap<>(); - - // transport Notification listener that will process subscription change notifications - private final UListener mNotificationListener = this::handleNotifications; - - - /** - * 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 - */ - public InMemoryUSubscriptionClient (UTransport transport) { - this(transport, new InMemoryRpcClient(transport), new SimpleNotifier(transport)); - } - - - /** - * Creates a new USubscription client passing {@link UTransport}, {@link CallOptions}, - * and an implementation of {@link RpcClient} and {@link Notifier}. - * - * @param transport the transport to use for sending the notifications - * @param rpcClient the rpc client to use for sending the RPC requests - * @param notifier the notifier to use for registering the notification listener - */ - public InMemoryUSubscriptionClient (UTransport transport, RpcClient rpcClient, Notifier notifier) { - Objects.requireNonNull(transport, UTransport.TRANSPORT_NULL_ERROR); - Objects.requireNonNull(rpcClient, "RpcClient missing"); - Objects.requireNonNull(notifier, "Notifier missing"); - this.transport = transport; - this.rpcClient = rpcClient; - this.notifier = notifier; - - // Register the notification listener to receive subscription change notifications - notifier.registerNotificationListener(NOTIFICATION_TOPIC, mNotificationListener).toCompletableFuture().join(); - } - - - /** - * 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 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. - */ - @Override - public CompletionStage subscribe( - UUri topic, - UListener listener, - CallOptions options, - SubscriptionChangeHandler handler) { - Objects.requireNonNull(topic, "Subscribe topic missing"); - Objects.requireNonNull(listener, "Request listener missing"); - Objects.requireNonNull(options, "CallOptions missing"); - - final SubscriptionRequest request = SubscriptionRequest.newBuilder() - .setTopic(topic) - .build(); - - // Send the subscription request and handle the response - return RpcMapper.mapResponse(rpcClient.invokeMethod( - SUBSCRIBE_METHOD, UPayload.pack(request), options), SubscriptionResponse.class) - - // Then register the listener to be called when messages are received - .thenCompose(response -> { - if ( response.getStatus().getState() == SubscriptionStatus.State.SUBSCRIBED || - response.getStatus().getState() == SubscriptionStatus.State.SUBSCRIBE_PENDING) { - // When registering the listener fails, we have end up in a situation where we - // have successfully (logically) subscribed to the topic via the USubscriptio 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. - return transport.registerListener(topic, listener).thenApply(status -> response); - } - return CompletableFuture.completedFuture(response); - }) - - // Then Add the handler (if the client provided one) so the client can be notified of - // changes to the subscription state. - .whenComplete( (response, exception) -> { - if (exception == null && handler != null) { - mHandlers.compute(topic, (k, existingHandler) -> { - if (existingHandler != null && existingHandler != handler) { - throw new UStatusException(UCode.ALREADY_EXISTS, "Handler already registered"); - } - return handler; - }); - } - }); - } - - - /** - * 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 messages are received. - * @param options The {@link CallOptions} to be used for the unsubscribe request. - * @return Returns {@link UStatus} with the result from the unsubscribe request. - */ - @Override - public CompletionStage unsubscribe(UUri topic, UListener listener, CallOptions options) { - Objects.requireNonNull(topic, "Unsubscribe topic missing"); - Objects.requireNonNull(listener, "listener missing"); - Objects.requireNonNull(options, "CallOptions missing"); - - final UnsubscribeRequest unsubscribeRequest = UnsubscribeRequest.newBuilder() - .setTopic(topic) - .build(); - - return RpcMapper.mapResponseToResult( - // Send the unsubscribe request - rpcClient.invokeMethod(UNSUBSCRIBE_METHOD, UPayload.pack(unsubscribeRequest), options), - UnsubscribeResponse.class) - // Then unregister the listener - .thenCompose( response -> { - if (response.isSuccess()) { - // Remove the handler only if unsubscribe was successful - mHandlers.remove(topic); - - return transport.unregisterListener(topic, listener); - } - return CompletableFuture.completedFuture(response.failureValue()); - }); - } - - - /** - * Unregister the 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. - * - * @param topic The topic to subscribe to. - * @param listener The listener to be called when messages are received. - * @return Returns {@link UStatus} with the status of the listener unregister request. - */ - @Override - public CompletionStage unregisterListener(UUri topic, UListener listener) { - Objects.requireNonNull(topic, "Unsubscribe topic missing"); - Objects.requireNonNull(listener, "Request listener missing"); - return transport.unregisterListener(topic, listener) - .whenComplete((status, exception) -> mHandlers.remove(topic)); - } - - /** - * Close the subscription client and clean up resources. - */ - public void close() { - mHandlers.clear(); - notifier.unregisterNotificationListener(NOTIFICATION_TOPIC, mNotificationListener) - .toCompletableFuture().join(); - } - - - /** - * Register for Subscription Change Notifications. - * - * This API allows producers to register to receive subscription change notifications for - * topics that they produce only. - * - * @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 register 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. - */ - @Override - public CompletionStage registerForNotifications(UUri topic, - SubscriptionChangeHandler handler, CallOptions options) { - Objects.requireNonNull(topic, "Topic missing"); - Objects.requireNonNull(handler, "Handler missing"); - Objects.requireNonNull(options, "CallOptions missing"); - - NotificationsRequest request = NotificationsRequest.newBuilder() - .setTopic(topic) - .build(); - - return RpcMapper.mapResponse(rpcClient.invokeMethod(REGISTER_NOTIFICATIONS_METHOD, - UPayload.pack(request), options), NotificationsResponse.class) - // Then Add the handler (if the client provided one) so the client can be notified of - // changes to the subscription state. - .whenComplete( (response, exception) -> { - if (exception == null) { - mHandlers.compute(topic, (k, existingHandler) -> { - if (existingHandler != null && existingHandler != handler) { - throw new UStatusException(UCode.ALREADY_EXISTS, "Handler already registered"); - } - return handler; - }); - } - }); - } - - - /** - * Unregister for subscription change notifications. - * - * @param topic The topic to unregister for notifications. - * @param options The {@link CallOptions} to be used for the unregister 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. {@link UCode#PERMISSION_DENIED} is - * returned if the topic ue_id does not equal the callers ue_id. - */ - @Override - public CompletionStage unregisterForNotifications(UUri topic, CallOptions options) { - Objects.requireNonNull(topic, "Topic missing"); - Objects.requireNonNull(options, "CallOptions missing"); - - NotificationsRequest request = NotificationsRequest.newBuilder() - .setTopic(topic) - .build(); - - return RpcMapper.mapResponse(rpcClient.invokeMethod(UNREGISTER_NOTIFICATIONS_METHOD, - UPayload.pack(request), options), NotificationsResponse.class) - .whenComplete((response, exception) -> mHandlers.remove(topic)); - } - - - /** - * 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 fetch 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. - */ - @Override - public CompletionStage fetchSubscribers(UUri topic, CallOptions options) { - Objects.requireNonNull(topic, "Topic missing"); - Objects.requireNonNull(options, "CallOptions missing"); - - FetchSubscribersRequest request = FetchSubscribersRequest.newBuilder().setTopic(topic).build(); - return RpcMapper.mapResponse(rpcClient.invokeMethod(FETCH_SUBSCRIBERS_METHOD, - UPayload.pack(request), options), FetchSubscribersResponse.class); - } - - - /** - * 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. - * - * @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. - */ - @Override - public CompletionStage fetchSubscriptions(FetchSubscriptionsRequest request, - CallOptions options) { - Objects.requireNonNull(request, "Request missing"); - Objects.requireNonNull(options, "CallOptions missing"); - - return RpcMapper.mapResponse(rpcClient.invokeMethod(FETCH_SUBSCRIPTIONS_METHOD, - UPayload.pack(request), options), FetchSubscriptionsResponse.class); - } - - - /** - * Handles incoming notifications from the USubscription service. - * - * @param message The notification message from the USubscription service - */ - private void handleNotifications(UMessage message) { - // Ignore messages that are not notifications - if (message.getAttributes().getType() != UMessageType.UMESSAGE_TYPE_NOTIFICATION) { - return; - } - - // Unpack the notification message from uSubscription called Update - Optional 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 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 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 unexpectedMessageHandler = mock(Consumer.class); + + var server = new InMemoryRpcServer( + transport, + uriProvider); + server.setSendErrorHandler(sendResponseErrorHandler); + server.setUnexpectedMessageHandler(unexpectedMessageHandler); + 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(invalidNotification); + verify(handler, never()).handleRequest(any(UMessage.class)); + verify(sendResponseErrorHandler, never()).accept(any(Throwable.class)); + verify(transport, never()).send(any(UMessage.class)); + verify(unexpectedMessageHandler).accept(invalidNotification); } - - // Helper method to create a UUri that matches that of the default TestUTransport - private UUri createMethodUri() { - return UUri.newBuilder() - .setAuthorityName("Hartley") - .setUeId(4) - .setUeVersionMajor(1) - .setResourceId(3).build(); + @Test + @DisplayName("Test handling a request where the handler returns a payload and completes successfully") + void testHandleRequestSucceedsWithPayload() { + 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); + + 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()); + + requestListener.getValue().onReceive(request); + verify(handler).handleRequest(request); + + final ArgumentCaptor responseMessage = ArgumentCaptor.forClass(UMessage.class); + verify(transport).send(responseMessage.capture()); + + assertEquals(UCode.OK, responseMessage.getValue().getAttributes().getCommstatus()); + assertEquals(responsePayload.data(), responseMessage.getValue().getPayload()); } - } diff --git a/src/test/java/org/eclipse/uprotocol/communication/InMemorySubscriberTest.java b/src/test/java/org/eclipse/uprotocol/communication/InMemorySubscriberTest.java new file mode 100644 index 00000000..7d102490 --- /dev/null +++ b/src/test/java/org/eclipse/uprotocol/communication/InMemorySubscriberTest.java @@ -0,0 +1,671 @@ +/** + * 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.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.function.Consumer; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; + +import org.mockito.junit.jupiter.MockitoExtension; + +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.SubscriptionStatus; +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.core.usubscription.v3.SubscriptionStatus.State; +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.transport.builder.UMessageBuilder; +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.UMessageType; +import org.eclipse.uprotocol.v1.UUri; + +@ExtendWith(MockitoExtension.class) +class InMemorySubscriberTest { + private static final UUri SUBSCRIPTION_SERVICE_URI = UUri.newBuilder() + .setAuthorityName("some-host") + .setUeId(0x0002_0000) + .setUeVersionMajor(0x03) + .build(); + private static final UUri SUBSCRIPTION_NOTIFICATION_TOPIC_URI = UUri.newBuilder(SUBSCRIPTION_SERVICE_URI) + .setResourceId(0x8000) + .build(); + private static final UUri TOPIC = UUri.newBuilder() + .setAuthorityName("my-vehicle") + .setUeId(0xa103) + .setUeVersionMajor(0x06) + .setResourceId(0xa10f) + .build(); + + private static final UUri SOURCE = UUri.newBuilder() + .setAuthorityName("my-vehicle") + .setUeId(0x0004) + .setUeVersionMajor(0x01) + .build(); + + private UTransport transport; + private LocalUriProvider uriProvider; + private Notifier notifier; + private USubscriptionClient subscriptionClient; + + @Mock + private SubscriptionChangeHandler subscriptionChangeHandler; + private ArgumentCaptor notificationListener; + + @Mock + private UListener listener; + + @BeforeEach + void setup() { + uriProvider = StaticUriProvider.of(SOURCE); + + 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.registerListener(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.unregisterListener(any(UUri.class), any(UListener.class))) + .thenReturn(CompletableFuture.completedFuture(null)); + + notifier = mock(Notifier.class); + Mockito.lenient().when(notifier.registerNotificationListener(any(UUri.class), any(UListener.class))) + .thenReturn(CompletableFuture.completedFuture(null)); + Mockito.lenient().when(notifier.unregisterNotificationListener(any(UUri.class), any(UListener.class))) + .thenReturn(CompletableFuture.completedFuture(null)); + + subscriptionClient = mock(USubscriptionClient.class); + Mockito.lenient().when(subscriptionClient.getSubscriptionServiceNotificationTopic()) + .thenReturn(SUBSCRIPTION_NOTIFICATION_TOPIC_URI); + + notificationListener = ArgumentCaptor.forClass(UListener.class); + } + + @Test + @DisplayName("Test constructors reject invalid arguments") + void testConstructorsRejectInvalidArguments() { + assertThrows(NullPointerException.class, () -> new InMemorySubscriber( + null, + uriProvider, + CallOptions.DEFAULT, + 0x0000, + "local")); + assertThrows(NullPointerException.class, () -> new InMemorySubscriber( + transport, + null, + CallOptions.DEFAULT, + 0x0000, + "local")); + assertThrows(NullPointerException.class, () -> new InMemorySubscriber( + transport, + uriProvider, + null, + 0x0000, + "local")); + assertThrows(IllegalArgumentException.class, () -> new InMemorySubscriber( + transport, + uriProvider, + CallOptions.DEFAULT, + 0xFFFF, + "local")); + assertThrows(IllegalArgumentException.class, () -> new InMemorySubscriber( + transport, + uriProvider, + CallOptions.DEFAULT, + -1, + "local")); + + assertThrows(NullPointerException.class, () -> new InMemorySubscriber( + null, + subscriptionClient, + notifier)); + assertThrows(NullPointerException.class, () -> new InMemorySubscriber( + transport, + null, + notifier)); + assertThrows(NullPointerException.class, () -> new InMemorySubscriber( + transport, + subscriptionClient, + null)); + } + + @Test + void testConstructorForTransportAndUriProvider() { + new InMemorySubscriber( + transport, + uriProvider, + CallOptions.DEFAULT, + 0x0002, + SUBSCRIPTION_NOTIFICATION_TOPIC_URI.getAuthorityName()); + verify(transport).registerListener( + eq(SUBSCRIPTION_NOTIFICATION_TOPIC_URI), + eq(SOURCE), + any(UListener.class)); + } + + @Test + void testSubscriberRegistersNotificationListener() { + // WHEN trying to create a Subscriber + new InMemorySubscriber(transport, subscriptionClient, notifier); + // THEN the Subscriber registers a notification listener + verify(notifier).registerNotificationListener( + eq(SUBSCRIPTION_NOTIFICATION_TOPIC_URI), + notificationListener.capture()); + } + + @Test + void testSubscriberCreationFailsWhenNotifierFailsToRegisterListener() { + // GIVEN a Notifier that is not connected to its transport + when(notifier.registerNotificationListener(any(UUri.class), any(UListener.class))) + .thenReturn(CompletableFuture.failedFuture( + new UStatusException(UCode.UNAVAILABLE, "not available"))); + + // WHEN trying to create a Subscriber for this Notifier + final var exception = assertThrows(CompletionException.class, () -> new InMemorySubscriber( + transport, + subscriptionClient, + notifier + )); + // THEN creation fails + verify(notifier).registerNotificationListener( + eq(SUBSCRIPTION_NOTIFICATION_TOPIC_URI), + any(UListener.class)); + assertEquals(UCode.UNAVAILABLE, ((UStatusException) exception.getCause()).getStatus().getCode()); + } + + + @Test + void testCloseForSuccessfulUnregistration() { + // GIVEN a Notifier that succeeds to unregister listeners + when(notifier.unregisterNotificationListener(any(UUri.class), any(UListener.class))) + .thenReturn(CompletableFuture.completedFuture(null)); + assertCloseIgnoresFailureToUnregisterNotificationListener(notifier); + } + + @Test + void testCloseForFailedUnregistration() { + // GIVEN a Notifier that fails to unregister listeners + when(notifier.unregisterNotificationListener(any(UUri.class), any(UListener.class))) + .thenReturn(CompletableFuture.failedFuture( + new UStatusException(UCode.UNAVAILABLE, "not available"))); + assertCloseIgnoresFailureToUnregisterNotificationListener(notifier); + } + + private void assertCloseIgnoresFailureToUnregisterNotificationListener(Notifier notifier) { + // GIVEN a Subscriber using the Notifier + var subscriber = new InMemorySubscriber(transport, subscriptionClient, notifier); + verify(notifier).registerNotificationListener( + eq(SUBSCRIPTION_NOTIFICATION_TOPIC_URI), + notificationListener.capture()); + + // WHEN trying to close the Subscriber + subscriber.close(); + + // THEN the listener is getting unregistered + verify(notifier).unregisterNotificationListener( + SUBSCRIPTION_NOTIFICATION_TOPIC_URI, + notificationListener.getValue()); + } + + @Test + void testSubscribeFailsWhenUSubscriptionInvocationFails() { + // GIVEN a USubscription client + // that fails to perform subscription due to different reasons + when(subscriptionClient.subscribe(any(SubscriptionRequest.class))) + .thenReturn(CompletableFuture.failedFuture( + new UStatusException(UCode.UNAVAILABLE, "not connected"))) + .thenReturn(CompletableFuture.completedFuture( + SubscriptionResponse.newBuilder() + .setTopic(TOPIC) + .setStatus(SubscriptionStatus.newBuilder() + .setState(SubscriptionStatus.State.UNSUBSCRIBED) + .setMessage("unsupported topic") + .build()) + .build())) + .thenReturn(CompletableFuture.completedFuture( + SubscriptionResponse.newBuilder() + .setTopic(TOPIC) + .setStatus(SubscriptionStatus.newBuilder() + .setMessage("unknown state") + .build()) + .build())); + + // and a Subscriber using that USubscription client + var subscriber = new InMemorySubscriber(transport, subscriptionClient, notifier); + verify(notifier).registerNotificationListener( + eq(SUBSCRIPTION_NOTIFICATION_TOPIC_URI), + notificationListener.capture()); + + // WHEN subscribing to a topic + var attempt1 = subscriber.subscribe(TOPIC, listener, Optional.empty()); + + // THEN the first attempt fails + var exception = assertThrows(CompletionException.class, () -> attempt1.toCompletableFuture().join()); + assertEquals(UCode.UNAVAILABLE, ((UStatusException) exception.getCause()).getCode()); + + // AND the second attempt fails as well + var attempt2 = subscriber.subscribe(TOPIC, listener, Optional.empty()); + exception = assertThrows(CompletionException.class, () -> attempt2.toCompletableFuture().join()); + assertEquals(UCode.INTERNAL, ((UStatusException) exception.getCause()).getCode()); + + // AND the third attempt fails as well + var attempt3 = subscriber.subscribe(TOPIC, listener, Optional.empty()); + exception = assertThrows(CompletionException.class, () -> attempt3.toCompletableFuture().join()); + assertEquals(UCode.INTERNAL, ((UStatusException) exception.getCause()).getCode()); + + verify(subscriptionClient, times(3)).subscribe(argThat(req -> req.getTopic().equals(TOPIC))); + } + + @Test + void testRepeatedSubscribeFailsForDifferentSubscriptionChangeHandlers() { + // GIVEN a USubscription client + // that succeeds to subscribe to topics + when(subscriptionClient.subscribe(any(SubscriptionRequest.class))) + .thenReturn(CompletableFuture.completedFuture( + SubscriptionResponse.newBuilder() + .setTopic(TOPIC) + .setStatus(SubscriptionStatus.newBuilder() + .setState(SubscriptionStatus.State.SUBSCRIBED) + .build()) + .build())); + + // AND a transport + when(transport.registerListener(any(UUri.class), any(UListener.class))) + // that fails to register a listener on the first attempt + .thenReturn(CompletableFuture.failedFuture( + new UStatusException(UCode.UNAVAILABLE, "not connected"))) + // but succeeds to do so on the second attempt + .thenReturn(CompletableFuture.completedFuture(null)); + + // AND a Subscriber using that USubscription client, Notifier and transport + var subscriber = new InMemorySubscriber(transport, subscriptionClient, notifier); + verify(notifier).registerNotificationListener( + eq(SUBSCRIPTION_NOTIFICATION_TOPIC_URI), + notificationListener.capture()); + + // WHEN subscribing to a topic + var attempt1 = subscriber.subscribe(TOPIC, listener, Optional.of(subscriptionChangeHandler)); + + // THEN the first attempt fails due to the transport having failed + var exception = assertThrows(CompletionException.class, () -> attempt1.toCompletableFuture().join()); + assertEquals(UCode.UNAVAILABLE, ((UStatusException) exception.getCause()).getCode()); + + // AND a second attempt using a different subscription change handler + var attempt2 = subscriber.subscribe(TOPIC, listener, Optional.of(mock(SubscriptionChangeHandler.class))); + exception = assertThrows(CompletionException.class, () -> attempt2.toCompletableFuture().join()); + // fails with an ALREADY_EXISTS error + assertEquals(UCode.ALREADY_EXISTS, ((UStatusException) exception.getCause()).getCode()); + + verify(transport, times(1)).registerListener(TOPIC, listener); + verify(subscriptionClient, times(2)).subscribe(argThat(req -> req.getTopic().equals(TOPIC))); + } + + @Test + void testSubscribeSucceedsOnSecondAttempt() { + + // GIVEN a USubscription client that succeeds to subscribe to topics + when(subscriptionClient.subscribe(any(SubscriptionRequest.class))) + .thenReturn(CompletableFuture.completedFuture( + SubscriptionResponse.newBuilder() + .setTopic(TOPIC) + .setStatus(SubscriptionStatus.newBuilder() + .setState(SubscriptionStatus.State.SUBSCRIBED) + .build()) + .build())); + + // and a transport + when(transport.registerListener(any(UUri.class), any(UListener.class))) + // that first fails to register a listener + .thenReturn(CompletableFuture.failedFuture( + new UStatusException(UCode.UNAVAILABLE, "not connected"))) + // but succeeds on the second attempt + .thenReturn(CompletableFuture.completedStage(null)); + + + // and a Subscriber using that USubscription client, Notifier and transport + var subscriber = new InMemorySubscriber(transport, subscriptionClient, notifier); + verify(notifier).registerNotificationListener( + eq(SUBSCRIPTION_NOTIFICATION_TOPIC_URI), + notificationListener.capture()); + + // WHEN subscribing to a topic + var attempt1 = subscriber.subscribe(TOPIC, listener, Optional.of(subscriptionChangeHandler)); + + // THEN the first attempt fails due to the transport having failed + var exception = assertThrows(CompletionException.class, () -> attempt1.toCompletableFuture().join()); + assertEquals(UCode.UNAVAILABLE, ((UStatusException) exception.getCause()).getCode()); + + // but the second attempt succeeds + subscriber.subscribe(TOPIC, listener, Optional.of(subscriptionChangeHandler)) + .toCompletableFuture().join(); + verify(subscriptionClient, times(2)).subscribe(argThat(req -> req.getTopic().equals(TOPIC))); + verify(transport, times(2)).registerListener(TOPIC, listener); + + // and the subscription change handler receives notifications + var update = Update.newBuilder() + .setTopic(TOPIC) + .setStatus(SubscriptionStatus.newBuilder() + .setState(SubscriptionStatus.State.UNSUBSCRIBED) + .build()) + .build(); + var subscriptionChange = UMessageBuilder.notification(SUBSCRIPTION_NOTIFICATION_TOPIC_URI, SOURCE) + .build(UPayload.pack(update)); + notificationListener.getValue().onReceive(subscriptionChange); + verify(subscriptionChangeHandler, times(1)).handleSubscriptionChange( + eq(TOPIC), any(SubscriptionStatus.class)); + } + + @Test + void testUnsubscribeFailsForUnknownListener() { + + // GIVEN a USubscription client + // that succeeds to unsubscribe from topics + when(subscriptionClient.unsubscribe(any(UnsubscribeRequest.class))) + .thenReturn(CompletableFuture.completedFuture(UnsubscribeResponse.newBuilder().build())); + + // AND a transport + // which fails to unregister an unknown listener + when(transport.unregisterListener(any(UUri.class), any(UListener.class))) + .thenReturn(CompletableFuture.failedFuture( + new UStatusException(UCode.NOT_FOUND, "no such listener"))); + + // AND a Subscriber using that USubscription client, Notifier and transport + var subscriber = new InMemorySubscriber(transport, subscriptionClient, notifier); + verify(notifier).registerNotificationListener( + eq(SUBSCRIPTION_NOTIFICATION_TOPIC_URI), + notificationListener.capture()); + + // WHEN unsubscribing from a topic for which no listener had been registered + var attempt = subscriber.unsubscribe(TOPIC, listener); + + // THEN the attempt fails + var exception = assertThrows(CompletionException.class, () -> attempt.toCompletableFuture().join()); + assertEquals(UCode.NOT_FOUND, ((UStatusException) exception.getCause()).getCode()); + verify(subscriptionClient).unsubscribe(argThat(req -> req.getTopic().equals(TOPIC))); + verify(transport).unregisterListener(TOPIC, listener); + } + + @Test + void testUnsubscribeFailsIfUSubscriptionInvocationFails() { + // GIVEN a USubscription client + // that fails to unsubscribe from topics + when(subscriptionClient.unsubscribe(any(UnsubscribeRequest.class))) + .thenReturn(CompletableFuture.failedFuture( + new UStatusException(UCode.UNAVAILABLE, "unknown"))); + + // AND a transport + // which succeeds to unregister listeners + // (no need to stub explicitly, because this is the default) + + // AND a Subscriber using that USubscription client, Notifier and transport + var subscriber = new InMemorySubscriber(transport, subscriptionClient, notifier); + verify(notifier).registerNotificationListener( + eq(SUBSCRIPTION_NOTIFICATION_TOPIC_URI), + notificationListener.capture()); + // which already has a listener and corresponding subscription change handler + // registered for a topic + subscriber.addSubscriptionChangeHandler(TOPIC, Optional.of(subscriptionChangeHandler)); + assertTrue(subscriber.hasSubscriptionChangeHandler(TOPIC)); + + // WHEN unsubscribing from the topic + var attempt = subscriber.unsubscribe(TOPIC, listener); + + // THEN the the attempt fails + var exception = assertThrows( + CompletionException.class, + () -> attempt.toCompletableFuture().join()); + assertEquals(UCode.UNAVAILABLE, ((UStatusException) exception.getCause()).getCode()); + verify(subscriptionClient).unsubscribe(argThat(req -> req.getTopic().equals(TOPIC))); + verify(transport, never()).unregisterListener(TOPIC, listener); + // AND the subscription change handler is still registered + assertTrue(subscriber.hasSubscriptionChangeHandler(TOPIC)); + } + + @Test + void testUnsubscribeSucceeds() { + // GIVEN a USubscription client + // that succeeds to unsubscribe from topics + when(subscriptionClient.unsubscribe(any(UnsubscribeRequest.class))) + .thenReturn(CompletableFuture.completedFuture(UnsubscribeResponse.newBuilder().build())); + + // and a transport + // which succeeds to unregister listeners + // (no need to stub explicitly, because this is the default) + + // AND a Subscriber using that USubscription client, Notifier and transport + var subscriber = new InMemorySubscriber(transport, subscriptionClient, notifier); + verify(notifier).registerNotificationListener( + eq(SUBSCRIPTION_NOTIFICATION_TOPIC_URI), + notificationListener.capture()); + // which already has a listener and corresponding subscription change handler + // registered for a topic + subscriber.addSubscriptionChangeHandler(TOPIC, Optional.of(subscriptionChangeHandler)); + assertTrue(subscriber.hasSubscriptionChangeHandler(TOPIC)); + + // WHEN unsubscribing from the topic + var attempt = subscriber.unsubscribe(TOPIC, listener); + + // THEN the the attempt succeeds + attempt.toCompletableFuture().join(); + verify(subscriptionClient).unsubscribe(argThat(req -> req.getTopic().equals(TOPIC))); + verify(transport).unregisterListener(TOPIC, listener); + // AND the subscription change handler is no longer registered + assertFalse(subscriber.hasSubscriptionChangeHandler(TOPIC)); + } + + @Test + void testUnsubscribeSucceedsOnSecondAttempt() { + // GIVEN a USubscription client + // that succeeds to unsubscribe from topics + when(subscriptionClient.unsubscribe(any(UnsubscribeRequest.class))) + .thenReturn(CompletableFuture.completedFuture(UnsubscribeResponse.newBuilder().build())); + + // and a transport + when(transport.unregisterListener(any(UUri.class), any(UListener.class))) + // that first fails to unregister a listener + .thenReturn(CompletableFuture.failedFuture( + new UStatusException(UCode.UNAVAILABLE, "not connected"))) + // but succeeds on the second attempt + .thenReturn(CompletableFuture.completedFuture(null)); + + // AND a Subscriber using that USubscription client, Notifier and transport + var subscriber = new InMemorySubscriber(transport, subscriptionClient, notifier); + verify(notifier).registerNotificationListener( + eq(SUBSCRIPTION_NOTIFICATION_TOPIC_URI), + notificationListener.capture()); + // which already has a listener and corresponding subscription change handler + // registered for a topic + subscriber.addSubscriptionChangeHandler(TOPIC, Optional.of(subscriptionChangeHandler)); + assertTrue(subscriber.hasSubscriptionChangeHandler(TOPIC)); + + // WHEN unsubscribing from a topic for which a listener had been registered before + var attempt = subscriber.unsubscribe(TOPIC, listener); + + // THEN the first attempt fails + var exception = assertThrows( + CompletionException.class, + () -> attempt.toCompletableFuture().join()); + assertEquals(UCode.UNAVAILABLE, ((UStatusException) exception.getCause()).getCode()); + + // but the second attempt succeeds + subscriber.unsubscribe(TOPIC, listener).toCompletableFuture().join(); + // AND the handler has been removed + assertFalse(subscriber.hasSubscriptionChangeHandler(TOPIC)); + + verify(subscriptionClient, times(2)).unsubscribe(argThat(req -> req.getTopic().equals(TOPIC))); + verify(transport, times(2)).unregisterListener(TOPIC, listener); + } + + @Test + void testHandleSubscriptionChangeNotificationHandlesInvalidMessages() { + // GIVEN a Subscriber + var subscriber = new InMemorySubscriber(transport, subscriptionClient, notifier); + verify(notifier).registerNotificationListener( + eq(SUBSCRIPTION_NOTIFICATION_TOPIC_URI), + notificationListener.capture()); + + @SuppressWarnings("unchecked") + Consumer unexpectedMessageHandler = mock(Consumer.class); + subscriber.setUnexpectedMessageHandler(unexpectedMessageHandler); + + // WHEN a non-notification message is received + var malformedPublishMessage = UMessage.newBuilder() + .setAttributes(UAttributes.newBuilder() + .setId(UuidFactory.Factories.UPROTOCOL.factory().create()) + .setType(UMessageType.UMESSAGE_TYPE_PUBLISH) + .setSource(SUBSCRIPTION_NOTIFICATION_TOPIC_URI) + .setSink(SOURCE) + .build()) + .build(); + notificationListener.getValue().onReceive(malformedPublishMessage); + + // THEN the unexpected message handler is invoked + verify(unexpectedMessageHandler).accept(malformedPublishMessage); + + // AND when a notification message is received for which no + // handler had been registered + var subscriptionChange = Update.newBuilder().setTopic(TOPIC).build(); + var notificationMessage = UMessageBuilder.notification( + SUBSCRIPTION_NOTIFICATION_TOPIC_URI, + SOURCE) + .build(UPayload.pack(subscriptionChange)); + + notificationListener.getValue().onReceive(notificationMessage); + + // THEN the unexpected message handler is invoked + verify(unexpectedMessageHandler).accept(notificationMessage); + } + + @Test + void testHandleSubscriptionChangeNotificationIgnoresErroneousHandler() { + // GIVEN a Subscriber + var subscriber = new InMemorySubscriber(transport, subscriptionClient, notifier); + verify(notifier).registerNotificationListener( + eq(SUBSCRIPTION_NOTIFICATION_TOPIC_URI), + notificationListener.capture()); + + // with a subscription change handler registered for a topic + subscriber.addSubscriptionChangeHandler(TOPIC, Optional.of(subscriptionChangeHandler)); + + // WHEN the handler for an incoming subscription update notification throws an exception + doThrow(new RuntimeException("boom")) + .when(subscriptionChangeHandler).handleSubscriptionChange( + eq(TOPIC), any(SubscriptionStatus.class)); + var subscriptionChange = Update.newBuilder().setTopic(TOPIC).build(); + var notificationMessage = UMessageBuilder.notification( + SUBSCRIPTION_NOTIFICATION_TOPIC_URI, + SOURCE) + .build(UPayload.pack(subscriptionChange)); + + // THEN the exception is caught and ignored + assertDoesNotThrow(() -> notificationListener.getValue().onReceive(notificationMessage)); + } + + @Test + void testRegisterSubscriptionChangeHandlerSucceeds() { + // GIVEN a USubscription client that succeeds to register for notifications + when(subscriptionClient.registerForNotifications(any(NotificationsRequest.class))) + .thenReturn(CompletableFuture.completedFuture(NotificationsResponse.getDefaultInstance())); + // AND a subscriber using the client + var subscriber = new InMemorySubscriber(transport, subscriptionClient, notifier); + verify(notifier).registerNotificationListener( + eq(SUBSCRIPTION_NOTIFICATION_TOPIC_URI), + notificationListener.capture()); + + // WHEN a subscription change handler is registered for a topic + subscriber.registerSubscriptionChangeHandler(TOPIC, subscriptionChangeHandler) + .toCompletableFuture().join(); + verify(subscriptionClient).registerForNotifications(argThat(req -> req.getTopic().equals(TOPIC))); + + // THEN the handler is getting invoked whenever a subscription change + // for the topic of interest is received + var subscriptionChange = Update.newBuilder() + .setTopic(TOPIC) + .setStatus(SubscriptionStatus.newBuilder().setState(State.UNSUBSCRIBED)) + .build(); + var notificationMessage = UMessageBuilder.notification( + SUBSCRIPTION_NOTIFICATION_TOPIC_URI, + SOURCE) + .build(UPayload.pack(subscriptionChange)); + notificationListener.getValue().onReceive(notificationMessage); + verify(subscriptionChangeHandler).handleSubscriptionChange(TOPIC, subscriptionChange.getStatus()); + } + + @Test + void testUnregisterSubscriptionChangeHandlerSucceeds() { + // GIVEN a USubscription client that succeeds to unregister for notifications + when(subscriptionClient.unregisterForNotifications(any(NotificationsRequest.class))) + .thenReturn(CompletableFuture.completedFuture(NotificationsResponse.getDefaultInstance())); + // AND a subscriber using the client + var subscriber = new InMemorySubscriber(transport, subscriptionClient, notifier); + verify(notifier).registerNotificationListener( + eq(SUBSCRIPTION_NOTIFICATION_TOPIC_URI), + notificationListener.capture()); + // that has a handler registered for subscription changes + subscriber.addSubscriptionChangeHandler(TOPIC, Optional.of(subscriptionChangeHandler)); + + // WHEN the handler is getting unregistered + subscriber.unregisterSubscriptionChangeHandler(TOPIC) + .toCompletableFuture().join(); + verify(subscriptionClient).unregisterForNotifications(argThat(req -> req.getTopic().equals(TOPIC))); + + // THEN the handler is no longer getting invoked when a subscription change + // for the topic of interest is received + var subscriptionChange = Update.newBuilder() + .setTopic(TOPIC) + .setStatus(SubscriptionStatus.newBuilder().setState(State.UNSUBSCRIBED)) + .build(); + var notificationMessage = UMessageBuilder.notification( + SUBSCRIPTION_NOTIFICATION_TOPIC_URI, + SOURCE) + .build(UPayload.pack(subscriptionChange)); + notificationListener.getValue().onReceive(notificationMessage); + verify(subscriptionChangeHandler, never()).handleSubscriptionChange(eq(TOPIC), any(SubscriptionStatus.class)); + } +} diff --git a/src/test/java/org/eclipse/uprotocol/communication/RpcMapperTest.java b/src/test/java/org/eclipse/uprotocol/communication/RpcMapperTest.java index 69556600..a00e77e0 100644 --- a/src/test/java/org/eclipse/uprotocol/communication/RpcMapperTest.java +++ b/src/test/java/org/eclipse/uprotocol/communication/RpcMapperTest.java @@ -18,7 +18,7 @@ public class RpcMapperTest { - + @Test @DisplayName("Test RpcMapper mapResponse using RpcClient interface invokeMethod API") public void testMapResponse() { diff --git a/src/test/java/org/eclipse/uprotocol/communication/SimpleNotifierTest.java b/src/test/java/org/eclipse/uprotocol/communication/SimpleNotifierTest.java index d6d3dd96..640db6d4 100644 --- a/src/test/java/org/eclipse/uprotocol/communication/SimpleNotifierTest.java +++ b/src/test/java/org/eclipse/uprotocol/communication/SimpleNotifierTest.java @@ -12,107 +12,119 @@ */ package org.eclipse.uprotocol.communication; +import static org.junit.Assert.assertThrows; 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 java.util.concurrent.CompletionStage; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import org.eclipse.uprotocol.transport.UListener; import org.eclipse.uprotocol.v1.UCode; import org.eclipse.uprotocol.v1.UMessage; -import org.eclipse.uprotocol.v1.UStatus; import org.eclipse.uprotocol.v1.UUri; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -public class SimpleNotifierTest { +class SimpleNotifierTest extends CommunicationLayerClientTestBase { + + private Notifier notifier; + private CallOptions options; + + @BeforeEach + void createNotifier() { + notifier = new SimpleNotifier(transport, uriProvider); + options = CallOptions.DEFAULT; + } + + private void assertNotificationAttributes(UMessage message) { + assertEquals(TOPIC_URI, message.getAttributes().getSource()); + assertEquals(DESTINATION_URI, message.getAttributes().getSink()); + assertEquals(options.priority(), message.getAttributes().getPriority()); + assertEquals(options.timeout(), message.getAttributes().getTtl()); + assertFalse(message.getAttributes().hasToken()); + } + @Test @DisplayName("Test sending a simple notification") - public void testSendNotification() { - Notifier notifier = new SimpleNotifier(new TestUTransport()); - CompletionStage result = notifier.notify(createTopic(), createDestinationUri()); - assertEquals(result.toCompletableFuture().join().getCode(), UCode.OK); + void testSendNotification() { + notifier.notify(TOPIC_URI.getResourceId(), DESTINATION_URI).toCompletableFuture().join(); + verify(transport).send(requestMessage.capture()); + assertNotificationAttributes(requestMessage.getValue()); } @Test @DisplayName("Test sending a simple notification passing CallOptions") - public void testSendNotificationWithOptions() { - Notifier notifier = new SimpleNotifier(new TestUTransport()); - CompletionStage result = notifier.notify(createTopic(), createDestinationUri(), CallOptions.DEFAULT); - assertEquals(result.toCompletableFuture().join().getCode(), UCode.OK); + void testSendNotificationWithOptions() { + notifier.notify(TOPIC_URI.getResourceId(), DESTINATION_URI, options).toCompletableFuture().join(); + verify(transport).send(requestMessage.capture()); + assertNotificationAttributes(requestMessage.getValue()); } - @Test @DisplayName("Test sending a simple notification passing a google.protobuf.Message payload") - public void testSendNotificationWithPayload() { - UUri uri = UUri.newBuilder().setAuthorityName("Hartley").build(); - Notifier notifier = new SimpleNotifier(new TestUTransport()); - CompletionStage result = notifier.notify(createTopic(), createDestinationUri(), - UPayload.pack(uri)); - assertEquals(result.toCompletableFuture().join().getCode(), UCode.OK); + void testSendNotificationWithPayload() { + final var payload = UPayload.pack(UUri.newBuilder().setAuthorityName("Hartley").build()); + notifier.notify(TOPIC_URI.getResourceId(), DESTINATION_URI, payload).toCompletableFuture().join(); + verify(transport).send(requestMessage.capture()); + assertNotificationAttributes(requestMessage.getValue()); + assertEquals(payload.data(), requestMessage.getValue().getPayload()); } @Test @DisplayName("Test sending a simple notification passing a google.protobuf.Any payload and CallOptions") - public void testSendNotificationWithAnyPayloadAndOptions() { - UUri uri = UUri.newBuilder().setAuthorityName("Hartley").build(); - Notifier notifier = new SimpleNotifier(new TestUTransport()); - CompletionStage result = notifier.notify(createTopic(), createDestinationUri(), - CallOptions.DEFAULT, UPayload.packToAny(uri)); - assertEquals(result.toCompletableFuture().join().getCode(), UCode.OK); + void testSendNotificationWithAnyPayloadAndOptions() { + final var payload = UPayload.pack(UUri.newBuilder().setAuthorityName("Hartley").build()); + notifier.notify(TOPIC_URI.getResourceId(), DESTINATION_URI, options, payload).toCompletableFuture().join(); + verify(transport).send(requestMessage.capture()); + assertNotificationAttributes(requestMessage.getValue()); + assertEquals(payload.data(), requestMessage.getValue().getPayload()); } + @Test + void testSendNotificationWithInvalidTopic() { + var exception = assertThrows( + CompletionException.class, + () -> + notifier.notify(0x5000, TOPIC_URI).toCompletableFuture().join() + ); + assertEquals(UCode.INVALID_ARGUMENT, ((UStatusException) exception.getCause()).getCode()); + } @Test @DisplayName("Test registering and unregistering a listener for a notification topic") - public void testRegisterListener() { - UListener listener = new UListener() { - @Override - public void onReceive(UMessage message) { - assertNotNull(message); - } - }; - - Notifier notifier = new SimpleNotifier(new TestUTransport()); - CompletionStage result = notifier.registerNotificationListener(createTopic(), listener); - assertEquals(result.toCompletableFuture().join().getCode(), UCode.OK); - - result = notifier.unregisterNotificationListener(createTopic(), listener); - assertEquals(result.toCompletableFuture().join().getCode(), UCode.OK); + void testRegisterListener() { + final var listener = mock(UListener.class); + notifier.registerNotificationListener(TOPIC_URI, listener).toCompletableFuture().join(); + verify(transport).registerListener( + TOPIC_URI, + TRANSPORT_SOURCE, + listener); + notifier.unregisterNotificationListener(TOPIC_URI, listener).toCompletableFuture().join(); + verify(transport).unregisterListener( + TOPIC_URI, + TRANSPORT_SOURCE, + listener); } - @Test @DisplayName("Test unregistering a listener that was not registered") - public void testUnregisterListenerNotRegistered() { - UListener listener = new UListener() { - @Override - public void onReceive(UMessage message) { - assertNotNull(message); - } - }; - Notifier notifier = new SimpleNotifier(new TestUTransport()); - CompletionStage result = notifier.unregisterNotificationListener(createTopic(), listener); - assertFalse(result.toCompletableFuture().isCompletedExceptionally()); - assertEquals(result.toCompletableFuture().join().getCode(), UCode.NOT_FOUND); - } - - - private UUri createTopic() { - return UUri.newBuilder() - .setAuthorityName("hartley") - .setUeId(3) - .setUeVersionMajor(1) - .setResourceId(0x8000) - .build(); - } - - - private UUri createDestinationUri() { - return UUri.newBuilder() - .setUeId(4) - .setUeVersionMajor(1) - .build(); + void testUnregisterListenerNotRegistered() { + final var listener = mock(UListener.class); + when(transport.unregisterListener(TOPIC_URI, TRANSPORT_SOURCE, listener)) + .thenReturn(CompletableFuture.failedFuture( + new UStatusException(UCode.NOT_FOUND, "no such listener"))); + final var exception = assertThrows(CompletionException.class, () -> { + notifier.unregisterNotificationListener(TOPIC_URI, listener).toCompletableFuture().join(); + }); + verify(transport).unregisterListener( + TOPIC_URI, + TRANSPORT_SOURCE, + listener); + assertEquals(UCode.NOT_FOUND, ((UStatusException) exception.getCause()).getCode()); } } diff --git a/src/test/java/org/eclipse/uprotocol/communication/SimplePublisherTest.java b/src/test/java/org/eclipse/uprotocol/communication/SimplePublisherTest.java index 5aa03b0b..fddb0bf6 100644 --- a/src/test/java/org/eclipse/uprotocol/communication/SimplePublisherTest.java +++ b/src/test/java/org/eclipse/uprotocol/communication/SimplePublisherTest.java @@ -13,58 +13,82 @@ */ package org.eclipse.uprotocol.communication; +import static org.junit.Assert.assertThrows; import static org.junit.jupiter.api.Assertions.assertEquals; -import java.util.concurrent.CompletionStage; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.mockito.Mockito.verify; + +import java.util.concurrent.CompletionException; import org.eclipse.uprotocol.v1.UCode; -import org.eclipse.uprotocol.v1.UStatus; +import org.eclipse.uprotocol.v1.UMessage; import org.eclipse.uprotocol.v1.UUri; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -public class SimplePublisherTest { +class SimplePublisherTest extends CommunicationLayerClientTestBase { + private Publisher publisher; + private CallOptions options; + + @BeforeEach + void createPublisher() { + publisher = new SimplePublisher(transport, uriProvider); + options = CallOptions.DEFAULT; + } + + private void assertEventAttributes(UMessage message) { + assertEquals(TOPIC_URI, message.getAttributes().getSource()); + assertFalse(message.getAttributes().hasSink()); + assertEquals(options.priority(), message.getAttributes().getPriority()); + assertEquals(options.timeout(), message.getAttributes().getTtl()); + assertFalse(message.getAttributes().hasToken()); + } + @Test @DisplayName("Test sending a simple publish message without a payload") - public void testSendPublish() { - Publisher publisher = new SimplePublisher(new TestUTransport()); - CompletionStage result = publisher.publish(createTopic()); - assertEquals(result.toCompletableFuture().join().getCode(), UCode.OK); + void testSendPublish() { + publisher.publish(TOPIC_URI.getResourceId()).toCompletableFuture().join(); + verify(transport).send(requestMessage.capture()); + assertEventAttributes(requestMessage.getValue()); } @Test @DisplayName("Test sending a simple publish message with CallOptions and no payload") - public void testSendPublishWithOptions() { - Publisher publisher = new SimplePublisher(new TestUTransport()); - CompletionStage result = publisher.publish(createTopic(), CallOptions.DEFAULT); - assertEquals(result.toCompletableFuture().join().getCode(), UCode.OK); + void testSendPublishWithOptions() { + publisher.publish(TOPIC_URI.getResourceId(), options).toCompletableFuture().join(); + verify(transport).send(requestMessage.capture()); + assertEventAttributes(requestMessage.getValue()); } @Test @DisplayName("Test sending a simple publish message with a stuffed UPayload that was build with packToAny()") - public void testSendPublishWithStuffedPayload() { - UUri uri = UUri.newBuilder().setAuthorityName("Hartley").build(); - Publisher publisher = new SimplePublisher(new TestUTransport()); - CompletionStage result = publisher.publish(createTopic(), UPayload.packToAny(uri)); - assertEquals(result.toCompletableFuture().join().getCode(), UCode.OK); + void testSendPublishWithStuffedPayload() { + final var payload = UPayload.pack(UUri.newBuilder().setAuthorityName("Hartley").build()); + publisher.publish(TOPIC_URI.getResourceId(), payload).toCompletableFuture().join(); + verify(transport).send(requestMessage.capture()); + assertEventAttributes(requestMessage.getValue()); + assertEquals(payload.data(), requestMessage.getValue().getPayload()); } @Test @DisplayName("Test sending a simple publish message with CallOptions and a stuffed UPayload") - public void testSendPublishWithPayloadAndOptions() { - UUri uri = UUri.newBuilder().setAuthorityName("Hartley").build(); - Publisher publisher = new SimplePublisher(new TestUTransport()); - CompletionStage result = publisher.publish(createTopic(), CallOptions.DEFAULT, UPayload.pack(uri)); - assertEquals(result.toCompletableFuture().join().getCode(), UCode.OK); + void testSendPublishWithPayloadAndOptions() { + final var payload = UPayload.pack(UUri.newBuilder().setAuthorityName("Hartley").build()); + publisher.publish(TOPIC_URI.getResourceId(), options, payload).toCompletableFuture().join(); + verify(transport).send(requestMessage.capture()); + assertEventAttributes(requestMessage.getValue()); + assertEquals(payload.data(), requestMessage.getValue().getPayload()); } - - private UUri createTopic() { - return UUri.newBuilder() - .setAuthorityName("hartley") - .setUeId(3) - .setUeVersionMajor(1) - .setResourceId(0x8000) - .build(); + + @Test + void testSendingPublishWithInvalidTopic() { + var exception = assertThrows( + CompletionException.class, + () -> + publisher.publish(0x5000).toCompletableFuture().join() + ); + assertEquals(UCode.INVALID_ARGUMENT, ((UStatusException) exception.getCause()).getCode()); } } - diff --git a/src/test/java/org/eclipse/uprotocol/communication/TestUTransport.java b/src/test/java/org/eclipse/uprotocol/communication/TestUTransport.java deleted file mode 100644 index 410c8c90..00000000 --- a/src/test/java/org/eclipse/uprotocol/communication/TestUTransport.java +++ /dev/null @@ -1,230 +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.communication; - -import java.util.Iterator; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.Executors; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.CompletableFuture; - -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.core.usubscription.v3.UnsubscribeResponse; -import org.eclipse.uprotocol.transport.UListener; -import org.eclipse.uprotocol.transport.UTransport; -import org.eclipse.uprotocol.transport.builder.UMessageBuilder; -import org.eclipse.uprotocol.transport.validate.UAttributesValidator; -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.eclipse.uprotocol.validation.ValidationException; - -import com.google.protobuf.InvalidProtocolBufferException; - -/** - * TestUTransport is a test implementation of the UTransport interface - * that can only hold a single listener for testing. - */ -public class TestUTransport implements UTransport { - - private List listeners = new CopyOnWriteArrayList<>(); - private final UUri mSource; - - protected List getListeners() { - return listeners; - } - - public UMessage buildResponse(UMessage request) { - // If the request is a subscribe or unsubscribe request, return the appropriate response - if (request.getAttributes().getSink().getUeId() == 0) { - if (request.getAttributes().getSink().getResourceId() == 1) { - try { - SubscriptionRequest subscriptionRequest = SubscriptionRequest.parseFrom(request.getPayload()); - SubscriptionResponse subResponse = SubscriptionResponse.newBuilder() - .setTopic(subscriptionRequest.getTopic()) - .setStatus(SubscriptionStatus.newBuilder() - .setState(SubscriptionStatus.State.SUBSCRIBED).build()) - .build(); - return UMessageBuilder.response(request.getAttributes()).build(UPayload.pack(subResponse)); - } catch (InvalidProtocolBufferException e) { - return UMessageBuilder.response(request.getAttributes()).build( - UPayload.pack(UnsubscribeResponse.newBuilder().build())); - } - } else { - return UMessageBuilder.response(request.getAttributes()).build( - UPayload.pack(UnsubscribeResponse.newBuilder().build())); - } - } - return UMessageBuilder.response(request.getAttributes()) - .build(UPayload.pack(request.getPayload(), request.getAttributes().getPayloadFormat())); - } - - public TestUTransport() { - this(UUri.newBuilder() - .setAuthorityName("Hartley").setUeId(4).setUeVersionMajor(1).build()); - } - - public TestUTransport(UUri source) { - mSource = source; - } - - @Override - public CompletionStage send(UMessage message) { - if (message == null) { - return CompletableFuture.completedFuture(UStatus.newBuilder() - .setCode(UCode.INVALID_ARGUMENT) - .setMessage("Message cannot be null") - .build()); - } - UAttributesValidator validator = UAttributesValidator.getValidator(message.getAttributes()); - try { - validator.validate(message.getAttributes()); - } catch (ValidationException e) { - return CompletableFuture.completedFuture(UStatus.newBuilder() - .setCode(UCode.INVALID_ARGUMENT) - .setMessage("Invalid message attributes") - .build()); - } - - if (message.getAttributes().getType() == UMessageType.UMESSAGE_TYPE_REQUEST) { - UMessage response = buildResponse(message); - Executors.newSingleThreadExecutor().execute(new Runnable() { - @Override - public void run() { - for (Iterator it = listeners.iterator(); it.hasNext();) { - UListener listener = it.next(); - listener.onReceive(response); - } - } - }); - } - - return CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()); - } - - /* - * Register a listener based on the source and sink URIs. - */ - @Override - public CompletionStage registerListener(UUri source, UUri sink, UListener listener) { - listeners.add(listener); - return CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()); - } - - @Override - public CompletionStage unregisterListener(UUri source, UUri sink, UListener listener) { - final UStatus result = UStatus.newBuilder().setCode(listeners.contains(listener) ? - UCode.OK : UCode.NOT_FOUND).build(); - if (listeners.contains(listener)) { - listeners.remove(listener); - } - return CompletableFuture.completedFuture(result); - } - - @Override - public UUri getSource() { - return mSource; - } - - @Override - public void close() { - listeners.clear(); - } -} - - -/** - * Timeout uTransport simply does not send a reply - */ -class TimeoutUTransport extends TestUTransport { - @Override - public CompletionStage send(UMessage message) { - return CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()); - } -}; - -class ErrorUTransport extends TestUTransport { - @Override - public CompletionStage send(UMessage message) { - return CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.FAILED_PRECONDITION).build()); - } - - @Override - public CompletionStage registerListener(UUri source, UUri sink, UListener listener) { - return CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.FAILED_PRECONDITION).build()); - } - - @Override - public CompletionStage unregisterListener(UUri source, UUri sink, UListener listener) { - return CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.FAILED_PRECONDITION).build()); - } -}; - -/** - * Test UTransport that will set the commstatus for an error - */ -class CommStatusTransport extends TestUTransport { - @Override - public UMessage buildResponse(UMessage request) { - UStatus status = UStatus.newBuilder() - .setCode(UCode.FAILED_PRECONDITION) - .setMessage("CommStatus Error") - .build(); - return UMessageBuilder.response(request.getAttributes()) - .withCommStatus(status.getCode()) - .build(UPayload.pack(status)); - } -}; - -/** - * Test UTransport that will set the commstatus for a success response - */ -class CommStatusOkTransport extends TestUTransport { - @Override - public UMessage buildResponse(UMessage request) { - UStatus status = UStatus.newBuilder() - .setCode(UCode.OK) - .setMessage("No Communication Error") - .build(); - return UMessageBuilder.response(request.getAttributes()) - .withCommStatus(status.getCode()) - .build(UPayload.pack(status)); - } -} - -class EchoUTransport extends TestUTransport { - @Override - public UMessage buildResponse(UMessage request) { - return request; - } - - @Override - public CompletionStage send(UMessage message) { - UMessage response = buildResponse(message); - Executors.newSingleThreadExecutor().execute(new Runnable() { - @Override - public void run() { - for (Iterator it = getListeners().iterator(); it.hasNext();) { - UListener listener = it.next(); - listener.onReceive(response); - } - } - }); - return CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()); - } -}; diff --git a/src/test/java/org/eclipse/uprotocol/communication/UClientTest.java b/src/test/java/org/eclipse/uprotocol/communication/UClientTest.java index cd31def3..28c59418 100644 --- a/src/test/java/org/eclipse/uprotocol/communication/UClientTest.java +++ b/src/test/java/org/eclipse/uprotocol/communication/UClientTest.java @@ -12,81 +12,131 @@ */ package org.eclipse.uprotocol.communication; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +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.concurrent.CompletableFuture; + +import org.eclipse.uprotocol.transport.StaticUriProvider; import org.eclipse.uprotocol.transport.UListener; -import org.eclipse.uprotocol.v1.UMessage; +import org.eclipse.uprotocol.transport.UTransport; +import org.eclipse.uprotocol.uri.factory.UriFactory; import org.eclipse.uprotocol.v1.UUri; -import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -public class UClientTest { - - // Main functionality is tested in the various individual implementations - @Test - @DisplayName("Test happy path for all APIs") - public void test() { - UClient client = UClient.create(new TestUTransport()); - UListener listener = new UListener() { - @Override - public void onReceive(UMessage message) { - assertNotNull(message); - } - }; - - assertDoesNotThrow(() -> - client.notify(createTopic(), createDestinationUri()).toCompletableFuture().get()); - - assertDoesNotThrow(() -> - client.publish(createTopic()).toCompletableFuture().get()); - - assertDoesNotThrow(() -> - client.invokeMethod(createMethodUri(), null, null).toCompletableFuture().get()); - - assertDoesNotThrow(() -> - client.registerNotificationListener(createTopic(), listener).toCompletableFuture().get()); - - assertDoesNotThrow(() -> - client.unregisterNotificationListener(createTopic(), listener).toCompletableFuture().get()); - - RequestHandler handler = mock(RequestHandler.class); - - assertDoesNotThrow(() -> - client.registerRequestHandler(createMethodUri(), handler).toCompletableFuture().get()); - - assertDoesNotThrow(() -> - client.unregisterRequestHandler(createMethodUri(), handler).toCompletableFuture().get()); +class UClientTest { + private static final UUri TRANSPORT_SOURCE = UUri.newBuilder() + .setAuthorityName("my-vehicle") + .setUeId(0xa1) + .setUeVersionMajor(0x01) + .setResourceId(0x0000) + .build(); + private static final UUri TOPIC_URI = UUri.newBuilder(TRANSPORT_SOURCE) + .setResourceId(0x8000) + .build(); + private static final UUri DESTINATION_URI = UUri.newBuilder() + .setAuthorityName("other-vehicle") + .setUeId(0x2bbbb) + .setUeVersionMajor(0x02) + .build(); + private static final UUri METHOD_URI = UUri.newBuilder(DESTINATION_URI) + .setResourceId(3) + .build(); - client.close(); + private RpcClient rpcClient; + private RpcServer rpcServer; + private Publisher publisher; + private Notifier notifier; + + @BeforeEach + void setUp() { + rpcClient = mock(RpcClient.class); + rpcServer = mock(RpcServer.class); + publisher = mock(Publisher.class); + notifier = mock(Notifier.class); } - - - private UUri createTopic() { - return UUri.newBuilder() - .setAuthorityName("Hartley") - .setUeId(4) - .setUeVersionMajor(1) - .setResourceId(0x8000) - .build(); + @Test + void testFactoryMethod() { + var transport = mock(UTransport.class); + when(transport.registerListener(any(UUri.class), any(UUri.class), any(UListener.class))) + .thenReturn(CompletableFuture.completedFuture(null)); + var uriProvider = StaticUriProvider.of(TRANSPORT_SOURCE); + UClient.create(transport, uriProvider); + verify(transport).registerListener(any(UUri.class), eq(TRANSPORT_SOURCE), any(UListener.class)); } + @Test + void testPublisher() { + when(publisher.publish(anyInt(), any(CallOptions.class), any(UPayload.class))) + .thenReturn(CompletableFuture.completedFuture(null)); + var client = new UClient(rpcClient, rpcServer, publisher, notifier); + client.publish(TOPIC_URI.getResourceId()).toCompletableFuture().join(); + verify(publisher).publish(eq(TOPIC_URI.getResourceId()), any(CallOptions.class), any(UPayload.class)); + } - private UUri createDestinationUri() { - return UUri.newBuilder() - .setUeId(4) - .setUeVersionMajor(1) - .build(); + @Test + void testNotifier() { + when(notifier.notify(anyInt(), any(UUri.class), any(CallOptions.class), any(UPayload.class))) + .thenReturn(CompletableFuture.completedFuture(null)); + var client = new UClient(rpcClient, rpcServer, publisher, notifier); + client.notify(TOPIC_URI.getResourceId(), DESTINATION_URI).toCompletableFuture().join(); + verify(notifier).notify( + eq(TOPIC_URI.getResourceId()), + eq(DESTINATION_URI), + any(CallOptions.class), + any(UPayload.class)); + + when(notifier.registerNotificationListener(any(UUri.class), any(UListener.class))) + .thenReturn(CompletableFuture.completedFuture(null)); + when(notifier.unregisterNotificationListener(any(UUri.class), any(UListener.class))) + .thenReturn(CompletableFuture.completedFuture(null)); + final var listener = mock(UListener.class); + client = new UClient(rpcClient, rpcServer, publisher, notifier); + client.registerNotificationListener(TOPIC_URI, listener).toCompletableFuture().join(); + verify(notifier).registerNotificationListener(TOPIC_URI, listener); + client.unregisterNotificationListener(TOPIC_URI, listener).toCompletableFuture().join(); + verify(notifier).unregisterNotificationListener(TOPIC_URI, listener); } + @Test + void testRpcClient() { + when(rpcClient.invokeMethod(any(UUri.class), any(UPayload.class), any(CallOptions.class))) + .thenReturn(CompletableFuture.completedFuture(null)); + var client = new UClient(rpcClient, rpcServer, publisher, notifier); + client.invokeMethod(METHOD_URI, UPayload.EMPTY, CallOptions.DEFAULT).toCompletableFuture().join(); + verify(rpcClient).invokeMethod(eq(METHOD_URI), eq(UPayload.EMPTY), eq(CallOptions.DEFAULT)); + } - private UUri createMethodUri() { - return UUri.newBuilder() - .setAuthorityName("Hartley") - .setUeId(4) - .setUeVersionMajor(1) - .setResourceId(3).build(); + @Test + void testRpcServer() { + when(rpcServer.registerRequestHandler(any(UUri.class), anyInt(), any(RequestHandler.class))) + .thenReturn(CompletableFuture.completedFuture(null)); + when(rpcServer.unregisterRequestHandler(any(UUri.class), anyInt(), any(RequestHandler.class))) + .thenReturn(CompletableFuture.completedFuture(null)); + final var handler = mock(RequestHandler.class); + var client = new UClient(rpcClient, rpcServer, publisher, notifier); + client.registerRequestHandler( + UriFactory.ANY, + METHOD_URI.getResourceId(), + handler).toCompletableFuture().join(); + verify(rpcServer).registerRequestHandler( + eq(UriFactory.ANY), + eq(METHOD_URI.getResourceId()), + eq(handler)); + + client.unregisterRequestHandler( + UriFactory.ANY, + METHOD_URI.getResourceId(), + handler).toCompletableFuture().join(); + verify(rpcServer).unregisterRequestHandler( + eq(UriFactory.ANY), + eq(METHOD_URI.getResourceId()), + eq(handler)); } } diff --git a/src/test/java/org/eclipse/uprotocol/communication/UPayloadTest.java b/src/test/java/org/eclipse/uprotocol/communication/UPayloadTest.java index 4850a468..ef5d8303 100644 --- a/src/test/java/org/eclipse/uprotocol/communication/UPayloadTest.java +++ b/src/test/java/org/eclipse/uprotocol/communication/UPayloadTest.java @@ -13,9 +13,13 @@ package org.eclipse.uprotocol.communication; import java.util.Optional; +import java.util.stream.Stream; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertTrue; import org.eclipse.uprotocol.v1.UMessage; @@ -23,6 +27,14 @@ import org.eclipse.uprotocol.v1.UUri; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import com.google.protobuf.Any; +import com.google.protobuf.ByteString; +import com.google.protobuf.Message; +import com.google.protobuf.StringValue; public class UPayloadTest { @@ -106,6 +118,70 @@ public void testUnpackToUnpackAMessageOfTheWrongType() { assertEquals(unpacked, Optional.empty()); } + static Stream unpackOrDefaultInstanceArgsProvider() { + var stringValue = StringValue.of("hello"); + var protobuf = stringValue.toByteString(); + + // Claims to have a string of length 20 but only provides 4 bytes + var invalidStringValueProtobuf = ByteString.fromHex("0A1441424344"); + + return Stream.of( + // ByteString, UPayloadFormat, Class, Exception + Arguments.of(null, null, null, NullPointerException.class), + Arguments.of(null, UPayloadFormat.UPAYLOAD_FORMAT_PROTOBUF, StringValue.class, NullPointerException.class), + Arguments.of(protobuf, null, StringValue.class, NullPointerException.class), + Arguments.of(protobuf, UPayloadFormat.UPAYLOAD_FORMAT_PROTOBUF, null, NullPointerException.class), + Arguments.of(protobuf, UPayloadFormat.UPAYLOAD_FORMAT_JSON, StringValue.class, UStatusException.class), + Arguments.of(protobuf, UPayloadFormat.UPAYLOAD_FORMAT_RAW, StringValue.class, UStatusException.class), + Arguments.of(protobuf, UPayloadFormat.UPAYLOAD_FORMAT_SHM, StringValue.class, UStatusException.class), + Arguments.of(protobuf, UPayloadFormat.UPAYLOAD_FORMAT_SOMEIP, StringValue.class, UStatusException.class), + Arguments.of( + protobuf, + UPayloadFormat.UPAYLOAD_FORMAT_SOMEIP_TLV, + StringValue.class, + UStatusException.class), + Arguments.of(ByteString.EMPTY, UPayloadFormat.UPAYLOAD_FORMAT_PROTOBUF, StringValue.class, null), + Arguments.of(protobuf, UPayloadFormat.UPAYLOAD_FORMAT_PROTOBUF, StringValue.class, null), + // unpacking a protobuf that does not match the expected type fails + Arguments.of( + invalidStringValueProtobuf, + UPayloadFormat.UPAYLOAD_FORMAT_PROTOBUF, + StringValue.class, + UStatusException.class), + // unpacking a protobuf to a different type succeeds because of missing type information + // from the protobuf + Arguments.of(protobuf, UPayloadFormat.UPAYLOAD_FORMAT_PROTOBUF, UUri.class, null), + // unpacking a protobuf to a different type fails because of the type information + // contained in the Any wrapper + Arguments.of( + Any.pack(stringValue).toByteString(), + UPayloadFormat.UPAYLOAD_FORMAT_PROTOBUF_WRAPPED_IN_ANY, + UUri.class, + UStatusException.class), + Arguments.of( + Any.pack(stringValue).toByteString(), + UPayloadFormat.UPAYLOAD_FORMAT_PROTOBUF_WRAPPED_IN_ANY, + StringValue.class, + null) + ); + } + + @ParameterizedTest(name = "Test unpackOrDefaultInstance: {index} => {arguments}") + @MethodSource("unpackOrDefaultInstanceArgsProvider") + void testUnpackOrDefaultInstanceSucceedsForSimpleProtobuf( + ByteString protobuf, + UPayloadFormat format, + Class expectedClass, + Class expectedException) { + if (expectedException != null) { + assertThrows(expectedException, () -> { + UPayload.unpackOrDefaultInstance(protobuf, format, expectedClass); + }); + } else { + var unpacked = UPayload.unpackOrDefaultInstance(protobuf, format, expectedClass); + assertInstanceOf(expectedClass, unpacked); + } + } @Test @DisplayName("Test equals when they are equal") @@ -133,7 +209,7 @@ public void testEqualsWhenTheyAreNotEqual() { public void testEqualsWhenObjectIsNull() { UUri uri = UUri.newBuilder().setAuthorityName("Hartley").build(); UPayload payload = UPayload.packToAny(uri); - assertFalse(payload.equals(null)); + assertNotNull(payload); } diff --git a/src/test/java/org/eclipse/uprotocol/transport/UTransportTest.java b/src/test/java/org/eclipse/uprotocol/transport/UTransportTest.java index 63586c3e..32c1a705 100644 --- a/src/test/java/org/eclipse/uprotocol/transport/UTransportTest.java +++ b/src/test/java/org/eclipse/uprotocol/transport/UTransportTest.java @@ -12,193 +12,61 @@ */ package org.eclipse.uprotocol.transport; -import java.util.concurrent.CompletionStage; import java.util.concurrent.CompletableFuture; -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.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 org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -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.mockito.Mockito; +import org.eclipse.uprotocol.uri.factory.UriFactory; import org.eclipse.uprotocol.v1.UUri; /** * Tests implementing and using uTransport API. */ -public class UTransportTest { - @Test - @DisplayName("Test happy path send message") - public void testHappySendMessage() { - UTransport transport = new HappyUTransport(); - UUri uri = UUri.newBuilder().setUeId(1).setUeVersionMajor(1).setResourceId(0x8000).build(); - - CompletionStage result = transport.send(UMessageBuilder.publish(uri).build()); - assertFalse(result.toCompletableFuture().isCompletedExceptionally()); - assertEquals(result.toCompletableFuture().join().getCode(), UCode.OK); - } - - @Test - @DisplayName("Test happy path register listener") - public void testHappyRegisterListener() { - UTransport transport = new HappyUTransport(); - assertFalse(transport.registerListener( - UUri.getDefaultInstance(), - new MyListener()).toCompletableFuture().isCompletedExceptionally()); +class UTransportTest { + private static final UUri SOURCE_FILTER = UUri.newBuilder() + .setAuthorityName("my-vehicle") + .setUeId(0x1a54) + .setUeVersionMajor(0x02) + .setResourceId(0xFFFF) + .build(); + + private UTransport transport; + private UListener listener; + + @BeforeEach + void setUp() { + transport = mock(UTransport.class); + Mockito.lenient().when(transport.registerListener(any(UUri.class), any(UListener.class))) + .thenCallRealMethod(); + Mockito.lenient().when(transport.unregisterListener(any(UUri.class), any(UListener.class))) + .thenCallRealMethod(); + listener = mock(UListener.class); } @Test - @DisplayName("Test happy path unregister listener") - public void testHappyRegisterUnlistener() { - UTransport transport = new HappyUTransport(); - assertFalse(transport.unregisterListener( - UUri.getDefaultInstance(), - new MyListener()).toCompletableFuture().isCompletedExceptionally()); - } + @DisplayName("Test default implementation of registerListener") + void testRegisterListener() { + when(transport.registerListener(any(UUri.class), any(UUri.class), any(UListener.class))) + .thenReturn(CompletableFuture.completedFuture(null)); - @Test - @DisplayName("Test unhappy path send message") - public void testUnhappySendMessage() { - UTransport transport = new SadUTransport(); - assertEquals(transport.send(null).toCompletableFuture().join().getCode(), UCode.INTERNAL); + transport.registerListener(SOURCE_FILTER, listener).toCompletableFuture().join(); + verify(transport).registerListener(eq(SOURCE_FILTER), eq(UriFactory.ANY), eq(listener)); } @Test - @DisplayName("Test unhappy path register listener") - public void testUnhappyRegisterListener() { - UTransport transport = new SadUTransport(); - CompletionStage result = transport.registerListener(UUri.getDefaultInstance(), - new MyListener()); - - assertEquals(result.toCompletableFuture().join().getCode(), UCode.INTERNAL); - } - - @Test - @DisplayName("Test unhappy path unregister listener") - public void testUnhappyRegisterUnlistener() { - UTransport transport = new SadUTransport(); - - CompletionStage result = transport.unregisterListener(UUri.getDefaultInstance(), - new MyListener()); - - assertEquals(result.toCompletableFuture().join().getCode(), UCode.INTERNAL); - } - - @Test - @DisplayName("Test happy path calling open() API") - public void testHappyOpen() { - UTransport transport = new HappyUTransport(); - assertEquals(transport.open().toCompletableFuture().join().getCode(), UCode.OK); - } - - @Test - @DisplayName("Test default oepn() and close() APIs") - public void testDefaultOpenClose() { - UTransport transport = new UTransport() { - @Override - public CompletionStage send(UMessage message) { - return CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()); - } - - @Override - public CompletionStage registerListener(UUri source, UUri sink, UListener listener) { - return CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()); - } - - @Override - public CompletionStage unregisterListener(UUri source, UUri sink, UListener listener) { - return CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()); - } - - @Override - public UUri getSource() { - return UUri.getDefaultInstance(); - } - }; - - assertDoesNotThrow(() -> transport.close()); - } - - - class MyListener implements UListener { - @Override - public void onReceive(UMessage message) { - // empty by intention - } - } - - private class HappyUTransport implements UTransport { - @Override - public CompletionStage send(UMessage message) { - return CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()); - } - - - @Override - public CompletionStage registerListener(UUri source, UUri sink, UListener listener) { - listener.onReceive(null); - return CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()); - } - - @Override - public CompletionStage unregisterListener(UUri source, UUri sink, UListener listener) { - return CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.OK).build()); - } - - - @Override - public UUri getSource() { - return UUri.getDefaultInstance(); - } - - @Override - public void close() { - } - } - - private class SadUTransport implements UTransport { - @Override - public CompletionStage send(UMessage message) { - return CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.INTERNAL).build()); - } - - @Override - public CompletionStage registerListener(UUri source, UUri sink, UListener listener) { - listener.onReceive(null); - return CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.INTERNAL).build()); - } - - @Override - public CompletionStage unregisterListener(UUri source, UUri sink, UListener listener) { - return CompletableFuture.completedFuture(UStatus.newBuilder().setCode(UCode.INTERNAL).build()); - } - - @Override - public UUri getSource() { - return UUri.getDefaultInstance(); - } - - @Override - public void close() { - } - } - - @Test - @DisplayName("Test happy path registerlistener with source filter only") - public void testHappyRegisterListenerSourceFilter() { - UTransport transport = new HappyUTransport(); - CompletionStage result = transport.registerListener(UUri.getDefaultInstance(), new MyListener()); - assertEquals(result.toCompletableFuture().join().getCode(), UCode.OK); - } - - @Test - @DisplayName("Test happy path unregisterlistener with source filter only") - public void testHappyUnregisterListenerSourceFilter() { - UTransport transport = new HappyUTransport(); - CompletionStage result = transport.unregisterListener(UUri.getDefaultInstance(), new MyListener()); - assertEquals(result.toCompletableFuture().join().getCode(), UCode.OK); + @DisplayName("Test happy path unregister listener") + void testUnregisterListener() { + when(transport.unregisterListener(any(UUri.class), any(UUri.class), any(UListener.class))) + .thenReturn(CompletableFuture.completedFuture(null)); + transport.unregisterListener(SOURCE_FILTER, listener).toCompletableFuture().join(); + verify(transport).unregisterListener(eq(SOURCE_FILTER), eq(UriFactory.ANY), eq(listener)); } } diff --git a/src/test/resources/junit-platform.properties b/src/test/resources/junit-platform.properties new file mode 100644 index 00000000..a6a8ce4a --- /dev/null +++ b/src/test/resources/junit-platform.properties @@ -0,0 +1,6 @@ +junit.platform.output.capture.stdout=true +junit.platform.output.capture.stderr=true + +# System properties for logging +systemProperty.logback.configurationFile=src/test/resources/logback-test.xml +systemProperty.java.util.logging.manager=org.slf4j.bridge.SLF4JBridgeHandler diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml new file mode 100644 index 00000000..f26aace1 --- /dev/null +++ b/src/test/resources/logback-test.xml @@ -0,0 +1,12 @@ + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + +