diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index f1f6176..2e83590 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -7,6 +7,17 @@ dependencies-toml-version = "2" distribution-version = "2201.9.2" +[[package]] +org = "ballerina" +name = "constraint" +version = "1.5.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] +modules = [ + {org = "ballerina", packageName = "constraint", moduleName = "constraint"} +] + [[package]] org = "ballerina" name = "jballerina.java" @@ -15,12 +26,25 @@ modules = [ {org = "ballerina", packageName = "jballerina.java", moduleName = "jballerina.java"} ] +[[package]] +org = "ballerina" +name = "time" +version = "2.4.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] +modules = [ + {org = "ballerina", packageName = "time", moduleName = "time"} +] + [[package]] org = "ballerinax" name = "aws.marketplace.mpm" version = "0.1.0" dependencies = [ - {org = "ballerina", name = "jballerina.java"} + {org = "ballerina", name = "constraint"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "time"} ] modules = [ {org = "ballerinax", packageName = "aws.marketplace.mpm", moduleName = "aws.marketplace.mpm"} diff --git a/ballerina/build.gradle b/ballerina/build.gradle index 59db10f..c9c3a5c 100644 --- a/ballerina/build.gradle +++ b/ballerina/build.gradle @@ -73,11 +73,6 @@ task commitTomlFiles { } } -clean { - delete 'build' - delete 'lib' -} - build.dependsOn copyToLib build.dependsOn ":${packageName}-native:build" diff --git a/ballerina/client.bal b/ballerina/client.bal new file mode 100644 index 0000000..c2212aa --- /dev/null +++ b/ballerina/client.bal @@ -0,0 +1,86 @@ +// Copyright (c) 2024 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/constraint; +import ballerina/jballerina.java; + +# AWS Marketplace metering client. +public isolated client class Client { + + # Initialize the Ballerina AWS MPM client. + # ```ballerina + # mpm:Client mpm = check new(region = mpm:US_EAST_1, auth = { + # accessKeyId: "", + # secretAccessKey: "" + # }); + # ``` + # + # + configs - The AWS MPM client configurations + # + return - The `mpm:Client` or an `mpm:Error` if the initialization failed + public isolated function init(*ConnectionConfig configs) returns Error? { + return self.externInit(configs); + } + + isolated function externInit(ConnectionConfig configs) returns Error? = + @java:Method { + name: "init", + 'class: "io.ballerina.lib.aws.mpm.NativeClientAdaptor" + } external; + + # Retrieves customer details mapped to a registration token. + # ```ballerina + # mpm:ResolveCustomerResponse response = check mpm->resolveCustomer(""); + # ``` + # + # + registrationToken - The registration-token provided by the customer + # + return - A Ballerina `mpm:Error` if there was an error while executing the operation or else `mpm:ResolveCustomerResponse` + remote function resolveCustomer(string registrationToken) returns ResolveCustomerResponse|Error = + @java:Method { + 'class: "io.ballerina.lib.aws.mpm.NativeClientAdaptor" + } external; + + # Retrieves the post-metering records for a set of customers. + # ```ballerina + # mpm:BatchMeterUsageResponse response = check mpm->batchMeterUsage(productCode = ""); + # ``` + # + # + request - The request parameters for the `BatchMeterUsage` operation + # + return - A Ballerina `mpm:Error` if there was an error while executing the operation or else `mpm:BatchMeterUsageResponse` + remote function batchMeterUsage(*BatchMeterUsageRequest request) returns BatchMeterUsageResponse|Error { + BatchMeterUsageRequest|constraint:Error validated = constraint:validate(request); + if validated is constraint:Error { + return error Error(string `Request validation failed: ${validated.message()}`); + } + return self.externBatchMeterUsage(validated); + } + + isolated function externBatchMeterUsage(BatchMeterUsageRequest request) returns BatchMeterUsageResponse|Error = + @java:Method { + name: "batchMeterUsage", + 'class: "io.ballerina.lib.aws.mpm.NativeClientAdaptor" + } external; + + # Closes the AWS MPM client resources. + # ```ballerina + # check mpm->close(); + # ``` + # + # + return - A `mpm:Error` if there is an error while closing the client resources or else nil. + remote function close() returns Error? = + @java:Method { + 'class: "io.ballerina.lib.aws.mpm.NativeClientAdaptor" + } external; +} diff --git a/ballerina/errors.bal b/ballerina/errors.bal new file mode 100644 index 0000000..3f78ad7 --- /dev/null +++ b/ballerina/errors.bal @@ -0,0 +1,30 @@ +// Copyright (c) 2024 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. + +# Represents a AWS Marketplace Metering distinct error. +public type Error distinct error; + +# The error details type for the AWS MPM module. +public type ErrorDetails record {| + # The HTTP status code for the error + int httpStatusCode?; + # The HTTP status text returned from the service + string httpStatusText?; + # The error code associated with the response + string errorCode?; + # The human-readable error message provided by the service + string errorMessage?; +|}; diff --git a/ballerina/types.bal b/ballerina/types.bal new file mode 100644 index 0000000..cb50715 --- /dev/null +++ b/ballerina/types.bal @@ -0,0 +1,194 @@ +// Copyright (c) 2024 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/constraint; +import ballerina/time; + +# Represents the Client configurations for AWS Marketplace Metering service. +public type ConnectionConfig record {| + # The AWS region with which the connector should communicate + Region region; + # The authentication configurations for the AWS Marketplace Metering service + AuthConfig auth; +|}; + +# An Amazon Web Services region that hosts a set of Amazon services. +public enum Region { + AF_SOUTH_1 = "af-south-1", + AP_EAST_1 = "ap-east-1", + AP_NORTHEAST_1 = "ap-northeast-1", + AP_NORTHEAST_2 = "ap-northeast-2", + AP_NORTHEAST_3 = "ap-northeast-3", + AP_SOUTH_1 = "ap-south-1", + AP_SOUTH_2 = "ap-south-2", + AP_SOUTHEAST_1 = "ap-southeast-1", + AP_SOUTHEAST_2 = "ap-southeast-2", + AP_SOUTHEAST_3 = "ap-southeast-3", + AP_SOUTHEAST_4 = "ap-southeast-4", + AWS_CN_GLOBAL = "aws-cn-global", + AWS_GLOBAL = "aws-global", + AWS_ISO_GLOBAL = "aws-iso-global", + AWS_ISO_B_GLOBAL = "aws-iso-b-global", + AWS_US_GOV_GLOBAL = "aws-us-gov-global", + CA_WEST_1 = "ca-west-1", + CA_CENTRAL_1 = "ca-central-1", + CN_NORTH_1 = "cn-north-1", + CN_NORTHWEST_1 = "cn-northwest-1", + EU_CENTRAL_1 = "eu-central-1", + EU_CENTRAL_2 = "eu-central-2", + EU_ISOE_WEST_1 = "eu-isoe-west-1", + EU_NORTH_1 = "eu-north-1", + EU_SOUTH_1 = "eu-south-1", + EU_SOUTH_2 = "eu-south-2", + EU_WEST_1 = "eu-west-1", + EU_WEST_2 = "eu-west-2", + EU_WEST_3 = "eu-west-3", + IL_CENTRAL_1 = "il-central-1", + ME_CENTRAL_1 = "me-central-1", + ME_SOUTH_1 = "me-south-1", + SA_EAST_1 = "sa-east-1", + US_EAST_1 = "us-east-1", + US_EAST_2 = "us-east-2", + US_GOV_EAST_1 = "us-gov-east-1", + US_GOV_WEST_1 = "us-gov-west-1", + US_ISOB_EAST_1 = "us-isob-east-1", + US_ISO_EAST_1 = "us-iso-east-1", + US_ISO_WEST_1 = "us-iso-west-1", + US_WEST_1 = "us-west-1", + US_WEST_2 = "us-west-2" +} + +# Represents the Authentication configurations for AWS Marketplace Metering service. +public type AuthConfig record {| + # The AWS access key, used to identify the user interacting with AWS + string accessKeyId; + # The AWS secret access key, used to authenticate the user interacting with AWS + string secretAccessKey; + # The AWS session token, retrieved from an AWS token service, used for authenticating + # a user with temporary permission to a resource + string sessionToken?; +|}; + +# Represents the result retrieved from `ResolveCustomer` operation. +public type ResolveCustomerResponse record {| + # The AWS account ID associated with the Customer identifier for the individual customer + string customerAWSAccountId; + # The unique identifier used to identify an individual customer + string customerIdentifier; + # The unique identifier for the Marketplace product + string productCode; +|}; + +# Represents the parameters used for `BatchMeterUsage` operation. +public type BatchMeterUsageRequest record {| + # The unique identifier for the Marketplace product + @constraint:String { + pattern: re `^[-a-zA-Z0-9/=:_.@]{1,255}$` + } + string productCode; + # The set of usage records. Each usage record provides information about an instance of product usage. + @constraint:Array { + maxLength: 25 + } + UsageRecord[] usageRecords = []; +|}; + +# Represents the details of the quantity of usage for a given product. +public type UsageRecord record {| + # The unique identifier used to identify an individual customer + @constraint:String { + pattern: re `[\s\S]{1,255}$` + } + string customerIdentifier; + # The dimension for which the usage is being reported + @constraint:String { + pattern: re `[\s\S]{1,255}$` + } + string dimension; + # The timestamp when the usage occurred (in UTC) + time:Utc timestamp; + # The quantity of usage consumed + @constraint:Int { + minValue: 0, + maxValue: 2147483647 + } + int quantity?; + # The list of usage allocations + @constraint:Array { + minLength: 1, + maxLength: 2500 + } + UsageAllocation[] usageAllocations?; +|}; + +# Represents a usage allocation for AWS Marketplace metering. +public type UsageAllocation record {| + # The total quantity allocated to this bucket of usage + @constraint:Int { + minValue: 0, + maxValue: 2147483647 + } + int allocatedUsageQuantity; + # The set of tags that define the bucket of usage + @constraint:Array { + minLength: 1, + maxLength: 5 + } + Tag[] tags?; +|}; + +# Represents the metadata assigned to a usage allocation. +public type Tag record {| + # The label that acts as the category for the specific tag values + @constraint:String { + pattern: re `^[a-zA-Z0-9+ -=._:\\/@]{1,100}$` + } + string 'key; + # The descriptor within a tag category (key) + @constraint:String { + pattern: re `^[a-zA-Z0-9+ -=._:\\/@]{1,256}$` + } + string value; +|}; + +# Represents the result retrieved from `BatchMeterUsage` operation. +public type BatchMeterUsageResponse record {| + # The list of all the `UsageRecord` instances successfully processed + UsageRecordResult[] results; + # The list of all the `UsageRecord` instances which were not processed + UsageRecord[] unprocessedRecords; +|}; + +# Represents the details regarding the status of a given `UsageRecord` processed by `BatchMeterUsage` operation. +public type UsageRecordResult record {| + # The unique identifier for this metering event + string meteringRecordId?; + # The status of the individual `UsageRecord` processed by the `BatchMeterUsage` operation + UsageRecordStatus status?; + # The `UsageRecord` which was part of the `BatchMeterUsage` request + UsageRecord usageRecord?; +|}; + +# Represents the possible status of a `UsageRecord` +public enum UsageRecordStatus { + # The `UsageRecord` was accepted by the `BatchMeterUsage` operation + SUCCESS = "Success", + # The provided customer identifier in the `BatchMeterUsage` request, is not able to use your product + CUSTOMER_NOT_SUBSCRIBED = "CustomerNotSubscribed", + # The provided `UsageRecord` matches a previously metered `UsageRecord` in terms of customer, dimension, and time + DUPLICATE_RECORD = "DuplicateRecord" +} + diff --git a/gradle.properties b/gradle.properties index daaa405..0ebbf66 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,3 +11,6 @@ testngVersion=7.6.1 eclipseLsp4jVersion=0.12.0 ballerinaGradlePluginVersion=2.2.4 ballerinaLangVersion=2201.9.2 + +stdlibTimeVersion=2.4.0 +awsMpMeteringSdkVersion=2.27.7 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml deleted file mode 100644 index 4ac3234..0000000 --- a/gradle/libs.versions.toml +++ /dev/null @@ -1,2 +0,0 @@ -# This file was generated by the Gradle 'init' task. -# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format diff --git a/native/build.gradle b/native/build.gradle index 1fc8a39..2dbb5fe 100644 --- a/native/build.gradle +++ b/native/build.gradle @@ -37,6 +37,10 @@ dependencies { implementation group: 'org.ballerinalang', name: 'ballerina-runtime', version: "${ballerinaLangVersion}" implementation group: 'org.ballerinalang', name: 'ballerina-lang', version: "${ballerinaLangVersion}" implementation group: 'org.ballerinalang', name: 'value', version: "${ballerinaLangVersion}" + implementation group: 'io.ballerina.stdlib', name: 'time-native', version: "${stdlibTimeVersion}" + implementation group: 'software.amazon.awssdk', name: 'marketplacemetering', version: "${awsMpMeteringSdkVersion}" + + dist group: 'software.amazon.awssdk', name: 'marketplacemetering', version: "${awsMpMeteringSdkVersion}" } tasks.withType(JavaCompile) { diff --git a/native/src/main/java/io/ballerina/lib/aws/mpm/AwsMpmThreadFactory.java b/native/src/main/java/io/ballerina/lib/aws/mpm/AwsMpmThreadFactory.java new file mode 100644 index 0000000..7068eba --- /dev/null +++ b/native/src/main/java/io/ballerina/lib/aws/mpm/AwsMpmThreadFactory.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024, 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. + */ + +package io.ballerina.lib.aws.mpm; + +import java.util.concurrent.ThreadFactory; + +/** + * A {@link ThreadFactory} object that creates new threads on demand for AWS MPM client network actions. + */ +public class AwsMpmThreadFactory implements ThreadFactory { + + @Override + public Thread newThread(Runnable runnable) { + Thread networkThread = new Thread(runnable); + networkThread.setName("balx-awsmpm-client-network-thread"); + return networkThread; + } +} diff --git a/native/src/main/java/io/ballerina/lib/aws/mpm/CommonUtils.java b/native/src/main/java/io/ballerina/lib/aws/mpm/CommonUtils.java new file mode 100644 index 0000000..8c6a69b --- /dev/null +++ b/native/src/main/java/io/ballerina/lib/aws/mpm/CommonUtils.java @@ -0,0 +1,237 @@ +/* + * Copyright (c) 2024, 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. + */ + +package io.ballerina.lib.aws.mpm; + +import io.ballerina.runtime.api.creators.ErrorCreator; +import io.ballerina.runtime.api.creators.TypeCreator; +import io.ballerina.runtime.api.creators.ValueCreator; +import io.ballerina.runtime.api.flags.SymbolFlags; +import io.ballerina.runtime.api.types.ArrayType; +import io.ballerina.runtime.api.types.RecordType; +import io.ballerina.runtime.api.utils.StringUtils; +import io.ballerina.runtime.api.values.BArray; +import io.ballerina.runtime.api.values.BError; +import io.ballerina.runtime.api.values.BMap; +import io.ballerina.runtime.api.values.BString; +import io.ballerina.stdlib.time.nativeimpl.Utc; +import software.amazon.awssdk.awscore.exception.AwsErrorDetails; +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.services.marketplacemetering.model.BatchMeterUsageRequest; +import software.amazon.awssdk.services.marketplacemetering.model.BatchMeterUsageResponse; +import software.amazon.awssdk.services.marketplacemetering.model.ResolveCustomerResponse; +import software.amazon.awssdk.services.marketplacemetering.model.Tag; +import software.amazon.awssdk.services.marketplacemetering.model.UsageAllocation; +import software.amazon.awssdk.services.marketplacemetering.model.UsageRecord; +import software.amazon.awssdk.services.marketplacemetering.model.UsageRecordResult; +import software.amazon.awssdk.services.marketplacemetering.model.UsageRecordResultStatus; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * {@code CommonUtils} contains the common utility functions for the Ballerina AWS MPM connector. + */ +public final class CommonUtils { + private static final RecordType USAGE_RECORD_RESULT_REC_TYPE = TypeCreator.createRecordType( + Constants.MPM_USAGE_RECORD_RESULT, ModuleUtils.getModule(), SymbolFlags.PUBLIC, true, 0); + private static final ArrayType USAGE_RECORD_RESULT_ARR_TYPE = TypeCreator.createArrayType( + USAGE_RECORD_RESULT_REC_TYPE); + private static final RecordType USAGE_RECORD_REC_TYPE = TypeCreator.createRecordType( + Constants.MPM_USAGE_RECORD, ModuleUtils.getModule(), SymbolFlags.PUBLIC, true, 0); + private static final ArrayType USAGE_RECORD_ARR_TYPE = TypeCreator.createArrayType(USAGE_RECORD_REC_TYPE); + private static final RecordType USAGE_ALLOC_REC_TYPE = TypeCreator.createRecordType( + Constants.MPM_USAGE_ALLOC, ModuleUtils.getModule(), SymbolFlags.PUBLIC, true, 0); + private static final ArrayType USAGE_ALLOC_ARR_TYPE = TypeCreator.createArrayType(USAGE_ALLOC_REC_TYPE); + private static final RecordType TAG_REC_TYPE = TypeCreator.createRecordType( + Constants.MPM_TAG, ModuleUtils.getModule(), SymbolFlags.PUBLIC, true, 0); + private static final ArrayType TAG_ARR_TYPE = TypeCreator.createArrayType(TAG_REC_TYPE); + + private CommonUtils() { + } + + public static BMap getBResolveCustomerResponse(ResolveCustomerResponse nativeResponse) { + BMap resolveCustomerResponse = ValueCreator.createRecordValue( + ModuleUtils.getModule(), Constants.MPM_RESOLVE_CUSTOMER); + resolveCustomerResponse.put(Constants.MPM_RESOLVE_CUSTOMER_AWS_ACNT_ID, + StringUtils.fromString(nativeResponse.customerAWSAccountId())); + resolveCustomerResponse.put(Constants.MPM_RESOLVE_CUSTOMER_IDNFR, + StringUtils.fromString(nativeResponse.customerIdentifier())); + resolveCustomerResponse.put(Constants.MPM_RESOLVE_CUSTOMER_PRODUCT_CODE, + StringUtils.fromString(nativeResponse.productCode())); + return resolveCustomerResponse; + } + + @SuppressWarnings("unchecked") + public static BatchMeterUsageRequest getNativeBatchMeterUsageRequest(BMap request) { + String productCode = request.getStringValue(Constants.MPM_BATCH_METER_USAGE_PRODUCT_CODE).getValue(); + BatchMeterUsageRequest.Builder requestBuilder = BatchMeterUsageRequest.builder().productCode(productCode); + BArray usageRecords = request.getArrayValue(Constants.MPM_BATCH_METER_USAGE_RECORDS); + List nativeUsageRecords = new ArrayList<>(); + for (int i = 0; i < usageRecords.size(); i++) { + BMap bUsageRecord = (BMap) usageRecords.get(i); + UsageRecord usageRecord = toNativeUsageRecord(bUsageRecord); + nativeUsageRecords.add(usageRecord); + } + return requestBuilder.usageRecords(nativeUsageRecords).build(); + } + + @SuppressWarnings("unchecked") + private static UsageRecord toNativeUsageRecord(BMap bUsageRecord) { + String customerIdentifier = bUsageRecord.getStringValue(Constants.MPM_USAGE_RECORD_CUSTOMER_IDFR).getValue(); + String dimension = bUsageRecord.getStringValue(Constants.MPM_USAGE_RECORD_DIMENSION).getValue(); + BArray timestamp = bUsageRecord.getArrayValue(Constants.MPM_USAGE_RECORD_TIMESTAMP); + Utc utcTimestamp = new Utc(timestamp); + UsageRecord.Builder builder = UsageRecord.builder() + .customerIdentifier(customerIdentifier) + .dimension(dimension) + .timestamp(utcTimestamp.generateInstant()); + if (bUsageRecord.containsKey(Constants.MPM_USAGE_RECORD_QUANTITY)) { + builder = builder.quantity(bUsageRecord.getIntValue(Constants.MPM_USAGE_RECORD_QUANTITY).intValue()); + } + if (bUsageRecord.containsKey(Constants.MPM_USAGE_RECORD_USAGE_ALLOCATION)) { + BArray usageAllocations = bUsageRecord.getArrayValue(Constants.MPM_USAGE_RECORD_USAGE_ALLOCATION); + List nativeUsageAllocations = new ArrayList<>(); + for (int i = 0; i < usageAllocations.size(); i++) { + BMap bUsageAllocation = (BMap) usageAllocations.get(i); + UsageAllocation usageAllocation = toNativeUsageAllocation(bUsageAllocation); + nativeUsageAllocations.add(usageAllocation); + } + builder = builder.usageAllocations(nativeUsageAllocations); + } + return builder.build(); + } + + @SuppressWarnings("unchecked") + private static UsageAllocation toNativeUsageAllocation(BMap bUsageAllocation) { + int allocatedQuantity = bUsageAllocation.getIntValue(Constants.MPM_USAGE_ALLOC_USAGE_QUANTITY).intValue(); + UsageAllocation.Builder builder = UsageAllocation.builder().allocatedUsageQuantity(allocatedQuantity); + if (bUsageAllocation.containsKey(Constants.MPM_USAGE_ALLOC_TAGS)) { + BArray tags = bUsageAllocation.getArrayValue(Constants.MPM_USAGE_ALLOC_TAGS); + List nativeTags = new ArrayList<>(); + for (int i = 0; i < tags.size(); i++) { + BMap bTag = (BMap) tags.get(i); + String key = bTag.getStringValue(Constants.MPM_TAG_KEY).getValue(); + String value = bTag.getStringValue(Constants.MPM_TAG_VALUE).getValue(); + Tag nativeTag = Tag.builder().key(key).value(value).build(); + nativeTags.add(nativeTag); + } + builder = builder.tags(nativeTags); + } + return builder.build(); + } + + public static BMap getBBatchMeterUsageResponse(BatchMeterUsageResponse nativeResponse) { + BMap batchMeterUsageResponse = ValueCreator.createRecordValue( + ModuleUtils.getModule(), Constants.MPM_BATCH_METER_USAGE_RESPONSE); + BArray usageRecordResults = ValueCreator.createArrayValue(USAGE_RECORD_RESULT_ARR_TYPE); + nativeResponse.results().forEach(result -> { + BMap bUsageRecordResult = toBUsageRecordResult(result); + usageRecordResults.append(bUsageRecordResult); + }); + batchMeterUsageResponse.put(Constants.MPM_BATCH_METER_USAGE_RESPONSE_RESULTS, usageRecordResults); + BArray unprocessedRecords = ValueCreator.createArrayValue(USAGE_RECORD_ARR_TYPE); + nativeResponse.unprocessedRecords().forEach(unprocessedRecord -> { + BMap bUsageRecord = toBUsageRecord(unprocessedRecord); + unprocessedRecords.append(bUsageRecord); + }); + batchMeterUsageResponse.put(Constants.MPM_BATCH_METER_USAGE_RESPONSE_UNPROC_RECORDS, unprocessedRecords); + return batchMeterUsageResponse; + } + + private static BMap toBUsageRecordResult(UsageRecordResult nativeUsageRecordResult) { + BMap bUsageRecordResult = ValueCreator.createRecordValue(USAGE_RECORD_RESULT_REC_TYPE); + String meteringRecordId = nativeUsageRecordResult.meteringRecordId(); + if (Objects.nonNull(meteringRecordId)) { + bUsageRecordResult.put( + Constants.MPM_USAGE_RECORD_RESULT_METERING_RECORD, StringUtils.fromString(meteringRecordId)); + } + UsageRecordResultStatus status = nativeUsageRecordResult.status(); + if (Objects.nonNull(status)) { + bUsageRecordResult.put(Constants.MPM_USAGE_RECORD_RESULT_STATUS, StringUtils.fromString(status.toString())); + } + UsageRecord usageRecord = nativeUsageRecordResult.usageRecord(); + if (Objects.nonNull(usageRecord)) { + BMap bUsageRecord = toBUsageRecord(usageRecord); + bUsageRecordResult.put(Constants.MPM_USAGE_RECORD_RESULT_USAGE_RECORD, bUsageRecord); + } + return bUsageRecordResult; + } + + private static BMap toBUsageRecord(UsageRecord nativeUsageRecord) { + BMap bUsageRecord = ValueCreator.createRecordValue(USAGE_RECORD_REC_TYPE); + bUsageRecord.put(Constants.MPM_USAGE_RECORD_CUSTOMER_IDFR, + StringUtils.fromString(nativeUsageRecord.customerIdentifier())); + bUsageRecord.put(Constants.MPM_USAGE_RECORD_DIMENSION, StringUtils.fromString(nativeUsageRecord.dimension())); + bUsageRecord.put(Constants.MPM_USAGE_RECORD_TIMESTAMP, new Utc(nativeUsageRecord.timestamp())); + Integer quantity = nativeUsageRecord.quantity(); + if (Objects.nonNull(quantity)) { + bUsageRecord.put(Constants.MPM_USAGE_RECORD_QUANTITY, quantity); + } + List nativeUsageAllocation = nativeUsageRecord.usageAllocations(); + if (Objects.nonNull(nativeUsageAllocation) && !nativeUsageAllocation.isEmpty()) { + BArray usageAllocations = ValueCreator.createArrayValue(USAGE_ALLOC_ARR_TYPE); + nativeUsageAllocation.forEach(usageAllocation -> { + BMap bUsageAllocation = toBUsageAllocation(usageAllocation); + usageAllocations.append(bUsageAllocation); + }); + bUsageRecord.put(Constants.MPM_USAGE_RECORD_USAGE_ALLOCATION, usageAllocations); + } + return bUsageRecord; + } + + private static BMap toBUsageAllocation(UsageAllocation nativeUsageAllocation) { + BMap bUsageAllocation = ValueCreator.createRecordValue(USAGE_ALLOC_REC_TYPE); + bUsageAllocation.put( + Constants.MPM_USAGE_ALLOC_USAGE_QUANTITY, nativeUsageAllocation.allocatedUsageQuantity()); + List nativeTags = nativeUsageAllocation.tags(); + if (Objects.nonNull(nativeTags) && !nativeTags.isEmpty()) { + BArray tags = ValueCreator.createArrayValue(TAG_ARR_TYPE); + nativeTags.forEach(t -> { + BMap bTag = ValueCreator.createRecordValue(TAG_REC_TYPE); + bTag.put(Constants.MPM_TAG_KEY, StringUtils.fromString(t.key())); + bTag.put(Constants.MPM_TAG_VALUE, StringUtils.fromString(t.value())); + tags.append(bTag); + }); + bUsageAllocation.put(Constants.MPM_USAGE_ALLOC_TAGS, tags); + } + return bUsageAllocation; + } + + public static BError createError(String message, Throwable exception) { + BError cause = ErrorCreator.createError(exception); + BMap errorDetails = ValueCreator.createRecordValue( + ModuleUtils.getModule(), Constants.MPM_ERROR_DETAILS); + if (exception instanceof AwsServiceException awsSvcExp && Objects.nonNull(awsSvcExp.awsErrorDetails())) { + AwsErrorDetails awsErrorDetails = awsSvcExp.awsErrorDetails(); + if (Objects.nonNull(awsErrorDetails.sdkHttpResponse())) { + errorDetails.put( + Constants.MPM_ERROR_DETAILS_HTTP_STATUS_CODE, awsErrorDetails.sdkHttpResponse().statusCode()); + awsErrorDetails.sdkHttpResponse().statusText().ifPresent(httpStatusTxt -> errorDetails.put( + Constants.MPM_ERROR_DETAILS_HTTP_STATUS_TXT, StringUtils.fromString(httpStatusTxt))); + } + errorDetails.put( + Constants.MPM_ERROR_DETAILS_ERR_CODE, StringUtils.fromString(awsErrorDetails.errorCode())); + errorDetails.put( + Constants.MPM_ERROR_DETAILS_ERR_MSG, StringUtils.fromString(awsErrorDetails.errorMessage())); + } + return ErrorCreator.createError( + ModuleUtils.getModule(), Constants.MPM_ERROR, StringUtils.fromString(message), cause, errorDetails); + } +} diff --git a/native/src/main/java/io/ballerina/lib/aws/mpm/ConnectionConfig.java b/native/src/main/java/io/ballerina/lib/aws/mpm/ConnectionConfig.java new file mode 100644 index 0000000..80f294b --- /dev/null +++ b/native/src/main/java/io/ballerina/lib/aws/mpm/ConnectionConfig.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2024, 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. + */ + +package io.ballerina.lib.aws.mpm; + +import io.ballerina.runtime.api.utils.StringUtils; +import io.ballerina.runtime.api.values.BMap; +import io.ballerina.runtime.api.values.BString; +import software.amazon.awssdk.regions.Region; + +import java.util.List; + +/** + * {@code ConnectionConfig} contains the java representation of the Ballerina AWS MPM client configurations. + * + * @param region The AWS region with which the connector should communicate + * @param accessKeyId The AWS access key, used to identify the user interacting with AWS. + * @param secretAccessKey The AWS secret access key, used to authenticate the user interacting with AWS. + * @param sessionToken The AWS session token, retrieved from an AWS token service, used for authenticating that + * this user has received temporary permission to access some resource. + */ +public record ConnectionConfig(Region region, String accessKeyId, String secretAccessKey, String sessionToken) { + private static final List AWS_GLOBAL_REGIONS = List.of( + Region.AWS_GLOBAL, Region.AWS_CN_GLOBAL, Region.AWS_US_GOV_GLOBAL, Region.AWS_ISO_GLOBAL, + Region.AWS_ISO_B_GLOBAL); + private static final BString REGION = StringUtils.fromString("region"); + private static final BString AUTH = StringUtils.fromString("auth"); + private static final BString AUTH_ACCESS_KEY_KEY = StringUtils.fromString("accessKeyId"); + private static final BString AUTH_SECRET_ACCESS_KEY = StringUtils.fromString("secretAccessKey"); + private static final BString AUTH_SESSION_TOKEN = StringUtils.fromString("sessionToken"); + + public ConnectionConfig(BMap configurations) { + this( + getRegion(configurations), + getAuthConfig(configurations, AUTH_ACCESS_KEY_KEY), + getAuthConfig(configurations, AUTH_SECRET_ACCESS_KEY), + getAuthConfig(configurations, AUTH_SESSION_TOKEN) + ); + } + + private static Region getRegion(BMap configurations) { + String region = configurations.getStringValue(REGION).getValue(); + return AWS_GLOBAL_REGIONS.stream().filter(gr -> gr.id().equals(region)).findFirst().orElse(Region.of(region)); + } + + @SuppressWarnings("unchecked") + private static String getAuthConfig(BMap configurations, BString key) { + BMap authConfig = (BMap) configurations.getMapValue(AUTH); + if (authConfig.containsKey(key)) { + return authConfig.getStringValue(key).getValue(); + } + return null; + } +} diff --git a/native/src/main/java/io/ballerina/lib/aws/mpm/Constants.java b/native/src/main/java/io/ballerina/lib/aws/mpm/Constants.java new file mode 100644 index 0000000..7db2d76 --- /dev/null +++ b/native/src/main/java/io/ballerina/lib/aws/mpm/Constants.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2024, 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. + */ + +package io.ballerina.lib.aws.mpm; + +import io.ballerina.runtime.api.utils.StringUtils; +import io.ballerina.runtime.api.values.BString; + +/** + * Represents the constants related to Ballerina MPM connector. + */ +public interface Constants { + // Constants related to MPM `ResolveCustomerResponse` + String MPM_RESOLVE_CUSTOMER = "ResolveCustomerResponse"; + BString MPM_RESOLVE_CUSTOMER_AWS_ACNT_ID = StringUtils.fromString("customerAWSAccountId"); + BString MPM_RESOLVE_CUSTOMER_IDNFR = StringUtils.fromString("customerIdentifier"); + BString MPM_RESOLVE_CUSTOMER_PRODUCT_CODE = StringUtils.fromString("productCode"); + + // Constants related to MPM `BatchMeterUsageRequest` + BString MPM_BATCH_METER_USAGE_PRODUCT_CODE = StringUtils.fromString("productCode"); + BString MPM_BATCH_METER_USAGE_RECORDS = StringUtils.fromString("usageRecords"); + + // Constants related to MPM `UsageRecord` + String MPM_USAGE_RECORD = "UsageRecord"; + BString MPM_USAGE_RECORD_CUSTOMER_IDFR = StringUtils.fromString("customerIdentifier"); + BString MPM_USAGE_RECORD_DIMENSION = StringUtils.fromString("dimension"); + BString MPM_USAGE_RECORD_TIMESTAMP = StringUtils.fromString("timestamp"); + BString MPM_USAGE_RECORD_QUANTITY = StringUtils.fromString("quantity"); + BString MPM_USAGE_RECORD_USAGE_ALLOCATION = StringUtils.fromString("usageAllocations"); + + // Constants related to MPM `UsageAllocation` + String MPM_USAGE_ALLOC = "UsageAllocation"; + BString MPM_USAGE_ALLOC_USAGE_QUANTITY = StringUtils.fromString("allocatedUsageQuantity"); + BString MPM_USAGE_ALLOC_TAGS = StringUtils.fromString("tags"); + + // Constants related to MPM `Tag` + String MPM_TAG = "Tag"; + BString MPM_TAG_KEY = StringUtils.fromString("key"); + BString MPM_TAG_VALUE = StringUtils.fromString("value"); + + // Constants related to MPM `BatchMeterUsageResponse` + String MPM_BATCH_METER_USAGE_RESPONSE = "BatchMeterUsageResponse"; + BString MPM_BATCH_METER_USAGE_RESPONSE_RESULTS = StringUtils.fromString("results"); + BString MPM_BATCH_METER_USAGE_RESPONSE_UNPROC_RECORDS = StringUtils.fromString("unprocessedRecords"); + + // Constants related to MPM `UsageRecordResult` + String MPM_USAGE_RECORD_RESULT = "UsageRecordResult"; + BString MPM_USAGE_RECORD_RESULT_METERING_RECORD = StringUtils.fromString("meteringRecordId"); + BString MPM_USAGE_RECORD_RESULT_STATUS = StringUtils.fromString("status"); + BString MPM_USAGE_RECORD_RESULT_USAGE_RECORD = StringUtils.fromString("usageRecord"); + + // Constants related to MPM Error + String MPM_ERROR = "Error"; + String MPM_ERROR_DETAILS = "ErrorDetails"; + BString MPM_ERROR_DETAILS_HTTP_STATUS_CODE = StringUtils.fromString("httpStatusCode"); + BString MPM_ERROR_DETAILS_HTTP_STATUS_TXT = StringUtils.fromString("httpStatusText"); + BString MPM_ERROR_DETAILS_ERR_CODE = StringUtils.fromString("errorCode"); + BString MPM_ERROR_DETAILS_ERR_MSG = StringUtils.fromString("errorMessage"); +} diff --git a/native/src/main/java/io/ballerina/lib/aws/mpm/NativeClientAdaptor.java b/native/src/main/java/io/ballerina/lib/aws/mpm/NativeClientAdaptor.java new file mode 100644 index 0000000..8a46498 --- /dev/null +++ b/native/src/main/java/io/ballerina/lib/aws/mpm/NativeClientAdaptor.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2024, 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. + */ + +package io.ballerina.lib.aws.mpm; + +import io.ballerina.runtime.api.Environment; +import io.ballerina.runtime.api.Future; +import io.ballerina.runtime.api.values.BError; +import io.ballerina.runtime.api.values.BMap; +import io.ballerina.runtime.api.values.BObject; +import io.ballerina.runtime.api.values.BString; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.AwsCredentials; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.services.marketplacemetering.MarketplaceMeteringClient; +import software.amazon.awssdk.services.marketplacemetering.model.BatchMeterUsageRequest; +import software.amazon.awssdk.services.marketplacemetering.model.BatchMeterUsageResponse; +import software.amazon.awssdk.services.marketplacemetering.model.ResolveCustomerRequest; +import software.amazon.awssdk.services.marketplacemetering.model.ResolveCustomerResponse; + +import java.util.Objects; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * Representation of {@link software.amazon.awssdk.services.marketplacemetering.MarketplaceMeteringClient} with + * utility methods to invoke as inter-op functions. + */ +public final class NativeClientAdaptor { + private static final String NATIVE_CLIENT = "nativeClient"; + private static final ExecutorService EXECUTOR_SERVICE = Executors.newCachedThreadPool(new AwsMpmThreadFactory()); + + private NativeClientAdaptor() { + } + + /** + * Creates an AWS MPM native client with the provided configurations. + * + * @param bAwsMpmClient The Ballerina AWS MPM client object. + * @param configurations AWS MPM client connection configurations. + * @return A Ballerina `mpm:Error` if failed to initialize the native client with the provided configurations. + */ + public static Object init(BObject bAwsMpmClient, BMap configurations) { + try { + ConnectionConfig connectionConfig = new ConnectionConfig(configurations); + AwsCredentials credentials = getCredentials(connectionConfig); + AwsCredentialsProvider credentialsProvider = StaticCredentialsProvider.create(credentials); + MarketplaceMeteringClient nativeClient = MarketplaceMeteringClient.builder() + .credentialsProvider(credentialsProvider) + .region(connectionConfig.region()).build(); + bAwsMpmClient.addNativeData(NATIVE_CLIENT, nativeClient); + } catch (Exception e) { + String errorMsg = String.format("Error occurred while initializing the marketplace metering client: %s", + e.getMessage()); + return CommonUtils.createError(errorMsg, e); + } + return null; + } + + private static AwsCredentials getCredentials(ConnectionConfig connectionConfig) { + if (Objects.nonNull(connectionConfig.sessionToken())) { + return AwsSessionCredentials.create(connectionConfig.accessKeyId(), connectionConfig.secretAccessKey(), + connectionConfig.sessionToken()); + } else { + return AwsBasicCredentials.create(connectionConfig.accessKeyId(), connectionConfig.secretAccessKey()); + } + } + + /** + * Retrieves customer details mapped to a registration token. + * + * @param env The Ballerina runtime environment. + * @param bAwsMpmClient The Ballerina AWS MPM client object. + * @param registrationToken The registration-token provided by the customer. + * @return A Ballerina `mpm:Error` if there was an error while executing the operation or else the AWS MPM + * resolve-customer response. + */ + public static Object resolveCustomer(Environment env, BObject bAwsMpmClient, BString registrationToken) { + MarketplaceMeteringClient nativeClient = (MarketplaceMeteringClient) bAwsMpmClient.getNativeData(NATIVE_CLIENT); + Future future = env.markAsync(); + EXECUTOR_SERVICE.execute(() -> { + try { + ResolveCustomerRequest resolveCustomerReq = ResolveCustomerRequest.builder() + .registrationToken(registrationToken.getValue()).build(); + ResolveCustomerResponse nativeResponse = nativeClient.resolveCustomer(resolveCustomerReq); + BMap bResponse = CommonUtils.getBResolveCustomerResponse(nativeResponse); + future.complete(bResponse); + } catch (Exception e) { + String errorMsg = String.format("Error occurred while executing resolve customer operation: %s", + e.getMessage()); + BError bError = CommonUtils.createError(errorMsg, e); + future.complete(bError); + } + }); + return null; + } + + /** + * Retrieves the post-metering records for a set of customers. + * + * @param env The Ballerina runtime environment. + * @param bAwsMpmClient The Ballerina AWS MPM client object. + * @param request The Ballerina AWS MPM `BatchMeterUsage` request. + * @return A Ballerina `mpm:Error` if there was an error while processing the request or else the AWS MPM + * batch-meter-usage response. + */ + public static Object batchMeterUsage(Environment env, BObject bAwsMpmClient, BMap request) { + MarketplaceMeteringClient nativeClient = (MarketplaceMeteringClient) bAwsMpmClient.getNativeData(NATIVE_CLIENT); + BatchMeterUsageRequest nativeRequest = CommonUtils.getNativeBatchMeterUsageRequest(request); + Future future = env.markAsync(); + EXECUTOR_SERVICE.execute(() -> { + try { + BatchMeterUsageResponse nativeResponse = nativeClient.batchMeterUsage(nativeRequest); + BMap bResponse = CommonUtils.getBBatchMeterUsageResponse(nativeResponse); + future.complete(bResponse); + } catch (Exception e) { + String errorMsg = String.format("Error occurred while executing batch-meter-usage operation: %s", + e.getMessage()); + BError bError = CommonUtils.createError(errorMsg, e); + future.complete(bError); + } + }); + return null; + } + + /** + * Closes the AWS MPM client native resources. + * + * @param bAwsMpmClient The Ballerina AWS MPM client object. + * @return A Ballerina `mpm:Error` if failed to close the underlying resources. + */ + public static Object close(BObject bAwsMpmClient) { + MarketplaceMeteringClient nativeClient = (MarketplaceMeteringClient) bAwsMpmClient.getNativeData(NATIVE_CLIENT); + try { + nativeClient.close(); + } catch (Exception e) { + String errorMsg = String.format("Error occurred while closing the marketplace metering client: %s", + e.getMessage()); + return CommonUtils.createError(errorMsg, e); + } + return null; + } +} diff --git a/native/src/main/java/module-info.java b/native/src/main/java/module-info.java index d6b64a9..1fe958c 100644 --- a/native/src/main/java/module-info.java +++ b/native/src/main/java/module-info.java @@ -19,6 +19,12 @@ module io.ballerina.lib.aws.mpm { requires io.ballerina.runtime; requires io.ballerina.lang.value; + requires io.ballerina.stdlib.time; + requires software.amazon.awssdk.auth; + requires software.amazon.awssdk.regions; + requires software.amazon.awssdk.services.marketplacemetering; + requires software.amazon.awssdk.awscore; + requires software.amazon.awssdk.core; exports io.ballerina.lib.aws.mpm; }