From 9b775d18f4cabb00bc0957ca0b0e9fee1502ab38 Mon Sep 17 00:00:00 2001 From: Mathias Vandaele Date: Thu, 19 Dec 2024 18:06:07 +0100 Subject: [PATCH] feature(aws-s3-connector): AWS s3 connector (#3744) * feature(s3-connector): initial commit * feature(s3-connector): checkpoint s3 connector * feature(s3-connector): checkpoint s3 connector * feature(s3-connector): checkpoint s3 connector * feature(s3-connector): checkpoint s3 connector 3 * feature(s3-connector): checkpoint s3 connector; pom commit * feature(s3-connector): checkpoint s3 connector; pom commit * feature(s3-connector): all features done * feature(s3-connector): correcting after Jonathan comments * feature(s3-connector): minor corrections * feature(s3-connector): minor corrections before rebase * feature(s3-connector): corrections * feature(s3-connector): corrections 2 --- bundle/default-bundle/pom.xml | 4 + bundle/pom.xml | 5 + .../aws-s3-outbound-connector.json | 369 +++++++++++++++++ .../aws-s3-outbound-connector-hybrid.json | 374 ++++++++++++++++++ connectors/aws/aws-s3/pom.xml | 76 ++++ .../connector/aws/s3/S3ConnectorFunction.java | 48 +++ .../connector/aws/s3/core/S3Executor.java | 145 +++++++ .../aws/s3/model/request/DeleteObject.java | 34 ++ .../aws/s3/model/request/DownloadObject.java | 45 +++ .../aws/s3/model/request/S3Action.java | 18 + .../aws/s3/model/request/S3Request.java | 42 ++ .../aws/s3/model/request/UploadObject.java | 45 +++ .../aws/s3/model/response/DeleteResponse.java | 9 + .../s3/model/response/DownloadResponse.java | 9 + .../aws/s3/model/response/Element.java | 17 + .../aws/s3/model/response/UploadResponse.java | 9 + ...tor.api.outbound.OutboundConnectorFunction | 1 + .../aws/aws-s3/src/main/resources/icon.svg | 34 ++ .../io/camunda/connector/aws/s3/BaseTest.java | 65 +++ .../aws/s3/S3ConnectorFunctionTest.java | 78 ++++ .../connector/aws/s3/core/S3ExecutorTest.java | 166 ++++++++ .../actions/deleteActionsExample.json | 15 + .../actions/downloadActionsExample.json | 18 + .../actions/uploadActionsExample.json | 22 ++ connectors/aws/pom.xml | 1 + .../client/jakarta/utils/JakartaUtils.java | 20 +- 26 files changed, 1664 insertions(+), 5 deletions(-) create mode 100644 connectors/aws/aws-s3/element-templates/aws-s3-outbound-connector.json create mode 100644 connectors/aws/aws-s3/element-templates/hybrid/aws-s3-outbound-connector-hybrid.json create mode 100644 connectors/aws/aws-s3/pom.xml create mode 100644 connectors/aws/aws-s3/src/main/java/io/camunda/connector/aws/s3/S3ConnectorFunction.java create mode 100644 connectors/aws/aws-s3/src/main/java/io/camunda/connector/aws/s3/core/S3Executor.java create mode 100644 connectors/aws/aws-s3/src/main/java/io/camunda/connector/aws/s3/model/request/DeleteObject.java create mode 100644 connectors/aws/aws-s3/src/main/java/io/camunda/connector/aws/s3/model/request/DownloadObject.java create mode 100644 connectors/aws/aws-s3/src/main/java/io/camunda/connector/aws/s3/model/request/S3Action.java create mode 100644 connectors/aws/aws-s3/src/main/java/io/camunda/connector/aws/s3/model/request/S3Request.java create mode 100644 connectors/aws/aws-s3/src/main/java/io/camunda/connector/aws/s3/model/request/UploadObject.java create mode 100644 connectors/aws/aws-s3/src/main/java/io/camunda/connector/aws/s3/model/response/DeleteResponse.java create mode 100644 connectors/aws/aws-s3/src/main/java/io/camunda/connector/aws/s3/model/response/DownloadResponse.java create mode 100644 connectors/aws/aws-s3/src/main/java/io/camunda/connector/aws/s3/model/response/Element.java create mode 100644 connectors/aws/aws-s3/src/main/java/io/camunda/connector/aws/s3/model/response/UploadResponse.java create mode 100644 connectors/aws/aws-s3/src/main/resources/META-INF/services/io.camunda.connector.api.outbound.OutboundConnectorFunction create mode 100644 connectors/aws/aws-s3/src/main/resources/icon.svg create mode 100644 connectors/aws/aws-s3/src/test/java/io/camunda/connector/aws/s3/BaseTest.java create mode 100644 connectors/aws/aws-s3/src/test/java/io/camunda/connector/aws/s3/S3ConnectorFunctionTest.java create mode 100644 connectors/aws/aws-s3/src/test/java/io/camunda/connector/aws/s3/core/S3ExecutorTest.java create mode 100644 connectors/aws/aws-s3/src/test/resources/actions/deleteActionsExample.json create mode 100644 connectors/aws/aws-s3/src/test/resources/actions/downloadActionsExample.json create mode 100644 connectors/aws/aws-s3/src/test/resources/actions/uploadActionsExample.json diff --git a/bundle/default-bundle/pom.xml b/bundle/default-bundle/pom.xml index 20e6852152..d125eaa4a2 100644 --- a/bundle/default-bundle/pom.xml +++ b/bundle/default-bundle/pom.xml @@ -37,6 +37,10 @@ io.camunda.connector connector-aws-bedrock + + io.camunda.connector + connector-aws-s3 + io.camunda.connector connector-aws-textract diff --git a/bundle/pom.xml b/bundle/pom.xml index ec364f3c57..b87a055f53 100644 --- a/bundle/pom.xml +++ b/bundle/pom.xml @@ -144,6 +144,11 @@ connector-aws-sagemaker ${project.version} + + io.camunda.connector + connector-aws-s3 + ${project.version} + io.camunda.connector connector-email diff --git a/connectors/aws/aws-s3/element-templates/aws-s3-outbound-connector.json b/connectors/aws/aws-s3/element-templates/aws-s3-outbound-connector.json new file mode 100644 index 0000000000..3d72cb6a73 --- /dev/null +++ b/connectors/aws/aws-s3/element-templates/aws-s3-outbound-connector.json @@ -0,0 +1,369 @@ +{ + "$schema" : "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "name" : "AWS S3 Outbound Connector", + "id" : "io.camunda.connectors.aws.s3.v1", + "description" : "Execute S3 requests", + "metadata" : { + "keywords" : [ ] + }, + "documentationRef" : "https://docs.camunda.io/docs/components/connectors/out-of-the-box-connectors/aws-s3/", + "version" : 1, + "category" : { + "id" : "connectors", + "name" : "Connectors" + }, + "appliesTo" : [ "bpmn:Task" ], + "elementType" : { + "value" : "bpmn:ServiceTask" + }, + "groups" : [ { + "id" : "authentication", + "label" : "Authentication" + }, { + "id" : "configuration", + "label" : "Configuration" + }, { + "id" : "action", + "label" : "Action" + }, { + "id" : "deleteObject", + "label" : "Delete an object" + }, { + "id" : "uploadObject", + "label" : "Upload an object" + }, { + "id" : "downloadObject", + "label" : "Download an object" + }, { + "id" : "output", + "label" : "Output mapping" + }, { + "id" : "error", + "label" : "Error handling" + }, { + "id" : "retries", + "label" : "Retries" + } ], + "properties" : [ { + "value" : "io.camunda:aws-s3:1", + "binding" : { + "property" : "type", + "type" : "zeebe:taskDefinition" + }, + "type" : "Hidden" + }, { + "id" : "authentication.type", + "label" : "Authentication", + "description" : "Specify AWS authentication strategy. Learn more at the documentation page", + "value" : "credentials", + "group" : "authentication", + "binding" : { + "name" : "authentication.type", + "type" : "zeebe:input" + }, + "type" : "Dropdown", + "choices" : [ { + "name" : "Default Credentials Chain (Hybrid/Self-Managed only)", + "value" : "defaultCredentialsChain" + }, { + "name" : "Credentials", + "value" : "credentials" + } ] + }, { + "id" : "authentication.accessKey", + "label" : "Access key", + "description" : "Provide an IAM access key tailored to a user, equipped with the necessary permissions", + "optional" : false, + "constraints" : { + "notEmpty" : true + }, + "feel" : "optional", + "group" : "authentication", + "binding" : { + "name" : "authentication.accessKey", + "type" : "zeebe:input" + }, + "condition" : { + "property" : "authentication.type", + "equals" : "credentials", + "type" : "simple" + }, + "type" : "String" + }, { + "id" : "authentication.secretKey", + "label" : "Secret key", + "description" : "Provide a secret key of a user with permissions to invoke specified AWS Lambda function", + "optional" : false, + "constraints" : { + "notEmpty" : true + }, + "feel" : "optional", + "group" : "authentication", + "binding" : { + "name" : "authentication.secretKey", + "type" : "zeebe:input" + }, + "condition" : { + "property" : "authentication.type", + "equals" : "credentials", + "type" : "simple" + }, + "type" : "String" + }, { + "id" : "configuration.region", + "label" : "Region", + "description" : "Specify the AWS region", + "optional" : false, + "constraints" : { + "notEmpty" : true + }, + "feel" : "optional", + "group" : "configuration", + "binding" : { + "name" : "configuration.region", + "type" : "zeebe:input" + }, + "type" : "String" + }, { + "id" : "configuration.endpoint", + "label" : "Endpoint", + "description" : "Specify endpoint if need to use custom endpoint", + "optional" : true, + "group" : "configuration", + "binding" : { + "name" : "configuration.endpoint", + "type" : "zeebe:input" + }, + "type" : "Hidden" + }, { + "id" : "actionDiscriminator", + "label" : "Action", + "value" : "uploadObject", + "group" : "action", + "binding" : { + "name" : "actionDiscriminator", + "type" : "zeebe:input" + }, + "type" : "Dropdown", + "choices" : [ { + "name" : "Delete object", + "value" : "deleteObject" + }, { + "name" : "Download object", + "value" : "downloadObject" + }, { + "name" : "Upload object", + "value" : "uploadObject" + } ] + }, { + "id" : "deleteActionBucket", + "label" : "AWS bucket", + "optional" : false, + "constraints" : { + "notEmpty" : true + }, + "feel" : "optional", + "group" : "deleteObject", + "binding" : { + "name" : "action.bucket", + "type" : "zeebe:input" + }, + "condition" : { + "property" : "actionDiscriminator", + "equals" : "deleteObject", + "type" : "simple" + }, + "tooltip" : "Bucket from where an object should be deleted", + "type" : "String" + }, { + "id" : "deleteActionKey", + "label" : "AWS key", + "optional" : false, + "constraints" : { + "notEmpty" : true + }, + "feel" : "optional", + "group" : "deleteObject", + "binding" : { + "name" : "action.key", + "type" : "zeebe:input" + }, + "condition" : { + "property" : "actionDiscriminator", + "equals" : "deleteObject", + "type" : "simple" + }, + "tooltip" : "Key of the object which should be deleted", + "type" : "String" + }, { + "id" : "uploadActionBucket", + "label" : "AWS bucket", + "optional" : false, + "constraints" : { + "notEmpty" : true + }, + "feel" : "optional", + "group" : "uploadObject", + "binding" : { + "name" : "action.bucket", + "type" : "zeebe:input" + }, + "condition" : { + "property" : "actionDiscriminator", + "equals" : "uploadObject", + "type" : "simple" + }, + "tooltip" : "Bucket from where an object should be uploaded", + "type" : "String" + }, { + "id" : "uploadActionKey", + "label" : "AWS key", + "optional" : true, + "feel" : "optional", + "group" : "uploadObject", + "binding" : { + "name" : "action.key", + "type" : "zeebe:input" + }, + "condition" : { + "property" : "actionDiscriminator", + "equals" : "uploadObject", + "type" : "simple" + }, + "tooltip" : "Key of the uploaded object, if not given. The file name from the document metadata will be used", + "type" : "String" + }, { + "id" : "uploadActionDocument", + "label" : "Document", + "optional" : false, + "feel" : "required", + "group" : "uploadObject", + "binding" : { + "name" : "action.document", + "type" : "zeebe:input" + }, + "condition" : { + "property" : "actionDiscriminator", + "equals" : "uploadObject", + "type" : "simple" + }, + "tooltip" : "Document to be uploaded on AWS S3", + "type" : "String" + }, { + "id" : "downloadActionBucket", + "label" : "AWS bucket", + "optional" : false, + "constraints" : { + "notEmpty" : true + }, + "feel" : "optional", + "group" : "downloadObject", + "binding" : { + "name" : "action.bucket", + "type" : "zeebe:input" + }, + "condition" : { + "property" : "actionDiscriminator", + "equals" : "downloadObject", + "type" : "simple" + }, + "tooltip" : "Bucket from where an object should be downloaded", + "type" : "String" + }, { + "id" : "downloadActionKey", + "label" : "AWS key", + "optional" : false, + "constraints" : { + "notEmpty" : true + }, + "feel" : "optional", + "group" : "downloadObject", + "binding" : { + "name" : "action.key", + "type" : "zeebe:input" + }, + "condition" : { + "property" : "actionDiscriminator", + "equals" : "downloadObject", + "type" : "simple" + }, + "tooltip" : "Key of the object which should be download", + "type" : "String" + }, { + "id" : "downloadActionAsFile", + "label" : "Create document", + "optional" : false, + "value" : true, + "feel" : "optional", + "group" : "downloadObject", + "binding" : { + "name" : "action.asFile", + "type" : "zeebe:input" + }, + "condition" : { + "property" : "actionDiscriminator", + "equals" : "downloadObject", + "type" : "simple" + }, + "tooltip" : "If set to true, a document reference will be created. If set to false, the content will be extracted and provided inside the response.", + "type" : "Boolean" + }, { + "id" : "resultVariable", + "label" : "Result variable", + "description" : "Name of variable to store the response in", + "group" : "output", + "binding" : { + "key" : "resultVariable", + "type" : "zeebe:taskHeader" + }, + "type" : "String" + }, { + "id" : "resultExpression", + "label" : "Result expression", + "description" : "Expression to map the response into process variables", + "feel" : "required", + "group" : "output", + "binding" : { + "key" : "resultExpression", + "type" : "zeebe:taskHeader" + }, + "type" : "Text" + }, { + "id" : "errorExpression", + "label" : "Error expression", + "description" : "Expression to handle errors. Details in the documentation.", + "feel" : "required", + "group" : "error", + "binding" : { + "key" : "errorExpression", + "type" : "zeebe:taskHeader" + }, + "type" : "Text" + }, { + "id" : "retryCount", + "label" : "Retries", + "description" : "Number of retries", + "value" : "3", + "feel" : "optional", + "group" : "retries", + "binding" : { + "property" : "retries", + "type" : "zeebe:taskDefinition" + }, + "type" : "String" + }, { + "id" : "retryBackoff", + "label" : "Retry backoff", + "description" : "ISO-8601 duration to wait between retries", + "value" : "PT0S", + "group" : "retries", + "binding" : { + "key" : "retryBackoff", + "type" : "zeebe:taskHeader" + }, + "type" : "String" + } ], + "icon" : { + "contents" : "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MjgiIGhlaWdodD0iNTEyIiB2aWV3Qm94PSIwIDAgNDI4IDUxMiI+CiAgPGRlZnM+CiAgICA8c3R5bGU+CiAgICAgIC5jbHMtMSB7CiAgICAgICAgZmlsbDogI2UyNTQ0NDsKICAgICAgfQoKICAgICAgLmNscy0xLCAuY2xzLTIsIC5jbHMtMyB7CiAgICAgICAgZmlsbC1ydWxlOiBldmVub2RkOwogICAgICB9CgogICAgICAuY2xzLTIgewogICAgICAgIGZpbGw6ICM3YjFkMTM7CiAgICAgIH0KCiAgICAgIC5jbHMtMyB7CiAgICAgICAgZmlsbDogIzU4MTUwZDsKICAgICAgfQogICAgPC9zdHlsZT4KICA8L2RlZnM+CiAgPHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMzc4LDk5TDI5NSwyNTdsODMsMTU4LDM0LTE5VjExOFoiLz4KICA8cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0zNzgsOTlMMjEyLDExOCwxMjcuNSwyNTcsMjEyLDM5NmwxNjYsMTlWOTlaIi8+CiAgPHBhdGggY2xhc3M9ImNscy0zIiBkPSJNNDMsOTlMMTYsMTExVjQwM2wyNywxMkwyMTIsMjU3WiIvPgogIDxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTQyLjYzNyw5OC42NjdsMTY5LjU4Nyw0Ny4xMTFWMzcyLjQ0NEw0Mi42MzcsNDE1LjExMVY5OC42NjdaIi8+CiAgPHBhdGggY2xhc3M9ImNscy0zIiBkPSJNMjEyLjMxMywxNzAuNjY3bC03Mi4wMDgtMTEuNTU2LDcyLjAwOC04MS43NzgsNzEuODMsODEuNzc4WiIvPgogIDxwYXRoIGNsYXNzPSJjbHMtMyIgZD0iTTI4NC4xNDMsMTU5LjExMWwtNzEuOTE5LDExLjczMy03MS45MTktMTEuNzMzVjc3LjMzMyIvPgogIDxwYXRoIGNsYXNzPSJjbHMtMyIgZD0iTTIxMi4zMTMsMzQyLjIyMmwtNzIuMDA4LDEzLjMzNCw3Mi4wMDgsNzAuMjIyLDcxLjgzLTcwLjIyMloiLz4KICA8cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0yMTIsMTZMMTQwLDU0VjE1OWw3Mi4yMjQtMjAuMzMzWiIvPgogIDxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTIxMi4yMjQsMTk2LjQ0NGwtNzEuOTE5LDcuODIzVjMwOS4xMDVsNzEuOTE5LDguMjI4VjE5Ni40NDRaIi8+CiAgPHBhdGggY2xhc3M9ImNscy0yIiBkPSJNMjEyLjIyNCwzNzMuMzMzTDE0MC4zMDUsMzU1LjNWNDU4LjM2M0wyMTIuMjI0LDQ5NlYzNzMuMzMzWiIvPgogIDxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTI4NC4xNDMsMzU1LjNsLTcxLjkxOSwxOC4wMzhWNDk2bDcxLjkxOS0zNy42MzdWMzU1LjNaIi8+CiAgPHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMjEyLjIyNCwxOTYuNDQ0bDcxLjkxOSw3LjgyM1YzMDkuMTA1bC03MS45MTksOC4yMjhWMTk2LjQ0NFoiLz4KICA8cGF0aCBjbGFzcz0iY2xzLTEiIGQ9Ik0yMTIsMTZsNzIsMzhWMTU5bC03Mi0yMFYxNloiLz4KPC9zdmc+Cg==" + } +} \ No newline at end of file diff --git a/connectors/aws/aws-s3/element-templates/hybrid/aws-s3-outbound-connector-hybrid.json b/connectors/aws/aws-s3/element-templates/hybrid/aws-s3-outbound-connector-hybrid.json new file mode 100644 index 0000000000..b9c4b1c960 --- /dev/null +++ b/connectors/aws/aws-s3/element-templates/hybrid/aws-s3-outbound-connector-hybrid.json @@ -0,0 +1,374 @@ +{ + "$schema" : "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "name" : "Hybrid AWS S3 Outbound Connector", + "id" : "io.camunda.connectors.aws.s3.v1-hybrid", + "description" : "Execute S3 requests", + "metadata" : { + "keywords" : [ ] + }, + "documentationRef" : "https://docs.camunda.io/docs/components/connectors/out-of-the-box-connectors/aws-s3/", + "version" : 1, + "category" : { + "id" : "connectors", + "name" : "Connectors" + }, + "appliesTo" : [ "bpmn:Task" ], + "elementType" : { + "value" : "bpmn:ServiceTask" + }, + "groups" : [ { + "id" : "taskDefinitionType", + "label" : "Task definition type" + }, { + "id" : "authentication", + "label" : "Authentication" + }, { + "id" : "configuration", + "label" : "Configuration" + }, { + "id" : "action", + "label" : "Action" + }, { + "id" : "deleteObject", + "label" : "Delete an object" + }, { + "id" : "uploadObject", + "label" : "Upload an object" + }, { + "id" : "downloadObject", + "label" : "Download an object" + }, { + "id" : "output", + "label" : "Output mapping" + }, { + "id" : "error", + "label" : "Error handling" + }, { + "id" : "retries", + "label" : "Retries" + } ], + "properties" : [ { + "id" : "taskDefinitionType", + "value" : "io.camunda:aws-s3:1", + "group" : "taskDefinitionType", + "binding" : { + "property" : "type", + "type" : "zeebe:taskDefinition" + }, + "type" : "String" + }, { + "id" : "authentication.type", + "label" : "Authentication", + "description" : "Specify AWS authentication strategy. Learn more at the documentation page", + "value" : "credentials", + "group" : "authentication", + "binding" : { + "name" : "authentication.type", + "type" : "zeebe:input" + }, + "type" : "Dropdown", + "choices" : [ { + "name" : "Default Credentials Chain (Hybrid/Self-Managed only)", + "value" : "defaultCredentialsChain" + }, { + "name" : "Credentials", + "value" : "credentials" + } ] + }, { + "id" : "authentication.accessKey", + "label" : "Access key", + "description" : "Provide an IAM access key tailored to a user, equipped with the necessary permissions", + "optional" : false, + "constraints" : { + "notEmpty" : true + }, + "feel" : "optional", + "group" : "authentication", + "binding" : { + "name" : "authentication.accessKey", + "type" : "zeebe:input" + }, + "condition" : { + "property" : "authentication.type", + "equals" : "credentials", + "type" : "simple" + }, + "type" : "String" + }, { + "id" : "authentication.secretKey", + "label" : "Secret key", + "description" : "Provide a secret key of a user with permissions to invoke specified AWS Lambda function", + "optional" : false, + "constraints" : { + "notEmpty" : true + }, + "feel" : "optional", + "group" : "authentication", + "binding" : { + "name" : "authentication.secretKey", + "type" : "zeebe:input" + }, + "condition" : { + "property" : "authentication.type", + "equals" : "credentials", + "type" : "simple" + }, + "type" : "String" + }, { + "id" : "configuration.region", + "label" : "Region", + "description" : "Specify the AWS region", + "optional" : false, + "constraints" : { + "notEmpty" : true + }, + "feel" : "optional", + "group" : "configuration", + "binding" : { + "name" : "configuration.region", + "type" : "zeebe:input" + }, + "type" : "String" + }, { + "id" : "configuration.endpoint", + "label" : "Endpoint", + "description" : "Specify endpoint if need to use custom endpoint", + "optional" : true, + "group" : "configuration", + "binding" : { + "name" : "configuration.endpoint", + "type" : "zeebe:input" + }, + "type" : "Hidden" + }, { + "id" : "actionDiscriminator", + "label" : "Action", + "value" : "uploadObject", + "group" : "action", + "binding" : { + "name" : "actionDiscriminator", + "type" : "zeebe:input" + }, + "type" : "Dropdown", + "choices" : [ { + "name" : "Delete object", + "value" : "deleteObject" + }, { + "name" : "Download object", + "value" : "downloadObject" + }, { + "name" : "Upload object", + "value" : "uploadObject" + } ] + }, { + "id" : "deleteActionBucket", + "label" : "AWS bucket", + "optional" : false, + "constraints" : { + "notEmpty" : true + }, + "feel" : "optional", + "group" : "deleteObject", + "binding" : { + "name" : "action.bucket", + "type" : "zeebe:input" + }, + "condition" : { + "property" : "actionDiscriminator", + "equals" : "deleteObject", + "type" : "simple" + }, + "tooltip" : "Bucket from where an object should be deleted", + "type" : "String" + }, { + "id" : "deleteActionKey", + "label" : "AWS key", + "optional" : false, + "constraints" : { + "notEmpty" : true + }, + "feel" : "optional", + "group" : "deleteObject", + "binding" : { + "name" : "action.key", + "type" : "zeebe:input" + }, + "condition" : { + "property" : "actionDiscriminator", + "equals" : "deleteObject", + "type" : "simple" + }, + "tooltip" : "Key of the object which should be deleted", + "type" : "String" + }, { + "id" : "uploadActionBucket", + "label" : "AWS bucket", + "optional" : false, + "constraints" : { + "notEmpty" : true + }, + "feel" : "optional", + "group" : "uploadObject", + "binding" : { + "name" : "action.bucket", + "type" : "zeebe:input" + }, + "condition" : { + "property" : "actionDiscriminator", + "equals" : "uploadObject", + "type" : "simple" + }, + "tooltip" : "Bucket from where an object should be uploaded", + "type" : "String" + }, { + "id" : "uploadActionKey", + "label" : "AWS key", + "optional" : true, + "feel" : "optional", + "group" : "uploadObject", + "binding" : { + "name" : "action.key", + "type" : "zeebe:input" + }, + "condition" : { + "property" : "actionDiscriminator", + "equals" : "uploadObject", + "type" : "simple" + }, + "tooltip" : "Key of the uploaded object, if not given. The file name from the document metadata will be used", + "type" : "String" + }, { + "id" : "uploadActionDocument", + "label" : "Document", + "optional" : false, + "feel" : "required", + "group" : "uploadObject", + "binding" : { + "name" : "action.document", + "type" : "zeebe:input" + }, + "condition" : { + "property" : "actionDiscriminator", + "equals" : "uploadObject", + "type" : "simple" + }, + "tooltip" : "Document to be uploaded on AWS S3", + "type" : "String" + }, { + "id" : "downloadActionBucket", + "label" : "AWS bucket", + "optional" : false, + "constraints" : { + "notEmpty" : true + }, + "feel" : "optional", + "group" : "downloadObject", + "binding" : { + "name" : "action.bucket", + "type" : "zeebe:input" + }, + "condition" : { + "property" : "actionDiscriminator", + "equals" : "downloadObject", + "type" : "simple" + }, + "tooltip" : "Bucket from where an object should be downloaded", + "type" : "String" + }, { + "id" : "downloadActionKey", + "label" : "AWS key", + "optional" : false, + "constraints" : { + "notEmpty" : true + }, + "feel" : "optional", + "group" : "downloadObject", + "binding" : { + "name" : "action.key", + "type" : "zeebe:input" + }, + "condition" : { + "property" : "actionDiscriminator", + "equals" : "downloadObject", + "type" : "simple" + }, + "tooltip" : "Key of the object which should be download", + "type" : "String" + }, { + "id" : "downloadActionAsFile", + "label" : "Create document", + "optional" : false, + "value" : true, + "feel" : "optional", + "group" : "downloadObject", + "binding" : { + "name" : "action.asFile", + "type" : "zeebe:input" + }, + "condition" : { + "property" : "actionDiscriminator", + "equals" : "downloadObject", + "type" : "simple" + }, + "tooltip" : "If set to true, a document reference will be created. If set to false, the content will be extracted and provided inside the response.", + "type" : "Boolean" + }, { + "id" : "resultVariable", + "label" : "Result variable", + "description" : "Name of variable to store the response in", + "group" : "output", + "binding" : { + "key" : "resultVariable", + "type" : "zeebe:taskHeader" + }, + "type" : "String" + }, { + "id" : "resultExpression", + "label" : "Result expression", + "description" : "Expression to map the response into process variables", + "feel" : "required", + "group" : "output", + "binding" : { + "key" : "resultExpression", + "type" : "zeebe:taskHeader" + }, + "type" : "Text" + }, { + "id" : "errorExpression", + "label" : "Error expression", + "description" : "Expression to handle errors. Details in the documentation.", + "feel" : "required", + "group" : "error", + "binding" : { + "key" : "errorExpression", + "type" : "zeebe:taskHeader" + }, + "type" : "Text" + }, { + "id" : "retryCount", + "label" : "Retries", + "description" : "Number of retries", + "value" : "3", + "feel" : "optional", + "group" : "retries", + "binding" : { + "property" : "retries", + "type" : "zeebe:taskDefinition" + }, + "type" : "String" + }, { + "id" : "retryBackoff", + "label" : "Retry backoff", + "description" : "ISO-8601 duration to wait between retries", + "value" : "PT0S", + "group" : "retries", + "binding" : { + "key" : "retryBackoff", + "type" : "zeebe:taskHeader" + }, + "type" : "String" + } ], + "icon" : { + "contents" : "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MjgiIGhlaWdodD0iNTEyIiB2aWV3Qm94PSIwIDAgNDI4IDUxMiI+CiAgPGRlZnM+CiAgICA8c3R5bGU+CiAgICAgIC5jbHMtMSB7CiAgICAgICAgZmlsbDogI2UyNTQ0NDsKICAgICAgfQoKICAgICAgLmNscy0xLCAuY2xzLTIsIC5jbHMtMyB7CiAgICAgICAgZmlsbC1ydWxlOiBldmVub2RkOwogICAgICB9CgogICAgICAuY2xzLTIgewogICAgICAgIGZpbGw6ICM3YjFkMTM7CiAgICAgIH0KCiAgICAgIC5jbHMtMyB7CiAgICAgICAgZmlsbDogIzU4MTUwZDsKICAgICAgfQogICAgPC9zdHlsZT4KICA8L2RlZnM+CiAgPHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMzc4LDk5TDI5NSwyNTdsODMsMTU4LDM0LTE5VjExOFoiLz4KICA8cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0zNzgsOTlMMjEyLDExOCwxMjcuNSwyNTcsMjEyLDM5NmwxNjYsMTlWOTlaIi8+CiAgPHBhdGggY2xhc3M9ImNscy0zIiBkPSJNNDMsOTlMMTYsMTExVjQwM2wyNywxMkwyMTIsMjU3WiIvPgogIDxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTQyLjYzNyw5OC42NjdsMTY5LjU4Nyw0Ny4xMTFWMzcyLjQ0NEw0Mi42MzcsNDE1LjExMVY5OC42NjdaIi8+CiAgPHBhdGggY2xhc3M9ImNscy0zIiBkPSJNMjEyLjMxMywxNzAuNjY3bC03Mi4wMDgtMTEuNTU2LDcyLjAwOC04MS43NzgsNzEuODMsODEuNzc4WiIvPgogIDxwYXRoIGNsYXNzPSJjbHMtMyIgZD0iTTI4NC4xNDMsMTU5LjExMWwtNzEuOTE5LDExLjczMy03MS45MTktMTEuNzMzVjc3LjMzMyIvPgogIDxwYXRoIGNsYXNzPSJjbHMtMyIgZD0iTTIxMi4zMTMsMzQyLjIyMmwtNzIuMDA4LDEzLjMzNCw3Mi4wMDgsNzAuMjIyLDcxLjgzLTcwLjIyMloiLz4KICA8cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0yMTIsMTZMMTQwLDU0VjE1OWw3Mi4yMjQtMjAuMzMzWiIvPgogIDxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTIxMi4yMjQsMTk2LjQ0NGwtNzEuOTE5LDcuODIzVjMwOS4xMDVsNzEuOTE5LDguMjI4VjE5Ni40NDRaIi8+CiAgPHBhdGggY2xhc3M9ImNscy0yIiBkPSJNMjEyLjIyNCwzNzMuMzMzTDE0MC4zMDUsMzU1LjNWNDU4LjM2M0wyMTIuMjI0LDQ5NlYzNzMuMzMzWiIvPgogIDxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTI4NC4xNDMsMzU1LjNsLTcxLjkxOSwxOC4wMzhWNDk2bDcxLjkxOS0zNy42MzdWMzU1LjNaIi8+CiAgPHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMjEyLjIyNCwxOTYuNDQ0bDcxLjkxOSw3LjgyM1YzMDkuMTA1bC03MS45MTksOC4yMjhWMTk2LjQ0NFoiLz4KICA8cGF0aCBjbGFzcz0iY2xzLTEiIGQ9Ik0yMTIsMTZsNzIsMzhWMTU5bC03Mi0yMFYxNloiLz4KPC9zdmc+Cg==" + } +} \ No newline at end of file diff --git a/connectors/aws/aws-s3/pom.xml b/connectors/aws/aws-s3/pom.xml new file mode 100644 index 0000000000..ed716ceee4 --- /dev/null +++ b/connectors/aws/aws-s3/pom.xml @@ -0,0 +1,76 @@ + + + 4.0.0 + + + io.camunda.connector + connectors-parent + 8.7.0-SNAPSHOT + ../../pom.xml + + + connector-aws-s3 + Camunda Connector AWS S3 + connector-aws-s3 + jar + + + + Camunda Self-Managed Free Edition license + + https://camunda.com/legal/terms/cloud-terms-and-conditions/camunda-cloud-self-managed-free-edition-terms/ + + + + Camunda Self-Managed Enterprise Edition license + + + + + + io.camunda.connector + connector-aws-base + ${project.version} + + + software.amazon.awssdk + s3 + ${version.software-aws-java-sdk} + + + software.amazon.awssdk + s3-transfer-manager + ${version.software-aws-java-sdk} + + + + + + + io.camunda.connector + element-template-generator-maven-plugin + ${project.version} + + + + io.camunda.connector.aws.s3.S3ConnectorFunction + + + io.camunda.connectors.aws.s3.v1 + aws-s3-outbound-connector.json + + + true + + + + io.camunda.connector:connector-aws-base + + + + + + + \ No newline at end of file diff --git a/connectors/aws/aws-s3/src/main/java/io/camunda/connector/aws/s3/S3ConnectorFunction.java b/connectors/aws/aws-s3/src/main/java/io/camunda/connector/aws/s3/S3ConnectorFunction.java new file mode 100644 index 0000000000..83211e6426 --- /dev/null +++ b/connectors/aws/aws-s3/src/main/java/io/camunda/connector/aws/s3/S3ConnectorFunction.java @@ -0,0 +1,48 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. Licensed under a proprietary license. + * See the License.txt file for more information. You may not use this file + * except in compliance with the proprietary license. + */ +package io.camunda.connector.aws.s3; + +import io.camunda.connector.api.annotation.OutboundConnector; +import io.camunda.connector.api.outbound.OutboundConnectorContext; +import io.camunda.connector.api.outbound.OutboundConnectorFunction; +import io.camunda.connector.aws.s3.core.S3Executor; +import io.camunda.connector.aws.s3.model.request.S3Request; +import io.camunda.connector.generator.java.annotation.ElementTemplate; +import io.camunda.document.Document; +import io.camunda.document.store.DocumentCreationRequest; +import java.util.function.Function; + +@OutboundConnector( + name = "AWS S3", + inputVariables = {"authentication", "configuration", "actionDiscriminator", "action"}, + type = "io.camunda:aws-s3:1") +@ElementTemplate( + id = "io.camunda.connectors.aws.s3.v1", + name = "AWS S3 Outbound Connector", + description = "Execute S3 requests", + inputDataClass = S3Request.class, + version = 1, + propertyGroups = { + @ElementTemplate.PropertyGroup(id = "authentication", label = "Authentication"), + @ElementTemplate.PropertyGroup(id = "configuration", label = "Configuration"), + @ElementTemplate.PropertyGroup(id = "action", label = "Action"), + @ElementTemplate.PropertyGroup(id = "deleteObject", label = "Delete an object"), + @ElementTemplate.PropertyGroup(id = "uploadObject", label = "Upload an object"), + @ElementTemplate.PropertyGroup(id = "downloadObject", label = "Download an object"), + }, + documentationRef = + "https://docs.camunda.io/docs/components/connectors/out-of-the-box-connectors/aws-s3/", + icon = "icon.svg") +public class S3ConnectorFunction implements OutboundConnectorFunction { + + @Override + public Object execute(OutboundConnectorContext context) { + Function createDocument = context::createDocument; + S3Request s3Request = context.bindVariables(S3Request.class); + return S3Executor.create(s3Request, createDocument).execute(s3Request.getAction()); + } +} diff --git a/connectors/aws/aws-s3/src/main/java/io/camunda/connector/aws/s3/core/S3Executor.java b/connectors/aws/aws-s3/src/main/java/io/camunda/connector/aws/s3/core/S3Executor.java new file mode 100644 index 0000000000..a16f862d42 --- /dev/null +++ b/connectors/aws/aws-s3/src/main/java/io/camunda/connector/aws/s3/core/S3Executor.java @@ -0,0 +1,145 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. Licensed under a proprietary license. + * See the License.txt file for more information. You may not use this file + * except in compliance with the proprietary license. + */ +package io.camunda.connector.aws.s3.core; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.camunda.connector.aws.CredentialsProviderSupportV2; +import io.camunda.connector.aws.s3.model.request.*; +import io.camunda.connector.aws.s3.model.response.DeleteResponse; +import io.camunda.connector.aws.s3.model.response.DownloadResponse; +import io.camunda.connector.aws.s3.model.response.Element; +import io.camunda.connector.aws.s3.model.response.UploadResponse; +import io.camunda.document.Document; +import io.camunda.document.store.DocumentCreationRequest; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Optional; +import java.util.function.Function; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.awssdk.core.ResponseInputStream; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; + +public class S3Executor { + + private static final Logger log = LoggerFactory.getLogger(S3Executor.class); + private final S3Client s3Client; + private final Function createDocument; + + public S3Executor(S3Client s3Client, Function createDocument) { + this.s3Client = s3Client; + this.createDocument = createDocument; + } + + public static S3Executor create( + S3Request s3Request, Function createDocument) { + return new S3Executor( + S3Client.builder() + .credentialsProvider(CredentialsProviderSupportV2.credentialsProvider(s3Request)) + .region(Region.of(s3Request.getConfiguration().region())) + .build(), + createDocument); + } + + public Object execute(S3Action s3Action) { + return switch (s3Action) { + case DeleteObject deleteObject -> delete(deleteObject); + case DownloadObject downloadObject -> download(downloadObject); + case UploadObject uploadObject -> upload(uploadObject); + }; + } + + private Object upload(UploadObject uploadObject) { + Long contentLength = uploadObject.document().metadata().getSize(); + String contentType = uploadObject.document().metadata().getContentType(); + + PutObjectRequest putObjectRequest = + PutObjectRequest.builder() + .bucket(uploadObject.bucket()) + .key( + Optional.ofNullable(uploadObject.key()) + .orElse(uploadObject.document().metadata().getFileName())) + .contentLength(contentLength) + .contentType(contentType) + .build(); + + this.s3Client.putObject( + putObjectRequest, + RequestBody.fromInputStream(uploadObject.document().asInputStream(), contentLength)); + + return new UploadResponse( + uploadObject.bucket(), + uploadObject.key(), + String.format("https://%s.s3.amazonaws.com/%s", uploadObject.bucket(), uploadObject.key())); + } + + private DownloadResponse download(DownloadObject downloadObject) { + GetObjectRequest getObjectRequest = + GetObjectRequest.builder() + .bucket(downloadObject.bucket()) + .key(downloadObject.key()) + .build(); + + ResponseInputStream getObjectResponse = + this.s3Client.getObject(getObjectRequest); + + if (!downloadObject.asFile()) { + try { + return retrieveResponseWithContent( + downloadObject.bucket(), downloadObject.key(), getObjectResponse); + } catch (IOException e) { + log.error("An error occurred while trying to read and parse the downloaded file", e); + throw new RuntimeException(e); + } + } else { + return this.createDocument + .andThen( + document -> + new DownloadResponse( + downloadObject.bucket(), + downloadObject.key(), + new Element.DocumentContent(document))) + .apply( + DocumentCreationRequest.from(getObjectResponse) + .contentType(getObjectResponse.response().contentType()) + .fileName(downloadObject.key()) + .build()); + } + } + + private DownloadResponse retrieveResponseWithContent( + String bucket, String key, ResponseInputStream responseResponseInputStream) + throws IOException { + byte[] rawBytes = responseResponseInputStream.readAllBytes(); + return switch (responseResponseInputStream.response().contentType()) { + case "text/plain" -> + new DownloadResponse( + bucket, key, new Element.StringContent(new String(rawBytes, StandardCharsets.UTF_8))); + case "application/json" -> + new DownloadResponse( + bucket, key, new Element.JsonContent(new ObjectMapper().readTree(rawBytes))); + default -> + new DownloadResponse( + bucket, key, new Element.StringContent(Base64.getEncoder().encodeToString(rawBytes))); + }; + } + + private DeleteResponse delete(DeleteObject deleteObject) { + DeleteObjectRequest deleteObjectRequest = + DeleteObjectRequest.builder().bucket(deleteObject.bucket()).key(deleteObject.key()).build(); + + this.s3Client.deleteObject(deleteObjectRequest); + return new DeleteResponse(deleteObject.bucket(), deleteObject.key()); + } +} diff --git a/connectors/aws/aws-s3/src/main/java/io/camunda/connector/aws/s3/model/request/DeleteObject.java b/connectors/aws/aws-s3/src/main/java/io/camunda/connector/aws/s3/model/request/DeleteObject.java new file mode 100644 index 0000000000..9b08f51cbf --- /dev/null +++ b/connectors/aws/aws-s3/src/main/java/io/camunda/connector/aws/s3/model/request/DeleteObject.java @@ -0,0 +1,34 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. Licensed under a proprietary license. + * See the License.txt file for more information. You may not use this file + * except in compliance with the proprietary license. + */ +package io.camunda.connector.aws.s3.model.request; + +import io.camunda.connector.generator.dsl.Property; +import io.camunda.connector.generator.java.annotation.TemplateProperty; +import io.camunda.connector.generator.java.annotation.TemplateSubType; +import jakarta.validation.constraints.NotBlank; + +@TemplateSubType(id = "deleteObject", label = "Delete object") +public record DeleteObject( + @TemplateProperty( + label = "AWS bucket", + id = "deleteActionBucket", + group = "deleteObject", + tooltip = "Bucket from where an object should be deleted", + feel = Property.FeelMode.optional, + binding = @TemplateProperty.PropertyBinding(name = "action.bucket")) + @NotBlank + String bucket, + @TemplateProperty( + label = "AWS key", + id = "deleteActionKey", + group = "deleteObject", + tooltip = "Key of the object which should be deleted", + feel = Property.FeelMode.optional, + binding = @TemplateProperty.PropertyBinding(name = "action.key")) + @NotBlank + String key) + implements S3Action {} diff --git a/connectors/aws/aws-s3/src/main/java/io/camunda/connector/aws/s3/model/request/DownloadObject.java b/connectors/aws/aws-s3/src/main/java/io/camunda/connector/aws/s3/model/request/DownloadObject.java new file mode 100644 index 0000000000..1cec918434 --- /dev/null +++ b/connectors/aws/aws-s3/src/main/java/io/camunda/connector/aws/s3/model/request/DownloadObject.java @@ -0,0 +1,45 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. Licensed under a proprietary license. + * See the License.txt file for more information. You may not use this file + * except in compliance with the proprietary license. + */ +package io.camunda.connector.aws.s3.model.request; + +import io.camunda.connector.generator.dsl.Property; +import io.camunda.connector.generator.java.annotation.TemplateProperty; +import io.camunda.connector.generator.java.annotation.TemplateSubType; +import jakarta.validation.constraints.NotBlank; + +@TemplateSubType(id = "downloadObject", label = "Download object") +public record DownloadObject( + @TemplateProperty( + label = "AWS bucket", + id = "downloadActionBucket", + group = "downloadObject", + tooltip = "Bucket from where an object should be downloaded", + feel = Property.FeelMode.optional, + binding = @TemplateProperty.PropertyBinding(name = "action.bucket")) + @NotBlank + String bucket, + @TemplateProperty( + label = "AWS key", + id = "downloadActionKey", + group = "downloadObject", + tooltip = "Key of the object which should be download", + feel = Property.FeelMode.optional, + binding = @TemplateProperty.PropertyBinding(name = "action.key")) + @NotBlank + String key, + @TemplateProperty( + label = "Create document", + id = "downloadActionAsFile", + group = "downloadObject", + tooltip = + "If set to true, a document reference will be created. If set to false, the content will be extracted and provided inside the response.", + type = TemplateProperty.PropertyType.Boolean, + defaultValueType = TemplateProperty.DefaultValueType.Boolean, + defaultValue = "true", + binding = @TemplateProperty.PropertyBinding(name = "action.asFile")) + boolean asFile) + implements S3Action {} diff --git a/connectors/aws/aws-s3/src/main/java/io/camunda/connector/aws/s3/model/request/S3Action.java b/connectors/aws/aws-s3/src/main/java/io/camunda/connector/aws/s3/model/request/S3Action.java new file mode 100644 index 0000000000..9ecbb7d891 --- /dev/null +++ b/connectors/aws/aws-s3/src/main/java/io/camunda/connector/aws/s3/model/request/S3Action.java @@ -0,0 +1,18 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. Licensed under a proprietary license. + * See the License.txt file for more information. You may not use this file + * except in compliance with the proprietary license. + */ +package io.camunda.connector.aws.s3.model.request; + +import io.camunda.connector.generator.java.annotation.TemplateDiscriminatorProperty; +import io.camunda.connector.generator.java.annotation.TemplateSubType; + +@TemplateDiscriminatorProperty( + label = "Action", + group = "action", + name = "actionDiscriminator", + defaultValue = "uploadObject") +@TemplateSubType(id = "action", label = "Action") +public sealed interface S3Action permits DeleteObject, DownloadObject, UploadObject {} diff --git a/connectors/aws/aws-s3/src/main/java/io/camunda/connector/aws/s3/model/request/S3Request.java b/connectors/aws/aws-s3/src/main/java/io/camunda/connector/aws/s3/model/request/S3Request.java new file mode 100644 index 0000000000..5cbdd21db8 --- /dev/null +++ b/connectors/aws/aws-s3/src/main/java/io/camunda/connector/aws/s3/model/request/S3Request.java @@ -0,0 +1,42 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. Licensed under a proprietary license. + * See the License.txt file for more information. You may not use this file + * except in compliance with the proprietary license. + */ +package io.camunda.connector.aws.s3.model.request; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.camunda.connector.aws.model.impl.AwsBaseRequest; +import io.camunda.connector.generator.java.annotation.NestedProperties; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; + +public class S3Request extends AwsBaseRequest { + + @JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.EXTERNAL_PROPERTY, + property = "actionDiscriminator") + @JsonSubTypes( + value = { + @JsonSubTypes.Type(value = DeleteObject.class, name = "deleteObject"), + @JsonSubTypes.Type(value = UploadObject.class, name = "uploadObject"), + @JsonSubTypes.Type(value = DownloadObject.class, name = "downloadObject"), + }) + @Valid + @NotNull + @NestedProperties(addNestedPath = false) + private S3Action action; + + public S3Request() {} + + public S3Action getAction() { + return action; + } + + public void setAction(S3Action action) { + this.action = action; + } +} diff --git a/connectors/aws/aws-s3/src/main/java/io/camunda/connector/aws/s3/model/request/UploadObject.java b/connectors/aws/aws-s3/src/main/java/io/camunda/connector/aws/s3/model/request/UploadObject.java new file mode 100644 index 0000000000..9023328b1c --- /dev/null +++ b/connectors/aws/aws-s3/src/main/java/io/camunda/connector/aws/s3/model/request/UploadObject.java @@ -0,0 +1,45 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. Licensed under a proprietary license. + * See the License.txt file for more information. You may not use this file + * except in compliance with the proprietary license. + */ +package io.camunda.connector.aws.s3.model.request; + +import io.camunda.connector.generator.dsl.Property; +import io.camunda.connector.generator.java.annotation.TemplateProperty; +import io.camunda.connector.generator.java.annotation.TemplateSubType; +import io.camunda.document.Document; +import jakarta.validation.constraints.NotBlank; + +@TemplateSubType(id = "uploadObject", label = "Upload object") +public record UploadObject( + @TemplateProperty( + label = "AWS bucket", + id = "uploadActionBucket", + group = "uploadObject", + tooltip = "Bucket from where an object should be uploaded", + feel = Property.FeelMode.optional, + binding = @TemplateProperty.PropertyBinding(name = "action.bucket")) + @NotBlank + String bucket, + @TemplateProperty( + label = "AWS key", + id = "uploadActionKey", + group = "uploadObject", + tooltip = + "Key of the uploaded object, if not given. The file name from the document metadata will be used", + optional = true, + feel = Property.FeelMode.optional, + binding = @TemplateProperty.PropertyBinding(name = "action.key")) + String key, + @TemplateProperty( + label = "Document", + group = "uploadObject", + id = "uploadActionDocument", + tooltip = "Document to be uploaded on AWS S3", + type = TemplateProperty.PropertyType.String, + feel = Property.FeelMode.required, + binding = @TemplateProperty.PropertyBinding(name = "action.document")) + Document document) + implements S3Action {} diff --git a/connectors/aws/aws-s3/src/main/java/io/camunda/connector/aws/s3/model/response/DeleteResponse.java b/connectors/aws/aws-s3/src/main/java/io/camunda/connector/aws/s3/model/response/DeleteResponse.java new file mode 100644 index 0000000000..94499db2bc --- /dev/null +++ b/connectors/aws/aws-s3/src/main/java/io/camunda/connector/aws/s3/model/response/DeleteResponse.java @@ -0,0 +1,9 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. Licensed under a proprietary license. + * See the License.txt file for more information. You may not use this file + * except in compliance with the proprietary license. + */ +package io.camunda.connector.aws.s3.model.response; + +public record DeleteResponse(String bucket, String key) {} diff --git a/connectors/aws/aws-s3/src/main/java/io/camunda/connector/aws/s3/model/response/DownloadResponse.java b/connectors/aws/aws-s3/src/main/java/io/camunda/connector/aws/s3/model/response/DownloadResponse.java new file mode 100644 index 0000000000..1180fc415d --- /dev/null +++ b/connectors/aws/aws-s3/src/main/java/io/camunda/connector/aws/s3/model/response/DownloadResponse.java @@ -0,0 +1,9 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. Licensed under a proprietary license. + * See the License.txt file for more information. You may not use this file + * except in compliance with the proprietary license. + */ +package io.camunda.connector.aws.s3.model.response; + +public record DownloadResponse(String bucket, String key, Element element) {} diff --git a/connectors/aws/aws-s3/src/main/java/io/camunda/connector/aws/s3/model/response/Element.java b/connectors/aws/aws-s3/src/main/java/io/camunda/connector/aws/s3/model/response/Element.java new file mode 100644 index 0000000000..4721693969 --- /dev/null +++ b/connectors/aws/aws-s3/src/main/java/io/camunda/connector/aws/s3/model/response/Element.java @@ -0,0 +1,17 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. Licensed under a proprietary license. + * See the License.txt file for more information. You may not use this file + * except in compliance with the proprietary license. + */ +package io.camunda.connector.aws.s3.model.response; + +import io.camunda.document.Document; + +public interface Element { + record DocumentContent(Document document) implements Element {} + + record StringContent(String content) implements Element {} + + record JsonContent(Object content) implements Element {} +} diff --git a/connectors/aws/aws-s3/src/main/java/io/camunda/connector/aws/s3/model/response/UploadResponse.java b/connectors/aws/aws-s3/src/main/java/io/camunda/connector/aws/s3/model/response/UploadResponse.java new file mode 100644 index 0000000000..f2125e6079 --- /dev/null +++ b/connectors/aws/aws-s3/src/main/java/io/camunda/connector/aws/s3/model/response/UploadResponse.java @@ -0,0 +1,9 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. Licensed under a proprietary license. + * See the License.txt file for more information. You may not use this file + * except in compliance with the proprietary license. + */ +package io.camunda.connector.aws.s3.model.response; + +public record UploadResponse(String bucket, String key, String link) {} diff --git a/connectors/aws/aws-s3/src/main/resources/META-INF/services/io.camunda.connector.api.outbound.OutboundConnectorFunction b/connectors/aws/aws-s3/src/main/resources/META-INF/services/io.camunda.connector.api.outbound.OutboundConnectorFunction new file mode 100644 index 0000000000..132adea638 --- /dev/null +++ b/connectors/aws/aws-s3/src/main/resources/META-INF/services/io.camunda.connector.api.outbound.OutboundConnectorFunction @@ -0,0 +1 @@ +io.camunda.connector.aws.s3.S3ConnectorFunction \ No newline at end of file diff --git a/connectors/aws/aws-s3/src/main/resources/icon.svg b/connectors/aws/aws-s3/src/main/resources/icon.svg new file mode 100644 index 0000000000..3f63be51fa --- /dev/null +++ b/connectors/aws/aws-s3/src/main/resources/icon.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + diff --git a/connectors/aws/aws-s3/src/test/java/io/camunda/connector/aws/s3/BaseTest.java b/connectors/aws/aws-s3/src/test/java/io/camunda/connector/aws/s3/BaseTest.java new file mode 100644 index 0000000000..0941fd87d7 --- /dev/null +++ b/connectors/aws/aws-s3/src/test/java/io/camunda/connector/aws/s3/BaseTest.java @@ -0,0 +1,65 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. Licensed under a proprietary license. + * See the License.txt file for more information. You may not use this file + * except in compliance with the proprietary license. + */ +package io.camunda.connector.aws.s3; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.nio.file.Files.readString; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.camunda.connector.api.json.ConnectorsObjectMapperSupplier; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.stream.Stream; +import org.junit.jupiter.params.provider.Arguments; + +public class BaseTest { + + public static Stream loadUploadActionVariables() { + try { + return loadTestCasesFromResourceFile("src/test/resources/actions/uploadActionsExample.json"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static Stream loadDownloadActionVariables() { + try { + return loadTestCasesFromResourceFile( + "src/test/resources/actions/downloadActionsExample.json"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static Stream loadDeleteActionVariables() { + try { + return loadTestCasesFromResourceFile("src/test/resources/actions/deleteActionsExample.json"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @SuppressWarnings("unchecked") + protected static Stream loadTestCasesFromResourceFile(final String fileWithTestCasesUri) + throws IOException { + final String cases = readString(new File(fileWithTestCasesUri).toPath(), UTF_8); + final ObjectMapper mapper = ConnectorsObjectMapperSupplier.getCopy(); + var array = mapper.readValue(cases, ArrayList.class); + return array.stream() + .map( + value -> { + try { + return mapper.writeValueAsString(value); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + }) + .map(Arguments::of); + } +} diff --git a/connectors/aws/aws-s3/src/test/java/io/camunda/connector/aws/s3/S3ConnectorFunctionTest.java b/connectors/aws/aws-s3/src/test/java/io/camunda/connector/aws/s3/S3ConnectorFunctionTest.java new file mode 100644 index 0000000000..bc84fabfad --- /dev/null +++ b/connectors/aws/aws-s3/src/test/java/io/camunda/connector/aws/s3/S3ConnectorFunctionTest.java @@ -0,0 +1,78 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. Licensed under a proprietary license. + * See the License.txt file for more information. You may not use this file + * except in compliance with the proprietary license. + */ +package io.camunda.connector.aws.s3; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import io.camunda.connector.aws.s3.core.S3Executor; +import io.camunda.connector.aws.s3.model.response.DeleteResponse; +import io.camunda.connector.aws.s3.model.response.DownloadResponse; +import io.camunda.connector.aws.s3.model.response.UploadResponse; +import io.camunda.connector.test.outbound.OutboundConnectorContextBuilder; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +class S3ConnectorFunctionTest extends BaseTest { + + @ParameterizedTest + @MethodSource("loadUploadActionVariables") + void executeUploadActionReturnsCorrectResult(String variables) { + + var s3ConnectorFunction = new S3ConnectorFunction(); + var context = OutboundConnectorContextBuilder.create().variables(variables).build(); + + var s3Executor = Mockito.mock(S3Executor.class); + + try (MockedStatic s3ExecutorMockedStatic = Mockito.mockStatic(S3Executor.class)) { + s3ExecutorMockedStatic.when(() -> S3Executor.create(any(), any())).thenReturn(s3Executor); + when(s3Executor.execute(any())).thenReturn(new UploadResponse("test", "test", "link")); + var response = s3ConnectorFunction.execute(context); + Assertions.assertNotNull(response); + Assertions.assertInstanceOf(UploadResponse.class, response); + } + } + + @ParameterizedTest + @MethodSource("loadDownloadActionVariables") + void executeDownloadActionReturnsCorrectResult(String variables) { + + var s3ConnectorFunction = new S3ConnectorFunction(); + var context = OutboundConnectorContextBuilder.create().variables(variables).build(); + + var s3Executor = Mockito.mock(S3Executor.class); + + try (MockedStatic s3ExecutorMockedStatic = Mockito.mockStatic(S3Executor.class)) { + s3ExecutorMockedStatic.when(() -> S3Executor.create(any(), any())).thenReturn(s3Executor); + when(s3Executor.execute(any())).thenReturn(new DownloadResponse("test", "test", null)); + var response = s3ConnectorFunction.execute(context); + Assertions.assertNotNull(response); + Assertions.assertInstanceOf(DownloadResponse.class, response); + } + } + + @ParameterizedTest + @MethodSource("loadDeleteActionVariables") + void executeDeleteActionReturnsCorrectResult(String variables) { + + var s3ConnectorFunction = new S3ConnectorFunction(); + var context = OutboundConnectorContextBuilder.create().variables(variables).build(); + + var s3Executor = Mockito.mock(S3Executor.class); + + try (MockedStatic s3ExecutorMockedStatic = Mockito.mockStatic(S3Executor.class)) { + s3ExecutorMockedStatic.when(() -> S3Executor.create(any(), any())).thenReturn(s3Executor); + when(s3Executor.execute(any())).thenReturn(new DeleteResponse("test", "test")); + var response = s3ConnectorFunction.execute(context); + Assertions.assertNotNull(response); + Assertions.assertInstanceOf(DeleteResponse.class, response); + } + } +} diff --git a/connectors/aws/aws-s3/src/test/java/io/camunda/connector/aws/s3/core/S3ExecutorTest.java b/connectors/aws/aws-s3/src/test/java/io/camunda/connector/aws/s3/core/S3ExecutorTest.java new file mode 100644 index 0000000000..b62492f9bb --- /dev/null +++ b/connectors/aws/aws-s3/src/test/java/io/camunda/connector/aws/s3/core/S3ExecutorTest.java @@ -0,0 +1,166 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. Licensed under a proprietary license. + * See the License.txt file for more information. You may not use this file + * except in compliance with the proprietary license. + */ +package io.camunda.connector.aws.s3.core; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import com.fasterxml.jackson.databind.JsonNode; +import io.camunda.connector.aws.s3.model.request.DeleteObject; +import io.camunda.connector.aws.s3.model.request.DownloadObject; +import io.camunda.connector.aws.s3.model.request.S3Action; +import io.camunda.connector.aws.s3.model.request.UploadObject; +import io.camunda.connector.aws.s3.model.response.DeleteResponse; +import io.camunda.connector.aws.s3.model.response.DownloadResponse; +import io.camunda.connector.aws.s3.model.response.Element; +import io.camunda.connector.aws.s3.model.response.UploadResponse; +import io.camunda.document.Document; +import io.camunda.document.store.DocumentCreationRequest; +import java.io.IOException; +import java.util.Base64; +import java.util.function.Function; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.core.ResponseInputStream; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; + +class S3ExecutorTest { + + @Test + void executeDeleteAction() { + S3Client s3Client = mock(S3Client.class); + Function function = doc -> mock(Document.class); + S3Executor executor = new S3Executor(s3Client, function); + S3Action s3Action = new DeleteObject("test", "key"); + + Object object = executor.execute(s3Action); + + verify(s3Client, times(1)).deleteObject(any(DeleteObjectRequest.class)); + assertInstanceOf(DeleteResponse.class, object); + } + + @Test + void executeUploadAction() { + S3Client s3Client = mock(S3Client.class); + Function function = doc -> mock(Document.class); + S3Executor executor = new S3Executor(s3Client, function); + Document document = mock(Document.class, RETURNS_DEEP_STUBS); + S3Action s3Action = new UploadObject("test", "key", document); + + when(document.metadata().getSize()).thenReturn(42L); + when(document.metadata().getContentType()).thenReturn("application/octet-stream"); + + Object object = executor.execute(s3Action); + + verify(s3Client, times(1)).putObject(any(PutObjectRequest.class), any(RequestBody.class)); + assertInstanceOf(UploadResponse.class, object); + } + + @Test + void executeDownloadAsDocumentAction() { + + S3Client s3Client = mock(S3Client.class); + Function function = doc -> mock(Document.class); + S3Executor executor = new S3Executor(s3Client, function); + ResponseInputStream responseInputStream = mock(ResponseInputStream.class); + GetObjectResponse getObjectResponse = mock(GetObjectResponse.class); + S3Action s3Action = new DownloadObject("test", "key", true); + + when(s3Client.getObject(any(GetObjectRequest.class))).thenReturn(responseInputStream); + when(responseInputStream.response()).thenReturn(getObjectResponse); + when(getObjectResponse.contentType()).thenReturn("application/octet-stream"); + Object object = executor.execute(s3Action); + + verify(s3Client, times(1)).getObject(any(GetObjectRequest.class)); + assertInstanceOf(DownloadResponse.class, object); + assertInstanceOf(Element.DocumentContent.class, ((DownloadResponse) object).element()); + } + + @Test + void executeDownloadAsTextContentAction() throws IOException { + + S3Client s3Client = mock(S3Client.class); + Function function = doc -> mock(Document.class); + S3Executor executor = new S3Executor(s3Client, function); + ResponseInputStream responseInputStream = mock(ResponseInputStream.class); + GetObjectResponse getObjectResponse = mock(GetObjectResponse.class); + S3Action s3Action = new DownloadObject("test", "key", false); + + when(s3Client.getObject(any(GetObjectRequest.class))).thenReturn(responseInputStream); + when(responseInputStream.response()).thenReturn(getObjectResponse); + when(responseInputStream.readAllBytes()).thenReturn("Hello World".getBytes()); + when(getObjectResponse.contentLength()).thenReturn(234L); + when(getObjectResponse.contentType()).thenReturn("text/plain"); + Object object = executor.execute(s3Action); + + verify(s3Client, times(1)).getObject(any(GetObjectRequest.class)); + assertInstanceOf(DownloadResponse.class, object); + assertNotNull(((DownloadResponse) object).element()); + assertInstanceOf(Element.StringContent.class, ((DownloadResponse) object).element()); + assertEquals( + "Hello World", ((Element.StringContent) ((DownloadResponse) object).element()).content()); + } + + @Test + void executeDownloadAsJsonContentAction() throws IOException { + + S3Client s3Client = mock(S3Client.class); + Function function = doc -> mock(Document.class); + S3Executor executor = new S3Executor(s3Client, function); + ResponseInputStream responseInputStream = mock(ResponseInputStream.class); + GetObjectResponse getObjectResponse = mock(GetObjectResponse.class); + S3Action s3Action = new DownloadObject("test", "key", false); + + when(s3Client.getObject(any(GetObjectRequest.class))).thenReturn(responseInputStream); + when(responseInputStream.response()).thenReturn(getObjectResponse); + when(responseInputStream.readAllBytes()).thenReturn("{ \"Hello\" : \"World\" }".getBytes()); + when(getObjectResponse.contentLength()).thenReturn(234L); + when(getObjectResponse.contentType()).thenReturn("application/json"); + Object object = executor.execute(s3Action); + + verify(s3Client, times(1)).getObject(any(GetObjectRequest.class)); + assertInstanceOf(DownloadResponse.class, object); + DownloadResponse downloadResponse = (DownloadResponse) object; + assertNotNull(downloadResponse.element()); + assertEquals( + "World", + ((JsonNode) ((Element.JsonContent) downloadResponse.element()).content()) + .get("Hello") + .asText()); + } + + @Test + void executeDownloadAsBase64BytesContentAction() throws IOException { + + S3Client s3Client = mock(S3Client.class); + Function function = doc -> mock(Document.class); + S3Executor executor = new S3Executor(s3Client, function); + ResponseInputStream responseInputStream = mock(ResponseInputStream.class); + GetObjectResponse getObjectResponse = mock(GetObjectResponse.class); + S3Action s3Action = new DownloadObject("test", "key", false); + + when(s3Client.getObject(any(GetObjectRequest.class))).thenReturn(responseInputStream); + when(responseInputStream.response()).thenReturn(getObjectResponse); + when(responseInputStream.readAllBytes()).thenReturn("Hello".getBytes()); + when(getObjectResponse.contentLength()).thenReturn(234L); + when(getObjectResponse.contentType()).thenReturn("application/octet-stream"); + Object object = executor.execute(s3Action); + + verify(s3Client, times(1)).getObject(any(GetObjectRequest.class)); + assertInstanceOf(DownloadResponse.class, object); + DownloadResponse downloadResponse = (DownloadResponse) object; + assertNotNull(downloadResponse.element()); + assertEquals( + Base64.getEncoder().encodeToString("Hello".getBytes()), + ((Element.StringContent) downloadResponse.element()).content()); + } +} diff --git a/connectors/aws/aws-s3/src/test/resources/actions/deleteActionsExample.json b/connectors/aws/aws-s3/src/test/resources/actions/deleteActionsExample.json new file mode 100644 index 0000000000..3f0e048cb0 --- /dev/null +++ b/connectors/aws/aws-s3/src/test/resources/actions/deleteActionsExample.json @@ -0,0 +1,15 @@ +[ + { + "action":{ + "bucket":"connector-aws-s3-test", + "key":"test" + }, "configuration":{ + "region":"eu-central-1" + }, + "authentication":{ + "type":"credentials", + "accessKey":"test", + "secretKey":"test" + }, "actionDiscriminator":"deleteObject" + } +] \ No newline at end of file diff --git a/connectors/aws/aws-s3/src/test/resources/actions/downloadActionsExample.json b/connectors/aws/aws-s3/src/test/resources/actions/downloadActionsExample.json new file mode 100644 index 0000000000..e90a965984 --- /dev/null +++ b/connectors/aws/aws-s3/src/test/resources/actions/downloadActionsExample.json @@ -0,0 +1,18 @@ +[ + { + "action":{ + "bucket":"connector-aws-s3-test", + "key":"attachment", + "asFile":false + }, + "configuration":{ + "region":"eu-central-1" + }, + "authentication":{ + "type":"credentials", + "accessKey":"test", + "secretKey":"test" + }, + "actionDiscriminator":"downloadObject" + } +] \ No newline at end of file diff --git a/connectors/aws/aws-s3/src/test/resources/actions/uploadActionsExample.json b/connectors/aws/aws-s3/src/test/resources/actions/uploadActionsExample.json new file mode 100644 index 0000000000..41de5ce929 --- /dev/null +++ b/connectors/aws/aws-s3/src/test/resources/actions/uploadActionsExample.json @@ -0,0 +1,22 @@ +[ + { + "action":{ + "bucket":"connector-aws-s3-test", + "key":"attachment", + "document":{ + "storeId":"in-memory", + "documentId":"41d2a87f-f39c-4ddd-a116-18d2091cc695", + "metadata":{ + "contentType":"text/plain", "size":41730, + "fileName":"test.txt" + }, + "camunda.document.type":"camunda" + } + }, "configuration":{"region":"eu-central-1"}, + "authentication":{ + "type":"credentials", + "accessKey":"test", + "secretKey":"test" + }, "actionDiscriminator":"uploadObject" + } +] \ No newline at end of file diff --git a/connectors/aws/pom.xml b/connectors/aws/pom.xml index bd2cc77c92..77c3845741 100644 --- a/connectors/aws/pom.xml +++ b/connectors/aws/pom.xml @@ -27,6 +27,7 @@ aws-bedrock aws-comprehend aws-textract + aws-s3 diff --git a/connectors/email/src/main/java/io/camunda/connector/email/client/jakarta/utils/JakartaUtils.java b/connectors/email/src/main/java/io/camunda/connector/email/client/jakarta/utils/JakartaUtils.java index aa1e8ccb55..ef904daba4 100644 --- a/connectors/email/src/main/java/io/camunda/connector/email/client/jakarta/utils/JakartaUtils.java +++ b/connectors/email/src/main/java/io/camunda/connector/email/client/jakarta/utils/JakartaUtils.java @@ -20,8 +20,10 @@ import io.camunda.connector.email.outbound.protocols.actions.SortFieldPop3; import io.camunda.connector.email.outbound.protocols.actions.SortOrder; import jakarta.mail.*; +import jakarta.mail.internet.ContentType; import jakarta.mail.internet.MimeMultipart; import jakarta.validation.constraints.NotNull; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.time.OffsetDateTime; @@ -32,9 +34,9 @@ public class JakartaUtils { + public static final String HTML_CHARSET = "text/html; charset=utf-8"; private static final Logger LOGGER = LoggerFactory.getLogger(JakartaUtils.class); private static final String REGEX_PATH_SPLITTER = "[./]"; - public static final String HTML_CHARSET = "text/html; charset=utf-8"; public Session createSession(Configuration configuration) { return Session.getInstance( @@ -282,11 +284,19 @@ private void processBodyPart( throws MessagingException, IOException { BodyPart bodyPart = multipart.getBodyPart(i); switch (bodyPart.getContent()) { - case InputStream attachment when bodyPart - .getDisposition() - .equalsIgnoreCase(Part.ATTACHMENT) -> + case InputStream attachment when Part.ATTACHMENT.equalsIgnoreCase( + bodyPart.getDisposition()) -> + emailBodyBuilder.addAttachment( + new EmailAttachment( + attachment, + bodyPart.getFileName(), + new ContentType(bodyPart.getContentType()).getBaseType())); + case String textAttachment when Part.ATTACHMENT.equalsIgnoreCase(bodyPart.getDisposition()) -> emailBodyBuilder.addAttachment( - new EmailAttachment(attachment, bodyPart.getFileName(), bodyPart.getContentType())); + new EmailAttachment( + new ByteArrayInputStream(textAttachment.getBytes()), + bodyPart.getFileName(), + new ContentType(bodyPart.getContentType()).getBaseType())); case String plainText when bodyPart.isMimeType("text/plain") -> emailBodyBuilder.withBodyAsPlainText(plainText); case String html when bodyPart.isMimeType("text/html") ->