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"; + } +}