diff --git a/java/.gitignore b/java/.gitignore
new file mode 100644
index 0000000..1db21f1
--- /dev/null
+++ b/java/.gitignore
@@ -0,0 +1,13 @@
+.classpath.txt
+target
+.classpath
+.project
+.idea
+.settings
+.vscode
+*.iml
+
+# CDK asset staging directory
+.cdk.staging
+cdk.out
+
diff --git a/java/infrastructure/cdk.json b/java/infrastructure/cdk.json
new file mode 100644
index 0000000..aaf72f1
--- /dev/null
+++ b/java/infrastructure/cdk.json
@@ -0,0 +1,34 @@
+{
+ "app": "mvn -e -q compile exec:java",
+ "watch": {
+ "include": [
+ "**"
+ ],
+ "exclude": [
+ "README.md",
+ "cdk*.json",
+ "target",
+ "pom.xml"
+ ]
+ },
+ "context": {
+ "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true,
+ "@aws-cdk/core:stackRelativeExports": true,
+ "@aws-cdk/aws-rds:lowercaseDbIdentifier": true,
+ "@aws-cdk/aws-lambda:recognizeVersionProps": true,
+ "@aws-cdk/aws-lambda:recognizeLayerVersion": true,
+ "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true,
+ "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
+ "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
+ "@aws-cdk/core:checkSecretUsage": true,
+ "@aws-cdk/aws-iam:minimizePolicies": true,
+ "@aws-cdk/core:validateSnapshotRemovalPolicy": true,
+ "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
+ "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
+ "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
+ "@aws-cdk/core:target-partitions": [
+ "aws",
+ "aws-cn"
+ ]
+ }
+}
diff --git a/java/infrastructure/pom.xml b/java/infrastructure/pom.xml
new file mode 100644
index 0000000..16ad383
--- /dev/null
+++ b/java/infrastructure/pom.xml
@@ -0,0 +1,59 @@
+
+
+ 4.0.0
+
+ com.cwworkshop
+ cwworkshop
+ 0.1
+
+
+ 11
+ 11
+ UTF-8
+ 2.34.2
+ [10.0.0,11.0.0)
+ 5.7.1
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.8.1
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 3.0.0
+
+ com.cwworkshop.WorkshopApp
+
+
+
+
+
+
+
+
+ software.amazon.awscdk
+ aws-cdk-lib
+ ${cdk.version}
+
+
+
+ software.constructs
+ constructs
+ ${constructs.version}
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ ${junit.version}
+ test
+
+
+
diff --git a/java/infrastructure/src/main/java/com/cwworkshop/LambdaBundling.java b/java/infrastructure/src/main/java/com/cwworkshop/LambdaBundling.java
new file mode 100644
index 0000000..4093d31
--- /dev/null
+++ b/java/infrastructure/src/main/java/com/cwworkshop/LambdaBundling.java
@@ -0,0 +1,38 @@
+package com.cwworkshop;
+
+import java.util.Arrays;
+import java.util.List;
+
+import software.amazon.awscdk.BundlingOptions;
+import software.amazon.awscdk.DockerVolume;
+import software.amazon.awscdk.services.lambda.Runtime;
+
+import static java.util.Collections.singletonList;
+import static software.amazon.awscdk.BundlingOutput.ARCHIVED;
+
+public class LambdaBundling {
+ public static BundlingOptions get(String packageName) {
+ String cmd = "mvn clean install " +
+ "&& cp /asset-input/target/%s.jar /asset-output/";
+
+ List packagingInstructions = Arrays.asList(
+ "/bin/sh",
+ "-c",
+ String.format(cmd, packageName));
+
+ BundlingOptions builderOptions = BundlingOptions.builder()
+ .command(packagingInstructions)
+ .image(Runtime.JAVA_11.getBundlingImage())
+ .volumes(singletonList(
+ // Mount local .m2 repo to avoid download all the dependencies again inside the container
+ DockerVolume.builder()
+ .hostPath(System.getProperty("user.home") + "/.m2/")
+ .containerPath("/root/.m2/")
+ .build()))
+ .user("root")
+ .outputType(ARCHIVED)
+ .build();
+
+ return builderOptions;
+ }
+}
diff --git a/java/infrastructure/src/main/java/com/cwworkshop/WorkshopApp.java b/java/infrastructure/src/main/java/com/cwworkshop/WorkshopApp.java
new file mode 100644
index 0000000..e0559dd
--- /dev/null
+++ b/java/infrastructure/src/main/java/com/cwworkshop/WorkshopApp.java
@@ -0,0 +1,34 @@
+package com.cwworkshop;
+
+import software.amazon.awscdk.App;
+import software.amazon.awscdk.Environment;
+import software.amazon.awscdk.StackProps;
+
+import com.cwworkshop.api.APIStack;
+import com.cwworkshop.integration.IntegrationStack;
+import com.cwworkshop.recognition.RecognitionStack;
+import com.cwworkshop.recognition.RecognitionStackProps;
+
+public class WorkshopApp {
+ private static final String DEFAULT_REGION = "us-east-2";
+
+ public static void main(final String[] args) {
+ App app = new App();
+
+ Environment defaultEnv = Environment.builder().region(DEFAULT_REGION).build();
+ StackProps defaultProps = StackProps.builder().env(defaultEnv).build();
+
+ APIStack apiStack = new APIStack(app, "APIStack", defaultProps);
+ IntegrationStack integrationStack = new IntegrationStack(app, "IntegrationStack", defaultProps);
+
+ RecognitionStackProps recognitionStackProps = new RecognitionStackProps()
+ .sqsArn(apiStack.getUploadQueueArn())
+ .sqsUrl(apiStack.getUploadQueueUrl())
+ .snsArn(integrationStack.getSnsArn())
+ .withEnv(defaultEnv);
+
+ new RecognitionStack(app, "RekognitionStack", recognitionStackProps);
+
+ app.synth();
+ }
+}
diff --git a/java/infrastructure/src/main/java/com/cwworkshop/api/APIStack.java b/java/infrastructure/src/main/java/com/cwworkshop/api/APIStack.java
new file mode 100644
index 0000000..7a89f3f
--- /dev/null
+++ b/java/infrastructure/src/main/java/com/cwworkshop/api/APIStack.java
@@ -0,0 +1,95 @@
+package com.cwworkshop.api;
+
+import software.constructs.Construct;
+
+import java.util.Map;
+
+import com.cwworkshop.LambdaBundling;
+
+import software.amazon.awscdk.Duration;
+import software.amazon.awscdk.RemovalPolicy;
+import software.amazon.awscdk.Stack;
+import software.amazon.awscdk.StackProps;
+import software.amazon.awscdk.services.s3.Bucket;
+import software.amazon.awscdk.services.s3.EventType;
+import software.amazon.awscdk.services.s3.assets.AssetOptions;
+import software.amazon.awscdk.services.s3.notifications.SnsDestination;
+import software.amazon.awscdk.services.sns.Topic;
+import software.amazon.awscdk.services.sns.subscriptions.SqsSubscription;
+import software.amazon.awscdk.services.sqs.Queue;
+import software.amazon.awscdk.services.lambda.Function;
+import software.amazon.awscdk.services.lambda.Runtime;
+import software.amazon.awscdk.services.apigateway.LambdaIntegration;
+import software.amazon.awscdk.services.apigateway.RestApi;
+import software.amazon.awscdk.services.lambda.Code;
+
+public class APIStack extends Stack {
+
+ private String uploadQueueUrl;
+ private String uploadQueueArn;
+
+ public String getUploadQueueUrl() {
+ return this.uploadQueueUrl;
+ }
+
+ public String getUploadQueueArn() {
+ return this.uploadQueueArn;
+ }
+
+ public APIStack(final Construct scope, final String id) {
+ this(scope, id, null);
+ }
+
+ public APIStack(final Construct scope, final String id, final StackProps props) {
+ super(scope, id, props);
+
+ final Bucket bucket = Bucket.Builder.create(this, "CW-Workshop-Images")
+ .removalPolicy(RemovalPolicy.DESTROY)
+ .build();
+
+ final Function imageGetAndSaveLambda = Function.Builder.create(this, "ImageGetAndSaveLambda")
+ .functionName("ImageGetAndSaveLambda")
+ .runtime(Runtime.JAVA_11)
+ .memorySize(1024)
+ .timeout(Duration.seconds(60))
+ .code(Code.fromAsset(
+ "../software/api",
+ AssetOptions.builder().bundling(LambdaBundling.get("api")).build()))
+ .handler("api.Handler")
+ .environment(Map.of("BUCKET_NAME", bucket.getBucketName()))
+ .build();
+
+ bucket.grantReadWrite(imageGetAndSaveLambda);
+
+ final RestApi api = RestApi.Builder.create(this, "REST_API")
+ .restApiName("Image Upload Service")
+ .description("CW workshop - upload image for workshop.")
+ .build();
+
+ final LambdaIntegration getImageIntegration = LambdaIntegration.Builder.create(imageGetAndSaveLambda)
+ .requestTemplates(Map.of("application/json", "{ \"statusCode\": \"200\" }"))
+ .build();
+
+ api.getRoot().addMethod("GET", getImageIntegration);
+
+ final Queue uploadQueue = Queue.Builder.create(this, "uploaded_image_queue")
+ .visibilityTimeout(Duration.seconds(30))
+ .build();
+
+ this.uploadQueueUrl = uploadQueue.getQueueUrl();
+ this.uploadQueueArn = uploadQueue.getQueueArn();
+
+ final SqsSubscription sqsSubscription = SqsSubscription.Builder.create(uploadQueue)
+ .rawMessageDelivery(true)
+ .build();
+
+ final Topic uploadEventTopic = Topic.Builder.create(this, "uploaded_image_topic")
+ .build();
+
+ uploadEventTopic.addSubscription(sqsSubscription);
+
+ bucket.addEventNotification(
+ EventType.OBJECT_CREATED_PUT,
+ new SnsDestination(uploadEventTopic));
+ }
+}
diff --git a/java/infrastructure/src/main/java/com/cwworkshop/integration/IntegrationStack.java b/java/infrastructure/src/main/java/com/cwworkshop/integration/IntegrationStack.java
new file mode 100644
index 0000000..813da88
--- /dev/null
+++ b/java/infrastructure/src/main/java/com/cwworkshop/integration/IntegrationStack.java
@@ -0,0 +1,60 @@
+package com.cwworkshop.integration;
+
+import com.cwworkshop.LambdaBundling;
+
+import software.constructs.Construct;
+
+import software.amazon.awscdk.Duration;
+import software.amazon.awscdk.Stack;
+import software.amazon.awscdk.StackProps;
+import software.amazon.awscdk.services.sns.Topic;
+import software.amazon.awscdk.services.sns.subscriptions.SqsSubscription;
+import software.amazon.awscdk.services.sqs.Queue;
+import software.amazon.awscdk.services.lambda.Function;
+import software.amazon.awscdk.services.lambda.Runtime;
+import software.amazon.awscdk.services.lambda.eventsources.SqsEventSource;
+import software.amazon.awscdk.services.s3.assets.AssetOptions;
+import software.amazon.awscdk.services.lambda.Code;
+
+public class IntegrationStack extends Stack {
+ private String rekognizedEventTopicArn;
+
+ public String getSnsArn() {
+ return this.rekognizedEventTopicArn;
+ }
+
+ public IntegrationStack(final Construct scope, final String id) {
+ this(scope, id, null);
+ }
+
+ public IntegrationStack(final Construct scope, final String id, final StackProps props) {
+ super(scope, id, props);
+
+ final Queue rekognizedQueue = Queue.Builder.create(this, "rekognized_image_queue")
+ .visibilityTimeout(Duration.seconds(30))
+ .build();
+
+ final SqsSubscription sqsSubscription = SqsSubscription.Builder.create(rekognizedQueue)
+ .rawMessageDelivery(true)
+ .build();
+
+ final Topic rekognizedEventTopic = Topic.Builder.create(this, "rekognized_image_topic")
+ .build();
+
+ this.rekognizedEventTopicArn = rekognizedEventTopic.getTopicArn();
+ rekognizedEventTopic.addSubscription(sqsSubscription);
+
+ final Function integrationLambda = Function.Builder.create(this, "IntegrationLambda")
+ .runtime(Runtime.JAVA_11)
+ .timeout(Duration.seconds(60))
+ .memorySize(1024)
+ .handler("integration.Handler")
+ .code(Code.fromAsset(
+ "../software/integration",
+ AssetOptions.builder().bundling(LambdaBundling.get("integration")).build()))
+ .build();
+
+ final SqsEventSource invokeEventSource = SqsEventSource.Builder.create(rekognizedQueue).build();
+ integrationLambda.addEventSource(invokeEventSource);
+ }
+}
diff --git a/java/infrastructure/src/main/java/com/cwworkshop/recognition/RecognitionStack.java b/java/infrastructure/src/main/java/com/cwworkshop/recognition/RecognitionStack.java
new file mode 100644
index 0000000..9636e02
--- /dev/null
+++ b/java/infrastructure/src/main/java/com/cwworkshop/recognition/RecognitionStack.java
@@ -0,0 +1,128 @@
+package com.cwworkshop.recognition;
+
+import software.constructs.Construct;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+
+import com.cwworkshop.LambdaBundling;
+
+import software.amazon.awscdk.Duration;
+import software.amazon.awscdk.RemovalPolicy;
+import software.amazon.awscdk.Stack;
+import software.amazon.awscdk.services.lambda.Function;
+import software.amazon.awscdk.services.lambda.Runtime;
+import software.amazon.awscdk.services.s3.assets.AssetOptions;
+import software.amazon.awscdk.services.apigateway.LambdaIntegration;
+import software.amazon.awscdk.services.apigateway.RestApi;
+import software.amazon.awscdk.services.dynamodb.Attribute;
+import software.amazon.awscdk.services.dynamodb.AttributeType;
+import software.amazon.awscdk.services.dynamodb.Table;
+import software.amazon.awscdk.services.iam.Group;
+import software.amazon.awscdk.services.iam.PolicyStatement;
+import software.amazon.awscdk.services.iam.User;
+import software.amazon.awscdk.services.lambda.Code;
+import software.amazon.awscdk.services.lambda.EventSourceMappingOptions;
+
+public class RecognitionStack extends Stack {
+ public RecognitionStack(final Construct scope, final String id) {
+ this(scope, id, null);
+ }
+
+ public RecognitionStack(final Construct scope, final String id, final RecognitionStackProps props) {
+ super(scope, id, props);
+
+ // create new IAM group and user
+ final Group group = Group.Builder.create(this, "RekGroup").build();
+ final User user = User.Builder.create(this, "RekUser").build();
+
+ // add IAM user to the new group
+ user.addToGroup(group);
+
+ // create DynamoDB table to hold Rekognition results
+ final Table table = Table.Builder.create(this, "Classifications")
+ .partitionKey(Attribute.builder().name("image").type(AttributeType.STRING).build())
+ .removalPolicy(RemovalPolicy.DESTROY)
+ .build();
+
+ AssetOptions packageAssetOptions = AssetOptions.builder().bundling(LambdaBundling.get("recognition")).build();
+
+ // create Lambda function
+ final Function lambda_function = Function.Builder.create(this, "image_recognition")
+ .runtime(Runtime.JAVA_11)
+ .timeout(Duration.seconds(60))
+ .memorySize(1024)
+ .handler("recognition.RecognitionHandler")
+ .code(Code.fromAsset("../software/recognition", packageAssetOptions))
+ .environment(Map.of(
+ "TABLE_NAME", table.getTableName(),
+ "SQS_QUEUE_URL", props.getSqsUrl(),
+ "TOPIC_ARN", props.getSnsArn()))
+ .build();
+
+ lambda_function.addEventSourceMapping("ImgRekognitionLambda",
+ EventSourceMappingOptions.builder().eventSourceArn(props.getSqsArn()).build());
+
+ // add Rekognition permissions for Lambda function
+ final PolicyStatement rekognition_statement = PolicyStatement.Builder.create()
+ .actions(Collections.singletonList("rekognition:DetectLabels"))
+ .resources(Collections.singletonList("*"))
+ .build();
+ lambda_function.addToRolePolicy(rekognition_statement);
+
+ // add SNS permissions for Lambda function
+ final PolicyStatement sns_permission = PolicyStatement.Builder.create()
+ .actions(Collections.singletonList("sns:publish"))
+ .resources(Collections.singletonList("*"))
+ .build();
+ lambda_function.addToRolePolicy(sns_permission);
+
+ // grant permission for lambda to receive/delete message from SQS
+ final PolicyStatement sqs_permission = PolicyStatement.Builder.create()
+ .actions(Arrays.asList(
+ "sqs:ChangeMessageVisibility",
+ "sqs:DeleteMessage",
+ "sqs:GetQueueAttributes",
+ "sqs:GetQueueUrl",
+ "sqs:ReceiveMessage"))
+ .resources(Collections.singletonList("*"))
+ .build();
+
+ lambda_function.addToRolePolicy(sqs_permission);
+
+ // grant permissions for lambda to read/write to DynamoDB table
+ table.grantReadWriteData(lambda_function);
+
+ // grant permissions for lambda to read from bucket
+ final PolicyStatement s3_permission = PolicyStatement.Builder.create()
+ .actions(Collections.singletonList("s3:get*"))
+ .resources(Collections.singletonList("*"))
+ .build();
+ lambda_function.addToRolePolicy(s3_permission);
+
+ // add additional API Gateway and lambda to list ddb
+ final Function list_img_lambda = Function.Builder.create(this, "ListImagesLambda")
+ .functionName("ListImagesLambda")
+ .runtime(Runtime.JAVA_11)
+ .timeout(Duration.seconds(60))
+ .memorySize(1024)
+ .code(Code.fromAsset("../software/recognition", packageAssetOptions))
+ .handler("recognition.ListItemsHandler")
+ .environment(Map.of("TABLE_NAME", table.getTableName()))
+ .build();
+
+ final RestApi api = RestApi.Builder.create(this, "REST_API")
+ .restApiName("List Images Service")
+ .description("CW workshop - list images recognized from workshop.")
+ .build();
+
+ final LambdaIntegration list_images = LambdaIntegration.Builder.create(list_img_lambda)
+ .requestTemplates(Map.of("application/json", "{ \"statusCode\": \"200\" }"))
+ .build();
+
+ api.getRoot().addMethod("GET", list_images);
+
+ table.grantReadData(list_img_lambda);
+ }
+}
diff --git a/java/infrastructure/src/main/java/com/cwworkshop/recognition/RecognitionStackProps.java b/java/infrastructure/src/main/java/com/cwworkshop/recognition/RecognitionStackProps.java
new file mode 100644
index 0000000..07fc84e
--- /dev/null
+++ b/java/infrastructure/src/main/java/com/cwworkshop/recognition/RecognitionStackProps.java
@@ -0,0 +1,51 @@
+package com.cwworkshop.recognition;
+
+import org.jetbrains.annotations.Nullable;
+
+import software.amazon.awscdk.Environment;
+import software.amazon.awscdk.StackProps;
+
+public class RecognitionStackProps implements StackProps {
+
+ private String sqsUrl;
+ private String sqsArn;
+ private String snsArn;
+ private Environment env;
+
+ @Override
+ public @Nullable Environment getEnv() {
+ return this.env;
+ }
+
+ public String getSqsUrl() {
+ return this.sqsUrl;
+ }
+
+ public String getSqsArn() {
+ return this.sqsArn;
+ }
+
+ public String getSnsArn() {
+ return this.snsArn;
+ }
+
+ public RecognitionStackProps sqsUrl(String sqsUrl) {
+ this.sqsUrl = sqsUrl;
+ return this;
+ }
+
+ public RecognitionStackProps sqsArn(String sqsArn) {
+ this.sqsArn = sqsArn;
+ return this;
+ }
+
+ public RecognitionStackProps snsArn(String snsArn) {
+ this.snsArn = snsArn;
+ return this;
+ }
+
+ public RecognitionStackProps withEnv(Environment env) {
+ this.env = env;
+ return this;
+ }
+}
diff --git a/java/software/api/pom.xml b/java/software/api/pom.xml
new file mode 100644
index 0000000..eb11c72
--- /dev/null
+++ b/java/software/api/pom.xml
@@ -0,0 +1,55 @@
+
+ 4.0.0
+ api
+ api
+ 1.0
+ jar
+ API
+
+ 11
+ 11
+ 2.18.0
+ UTF-8
+
+
+
+
+ com.amazonaws
+ aws-java-sdk-s3
+ 1.12.272
+
+
+
+ com.amazonaws
+ aws-lambda-java-core
+ 1.2.1
+
+
+ com.amazonaws
+ aws-lambda-java-events
+ 3.11.0
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.3.0
+
+ false
+ api
+
+
+
+ package
+
+ shade
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/java/software/api/src/main/java/api/FileDownloader.java b/java/software/api/src/main/java/api/FileDownloader.java
new file mode 100644
index 0000000..cb75808
--- /dev/null
+++ b/java/software/api/src/main/java/api/FileDownloader.java
@@ -0,0 +1,52 @@
+package api;
+
+import java.net.URL;
+import java.net.URLConnection;
+
+import com.amazonaws.services.s3.AmazonS3;
+import com.amazonaws.services.s3.AmazonS3ClientBuilder;
+
+public class FileDownloader {
+
+ // 1.) Function to download a file from URL
+ public static void downloadFile(String fileURL, String fileName) {
+ try {
+ // 2.) Open a URL connection
+ URLConnection urlConnection = new URL(fileURL).openConnection();
+
+ // 3.) Specify a file name and directory to save the file
+ urlConnection.setRequestProperty("Content-Disposition", "attachment; filename=" + fileName);
+
+ // 4.) Get the input stream of the connection
+ java.io.InputStream inputStream = urlConnection.getInputStream();
+
+ // 5.) Create a new file and write the contents
+ java.io.File file = new java.io.File(fileName);
+ java.io.OutputStream outputStream = new java.io.FileOutputStream(file);
+
+ int bytesRead;
+ byte[] buffer = new byte[8192];
+ while ((bytesRead = inputStream.read(buffer)) != -1) {
+ outputStream.write(buffer, 0, bytesRead);
+ }
+
+ // 6.) Close the output stream
+ outputStream.close();
+
+ // 7.) Close the input stream
+ inputStream.close();
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ // 2.) Function to upload image to S3 bucket
+ public static void uploadFile(String fileName, String bucketName) {
+ // 1.) Build a client
+ AmazonS3 s3 = AmazonS3ClientBuilder.defaultClient();
+
+ // 2.) Upload a file to S3
+ s3.putObject(bucketName, fileName, new java.io.File(fileName));
+ }
+}
diff --git a/java/software/api/src/main/java/api/Handler.java b/java/software/api/src/main/java/api/Handler.java
new file mode 100644
index 0000000..360d238
--- /dev/null
+++ b/java/software/api/src/main/java/api/Handler.java
@@ -0,0 +1,23 @@
+package api;
+
+import com.amazonaws.services.lambda.runtime.Context;
+import com.amazonaws.services.lambda.runtime.RequestHandler;
+import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
+import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
+
+public class Handler implements RequestHandler {
+ private static final String S3_BUCKET = System.getenv("BUCKET_NAME");
+
+ @Override
+ public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent event, Context context) {
+
+ final String url = event.getQueryStringParameters().get("url");
+ final String name = "/tmp/" + event.getQueryStringParameters().get("name");
+
+ // pass the output of method #1 as input to method #2
+ FileDownloader.downloadFile(url, name);
+ FileDownloader.uploadFile(name, S3_BUCKET);
+
+ return new APIGatewayProxyResponseEvent().withStatusCode(200).withBody("Successfully Uploaded Img!");
+ }
+}
diff --git a/java/software/integration/pom.xml b/java/software/integration/pom.xml
new file mode 100644
index 0000000..93e7db2
--- /dev/null
+++ b/java/software/integration/pom.xml
@@ -0,0 +1,59 @@
+
+ 4.0.0
+ integration
+ integration
+ 1.0
+ jar
+ Integration
+
+ 11
+ 11
+ 2.18.0
+ UTF-8
+
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ 2.13.3
+
+
+ com.fasterxml.jackson.dataformat
+ jackson-dataformat-xml
+ 2.13.3
+
+
+ com.amazonaws
+ aws-lambda-java-core
+ 1.2.1
+
+
+ com.amazonaws
+ aws-lambda-java-events
+ 3.11.0
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.3.0
+
+ false
+ integration
+
+
+
+ package
+
+ shade
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/java/software/integration/src/main/java/integration/Handler.java b/java/software/integration/src/main/java/integration/Handler.java
new file mode 100644
index 0000000..a73a0d4
--- /dev/null
+++ b/java/software/integration/src/main/java/integration/Handler.java
@@ -0,0 +1,28 @@
+package integration;
+
+import com.amazonaws.services.lambda.runtime.Context;
+import com.amazonaws.services.lambda.runtime.RequestHandler;
+import com.amazonaws.services.lambda.runtime.events.SQSEvent;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public class Handler implements RequestHandler {
+
+ public String handleRequest(SQSEvent event, Context context) {
+
+ try {
+ final var body = new ObjectMapper().writeValueAsString(event);
+
+ // Call MailServerIntegration class with var "event" to convert json to xml
+ String xml = MailServerIntegration.jsonToXml(body);
+
+ // Call MailServerIntegration class to post xml
+ String response = MailServerIntegration.sendPost(xml);
+
+ return "200 OK";
+ } catch (Exception e) {
+ e.printStackTrace();
+ return "500";
+ }
+ }
+}
diff --git a/java/software/integration/src/main/java/integration/MailServerIntegration.java b/java/software/integration/src/main/java/integration/MailServerIntegration.java
new file mode 100644
index 0000000..0368dcb
--- /dev/null
+++ b/java/software/integration/src/main/java/integration/MailServerIntegration.java
@@ -0,0 +1,37 @@
+package integration;
+
+import java.net.URL;
+import java.net.URLConnection;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.xml.XmlMapper;
+
+public class MailServerIntegration {
+ // 1.) Convert JSON string to XML string
+ public static String jsonToXml(String json) {
+ try {
+ XmlMapper xmlMapper = new XmlMapper();
+ ObjectMapper objectMapper = new ObjectMapper();
+ return xmlMapper.writeValueAsString(objectMapper.readValue(json, Object.class));
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new RuntimeException(e);
+ }
+ }
+
+ // 2.) Send XML string with HTTP POST
+ public static String sendPost(String xml) {
+ try {
+ URLConnection connection = new URL("https://www.example.com/sendmail").openConnection();
+ connection.setDoOutput(true);
+ connection.setRequestProperty("Content-Type", "application/xml");
+ connection.setRequestProperty("Accept", "application/xml");
+ connection.setRequestProperty("Content-Length", String.valueOf(xml.length()));
+ connection.getOutputStream().write(xml.getBytes());
+ return connection.getInputStream().toString();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+}
diff --git a/java/software/recognition/pom.xml b/java/software/recognition/pom.xml
new file mode 100644
index 0000000..4b936b7
--- /dev/null
+++ b/java/software/recognition/pom.xml
@@ -0,0 +1,74 @@
+
+ 4.0.0
+ recognition
+ recognition
+ 1.0
+ jar
+ Recognition
+
+ 11
+ 11
+ 2.18.0
+ UTF-8
+
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ 2.13.3
+
+
+ software.amazon.awssdk
+ sqs
+ 2.17.247
+
+
+ software.amazon.awssdk
+ sns
+ 2.17.247
+
+
+ software.amazon.awssdk
+ dynamodb
+ 2.17.247
+
+
+ software.amazon.awssdk
+ rekognition
+ 2.17.247
+
+
+ com.amazonaws
+ aws-lambda-java-core
+ 1.2.1
+
+
+ com.amazonaws
+ aws-lambda-java-events
+ 3.11.0
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.3.0
+
+ false
+ recognition
+
+
+
+ package
+
+ shade
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/java/software/recognition/src/main/java/recognition/DynamoDbTableScanner.java b/java/software/recognition/src/main/java/recognition/DynamoDbTableScanner.java
new file mode 100644
index 0000000..3d3f804
--- /dev/null
+++ b/java/software/recognition/src/main/java/recognition/DynamoDbTableScanner.java
@@ -0,0 +1,22 @@
+package recognition;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
+
+public final class DynamoDbTableScanner {
+
+ // 1.) Function to list all items from a DynamoDB table
+ public static List> listItems(String tableName) {
+ // Create a DynamoDbClient object
+ DynamoDbClient ddb = DynamoDbClient.create();
+ // Get all items from the table
+ var response = ddb.scan(scanRequest -> scanRequest.tableName(tableName));
+ var items = response.items().stream()
+ .map(t -> t.values().stream().map(v -> v.s()).collect(Collectors.toList()))
+ .collect(Collectors.toList());
+ // Return all the items in the table
+ return items;
+ }
+}
diff --git a/java/software/recognition/src/main/java/recognition/ImageRecognizer.java b/java/software/recognition/src/main/java/recognition/ImageRecognizer.java
new file mode 100644
index 0000000..8aea0c6
--- /dev/null
+++ b/java/software/recognition/src/main/java/recognition/ImageRecognizer.java
@@ -0,0 +1,35 @@
+package recognition;
+
+import software.amazon.awssdk.services.rekognition.RekognitionClient;
+import software.amazon.awssdk.services.rekognition.model.DetectLabelsRequest;
+import software.amazon.awssdk.services.rekognition.model.DetectLabelsResponse;
+import software.amazon.awssdk.services.rekognition.model.Image;
+
+public class ImageRecognizer {
+
+ // 1.) Function to detect labels from image with Rekognition as "labels"
+ public static String[] detectLabels(String bucketName, String imageName) {
+ String[] labels = null;
+ try {
+ // Initialize Rekognition client
+ RekognitionClient rekClient = RekognitionClient.create();
+
+ // Set the image to be searched for faces
+ DetectLabelsRequest request = DetectLabelsRequest.builder()
+ .image(Image.builder()
+ .s3Object(builder -> builder.bucket(bucketName).name(imageName))
+ .build())
+ .minConfidence(70.0f)
+ .maxLabels(10)
+ .build();
+
+ // Call Rekognition to detect the labels
+ DetectLabelsResponse response = rekClient.detectLabels(request);
+ labels = response.labels().stream().map(label -> label.name()).toArray(String[]::new);
+
+ } catch (Exception e) {
+ System.out.println(e.getMessage());
+ }
+ return labels;
+ }
+}
diff --git a/java/software/recognition/src/main/java/recognition/LabelStorage.java b/java/software/recognition/src/main/java/recognition/LabelStorage.java
new file mode 100644
index 0000000..0d6aeaa
--- /dev/null
+++ b/java/software/recognition/src/main/java/recognition/LabelStorage.java
@@ -0,0 +1,68 @@
+package recognition;
+
+import java.util.Map;
+
+import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
+import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
+import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;
+import software.amazon.awssdk.services.dynamodb.model.PutItemResponse;
+
+import software.amazon.awssdk.services.sns.SnsClient;
+import software.amazon.awssdk.services.sns.model.PublishRequest;
+import software.amazon.awssdk.services.sns.model.PublishResponse;
+
+import software.amazon.awssdk.services.sqs.SqsClient;
+import software.amazon.awssdk.services.sqs.model.DeleteMessageRequest;
+
+public class LabelStorage {
+ // 2.) Save json item to to DynamoDB
+ public static void saveLabelsToDynamoDB(String tableName, Map dbItem) {
+ // Create Client
+ DynamoDbClient ddb = DynamoDbClient.create();
+ // Create PutItemRequest object
+ PutItemRequest request = PutItemRequest.builder()
+ .tableName(tableName)
+ .item(dbItem)
+ .build();
+ try {
+ // Put Item
+ PutItemResponse response = ddb.putItem(request);
+ } catch (Exception e) {
+ System.err.println("Error saving item in DynamoDB: " + e);
+ }
+ }
+
+ // 3.) Publish item to SNS
+ public static void publishToSNS(String topicArn, String msg) {
+ // Create Client
+ SnsClient snsClient = SnsClient.create();
+ // Create PublishRequest object
+ PublishRequest publishRequest = PublishRequest.builder()
+ .topicArn(topicArn)
+ .message(msg)
+ .build();
+ try {
+ // Publish Message
+ PublishResponse publishResponse = snsClient.publish(publishRequest);
+ } catch (Exception e) {
+ System.err.println("Error publishing message to SNS: " + e);
+ }
+ }
+
+ // 4.) Delete message from SQS
+ public static void deleteMessage(String queueUrl, String msgHandle) {
+ // Create Client
+ SqsClient sqsClient = SqsClient.create();
+ // Create DeleteMessageRequest object
+ DeleteMessageRequest deleteMessageRequest = DeleteMessageRequest.builder()
+ .queueUrl(queueUrl)
+ .receiptHandle(msgHandle)
+ .build();
+ try {
+ // Delete Message
+ sqsClient.deleteMessage(deleteMessageRequest);
+ } catch (Exception e) {
+ System.err.println("Error deleting message from SQS: " + e);
+ }
+ }
+}
diff --git a/java/software/recognition/src/main/java/recognition/ListItemsHandler.java b/java/software/recognition/src/main/java/recognition/ListItemsHandler.java
new file mode 100644
index 0000000..940c985
--- /dev/null
+++ b/java/software/recognition/src/main/java/recognition/ListItemsHandler.java
@@ -0,0 +1,25 @@
+package recognition;
+
+import com.amazonaws.services.lambda.runtime.Context;
+import com.amazonaws.services.lambda.runtime.RequestHandler;
+import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
+import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public class ListItemsHandler implements RequestHandler
+{
+ private static final String tableName = System.getenv("TABLE_NAME");
+
+ @Override
+ public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent event, Context context) {
+ final var items = DynamoDbTableScanner.listItems(tableName);
+
+ try {
+ final String body = new ObjectMapper().writeValueAsString(items);
+ return new APIGatewayProxyResponseEvent().withBody(body);
+ } catch (Exception e) {
+ e.printStackTrace();
+ return new APIGatewayProxyResponseEvent().withStatusCode(500).withBody(e.getMessage());
+ }
+ }
+}
diff --git a/java/software/recognition/src/main/java/recognition/RecognitionHandler.java b/java/software/recognition/src/main/java/recognition/RecognitionHandler.java
new file mode 100644
index 0000000..08a4770
--- /dev/null
+++ b/java/software/recognition/src/main/java/recognition/RecognitionHandler.java
@@ -0,0 +1,60 @@
+package recognition;
+
+import java.util.HashMap;
+
+import com.amazonaws.services.lambda.runtime.Context;
+import com.amazonaws.services.lambda.runtime.RequestHandler;
+import com.amazonaws.services.lambda.runtime.events.SQSEvent;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
+
+public class RecognitionHandler implements RequestHandler {
+
+ private static final String queueUrl = System.getenv("SQS_QUEUE_URL");
+ private static final String tableName = System.getenv("TABLE_NAME");
+ private static final String topicArn = System.getenv("TOPIC_ARN");
+
+ public String handleRequest(SQSEvent event, Context context) {
+ try {
+ for (var eventRecord : event.getRecords()) {
+ final var receiptHandle = eventRecord.getReceiptHandle();
+
+ final ObjectMapper mapper = new ObjectMapper();
+ final JsonNode root = mapper.readTree(eventRecord.getBody());
+
+ for (final var record : root.get("Records") ) {
+ final var bucketName = record.get("s3").get("bucket").get("name").asText();
+ final var key = record.get("s3").get("object").get("key").asText();
+
+ // call method 1.) to generate image label and store as var "labels"
+ var labels = ImageRecognizer.detectLabels(bucketName, key);
+
+ // code snippet to create dynamodb item from labels
+ final var labelsString = new ObjectMapper().writeValueAsString(labels);
+ System.out.println("Detected labels: " + labelsString);
+
+ final var dbItem = new HashMap();
+ dbItem.put("image", AttributeValue.fromS(key));
+ dbItem.put("labels", AttributeValue.fromS(labelsString));
+
+ // call method 2.) to store "dbItem" result on DynamoDB
+ LabelStorage.saveLabelsToDynamoDB(tableName, dbItem);
+
+ // call method 3.) to send message to SNS
+ LabelStorage.publishToSNS(topicArn, labelsString);
+
+ // call method 4.) to delete img from SQS
+ LabelStorage.deleteMessage(queueUrl, receiptHandle);
+ }
+ }
+ } catch (Exception e) {
+ System.err.println("An error occurred while recognizing images");
+ e.printStackTrace();
+ return "500 Internal Server Error";
+ }
+
+ return "200 OK";
+ }
+}