From c838bb78027b87472631df39b2204af48bef2f55 Mon Sep 17 00:00:00 2001 From: DenovVasil Date: Fri, 23 Aug 2024 15:21:02 +0300 Subject: [PATCH] feat(textract): introduce Amazon textract connector --- bundle/default-bundle/pom.xml | 4 + bundle/pom.xml | 5 + connectors/aws/aws-textract/LICENSE.txt | 5 + connectors/aws/aws-textract/README.md | 3 + .../aws-textract-outbound-connector.json | 450 +++++++++++++++++ ...ws-textract-outbound-connector-hybrid.json | 455 ++++++++++++++++++ connectors/aws/aws-textract/pom.xml | 78 +++ .../textract/TextractConnectorFunction.java | 81 ++++ .../textract/caller/AsyncTextractCaller.java | 63 +++ .../caller/PollingTextractCalller.java | 59 +++ .../textract/caller/SyncTextractCaller.java | 34 ++ .../textract/caller/TextractCaller.java | 58 +++ .../textract/model/TextractExecutionType.java | 13 + .../textract/model/TextractRequest.java | 50 ++ .../textract/model/TextractRequestData.java | 184 +++++++ .../textract/model/TextractTask.java | 30 ++ .../AmazonTextractClientSupplier.java | 31 ++ ...tor.api.outbound.OutboundConnectorFunction | 1 + .../aws-textract/src/main/resources/icon.svg | 10 + .../TextractConnectorFunctionTest.java | 121 +++++ .../caller/AsyncTextractCallerTest.java | 137 ++++++ .../caller/PollingTextractCalllerTest.java | 54 +++ .../caller/SyncTextractCallerTest.java | 51 ++ .../textract/caller/TextractCallerTest.java | 115 +++++ .../AmazonTextractClientSupplierTest.java | 47 ++ .../textract/util/TextractTestUtils.java | 211 ++++++++ connectors/aws/pom.xml | 1 + 27 files changed, 2351 insertions(+) create mode 100644 connectors/aws/aws-textract/LICENSE.txt create mode 100644 connectors/aws/aws-textract/README.md create mode 100644 connectors/aws/aws-textract/element-templates/aws-textract-outbound-connector.json create mode 100644 connectors/aws/aws-textract/element-templates/hybrid/aws-textract-outbound-connector-hybrid.json create mode 100644 connectors/aws/aws-textract/pom.xml create mode 100644 connectors/aws/aws-textract/src/main/java/io/camunda/connector/textract/TextractConnectorFunction.java create mode 100644 connectors/aws/aws-textract/src/main/java/io/camunda/connector/textract/caller/AsyncTextractCaller.java create mode 100644 connectors/aws/aws-textract/src/main/java/io/camunda/connector/textract/caller/PollingTextractCalller.java create mode 100644 connectors/aws/aws-textract/src/main/java/io/camunda/connector/textract/caller/SyncTextractCaller.java create mode 100644 connectors/aws/aws-textract/src/main/java/io/camunda/connector/textract/caller/TextractCaller.java create mode 100644 connectors/aws/aws-textract/src/main/java/io/camunda/connector/textract/model/TextractExecutionType.java create mode 100644 connectors/aws/aws-textract/src/main/java/io/camunda/connector/textract/model/TextractRequest.java create mode 100644 connectors/aws/aws-textract/src/main/java/io/camunda/connector/textract/model/TextractRequestData.java create mode 100644 connectors/aws/aws-textract/src/main/java/io/camunda/connector/textract/model/TextractTask.java create mode 100644 connectors/aws/aws-textract/src/main/java/io/camunda/connector/textract/suppliers/AmazonTextractClientSupplier.java create mode 100644 connectors/aws/aws-textract/src/main/resources/META-INF/services/io.camunda.connector.api.outbound.OutboundConnectorFunction create mode 100644 connectors/aws/aws-textract/src/main/resources/icon.svg create mode 100644 connectors/aws/aws-textract/src/test/java/io/camunda/connector/textract/TextractConnectorFunctionTest.java create mode 100644 connectors/aws/aws-textract/src/test/java/io/camunda/connector/textract/caller/AsyncTextractCallerTest.java create mode 100644 connectors/aws/aws-textract/src/test/java/io/camunda/connector/textract/caller/PollingTextractCalllerTest.java create mode 100644 connectors/aws/aws-textract/src/test/java/io/camunda/connector/textract/caller/SyncTextractCallerTest.java create mode 100644 connectors/aws/aws-textract/src/test/java/io/camunda/connector/textract/caller/TextractCallerTest.java create mode 100644 connectors/aws/aws-textract/src/test/java/io/camunda/connector/textract/suppliers/AmazonTextractClientSupplierTest.java create mode 100644 connectors/aws/aws-textract/src/test/java/io/camunda/connector/textract/util/TextractTestUtils.java diff --git a/bundle/default-bundle/pom.xml b/bundle/default-bundle/pom.xml index f51ecde097..571a45cbf3 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-textract + io.camunda.connector connector-google-drive diff --git a/bundle/pom.xml b/bundle/pom.xml index af08c6cd8a..997170cf70 100644 --- a/bundle/pom.xml +++ b/bundle/pom.xml @@ -69,6 +69,11 @@ connector-aws-sns ${project.version} + + io.camunda.connector + connector-aws-textract + ${project.version} + io.camunda.connector connector-kafka diff --git a/connectors/aws/aws-textract/LICENSE.txt b/connectors/aws/aws-textract/LICENSE.txt new file mode 100644 index 0000000000..85fdd16e79 --- /dev/null +++ b/connectors/aws/aws-textract/LICENSE.txt @@ -0,0 +1,5 @@ +Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH under one or more contributor license agreements and licensed to you under a proprietary license. +You may not use this file except in compliance with the proprietary license. +The proprietary license can be either the Camunda Self-Managed Free Edition license (available on Camunda’s website) or the Camunda Self-Managed Enterprise Edition license (a copy you obtain when you contact Camunda). +The Camunda Self-Managed Free Edition comes for free but only allows for usage of the software (file) in non-production environments. +If you want to use the software (file) in production, you need to purchase the Camunda Self-Managed Enterprise Edition. \ No newline at end of file diff --git a/connectors/aws/aws-textract/README.md b/connectors/aws/aws-textract/README.md new file mode 100644 index 0000000000..6d2a5e57c9 --- /dev/null +++ b/connectors/aws/aws-textract/README.md @@ -0,0 +1,3 @@ +# AWS Textract Connector + +The **AWS Textract Connector** allows you to automatically extract printed text, handwriting, layout elements, and data from any document diff --git a/connectors/aws/aws-textract/element-templates/aws-textract-outbound-connector.json b/connectors/aws/aws-textract/element-templates/aws-textract-outbound-connector.json new file mode 100644 index 0000000000..ddd1368f63 --- /dev/null +++ b/connectors/aws/aws-textract/element-templates/aws-textract-outbound-connector.json @@ -0,0 +1,450 @@ +{ + "$schema" : "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "name" : "AWS Textract Outbound Connector", + "id" : "io.camunda.connectors.AWSTEXTRACT.v1", + "description" : "Automatically extract printed text, handwriting, layout elements, and data from any document", + "documentationRef" : "https://docs.camunda.io/docs/next/components/connectors/out-of-the-box-connectors/amazon-textract/", + "version" : 1, + "category" : { + "id" : "connectors", + "name" : "Connectors" + }, + "appliesTo" : [ "bpmn:Task" ], + "elementType" : { + "value" : "bpmn:ServiceTask" + }, + "groups" : [ { + "id" : "authentication", + "label" : "Authentication" + }, { + "id" : "configuration", + "label" : "Configuration" + }, { + "id" : "input", + "label" : "Configure input" + }, { + "id" : "output", + "label" : "Output mapping" + }, { + "id" : "error", + "label" : "Error handling" + }, { + "id" : "retries", + "label" : "Retries" + } ], + "properties" : [ { + "value" : "io.camunda:aws-textract:1", + "binding" : { + "property" : "type", + "type" : "zeebe:taskDefinition" + }, + "type" : "Hidden" + }, { + "id" : "input.WRONG_OUTPUT_VALUES_MSG", + "label" : "WRONG_OUTPUT_VALUES_MSG", + "feel" : "optional", + "binding" : { + "name" : "input.WRONG_OUTPUT_VALUES_MSG", + "type" : "zeebe:input" + }, + "type" : "String" + }, { + "id" : "input.WRONG_NOTIFICATION_VALUES_MSG", + "label" : "WRONG_NOTIFICATION_VALUES_MSG", + "feel" : "optional", + "binding" : { + "name" : "input.WRONG_NOTIFICATION_VALUES_MSG", + "type" : "zeebe:input" + }, + "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" : "input.executionType", + "label" : "Execution type", + "description" : "Endpoint inference type", + "optional" : false, + "value" : "ASYNC", + "constraints" : { + "notEmpty" : true + }, + "group" : "input", + "binding" : { + "name" : "input.executionType", + "type" : "zeebe:input" + }, + "type" : "Dropdown", + "choices" : [ { + "name" : "Asynchronous", + "value" : "ASYNC" + }, { + "name" : "Real-time", + "value" : "SYNC" + }, { + "name" : "Polling", + "value" : "POLLING" + } ] + }, { + "id" : "input.documentS3Bucket", + "label" : "Document bucket", + "description" : "S3 bucket that contains document that needs to be processed", + "optional" : false, + "constraints" : { + "notEmpty" : true + }, + "feel" : "optional", + "group" : "input", + "binding" : { + "name" : "input.documentS3Bucket", + "type" : "zeebe:input" + }, + "type" : "String" + }, { + "id" : "input.documentName", + "label" : "Document path", + "description" : "S3 document path to be processed", + "optional" : false, + "constraints" : { + "notEmpty" : true + }, + "feel" : "optional", + "group" : "input", + "binding" : { + "name" : "input.documentName", + "type" : "zeebe:input" + }, + "type" : "String" + }, { + "id" : "input.documentVersion", + "label" : "Document version", + "description" : "S3 document version to be processed", + "optional" : true, + "feel" : "optional", + "group" : "input", + "binding" : { + "name" : "input.documentVersion", + "type" : "zeebe:input" + }, + "type" : "String" + }, { + "id" : "input.analyzeTables", + "label" : "Analyze tables", + "optional" : false, + "value" : true, + "constraints" : { + "notEmpty" : true + }, + "feel" : "optional", + "group" : "input", + "binding" : { + "name" : "input.analyzeTables", + "type" : "zeebe:input" + }, + "type" : "Boolean" + }, { + "id" : "input.analyzeForms", + "label" : "Analyze form", + "optional" : false, + "value" : true, + "constraints" : { + "notEmpty" : true + }, + "feel" : "optional", + "group" : "input", + "binding" : { + "name" : "input.analyzeForms", + "type" : "zeebe:input" + }, + "type" : "Boolean" + }, { + "id" : "input.analyzeSignatures", + "label" : "Analyze signatures", + "optional" : false, + "value" : true, + "constraints" : { + "notEmpty" : true + }, + "feel" : "optional", + "group" : "input", + "binding" : { + "name" : "input.analyzeSignatures", + "type" : "zeebe:input" + }, + "type" : "Boolean" + }, { + "id" : "input.analyzeLayout", + "label" : "Analyze layout", + "optional" : false, + "value" : true, + "constraints" : { + "notEmpty" : true + }, + "feel" : "optional", + "group" : "input", + "binding" : { + "name" : "input.analyzeLayout", + "type" : "zeebe:input" + }, + "type" : "Boolean" + }, { + "id" : "input.clientRequestToken", + "label" : "Client request token", + "description" : "The idempotent token that you use to identify the start request", + "optional" : true, + "feel" : "optional", + "group" : "input", + "binding" : { + "name" : "input.clientRequestToken", + "type" : "zeebe:input" + }, + "condition" : { + "property" : "input.executionType", + "equals" : "ASYNC", + "type" : "simple" + }, + "type" : "String" + }, { + "id" : "input.jobTag", + "label" : "Job tag", + "description" : "An identifier that you specify that's included in the completion notification published to the Amazon SNS topic", + "optional" : true, + "feel" : "optional", + "group" : "input", + "binding" : { + "name" : "input.jobTag", + "type" : "zeebe:input" + }, + "condition" : { + "property" : "input.executionType", + "equals" : "ASYNC", + "type" : "simple" + }, + "type" : "String" + }, { + "id" : "input.kmsKeyId", + "label" : "KMS key ID", + "description" : "The KMS key used to encrypt the inference results", + "optional" : true, + "feel" : "optional", + "group" : "input", + "binding" : { + "name" : "input.kmsKeyId", + "type" : "zeebe:input" + }, + "condition" : { + "property" : "input.executionType", + "equals" : "ASYNC", + "type" : "simple" + }, + "type" : "String" + }, { + "id" : "input.notificationChannelRoleArn", + "label" : "Notification channel role ARN", + "description" : "The Amazon SNS topic role ARN that you want Amazon Textract to publish the completion status of the operation to", + "optional" : true, + "feel" : "optional", + "group" : "input", + "binding" : { + "name" : "input.notificationChannelRoleArn", + "type" : "zeebe:input" + }, + "condition" : { + "property" : "input.executionType", + "equals" : "ASYNC", + "type" : "simple" + }, + "type" : "String" + }, { + "id" : "input.notificationChannelSnsTopicArn", + "label" : "Notification channel SNS topic ARN", + "description" : "The Amazon SNS topic ARN that you want Amazon Textract to publish the completion status of the operation to", + "optional" : true, + "feel" : "optional", + "group" : "input", + "binding" : { + "name" : "input.notificationChannelSnsTopicArn", + "type" : "zeebe:input" + }, + "condition" : { + "property" : "input.executionType", + "equals" : "ASYNC", + "type" : "simple" + }, + "type" : "String" + }, { + "id" : "input.outputConfigS3Bucket", + "label" : "Output S3 bucket", + "description" : "The name of the bucket your output will go to", + "optional" : true, + "feel" : "optional", + "group" : "input", + "binding" : { + "name" : "input.outputConfigS3Bucket", + "type" : "zeebe:input" + }, + "condition" : { + "property" : "input.executionType", + "equals" : "ASYNC", + "type" : "simple" + }, + "type" : "String" + }, { + "id" : "input.outputConfigS3Prefix", + "label" : "Output S3 prefix", + "description" : "The prefix of the object key that the output will be saved to", + "optional" : true, + "feel" : "optional", + "group" : "input", + "binding" : { + "name" : "input.outputConfigS3Prefix", + "type" : "zeebe:input" + }, + "condition" : { + "property" : "input.executionType", + "equals" : "ASYNC", + "type" : "simple" + }, + "type" : "String" + }, { + "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", + "feel" : "optional", + "group" : "retries", + "binding" : { + "key" : "retryBackoff", + "type" : "zeebe:taskHeader" + }, + "type" : "String" + } ], + "icon" : { + "contents" : "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iODBweCIgaGVpZ2h0PSI4MHB4IiB2aWV3Qm94PSIwIDAgODAgODAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgICA8dGl0bGU+SWNvbi1BcmNoaXRlY3R1cmUvNjQvQXJjaF9BbWF6b24tVGV4dHJhY3RfNjQ8L3RpdGxlPgogICAgPGcgaWQ9Ikljb24tQXJjaGl0ZWN0dXJlLzY0L0FyY2hfQW1hem9uLVRleHRyYWN0XzY0IiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj4KICAgICAgICA8ZyBpZD0iSWNvbi1BcmNoaXRlY3R1cmUtQkcvNjQvTWFjaGluZS1MZWFybmluZyIgZmlsbD0iIzk5NjlmNyI+CiAgICAgICAgICAgIDxyZWN0IGlkPSJSZWN0YW5nbGUiIHg9IjAiIHk9IjAiIHdpZHRoPSI4MCIgaGVpZ2h0PSI4MCI+PC9yZWN0PgogICAgICAgIDwvZz4KICAgICAgICA8cGF0aCBkPSJNMjIuMDYyNDEwMiw1MCBDMjQuMzc2Mzg5NSw1My42MDMgMjguNDEwMzUzNSw1NiAzMy4wMDAzMTI1LDU2IEM0MC4xNjcyNDg1LDU2IDQ1Ljk5OTE5NjQsNTAuMTY4IDQ1Ljk5OTE5NjQsNDMgQzQ1Ljk5OTE5NjQsMzUuODMyIDQwLjE2NzI0ODUsMzAgMzMuMDAwMzEyNSwzMCBDMjcuNjAzMzYwNywzMCAyMi45NjY0MDIxLDMzLjMwNyAyMS4wMDI0MTk2LDM4IEwyMy4yMTQzOTk5LDM4IEMyNS4wMzkzODM2LDM0LjQ0NCAyOC43MzYzNTA2LDMyIDMzLjAwMDMxMjUsMzIgQzM5LjA2NTI1ODMsMzIgNDMuOTk5MjE0MywzNi45MzUgNDMuOTk5MjE0Myw0MyBDNDMuOTk5MjE0Myw0OS4wNjUgMzkuMDY1MjU4Myw1NCAzMy4wMDAzMTI1LDU0IEMyOS41OTEzNDI5LDU0IDI2LjU0MTM3MDIsNTIuNDQxIDI0LjUyMTM4ODIsNTAgTDIyLjA2MjQxMDIsNTAgWiBNMzcuMDAwMjc2OCw0NSBMMzcuMDAwMjc2OCw0MyBMNDEuOTk5MjMyMSw0MyBDNDEuOTk5MjMyMSwzOC4wMzggMzcuOTYyMjY4MiwzNCAzMy4wMDAzMTI1LDM0IEMyOC4wMzczNTY4LDM0IDIzLjk5OTM5MjksMzguMDM4IDIzLjk5OTM5MjksNDMgTDI4Ljk5OTM0ODIsNDMgTDI4Ljk5OTM0ODIsNDUgTDI0LjIzMTM5MDgsNDUgQzI1LjE0NDM4MjYsNDkuMDAyIDI4LjcyNTM1MDcsNTIgMzMuMDAwMzEyNSw1MiBDMzUuMTM2MjkzNCw1MiAzNy4wOTkyNzU5LDUxLjI0OSAzOC42NDQyNjIxLDUwIEwzNC4wMDAzMDM2LDUwIEwzNC4wMDAzMDM2LDQ4IEw0MC40NzgyNDU3LDQ4IEM0MS4wODEyNDAzLDQ3LjEwMiA0MS41MjAyMzY0LDQ2LjA4NyA0MS43NjgyMzQyLDQ1IEwzNy4wMDAyNzY4LDQ1IFogTTIxLjAwMjQxOTYsNDggTDIzLjIxNDM5OTksNDggQzIyLjQ0MzQwNjgsNDYuNDk4IDIyLjAwMDQxMDcsNDQuODAxIDIyLjAwMDQxMDcsNDMgQzIyLjAwMDQxMDcsNDEuOTU5IDIyLjE1NTQwOTMsNDAuOTU1IDIyLjQyNjQwNjksNDAgTDIwLjM2MzQyNTMsNDAgQzIwLjEzNDQyNzQsNDAuOTY1IDE5Ljk5OTQyODYsNDEuOTY2IDE5Ljk5OTQyODYsNDMgQzE5Ljk5OTQyODYsNDQuNzcxIDIwLjM1ODQyNTQsNDYuNDYgMjEuMDAyNDE5Niw0OCBMMjEuMDAyNDE5Niw0OCBaIE0xOS43NDM0MzA5LDUwIEwxNy4wMDA0NTU0LDUwIEwxNy4wMDA0NTU0LDQ4IEwxOC44NzQ0Mzg2LDQ4IEMxOC41MzQ0NDE3LDQ3LjA0IDE4LjI4OTQ0MzgsNDYuMDM4IDE4LjE0OTQ0NTEsNDUgTDE1LjQxNDQ2OTUsNDUgTDE2LjcwNzQ1OCw0Ni4yOTMgTDE1LjI5MjQ3MDYsNDcuNzA3IEwxMi4yOTI0OTc0LDQ0LjcwNyBDMTEuOTAyNTAwOSw0NC4zMTYgMTEuOTAyNTAwOSw0My42ODQgMTIuMjkyNDk3NCw0My4yOTMgTDE1LjI5MjQ3MDYsNDAuMjkzIEwxNi43MDc0NTgsNDEuNzA3IEwxNS40MTQ0Njk1LDQzIEwxOC4wMDA0NDY0LDQzIEMxOC4wMDA0NDY0LDQxLjk3MyAxOC4xMDQ0NDU1LDQwLjk3IDE4LjMwMjQ0MzcsNDAgTDE3LjAwMDQ1NTQsNDAgTDE3LjAwMDQ1NTQsMzggTDE4Ljg3NDQzODYsMzggQzIwLjk0MDQyMDIsMzIuMTg0IDI2LjQ4MzM3MDcsMjggMzMuMDAwMzEyNSwyOCBDMzcuNDI3MjczLDI4IDQxLjQwMDIzNzUsMjkuOTM5IDQ0LjE0ODIxMywzMyBMNTkuMDAwMDgwNCwzMyBMNTkuMDAwMDgwNCwzNSBMNDUuNjY2MTk5NCwzNSBDNDcuMTM1MTg2MywzNy4zMTggNDcuOTk5MTc4Niw0MC4wNTggNDcuOTk5MTc4Niw0MyBMNTkuMDAwMDgwNCw0MyBMNTkuMDAwMDgwNCw0NSBMNDcuODUwMTc5OSw0NSBDNDYuODY4MTg4Nyw1Mi4zMjcgNDAuNTkxMjQ0Nyw1OCAzMy4wMDAzMTI1LDU4IEMyNy4yNTYzNjM4LDU4IDIyLjI2MjQwODQsNTQuNzUyIDE5Ljc0MzQzMDksNTAgTDE5Ljc0MzQzMDksNTAgWiBNMzcuMDAwMjc2OCwzOSBDMzcuMDAwMjc2OCwzOC40NDggMzYuNTUyMjgwOCwzOCAzNi4wMDAyODU3LDM4IEwyOS45OTkzNDgyLDM4IEMyOS40NDczNDQyLDM4IDI4Ljk5OTM0ODIsMzguNDQ4IDI4Ljk5OTM0ODIsMzkgTDI4Ljk5OTM0ODIsNDEgTDMxLjAwMDMzMDQsNDEgTDMxLjAwMDMzMDQsNDAgTDMyLjAwMDMyMTQsNDAgTDMyLjAwMDMyMTQsNDMgTDMxLjAwMDMzMDQsNDMgTDMxLjAwMDMzMDQsNDUgTDM1LjAwMDI5NDYsNDUgTDM1LjAwMDI5NDYsNDMgTDM0LjAwMDMwMzYsNDMgTDM0LjAwMDMwMzYsNDAgTDM1LjAwMDI5NDYsNDAgTDM1LjAwMDI5NDYsNDEgTDM3LjAwMDI3NjgsNDEgTDM3LjAwMDI3NjgsMzkgWiBNNDkuMDAwMTY5Niw0MCBMNTkuMDAwMDgwNCw0MCBMNTkuMDAwMDgwNCwzOCBMNDkuMDAwMTY5NiwzOCBMNDkuMDAwMTY5Niw0MCBaIE00OS4wMDAxNjk2LDUwIEw1OS4wMDAwODA0LDUwIEw1OS4wMDAwODA0LDQ4IEw0OS4wMDAxNjk2LDQ4IEw0OS4wMDAxNjk2LDUwIFogTTU3LjAwMDA5ODIsMjcgTDYwLjU4NTA2NjIsMjcgTDU3LjAwMDA5ODIsMjMuNDE0IEw1Ny4wMDAwOTgyLDI3IFogTTYzLjcwNzAzODMsMjcuMjkzIEM2My44OTQwMzY3LDI3LjQ4IDY0LjAwMDAzNTcsMjcuNzM1IDY0LjAwMDAzNTcsMjggTDY0LjAwMDAzNTcsNjMgQzY0LjAwMDAzNTcsNjMuNTUyIDYzLjU1MjAzOTcsNjQgNjMuMDAwMDQ0Niw2NCBMMzIuMDAwMzMwNCw2NCBDMzEuNDQ3MzI2NCw2NCAzMS4wMDAzMzA0LDYzLjU1MiAzMS4wMDAzMzA0LDYzIEwzMS4wMDAzMzA0LDU5IEwzMy4wMDAzMTI1LDU5IEwzMy4wMDAzMTI1LDYyIEw2Mi4wMDAwNTM2LDYyIEw2Mi4wMDAwNTM2LDI5IEw1Ni4wMDAxMDcxLDI5IEM1NS40NDcxMTIxLDI5IDU1LjAwMDExNjEsMjguNTUyIDU1LjAwMDExNjEsMjggTDU1LjAwMDExNjEsMjIgTDMzLjAwMDMxMjUsMjIgTDMzLjAwMDMxMjUsMjcgTDMxLjAwMDMzMDQsMjcgTDMxLjAwMDMzMDQsMjEgQzMxLjAwMDMzMDQsMjAuNDQ4IDMxLjQ0NzMyNjQsMjAgMzIuMDAwMzMwNCwyMCBMNTYuMDAwMTA3MSwyMCBDNTYuMjY1MTA0OCwyMCA1Ni41MTkxMDI1LDIwLjEwNSA1Ni43MDcxMDA4LDIwLjI5MyBMNjMuNzA3MDM4MywyNy4yOTMgWiBNNjgsMjQuMTY2IEw2OCw2MSBDNjgsNjEuNTUyIDY3LjU1MjAwNCw2MiA2Ny4wMDAwMDg5LDYyIEw2NS4wMDAwMjY4LDYyIEw2NS4wMDAwMjY4LDYwIEw2Ni4wMDAwMTc5LDYwIEw2Ni4wMDAwMTc5LDI0LjYxMiBMNTguNjE3MDgzOCwxOCBMMzYuMDAwMjg1NywxOCBMMzYuMDAwMjg1NywxOSBMMzQuMDAwMzAzNiwxOSBMMzQuMDAwMzAzNiwxNyBDMzQuMDAwMzAzNiwxNi40NDggMzQuNDQ3Mjk5NiwxNiAzNS4wMDAzMDM2LDE2IEw1OS4wMDAwODA0LDE2IEM1OS4yNDYwNzgyLDE2IDU5LjQ4MzA3NiwxNi4wOTEgNTkuNjY2MDc0NCwxNi4yNTUgTDY3LjY2NjAwMywyMy40MiBDNjcuODc4MDAxMSwyMy42MSA2OCwyMy44ODEgNjgsMjQuMTY2IEw2OCwyNC4xNjYgWiIgaWQ9IkFtYXpvbi1UZXh0cmFjdF9JY29uXzY0X1NxdWlkIiBmaWxsPSIjRkZGRkZGIj48L3BhdGg+CiAgICA8L2c+Cjwvc3ZnPg==" + } +} \ No newline at end of file diff --git a/connectors/aws/aws-textract/element-templates/hybrid/aws-textract-outbound-connector-hybrid.json b/connectors/aws/aws-textract/element-templates/hybrid/aws-textract-outbound-connector-hybrid.json new file mode 100644 index 0000000000..ea9062d602 --- /dev/null +++ b/connectors/aws/aws-textract/element-templates/hybrid/aws-textract-outbound-connector-hybrid.json @@ -0,0 +1,455 @@ +{ + "$schema" : "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "name" : "Hybrid AWS Textract Outbound Connector", + "id" : "io.camunda.connectors.AWSTEXTRACT.v1-hybrid", + "description" : "Automatically extract printed text, handwriting, layout elements, and data from any document", + "documentationRef" : "https://docs.camunda.io/docs/next/components/connectors/out-of-the-box-connectors/amazon-textract/", + "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" : "input", + "label" : "Configure input" + }, { + "id" : "output", + "label" : "Output mapping" + }, { + "id" : "error", + "label" : "Error handling" + }, { + "id" : "retries", + "label" : "Retries" + } ], + "properties" : [ { + "id" : "taskDefinitionType", + "value" : "io.camunda:aws-textract:1", + "group" : "taskDefinitionType", + "binding" : { + "property" : "type", + "type" : "zeebe:taskDefinition" + }, + "type" : "String" + }, { + "id" : "input.WRONG_OUTPUT_VALUES_MSG", + "label" : "WRONG_OUTPUT_VALUES_MSG", + "feel" : "optional", + "binding" : { + "name" : "input.WRONG_OUTPUT_VALUES_MSG", + "type" : "zeebe:input" + }, + "type" : "String" + }, { + "id" : "input.WRONG_NOTIFICATION_VALUES_MSG", + "label" : "WRONG_NOTIFICATION_VALUES_MSG", + "feel" : "optional", + "binding" : { + "name" : "input.WRONG_NOTIFICATION_VALUES_MSG", + "type" : "zeebe:input" + }, + "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" : "input.executionType", + "label" : "Execution type", + "description" : "Endpoint inference type", + "optional" : false, + "value" : "ASYNC", + "constraints" : { + "notEmpty" : true + }, + "group" : "input", + "binding" : { + "name" : "input.executionType", + "type" : "zeebe:input" + }, + "type" : "Dropdown", + "choices" : [ { + "name" : "Asynchronous", + "value" : "ASYNC" + }, { + "name" : "Real-time", + "value" : "SYNC" + }, { + "name" : "Polling", + "value" : "POLLING" + } ] + }, { + "id" : "input.documentS3Bucket", + "label" : "Document bucket", + "description" : "S3 bucket that contains document that needs to be processed", + "optional" : false, + "constraints" : { + "notEmpty" : true + }, + "feel" : "optional", + "group" : "input", + "binding" : { + "name" : "input.documentS3Bucket", + "type" : "zeebe:input" + }, + "type" : "String" + }, { + "id" : "input.documentName", + "label" : "Document path", + "description" : "S3 document path to be processed", + "optional" : false, + "constraints" : { + "notEmpty" : true + }, + "feel" : "optional", + "group" : "input", + "binding" : { + "name" : "input.documentName", + "type" : "zeebe:input" + }, + "type" : "String" + }, { + "id" : "input.documentVersion", + "label" : "Document version", + "description" : "S3 document version to be processed", + "optional" : true, + "feel" : "optional", + "group" : "input", + "binding" : { + "name" : "input.documentVersion", + "type" : "zeebe:input" + }, + "type" : "String" + }, { + "id" : "input.analyzeTables", + "label" : "Analyze tables", + "optional" : false, + "value" : true, + "constraints" : { + "notEmpty" : true + }, + "feel" : "optional", + "group" : "input", + "binding" : { + "name" : "input.analyzeTables", + "type" : "zeebe:input" + }, + "type" : "Boolean" + }, { + "id" : "input.analyzeForms", + "label" : "Analyze form", + "optional" : false, + "value" : true, + "constraints" : { + "notEmpty" : true + }, + "feel" : "optional", + "group" : "input", + "binding" : { + "name" : "input.analyzeForms", + "type" : "zeebe:input" + }, + "type" : "Boolean" + }, { + "id" : "input.analyzeSignatures", + "label" : "Analyze signatures", + "optional" : false, + "value" : true, + "constraints" : { + "notEmpty" : true + }, + "feel" : "optional", + "group" : "input", + "binding" : { + "name" : "input.analyzeSignatures", + "type" : "zeebe:input" + }, + "type" : "Boolean" + }, { + "id" : "input.analyzeLayout", + "label" : "Analyze layout", + "optional" : false, + "value" : true, + "constraints" : { + "notEmpty" : true + }, + "feel" : "optional", + "group" : "input", + "binding" : { + "name" : "input.analyzeLayout", + "type" : "zeebe:input" + }, + "type" : "Boolean" + }, { + "id" : "input.clientRequestToken", + "label" : "Client request token", + "description" : "The idempotent token that you use to identify the start request", + "optional" : true, + "feel" : "optional", + "group" : "input", + "binding" : { + "name" : "input.clientRequestToken", + "type" : "zeebe:input" + }, + "condition" : { + "property" : "input.executionType", + "equals" : "ASYNC", + "type" : "simple" + }, + "type" : "String" + }, { + "id" : "input.jobTag", + "label" : "Job tag", + "description" : "An identifier that you specify that's included in the completion notification published to the Amazon SNS topic", + "optional" : true, + "feel" : "optional", + "group" : "input", + "binding" : { + "name" : "input.jobTag", + "type" : "zeebe:input" + }, + "condition" : { + "property" : "input.executionType", + "equals" : "ASYNC", + "type" : "simple" + }, + "type" : "String" + }, { + "id" : "input.kmsKeyId", + "label" : "KMS key ID", + "description" : "The KMS key used to encrypt the inference results", + "optional" : true, + "feel" : "optional", + "group" : "input", + "binding" : { + "name" : "input.kmsKeyId", + "type" : "zeebe:input" + }, + "condition" : { + "property" : "input.executionType", + "equals" : "ASYNC", + "type" : "simple" + }, + "type" : "String" + }, { + "id" : "input.notificationChannelRoleArn", + "label" : "Notification channel role ARN", + "description" : "The Amazon SNS topic role ARN that you want Amazon Textract to publish the completion status of the operation to", + "optional" : true, + "feel" : "optional", + "group" : "input", + "binding" : { + "name" : "input.notificationChannelRoleArn", + "type" : "zeebe:input" + }, + "condition" : { + "property" : "input.executionType", + "equals" : "ASYNC", + "type" : "simple" + }, + "type" : "String" + }, { + "id" : "input.notificationChannelSnsTopicArn", + "label" : "Notification channel SNS topic ARN", + "description" : "The Amazon SNS topic ARN that you want Amazon Textract to publish the completion status of the operation to", + "optional" : true, + "feel" : "optional", + "group" : "input", + "binding" : { + "name" : "input.notificationChannelSnsTopicArn", + "type" : "zeebe:input" + }, + "condition" : { + "property" : "input.executionType", + "equals" : "ASYNC", + "type" : "simple" + }, + "type" : "String" + }, { + "id" : "input.outputConfigS3Bucket", + "label" : "Output S3 bucket", + "description" : "The name of the bucket your output will go to", + "optional" : true, + "feel" : "optional", + "group" : "input", + "binding" : { + "name" : "input.outputConfigS3Bucket", + "type" : "zeebe:input" + }, + "condition" : { + "property" : "input.executionType", + "equals" : "ASYNC", + "type" : "simple" + }, + "type" : "String" + }, { + "id" : "input.outputConfigS3Prefix", + "label" : "Output S3 prefix", + "description" : "The prefix of the object key that the output will be saved to", + "optional" : true, + "feel" : "optional", + "group" : "input", + "binding" : { + "name" : "input.outputConfigS3Prefix", + "type" : "zeebe:input" + }, + "condition" : { + "property" : "input.executionType", + "equals" : "ASYNC", + "type" : "simple" + }, + "type" : "String" + }, { + "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", + "feel" : "optional", + "group" : "retries", + "binding" : { + "key" : "retryBackoff", + "type" : "zeebe:taskHeader" + }, + "type" : "String" + } ], + "icon" : { + "contents" : "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iODBweCIgaGVpZ2h0PSI4MHB4IiB2aWV3Qm94PSIwIDAgODAgODAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgICA8dGl0bGU+SWNvbi1BcmNoaXRlY3R1cmUvNjQvQXJjaF9BbWF6b24tVGV4dHJhY3RfNjQ8L3RpdGxlPgogICAgPGcgaWQ9Ikljb24tQXJjaGl0ZWN0dXJlLzY0L0FyY2hfQW1hem9uLVRleHRyYWN0XzY0IiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj4KICAgICAgICA8ZyBpZD0iSWNvbi1BcmNoaXRlY3R1cmUtQkcvNjQvTWFjaGluZS1MZWFybmluZyIgZmlsbD0iIzk5NjlmNyI+CiAgICAgICAgICAgIDxyZWN0IGlkPSJSZWN0YW5nbGUiIHg9IjAiIHk9IjAiIHdpZHRoPSI4MCIgaGVpZ2h0PSI4MCI+PC9yZWN0PgogICAgICAgIDwvZz4KICAgICAgICA8cGF0aCBkPSJNMjIuMDYyNDEwMiw1MCBDMjQuMzc2Mzg5NSw1My42MDMgMjguNDEwMzUzNSw1NiAzMy4wMDAzMTI1LDU2IEM0MC4xNjcyNDg1LDU2IDQ1Ljk5OTE5NjQsNTAuMTY4IDQ1Ljk5OTE5NjQsNDMgQzQ1Ljk5OTE5NjQsMzUuODMyIDQwLjE2NzI0ODUsMzAgMzMuMDAwMzEyNSwzMCBDMjcuNjAzMzYwNywzMCAyMi45NjY0MDIxLDMzLjMwNyAyMS4wMDI0MTk2LDM4IEwyMy4yMTQzOTk5LDM4IEMyNS4wMzkzODM2LDM0LjQ0NCAyOC43MzYzNTA2LDMyIDMzLjAwMDMxMjUsMzIgQzM5LjA2NTI1ODMsMzIgNDMuOTk5MjE0MywzNi45MzUgNDMuOTk5MjE0Myw0MyBDNDMuOTk5MjE0Myw0OS4wNjUgMzkuMDY1MjU4Myw1NCAzMy4wMDAzMTI1LDU0IEMyOS41OTEzNDI5LDU0IDI2LjU0MTM3MDIsNTIuNDQxIDI0LjUyMTM4ODIsNTAgTDIyLjA2MjQxMDIsNTAgWiBNMzcuMDAwMjc2OCw0NSBMMzcuMDAwMjc2OCw0MyBMNDEuOTk5MjMyMSw0MyBDNDEuOTk5MjMyMSwzOC4wMzggMzcuOTYyMjY4MiwzNCAzMy4wMDAzMTI1LDM0IEMyOC4wMzczNTY4LDM0IDIzLjk5OTM5MjksMzguMDM4IDIzLjk5OTM5MjksNDMgTDI4Ljk5OTM0ODIsNDMgTDI4Ljk5OTM0ODIsNDUgTDI0LjIzMTM5MDgsNDUgQzI1LjE0NDM4MjYsNDkuMDAyIDI4LjcyNTM1MDcsNTIgMzMuMDAwMzEyNSw1MiBDMzUuMTM2MjkzNCw1MiAzNy4wOTkyNzU5LDUxLjI0OSAzOC42NDQyNjIxLDUwIEwzNC4wMDAzMDM2LDUwIEwzNC4wMDAzMDM2LDQ4IEw0MC40NzgyNDU3LDQ4IEM0MS4wODEyNDAzLDQ3LjEwMiA0MS41MjAyMzY0LDQ2LjA4NyA0MS43NjgyMzQyLDQ1IEwzNy4wMDAyNzY4LDQ1IFogTTIxLjAwMjQxOTYsNDggTDIzLjIxNDM5OTksNDggQzIyLjQ0MzQwNjgsNDYuNDk4IDIyLjAwMDQxMDcsNDQuODAxIDIyLjAwMDQxMDcsNDMgQzIyLjAwMDQxMDcsNDEuOTU5IDIyLjE1NTQwOTMsNDAuOTU1IDIyLjQyNjQwNjksNDAgTDIwLjM2MzQyNTMsNDAgQzIwLjEzNDQyNzQsNDAuOTY1IDE5Ljk5OTQyODYsNDEuOTY2IDE5Ljk5OTQyODYsNDMgQzE5Ljk5OTQyODYsNDQuNzcxIDIwLjM1ODQyNTQsNDYuNDYgMjEuMDAyNDE5Niw0OCBMMjEuMDAyNDE5Niw0OCBaIE0xOS43NDM0MzA5LDUwIEwxNy4wMDA0NTU0LDUwIEwxNy4wMDA0NTU0LDQ4IEwxOC44NzQ0Mzg2LDQ4IEMxOC41MzQ0NDE3LDQ3LjA0IDE4LjI4OTQ0MzgsNDYuMDM4IDE4LjE0OTQ0NTEsNDUgTDE1LjQxNDQ2OTUsNDUgTDE2LjcwNzQ1OCw0Ni4yOTMgTDE1LjI5MjQ3MDYsNDcuNzA3IEwxMi4yOTI0OTc0LDQ0LjcwNyBDMTEuOTAyNTAwOSw0NC4zMTYgMTEuOTAyNTAwOSw0My42ODQgMTIuMjkyNDk3NCw0My4yOTMgTDE1LjI5MjQ3MDYsNDAuMjkzIEwxNi43MDc0NTgsNDEuNzA3IEwxNS40MTQ0Njk1LDQzIEwxOC4wMDA0NDY0LDQzIEMxOC4wMDA0NDY0LDQxLjk3MyAxOC4xMDQ0NDU1LDQwLjk3IDE4LjMwMjQ0MzcsNDAgTDE3LjAwMDQ1NTQsNDAgTDE3LjAwMDQ1NTQsMzggTDE4Ljg3NDQzODYsMzggQzIwLjk0MDQyMDIsMzIuMTg0IDI2LjQ4MzM3MDcsMjggMzMuMDAwMzEyNSwyOCBDMzcuNDI3MjczLDI4IDQxLjQwMDIzNzUsMjkuOTM5IDQ0LjE0ODIxMywzMyBMNTkuMDAwMDgwNCwzMyBMNTkuMDAwMDgwNCwzNSBMNDUuNjY2MTk5NCwzNSBDNDcuMTM1MTg2MywzNy4zMTggNDcuOTk5MTc4Niw0MC4wNTggNDcuOTk5MTc4Niw0MyBMNTkuMDAwMDgwNCw0MyBMNTkuMDAwMDgwNCw0NSBMNDcuODUwMTc5OSw0NSBDNDYuODY4MTg4Nyw1Mi4zMjcgNDAuNTkxMjQ0Nyw1OCAzMy4wMDAzMTI1LDU4IEMyNy4yNTYzNjM4LDU4IDIyLjI2MjQwODQsNTQuNzUyIDE5Ljc0MzQzMDksNTAgTDE5Ljc0MzQzMDksNTAgWiBNMzcuMDAwMjc2OCwzOSBDMzcuMDAwMjc2OCwzOC40NDggMzYuNTUyMjgwOCwzOCAzNi4wMDAyODU3LDM4IEwyOS45OTkzNDgyLDM4IEMyOS40NDczNDQyLDM4IDI4Ljk5OTM0ODIsMzguNDQ4IDI4Ljk5OTM0ODIsMzkgTDI4Ljk5OTM0ODIsNDEgTDMxLjAwMDMzMDQsNDEgTDMxLjAwMDMzMDQsNDAgTDMyLjAwMDMyMTQsNDAgTDMyLjAwMDMyMTQsNDMgTDMxLjAwMDMzMDQsNDMgTDMxLjAwMDMzMDQsNDUgTDM1LjAwMDI5NDYsNDUgTDM1LjAwMDI5NDYsNDMgTDM0LjAwMDMwMzYsNDMgTDM0LjAwMDMwMzYsNDAgTDM1LjAwMDI5NDYsNDAgTDM1LjAwMDI5NDYsNDEgTDM3LjAwMDI3NjgsNDEgTDM3LjAwMDI3NjgsMzkgWiBNNDkuMDAwMTY5Niw0MCBMNTkuMDAwMDgwNCw0MCBMNTkuMDAwMDgwNCwzOCBMNDkuMDAwMTY5NiwzOCBMNDkuMDAwMTY5Niw0MCBaIE00OS4wMDAxNjk2LDUwIEw1OS4wMDAwODA0LDUwIEw1OS4wMDAwODA0LDQ4IEw0OS4wMDAxNjk2LDQ4IEw0OS4wMDAxNjk2LDUwIFogTTU3LjAwMDA5ODIsMjcgTDYwLjU4NTA2NjIsMjcgTDU3LjAwMDA5ODIsMjMuNDE0IEw1Ny4wMDAwOTgyLDI3IFogTTYzLjcwNzAzODMsMjcuMjkzIEM2My44OTQwMzY3LDI3LjQ4IDY0LjAwMDAzNTcsMjcuNzM1IDY0LjAwMDAzNTcsMjggTDY0LjAwMDAzNTcsNjMgQzY0LjAwMDAzNTcsNjMuNTUyIDYzLjU1MjAzOTcsNjQgNjMuMDAwMDQ0Niw2NCBMMzIuMDAwMzMwNCw2NCBDMzEuNDQ3MzI2NCw2NCAzMS4wMDAzMzA0LDYzLjU1MiAzMS4wMDAzMzA0LDYzIEwzMS4wMDAzMzA0LDU5IEwzMy4wMDAzMTI1LDU5IEwzMy4wMDAzMTI1LDYyIEw2Mi4wMDAwNTM2LDYyIEw2Mi4wMDAwNTM2LDI5IEw1Ni4wMDAxMDcxLDI5IEM1NS40NDcxMTIxLDI5IDU1LjAwMDExNjEsMjguNTUyIDU1LjAwMDExNjEsMjggTDU1LjAwMDExNjEsMjIgTDMzLjAwMDMxMjUsMjIgTDMzLjAwMDMxMjUsMjcgTDMxLjAwMDMzMDQsMjcgTDMxLjAwMDMzMDQsMjEgQzMxLjAwMDMzMDQsMjAuNDQ4IDMxLjQ0NzMyNjQsMjAgMzIuMDAwMzMwNCwyMCBMNTYuMDAwMTA3MSwyMCBDNTYuMjY1MTA0OCwyMCA1Ni41MTkxMDI1LDIwLjEwNSA1Ni43MDcxMDA4LDIwLjI5MyBMNjMuNzA3MDM4MywyNy4yOTMgWiBNNjgsMjQuMTY2IEw2OCw2MSBDNjgsNjEuNTUyIDY3LjU1MjAwNCw2MiA2Ny4wMDAwMDg5LDYyIEw2NS4wMDAwMjY4LDYyIEw2NS4wMDAwMjY4LDYwIEw2Ni4wMDAwMTc5LDYwIEw2Ni4wMDAwMTc5LDI0LjYxMiBMNTguNjE3MDgzOCwxOCBMMzYuMDAwMjg1NywxOCBMMzYuMDAwMjg1NywxOSBMMzQuMDAwMzAzNiwxOSBMMzQuMDAwMzAzNiwxNyBDMzQuMDAwMzAzNiwxNi40NDggMzQuNDQ3Mjk5NiwxNiAzNS4wMDAzMDM2LDE2IEw1OS4wMDAwODA0LDE2IEM1OS4yNDYwNzgyLDE2IDU5LjQ4MzA3NiwxNi4wOTEgNTkuNjY2MDc0NCwxNi4yNTUgTDY3LjY2NjAwMywyMy40MiBDNjcuODc4MDAxMSwyMy42MSA2OCwyMy44ODEgNjgsMjQuMTY2IEw2OCwyNC4xNjYgWiIgaWQ9IkFtYXpvbi1UZXh0cmFjdF9JY29uXzY0X1NxdWlkIiBmaWxsPSIjRkZGRkZGIj48L3BhdGg+CiAgICA8L2c+Cjwvc3ZnPg==" + } +} \ No newline at end of file diff --git a/connectors/aws/aws-textract/pom.xml b/connectors/aws/aws-textract/pom.xml new file mode 100644 index 0000000000..ecafa57ca1 --- /dev/null +++ b/connectors/aws/aws-textract/pom.xml @@ -0,0 +1,78 @@ + + 4.0.0 + + + io.camunda.connector + connector-aws-parent + 8.6.0-SNAPSHOT + ../pom.xml + + + connector-aws-textract + Camunda AWS Textract Connector + connector-aws-textract + 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 + + + + + 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. + + + + + io.camunda.connector + connector-aws-base + ${project.version} + + + + com.amazonaws + aws-java-sdk-textract + ${version.aws-java-sdk} + + + + + + + + io.camunda.connector + element-template-generator-maven-plugin + ${project.version} + + + + io.camunda.connector.textract.TextractConnectorFunction + + + io.camunda.connectors.AWSTEXTRACT.v1 + aws-textract-outbound-connector.json + + + true + + + + io.camunda.connector:connector-aws-base + + + + + + + + + diff --git a/connectors/aws/aws-textract/src/main/java/io/camunda/connector/textract/TextractConnectorFunction.java b/connectors/aws/aws-textract/src/main/java/io/camunda/connector/textract/TextractConnectorFunction.java new file mode 100644 index 0000000000..7eb1abde7d --- /dev/null +++ b/connectors/aws/aws-textract/src/main/java/io/camunda/connector/textract/TextractConnectorFunction.java @@ -0,0 +1,81 @@ +/* + * 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.textract; + +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.generator.java.annotation.ElementTemplate; +import io.camunda.connector.textract.caller.AsyncTextractCaller; +import io.camunda.connector.textract.caller.PollingTextractCalller; +import io.camunda.connector.textract.caller.SyncTextractCaller; +import io.camunda.connector.textract.model.TextractRequest; +import io.camunda.connector.textract.suppliers.AmazonTextractClientSupplier; + +@OutboundConnector( + name = "AWS Textract", + inputVariables = {"authentication", "configuration", "input"}, + type = "io.camunda:aws-textract:1") +@ElementTemplate( + id = "io.camunda.connectors.AWSTEXTRACT.v1", + name = "AWS Textract Outbound Connector", + description = + "Automatically extract printed text, handwriting, layout elements, and data from any document", + inputDataClass = TextractRequest.class, + version = 1, + propertyGroups = { + @ElementTemplate.PropertyGroup(id = "authentication", label = "Authentication"), + @ElementTemplate.PropertyGroup(id = "configuration", label = "Configuration"), + @ElementTemplate.PropertyGroup(id = "input", label = "Configure input") + }, + documentationRef = + "https://docs.camunda.io/docs/next/components/connectors/out-of-the-box-connectors/amazon-textract/", + icon = "icon.svg") +public class TextractConnectorFunction implements OutboundConnectorFunction { + + private final AmazonTextractClientSupplier clientSupplier; + + private final SyncTextractCaller syncTextractCaller; + + private final PollingTextractCalller pollingTextractCaller; + + private final AsyncTextractCaller asyncTextractCaller; + + public TextractConnectorFunction() { + this.clientSupplier = new AmazonTextractClientSupplier(); + this.syncTextractCaller = new SyncTextractCaller(); + this.pollingTextractCaller = new PollingTextractCalller(); + this.asyncTextractCaller = new AsyncTextractCaller(); + } + + public TextractConnectorFunction( + AmazonTextractClientSupplier clientSupplier, + SyncTextractCaller syncTextractCaller, + PollingTextractCalller pollingTextractCaller, + AsyncTextractCaller asyncTextractCaller) { + this.clientSupplier = clientSupplier; + this.syncTextractCaller = syncTextractCaller; + this.pollingTextractCaller = pollingTextractCaller; + this.asyncTextractCaller = asyncTextractCaller; + } + + @Override + public Object execute(OutboundConnectorContext context) throws Exception { + TextractRequest request = context.bindVariables(TextractRequest.class); + return switch (request.getInput().executionType()) { + case SYNC -> + syncTextractCaller.call( + request.getInput(), clientSupplier.getSyncTextractClient(request)); + case POLLING -> + pollingTextractCaller.call( + request.getInput(), clientSupplier.getAsyncTextractClient(request)); + case ASYNC -> + asyncTextractCaller.call( + request.getInput(), clientSupplier.getAsyncTextractClient(request)); + }; + } +} diff --git a/connectors/aws/aws-textract/src/main/java/io/camunda/connector/textract/caller/AsyncTextractCaller.java b/connectors/aws/aws-textract/src/main/java/io/camunda/connector/textract/caller/AsyncTextractCaller.java new file mode 100644 index 0000000000..8d19bd1a6a --- /dev/null +++ b/connectors/aws/aws-textract/src/main/java/io/camunda/connector/textract/caller/AsyncTextractCaller.java @@ -0,0 +1,63 @@ +/* + * 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.textract.caller; + +import com.amazonaws.services.textract.AmazonTextract; +import com.amazonaws.services.textract.model.NotificationChannel; +import com.amazonaws.services.textract.model.OutputConfig; +import com.amazonaws.services.textract.model.StartDocumentAnalysisRequest; +import com.amazonaws.services.textract.model.StartDocumentAnalysisResult; +import io.camunda.connector.textract.model.TextractRequestData; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AsyncTextractCaller implements TextractCaller { + + private static final Logger LOGGER = LoggerFactory.getLogger(AsyncTextractCaller.class); + + @Override + public StartDocumentAnalysisResult call( + TextractRequestData requestData, AmazonTextract textractClient) { + LOGGER.debug("Starting async task for document analysis with request data: {}", requestData); + final StartDocumentAnalysisRequest startDocumentAnalysisRequest = + new StartDocumentAnalysisRequest() + .withFeatureTypes(prepareFeatureTypes(requestData)) + .withDocumentLocation(prepareDocumentLocation(requestData)) + .withClientRequestToken(requestData.clientRequestToken()) + .withJobTag(requestData.jobTag()) + .withKMSKeyId(requestData.kmsKeyId()); + + prepareNotification(startDocumentAnalysisRequest, requestData); + prepareOutput(startDocumentAnalysisRequest, requestData); + + return textractClient.startDocumentAnalysis(startDocumentAnalysisRequest); + } + + private void prepareNotification( + StartDocumentAnalysisRequest startDocumentAnalysisRequest, TextractRequestData requestData) { + String roleArn = requestData.notificationChannelRoleArn(); + String snsTopic = requestData.notificationChannelSnsTopicArn(); + if (StringUtils.isNoneBlank(roleArn, snsTopic)) { + LOGGER.debug("Notification data roleArn: {}, snsTopic: {}", roleArn, snsTopic); + NotificationChannel notificationChannel = + new NotificationChannel().withSNSTopicArn(snsTopic).withRoleArn(roleArn); + startDocumentAnalysisRequest.withNotificationChannel(notificationChannel); + } + } + + private void prepareOutput( + StartDocumentAnalysisRequest startDocumentAnalysisRequest, TextractRequestData requestData) { + String s3Bucket = requestData.outputConfigS3Bucket(); + String s3Prefix = requestData.outputConfigS3Prefix(); + if (StringUtils.isNoneBlank(s3Bucket)) { + LOGGER.debug("Output data s3Bucket: {}, s3Prefix: {} ", s3Bucket, s3Prefix); + OutputConfig outputConfig = new OutputConfig().withS3Bucket(s3Bucket).withS3Prefix(s3Prefix); + startDocumentAnalysisRequest.withOutputConfig(outputConfig); + } + } +} diff --git a/connectors/aws/aws-textract/src/main/java/io/camunda/connector/textract/caller/PollingTextractCalller.java b/connectors/aws/aws-textract/src/main/java/io/camunda/connector/textract/caller/PollingTextractCalller.java new file mode 100644 index 0000000000..aa50517fb2 --- /dev/null +++ b/connectors/aws/aws-textract/src/main/java/io/camunda/connector/textract/caller/PollingTextractCalller.java @@ -0,0 +1,59 @@ +/* + * 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.textract.caller; + +import static java.util.concurrent.TimeUnit.SECONDS; + +import com.amazonaws.services.textract.AmazonTextract; +import com.amazonaws.services.textract.AmazonTextractAsync; +import com.amazonaws.services.textract.model.GetDocumentAnalysisRequest; +import com.amazonaws.services.textract.model.GetDocumentAnalysisResult; +import com.amazonaws.services.textract.model.StartDocumentAnalysisRequest; +import com.amazonaws.services.textract.model.StartDocumentAnalysisResult; +import io.camunda.connector.textract.model.TextractRequestData; +import io.camunda.connector.textract.model.TextractTask; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class PollingTextractCalller implements TextractCaller { + public static final long DELAY_BETWEEN_POLLING = 5; + + private static final Logger LOGGER = LoggerFactory.getLogger(PollingTextractCalller.class); + + @Override + public GetDocumentAnalysisResult call( + TextractRequestData requestData, AmazonTextract textractClient) throws Exception { + LOGGER.debug("Starting polling task for document analysis with request data: {}", requestData); + final StartDocumentAnalysisRequest startDocReq = + new StartDocumentAnalysisRequest() + .withFeatureTypes(this.prepareFeatureTypes(requestData)) + .withDocumentLocation(this.prepareDocumentLocation(requestData)); + + final StartDocumentAnalysisResult result = textractClient.startDocumentAnalysis(startDocReq); + final var documentAnalysisReq = new GetDocumentAnalysisRequest().withJobId(result.getJobId()); + final var textractTask = + new TextractTask(documentAnalysisReq, (AmazonTextractAsync) textractClient); + + ScheduledFuture future; + try (ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor()) { + future = executorService.schedule(textractTask, 0, SECONDS); + + while (continuePolling(future.get().getJobStatus())) { + future = executorService.schedule(textractTask, DELAY_BETWEEN_POLLING, SECONDS); + } + } + + return future.get(); + } + + private boolean continuePolling(String status) { + return "IN_PROGRESS".equals(status); + } +} diff --git a/connectors/aws/aws-textract/src/main/java/io/camunda/connector/textract/caller/SyncTextractCaller.java b/connectors/aws/aws-textract/src/main/java/io/camunda/connector/textract/caller/SyncTextractCaller.java new file mode 100644 index 0000000000..dfcd667f67 --- /dev/null +++ b/connectors/aws/aws-textract/src/main/java/io/camunda/connector/textract/caller/SyncTextractCaller.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.textract.caller; + +import com.amazonaws.services.textract.AmazonTextract; +import com.amazonaws.services.textract.model.AnalyzeDocumentRequest; +import com.amazonaws.services.textract.model.AnalyzeDocumentResult; +import com.amazonaws.services.textract.model.Document; +import io.camunda.connector.textract.model.TextractRequestData; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SyncTextractCaller implements TextractCaller { + + private static final Logger LOGGER = LoggerFactory.getLogger(SyncTextractCaller.class); + + @Override + public AnalyzeDocumentResult call( + TextractRequestData requestData, AmazonTextract textractClient) { + LOGGER.debug("Starting sync task for document analysis with request data: {}", requestData); + final Document document = new Document().withS3Object(prepareS3Obj(requestData)); + + final AnalyzeDocumentRequest analyzeDocumentRequest = + new AnalyzeDocumentRequest() + .withFeatureTypes(prepareFeatureTypes(requestData)) + .withDocument(document); + + return textractClient.analyzeDocument(analyzeDocumentRequest); + } +} diff --git a/connectors/aws/aws-textract/src/main/java/io/camunda/connector/textract/caller/TextractCaller.java b/connectors/aws/aws-textract/src/main/java/io/camunda/connector/textract/caller/TextractCaller.java new file mode 100644 index 0000000000..046ea81bea --- /dev/null +++ b/connectors/aws/aws-textract/src/main/java/io/camunda/connector/textract/caller/TextractCaller.java @@ -0,0 +1,58 @@ +/* + * 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.textract.caller; + +import com.amazonaws.AmazonWebServiceResult; +import com.amazonaws.ResponseMetadata; +import com.amazonaws.services.textract.AmazonTextract; +import com.amazonaws.services.textract.model.DocumentLocation; +import com.amazonaws.services.textract.model.FeatureType; +import com.amazonaws.services.textract.model.S3Object; +import io.camunda.connector.textract.model.TextractRequestData; +import java.util.HashSet; +import java.util.Set; + +public interface TextractCaller> { + + String WRONG_ANALYZE_TYPE_MSG = "At least one analyze type should be selected"; + + T call(final TextractRequestData request, final AmazonTextract textractClient) throws Exception; + + default S3Object prepareS3Obj(final TextractRequestData requestData) { + return new S3Object() + .withBucket(requestData.documentS3Bucket()) + .withName(requestData.documentName()) + .withVersion(requestData.documentVersion()); + } + + default Set prepareFeatureTypes(final TextractRequestData request) { + final Set types = new HashSet<>(); + if (request.analyzeForms()) { + types.add(FeatureType.FORMS.name()); + } + + if (request.analyzeLayout()) { + types.add(FeatureType.LAYOUT.name()); + } + + if (request.analyzeSignatures()) { + types.add(FeatureType.SIGNATURES.name()); + } + if (request.analyzeTables()) { + types.add(FeatureType.TABLES.name()); + } + if (types.isEmpty()) { + throw new IllegalArgumentException(WRONG_ANALYZE_TYPE_MSG); + } + return types; + } + + default DocumentLocation prepareDocumentLocation(final TextractRequestData request) { + final S3Object s3Obj = prepareS3Obj(request); + return new DocumentLocation().withS3Object(s3Obj); + } +} diff --git a/connectors/aws/aws-textract/src/main/java/io/camunda/connector/textract/model/TextractExecutionType.java b/connectors/aws/aws-textract/src/main/java/io/camunda/connector/textract/model/TextractExecutionType.java new file mode 100644 index 0000000000..fed5cc319f --- /dev/null +++ b/connectors/aws/aws-textract/src/main/java/io/camunda/connector/textract/model/TextractExecutionType.java @@ -0,0 +1,13 @@ +/* + * 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.textract.model; + +public enum TextractExecutionType { + SYNC, + POLLING, + ASYNC +} diff --git a/connectors/aws/aws-textract/src/main/java/io/camunda/connector/textract/model/TextractRequest.java b/connectors/aws/aws-textract/src/main/java/io/camunda/connector/textract/model/TextractRequest.java new file mode 100644 index 0000000000..d046103b74 --- /dev/null +++ b/connectors/aws/aws-textract/src/main/java/io/camunda/connector/textract/model/TextractRequest.java @@ -0,0 +1,50 @@ +/* + * 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.textract.model; + +import io.camunda.connector.aws.model.impl.AwsBaseRequest; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + +public class TextractRequest extends AwsBaseRequest { + @Valid @NotNull private TextractRequestData input; + + public TextractRequestData getInput() { + return input; + } + + public void setInput(TextractRequestData input) { + this.input = input; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()) { + return false; + } + + TextractRequest that = (TextractRequest) o; + + return new EqualsBuilder().appendSuper(super.equals(o)).append(input, that.input).isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37).appendSuper(super.hashCode()).append(input).toHashCode(); + } + + @Override + public String toString() { + return "TextractRequest{" + "input=" + input + '}'; + } +} diff --git a/connectors/aws/aws-textract/src/main/java/io/camunda/connector/textract/model/TextractRequestData.java b/connectors/aws/aws-textract/src/main/java/io/camunda/connector/textract/model/TextractRequestData.java new file mode 100644 index 0000000000..7121d55913 --- /dev/null +++ b/connectors/aws/aws-textract/src/main/java/io/camunda/connector/textract/model/TextractRequestData.java @@ -0,0 +1,184 @@ +/* + * 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.textract.model; + +import io.camunda.connector.generator.dsl.Property.FeelMode; +import io.camunda.connector.generator.java.annotation.TemplateProperty; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import org.apache.commons.lang3.StringUtils; + +public record TextractRequestData( + @TemplateProperty( + label = "Execution type", + group = "input", + type = TemplateProperty.PropertyType.Dropdown, + defaultValue = "ASYNC", + feel = FeelMode.disabled, + choices = { + @TemplateProperty.DropdownPropertyChoice(value = "ASYNC", label = "Asynchronous"), + @TemplateProperty.DropdownPropertyChoice(value = "SYNC", label = "Real-time"), + @TemplateProperty.DropdownPropertyChoice(value = "POLLING", label = "Polling") + }, + description = "Endpoint inference type") + @NotNull + TextractExecutionType executionType, + @TemplateProperty( + group = "input", + label = "Document bucket", + description = "S3 bucket that contains document that needs to be processed") + @NotBlank + String documentS3Bucket, + @TemplateProperty( + group = "input", + label = "Document path", + description = "S3 document path to be processed") + @NotBlank + String documentName, + @TemplateProperty( + group = "input", + label = "Document version", + description = "S3 document version to be processed", + optional = true) + String documentVersion, + @TemplateProperty( + label = "Analyze tables", + group = "input", + type = TemplateProperty.PropertyType.Boolean, + defaultValueType = TemplateProperty.DefaultValueType.Boolean, + defaultValue = "true") + @NotNull + boolean analyzeTables, + @TemplateProperty( + label = "Analyze form", + group = "input", + type = TemplateProperty.PropertyType.Boolean, + defaultValueType = TemplateProperty.DefaultValueType.Boolean, + defaultValue = "true") + @NotNull + boolean analyzeForms, + @TemplateProperty( + label = "Analyze signatures", + group = "input", + type = TemplateProperty.PropertyType.Boolean, + defaultValueType = TemplateProperty.DefaultValueType.Boolean, + defaultValue = "true") + @NotNull + boolean analyzeSignatures, + @TemplateProperty( + label = "Analyze layout", + group = "input", + type = TemplateProperty.PropertyType.Boolean, + defaultValueType = TemplateProperty.DefaultValueType.Boolean, + defaultValue = "true") + @NotNull + boolean analyzeLayout, + @TemplateProperty( + group = "input", + label = "Client request token", + description = "The idempotent token that you use to identify the start request", + optional = true, + condition = + @TemplateProperty.PropertyCondition( + property = "input.executionType", + equals = "ASYNC")) + String clientRequestToken, + @TemplateProperty( + group = "input", + label = "Job tag", + description = + "An identifier that you specify that's included in the completion notification published to the Amazon SNS topic", + optional = true, + condition = + @TemplateProperty.PropertyCondition( + property = "input.executionType", + equals = "ASYNC")) + String jobTag, + @TemplateProperty( + group = "input", + label = "KMS key ID", + description = "The KMS key used to encrypt the inference results", + optional = true, + condition = + @TemplateProperty.PropertyCondition( + property = "input.executionType", + equals = "ASYNC")) + String kmsKeyId, + @TemplateProperty( + group = "input", + label = "Notification channel role ARN", + description = + "The Amazon SNS topic role ARN that you want Amazon Textract to publish the completion status of the operation to", + optional = true, + condition = + @TemplateProperty.PropertyCondition( + property = "input.executionType", + equals = "ASYNC")) + String notificationChannelRoleArn, + @TemplateProperty( + group = "input", + label = "Notification channel SNS topic ARN", + description = + "The Amazon SNS topic ARN that you want Amazon Textract to publish the completion status of the operation to", + optional = true, + condition = + @TemplateProperty.PropertyCondition( + property = "input.executionType", + equals = "ASYNC")) + String notificationChannelSnsTopicArn, + @TemplateProperty( + group = "input", + label = "Output S3 bucket", + description = "The name of the bucket your output will go to", + optional = true, + condition = + @TemplateProperty.PropertyCondition( + property = "input.executionType", + equals = "ASYNC")) + String outputConfigS3Bucket, + @TemplateProperty( + group = "input", + label = "Output S3 prefix", + description = "The prefix of the object key that the output will be saved to", + optional = true, + condition = + @TemplateProperty.PropertyCondition( + property = "input.executionType", + equals = "ASYNC")) + String outputConfigS3Prefix) { + public static final String WRONG_OUTPUT_VALUES_MSG = + "Output S3 bucket must be filled in if output S3 prefix is filled in"; + + public static final String WRONG_NOTIFICATION_VALUES_MSG = + "either both notification values role ARN and topic ARN must be filled in or none of them"; + + @AssertTrue(message = WRONG_NOTIFICATION_VALUES_MSG) + public boolean isValidNotificationProperties() { + if (executionType != TextractExecutionType.ASYNC) { + return true; + } + return StringUtils.isNoneBlank(notificationChannelRoleArn, notificationChannelSnsTopicArn) + || StringUtils.isAllBlank(notificationChannelRoleArn, notificationChannelSnsTopicArn); + } + + @AssertTrue(message = WRONG_OUTPUT_VALUES_MSG) + public boolean isValidOutputConfigProperties() { + if (executionType != TextractExecutionType.ASYNC) { + return true; + } + + if (StringUtils.isAllBlank(outputConfigS3Bucket, outputConfigS3Prefix)) { + return true; + } + + if (StringUtils.isNoneBlank(outputConfigS3Bucket, outputConfigS3Prefix)) { + return true; + } + return !StringUtils.isBlank(outputConfigS3Bucket); + } +} diff --git a/connectors/aws/aws-textract/src/main/java/io/camunda/connector/textract/model/TextractTask.java b/connectors/aws/aws-textract/src/main/java/io/camunda/connector/textract/model/TextractTask.java new file mode 100644 index 0000000000..98d548bac3 --- /dev/null +++ b/connectors/aws/aws-textract/src/main/java/io/camunda/connector/textract/model/TextractTask.java @@ -0,0 +1,30 @@ +/* + * 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.textract.model; + +import com.amazonaws.services.textract.AmazonTextractAsync; +import com.amazonaws.services.textract.model.GetDocumentAnalysisRequest; +import com.amazonaws.services.textract.model.GetDocumentAnalysisResult; +import java.util.concurrent.Callable; + +public class TextractTask implements Callable { + + private final GetDocumentAnalysisRequest docAnalysisReq; + + private final AmazonTextractAsync amazonTextract; + + public TextractTask( + GetDocumentAnalysisRequest documentAnalysisRequest, AmazonTextractAsync amazonTextract) { + this.docAnalysisReq = documentAnalysisRequest; + this.amazonTextract = amazonTextract; + } + + @Override + public GetDocumentAnalysisResult call() { + return this.amazonTextract.getDocumentAnalysis(docAnalysisReq); + } +} diff --git a/connectors/aws/aws-textract/src/main/java/io/camunda/connector/textract/suppliers/AmazonTextractClientSupplier.java b/connectors/aws/aws-textract/src/main/java/io/camunda/connector/textract/suppliers/AmazonTextractClientSupplier.java new file mode 100644 index 0000000000..61b0dc0ed7 --- /dev/null +++ b/connectors/aws/aws-textract/src/main/java/io/camunda/connector/textract/suppliers/AmazonTextractClientSupplier.java @@ -0,0 +1,31 @@ +/* + * 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.textract.suppliers; + +import com.amazonaws.services.textract.AmazonTextract; +import com.amazonaws.services.textract.AmazonTextractAsync; +import com.amazonaws.services.textract.AmazonTextractAsyncClientBuilder; +import com.amazonaws.services.textract.AmazonTextractClientBuilder; +import io.camunda.connector.aws.CredentialsProviderSupport; +import io.camunda.connector.textract.model.TextractRequest; + +public class AmazonTextractClientSupplier { + + public AmazonTextract getSyncTextractClient(final TextractRequest request) { + return AmazonTextractClientBuilder.standard() + .withCredentials(CredentialsProviderSupport.credentialsProvider(request)) + .withRegion(request.getConfiguration().region()) + .build(); + } + + public AmazonTextractAsync getAsyncTextractClient(final TextractRequest request) { + return AmazonTextractAsyncClientBuilder.standard() + .withCredentials(CredentialsProviderSupport.credentialsProvider(request)) + .withRegion(request.getConfiguration().region()) + .build(); + } +} diff --git a/connectors/aws/aws-textract/src/main/resources/META-INF/services/io.camunda.connector.api.outbound.OutboundConnectorFunction b/connectors/aws/aws-textract/src/main/resources/META-INF/services/io.camunda.connector.api.outbound.OutboundConnectorFunction new file mode 100644 index 0000000000..09eafaba71 --- /dev/null +++ b/connectors/aws/aws-textract/src/main/resources/META-INF/services/io.camunda.connector.api.outbound.OutboundConnectorFunction @@ -0,0 +1 @@ +io.camunda.connector.textract.TextractConnectorFunction \ No newline at end of file diff --git a/connectors/aws/aws-textract/src/main/resources/icon.svg b/connectors/aws/aws-textract/src/main/resources/icon.svg new file mode 100644 index 0000000000..8fba004b73 --- /dev/null +++ b/connectors/aws/aws-textract/src/main/resources/icon.svg @@ -0,0 +1,10 @@ + + + Icon-Architecture/64/Arch_Amazon-Textract_64 + + + + + + + \ No newline at end of file diff --git a/connectors/aws/aws-textract/src/test/java/io/camunda/connector/textract/TextractConnectorFunctionTest.java b/connectors/aws/aws-textract/src/test/java/io/camunda/connector/textract/TextractConnectorFunctionTest.java new file mode 100644 index 0000000000..60f73c3882 --- /dev/null +++ b/connectors/aws/aws-textract/src/test/java/io/camunda/connector/textract/TextractConnectorFunctionTest.java @@ -0,0 +1,121 @@ +/* + * 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.textract; + +import static io.camunda.connector.textract.util.TextractTestUtils.ASYNC_EXECUTION_JSON_WITH_ROLE_ARN_AND_WITHOUT_SNS_TOPIC; +import static io.camunda.connector.textract.util.TextractTestUtils.ASYNC_EXECUTION_JSON_WITH_SNS_TOPIC_AND_WITHOUT_ROLE_ARN; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import com.amazonaws.services.textract.model.AnalyzeDocumentResult; +import com.amazonaws.services.textract.model.GetDocumentAnalysisResult; +import com.amazonaws.services.textract.model.StartDocumentAnalysisResult; +import io.camunda.connector.api.error.ConnectorInputException; +import io.camunda.connector.test.outbound.OutboundConnectorContextBuilder; +import io.camunda.connector.textract.caller.AsyncTextractCaller; +import io.camunda.connector.textract.caller.PollingTextractCalller; +import io.camunda.connector.textract.caller.SyncTextractCaller; +import io.camunda.connector.textract.suppliers.AmazonTextractClientSupplier; +import io.camunda.connector.textract.util.TextractTestUtils; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class TextractConnectorFunctionTest { + + @Mock private SyncTextractCaller syncCaller; + @Mock private PollingTextractCalller pollingCaller; + @Mock private AsyncTextractCaller asyncCaller; + + @Mock private AmazonTextractClientSupplier clientSupplier; + + @InjectMocks private TextractConnectorFunction textractConnectorFunction; + + @Test + void executeSyncReq() throws Exception { + var outBounderContext = prepareConnectorContext(TextractTestUtils.SYNC_EXECUTION_JSON); + + when(clientSupplier.getSyncTextractClient(any())).thenCallRealMethod(); + when(syncCaller.call(any(), any())).thenReturn(new AnalyzeDocumentResult()); + + var result = textractConnectorFunction.execute(outBounderContext); + assertThat(result).isInstanceOf(AnalyzeDocumentResult.class); + } + + @Test + void executeAsyncReq() throws Exception { + var outBounderContext = prepareConnectorContext(TextractTestUtils.ASYNC_EXECUTION_JSON); + + when(clientSupplier.getAsyncTextractClient(any())).thenCallRealMethod(); + when(asyncCaller.call(any(), any())).thenReturn(new StartDocumentAnalysisResult()); + + var result = textractConnectorFunction.execute(outBounderContext); + assertThat(result).isInstanceOf(StartDocumentAnalysisResult.class); + } + + @Test + void executePollingReq() throws Exception { + var outBounderContext = prepareConnectorContext(TextractTestUtils.POLLING_EXECUTION_JSON); + + when(clientSupplier.getAsyncTextractClient(any())).thenCallRealMethod(); + when(pollingCaller.call(any(), any())).thenReturn(new GetDocumentAnalysisResult()); + + var result = textractConnectorFunction.execute(outBounderContext); + assertThat(result).isInstanceOf(GetDocumentAnalysisResult.class); + } + + @Test + void executeAsyncReqWithS3PrefixAndWithoutS3Bucket() { + var outBounderContext = + prepareConnectorContext(TextractTestUtils.ASYNC_EXECUTION_JSON_WITHOUT_S3_BUCKET_OUTPUT); + + Exception exception = + assertThrows( + ConnectorInputException.class, + () -> textractConnectorFunction.execute(outBounderContext)); + + assertThat(exception).isInstanceOf(ConnectorInputException.class); + } + + @ParameterizedTest + @ValueSource( + strings = { + ASYNC_EXECUTION_JSON_WITH_ROLE_ARN_AND_WITHOUT_SNS_TOPIC, + ASYNC_EXECUTION_JSON_WITH_SNS_TOPIC_AND_WITHOUT_ROLE_ARN + }) + void executeAsyncReqWithWrongNotificationData(String input) { + var outBounderContext = + OutboundConnectorContextBuilder.create() + .secret("ACCESS_KEY", TextractTestUtils.ACTUAL_ACCESS_KEY) + .secret("SECRET_KEY", TextractTestUtils.ACTUAL_SECRET_KEY) + .variables(input) + .build(); + + Exception exception = + assertThrows( + ConnectorInputException.class, + () -> textractConnectorFunction.execute(outBounderContext)); + + assertThat(exception).isInstanceOf(ConnectorInputException.class); + } + + private OutboundConnectorContextBuilder.TestConnectorContext prepareConnectorContext( + String json) { + return OutboundConnectorContextBuilder.create() + .secret("ACCESS_KEY", TextractTestUtils.ACTUAL_ACCESS_KEY) + .secret("SECRET_KEY", TextractTestUtils.ACTUAL_SECRET_KEY) + .variables(json) + .build(); + } +} diff --git a/connectors/aws/aws-textract/src/test/java/io/camunda/connector/textract/caller/AsyncTextractCallerTest.java b/connectors/aws/aws-textract/src/test/java/io/camunda/connector/textract/caller/AsyncTextractCallerTest.java new file mode 100644 index 0000000000..2181647975 --- /dev/null +++ b/connectors/aws/aws-textract/src/test/java/io/camunda/connector/textract/caller/AsyncTextractCallerTest.java @@ -0,0 +1,137 @@ +/* + * 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.textract.caller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.amazonaws.services.textract.AmazonTextractAsyncClient; +import com.amazonaws.services.textract.model.StartDocumentAnalysisRequest; +import com.amazonaws.services.textract.model.StartDocumentAnalysisResult; +import io.camunda.connector.textract.model.TextractExecutionType; +import io.camunda.connector.textract.model.TextractRequestData; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class AsyncTextractCallerTest { + + @Captor private ArgumentCaptor requestArgumentCaptor; + + @Test + void callWithAllFields() { + TextractRequestData requestData = prepareReqData("roleArn", "topicArna"); + + AmazonTextractAsyncClient asyncClient = Mockito.mock(AmazonTextractAsyncClient.class); + when(asyncClient.startDocumentAnalysis(any(StartDocumentAnalysisRequest.class))) + .thenReturn(new StartDocumentAnalysisResult()); + + new AsyncTextractCaller().call(requestData, asyncClient); + + verify(asyncClient).startDocumentAnalysis(requestArgumentCaptor.capture()); + + final StartDocumentAnalysisRequest startDocumentAnalysisRequest = + requestArgumentCaptor.getValue(); + + assertThat(startDocumentAnalysisRequest.getFeatureTypes().size()).isEqualTo(4); + assertThat(startDocumentAnalysisRequest.getDocumentLocation()).isNotNull(); + assertThat(startDocumentAnalysisRequest.getClientRequestToken()) + .isEqualTo(requestData.clientRequestToken()); + assertThat(startDocumentAnalysisRequest.getJobTag()).isEqualTo(requestData.jobTag()); + assertThat(startDocumentAnalysisRequest.getKMSKeyId()).isEqualTo(requestData.kmsKeyId()); + assertThat(startDocumentAnalysisRequest.getNotificationChannel()).isNotNull(); + assertThat(startDocumentAnalysisRequest.getNotificationChannel().getRoleArn()) + .isEqualTo(requestData.notificationChannelRoleArn()); + assertThat(startDocumentAnalysisRequest.getNotificationChannel().getSNSTopicArn()) + .isEqualTo(requestData.notificationChannelSnsTopicArn()); + assertThat(startDocumentAnalysisRequest.getOutputConfig()).isNotNull(); + assertThat(startDocumentAnalysisRequest.getOutputConfig().getS3Bucket()) + .isEqualTo(requestData.outputConfigS3Bucket()); + assertThat(startDocumentAnalysisRequest.getOutputConfig().getS3Prefix()) + .isEqualTo(requestData.outputConfigS3Prefix()); + } + + @Test + void callWithoutNotificationChanelFieldsShouldNotCreateNotificationObj() { + TextractRequestData requestData = prepareReqData("", ""); + + AmazonTextractAsyncClient asyncClient = Mockito.mock(AmazonTextractAsyncClient.class); + when(asyncClient.startDocumentAnalysis(any(StartDocumentAnalysisRequest.class))) + .thenReturn(new StartDocumentAnalysisResult()); + + new AsyncTextractCaller().call(requestData, asyncClient); + + verify(asyncClient).startDocumentAnalysis(requestArgumentCaptor.capture()); + + final StartDocumentAnalysisRequest startDocumentAnalysisRequest = + requestArgumentCaptor.getValue(); + + assertThat(startDocumentAnalysisRequest.getNotificationChannel()).isNull(); + } + + @Test + void callWithoutOutputS3BucketShouldNotCreateOutputObj() { + TextractRequestData requestData = prepareReqDataWithoutOutputS3Bucket(); + + AmazonTextractAsyncClient asyncClient = Mockito.mock(AmazonTextractAsyncClient.class); + when(asyncClient.startDocumentAnalysis(any(StartDocumentAnalysisRequest.class))) + .thenReturn(new StartDocumentAnalysisResult()); + + new AsyncTextractCaller().call(requestData, asyncClient); + + verify(asyncClient).startDocumentAnalysis(requestArgumentCaptor.capture()); + + final StartDocumentAnalysisRequest startDocumentAnalysisRequest = + requestArgumentCaptor.getValue(); + + assertThat(startDocumentAnalysisRequest.getOutputConfig()).isNull(); + } + + private TextractRequestData prepareReqData(String roleArn, String topicArn) { + return new TextractRequestData( + TextractExecutionType.ASYNC, + "test-bucket", + "test-object", + "1", + true, + true, + true, + true, + "token", + "jobTag", + "kmsId", + roleArn, + topicArn, + "outputBucket", + "prefix"); + } + + private TextractRequestData prepareReqDataWithoutOutputS3Bucket() { + return new TextractRequestData( + TextractExecutionType.ASYNC, + "test-bucket", + "test-object", + "1", + true, + true, + true, + true, + "token", + "jobTag", + "kmsId", + "roleArn", + "topicArn", + "", + "prefix"); + } +} diff --git a/connectors/aws/aws-textract/src/test/java/io/camunda/connector/textract/caller/PollingTextractCalllerTest.java b/connectors/aws/aws-textract/src/test/java/io/camunda/connector/textract/caller/PollingTextractCalllerTest.java new file mode 100644 index 0000000000..6bec1bdc36 --- /dev/null +++ b/connectors/aws/aws-textract/src/test/java/io/camunda/connector/textract/caller/PollingTextractCalllerTest.java @@ -0,0 +1,54 @@ +/* + * 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.textract.caller; + +import static io.camunda.connector.textract.util.TextractTestUtils.FULL_FILLED_ASYNC_TEXTRACT_DATA; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import com.amazonaws.services.textract.AmazonTextractAsyncClient; +import com.amazonaws.services.textract.model.GetDocumentAnalysisRequest; +import com.amazonaws.services.textract.model.GetDocumentAnalysisResult; +import com.amazonaws.services.textract.model.StartDocumentAnalysisResult; +import java.util.stream.Stream; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class PollingTextractCalllerTest { + + private static Stream provideStatuses() { + return Stream.of( + Arguments.of("IN_PROGRESS", "SUCCEEDED"), + Arguments.of("IN_PROGRESS", "FAILED"), + Arguments.of("IN_PROGRESS", "PARTIAL_SUCCESS")); + } + + @ParameterizedTest + @MethodSource("provideStatuses") + void callUntilSucceedOrFailedResult(String firstCallStatus, String secondCallStatus) + throws Exception { + AmazonTextractAsyncClient asyncClient = Mockito.mock(AmazonTextractAsyncClient.class); + StartDocumentAnalysisResult startDocRequest = new StartDocumentAnalysisResult(); + when(asyncClient.startDocumentAnalysis(any())).thenReturn(startDocRequest); + + GetDocumentAnalysisResult mockResult1 = + new GetDocumentAnalysisResult().withJobStatus(firstCallStatus); + GetDocumentAnalysisResult mockResult2 = + new GetDocumentAnalysisResult().withJobStatus(secondCallStatus); + when(asyncClient.getDocumentAnalysis(any(GetDocumentAnalysisRequest.class))) + .thenReturn(mockResult1, mockResult2); + + new PollingTextractCalller().call(FULL_FILLED_ASYNC_TEXTRACT_DATA, asyncClient); + + verify(asyncClient, times(2)).getDocumentAnalysis(any(GetDocumentAnalysisRequest.class)); + } +} diff --git a/connectors/aws/aws-textract/src/test/java/io/camunda/connector/textract/caller/SyncTextractCallerTest.java b/connectors/aws/aws-textract/src/test/java/io/camunda/connector/textract/caller/SyncTextractCallerTest.java new file mode 100644 index 0000000000..9e18ac88f2 --- /dev/null +++ b/connectors/aws/aws-textract/src/test/java/io/camunda/connector/textract/caller/SyncTextractCallerTest.java @@ -0,0 +1,51 @@ +/* + * 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.textract.caller; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.amazonaws.services.textract.AmazonTextractClient; +import com.amazonaws.services.textract.model.AnalyzeDocumentRequest; +import com.amazonaws.services.textract.model.AnalyzeDocumentResult; +import io.camunda.connector.textract.model.TextractExecutionType; +import io.camunda.connector.textract.model.TextractRequestData; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class SyncTextractCallerTest { + @Test + void call() { + TextractRequestData requestData = + new TextractRequestData( + TextractExecutionType.SYNC, + "test-bucket", + "test-object", + "1", + true, + true, + true, + true, + "token", + "client-request-token", + "job-tag", + "notification-channel", + "role-arn", + "outputBucket", + "prefix"); + + AmazonTextractClient textractClient = Mockito.mock(AmazonTextractClient.class); + + when(textractClient.analyzeDocument(any(AnalyzeDocumentRequest.class))) + .thenReturn(new AnalyzeDocumentResult()); + + new SyncTextractCaller().call(requestData, textractClient); + + verify(textractClient).analyzeDocument(any(AnalyzeDocumentRequest.class)); + } +} diff --git a/connectors/aws/aws-textract/src/test/java/io/camunda/connector/textract/caller/TextractCallerTest.java b/connectors/aws/aws-textract/src/test/java/io/camunda/connector/textract/caller/TextractCallerTest.java new file mode 100644 index 0000000000..80584c3cd8 --- /dev/null +++ b/connectors/aws/aws-textract/src/test/java/io/camunda/connector/textract/caller/TextractCallerTest.java @@ -0,0 +1,115 @@ +/* + * 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.textract.caller; + +import static io.camunda.connector.textract.caller.TextractCaller.WRONG_ANALYZE_TYPE_MSG; +import static io.camunda.connector.textract.util.TextractTestUtils.FULL_FILLED_ASYNC_TEXTRACT_DATA; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.amazonaws.services.textract.model.AnalyzeDocumentResult; +import com.amazonaws.services.textract.model.DocumentLocation; +import com.amazonaws.services.textract.model.S3Object; +import io.camunda.connector.textract.model.TextractExecutionType; +import io.camunda.connector.textract.model.TextractRequestData; +import java.util.Set; +import org.junit.jupiter.api.Test; + +class TextractCallerTest { + + private final TextractCaller textractCaller = (data, client) -> null; + + @Test + void prepareS3Obj() { + S3Object s3Object = textractCaller.prepareS3Obj(FULL_FILLED_ASYNC_TEXTRACT_DATA); + + assertThat(s3Object.getBucket()).isEqualTo(FULL_FILLED_ASYNC_TEXTRACT_DATA.documentS3Bucket()); + assertThat(s3Object.getName()).isEqualTo(FULL_FILLED_ASYNC_TEXTRACT_DATA.documentName()); + assertThat(s3Object.getVersion()).isEqualTo(FULL_FILLED_ASYNC_TEXTRACT_DATA.documentVersion()); + } + + @Test + void prepareFeatureTypesAllEnabled() { + TextractRequestData requestData1 = + new TextractRequestData( + TextractExecutionType.SYNC, + "test-bucket", + "test-object", + "1", + true, + true, + true, + true, + "token", + "client-request-token", + "job-tag", + "notification-channel", + "role-arn", + "outputBucket", + "prefix"); + Set featureTypes = textractCaller.prepareFeatureTypes(requestData1); + assertThat(featureTypes).containsExactlyInAnyOrder("FORMS", "LAYOUT", "SIGNATURES", "TABLES"); + } + + @Test + void prepareFeatureTypesNoFeaturesEnabled() { + TextractRequestData requestData = + new TextractRequestData( + TextractExecutionType.SYNC, + "test-bucket", + "test-object", + "1", + false, + false, + false, + false, + "token", + "client-request-token", + "job-tag", + "notification-channel", + "role-arn", + "outputBucket", + "prefix"); + + Exception exception = + assertThrows( + IllegalArgumentException.class, () -> textractCaller.prepareFeatureTypes(requestData)); + + assertThat(exception.getMessage()).isEqualTo(WRONG_ANALYZE_TYPE_MSG); + } + + @Test + void prepareFeatureTypesOnlyTablesAndLayout() { + TextractRequestData requestData = + new TextractRequestData( + TextractExecutionType.SYNC, + "test-bucket", + "test-object", + "1", + true, + false, + false, + true, + "token", + "client-request-token", + "job-tag", + "notification-channel", + "role-arn", + "outputBucket", + "prefix"); + Set featureTypes = textractCaller.prepareFeatureTypes(requestData); + assertThat(featureTypes).containsExactlyInAnyOrder("TABLES", "LAYOUT"); + } + + @Test + void prepareDocumentLocation() { + DocumentLocation documentLocation = + textractCaller.prepareDocumentLocation(FULL_FILLED_ASYNC_TEXTRACT_DATA); + + assertThat(documentLocation.getS3Object()).isNotNull(); + } +} diff --git a/connectors/aws/aws-textract/src/test/java/io/camunda/connector/textract/suppliers/AmazonTextractClientSupplierTest.java b/connectors/aws/aws-textract/src/test/java/io/camunda/connector/textract/suppliers/AmazonTextractClientSupplierTest.java new file mode 100644 index 0000000000..552bfa5ace --- /dev/null +++ b/connectors/aws/aws-textract/src/test/java/io/camunda/connector/textract/suppliers/AmazonTextractClientSupplierTest.java @@ -0,0 +1,47 @@ +/* + * 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.textract.suppliers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.amazonaws.services.textract.AmazonTextractAsync; +import com.amazonaws.services.textract.AmazonTextractAsyncClient; +import com.amazonaws.services.textract.AmazonTextractClient; +import io.camunda.connector.aws.model.impl.AwsBaseConfiguration; +import io.camunda.connector.textract.model.TextractRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class AmazonTextractClientSupplierTest { + + private TextractRequest request; + private AmazonTextractClientSupplier clientSupplier; + + @BeforeEach + public void setUp() { + clientSupplier = new AmazonTextractClientSupplier(); + AwsBaseConfiguration configuration = new AwsBaseConfiguration("region", ""); + + request = new TextractRequest(); + request.setConfiguration(configuration); + } + + @Test + void getSyncTextractClient() { + AmazonTextractClient client = + (AmazonTextractClient) clientSupplier.getSyncTextractClient(request); + assertThat(client).isInstanceOf(AmazonTextractClient.class); + } + + @Test + void getAsyncTextractClient() { + AmazonTextractAsync client = clientSupplier.getAsyncTextractClient(request); + assertNotNull(client); + assertThat(client).isInstanceOf(AmazonTextractAsyncClient.class); + } +} diff --git a/connectors/aws/aws-textract/src/test/java/io/camunda/connector/textract/util/TextractTestUtils.java b/connectors/aws/aws-textract/src/test/java/io/camunda/connector/textract/util/TextractTestUtils.java new file mode 100644 index 0000000000..26acf894ea --- /dev/null +++ b/connectors/aws/aws-textract/src/test/java/io/camunda/connector/textract/util/TextractTestUtils.java @@ -0,0 +1,211 @@ +/* + * 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.textract.util; + +import io.camunda.connector.textract.model.TextractExecutionType; +import io.camunda.connector.textract.model.TextractRequestData; + +public class TextractTestUtils { + + public static final String ACTUAL_ACCESS_KEY = "DDDCCCBBBBAAAA"; + public static final String ACTUAL_SECRET_KEY = "AAAABBBBCCCDDD"; + public static final String ASYNC_EXECUTION_JSON = + """ + { + "input": { + "accept": "application/json", + "executionType": "ASYNC", + "documentS3Bucket": "bucket", + "documentName": "file.png", + "documentVersion": "2", + "analyzeTables": true, + "analyzeForms": true, + "analyzeSignatures": true, + "analyzeLayout": true, + "clientRequestToken": "token", + "jobTag": "jobId", + "kmsKeyId": "keyId", + "notificationChannelRoleArn": "roleArn", + "notificationChannelSnsTopicArn": "topicArn", + "outputConfigS3Bucket": "bucket", + "outputConfigS3Prefix": "outputPrefix" + }, + "configuration": { + "region": "eu-central-1" + }, + "authentication": { + "type": "defaultCredentialsChain", + "accessKey": "{{secrets.ACCESS_KEY}}", + "secretKey": "{{secrets.SECRET_KEY}}" + } + } + """; + + public static final String ASYNC_EXECUTION_JSON_WITHOUT_S3_BUCKET_OUTPUT = + """ + { + "input": { + "accept": "application/json", + "executionType": "ASYNC", + "documentS3Bucket": "bucket", + "documentName": "file.png", + "documentVersion": "2", + "analyzeTables": true, + "analyzeForms": true, + "analyzeSignatures": true, + "analyzeLayout": true, + "clientRequestToken": "token", + "jobTag": "jobId", + "kmsKeyId": "keyId", + "notificationChannelRoleArn": "roleArn", + "notificationChannelSnsTopicArn": "topicArn", + "outputConfigS3Bucket": "", + "outputConfigS3Prefix": "outputPrefix" + }, + "configuration": { + "region": "eu-central-1" + }, + "authentication": { + "type": "defaultCredentialsChain", + "accessKey": "{{secrets.ACCESS_KEY}}", + "secretKey": "{{secrets.SECRET_KEY}}" + } + } + """; + public static final String ASYNC_EXECUTION_JSON_WITH_ROLE_ARN_AND_WITHOUT_SNS_TOPIC = + """ + { + "input": { + "accept": "application/json", + "executionType": "ASYNC", + "documentS3Bucket": "bucket", + "documentName": "file.png", + "documentVersion": "2", + "analyzeTables": true, + "analyzeForms": true, + "analyzeSignatures": true, + "analyzeLayout": true, + "clientRequestToken": "token", + "jobTag": "jobId", + "kmsKeyId": "keyId", + "notificationChannelRoleArn": "roleArn", + "notificationChannelSnsTopicArn": "", + "outputConfigS3Bucket": "bucket", + "outputConfigS3Prefix": "outputPrefix" + }, + "configuration": { + "region": "eu-central-1" + }, + "authentication": { + "type": "defaultCredentialsChain", + "accessKey": "{{secrets.ACCESS_KEY}}", + "secretKey": "{{secrets.SECRET_KEY}}" + } + } + """; + + public static final String ASYNC_EXECUTION_JSON_WITH_SNS_TOPIC_AND_WITHOUT_ROLE_ARN = + """ + { + "input": { + "accept": "application/json", + "executionType": "ASYNC", + "documentS3Bucket": "bucket", + "documentName": "file.png", + "documentVersion": "2", + "analyzeTables": true, + "analyzeForms": true, + "analyzeSignatures": true, + "analyzeLayout": true, + "clientRequestToken": "token", + "jobTag": "jobId", + "kmsKeyId": "keyId", + "notificationChannelRoleArn": "", + "notificationChannelSnsTopicArn": "snsTopic", + "outputConfigS3Bucket": "bucket", + "outputConfigS3Prefix": "outputPrefix" + }, + "configuration": { + "region": "eu-central-1" + }, + "authentication": { + "type": "defaultCredentialsChain", + "accessKey": "{{secrets.ACCESS_KEY}}", + "secretKey": "{{secrets.SECRET_KEY}}" + } + } + """; + public static final String SYNC_EXECUTION_JSON = + """ + { + "input": { + "accept": "application/json", + "executionType": "SYNC", + "documentS3Bucket": "bucket", + "documentName": "file.png", + "documentVersion": "2", + "analyzeTables": true, + "analyzeForms": true, + "analyzeSignatures": true, + "analyzeLayout": true + + }, + "configuration": { + "region": "eu-central-1" + }, + "authentication": { + "type": "defaultCredentialsChain", + "accessKey": "{{secrets.ACCESS_KEY}}", + "secretKey": "{{secrets.SECRET_KEY}}" + } + } + """; + + public static final String POLLING_EXECUTION_JSON = + """ + { + "input": { + "accept": "application/json", + "executionType": "POLLING", + "documentS3Bucket": "bucket", + "documentName": "file.png", + "documentVersion": "2", + "analyzeTables": true, + "analyzeForms": true, + "analyzeSignatures": true, + "analyzeLayout": true + + }, + "configuration": { + "region": "eu-central-1" + }, + "authentication": { + "type": "defaultCredentialsChain", + "accessKey": "{{secrets.ACCESS_KEY}}", + "secretKey": "{{secrets.SECRET_KEY}}" + } + } + """; + + public static final TextractRequestData FULL_FILLED_ASYNC_TEXTRACT_DATA = + new TextractRequestData( + TextractExecutionType.ASYNC, + "test-bucket", + "test-object", + "1", + true, + true, + true, + true, + "token", + "jobTag", + "kmsId", + "notification-channel", + "sns-arn", + "outputBucket", + "prefix"); +} diff --git a/connectors/aws/pom.xml b/connectors/aws/pom.xml index 461acc64d6..ad684345de 100644 --- a/connectors/aws/pom.xml +++ b/connectors/aws/pom.xml @@ -25,6 +25,7 @@ aws-sqs aws-sagemaker aws-bedrock + aws-textract