diff --git a/ballerina/tests/subscriber_with_readonly_method_params_test.bal b/ballerina/tests/subscriber_with_readonly_method_params_test.bal new file mode 100644 index 00000000..70d28d4c --- /dev/null +++ b/ballerina/tests/subscriber_with_readonly_method_params_test.bal @@ -0,0 +1,103 @@ +// Copyright (c) 2022 WSO2 Inc. (http://www.wso2.org) All Rights Reserved. +// +// WSO2 Inc. 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/log; +import ballerina/test; +import ballerina/mime; +import ballerina/http; + +@SubscriberServiceConfig {} +service /subscriber on new Listener(9104) { + isolated remote function onSubscriptionValidationDenied(readonly & SubscriptionDeniedError msg) returns Acknowledgement? { + test:assertTrue(msg is readonly); + return ACKNOWLEDGEMENT; + } + + isolated remote function onSubscriptionVerification(readonly & SubscriptionVerification msg) + returns SubscriptionVerificationSuccess|SubscriptionVerificationError { + test:assertTrue(msg is readonly); + if (msg.hubTopic == "test1") { + return SUBSCRIPTION_VERIFICATION_ERROR; + } else { + return SUBSCRIPTION_VERIFICATION_SUCCESS; + } + } + + remote function onUnsubscriptionVerification(readonly & UnsubscriptionVerification msg) + returns UnsubscriptionVerificationSuccess|UnsubscriptionVerificationError { + test:assertTrue(msg is readonly); + if (msg.hubTopic == "test1") { + return UNSUBSCRIPTION_VERIFICATION_ERROR; + } else { + return UNSUBSCRIPTION_VERIFICATION_SUCCESS; + } + } + + isolated remote function onEventNotification(readonly & ContentDistributionMessage event) + returns Acknowledgement|SubscriptionDeletedError? { + test:assertTrue(event is readonly); + match event.contentType { + mime:APPLICATION_FORM_URLENCODED => { + map content = >event.content; + log:printInfo("URL encoded content received ", content = content); + } + _ => { + log:printDebug("onEventNotification invoked ", contentDistributionMessage = event); + } + } + + return ACKNOWLEDGEMENT; + } +} + +http:Client readonlyParamTestClient = check new ("http://localhost:9104/subscriber"); + +@test:Config { + groups: ["subscriberWithReadonlyParams"] +} +function testOnSubscriptionValidationWithReadonly() returns error? { + http:Response response = check readonlyParamTestClient->get("/?hub.mode=denied&hub.reason=justToTest"); + test:assertEquals(response.statusCode, 200); +} + +@test:Config { + groups: ["subscriberWithReadonlyParams"] +} +function testOnIntentVerificationSuccessWithReadonly() returns error? { + http:Response response = check readonlyParamTestClient->get("/?hub.mode=subscribe&hub.topic=test&hub.challenge=1234"); + test:assertEquals(response.statusCode, 200); + test:assertEquals(response.getTextPayload(), "1234"); +} + +@test:Config { + groups: ["subscriberWithReadonlyParams"] +} +function testOnEventNotificationSuccessWithReadonly() returns error? { + http:Request request = new; + json payload = {"action": "publish", "mode": "remote-hub"}; + request.setPayload(payload); + http:Response response = check readonlyParamTestClient->post("/", request); + test:assertEquals(response.statusCode, 202); +} + +@test:Config { + groups: ["subscriberWithReadonlyParams"] +} +function testUnsubscriptionIntentVerificationSuccessWithReadonly() returns error? { + http:Response response = check readonlyParamTestClient->get("/?hub.mode=unsubscribe&hub.topic=test&hub.challenge=1234"); + test:assertEquals(response.statusCode, 200); + test:assertEquals(response.getTextPayload(), "1234"); +} diff --git a/changelog.md b/changelog.md index 9e0a8766..757307c1 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- [WebSub/WebSubHub should support `readonly` parameters for remote methods](https://github.com/ballerina-platform/ballerina-standard-library/issues/2604) + +## [2.1.0] - 2021-12-14 + ### Fixed - [Notify user of Subscription denial from the `hub` when `onSubscriptionDenied` is not implemented](https://github.com/ballerina-platform/ballerina-standard-library/issues/2448) diff --git a/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/websub/CompilerPluginTest.java b/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/websub/CompilerPluginTest.java index 8f49c904..4d356885 100644 --- a/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/websub/CompilerPluginTest.java +++ b/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/websub/CompilerPluginTest.java @@ -375,6 +375,17 @@ public void testValidWebsubServiceDeclarationWithUnsubVerification() { Assert.assertEquals(errorDiagnostics.size(), 0); } + @Test + public void testValidWebsubServiceDeclarationWithIntersectionTypes() { + Package currentPackage = loadPackage("sample_23"); + PackageCompilation compilation = currentPackage.getCompilation(); + DiagnosticResult diagnosticResult = compilation.diagnosticResult(); + List errorDiagnostics = diagnosticResult.diagnostics().stream() + .filter(d -> DiagnosticSeverity.ERROR.equals(d.diagnosticInfo().severity())) + .collect(Collectors.toList()); + Assert.assertEquals(errorDiagnostics.size(), 0); + } + private Package loadPackage(String path) { Path projectDirPath = RESOURCE_DIRECTORY.resolve(path); BuildProject project = BuildProject.load(getEnvironmentBuilder(), projectDirPath); diff --git a/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_23/Ballerina.toml b/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_23/Ballerina.toml new file mode 100644 index 00000000..f004d960 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_23/Ballerina.toml @@ -0,0 +1,7 @@ +[package] +org = "websub_test" +name = "sample_23" +version = "0.1.0" + +[build-options] +observabilityIncluded = true diff --git a/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_23/service.bal b/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_23/service.bal new file mode 100644 index 00000000..bee540d8 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_23/service.bal @@ -0,0 +1,59 @@ +// Copyright (c) 2022 WSO2 Inc. (http://www.wso2.org) All Rights Reserved. +// +// WSO2 Inc. 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/log; +import ballerina/mime; +import ballerina/websub; + +@websub:SubscriberServiceConfig {} +service /subscriber on new websub:Listener(9104) { + isolated remote function onSubscriptionValidationDenied(readonly & websub:SubscriptionDeniedError msg) returns websub:Acknowledgement? { + return websub:ACKNOWLEDGEMENT; + } + + isolated remote function onSubscriptionVerification(readonly & websub:SubscriptionVerification msg) + returns websub:SubscriptionVerificationSuccess|websub:SubscriptionVerificationError { + if (msg.hubTopic == "test1") { + return websub:SUBSCRIPTION_VERIFICATION_ERROR; + } else { + return websub:SUBSCRIPTION_VERIFICATION_SUCCESS; + } + } + + remote function onUnsubscriptionVerification(readonly & websub:UnsubscriptionVerification msg) + returns websub:UnsubscriptionVerificationSuccess|websub:UnsubscriptionVerificationError { + if (msg.hubTopic == "test1") { + return websub:UNSUBSCRIPTION_VERIFICATION_ERROR; + } else { + return websub:UNSUBSCRIPTION_VERIFICATION_SUCCESS; + } + } + + isolated remote function onEventNotification(readonly & websub:ContentDistributionMessage event) + returns websub:Acknowledgement|websub:SubscriptionDeletedError? { + match event.contentType { + mime:APPLICATION_FORM_URLENCODED => { + map content = >event.content; + log:printInfo("URL encoded content received ", content = content); + } + _ => { + log:printDebug("onEventNotification invoked ", contentDistributionMessage = event); + } + } + + return websub:ACKNOWLEDGEMENT; + } +} diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/websub/task/AnalyserUtils.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/websub/task/AnalyserUtils.java index 02e54618..ab67b6a9 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/websub/task/AnalyserUtils.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/websub/task/AnalyserUtils.java @@ -21,6 +21,7 @@ import io.ballerina.compiler.api.ModuleID; import io.ballerina.compiler.api.symbols.ErrorTypeSymbol; import io.ballerina.compiler.api.symbols.FunctionSymbol; +import io.ballerina.compiler.api.symbols.IntersectionTypeSymbol; import io.ballerina.compiler.api.symbols.ModuleSymbol; import io.ballerina.compiler.api.symbols.ObjectTypeSymbol; import io.ballerina.compiler.api.symbols.Qualifier; @@ -109,6 +110,13 @@ public static String getTypeDescription(TypeSymbol paramType) { .filter(e -> !e.isEmpty() && !e.isBlank()) .reduce((a, b) -> String.join("|", a, b)).orElse(""); return optionalSymbolAvailable ? concatenatedTypeDesc + Constants.OPTIONAL : concatenatedTypeDesc; + } else if (TypeDescKind.INTERSECTION.equals(paramKind)) { + List availableTypes = ((IntersectionTypeSymbol) paramType).memberTypeDescriptors(); + return availableTypes.stream() + .filter(e -> TypeDescKind.TYPE_REFERENCE.equals(e.typeKind())) + .map(AnalyserUtils::getTypeDescription) + .filter(e -> !e.isEmpty() && !e.isBlank()) + .reduce((a, b) -> String.join("&", a, b)).orElse(""); } else if (TypeDescKind.ERROR.equals(paramKind)) { return getErrorTypeDescription(paramType); } else { diff --git a/native/src/main/java/io/ballerina/stdlib/websub/Constants.java b/native/src/main/java/io/ballerina/stdlib/websub/Constants.java index f3bf4565..92a8a843 100644 --- a/native/src/main/java/io/ballerina/stdlib/websub/Constants.java +++ b/native/src/main/java/io/ballerina/stdlib/websub/Constants.java @@ -36,6 +36,11 @@ public interface Constants { String SERVICE_INFO_REGISTRY = "SERVICE_INFO_REGISTRY"; String SUBSCRIBER_CONFIG = "SUBSCRIBER_CONFIG"; + String ON_SUBSCRIPTION_VERIFICATION = "onSubscriptionVerification"; + String ON_UNSUBSCRIPTION_VERIFICATION = "onUnsubscriptionVerification"; + String ON_SUBSCRIPTION_VALIDATION_DENIED = "onSubscriptionValidationDenied"; + String ON_EVENT_NOTIFICATION = "onEventNotification"; + String ANN_NAME_HTTP_INTROSPECTION_DOC_CONFIG = "IntrospectionDocConfig"; BString ANN_FIELD_DOC_NAME = StringUtils.fromString("name"); } diff --git a/native/src/main/java/io/ballerina/stdlib/websub/NativeHttpToWebsubAdaptor.java b/native/src/main/java/io/ballerina/stdlib/websub/NativeHttpToWebsubAdaptor.java index 29eafeb5..23ac6abc 100644 --- a/native/src/main/java/io/ballerina/stdlib/websub/NativeHttpToWebsubAdaptor.java +++ b/native/src/main/java/io/ballerina/stdlib/websub/NativeHttpToWebsubAdaptor.java @@ -22,9 +22,13 @@ import io.ballerina.runtime.api.Future; import io.ballerina.runtime.api.Module; import io.ballerina.runtime.api.PredefinedTypes; +import io.ballerina.runtime.api.TypeTags; import io.ballerina.runtime.api.async.StrandMetadata; import io.ballerina.runtime.api.creators.ValueCreator; +import io.ballerina.runtime.api.types.IntersectionType; import io.ballerina.runtime.api.types.MethodType; +import io.ballerina.runtime.api.types.Parameter; +import io.ballerina.runtime.api.types.Type; import io.ballerina.runtime.api.utils.StringUtils; import io.ballerina.runtime.api.values.BArray; import io.ballerina.runtime.api.values.BError; @@ -33,9 +37,14 @@ import io.ballerina.runtime.api.values.BString; import java.util.ArrayList; +import java.util.List; import java.util.Objects; import static io.ballerina.stdlib.websub.Constants.HTTP_REQUEST; +import static io.ballerina.stdlib.websub.Constants.ON_EVENT_NOTIFICATION; +import static io.ballerina.stdlib.websub.Constants.ON_SUBSCRIPTION_VALIDATION_DENIED; +import static io.ballerina.stdlib.websub.Constants.ON_SUBSCRIPTION_VERIFICATION; +import static io.ballerina.stdlib.websub.Constants.ON_UNSUBSCRIPTION_VERIFICATION; import static io.ballerina.stdlib.websub.Constants.SERVICE_OBJECT; import static io.ballerina.stdlib.websub.Constants.SUBSCRIBER_CONFIG; @@ -68,29 +77,58 @@ public static BArray getServiceMethodNames(BObject adaptor) { public static Object callOnSubscriptionVerificationMethod(Environment env, BObject adaptor, BMap message) { BObject serviceObj = (BObject) adaptor.getNativeData(SERVICE_OBJECT); + boolean isReadOnly = isReadOnlyParam(serviceObj, ON_SUBSCRIPTION_VERIFICATION); + if (isReadOnly) { + message.freezeDirect(); + } return invokeRemoteFunction(env, serviceObj, message, - "callOnSubscriptionVerificationMethod", "onSubscriptionVerification"); + "callOnSubscriptionVerificationMethod", ON_SUBSCRIPTION_VERIFICATION); } public static Object callOnUnsubscriptionVerificationMethod(Environment env, BObject adaptor, BMap message) { BObject serviceObj = (BObject) adaptor.getNativeData(SERVICE_OBJECT); + boolean isReadOnly = isReadOnlyParam(serviceObj, ON_UNSUBSCRIPTION_VERIFICATION); + if (isReadOnly) { + message.freezeDirect(); + } return invokeRemoteFunction(env, serviceObj, message, - "callOnUnsubscriptionVerificationMethod", "onUnsubscriptionVerification"); + "callOnUnsubscriptionVerificationMethod", ON_UNSUBSCRIPTION_VERIFICATION); } public static Object callOnSubscriptionDeniedMethod(Environment env, BObject adaptor, BError message) { BObject serviceObj = (BObject) adaptor.getNativeData(SERVICE_OBJECT); return invokeRemoteFunction(env, serviceObj, message, - "callOnSubscriptionDeniedMethod", "onSubscriptionValidationDenied"); + "callOnSubscriptionDeniedMethod", ON_SUBSCRIPTION_VALIDATION_DENIED); } public static Object callOnEventNotificationMethod(Environment env, BObject adaptor, BMap message, BObject bHttpRequest) { message.addNativeData(HTTP_REQUEST, bHttpRequest); BObject serviceObj = (BObject) adaptor.getNativeData(SERVICE_OBJECT); + boolean isReadOnly = isReadOnlyParam(serviceObj, ON_EVENT_NOTIFICATION); + if (isReadOnly) { + message.freezeDirect(); + } return invokeRemoteFunction(env, serviceObj, message, - "callOnEventNotificationMethod", "onEventNotification"); + "callOnEventNotificationMethod", ON_EVENT_NOTIFICATION); + } + + private static boolean isReadOnlyParam(BObject serviceObj, String remoteMethod) { + for (MethodType method : serviceObj.getType().getMethods()) { + if (method.getName().equals(remoteMethod)) { + Parameter[] parameters = method.getParameters(); + if (parameters.length >= 1) { + Parameter parameter = parameters[0]; + Type paramType = parameter.type; + if (paramType instanceof IntersectionType) { + List constituentTypes = ((IntersectionType) paramType).getConstituentTypes(); + return constituentTypes.stream().anyMatch(t -> TypeTags.READONLY_TAG == t.getTag()); + } + } + } + } + return false; } public static BObject retrieveHttpRequest(BMap message) {