From 8ef867ee77dcf2de2bfb9aa4fba1ed67e5d7357d Mon Sep 17 00:00:00 2001 From: ayash Date: Wed, 26 Mar 2025 15:35:56 +0530 Subject: [PATCH 01/38] [Automated] Update the native jar versions --- ballerina/Ballerina.toml | 8 ++++---- ballerina/CompilerPlugin.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ballerina/Ballerina.toml b/ballerina/Ballerina.toml index ba4c58425..3a9267535 100644 --- a/ballerina/Ballerina.toml +++ b/ballerina/Ballerina.toml @@ -1,7 +1,7 @@ [package] org = "ballerina" name = "websocket" -version = "2.14.0" +version = "2.14.1" authors = ["Ballerina"] keywords = ["ws", "network", "bi-directional", "streaming", "service", "client"] repository = "https://github.com/ballerina-platform/module-ballerina-websocket" @@ -15,8 +15,8 @@ graalvmCompatible = true [[platform.java21.dependency]] groupId = "io.ballerina.stdlib" artifactId = "websocket-native" -version = "2.14.0" -path = "../native/build/libs/websocket-native-2.14.0.jar" +version = "2.14.1" +path = "../native/build/libs/websocket-native-2.14.1-SNAPSHOT.jar" [[platform.java21.dependency]] groupId = "io.ballerina.stdlib" @@ -85,5 +85,5 @@ version = "4.1.118.Final" path = "./lib/netty-handler-proxy-4.1.118.Final.jar" [[platform.java21.dependency]] -path = "../test-utils/build/libs/websocket-test-utils-2.14.0.jar" +path = "../test-utils/build/libs/websocket-test-utils-2.14.1-SNAPSHOT.jar" scope = "testOnly" diff --git a/ballerina/CompilerPlugin.toml b/ballerina/CompilerPlugin.toml index 03e503781..f0c2e2e19 100644 --- a/ballerina/CompilerPlugin.toml +++ b/ballerina/CompilerPlugin.toml @@ -3,4 +3,4 @@ id = "websocket-compiler-plugin" class = "io.ballerina.stdlib.websocket.plugin.WebSocketCompilerPlugin" [[dependency]] -path = "../compiler-plugin/build/libs/websocket-compiler-plugin-2.14.0.jar" +path = "../compiler-plugin/build/libs/websocket-compiler-plugin-2.14.1-SNAPSHOT.jar" From 14a35338847213de6ff5e4e6b4e0db52779bbc9b Mon Sep 17 00:00:00 2001 From: ayash Date: Wed, 26 Mar 2025 16:22:09 +0530 Subject: [PATCH 02/38] [Automated] Update the native jar versions --- ballerina/Dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index 7353ca03a..bf4b4e276 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -342,7 +342,7 @@ dependencies = [ [[package]] org = "ballerina" name = "websocket" -version = "2.14.0" +version = "2.14.1" dependencies = [ {org = "ballerina", name = "auth"}, {org = "ballerina", name = "constraint"}, From 1068e583cd908d9e37bec8685a1a1598418396ff Mon Sep 17 00:00:00 2001 From: ayash Date: Wed, 26 Mar 2025 16:38:09 +0530 Subject: [PATCH 03/38] Add DispatcherConfig annotation for WebSocket remote functions --- ballerina/remote_method_annotation.bal | 29 +++++++++++ ballerina/tests/remote_method_annotation.bal | 52 +++++++++++++++++++ .../stdlib/websocket/WebSocketConstants.java | 3 ++ .../WebSocketResourceDispatcher.java | 37 +++++++++---- 4 files changed, 112 insertions(+), 9 deletions(-) create mode 100644 ballerina/remote_method_annotation.bal create mode 100644 ballerina/tests/remote_method_annotation.bal diff --git a/ballerina/remote_method_annotation.bal b/ballerina/remote_method_annotation.bal new file mode 100644 index 000000000..a2e94fb62 --- /dev/null +++ b/ballerina/remote_method_annotation.bal @@ -0,0 +1,29 @@ +// Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +////////////////////////////////////// +/// Dispatcher Config Annotations //// +////////////////////////////////////// + +# Configurations for WebSocket remote functions. +# +# + value - The value which is going to be used for dispatching to custom remote functions. +public type WSDispatcherConfig record {| + string value; +|}; + +# The annotation which is used to configure WebSocket remote functions. +public annotation WSDispatcherConfig DispatcherConfig on function; diff --git a/ballerina/tests/remote_method_annotation.bal b/ballerina/tests/remote_method_annotation.bal new file mode 100644 index 000000000..df08253f2 --- /dev/null +++ b/ballerina/tests/remote_method_annotation.bal @@ -0,0 +1,52 @@ +// Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/test; + +type Subscribe record {| + string event = "subscribe"; + string data; +|}; + +@ServiceConfig { + dispatcherKey: "event" +} +service / on new Listener(22101) { + resource function get .() returns Service|UpgradeError { + return new WsService22101(); + } +} + +service class WsService22101 { + *Service; + + @DispatcherConfig { + value: "subscribe" + } + remote function onSubscribeMessage(Subscribe message) returns string { + return "Subscribed"; + } +} + +@test:Config { + groups: ["dispatcherConfigAnnotation"] +} +public function testDispatcherConfigAnnotation() returns Error? { + Client wsClient = check new ("ws://localhost:22101/"); + check wsClient->writeMessage({event: "subscribe", data: "test"}); + string res = check wsClient->readMessage(); + test:assertEquals(res, "Subscribed"); +} diff --git a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketConstants.java b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketConstants.java index e3ad7aa16..682f7f7e3 100644 --- a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketConstants.java +++ b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketConstants.java @@ -51,6 +51,9 @@ public class WebSocketConstants { public static final BString ANNOTATION_ATTR_VALIDATION_ENABLED = StringUtils.fromString("validation"); public static final BString ANNOTATION_ATTR_DISPATCHER_KEY = StringUtils.fromString("dispatcherKey"); + public static final String WEBSOCKET_DISPATCHER_CONFIG_ANNOTATION = "DispatcherConfig"; + public static final BString ANNOTATION_ATTR_DISPATCHER_VALUE = StringUtils.fromString("value"); + public static final BString RETRY_CONFIG = StringUtils.fromString("retryConfig"); public static final String LOG_MESSAGE = "{} {}"; public static final int STATUS_CODE_ABNORMAL_CLOSURE = 1006; diff --git a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketResourceDispatcher.java b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketResourceDispatcher.java index c9a9ee6fb..f96049df1 100644 --- a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketResourceDispatcher.java +++ b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketResourceDispatcher.java @@ -97,6 +97,7 @@ import static io.ballerina.stdlib.websocket.WebSocketConstants.CONSTRAINT_VALIDATION; import static io.ballerina.stdlib.websocket.WebSocketConstants.HEADER_ANNOTATION; import static io.ballerina.stdlib.websocket.WebSocketConstants.PARAM_ANNOT_PREFIX; +import static io.ballerina.stdlib.websocket.WebSocketConstants.UNCHECKED; import static io.ballerina.stdlib.websocket.WebSocketUtil.getBString; import static io.ballerina.stdlib.websocket.WebSocketUtil.hasByteArrayType; import static io.ballerina.stdlib.websocket.observability.WebSocketObservabilityConstants.ERROR_TYPE_MESSAGE_RECEIVED; @@ -406,19 +407,25 @@ public static void dispatchOnText(WebSocketConnectionInfo connectionInfo, WebSoc return; } String dispatchingKey = ((WebSocketServerService) wsService).getDispatchingKey(); - Optional customRemoteMethodName = getCustomRemoteMethodName(dispatchingKey, stringAggregator); + Optional dispatchingValue = getDispatchingValue(dispatchingKey, stringAggregator); + Optional customRemoteMethodName = dispatchingValue + .map(WebSocketResourceDispatcher::createCustomRemoteFunction); MethodType onTextMessageResource = null; BObject wsEndpoint = connectionInfo.getWebSocketEndpoint(); Object dispatchingService = wsService.getWsService(connectionInfo.getWebSocketConnection().getChannelId()); MethodType[] remoteFunctions = ((ServiceType) (((BValue) dispatchingService).getType())).getMethods(); for (MethodType remoteFunc : remoteFunctions) { - String funcName = remoteFunc.getName(); - if (customRemoteMethodName.isPresent() && funcName.equals(customRemoteMethodName.get())) { + String funcDispatcherValue = getFunctionDispatchingValue(remoteFunc); + if (dispatchingValue.isPresent() && funcDispatcherValue.equals(dispatchingValue.get())) { onTextMessageResource = remoteFunc; break; } - if (funcName.equals(WebSocketConstants.RESOURCE_NAME_ON_TEXT_MESSAGE) || - funcName.equals(WebSocketConstants.RESOURCE_NAME_ON_MESSAGE)) { + if (customRemoteMethodName.isPresent() && funcDispatcherValue.equals(customRemoteMethodName.get())) { + onTextMessageResource = remoteFunc; + break; + } + if (funcDispatcherValue.equals(WebSocketConstants.RESOURCE_NAME_ON_TEXT_MESSAGE) || + funcDispatcherValue.equals(WebSocketConstants.RESOURCE_NAME_ON_MESSAGE)) { onTextMessageResource = remoteFunc; } } @@ -557,16 +564,16 @@ private static void handleDataBindingError(WebSocketConnectionInfo connectionInf } } - private static Optional getCustomRemoteMethodName(String dispatchingKey, + private static Optional getDispatchingValue(String dispatchingKey, WebSocketConnectionInfo.StringAggregator stringAggregator) { return Optional.ofNullable(dispatchingKey) .flatMap(key -> { try { - BString dispatchingValue = ((BMap) FromJsonString.fromJsonString( + String dispatchingValue = ((BMap) FromJsonString.fromJsonString( StringUtils.fromString(stringAggregator.getAggregateString()))) - .getStringValue(StringUtils.fromString(dispatchingKey)); - return Optional.of(createCustomRemoteFunction(dispatchingValue.getValue())); + .getStringValue(StringUtils.fromString(dispatchingKey)).getValue(); + return Optional.of(dispatchingValue); } catch (RuntimeException e) { return Optional.empty(); } @@ -590,6 +597,18 @@ private static String createCustomRemoteFunction(String dispatchingValue) { return builder.toString(); } + @SuppressWarnings(UNCHECKED) + public static String getFunctionDispatchingValue(MethodType remoteFunc) { + BMap annotations = (BMap) remoteFunc.getAnnotation(StringUtils.fromString( + ModuleUtils.getPackageIdentifier() + ":" + WebSocketConstants.WEBSOCKET_DISPATCHER_CONFIG_ANNOTATION)); + if (annotations != null && annotations.containsKey(WebSocketConstants.ANNOTATION_ATTR_DISPATCHER_VALUE)) { + String dispatchingValue = annotations. + getStringValue(WebSocketConstants.ANNOTATION_ATTR_DISPATCHER_VALUE).getValue(); + return createCustomRemoteFunction(dispatchingValue); + } + return remoteFunc.getName(); + } + private static void sendDataBindingError(WebSocketConnection webSocketConnection, String errorMessage) { if (errorMessage.length() > 100) { errorMessage = errorMessage.substring(0, 80) + "..."; From 1c0b3a5512614cfb2debdce6d941b2e852c180cc Mon Sep 17 00:00:00 2001 From: ayash Date: Thu, 27 Mar 2025 11:40:02 +0530 Subject: [PATCH 04/38] Add support for dispatching messages to remote functions based on priority --- ballerina/tests/remote_method_annotation.bal | 8 ++- .../WebSocketResourceDispatcher.java | 53 +++++-------------- .../stdlib/websocket/WebSocketService.java | 49 +++++++++++++++++ 3 files changed, 68 insertions(+), 42 deletions(-) diff --git a/ballerina/tests/remote_method_annotation.bal b/ballerina/tests/remote_method_annotation.bal index df08253f2..f9b9fb035 100644 --- a/ballerina/tests/remote_method_annotation.bal +++ b/ballerina/tests/remote_method_annotation.bal @@ -33,11 +33,15 @@ service / on new Listener(22101) { service class WsService22101 { *Service; + remote function onSubscribe(Subscribe message) returns string { + return "onSubscribe"; + } + @DispatcherConfig { value: "subscribe" } remote function onSubscribeMessage(Subscribe message) returns string { - return "Subscribed"; + return "onSubscribeMessage"; } } @@ -48,5 +52,5 @@ public function testDispatcherConfigAnnotation() returns Error? { Client wsClient = check new ("ws://localhost:22101/"); check wsClient->writeMessage({event: "subscribe", data: "test"}); string res = check wsClient->readMessage(); - test:assertEquals(res, "Subscribed"); + test:assertEquals(res, "onSubscribeMessage"); } diff --git a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketResourceDispatcher.java b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketResourceDispatcher.java index f96049df1..835ef1a72 100644 --- a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketResourceDispatcher.java +++ b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketResourceDispatcher.java @@ -97,7 +97,6 @@ import static io.ballerina.stdlib.websocket.WebSocketConstants.CONSTRAINT_VALIDATION; import static io.ballerina.stdlib.websocket.WebSocketConstants.HEADER_ANNOTATION; import static io.ballerina.stdlib.websocket.WebSocketConstants.PARAM_ANNOT_PREFIX; -import static io.ballerina.stdlib.websocket.WebSocketConstants.UNCHECKED; import static io.ballerina.stdlib.websocket.WebSocketUtil.getBString; import static io.ballerina.stdlib.websocket.WebSocketUtil.hasByteArrayType; import static io.ballerina.stdlib.websocket.observability.WebSocketObservabilityConstants.ERROR_TYPE_MESSAGE_RECEIVED; @@ -413,29 +412,24 @@ public static void dispatchOnText(WebSocketConnectionInfo connectionInfo, WebSoc MethodType onTextMessageResource = null; BObject wsEndpoint = connectionInfo.getWebSocketEndpoint(); Object dispatchingService = wsService.getWsService(connectionInfo.getWebSocketConnection().getChannelId()); - MethodType[] remoteFunctions = ((ServiceType) (((BValue) dispatchingService).getType())).getMethods(); - for (MethodType remoteFunc : remoteFunctions) { - String funcDispatcherValue = getFunctionDispatchingValue(remoteFunc); - if (dispatchingValue.isPresent() && funcDispatcherValue.equals(dispatchingValue.get())) { - onTextMessageResource = remoteFunc; - break; - } - if (customRemoteMethodName.isPresent() && funcDispatcherValue.equals(customRemoteMethodName.get())) { - onTextMessageResource = remoteFunc; - break; - } - if (funcDispatcherValue.equals(WebSocketConstants.RESOURCE_NAME_ON_TEXT_MESSAGE) || - funcDispatcherValue.equals(WebSocketConstants.RESOURCE_NAME_ON_MESSAGE)) { - onTextMessageResource = remoteFunc; - } + Map dispatchingFunctions = wsService + .getDispatchingFunctions(connectionInfo.getWebSocketConnection().getChannelId()); + if (dispatchingValue.isPresent() && dispatchingFunctions.containsKey(dispatchingValue.get())) { + onTextMessageResource = dispatchingFunctions.get(dispatchingValue.get()); + } else if (customRemoteMethodName.isPresent() + && dispatchingFunctions.containsKey(customRemoteMethodName.get())) { + onTextMessageResource = dispatchingFunctions.get(customRemoteMethodName.get()); + } else if (dispatchingFunctions.containsKey(WebSocketConstants.RESOURCE_NAME_ON_TEXT_MESSAGE)) { + onTextMessageResource = dispatchingFunctions.get(WebSocketConstants.RESOURCE_NAME_ON_TEXT_MESSAGE); + } else if (dispatchingFunctions.containsKey(WebSocketConstants.RESOURCE_NAME_ON_MESSAGE)) { + onTextMessageResource = dispatchingFunctions.get(WebSocketConstants.RESOURCE_NAME_ON_MESSAGE); } - boolean hasOnError = Arrays.stream(remoteFunctions).anyMatch(remoteFunc -> remoteFunc.getName() - .equals(WebSocketConstants.RESOURCE_NAME_ON_ERROR)); + boolean hasOnError = dispatchingFunctions.containsKey(WebSocketConstants.RESOURCE_NAME_ON_ERROR); String errorMethodName = null; boolean hasOnCustomError = false; if (customRemoteMethodName.isPresent()) { errorMethodName = customRemoteMethodName.get() + "Error"; - hasOnCustomError = hasCustomErrorRemoteFunction(remoteFunctions, errorMethodName); + hasOnCustomError = dispatchingFunctions.containsKey(errorMethodName); } if (onTextMessageResource == null) { stringAggregator.resetAggregateString(); @@ -537,15 +531,6 @@ private static Object getBvaluesForTextMessage(Type param, int typeTag, BObject return bValue; } - private static boolean hasCustomErrorRemoteFunction(MethodType[] remoteFunctions, String errorMethodName) { - for (MethodType remoteFunc : remoteFunctions) { - if (remoteFunc.getName().equals(errorMethodName)) { - return true; - } - } - return false; - } - private static void handleError(WebSocketConnectionInfo connectionInfo, BError error, boolean hasOnCustomError, String errorMethodName, boolean hasOnError) throws IllegalAccessException { if (hasOnCustomError) { @@ -597,18 +582,6 @@ private static String createCustomRemoteFunction(String dispatchingValue) { return builder.toString(); } - @SuppressWarnings(UNCHECKED) - public static String getFunctionDispatchingValue(MethodType remoteFunc) { - BMap annotations = (BMap) remoteFunc.getAnnotation(StringUtils.fromString( - ModuleUtils.getPackageIdentifier() + ":" + WebSocketConstants.WEBSOCKET_DISPATCHER_CONFIG_ANNOTATION)); - if (annotations != null && annotations.containsKey(WebSocketConstants.ANNOTATION_ATTR_DISPATCHER_VALUE)) { - String dispatchingValue = annotations. - getStringValue(WebSocketConstants.ANNOTATION_ATTR_DISPATCHER_VALUE).getValue(); - return createCustomRemoteFunction(dispatchingValue); - } - return remoteFunc.getName(); - } - private static void sendDataBindingError(WebSocketConnection webSocketConnection, String errorMessage) { if (errorMessage.length() > 100) { errorMessage = errorMessage.substring(0, 80) + "..."; diff --git a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketService.java b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketService.java index e158e029e..1c3b3bf80 100644 --- a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketService.java +++ b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketService.java @@ -21,12 +21,22 @@ import io.ballerina.runtime.api.Runtime; import io.ballerina.runtime.api.types.MethodType; import io.ballerina.runtime.api.types.ObjectType; +import io.ballerina.runtime.api.types.ServiceType; +import io.ballerina.runtime.api.utils.StringUtils; import io.ballerina.runtime.api.utils.TypeUtils; +import io.ballerina.runtime.api.values.BMap; import io.ballerina.runtime.api.values.BObject; +import io.ballerina.runtime.api.values.BString; +import io.ballerina.runtime.api.values.BValue; +import java.util.HashSet; import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import static io.ballerina.stdlib.websocket.WebSocketConstants.UNCHECKED; + /** * WebSocket service for service dispatching. */ @@ -36,6 +46,7 @@ public class WebSocketService { protected Runtime runtime; private final Map resourcesMap = new ConcurrentHashMap<>(); private Map wsServices = new ConcurrentHashMap<>(); + private Map> wsServicesDispatchingFunctions = new ConcurrentHashMap<>(); public WebSocketService(Runtime runtime) { this.runtime = runtime; @@ -55,6 +66,39 @@ private void populateResourcesMap(BObject service) { } } + private Map getDispatchingFunctionMap(Object dispatchingService) { + Map dispatchingFunctions = new ConcurrentHashMap<>(); + Set seenRemoteFunctionNames = new HashSet<>(); + MethodType[] remoteFunctions = ((ServiceType) (((BValue) dispatchingService).getType())).getMethods(); + // 1. DispatcherConfig values have the highest priority + for (MethodType remoteFunc : remoteFunctions) { + Optional dispatchingValue = getAnnotationDispatchingValue(remoteFunc); + if (dispatchingValue.isPresent()) { + dispatchingFunctions.put(dispatchingValue.get(), remoteFunc); + seenRemoteFunctionNames.add(remoteFunc.getName()); + } + } + // 2. Custom remote function names have the next priority + for (MethodType remoteFunc : remoteFunctions) { + if (!seenRemoteFunctionNames.contains(remoteFunc.getName())) { + dispatchingFunctions.put(remoteFunc.getName(), remoteFunc); + } + } + return dispatchingFunctions; + } + + @SuppressWarnings(UNCHECKED) + public static Optional getAnnotationDispatchingValue(MethodType remoteFunc) { + BMap annotations = (BMap) remoteFunc.getAnnotation(StringUtils.fromString( + ModuleUtils.getPackageIdentifier() + ":" + WebSocketConstants.WEBSOCKET_DISPATCHER_CONFIG_ANNOTATION)); + if (annotations != null && annotations.containsKey(WebSocketConstants.ANNOTATION_ATTR_DISPATCHER_VALUE)) { + String dispatchingValue = annotations. + getStringValue(WebSocketConstants.ANNOTATION_ATTR_DISPATCHER_VALUE).getValue(); + return Optional.of(dispatchingValue); + } + return Optional.empty(); + } + public MethodType getResourceByName(String resourceName) { return resourcesMap.get(resourceName); } @@ -69,9 +113,14 @@ public Runtime getRuntime() { public void addWsService(String channelId, Object dispatchingService) { this.wsServices.put(channelId, dispatchingService); + this.wsServicesDispatchingFunctions.put(channelId, getDispatchingFunctionMap(dispatchingService)); } public Object getWsService(String key) { return this.wsServices.get(key); } + + public Map getDispatchingFunctions(String key) { + return this.wsServicesDispatchingFunctions.get(key); + } } From be883e087d8f2fbf7f8272d4cc3719248bf785d2 Mon Sep 17 00:00:00 2001 From: ayash Date: Fri, 28 Mar 2025 13:21:39 +0530 Subject: [PATCH 05/38] Update license header and WsDispatcherConfig naming convention --- ballerina/remote_method_annotation.bal | 6 +++--- ballerina/tests/remote_method_annotation.bal | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ballerina/remote_method_annotation.bal b/ballerina/remote_method_annotation.bal index a2e94fb62..14623cc15 100644 --- a/ballerina/remote_method_annotation.bal +++ b/ballerina/remote_method_annotation.bal @@ -1,4 +1,4 @@ -// Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org). +// Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). // // WSO2 LLC. licenses this file to you under the Apache License, // Version 2.0 (the "License"); you may not use this file except @@ -21,9 +21,9 @@ # Configurations for WebSocket remote functions. # # + value - The value which is going to be used for dispatching to custom remote functions. -public type WSDispatcherConfig record {| +public type WsDispatcherConfig record {| string value; |}; # The annotation which is used to configure WebSocket remote functions. -public annotation WSDispatcherConfig DispatcherConfig on function; +public annotation WsDispatcherConfig DispatcherConfig on function; diff --git a/ballerina/tests/remote_method_annotation.bal b/ballerina/tests/remote_method_annotation.bal index f9b9fb035..3d2771830 100644 --- a/ballerina/tests/remote_method_annotation.bal +++ b/ballerina/tests/remote_method_annotation.bal @@ -1,4 +1,4 @@ -// Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org). +// Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). // // WSO2 LLC. licenses this file to you under the Apache License, // Version 2.0 (the "License"); you may not use this file except From d587e8ab4b31404f1723c5962669eb6b0a918d88 Mon Sep 17 00:00:00 2001 From: ayash Date: Fri, 28 Mar 2025 14:06:08 +0530 Subject: [PATCH 06/38] Change WsDispatcherConfig annotation to a constant --- ballerina/remote_method_annotation.bal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ballerina/remote_method_annotation.bal b/ballerina/remote_method_annotation.bal index 14623cc15..e724457d3 100644 --- a/ballerina/remote_method_annotation.bal +++ b/ballerina/remote_method_annotation.bal @@ -26,4 +26,4 @@ public type WsDispatcherConfig record {| |}; # The annotation which is used to configure WebSocket remote functions. -public annotation WsDispatcherConfig DispatcherConfig on function; +public const annotation WsDispatcherConfig DispatcherConfig on function; From 95bedbf54ea7030976c1111293a257e097b74cba Mon Sep 17 00:00:00 2001 From: ayash Date: Fri, 28 Mar 2025 14:17:46 +0530 Subject: [PATCH 07/38] Rename WS service name in the test --- ...notation.bal => test_dispatcher_config_annotation.bal} | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) rename ballerina/tests/{remote_method_annotation.bal => test_dispatcher_config_annotation.bal} (90%) diff --git a/ballerina/tests/remote_method_annotation.bal b/ballerina/tests/test_dispatcher_config_annotation.bal similarity index 90% rename from ballerina/tests/remote_method_annotation.bal rename to ballerina/tests/test_dispatcher_config_annotation.bal index 3d2771830..4787c8b70 100644 --- a/ballerina/tests/remote_method_annotation.bal +++ b/ballerina/tests/test_dispatcher_config_annotation.bal @@ -24,13 +24,13 @@ type Subscribe record {| @ServiceConfig { dispatcherKey: "event" } -service / on new Listener(22101) { +service / on new Listener(22103) { resource function get .() returns Service|UpgradeError { - return new WsService22101(); + return new WsService22103(); } } -service class WsService22101 { +service class WsService22103 { *Service; remote function onSubscribe(Subscribe message) returns string { @@ -49,7 +49,7 @@ service class WsService22101 { groups: ["dispatcherConfigAnnotation"] } public function testDispatcherConfigAnnotation() returns Error? { - Client wsClient = check new ("ws://localhost:22101/"); + Client wsClient = check new ("ws://localhost:22103/"); check wsClient->writeMessage({event: "subscribe", data: "test"}); string res = check wsClient->readMessage(); test:assertEquals(res, "onSubscribeMessage"); From 855afda42997d125ccf61d11bdcbe3160b8495bb Mon Sep 17 00:00:00 2001 From: ayash Date: Fri, 28 Mar 2025 14:20:13 +0530 Subject: [PATCH 08/38] Rename annotation file name --- ...ote_method_annotation.bal => dispatcher_config_annotation.bal} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename ballerina/{remote_method_annotation.bal => dispatcher_config_annotation.bal} (100%) diff --git a/ballerina/remote_method_annotation.bal b/ballerina/dispatcher_config_annotation.bal similarity index 100% rename from ballerina/remote_method_annotation.bal rename to ballerina/dispatcher_config_annotation.bal From 753c4103437aaa7eabf290d0f0f449c1369dc396 Mon Sep 17 00:00:00 2001 From: ayash Date: Mon, 31 Mar 2025 09:46:01 +0530 Subject: [PATCH 09/38] Update DispatcherConfig annotation doc comment --- ballerina/dispatcher_config_annotation.bal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ballerina/dispatcher_config_annotation.bal b/ballerina/dispatcher_config_annotation.bal index e724457d3..9f360f6d7 100644 --- a/ballerina/dispatcher_config_annotation.bal +++ b/ballerina/dispatcher_config_annotation.bal @@ -25,5 +25,5 @@ public type WsDispatcherConfig record {| string value; |}; -# The annotation which is used to configure WebSocket remote functions. +# The annotation which is used to configure the dispatching rules for WebSocket remote functions. public const annotation WsDispatcherConfig DispatcherConfig on function; From d979a5a5b5e229aca2ba266fc7a1a97985e7e03a Mon Sep 17 00:00:00 2001 From: ayash Date: Tue, 1 Apr 2025 11:30:59 +0530 Subject: [PATCH 10/38] Refactor ANNOTATION_ATTR_DISPATCHER_VALUE to string --- .../io/ballerina/stdlib/websocket/WebSocketConstants.java | 2 +- .../io/ballerina/stdlib/websocket/WebSocketService.java | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketConstants.java b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketConstants.java index 682f7f7e3..95c7aacd9 100644 --- a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketConstants.java +++ b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketConstants.java @@ -52,7 +52,7 @@ public class WebSocketConstants { public static final BString ANNOTATION_ATTR_DISPATCHER_KEY = StringUtils.fromString("dispatcherKey"); public static final String WEBSOCKET_DISPATCHER_CONFIG_ANNOTATION = "DispatcherConfig"; - public static final BString ANNOTATION_ATTR_DISPATCHER_VALUE = StringUtils.fromString("value"); + public static final String ANNOTATION_ATTR_DISPATCHER_VALUE = "value"; public static final BString RETRY_CONFIG = StringUtils.fromString("retryConfig"); public static final String LOG_MESSAGE = "{} {}"; diff --git a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketService.java b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketService.java index 1c3b3bf80..712e8a41f 100644 --- a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketService.java +++ b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketService.java @@ -35,6 +35,8 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import static io.ballerina.runtime.api.utils.StringUtils.fromString; +import static io.ballerina.stdlib.websocket.WebSocketConstants.ANNOTATION_ATTR_DISPATCHER_VALUE; import static io.ballerina.stdlib.websocket.WebSocketConstants.UNCHECKED; /** @@ -89,11 +91,11 @@ private Map getDispatchingFunctionMap(Object dispatchingServ @SuppressWarnings(UNCHECKED) public static Optional getAnnotationDispatchingValue(MethodType remoteFunc) { - BMap annotations = (BMap) remoteFunc.getAnnotation(StringUtils.fromString( + BMap annotations = (BMap) remoteFunc.getAnnotation(fromString( ModuleUtils.getPackageIdentifier() + ":" + WebSocketConstants.WEBSOCKET_DISPATCHER_CONFIG_ANNOTATION)); - if (annotations != null && annotations.containsKey(WebSocketConstants.ANNOTATION_ATTR_DISPATCHER_VALUE)) { + if (annotations != null && annotations.containsKey(fromString(ANNOTATION_ATTR_DISPATCHER_VALUE))) { String dispatchingValue = annotations. - getStringValue(WebSocketConstants.ANNOTATION_ATTR_DISPATCHER_VALUE).getValue(); + getStringValue(fromString(ANNOTATION_ATTR_DISPATCHER_VALUE)).getValue(); return Optional.of(dispatchingValue); } return Optional.empty(); From f0a6982dcd27783c69ce15d37d2276a5add9f908 Mon Sep 17 00:00:00 2001 From: ayash Date: Tue, 1 Apr 2025 13:39:01 +0530 Subject: [PATCH 11/38] Add compiler plugin validation for DispatcherConfig annotation --- .../WebSocketServiceValidationTest.java | 10 +++ .../sample_package_63/Ballerina.toml | 4 + .../sample_package_63/server.bal | 46 ++++++++++ .../websocket/plugin/PluginConstants.java | 2 + .../plugin/WebSocketServiceValidator.java | 88 ++++++++++++++++++- .../stdlib/websocket/WebSocketService.java | 1 - 6 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 compiler-plugin-tests/src/test/resources/ballerina_sources/sample_package_63/Ballerina.toml create mode 100644 compiler-plugin-tests/src/test/resources/ballerina_sources/sample_package_63/server.bal diff --git a/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/websocket/compiler/WebSocketServiceValidationTest.java b/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/websocket/compiler/WebSocketServiceValidationTest.java index 6e168a4ee..abd1cf25e 100644 --- a/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/websocket/compiler/WebSocketServiceValidationTest.java +++ b/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/websocket/compiler/WebSocketServiceValidationTest.java @@ -555,6 +555,16 @@ public void testDispatcherStreamIdWithoutDispatcherKey() { assertDiagnostic(diagnostic, PluginConstants.CompilationErrors.DISPATCHER_STREAM_ID_WITHOUT_KEY); } + @Test + public void testDispatcherConfigAnnotation() { + Package currentPackage = loadPackage("sample_package_63"); + PackageCompilation compilation = currentPackage.getCompilation(); + DiagnosticResult diagnosticResult = compilation.diagnosticResult(); + Assert.assertEquals(diagnosticResult.errorCount(), 1); + Diagnostic diagnostic = (Diagnostic) diagnosticResult.errors().toArray()[0]; + assertDiagnostic(diagnostic, PluginConstants.CompilationErrors.RE_DECLARED_REMOTE_FUNCTIONS); + } + private void assertDiagnostic(Diagnostic diagnostic, PluginConstants.CompilationErrors error) { Assert.assertEquals(diagnostic.diagnosticInfo().code(), error.getErrorCode()); Assert.assertEquals(diagnostic.diagnosticInfo().messageFormat(), diff --git a/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_package_63/Ballerina.toml b/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_package_63/Ballerina.toml new file mode 100644 index 000000000..5cd60fa22 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_package_63/Ballerina.toml @@ -0,0 +1,4 @@ +[package] +org = "websocket_test" +name = "sample_63" +version = "0.1.0" diff --git a/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_package_63/server.bal b/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_package_63/server.bal new file mode 100644 index 000000000..71cf16ae1 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_package_63/server.bal @@ -0,0 +1,46 @@ +// Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/websocket; + +type Subscribe record {| + string event = "subscribe"; + string data; +|}; + +@websocket:ServiceConfig { + dispatcherKey: "event" +} +service / on new websocket:Listener(9090) { + resource function get .() returns websocket:Service|websocket:UpgradeError { + return new WsService(); + } +} + +service class WsService { + *websocket:Service; + + remote function onSubscribe(Subscribe message) returns string { + return "onSubscribe"; + } + + @websocket:DispatcherConfig { + value: "subscribe" + } + remote function onSubscribeMessage(Subscribe message) returns string { + return "onSubscribeMessage"; + } +} diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/PluginConstants.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/PluginConstants.java index fbb29abe3..9971443ba 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/PluginConstants.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/PluginConstants.java @@ -78,6 +78,8 @@ public enum CompilationErrors { "WEBSOCKET_215"), INVALID_REMOTE_FUNCTIONS("Cannot have `{0}` with `onMessage` remote function", "WEBSOCKET_216"), + RE_DECLARED_REMOTE_FUNCTIONS("Cannot have `{0}` because the message type `{1}` is already " + + "associated with `{2}` remote function", "WEBSOCKET_217"), INVALID_RESOURCE_ERROR("There should be only one `get` resource for the service", "WEBSOCKET_101"), MORE_THAN_ONE_RESOURCE_PARAM_ERROR("There should be only http:Request as a parameter", diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/WebSocketServiceValidator.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/WebSocketServiceValidator.java index a4fc6792e..8be81dfef 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/WebSocketServiceValidator.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/WebSocketServiceValidator.java @@ -20,24 +20,41 @@ import io.ballerina.compiler.api.symbols.FunctionTypeSymbol; import io.ballerina.compiler.api.symbols.MethodSymbol; import io.ballerina.compiler.api.symbols.Qualifier; +import io.ballerina.compiler.api.symbols.Symbol; +import io.ballerina.compiler.syntax.tree.AnnotationNode; import io.ballerina.compiler.syntax.tree.ClassDefinitionNode; +import io.ballerina.compiler.syntax.tree.ExpressionNode; import io.ballerina.compiler.syntax.tree.FunctionDefinitionNode; +import io.ballerina.compiler.syntax.tree.MappingConstructorExpressionNode; +import io.ballerina.compiler.syntax.tree.Node; import io.ballerina.compiler.syntax.tree.NodeList; +import io.ballerina.compiler.syntax.tree.SpecificFieldNode; import io.ballerina.compiler.syntax.tree.SyntaxKind; import io.ballerina.compiler.syntax.tree.Token; import io.ballerina.projects.plugins.SyntaxNodeAnalysisContext; +import io.ballerina.stdlib.websocket.WebSocketConstants; import io.ballerina.tools.diagnostics.DiagnosticFactory; import io.ballerina.tools.diagnostics.DiagnosticInfo; import io.ballerina.tools.diagnostics.DiagnosticSeverity; +import java.util.Locale; import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; +import static io.ballerina.stdlib.websocket.WebSocketConstants.ANNOTATION_ATTR_DISPATCHER_VALUE; +import static io.ballerina.stdlib.websocket.plugin.PluginConstants.CompilationErrors.RE_DECLARED_REMOTE_FUNCTIONS; + /** * A class for validating websocket service. */ public class WebSocketServiceValidator { public static final String GENERIC_FUNCTION = "generic function"; + private final Set specialRemoteMethods = Set.of(PluginConstants.ON_OPEN, PluginConstants.ON_CLOSE, + PluginConstants.ON_ERROR, PluginConstants.ON_IDLE_TIMEOUT, PluginConstants.ON_TEXT_MESSAGE, + PluginConstants.ON_BINARY_MESSAGE, PluginConstants.ON_MESSAGE, PluginConstants.ON_PING_MESSAGE, + PluginConstants.ON_PONG_MESSAGE); private SyntaxNodeAnalysisContext ctx; WebSocketServiceValidator(SyntaxNodeAnalysisContext syntaxNodeAnalysisContext) { @@ -70,7 +87,7 @@ public void validate() { classDefNode.location(), PluginConstants.ON_TEXT_MESSAGE); } if (functionSet.containsKey(PluginConstants.ON_MESSAGE) && - functionSet.containsKey(PluginConstants.ON_BINARY_MESSAGE)) { + functionSet.containsKey(PluginConstants.ON_BINARY_MESSAGE)) { Utils.reportDiagnostics(ctx, PluginConstants.CompilationErrors.INVALID_REMOTE_FUNCTIONS, classDefNode.location(), PluginConstants.ON_BINARY_MESSAGE); } @@ -105,6 +122,75 @@ public void validate() { !functionSet.containsKey(PluginConstants.ON_BINARY_MESSAGE)) { reportDiagnostic(classDefNode, PluginConstants.CompilationErrors.ON_MESSAGE_GENERATION_HINT); } + + for (Node node : classDefNode.members()) { + if (node instanceof FunctionDefinitionNode funcDefinitionNode) { + String funcName = funcDefinitionNode.functionName().toString(); + Optional annoDispatchingValue = + getDispatcherConfigAnnotatedFunctionName(funcDefinitionNode, ctx); + if (annoDispatchingValue.isPresent()) { + String customRemoteFunctionName = createCustomRemoteFunction(annoDispatchingValue.get()); + if (functionSet.containsKey(customRemoteFunctionName) && + !customRemoteFunctionName.equals(funcName) && + !specialRemoteMethods.contains(customRemoteFunctionName)) { + Utils.reportDiagnostics(ctx, RE_DECLARED_REMOTE_FUNCTIONS, classDefNode.location(), + customRemoteFunctionName, annoDispatchingValue.get(), funcName); + } + } + } + } + } + + private static Optional getDispatcherConfigAnnotatedFunctionName(FunctionDefinitionNode node, + SyntaxNodeAnalysisContext ctx) { + if (node.metadata().isEmpty()) { + return Optional.empty(); + } + for (AnnotationNode annotationNode : node.metadata().get().annotations()) { + Optional annotationType = ctx.semanticModel().symbol(annotationNode); + if (annotationType.isEmpty()) { + continue; + } + if (!annotationType.get().getModule().flatMap(Symbol::getName) + .orElse("").equals(WebSocketConstants.PACKAGE_WEBSOCKET) || + !annotationType.get().getName().orElse("") + .equals(WebSocketConstants.WEBSOCKET_DISPATCHER_CONFIG_ANNOTATION)) { + continue; + } + if (annotationNode.annotValue().isEmpty()) { + return Optional.empty(); + } + MappingConstructorExpressionNode annotationValue = annotationNode.annotValue().get(); + for (Node field : annotationValue.fields()) { + if (field instanceof SpecificFieldNode specificFieldNode) { + String filedName = specificFieldNode.fieldName().toString().strip(); + Optional filedValue = specificFieldNode.valueExpr(); + if (filedName.equals(ANNOTATION_ATTR_DISPATCHER_VALUE) && + filedValue.isPresent()) { + return Optional.of(filedValue.get().toString().strip() + .replaceAll("\"", "")); + } + } + } + } + return Optional.empty(); + } + + private static String createCustomRemoteFunction(String dispatchingValue) { + dispatchingValue = "on " + dispatchingValue; + StringBuilder builder = new StringBuilder(); + String[] words = dispatchingValue.split("[\\W_]+"); + for (int i = 0; i < words.length; i++) { + String word = words[i]; + if (i == 0) { + word = word.isEmpty() ? word : word.toLowerCase(Locale.ENGLISH); + } else { + word = word.isEmpty() ? word : Character.toUpperCase(word.charAt(0)) + word.substring(1) + .toLowerCase(Locale.ENGLISH); + } + builder.append(word); + } + return builder.toString(); } private void filterRemoteFunctions(FunctionDefinitionNode functionDefinitionNode) { diff --git a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketService.java b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketService.java index 712e8a41f..da48d5cd2 100644 --- a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketService.java +++ b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketService.java @@ -22,7 +22,6 @@ import io.ballerina.runtime.api.types.MethodType; import io.ballerina.runtime.api.types.ObjectType; import io.ballerina.runtime.api.types.ServiceType; -import io.ballerina.runtime.api.utils.StringUtils; import io.ballerina.runtime.api.utils.TypeUtils; import io.ballerina.runtime.api.values.BMap; import io.ballerina.runtime.api.values.BObject; From 428a9cf2a693d0ed40460e06fa26f8e36688df84 Mon Sep 17 00:00:00 2001 From: ayash Date: Tue, 1 Apr 2025 19:21:50 +0530 Subject: [PATCH 12/38] Add validation for duplicated DispatcherConfig annotation values --- .../WebSocketServiceValidationTest.java | 8 ++++--- .../sample_package_63/server.bal | 7 +++++++ .../websocket/plugin/PluginConstants.java | 2 ++ .../plugin/WebSocketServiceValidator.java | 21 +++++++++++++------ 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/websocket/compiler/WebSocketServiceValidationTest.java b/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/websocket/compiler/WebSocketServiceValidationTest.java index abd1cf25e..3b32374c2 100644 --- a/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/websocket/compiler/WebSocketServiceValidationTest.java +++ b/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/websocket/compiler/WebSocketServiceValidationTest.java @@ -560,9 +560,11 @@ public void testDispatcherConfigAnnotation() { Package currentPackage = loadPackage("sample_package_63"); PackageCompilation compilation = currentPackage.getCompilation(); DiagnosticResult diagnosticResult = compilation.diagnosticResult(); - Assert.assertEquals(diagnosticResult.errorCount(), 1); - Diagnostic diagnostic = (Diagnostic) diagnosticResult.errors().toArray()[0]; - assertDiagnostic(diagnostic, PluginConstants.CompilationErrors.RE_DECLARED_REMOTE_FUNCTIONS); + Assert.assertEquals(diagnosticResult.errorCount(), 2); + Diagnostic firstDiagnostic = (Diagnostic) diagnosticResult.errors().toArray()[0]; + assertDiagnostic(firstDiagnostic, PluginConstants.CompilationErrors.RE_DECLARED_REMOTE_FUNCTIONS); + Diagnostic secondDiagnostic = (Diagnostic) diagnosticResult.errors().toArray()[1]; + assertDiagnostic(secondDiagnostic, PluginConstants.CompilationErrors.DUPLICATED_DISPATCHER_CONFIG_VALUE); } private void assertDiagnostic(Diagnostic diagnostic, PluginConstants.CompilationErrors error) { diff --git a/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_package_63/server.bal b/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_package_63/server.bal index 71cf16ae1..b3c3c4e0b 100644 --- a/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_package_63/server.bal +++ b/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_package_63/server.bal @@ -43,4 +43,11 @@ service class WsService { remote function onSubscribeMessage(Subscribe message) returns string { return "onSubscribeMessage"; } + + @websocket:DispatcherConfig { + value: "subscribe" + } + remote function onSubscribeText(Subscribe message) returns string { + return "onSubscribeText"; + } } diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/PluginConstants.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/PluginConstants.java index 9971443ba..16c9146e9 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/PluginConstants.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/PluginConstants.java @@ -80,6 +80,8 @@ public enum CompilationErrors { "WEBSOCKET_216"), RE_DECLARED_REMOTE_FUNCTIONS("Cannot have `{0}` because the message type `{1}` is already " + "associated with `{2}` remote function", "WEBSOCKET_217"), + DUPLICATED_DISPATCHER_CONFIG_VALUE("DispatcherConfig annotation value `{0}` is already " + + "exists", "WEBSOCKET_218"), INVALID_RESOURCE_ERROR("There should be only one `get` resource for the service", "WEBSOCKET_101"), MORE_THAN_ONE_RESOURCE_PARAM_ERROR("There should be only http:Request as a parameter", diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/WebSocketServiceValidator.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/WebSocketServiceValidator.java index 8be81dfef..61b82667f 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/WebSocketServiceValidator.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/WebSocketServiceValidator.java @@ -37,6 +37,7 @@ import io.ballerina.tools.diagnostics.DiagnosticInfo; import io.ballerina.tools.diagnostics.DiagnosticSeverity; +import java.util.HashSet; import java.util.Locale; import java.util.Map; import java.util.Optional; @@ -44,6 +45,7 @@ import java.util.stream.Collectors; import static io.ballerina.stdlib.websocket.WebSocketConstants.ANNOTATION_ATTR_DISPATCHER_VALUE; +import static io.ballerina.stdlib.websocket.plugin.PluginConstants.CompilationErrors.DUPLICATED_DISPATCHER_CONFIG_VALUE; import static io.ballerina.stdlib.websocket.plugin.PluginConstants.CompilationErrors.RE_DECLARED_REMOTE_FUNCTIONS; /** @@ -123,18 +125,25 @@ public void validate() { reportDiagnostic(classDefNode, PluginConstants.CompilationErrors.ON_MESSAGE_GENERATION_HINT); } + Set seenAnnotationValues = new HashSet<>(); for (Node node : classDefNode.members()) { if (node instanceof FunctionDefinitionNode funcDefinitionNode) { String funcName = funcDefinitionNode.functionName().toString(); Optional annoDispatchingValue = getDispatcherConfigAnnotatedFunctionName(funcDefinitionNode, ctx); if (annoDispatchingValue.isPresent()) { - String customRemoteFunctionName = createCustomRemoteFunction(annoDispatchingValue.get()); - if (functionSet.containsKey(customRemoteFunctionName) && - !customRemoteFunctionName.equals(funcName) && - !specialRemoteMethods.contains(customRemoteFunctionName)) { - Utils.reportDiagnostics(ctx, RE_DECLARED_REMOTE_FUNCTIONS, classDefNode.location(), - customRemoteFunctionName, annoDispatchingValue.get(), funcName); + if (seenAnnotationValues.contains(annoDispatchingValue.get())) { + Utils.reportDiagnostics(ctx, DUPLICATED_DISPATCHER_CONFIG_VALUE, + funcDefinitionNode.location(), annoDispatchingValue.get()); + } else { + seenAnnotationValues.add(annoDispatchingValue.get()); + String customRemoteFunctionName = createCustomRemoteFunction(annoDispatchingValue.get()); + if (functionSet.containsKey(customRemoteFunctionName) && + !customRemoteFunctionName.equals(funcName) && + !specialRemoteMethods.contains(customRemoteFunctionName)) { + Utils.reportDiagnostics(ctx, RE_DECLARED_REMOTE_FUNCTIONS, classDefNode.location(), + customRemoteFunctionName, annoDispatchingValue.get(), funcName); + } } } } From f1d760a5624b08a0eb674c3d5a3905fbde4cbbb7 Mon Sep 17 00:00:00 2001 From: ayash Date: Wed, 2 Apr 2025 11:03:20 +0530 Subject: [PATCH 13/38] Add validation for DispatcherConfig used with special functions --- .../websocket/compiler/WebSocketServiceValidationTest.java | 4 +++- .../ballerina_sources/sample_package_63/server.bal | 7 +++++++ .../ballerina/stdlib/websocket/plugin/PluginConstants.java | 1 + .../stdlib/websocket/plugin/WebSocketServiceValidator.java | 6 +++++- 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/websocket/compiler/WebSocketServiceValidationTest.java b/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/websocket/compiler/WebSocketServiceValidationTest.java index 3b32374c2..d6df9994d 100644 --- a/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/websocket/compiler/WebSocketServiceValidationTest.java +++ b/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/websocket/compiler/WebSocketServiceValidationTest.java @@ -560,11 +560,13 @@ public void testDispatcherConfigAnnotation() { Package currentPackage = loadPackage("sample_package_63"); PackageCompilation compilation = currentPackage.getCompilation(); DiagnosticResult diagnosticResult = compilation.diagnosticResult(); - Assert.assertEquals(diagnosticResult.errorCount(), 2); + Assert.assertEquals(diagnosticResult.errorCount(), 3); Diagnostic firstDiagnostic = (Diagnostic) diagnosticResult.errors().toArray()[0]; assertDiagnostic(firstDiagnostic, PluginConstants.CompilationErrors.RE_DECLARED_REMOTE_FUNCTIONS); Diagnostic secondDiagnostic = (Diagnostic) diagnosticResult.errors().toArray()[1]; assertDiagnostic(secondDiagnostic, PluginConstants.CompilationErrors.DUPLICATED_DISPATCHER_CONFIG_VALUE); + Diagnostic thirdDiagnostic = (Diagnostic) diagnosticResult.errors().toArray()[2]; + assertDiagnostic(thirdDiagnostic, PluginConstants.CompilationErrors.INVALID_FUNCTION_ANNOTATION); } private void assertDiagnostic(Diagnostic diagnostic, PluginConstants.CompilationErrors error) { diff --git a/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_package_63/server.bal b/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_package_63/server.bal index b3c3c4e0b..e2bc0443a 100644 --- a/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_package_63/server.bal +++ b/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_package_63/server.bal @@ -50,4 +50,11 @@ service class WsService { remote function onSubscribeText(Subscribe message) returns string { return "onSubscribeText"; } + + @websocket:DispatcherConfig { + value: "ping" + } + remote function onPing(Subscribe message) returns string { + return "onPing"; + } } diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/PluginConstants.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/PluginConstants.java index 16c9146e9..fd4017d30 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/PluginConstants.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/PluginConstants.java @@ -82,6 +82,7 @@ public enum CompilationErrors { "associated with `{2}` remote function", "WEBSOCKET_217"), DUPLICATED_DISPATCHER_CONFIG_VALUE("DispatcherConfig annotation value `{0}` is already " + "exists", "WEBSOCKET_218"), + INVALID_FUNCTION_ANNOTATION("Invalid annotation provided for `{0}` remote function", "WEBSOCKET_219"), INVALID_RESOURCE_ERROR("There should be only one `get` resource for the service", "WEBSOCKET_101"), MORE_THAN_ONE_RESOURCE_PARAM_ERROR("There should be only http:Request as a parameter", diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/WebSocketServiceValidator.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/WebSocketServiceValidator.java index 61b82667f..e1ffdcd6f 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/WebSocketServiceValidator.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/WebSocketServiceValidator.java @@ -46,6 +46,7 @@ import static io.ballerina.stdlib.websocket.WebSocketConstants.ANNOTATION_ATTR_DISPATCHER_VALUE; import static io.ballerina.stdlib.websocket.plugin.PluginConstants.CompilationErrors.DUPLICATED_DISPATCHER_CONFIG_VALUE; +import static io.ballerina.stdlib.websocket.plugin.PluginConstants.CompilationErrors.INVALID_FUNCTION_ANNOTATION; import static io.ballerina.stdlib.websocket.plugin.PluginConstants.CompilationErrors.RE_DECLARED_REMOTE_FUNCTIONS; /** @@ -138,7 +139,10 @@ public void validate() { } else { seenAnnotationValues.add(annoDispatchingValue.get()); String customRemoteFunctionName = createCustomRemoteFunction(annoDispatchingValue.get()); - if (functionSet.containsKey(customRemoteFunctionName) && + if (specialRemoteMethods.contains(funcName)) { + Utils.reportDiagnostics(ctx, INVALID_FUNCTION_ANNOTATION, funcDefinitionNode.location(), + funcName); + } else if (functionSet.containsKey(customRemoteFunctionName) && !customRemoteFunctionName.equals(funcName) && !specialRemoteMethods.contains(customRemoteFunctionName)) { Utils.reportDiagnostics(ctx, RE_DECLARED_REMOTE_FUNCTIONS, classDefNode.location(), From cb2e95c3c7cca909412d522af913ab6c497e6469 Mon Sep 17 00:00:00 2001 From: ayash Date: Thu, 3 Apr 2025 09:47:15 +0530 Subject: [PATCH 14/38] Handling dispatcher config annotation with customOnError methods --- .../test_dispatcher_config_annotation.bal | 20 ++++++++++++++----- .../WebSocketResourceDispatcher.java | 5 ++++- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/ballerina/tests/test_dispatcher_config_annotation.bal b/ballerina/tests/test_dispatcher_config_annotation.bal index 4787c8b70..1f1718020 100644 --- a/ballerina/tests/test_dispatcher_config_annotation.bal +++ b/ballerina/tests/test_dispatcher_config_annotation.bal @@ -33,24 +33,34 @@ service / on new Listener(22103) { service class WsService22103 { *Service; - remote function onSubscribe(Subscribe message) returns string { - return "onSubscribe"; - } - @DispatcherConfig { value: "subscribe" } remote function onSubscribeMessage(Subscribe message) returns string { return "onSubscribeMessage"; } + + remote function onSubscribeMessageError(Caller caller, error message) returns error? { + check caller->writeMessage("onSubscribeMessageError"); + } } @test:Config { groups: ["dispatcherConfigAnnotation"] } -public function testDispatcherConfigAnnotation() returns Error? { +public function testDispatcherConfigAnnotation() returns error? { Client wsClient = check new ("ws://localhost:22103/"); check wsClient->writeMessage({event: "subscribe", data: "test"}); string res = check wsClient->readMessage(); test:assertEquals(res, "onSubscribeMessage"); } + +@test:Config { + groups: ["dispatcherConfigAnnotation"] +} +public function testDispatcherConfigAnnotationWithCustomOnError() returns error? { + Client wsClient = check new ("ws://localhost:22103/"); + check wsClient->writeMessage({event: "subscribe", invalidField: "test"}); + string res = check wsClient->readMessage(); + test:assertEquals(res, "onSubscribeMessageError"); +} diff --git a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketResourceDispatcher.java b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketResourceDispatcher.java index 835ef1a72..3b1267c87 100644 --- a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketResourceDispatcher.java +++ b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketResourceDispatcher.java @@ -427,7 +427,10 @@ public static void dispatchOnText(WebSocketConnectionInfo connectionInfo, WebSoc boolean hasOnError = dispatchingFunctions.containsKey(WebSocketConstants.RESOURCE_NAME_ON_ERROR); String errorMethodName = null; boolean hasOnCustomError = false; - if (customRemoteMethodName.isPresent()) { + if (onTextMessageResource != null) { + errorMethodName = onTextMessageResource.getName() + "Error"; + hasOnCustomError = dispatchingFunctions.containsKey(errorMethodName); + } else if (customRemoteMethodName.isPresent()) { errorMethodName = customRemoteMethodName.get() + "Error"; hasOnCustomError = dispatchingFunctions.containsKey(errorMethodName); } From 9c123a476700af455a8bbe420e8c0ce494200535 Mon Sep 17 00:00:00 2001 From: ayash Date: Thu, 3 Apr 2025 10:06:58 +0530 Subject: [PATCH 15/38] Move DispatcherConfig annotation to annotation file --- ballerina/annotation.bal | 14 ++++++++--- ballerina/dispatcher_config_annotation.bal | 29 ---------------------- 2 files changed, 10 insertions(+), 33 deletions(-) delete mode 100644 ballerina/dispatcher_config_annotation.bal diff --git a/ballerina/annotation.bal b/ballerina/annotation.bal index 3286fcc18..91ae36cdd 100644 --- a/ballerina/annotation.bal +++ b/ballerina/annotation.bal @@ -14,10 +14,6 @@ // specific language governing permissions and limitations // under the License. -/////////////////////////// -/// Service Annotations /// -/////////////////////////// - # Configurations for a WebSocket service. # # + subProtocols - Negotiable sub protocol by the service @@ -42,3 +38,13 @@ public type WSServiceConfig record {| # The annotation which is used to configure a WebSocket service. public annotation WSServiceConfig ServiceConfig on service; + +# Configurations for WebSocket remote functions. +# +# + value - The value which is going to be used for dispatching to custom remote functions. +public type WsDispatcherConfig record {| + string value; +|}; + +# The annotation which is used to configure the dispatching rules for WebSocket remote functions. +public const annotation WsDispatcherConfig DispatcherConfig on function; diff --git a/ballerina/dispatcher_config_annotation.bal b/ballerina/dispatcher_config_annotation.bal deleted file mode 100644 index 9f360f6d7..000000000 --- a/ballerina/dispatcher_config_annotation.bal +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). -// -// WSO2 LLC. licenses this file to you under the Apache License, -// Version 2.0 (the "License"); you may not use this file except -// in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -////////////////////////////////////// -/// Dispatcher Config Annotations //// -////////////////////////////////////// - -# Configurations for WebSocket remote functions. -# -# + value - The value which is going to be used for dispatching to custom remote functions. -public type WsDispatcherConfig record {| - string value; -|}; - -# The annotation which is used to configure the dispatching rules for WebSocket remote functions. -public const annotation WsDispatcherConfig DispatcherConfig on function; From 8b45634689afc7a0e9763265495d05f9b1a8a889 Mon Sep 17 00:00:00 2001 From: ayash Date: Thu, 3 Apr 2025 10:44:57 +0530 Subject: [PATCH 16/38] Rename DispatcherConfig annotation to DispatcherMapping --- ballerina/annotation.bal | 4 ++-- .../tests/test_dispatcher_config_annotation.bal | 6 +++--- .../compiler/WebSocketServiceValidationTest.java | 4 ++-- .../ballerina_sources/sample_package_63/server.bal | 6 +++--- .../stdlib/websocket/plugin/PluginConstants.java | 2 +- .../websocket/plugin/WebSocketServiceValidator.java | 12 ++++++------ .../stdlib/websocket/WebSocketConstants.java | 2 +- .../ballerina/stdlib/websocket/WebSocketService.java | 4 ++-- 8 files changed, 20 insertions(+), 20 deletions(-) diff --git a/ballerina/annotation.bal b/ballerina/annotation.bal index 91ae36cdd..baaad97e7 100644 --- a/ballerina/annotation.bal +++ b/ballerina/annotation.bal @@ -42,9 +42,9 @@ public annotation WSServiceConfig ServiceConfig on service; # Configurations for WebSocket remote functions. # # + value - The value which is going to be used for dispatching to custom remote functions. -public type WsDispatcherConfig record {| +public type WsDispatcherMapping record {| string value; |}; # The annotation which is used to configure the dispatching rules for WebSocket remote functions. -public const annotation WsDispatcherConfig DispatcherConfig on function; +public const annotation WsDispatcherMapping DispatcherMapping on function; diff --git a/ballerina/tests/test_dispatcher_config_annotation.bal b/ballerina/tests/test_dispatcher_config_annotation.bal index 1f1718020..d53d32151 100644 --- a/ballerina/tests/test_dispatcher_config_annotation.bal +++ b/ballerina/tests/test_dispatcher_config_annotation.bal @@ -33,7 +33,7 @@ service / on new Listener(22103) { service class WsService22103 { *Service; - @DispatcherConfig { + @DispatcherMapping { value: "subscribe" } remote function onSubscribeMessage(Subscribe message) returns string { @@ -48,7 +48,7 @@ service class WsService22103 { @test:Config { groups: ["dispatcherConfigAnnotation"] } -public function testDispatcherConfigAnnotation() returns error? { +public function testDispatcherMappingAnnotation() returns error? { Client wsClient = check new ("ws://localhost:22103/"); check wsClient->writeMessage({event: "subscribe", data: "test"}); string res = check wsClient->readMessage(); @@ -58,7 +58,7 @@ public function testDispatcherConfigAnnotation() returns error? { @test:Config { groups: ["dispatcherConfigAnnotation"] } -public function testDispatcherConfigAnnotationWithCustomOnError() returns error? { +public function testDispatcherMappingAnnotationWithCustomOnError() returns error? { Client wsClient = check new ("ws://localhost:22103/"); check wsClient->writeMessage({event: "subscribe", invalidField: "test"}); string res = check wsClient->readMessage(); diff --git a/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/websocket/compiler/WebSocketServiceValidationTest.java b/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/websocket/compiler/WebSocketServiceValidationTest.java index d6df9994d..5c78b8d39 100644 --- a/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/websocket/compiler/WebSocketServiceValidationTest.java +++ b/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/websocket/compiler/WebSocketServiceValidationTest.java @@ -556,7 +556,7 @@ public void testDispatcherStreamIdWithoutDispatcherKey() { } @Test - public void testDispatcherConfigAnnotation() { + public void testDispatcherMappingAnnotation() { Package currentPackage = loadPackage("sample_package_63"); PackageCompilation compilation = currentPackage.getCompilation(); DiagnosticResult diagnosticResult = compilation.diagnosticResult(); @@ -564,7 +564,7 @@ public void testDispatcherConfigAnnotation() { Diagnostic firstDiagnostic = (Diagnostic) diagnosticResult.errors().toArray()[0]; assertDiagnostic(firstDiagnostic, PluginConstants.CompilationErrors.RE_DECLARED_REMOTE_FUNCTIONS); Diagnostic secondDiagnostic = (Diagnostic) diagnosticResult.errors().toArray()[1]; - assertDiagnostic(secondDiagnostic, PluginConstants.CompilationErrors.DUPLICATED_DISPATCHER_CONFIG_VALUE); + assertDiagnostic(secondDiagnostic, PluginConstants.CompilationErrors.DUPLICATED_DISPATCHER_MAPPING_VALUE); Diagnostic thirdDiagnostic = (Diagnostic) diagnosticResult.errors().toArray()[2]; assertDiagnostic(thirdDiagnostic, PluginConstants.CompilationErrors.INVALID_FUNCTION_ANNOTATION); } diff --git a/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_package_63/server.bal b/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_package_63/server.bal index e2bc0443a..28af03ec5 100644 --- a/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_package_63/server.bal +++ b/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_package_63/server.bal @@ -37,21 +37,21 @@ service class WsService { return "onSubscribe"; } - @websocket:DispatcherConfig { + @websocket:DispatcherMapping { value: "subscribe" } remote function onSubscribeMessage(Subscribe message) returns string { return "onSubscribeMessage"; } - @websocket:DispatcherConfig { + @websocket:DispatcherMapping { value: "subscribe" } remote function onSubscribeText(Subscribe message) returns string { return "onSubscribeText"; } - @websocket:DispatcherConfig { + @websocket:DispatcherMapping { value: "ping" } remote function onPing(Subscribe message) returns string { diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/PluginConstants.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/PluginConstants.java index fd4017d30..904548ea5 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/PluginConstants.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/PluginConstants.java @@ -80,7 +80,7 @@ public enum CompilationErrors { "WEBSOCKET_216"), RE_DECLARED_REMOTE_FUNCTIONS("Cannot have `{0}` because the message type `{1}` is already " + "associated with `{2}` remote function", "WEBSOCKET_217"), - DUPLICATED_DISPATCHER_CONFIG_VALUE("DispatcherConfig annotation value `{0}` is already " + + DUPLICATED_DISPATCHER_MAPPING_VALUE("DispatcherMapping annotation value `{0}` is already " + "exists", "WEBSOCKET_218"), INVALID_FUNCTION_ANNOTATION("Invalid annotation provided for `{0}` remote function", "WEBSOCKET_219"), INVALID_RESOURCE_ERROR("There should be only one `get` resource for the service", diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/WebSocketServiceValidator.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/WebSocketServiceValidator.java index e1ffdcd6f..bab47f808 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/WebSocketServiceValidator.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/WebSocketServiceValidator.java @@ -45,7 +45,7 @@ import java.util.stream.Collectors; import static io.ballerina.stdlib.websocket.WebSocketConstants.ANNOTATION_ATTR_DISPATCHER_VALUE; -import static io.ballerina.stdlib.websocket.plugin.PluginConstants.CompilationErrors.DUPLICATED_DISPATCHER_CONFIG_VALUE; +import static io.ballerina.stdlib.websocket.plugin.PluginConstants.CompilationErrors.DUPLICATED_DISPATCHER_MAPPING_VALUE; import static io.ballerina.stdlib.websocket.plugin.PluginConstants.CompilationErrors.INVALID_FUNCTION_ANNOTATION; import static io.ballerina.stdlib.websocket.plugin.PluginConstants.CompilationErrors.RE_DECLARED_REMOTE_FUNCTIONS; @@ -131,10 +131,10 @@ public void validate() { if (node instanceof FunctionDefinitionNode funcDefinitionNode) { String funcName = funcDefinitionNode.functionName().toString(); Optional annoDispatchingValue = - getDispatcherConfigAnnotatedFunctionName(funcDefinitionNode, ctx); + getDispatcherMappingAnnotatedFunctionName(funcDefinitionNode, ctx); if (annoDispatchingValue.isPresent()) { if (seenAnnotationValues.contains(annoDispatchingValue.get())) { - Utils.reportDiagnostics(ctx, DUPLICATED_DISPATCHER_CONFIG_VALUE, + Utils.reportDiagnostics(ctx, DUPLICATED_DISPATCHER_MAPPING_VALUE, funcDefinitionNode.location(), annoDispatchingValue.get()); } else { seenAnnotationValues.add(annoDispatchingValue.get()); @@ -154,8 +154,8 @@ public void validate() { } } - private static Optional getDispatcherConfigAnnotatedFunctionName(FunctionDefinitionNode node, - SyntaxNodeAnalysisContext ctx) { + private static Optional getDispatcherMappingAnnotatedFunctionName(FunctionDefinitionNode node, + SyntaxNodeAnalysisContext ctx) { if (node.metadata().isEmpty()) { return Optional.empty(); } @@ -167,7 +167,7 @@ private static Optional getDispatcherConfigAnnotatedFunctionName(Functio if (!annotationType.get().getModule().flatMap(Symbol::getName) .orElse("").equals(WebSocketConstants.PACKAGE_WEBSOCKET) || !annotationType.get().getName().orElse("") - .equals(WebSocketConstants.WEBSOCKET_DISPATCHER_CONFIG_ANNOTATION)) { + .equals(WebSocketConstants.WEBSOCKET_DISPATCHER_MAPPING_ANNOTATION)) { continue; } if (annotationNode.annotValue().isEmpty()) { diff --git a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketConstants.java b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketConstants.java index 95c7aacd9..a6a7575b2 100644 --- a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketConstants.java +++ b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketConstants.java @@ -51,7 +51,7 @@ public class WebSocketConstants { public static final BString ANNOTATION_ATTR_VALIDATION_ENABLED = StringUtils.fromString("validation"); public static final BString ANNOTATION_ATTR_DISPATCHER_KEY = StringUtils.fromString("dispatcherKey"); - public static final String WEBSOCKET_DISPATCHER_CONFIG_ANNOTATION = "DispatcherConfig"; + public static final String WEBSOCKET_DISPATCHER_MAPPING_ANNOTATION = "DispatcherMapping"; public static final String ANNOTATION_ATTR_DISPATCHER_VALUE = "value"; public static final BString RETRY_CONFIG = StringUtils.fromString("retryConfig"); diff --git a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketService.java b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketService.java index da48d5cd2..38ce2c357 100644 --- a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketService.java +++ b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketService.java @@ -71,7 +71,7 @@ private Map getDispatchingFunctionMap(Object dispatchingServ Map dispatchingFunctions = new ConcurrentHashMap<>(); Set seenRemoteFunctionNames = new HashSet<>(); MethodType[] remoteFunctions = ((ServiceType) (((BValue) dispatchingService).getType())).getMethods(); - // 1. DispatcherConfig values have the highest priority + // 1. DispatcherMapping values have the highest priority for (MethodType remoteFunc : remoteFunctions) { Optional dispatchingValue = getAnnotationDispatchingValue(remoteFunc); if (dispatchingValue.isPresent()) { @@ -91,7 +91,7 @@ private Map getDispatchingFunctionMap(Object dispatchingServ @SuppressWarnings(UNCHECKED) public static Optional getAnnotationDispatchingValue(MethodType remoteFunc) { BMap annotations = (BMap) remoteFunc.getAnnotation(fromString( - ModuleUtils.getPackageIdentifier() + ":" + WebSocketConstants.WEBSOCKET_DISPATCHER_CONFIG_ANNOTATION)); + ModuleUtils.getPackageIdentifier() + ":" + WebSocketConstants.WEBSOCKET_DISPATCHER_MAPPING_ANNOTATION)); if (annotations != null && annotations.containsKey(fromString(ANNOTATION_ATTR_DISPATCHER_VALUE))) { String dispatchingValue = annotations. getStringValue(fromString(ANNOTATION_ATTR_DISPATCHER_VALUE)).getValue(); From cafc5228233fd72e42f5d3090f994b986825d797 Mon Sep 17 00:00:00 2001 From: ayash Date: Thu, 3 Apr 2025 11:46:18 +0530 Subject: [PATCH 17/38] Fix missing closing brace in WebSocketServiceValidationTest --- .../websocket/compiler/WebSocketServiceValidationTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/websocket/compiler/WebSocketServiceValidationTest.java b/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/websocket/compiler/WebSocketServiceValidationTest.java index 870d52f53..193ad2253 100644 --- a/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/websocket/compiler/WebSocketServiceValidationTest.java +++ b/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/websocket/compiler/WebSocketServiceValidationTest.java @@ -570,6 +570,7 @@ public void testRemoteFunctionWithStreamAndCloseFrameReturnTypes() { PackageCompilation compilation = currentPackage.getCompilation(); DiagnosticResult diagnosticResult = compilation.diagnosticResult(); Assert.assertEquals(diagnosticResult.errorCount(), 0); + } @Test public void testDispatcherMappingAnnotation() { From 3e7a192363491929a20e8b098b4e9f3c79518922 Mon Sep 17 00:00:00 2001 From: ayash Date: Thu, 3 Apr 2025 11:52:39 +0530 Subject: [PATCH 18/38] [Automated] Update the native jar versions --- ballerina/Ballerina.toml | 8 ++++---- ballerina/CompilerPlugin.toml | 2 +- ballerina/Dependencies.toml | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ballerina/Ballerina.toml b/ballerina/Ballerina.toml index 3a9267535..826b7eee2 100644 --- a/ballerina/Ballerina.toml +++ b/ballerina/Ballerina.toml @@ -1,7 +1,7 @@ [package] org = "ballerina" name = "websocket" -version = "2.14.1" +version = "2.15.0" authors = ["Ballerina"] keywords = ["ws", "network", "bi-directional", "streaming", "service", "client"] repository = "https://github.com/ballerina-platform/module-ballerina-websocket" @@ -15,8 +15,8 @@ graalvmCompatible = true [[platform.java21.dependency]] groupId = "io.ballerina.stdlib" artifactId = "websocket-native" -version = "2.14.1" -path = "../native/build/libs/websocket-native-2.14.1-SNAPSHOT.jar" +version = "2.15.0" +path = "../native/build/libs/websocket-native-2.15.0-SNAPSHOT.jar" [[platform.java21.dependency]] groupId = "io.ballerina.stdlib" @@ -85,5 +85,5 @@ version = "4.1.118.Final" path = "./lib/netty-handler-proxy-4.1.118.Final.jar" [[platform.java21.dependency]] -path = "../test-utils/build/libs/websocket-test-utils-2.14.1-SNAPSHOT.jar" +path = "../test-utils/build/libs/websocket-test-utils-2.15.0-SNAPSHOT.jar" scope = "testOnly" diff --git a/ballerina/CompilerPlugin.toml b/ballerina/CompilerPlugin.toml index f0c2e2e19..35c0f6193 100644 --- a/ballerina/CompilerPlugin.toml +++ b/ballerina/CompilerPlugin.toml @@ -3,4 +3,4 @@ id = "websocket-compiler-plugin" class = "io.ballerina.stdlib.websocket.plugin.WebSocketCompilerPlugin" [[dependency]] -path = "../compiler-plugin/build/libs/websocket-compiler-plugin-2.14.1-SNAPSHOT.jar" +path = "../compiler-plugin/build/libs/websocket-compiler-plugin-2.15.0-SNAPSHOT.jar" diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index bf4b4e276..729ac223f 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -342,7 +342,7 @@ dependencies = [ [[package]] org = "ballerina" name = "websocket" -version = "2.14.1" +version = "2.15.0" dependencies = [ {org = "ballerina", name = "auth"}, {org = "ballerina", name = "constraint"}, From 4bd2f4be1cd37ecbd4796685578967cb3eaf9df1 Mon Sep 17 00:00:00 2001 From: ayash Date: Thu, 3 Apr 2025 12:02:09 +0530 Subject: [PATCH 19/38] Bump version to 2.15.0 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index b6314f160..28023d5be 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ org.gradle.caching=true group=io.ballerina.stdlib -version=2.14.1-SNAPSHOT +version=2.15.0-SNAPSHOT ballerinaLangVersion=2201.12.0 ballerinaTomlParserVersion=1.2.2 nettyVersion=4.1.118.Final From 80681d7bc5dfcc89167f7366944162498db5b08d Mon Sep 17 00:00:00 2001 From: ayash Date: Thu, 3 Apr 2025 14:26:21 +0530 Subject: [PATCH 20/38] [Automated] Update the native jar versions --- ballerina/CompilerPlugin.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ballerina/CompilerPlugin.toml b/ballerina/CompilerPlugin.toml index 35c0f6193..fc5ed938f 100644 --- a/ballerina/CompilerPlugin.toml +++ b/ballerina/CompilerPlugin.toml @@ -4,3 +4,9 @@ class = "io.ballerina.stdlib.websocket.plugin.WebSocketCompilerPlugin" [[dependency]] path = "../compiler-plugin/build/libs/websocket-compiler-plugin-2.15.0-SNAPSHOT.jar" + +[[dependency]] +path = "../native/build/libs/websocket-native-2.15.0-SNAPSHOT.jar" + +[[dependency]] +path = "./lib/http-native-2.14.0.jar" From d34b57f91840eec3375ab78b6661afa426a4ba04 Mon Sep 17 00:00:00 2001 From: ayash Date: Thu, 3 Apr 2025 14:35:13 +0530 Subject: [PATCH 21/38] Reuse createCustomRemoteFunction method and update dependencies required for the function --- ballerina/build.gradle | 1 + build-config/resources/CompilerPlugin.toml | 6 ++++++ .../plugin/WebSocketServiceValidator.java | 19 +------------------ .../WebSocketResourceDispatcher.java | 2 +- 4 files changed, 9 insertions(+), 19 deletions(-) diff --git a/ballerina/build.gradle b/ballerina/build.gradle index fac0dc139..6f3c8ed43 100644 --- a/ballerina/build.gradle +++ b/ballerina/build.gradle @@ -122,6 +122,7 @@ task updateTomlFiles { ballerinaTomlFile.text = newConfig def newPluginConfig = compilerPluginTomlFilePlaceHolder.text.replace("@project.version@", project.version) + newPluginConfig = newPluginConfig.replace("@stdlib.httpnative.version@", stdlibDependentHttpNativeVersion) compilerPluginTomlFile.text = newPluginConfig } } diff --git a/build-config/resources/CompilerPlugin.toml b/build-config/resources/CompilerPlugin.toml index b5a86c280..4b06f7c63 100644 --- a/build-config/resources/CompilerPlugin.toml +++ b/build-config/resources/CompilerPlugin.toml @@ -4,3 +4,9 @@ class = "io.ballerina.stdlib.websocket.plugin.WebSocketCompilerPlugin" [[dependency]] path = "../compiler-plugin/build/libs/websocket-compiler-plugin-@project.version@.jar" + +[[dependency]] +path = "../native/build/libs/websocket-native-@project.version@.jar" + +[[dependency]] +path = "./lib/http-native-@stdlib.httpnative.version@.jar" diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/WebSocketServiceValidator.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/WebSocketServiceValidator.java index bab47f808..b9ed5b380 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/WebSocketServiceValidator.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/WebSocketServiceValidator.java @@ -38,13 +38,13 @@ import io.ballerina.tools.diagnostics.DiagnosticSeverity; import java.util.HashSet; -import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import static io.ballerina.stdlib.websocket.WebSocketConstants.ANNOTATION_ATTR_DISPATCHER_VALUE; +import static io.ballerina.stdlib.websocket.WebSocketResourceDispatcher.createCustomRemoteFunction; import static io.ballerina.stdlib.websocket.plugin.PluginConstants.CompilationErrors.DUPLICATED_DISPATCHER_MAPPING_VALUE; import static io.ballerina.stdlib.websocket.plugin.PluginConstants.CompilationErrors.INVALID_FUNCTION_ANNOTATION; import static io.ballerina.stdlib.websocket.plugin.PluginConstants.CompilationErrors.RE_DECLARED_REMOTE_FUNCTIONS; @@ -189,23 +189,6 @@ private static Optional getDispatcherMappingAnnotatedFunctionName(Functi return Optional.empty(); } - private static String createCustomRemoteFunction(String dispatchingValue) { - dispatchingValue = "on " + dispatchingValue; - StringBuilder builder = new StringBuilder(); - String[] words = dispatchingValue.split("[\\W_]+"); - for (int i = 0; i < words.length; i++) { - String word = words[i]; - if (i == 0) { - word = word.isEmpty() ? word : word.toLowerCase(Locale.ENGLISH); - } else { - word = word.isEmpty() ? word : Character.toUpperCase(word.charAt(0)) + word.substring(1) - .toLowerCase(Locale.ENGLISH); - } - builder.append(word); - } - return builder.toString(); - } - private void filterRemoteFunctions(FunctionDefinitionNode functionDefinitionNode) { FunctionTypeSymbol functionTypeSymbol = ((MethodSymbol) ctx.semanticModel().symbol(functionDefinitionNode) .get()).typeDescriptor(); diff --git a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketResourceDispatcher.java b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketResourceDispatcher.java index 7cf6b20a3..d9b2f7165 100644 --- a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketResourceDispatcher.java +++ b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketResourceDispatcher.java @@ -570,7 +570,7 @@ private static Optional getDispatchingValue(String dispatchingKey, }); } - private static String createCustomRemoteFunction(String dispatchingValue) { + public static String createCustomRemoteFunction(String dispatchingValue) { dispatchingValue = "on " + dispatchingValue; StringBuilder builder = new StringBuilder(); String[] words = dispatchingValue.split("[\\W_]+"); From 5bde2e2083199172eafda9fe032fb82a8d820f6b Mon Sep 17 00:00:00 2001 From: ayash Date: Thu, 3 Apr 2025 14:37:16 +0530 Subject: [PATCH 22/38] Fix typo in WebSocketServiceValidator --- .../stdlib/websocket/plugin/WebSocketServiceValidator.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/WebSocketServiceValidator.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/WebSocketServiceValidator.java index b9ed5b380..ca0f75a80 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/WebSocketServiceValidator.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/WebSocketServiceValidator.java @@ -176,9 +176,9 @@ private static Optional getDispatcherMappingAnnotatedFunctionName(Functi MappingConstructorExpressionNode annotationValue = annotationNode.annotValue().get(); for (Node field : annotationValue.fields()) { if (field instanceof SpecificFieldNode specificFieldNode) { - String filedName = specificFieldNode.fieldName().toString().strip(); + String fieldName = specificFieldNode.fieldName().toString().strip(); Optional filedValue = specificFieldNode.valueExpr(); - if (filedName.equals(ANNOTATION_ATTR_DISPATCHER_VALUE) && + if (fieldName.equals(ANNOTATION_ATTR_DISPATCHER_VALUE) && filedValue.isPresent()) { return Optional.of(filedValue.get().toString().strip() .replaceAll("\"", "")); From 9ffecc6a1e893436a10f1dc04c4e20b141fc60e7 Mon Sep 17 00:00:00 2001 From: ayash Date: Fri, 4 Apr 2025 07:16:02 +0530 Subject: [PATCH 23/38] Update the spec --- docs/spec/spec.md | 85 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 60 insertions(+), 25 deletions(-) diff --git a/docs/spec/spec.md b/docs/spec/spec.md index 53fe9f17b..71057fe9a 100644 --- a/docs/spec/spec.md +++ b/docs/spec/spec.md @@ -35,7 +35,7 @@ The conforming implementation of the specification is released and included in t * [onClose](#onclose) * [onError](#onerror) * 3.2.2. [Dispatching custom remote methods](#322-dispatching-custom-remote-methods) - * [Dispatching custom error remote methods](#Dispatching custom error remote methods) + * [Dispatching custom error remote methods](#dispatching-custom-error-remote-methods) * 3.2.3. [Return types](#323-return-types) 4. [Client](#4-client) * 4.1. [Client Configurations](#41-client-configurations) @@ -358,45 +358,80 @@ For example, if the message is `{"event": "heartbeat"}` it will get dispatched t 1. The user can configure the field name(key) to identify the messages and the allowed values as message types. -The `dispatcherKey` is used to identify the event type of the incoming message by its value. -The `dispatcherStreamId` is used to distinguish between requests and their corresponding responses in a multiplexing scenario. + The `dispatcherKey` is used to identify the event type of the incoming message by its value. The `dispatcherStreamId` is used to distinguish between requests and their corresponding responses in a multiplexing scenario. -```ballerina + ```ballerina + Ex: + incoming message = ` {"event": "heartbeat", "id": "1"}` + dispatcherKey = "event" + dispatcherStreamId = "id" + event/message type = "heartbeat" + dispatching to remote function = "onHeartbeat" -Ex: -incoming message = ` {"event": "heartbeat", "id": "1"}` -dispatcherKey = "event" -dispatcherStreamId = "id" -event/message type = "heartbeat" -dispatching to remote function = "onHeartbeat" + ```ballerina + @websocket:ServiceConfig { + dispatcherKey: "event", + dispatcherStreamId: "id" + } + service / on new websocket:Listener(9090) {} + ``` -```ballerina -@websocket:ServiceConfig { - dispatcherKey: "event", - dispatcherStreamId: "id" -} -service / on new websocket:Listener(9090) {} -``` +2. Naming of the remote function. + + * If there are spaces and underscores between message types, those will be removed and made camel case("un subscribe" -> "onUnSubscribe"). + * The 'on' word is added as the predecessor and the remote function name is in the camel case("heartbeat" -> "onHeartbeat"). + +3. Custom Dispatching with `@DispatcherMapping` annotation + + The `@DispatcherMapping` annotation allows users to explicitly define the dispatching behavior for remote functions. If an incoming message type matches the value in the annotation, the respective remote function will be invoked. + + ```ballerina + @DispatcherMapping { + value: "subscribe" + } + remote function onSubscribeMessage(Subscribe message) returns string { + return "onSubscribeMessage"; + } + ``` + + In this case, when a message of type "subscribe" is received, the `onSubscribeMessage` remote function is invoked. -##### [Dispatching custom error remote methods](#Dispatching custom error remote methods) +4. If an unmatching message type receives where a matching remote function is not implemented in the WebSocket service by the user, it gets dispatched to the default `onMessage` remote function if it is implemented. Or else it will get ignored. + +##### [Dispatching custom error remote methods](#dispatching-custom-error-remote-methods) If the user has defined a remote function with the name `customRemoteFunction` + `Error` in the WebSocket service, the error messages will get dispatched to that remote function when there is a data binding error. If that is not defined, the generic `onError` remote function gets dispatched. +* Example 1 + ```ballerina -Ex: +remote function onHeartbeat(Heartbeat message) returns error? { +} + +remote function onHeartbeatError(error message) returns error? { +} + incoming message = ` {"event": "heartbeat"}` -dispatcherKey = "event" -event/message type = "heartbeat" dispatching remote function = "onHeartbeat" dispatching error remote function = "onHeartbeatError" ``` -2. Naming of the remote function. +* Example 2 -- If there are spaces and underscores between message types, those will be removed and made camel case("un subscribe" -> "onUnSubscribe"). -- The 'on' word is added as the predecessor and the remote function name is in the camel case("heartbeat" -> "onHeartbeat"). +```ballerina +@websocket:DispatcherMapping { + value: "subscribe" +} +remote function onSubscribeMessage(Subscribe message) returns error? { +} -3. If an unmatching message type receives where a matching remote function is not implemented in the WebSocket service by the user, it gets dispatched to the default `onMessage` remote function if it is implemented. Or else it will get ignored. +remote function onSubscribeMessageError(error message) returns error? { +} + +incoming message = ` {"event": "subscribe"}` +dispatching remote function = "onSubscribeMessage" +dispatching error remote function = "onSubscribeMessageError" +``` #### 3.2.3. Return types From 13839d090600c8beacc09c7928469a35a6346b8c Mon Sep 17 00:00:00 2001 From: ayash Date: Fri, 4 Apr 2025 08:48:59 +0530 Subject: [PATCH 24/38] Remove unnecessary comment --- .../java/io/ballerina/stdlib/websocket/WebSocketService.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketService.java b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketService.java index 38ce2c357..fe2f3728b 100644 --- a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketService.java +++ b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketService.java @@ -71,7 +71,6 @@ private Map getDispatchingFunctionMap(Object dispatchingServ Map dispatchingFunctions = new ConcurrentHashMap<>(); Set seenRemoteFunctionNames = new HashSet<>(); MethodType[] remoteFunctions = ((ServiceType) (((BValue) dispatchingService).getType())).getMethods(); - // 1. DispatcherMapping values have the highest priority for (MethodType remoteFunc : remoteFunctions) { Optional dispatchingValue = getAnnotationDispatchingValue(remoteFunc); if (dispatchingValue.isPresent()) { @@ -79,7 +78,6 @@ private Map getDispatchingFunctionMap(Object dispatchingServ seenRemoteFunctionNames.add(remoteFunc.getName()); } } - // 2. Custom remote function names have the next priority for (MethodType remoteFunc : remoteFunctions) { if (!seenRemoteFunctionNames.contains(remoteFunc.getName())) { dispatchingFunctions.put(remoteFunc.getName(), remoteFunc); From a7e6fe4315856e3adbc8f558416937323cd2651e Mon Sep 17 00:00:00 2001 From: ayash Date: Fri, 4 Apr 2025 09:24:32 +0530 Subject: [PATCH 25/38] Use alias for websocket test --- .../sample_package_63/server.bal | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_package_63/server.bal b/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_package_63/server.bal index 28af03ec5..3f528a2de 100644 --- a/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_package_63/server.bal +++ b/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_package_63/server.bal @@ -14,44 +14,44 @@ // specific language governing permissions and limitations // under the License. -import ballerina/websocket; +import ballerina/websocket as ws; type Subscribe record {| string event = "subscribe"; string data; |}; -@websocket:ServiceConfig { +@ws:ServiceConfig { dispatcherKey: "event" } -service / on new websocket:Listener(9090) { - resource function get .() returns websocket:Service|websocket:UpgradeError { +service / on new ws:Listener(9090) { + resource function get .() returns ws:Service|ws:UpgradeError { return new WsService(); } } service class WsService { - *websocket:Service; + *ws:Service; remote function onSubscribe(Subscribe message) returns string { return "onSubscribe"; } - @websocket:DispatcherMapping { + @ws:DispatcherMapping { value: "subscribe" } remote function onSubscribeMessage(Subscribe message) returns string { return "onSubscribeMessage"; } - @websocket:DispatcherMapping { + @ws:DispatcherMapping { value: "subscribe" } remote function onSubscribeText(Subscribe message) returns string { return "onSubscribeText"; } - @websocket:DispatcherMapping { + @ws:DispatcherMapping { value: "ping" } remote function onPing(Subscribe message) returns string { From 1c8a1142a276195da3ce98abffa9d81354515787 Mon Sep 17 00:00:00 2001 From: ayash Date: Fri, 4 Apr 2025 09:25:38 +0530 Subject: [PATCH 26/38] Refactor WebSocketServiceValidator to use semanticModel for getting a function name --- .../websocket/plugin/WebSocketServiceValidator.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/WebSocketServiceValidator.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/WebSocketServiceValidator.java index ca0f75a80..67a3e4322 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/WebSocketServiceValidator.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/WebSocketServiceValidator.java @@ -129,24 +129,24 @@ public void validate() { Set seenAnnotationValues = new HashSet<>(); for (Node node : classDefNode.members()) { if (node instanceof FunctionDefinitionNode funcDefinitionNode) { - String funcName = funcDefinitionNode.functionName().toString(); + Optional funcName = ctx.semanticModel().symbol(funcDefinitionNode).flatMap(Symbol::getName); Optional annoDispatchingValue = getDispatcherMappingAnnotatedFunctionName(funcDefinitionNode, ctx); - if (annoDispatchingValue.isPresent()) { + if (funcName.isPresent() && annoDispatchingValue.isPresent()) { if (seenAnnotationValues.contains(annoDispatchingValue.get())) { Utils.reportDiagnostics(ctx, DUPLICATED_DISPATCHER_MAPPING_VALUE, funcDefinitionNode.location(), annoDispatchingValue.get()); } else { seenAnnotationValues.add(annoDispatchingValue.get()); String customRemoteFunctionName = createCustomRemoteFunction(annoDispatchingValue.get()); - if (specialRemoteMethods.contains(funcName)) { + if (specialRemoteMethods.contains(funcName.get())) { Utils.reportDiagnostics(ctx, INVALID_FUNCTION_ANNOTATION, funcDefinitionNode.location(), - funcName); + funcName.get()); } else if (functionSet.containsKey(customRemoteFunctionName) && - !customRemoteFunctionName.equals(funcName) && + !customRemoteFunctionName.equals(funcName.get()) && !specialRemoteMethods.contains(customRemoteFunctionName)) { Utils.reportDiagnostics(ctx, RE_DECLARED_REMOTE_FUNCTIONS, classDefNode.location(), - customRemoteFunctionName, annoDispatchingValue.get(), funcName); + customRemoteFunctionName, annoDispatchingValue.get(), funcName.get()); } } } From 6ad3ef1f8328b9fd5ca75e3f3a9c9a21d7cf8a7e Mon Sep 17 00:00:00 2001 From: ayash Date: Fri, 4 Apr 2025 09:37:49 +0530 Subject: [PATCH 27/38] Rename dispatcherConfig to dispatcherMapping --- ..._annotation.bal => test_dispatcher_mapping_annotation.bal} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename ballerina/tests/{test_dispatcher_config_annotation.bal => test_dispatcher_mapping_annotation.bal} (95%) diff --git a/ballerina/tests/test_dispatcher_config_annotation.bal b/ballerina/tests/test_dispatcher_mapping_annotation.bal similarity index 95% rename from ballerina/tests/test_dispatcher_config_annotation.bal rename to ballerina/tests/test_dispatcher_mapping_annotation.bal index d53d32151..f31b37496 100644 --- a/ballerina/tests/test_dispatcher_config_annotation.bal +++ b/ballerina/tests/test_dispatcher_mapping_annotation.bal @@ -46,7 +46,7 @@ service class WsService22103 { } @test:Config { - groups: ["dispatcherConfigAnnotation"] + groups: ["dispatcherMappingAnnotation"] } public function testDispatcherMappingAnnotation() returns error? { Client wsClient = check new ("ws://localhost:22103/"); @@ -56,7 +56,7 @@ public function testDispatcherMappingAnnotation() returns error? { } @test:Config { - groups: ["dispatcherConfigAnnotation"] + groups: ["dispatcherMappingAnnotation"] } public function testDispatcherMappingAnnotationWithCustomOnError() returns error? { Client wsClient = check new ("ws://localhost:22103/"); From 0ef3e160ee28f5aebe01f86b3cc2ed6f90dae7f3 Mon Sep 17 00:00:00 2001 From: ayash Date: Fri, 4 Apr 2025 12:03:14 +0530 Subject: [PATCH 28/38] Update changelog --- changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.md b/changelog.md index 22ce616e3..9505f4a00 100644 --- a/changelog.md +++ b/changelog.md @@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Added +- [Support Custom Remote Function Mapping via Annotation](https://github.com/ballerina-platform/ballerina-library/issues/7733) - [Implement websocket close frame support](https://github.com/ballerina-platform/ballerina-library/issues/7578) ### Fixed From f92a9cd546c16a72024fe45de96f7abb4ee6f726 Mon Sep 17 00:00:00 2001 From: ayash Date: Fri, 4 Apr 2025 21:54:13 +0530 Subject: [PATCH 29/38] Remove if else ladders and use kind check instead of instanceof --- .../plugin/WebSocketServiceValidator.java | 110 ++++++++++-------- 1 file changed, 59 insertions(+), 51 deletions(-) diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/WebSocketServiceValidator.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/WebSocketServiceValidator.java index 67a3e4322..b39727853 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/WebSocketServiceValidator.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/WebSocketServiceValidator.java @@ -64,6 +64,41 @@ public class WebSocketServiceValidator { this.ctx = syntaxNodeAnalysisContext; } + private static Optional getDispatcherMappingAnnotatedFunctionName(FunctionDefinitionNode node, + SyntaxNodeAnalysisContext ctx) { + if (node.metadata().isEmpty()) { + return Optional.empty(); + } + for (AnnotationNode annotationNode : node.metadata().get().annotations()) { + Optional annotationType = ctx.semanticModel().symbol(annotationNode); + if (annotationType.isEmpty()) { + continue; + } + if (!annotationType.get().getModule().flatMap(Symbol::getName) + .orElse("").equals(WebSocketConstants.PACKAGE_WEBSOCKET) || + !annotationType.get().getName().orElse("") + .equals(WebSocketConstants.WEBSOCKET_DISPATCHER_MAPPING_ANNOTATION)) { + continue; + } + if (annotationNode.annotValue().isEmpty()) { + return Optional.empty(); + } + MappingConstructorExpressionNode annotationValue = annotationNode.annotValue().get(); + for (Node field : annotationValue.fields()) { + if (!field.kind().equals(SyntaxKind.SPECIFIC_FIELD)) { + continue; + } + String fieldName = ((SpecificFieldNode) field).fieldName().toString().strip(); + Optional filedValue = ((SpecificFieldNode) field).valueExpr(); + if (!fieldName.equals(ANNOTATION_ATTR_DISPATCHER_VALUE) || filedValue.isEmpty()) { + continue; + } + return Optional.of(filedValue.get().toString().replaceAll("\"", "").strip()); + } + } + return Optional.empty(); + } + public void validate() { ClassDefinitionNode classDefNode = (ClassDefinitionNode) ctx.node(); Map functionSet = classDefNode.members().stream().filter(child -> @@ -125,68 +160,41 @@ public void validate() { !functionSet.containsKey(PluginConstants.ON_BINARY_MESSAGE)) { reportDiagnostic(classDefNode, PluginConstants.CompilationErrors.ON_MESSAGE_GENERATION_HINT); } + validateDispatcherMappingAnnotations(classDefNode, functionSet); + } + private void validateDispatcherMappingAnnotations(ClassDefinitionNode classDefNode, + Map functionSet) { Set seenAnnotationValues = new HashSet<>(); for (Node node : classDefNode.members()) { - if (node instanceof FunctionDefinitionNode funcDefinitionNode) { - Optional funcName = ctx.semanticModel().symbol(funcDefinitionNode).flatMap(Symbol::getName); - Optional annoDispatchingValue = - getDispatcherMappingAnnotatedFunctionName(funcDefinitionNode, ctx); - if (funcName.isPresent() && annoDispatchingValue.isPresent()) { - if (seenAnnotationValues.contains(annoDispatchingValue.get())) { - Utils.reportDiagnostics(ctx, DUPLICATED_DISPATCHER_MAPPING_VALUE, - funcDefinitionNode.location(), annoDispatchingValue.get()); - } else { - seenAnnotationValues.add(annoDispatchingValue.get()); - String customRemoteFunctionName = createCustomRemoteFunction(annoDispatchingValue.get()); - if (specialRemoteMethods.contains(funcName.get())) { - Utils.reportDiagnostics(ctx, INVALID_FUNCTION_ANNOTATION, funcDefinitionNode.location(), - funcName.get()); - } else if (functionSet.containsKey(customRemoteFunctionName) && - !customRemoteFunctionName.equals(funcName.get()) && - !specialRemoteMethods.contains(customRemoteFunctionName)) { - Utils.reportDiagnostics(ctx, RE_DECLARED_REMOTE_FUNCTIONS, classDefNode.location(), - customRemoteFunctionName, annoDispatchingValue.get(), funcName.get()); - } - } - } + if (!node.kind().equals(SyntaxKind.OBJECT_METHOD_DEFINITION)) { + continue; } - } - } - - private static Optional getDispatcherMappingAnnotatedFunctionName(FunctionDefinitionNode node, - SyntaxNodeAnalysisContext ctx) { - if (node.metadata().isEmpty()) { - return Optional.empty(); - } - for (AnnotationNode annotationNode : node.metadata().get().annotations()) { - Optional annotationType = ctx.semanticModel().symbol(annotationNode); - if (annotationType.isEmpty()) { + FunctionDefinitionNode funcDefinitionNode = (FunctionDefinitionNode) node; + Optional funcName = ctx.semanticModel().symbol(funcDefinitionNode).flatMap(Symbol::getName); + Optional annoDispatchingValue = getDispatcherMappingAnnotatedFunctionName(funcDefinitionNode, ctx); + if (funcName.isEmpty() || annoDispatchingValue.isEmpty()) { continue; } - if (!annotationType.get().getModule().flatMap(Symbol::getName) - .orElse("").equals(WebSocketConstants.PACKAGE_WEBSOCKET) || - !annotationType.get().getName().orElse("") - .equals(WebSocketConstants.WEBSOCKET_DISPATCHER_MAPPING_ANNOTATION)) { + if (seenAnnotationValues.contains(annoDispatchingValue.get())) { + Utils.reportDiagnostics(ctx, DUPLICATED_DISPATCHER_MAPPING_VALUE, + funcDefinitionNode.location(), annoDispatchingValue.get()); continue; } - if (annotationNode.annotValue().isEmpty()) { - return Optional.empty(); + seenAnnotationValues.add(annoDispatchingValue.get()); + String customRemoteFunctionName = createCustomRemoteFunction(annoDispatchingValue.get()); + if (specialRemoteMethods.contains(funcName.get())) { + Utils.reportDiagnostics(ctx, INVALID_FUNCTION_ANNOTATION, funcDefinitionNode.location(), + funcName.get()); + continue; } - MappingConstructorExpressionNode annotationValue = annotationNode.annotValue().get(); - for (Node field : annotationValue.fields()) { - if (field instanceof SpecificFieldNode specificFieldNode) { - String fieldName = specificFieldNode.fieldName().toString().strip(); - Optional filedValue = specificFieldNode.valueExpr(); - if (fieldName.equals(ANNOTATION_ATTR_DISPATCHER_VALUE) && - filedValue.isPresent()) { - return Optional.of(filedValue.get().toString().strip() - .replaceAll("\"", "")); - } - } + if (functionSet.containsKey(customRemoteFunctionName) && + !customRemoteFunctionName.equals(funcName.get()) && + !specialRemoteMethods.contains(customRemoteFunctionName)) { + Utils.reportDiagnostics(ctx, RE_DECLARED_REMOTE_FUNCTIONS, classDefNode.location(), + customRemoteFunctionName, annoDispatchingValue.get(), funcName.get()); } } - return Optional.empty(); } private void filterRemoteFunctions(FunctionDefinitionNode functionDefinitionNode) { From 56c07947999f5a9d840fab2578f31ad18836754e Mon Sep 17 00:00:00 2001 From: ayash Date: Mon, 7 Apr 2025 10:31:26 +0530 Subject: [PATCH 30/38] Update WsDispatcherMapping doc comment --- ballerina/annotation.bal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ballerina/annotation.bal b/ballerina/annotation.bal index baaad97e7..3e7aafd55 100644 --- a/ballerina/annotation.bal +++ b/ballerina/annotation.bal @@ -39,7 +39,7 @@ public type WSServiceConfig record {| # The annotation which is used to configure a WebSocket service. public annotation WSServiceConfig ServiceConfig on service; -# Configurations for WebSocket remote functions. +# Configurations used to define dispatching rules for remote functions. # # + value - The value which is going to be used for dispatching to custom remote functions. public type WsDispatcherMapping record {| From e8a82d26480ee1dbe28dee39a4c0a61edead19d2 Mon Sep 17 00:00:00 2001 From: ayash Date: Mon, 7 Apr 2025 11:45:23 +0530 Subject: [PATCH 31/38] Refactor WebSocketServiceValidator to use 'this' keyword for specialRemoteMethods --- .../stdlib/websocket/plugin/WebSocketServiceValidator.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/WebSocketServiceValidator.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/WebSocketServiceValidator.java index b39727853..3caeb63cb 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/WebSocketServiceValidator.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/WebSocketServiceValidator.java @@ -183,14 +183,14 @@ private void validateDispatcherMappingAnnotations(ClassDefinitionNode classDefNo } seenAnnotationValues.add(annoDispatchingValue.get()); String customRemoteFunctionName = createCustomRemoteFunction(annoDispatchingValue.get()); - if (specialRemoteMethods.contains(funcName.get())) { + if (this.specialRemoteMethods.contains(funcName.get())) { Utils.reportDiagnostics(ctx, INVALID_FUNCTION_ANNOTATION, funcDefinitionNode.location(), funcName.get()); continue; } if (functionSet.containsKey(customRemoteFunctionName) && !customRemoteFunctionName.equals(funcName.get()) && - !specialRemoteMethods.contains(customRemoteFunctionName)) { + !this.specialRemoteMethods.contains(customRemoteFunctionName)) { Utils.reportDiagnostics(ctx, RE_DECLARED_REMOTE_FUNCTIONS, classDefNode.location(), customRemoteFunctionName, annoDispatchingValue.get(), funcName.get()); } From 6d4fc9045dc30cb81e87f10adbba594be382e3f7 Mon Sep 17 00:00:00 2001 From: ayash Date: Mon, 7 Apr 2025 16:52:59 +0530 Subject: [PATCH 32/38] update getDispatchingFunctionMap method --- .../stdlib/websocket/WebSocketService.java | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketService.java b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketService.java index fe2f3728b..f399ef4c4 100644 --- a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketService.java +++ b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketService.java @@ -28,10 +28,8 @@ import io.ballerina.runtime.api.values.BString; import io.ballerina.runtime.api.values.BValue; -import java.util.HashSet; import java.util.Map; import java.util.Optional; -import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import static io.ballerina.runtime.api.utils.StringUtils.fromString; @@ -67,20 +65,14 @@ private void populateResourcesMap(BObject service) { } } - private Map getDispatchingFunctionMap(Object dispatchingService) { + private Map getDispatchingFunctionMap(ServiceType dispatchingService) { Map dispatchingFunctions = new ConcurrentHashMap<>(); - Set seenRemoteFunctionNames = new HashSet<>(); - MethodType[] remoteFunctions = ((ServiceType) (((BValue) dispatchingService).getType())).getMethods(); - for (MethodType remoteFunc : remoteFunctions) { - Optional dispatchingValue = getAnnotationDispatchingValue(remoteFunc); + for (MethodType method : dispatchingService.getMethods()) { + Optional dispatchingValue = getAnnotationDispatchingValue(method); if (dispatchingValue.isPresent()) { - dispatchingFunctions.put(dispatchingValue.get(), remoteFunc); - seenRemoteFunctionNames.add(remoteFunc.getName()); - } - } - for (MethodType remoteFunc : remoteFunctions) { - if (!seenRemoteFunctionNames.contains(remoteFunc.getName())) { - dispatchingFunctions.put(remoteFunc.getName(), remoteFunc); + dispatchingFunctions.put(dispatchingValue.get(), method); + } else { + dispatchingFunctions.put(method.getName(), method); } } return dispatchingFunctions; @@ -112,7 +104,8 @@ public Runtime getRuntime() { public void addWsService(String channelId, Object dispatchingService) { this.wsServices.put(channelId, dispatchingService); - this.wsServicesDispatchingFunctions.put(channelId, getDispatchingFunctionMap(dispatchingService)); + this.wsServicesDispatchingFunctions.put(channelId, + getDispatchingFunctionMap(((ServiceType) (((BValue) dispatchingService).getType())))); } public Object getWsService(String key) { From 57c4acc76dee4b74378d854a052dee030641180c Mon Sep 17 00:00:00 2001 From: ayash Date: Mon, 7 Apr 2025 20:54:05 +0530 Subject: [PATCH 33/38] Update INVALID_FUNCTION_ANNOTATION message to clarify usage with custom dispatcher functions --- .../io/ballerina/stdlib/websocket/plugin/PluginConstants.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/PluginConstants.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/PluginConstants.java index 508f94a88..3d15d5f45 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/PluginConstants.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/PluginConstants.java @@ -83,7 +83,8 @@ public enum CompilationErrors { "associated with `{2}` remote function", "WEBSOCKET_217"), DUPLICATED_DISPATCHER_MAPPING_VALUE("DispatcherMapping annotation value `{0}` is already " + "exists", "WEBSOCKET_218"), - INVALID_FUNCTION_ANNOTATION("Invalid annotation provided for `{0}` remote function", "WEBSOCKET_219"), + INVALID_FUNCTION_ANNOTATION("Invalid annotation provided for `{0}` remote function. " + + "This annotation can only be used with the custom dispatcher functions", "WEBSOCKET_219"), INVALID_RESOURCE_ERROR("There should be only one `get` resource for the service", "WEBSOCKET_101"), MORE_THAN_ONE_RESOURCE_PARAM_ERROR("There should be only http:Request as a parameter", From df9da2d43f95ab57ec0215e0841f3d7f55be9e38 Mon Sep 17 00:00:00 2001 From: ayash Date: Tue, 8 Apr 2025 10:25:15 +0530 Subject: [PATCH 34/38] update getDispatchingFunctionMap method to filter the remote methods --- .../WebSocketResourceDispatcher.java | 3 ++- .../stdlib/websocket/WebSocketService.java | 20 +++++++++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketResourceDispatcher.java b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketResourceDispatcher.java index d9b2f7165..419db04e3 100644 --- a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketResourceDispatcher.java +++ b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketResourceDispatcher.java @@ -28,6 +28,7 @@ import io.ballerina.runtime.api.types.ObjectType; import io.ballerina.runtime.api.types.Parameter; import io.ballerina.runtime.api.types.PredefinedTypes; +import io.ballerina.runtime.api.types.RemoteMethodType; import io.ballerina.runtime.api.types.ResourceMethodType; import io.ballerina.runtime.api.types.ServiceType; import io.ballerina.runtime.api.types.Type; @@ -414,7 +415,7 @@ public static void dispatchOnText(WebSocketConnectionInfo connectionInfo, WebSoc MethodType onTextMessageResource = null; BObject wsEndpoint = connectionInfo.getWebSocketEndpoint(); Object dispatchingService = wsService.getWsService(connectionInfo.getWebSocketConnection().getChannelId()); - Map dispatchingFunctions = wsService + Map dispatchingFunctions = wsService .getDispatchingFunctions(connectionInfo.getWebSocketConnection().getChannelId()); if (dispatchingValue.isPresent() && dispatchingFunctions.containsKey(dispatchingValue.get())) { onTextMessageResource = dispatchingFunctions.get(dispatchingValue.get()); diff --git a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketService.java b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketService.java index f399ef4c4..9fafbcc8b 100644 --- a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketService.java +++ b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketService.java @@ -21,6 +21,7 @@ import io.ballerina.runtime.api.Runtime; import io.ballerina.runtime.api.types.MethodType; import io.ballerina.runtime.api.types.ObjectType; +import io.ballerina.runtime.api.types.RemoteMethodType; import io.ballerina.runtime.api.types.ServiceType; import io.ballerina.runtime.api.utils.TypeUtils; import io.ballerina.runtime.api.values.BMap; @@ -45,7 +46,7 @@ public class WebSocketService { protected Runtime runtime; private final Map resourcesMap = new ConcurrentHashMap<>(); private Map wsServices = new ConcurrentHashMap<>(); - private Map> wsServicesDispatchingFunctions = new ConcurrentHashMap<>(); + private Map> wsServicesDispatchingFunctions = new ConcurrentHashMap<>(); public WebSocketService(Runtime runtime) { this.runtime = runtime; @@ -65,21 +66,24 @@ private void populateResourcesMap(BObject service) { } } - private Map getDispatchingFunctionMap(ServiceType dispatchingService) { - Map dispatchingFunctions = new ConcurrentHashMap<>(); + private Map getDispatchingFunctionMap(ServiceType dispatchingService) { + Map dispatchingFunctions = new ConcurrentHashMap<>(); for (MethodType method : dispatchingService.getMethods()) { - Optional dispatchingValue = getAnnotationDispatchingValue(method); + if (!(method instanceof RemoteMethodType remoteMethodType)) { + continue; + } + Optional dispatchingValue = getAnnotationDispatchingValue(remoteMethodType); if (dispatchingValue.isPresent()) { - dispatchingFunctions.put(dispatchingValue.get(), method); + dispatchingFunctions.put(dispatchingValue.get(), remoteMethodType); } else { - dispatchingFunctions.put(method.getName(), method); + dispatchingFunctions.put(remoteMethodType.getName(), remoteMethodType); } } return dispatchingFunctions; } @SuppressWarnings(UNCHECKED) - public static Optional getAnnotationDispatchingValue(MethodType remoteFunc) { + public static Optional getAnnotationDispatchingValue(RemoteMethodType remoteFunc) { BMap annotations = (BMap) remoteFunc.getAnnotation(fromString( ModuleUtils.getPackageIdentifier() + ":" + WebSocketConstants.WEBSOCKET_DISPATCHER_MAPPING_ANNOTATION)); if (annotations != null && annotations.containsKey(fromString(ANNOTATION_ATTR_DISPATCHER_VALUE))) { @@ -112,7 +116,7 @@ public Object getWsService(String key) { return this.wsServices.get(key); } - public Map getDispatchingFunctions(String key) { + public Map getDispatchingFunctions(String key) { return this.wsServicesDispatchingFunctions.get(key); } } From 3a3f72971634fa90b95c7de1c6cc3d21b4fdf545 Mon Sep 17 00:00:00 2001 From: ayash Date: Thu, 10 Apr 2025 07:11:57 +0530 Subject: [PATCH 35/38] Add logging to testConnectionClosureTimeoutCaller test --- ballerina/tests/connection_closure_timeout.bal | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ballerina/tests/connection_closure_timeout.bal b/ballerina/tests/connection_closure_timeout.bal index ec3fe4dde..a392e2bc0 100644 --- a/ballerina/tests/connection_closure_timeout.bal +++ b/ballerina/tests/connection_closure_timeout.bal @@ -16,6 +16,7 @@ import ballerina/lang.runtime; import ballerina/test; +import ballerina/io; map callers = {}; error negativeTimeoutErrorMessage = error(""); @@ -67,6 +68,7 @@ service class ConnectionClosureTimeoutService { groups: ["connectionClosureTimeout"] } public function testConnectionClosureTimeoutCaller() returns error? { + io:println("Testing connection closure timeout with caller"); Client wsClient1 = check new ("ws://localhost:22100/"); check wsClient1->writeMessage({event: "subscribe"}); runtime:sleep(8); @@ -76,6 +78,7 @@ public function testConnectionClosureTimeoutCaller() returns error? { check wsClient2->writeMessage({event: "is_closed", name: "onSubscribe"}); boolean isClosed = check wsClient2->readMessage(); test:assertTrue(isClosed); + io:println("Connection closure timeout test with caller passed"); } @test:Config { From ce9a8c7a5e1a449118dd425ef8ae7aab7ab157d3 Mon Sep 17 00:00:00 2001 From: ayash Date: Thu, 10 Apr 2025 19:36:04 +0530 Subject: [PATCH 36/38] Removed unnecessary prints --- ballerina/tests/connection_closure_timeout.bal | 3 --- 1 file changed, 3 deletions(-) diff --git a/ballerina/tests/connection_closure_timeout.bal b/ballerina/tests/connection_closure_timeout.bal index a392e2bc0..ec3fe4dde 100644 --- a/ballerina/tests/connection_closure_timeout.bal +++ b/ballerina/tests/connection_closure_timeout.bal @@ -16,7 +16,6 @@ import ballerina/lang.runtime; import ballerina/test; -import ballerina/io; map callers = {}; error negativeTimeoutErrorMessage = error(""); @@ -68,7 +67,6 @@ service class ConnectionClosureTimeoutService { groups: ["connectionClosureTimeout"] } public function testConnectionClosureTimeoutCaller() returns error? { - io:println("Testing connection closure timeout with caller"); Client wsClient1 = check new ("ws://localhost:22100/"); check wsClient1->writeMessage({event: "subscribe"}); runtime:sleep(8); @@ -78,7 +76,6 @@ public function testConnectionClosureTimeoutCaller() returns error? { check wsClient2->writeMessage({event: "is_closed", name: "onSubscribe"}); boolean isClosed = check wsClient2->readMessage(); test:assertTrue(isClosed); - io:println("Connection closure timeout test with caller passed"); } @test:Config { From 9dff3df3e95ab7d2f98d3ab0fb397b78eed2bc2f Mon Sep 17 00:00:00 2001 From: ayash Date: Fri, 11 Apr 2025 00:55:14 +0530 Subject: [PATCH 37/38] Validate remote qualifier in WebSocket service functions --- .../stdlib/websocket/plugin/WebSocketServiceValidator.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/WebSocketServiceValidator.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/WebSocketServiceValidator.java index 3caeb63cb..e283061f1 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/WebSocketServiceValidator.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/websocket/plugin/WebSocketServiceValidator.java @@ -171,6 +171,10 @@ private void validateDispatcherMappingAnnotations(ClassDefinitionNode classDefNo continue; } FunctionDefinitionNode funcDefinitionNode = (FunctionDefinitionNode) node; + if (funcDefinitionNode.qualifierList().stream() + .noneMatch(token -> token.text().equals(Qualifier.REMOTE.getValue()))) { + continue; + } Optional funcName = ctx.semanticModel().symbol(funcDefinitionNode).flatMap(Symbol::getName); Optional annoDispatchingValue = getDispatcherMappingAnnotatedFunctionName(funcDefinitionNode, ctx); if (funcName.isEmpty() || annoDispatchingValue.isEmpty()) { From e2b63e6c0b1e84dbc9b7ec8dbfd9084b31b68664 Mon Sep 17 00:00:00 2001 From: ayash Date: Fri, 11 Apr 2025 10:06:06 +0530 Subject: [PATCH 38/38] Refactor WebSocketService to validate remote method types using SymbolFlags --- .../java/io/ballerina/stdlib/websocket/WebSocketService.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketService.java b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketService.java index 9fafbcc8b..f6ae9a08a 100644 --- a/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketService.java +++ b/native/src/main/java/io/ballerina/stdlib/websocket/WebSocketService.java @@ -19,6 +19,7 @@ package io.ballerina.stdlib.websocket; import io.ballerina.runtime.api.Runtime; +import io.ballerina.runtime.api.flags.SymbolFlags; import io.ballerina.runtime.api.types.MethodType; import io.ballerina.runtime.api.types.ObjectType; import io.ballerina.runtime.api.types.RemoteMethodType; @@ -69,9 +70,10 @@ private void populateResourcesMap(BObject service) { private Map getDispatchingFunctionMap(ServiceType dispatchingService) { Map dispatchingFunctions = new ConcurrentHashMap<>(); for (MethodType method : dispatchingService.getMethods()) { - if (!(method instanceof RemoteMethodType remoteMethodType)) { + if (!(SymbolFlags.isFlagOn(method.getFlags(), SymbolFlags.REMOTE))) { continue; } + RemoteMethodType remoteMethodType = (RemoteMethodType) method; Optional dispatchingValue = getAnnotationDispatchingValue(remoteMethodType); if (dispatchingValue.isPresent()) { dispatchingFunctions.put(dispatchingValue.get(), remoteMethodType);