├── .gitignore ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── .pre-commit-config.yaml ├── README.md ├── app_diagram.png ├── docker-compose.yml ├── mvnw ├── mvnw.cmd ├── package-lock.json ├── pom.xml ├── sample-pictures ├── cat.jpg ├── open-package.jpg ├── package.png ├── scale.jpg ├── television.jpg └── wrong-format-file.txt ├── shipment-picture-lambda-validator ├── pom.xml └── src │ └── main │ ├── java │ └── dev │ │ └── ancaghenade │ │ └── shipmentpicturelambdavalidator │ │ ├── Location.java │ │ ├── PropertiesProvider.java │ │ ├── S3ClientHelper.java │ │ ├── SNSClientHelper.java │ │ ├── ServiceHandler.java │ │ ├── TextParser.java │ │ └── Watermark.java │ └── resources │ ├── config.properties │ ├── lambda_update_script.sh │ └── placeholder.jpg ├── src ├── main │ ├── java │ │ └── dev │ │ │ └── ancaghenade │ │ │ └── shipmentlistdemo │ │ │ ├── ShipmentListDemoApplication.java │ │ │ ├── buckets │ │ │ └── BucketName.java │ │ │ ├── config │ │ │ ├── AWSClientConfig.java │ │ │ ├── AmazonS3Config.java │ │ │ ├── AmazonSQSConfig.java │ │ │ └── DynamoDBConfig.java │ │ │ ├── controller │ │ │ ├── MessageReceiver.java │ │ │ └── ShipmentController.java │ │ │ ├── entity │ │ │ ├── Address.java │ │ │ ├── Participant.java │ │ │ └── Shipment.java │ │ │ ├── repository │ │ │ ├── DynamoDBService.java │ │ │ └── S3StorageService.java │ │ │ ├── service │ │ │ └── ShipmentService.java │ │ │ └── util │ │ │ ├── FileUtil.java │ │ │ └── ResourceReader.java │ ├── resources │ │ ├── application-dev.yml │ │ ├── application-prod.yml │ │ ├── application.yml │ │ ├── buckets.properties │ │ └── placeholder.jpg │ └── shipment-list-frontend │ │ ├── README.md │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ │ └── src │ │ ├── App.css │ │ ├── App.js │ │ ├── App.test.js │ │ ├── SSEManager.js │ │ ├── index.css │ │ ├── index.js │ │ ├── placeholder.jpg │ │ ├── reportWebVitals.js │ │ └── setupTests.js └── test │ └── java │ └── dev │ └── ancaghenade │ └── shipmentlistdemo │ └── ShipmentListDemoApplicationTests.java └── terraform ├── cleanup.sh ├── data.json ├── locals.tf └── main.tf /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | 35 | ### Frontend ### 36 | 37 | # dependencies 38 | src/main/shipment-list-frontend/node_modules 39 | src/main/shipment-list-frontend/.pnp 40 | src/main/shipment-list-frontend/.pnp.js 41 | 42 | # testing 43 | src/main/shipment-list-frontend/coverage 44 | 45 | # production 46 | src/main/shipment-list-frontend/build 47 | 48 | # misc 49 | src/main/shipment-list-frontend/.DS_Store 50 | src/main/shipment-list-frontend/.env.local 51 | src/main/shipment-list-frontend/.env.development.local 52 | src/main/shipment-list-frontend/.env.test.local 53 | src/main/shipment-list-frontend/.env.production.local 54 | 55 | src/main/shipment-list-frontend/npm-debug.log* 56 | src/main/shipment-list-frontend/yarn-debug.log* 57 | src/main/shipment-list-frontend/yarn-error.log* 58 | 59 | # terraform 60 | 61 | setup/terraform/terraform.tfstate 62 | setup/terraform/.terraform.lock.hcl 63 | setup/terraform/terraform.tfstate.backup 64 | 65 | # lambda module 66 | /shipment-picture-lambda-validator/.idea 67 | /shipment-picture-lambda-validator/target/ 68 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinyg210/shipment-list-demo/984381c916dbde13053cc53935bcf98724240053/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar 3 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | - repo: https://github.com/pre-commit/pre-commit-hooks 2 | rev: v4.4.0 3 | hooks: 4 | - id: detect-aws-credentials -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Shipment List Demo Application - AWS in PROD and LocalStack on DEV environment 3 | 4 | 5 | | Environment | | 6 | |------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 7 | | __Services__ | Amazon S3, Lambda, DynamoDB, SNS, SQS | 8 | | __Integrations__ | AWS SDK, Terraform, AWS CLI | 9 | | __Categories__ | Spring Boot, S3 Trigger | 10 | | __Level__ | Intermediate | 11 | | __Works on__ | LocalStack v2 | 12 | 13 | 14 | ### UPDATE 15 | 16 | The Terraform configuration file now randomly generates names for the bucket, in order to avoid conflicts 17 | at a global scale on AWS. This name shall be written out to a properties file, which the app will pick up 18 | and use for the S3 client. Furthermore, the name is also passed as an environment variable to the Lambda function by Terraform, 19 | so there's no need to worry about managing it. 20 | 21 | 22 | ## Introduction 23 | 24 | This application was created for demonstration purposes to highlight the ease of switching from 25 | using actual AWS dependencies to having them emulated on LocalStack for your *developer environment* . 26 | Of course this comes with other advantages, but the first focus point is making the transition. 27 | 28 | ## Architecture Overview 29 | 30 | ![Diagram](app_diagram.png) 31 | 32 | ## Prerequisites 33 | 34 | - [Maven 3.8.5](https://maven.apache.org/install.html) & [Java 17](https://www.java.com/en/download/help/download_options.html) 35 | - [AWS free tier account](https://aws.amazon.com/free/) 36 | - [LocalStack](https://localstack.cloud/) 37 | - [Docker](https://docs.docker.com/get-docker/) - for running LocalStack 38 | - [Terraform](https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli) (+ Python pip for [tflocal](https://pypi.org/project/terraform-local/)) for creating AWS & LocalStack resources 39 | - [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) - for running the frontend app 40 | 41 | #### What it does 42 | 43 | *shipment-list-demo* is a Spring Boot application dealing with CRUD operations a person can 44 | execute 45 | on a bunch of shipments that they're allowed to view - think of it like the Post app. 46 | The demo consists of a backend and a frontend implementation, using React to display the 47 | information. 48 | The AWS services involved are: 49 | 50 | - [S3](https://docs.localstack.cloud/user-guide/aws/s3/) for storing pictures 51 | - [DynamoDB](https://docs.localstack.cloud/user-guide/aws/dynamodb/) for the entities 52 | - [Lambda](https://docs.localstack.cloud/user-guide/aws/lambda/) function that will validate the pictures, apply a watermark and replace non-compliant files. 53 | - [SNS](https://docs.localstack.cloud/user-guide/aws/sns/) that receives update notifications 54 | - [SQS](https://docs.localstack.cloud/user-guide/aws/sqs/) that subscribes to a topic and delivers the messages to the Spring Boot app 55 | 56 | 57 | #### How to use it 58 | 59 | We’ll be walking through a few scenarios using the application, and we expect it to maintain the 60 | behavior in both production (AWS) and development (LocalStack) environments. 61 | 62 | We’ll take advantage of one of the core features of the Spring framework that allows us to bind our 63 | beans to different profiles, such as dev, test, and prod. Of course, these beans need to know how to 64 | behave in each environment, so they’ll get that information from their designated configuration 65 | files, `application-prod.yml`, and `application-dev.yml`. 66 | 67 | #### Terraform 68 | 69 | The Terraform configuration file will create the needed S3 bucket, the DynamoDB `shipment` table and populate it with some 70 | sample data, the Lambda function that will help with the picture processing (make sure you create the jar), 71 | the SQS and SNS which will bring back the notification when the processing is finished. 72 | 73 | 74 | ## Instructions 75 | 76 | ### Only run once 77 | 78 | The following instructions only need to run once, weather you choose to run both cases, on AWS and 79 | LocalStack, or just jump straight to LocalStack. 80 | 81 | ### Building the validator module 82 | 83 | Step into the `shipment-picture-lambda-validator` module and run `mvn clean package shade:shade`. 84 | This will create an uber-jar by packaging all its dependencies. We'll need this one in the next 85 | steps. We can keep the same jar for both running on AWS and LocalStack. 86 | 87 | 88 | ### Running the GUI 89 | 90 | `cd` into `src/main/shipment-list-frontend` and run `npm install` and `npm start`. 91 | This will spin up the React app that can be accessed on `localhost:3000`. 92 | You'll only see the title, as the backend is not running yet to provide the list of shipments. 93 | 94 | For running it on Windows, there are some 95 | [extra requirements](https://learn.microsoft.com/en-us/windows/dev-environment/javascript/react-on-windows) 96 | , 97 | but no worries, it should be straightforward. 98 | 99 | #### How to use the GUI 100 | 101 | After starting the backend, refreshing the React app will fetch a list of shipments. 102 | The weight of a shipment is already given, but not the size, that's why we need pictures to 103 | understand it better, using the "banana for scale" measuring unit. How else would we know?? 104 | 105 | Current available actions using the GUI: 106 | 107 | - upload a new image 108 | - delete shipment from the list 109 | - create and update shipment are available only via Postman (or any other API platform) 110 | 111 | Files that are not pictures will be deleted 112 | and the shipment picture will be replaced with a generic icon, because we don't want any trouble. 113 | 114 | 115 | ## Running on AWS 116 | 117 | Now, we don’t have a real production environment because that’s not the point here, but most likely 118 | an application like this runs on a container orchestration platform, and all the necessary configs 119 | are still provided. Since we’re only simulating a production instance, all the configurations are 120 | kept in the `application-prod.yml` file. 121 | 122 | ### User credentials 123 | 124 | Before getting started, it's important to note that an IAM user, who's credentials will be used, 125 | needs to be created with the following policies: 126 | 127 | - AmazonS3FullAccess 128 | - AWSLambda_FullAccess 129 | - AmazonDynamoDBFullAccess 130 | - AmazonSNSFullAccess 131 | - AmazonSQSFullAccess 132 | - AWSLambdaExecute 133 | - AmazonS3ObjectLambdaExecutionRolePolicy 134 | 135 | For simplicity, we chose to use full access to all the services, so we don't have to add new permissions later on. 136 | We will be using the user's credentials and export them as temporary environment variables with the 137 | `export` (`set` on Windows) command: 138 | 139 | ``` 140 | $ export AWS_ACCESS_KEY_ID=[your_aws_access_key_id] 141 | $ export AWS_SECRET_ACCESS_KEY=[your_aws_secret_access_key_id] 142 | ``` 143 | 144 | ### Creating resources - running Terraform 145 | 146 | Make sure you have Terraform [installed](https://developer.hashicorp.com/terraform/downloads) 147 | 148 | Under `terraform` run: 149 | 150 | ``` 151 | $ terraform init 152 | $ terraform plan 153 | ``` 154 | 155 | Once these 2 commands run successfully and no errors occur, it's time to run: 156 | 157 | ``` 158 | $ terraform apply 159 | ``` 160 | If everything finishes successfully, the AWS services should be up and running. 161 | 162 | ### Starting the backend 163 | 164 | Go back to the root folder and run the backend simply by using 165 | 166 | ``` 167 | $ mvn spring-boot:run -Dspring-boot.run.profiles=prod 168 | ``` 169 | 170 | Notice the `prod` profile is being set via command line arguments. 171 | 172 | ### Using the application 173 | 174 | At `localhost:3000` you should now be able to see a list of shipments with standard icons, 175 | that means that only the database is populated, the pictures still need to be added from the 176 | `sample-pictures` folder. 177 | You can now interact with the application using the React app. All services used in the backend are 178 | running on the real AWS cloud. 179 | 180 | 181 | Before moving on, make sure you clean up your AWS resources by running (also in the `terraform` folder): 182 | 183 | ``` 184 | $ terraform destroy 185 | ``` 186 | 187 | ## Running on LocalStack 188 | 189 | 190 | To switch to using LocalStack instead of AWS services just run `docker compose up` in the root 191 | folder to spin up a Localstack container. 192 | 193 | ### Creating resources on LocalStack 194 | 195 | To generate the exact same resources on LocalStack, we need `tflocal`, a thin wrapper script around 196 | the terraform command line client. `tflocal` takes care of automatically configuring the local 197 | service 198 | endpoints, which allows you to easily deploy your unmodified Terraform scripts against LocalStack. 199 | 200 | You can [install](https://docs.localstack.cloud/user-guide/integrations/terraform/) the `tflocal` 201 | command via pip (requires a local Python installation): 202 | 203 | ``` 204 | $ pip install terraform-local 205 | ``` 206 | 207 | Once installed, the `tflocal` command should be available, with the same interface as the terraform 208 | command line. Try it out: 209 | 210 | ``` 211 | $ tflocal --help 212 | Usage: terraform [global options] [args] 213 | ... 214 | ``` 215 | 216 | From here on, it's the same as using AWS. In the `terraform` folder, run the `cleanup` script 217 | to get rid of any files that keep track of the resources' state. Then: 218 | 219 | ``` 220 | $ tflocal init 221 | $ tflocal plan 222 | $ tflocal apply 223 | ``` 224 | 225 | We run the exact same commands for the exact same file. We no longer need to pass any environment 226 | variables, since the bucket name is generated and passed by Terraform. 227 | 228 | ### Starting the backend 229 | 230 | After that, the Spring Boot application needs to start using the dev profile (make sure you're in 231 | the 232 | root folder): 233 | 234 | ``` 235 | $ mvn spring-boot:run -Dspring-boot.run.profiles=dev 236 | ``` 237 | ### Using the application 238 | 239 | Go back to `localhost:3000` and a new list will be available; notice that the functionalities of 240 | the application have not changed. 241 | 242 | There you have it, smooth transition from AWS to Localstack, with no code change. 👍🏻 243 | 244 | 245 | -------------------------------------------------------------------------------- /app_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinyg210/shipment-list-demo/984381c916dbde13053cc53935bcf98724240053/app_diagram.png -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | 3 | services: 4 | localstack: 5 | container_name: localstack 6 | image: localstack/localstack:latest 7 | ports: 8 | - "127.0.0.1:4566:4566" # LocalStack Gateway 9 | - "127.0.0.1:4510-4559:4510-4559" # external services port range 10 | environment: 11 | - DEBUG=1 # enable more verbose logs 12 | - DOCKER_HOST=unix:///var/run/docker.sock #unix socket to communicate with the docker daemon 13 | # - LAMBDA_KEEPALIVE_MS=0 # disable lambda keepalive 14 | - LOCALSTACK_HOST=localstack # where services are available from other containers 15 | - ENFORCE_IAM=1 # enforce IAM policies 16 | volumes: 17 | - "${LOCALSTACK_VOLUME_DIR:-./volume}:/var/lib/localstack" 18 | - "/var/run/docker.sock:/var/run/docker.sock" 19 | 20 | networks: 21 | ls: 22 | name: ls 23 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /usr/local/etc/mavenrc ] ; then 40 | . /usr/local/etc/mavenrc 41 | fi 42 | 43 | if [ -f /etc/mavenrc ] ; then 44 | . /etc/mavenrc 45 | fi 46 | 47 | if [ -f "$HOME/.mavenrc" ] ; then 48 | . "$HOME/.mavenrc" 49 | fi 50 | 51 | fi 52 | 53 | # OS specific support. $var _must_ be set to either true or false. 54 | cygwin=false; 55 | darwin=false; 56 | mingw=false 57 | case "`uname`" in 58 | CYGWIN*) cygwin=true ;; 59 | MINGW*) mingw=true;; 60 | Darwin*) darwin=true 61 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 62 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 63 | if [ -z "$JAVA_HOME" ]; then 64 | if [ -x "/usr/libexec/java_home" ]; then 65 | export JAVA_HOME="`/usr/libexec/java_home`" 66 | else 67 | export JAVA_HOME="/Library/Java/Home" 68 | fi 69 | fi 70 | ;; 71 | esac 72 | 73 | if [ -z "$JAVA_HOME" ] ; then 74 | if [ -r /etc/gentoo-release ] ; then 75 | JAVA_HOME=`java-config --jre-home` 76 | fi 77 | fi 78 | 79 | if [ -z "$M2_HOME" ] ; then 80 | ## resolve links - $0 may be a link to maven's home 81 | PRG="$0" 82 | 83 | # need this for relative symlinks 84 | while [ -h "$PRG" ] ; do 85 | ls=`ls -ld "$PRG"` 86 | link=`expr "$ls" : '.*-> \(.*\)$'` 87 | if expr "$link" : '/.*' > /dev/null; then 88 | PRG="$link" 89 | else 90 | PRG="`dirname "$PRG"`/$link" 91 | fi 92 | done 93 | 94 | saveddir=`pwd` 95 | 96 | M2_HOME=`dirname "$PRG"`/.. 97 | 98 | # make it fully qualified 99 | M2_HOME=`cd "$M2_HOME" && pwd` 100 | 101 | cd "$saveddir" 102 | # echo Using m2 at $M2_HOME 103 | fi 104 | 105 | # For Cygwin, ensure paths are in UNIX format before anything is touched 106 | if $cygwin ; then 107 | [ -n "$M2_HOME" ] && 108 | M2_HOME=`cygpath --unix "$M2_HOME"` 109 | [ -n "$JAVA_HOME" ] && 110 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 111 | [ -n "$CLASSPATH" ] && 112 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 113 | fi 114 | 115 | # For Mingw, ensure paths are in UNIX format before anything is touched 116 | if $mingw ; then 117 | [ -n "$M2_HOME" ] && 118 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 119 | [ -n "$JAVA_HOME" ] && 120 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 121 | fi 122 | 123 | if [ -z "$JAVA_HOME" ]; then 124 | javaExecutable="`which javac`" 125 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 126 | # readlink(1) is not available as standard on Solaris 10. 127 | readLink=`which readlink` 128 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 129 | if $darwin ; then 130 | javaHome="`dirname \"$javaExecutable\"`" 131 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 132 | else 133 | javaExecutable="`readlink -f \"$javaExecutable\"`" 134 | fi 135 | javaHome="`dirname \"$javaExecutable\"`" 136 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 137 | JAVA_HOME="$javaHome" 138 | export JAVA_HOME 139 | fi 140 | fi 141 | fi 142 | 143 | if [ -z "$JAVACMD" ] ; then 144 | if [ -n "$JAVA_HOME" ] ; then 145 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 146 | # IBM's JDK on AIX uses strange locations for the executables 147 | JAVACMD="$JAVA_HOME/jre/sh/java" 148 | else 149 | JAVACMD="$JAVA_HOME/bin/java" 150 | fi 151 | else 152 | JAVACMD="`\\unset -f command; \\command -v java`" 153 | fi 154 | fi 155 | 156 | if [ ! -x "$JAVACMD" ] ; then 157 | echo "Error: JAVA_HOME is not defined correctly." >&2 158 | echo " We cannot execute $JAVACMD" >&2 159 | exit 1 160 | fi 161 | 162 | if [ -z "$JAVA_HOME" ] ; then 163 | echo "Warning: JAVA_HOME environment variable is not set." 164 | fi 165 | 166 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 167 | 168 | # traverses directory structure from process work directory to filesystem root 169 | # first directory with .mvn subdirectory is considered project base directory 170 | find_maven_basedir() { 171 | 172 | if [ -z "$1" ] 173 | then 174 | echo "Path not specified to find_maven_basedir" 175 | return 1 176 | fi 177 | 178 | basedir="$1" 179 | wdir="$1" 180 | while [ "$wdir" != '/' ] ; do 181 | if [ -d "$wdir"/.mvn ] ; then 182 | basedir=$wdir 183 | break 184 | fi 185 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 186 | if [ -d "${wdir}" ]; then 187 | wdir=`cd "$wdir/.."; pwd` 188 | fi 189 | # end of workaround 190 | done 191 | echo "${basedir}" 192 | } 193 | 194 | # concatenates all lines of a file 195 | concat_lines() { 196 | if [ -f "$1" ]; then 197 | echo "$(tr -s '\n' ' ' < "$1")" 198 | fi 199 | } 200 | 201 | BASE_DIR=`find_maven_basedir "$(pwd)"` 202 | if [ -z "$BASE_DIR" ]; then 203 | exit 1; 204 | fi 205 | 206 | ########################################################################################## 207 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 208 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 209 | ########################################################################################## 210 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Found .mvn/wrapper/maven-wrapper.jar" 213 | fi 214 | else 215 | if [ "$MVNW_VERBOSE" = true ]; then 216 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 217 | fi 218 | if [ -n "$MVNW_REPOURL" ]; then 219 | jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 220 | else 221 | jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 222 | fi 223 | while IFS="=" read key value; do 224 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 225 | esac 226 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 227 | if [ "$MVNW_VERBOSE" = true ]; then 228 | echo "Downloading from: $jarUrl" 229 | fi 230 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 231 | if $cygwin; then 232 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 233 | fi 234 | 235 | if command -v wget > /dev/null; then 236 | if [ "$MVNW_VERBOSE" = true ]; then 237 | echo "Found wget ... using wget" 238 | fi 239 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 240 | wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 241 | else 242 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 243 | fi 244 | elif command -v curl > /dev/null; then 245 | if [ "$MVNW_VERBOSE" = true ]; then 246 | echo "Found curl ... using curl" 247 | fi 248 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 249 | curl -o "$wrapperJarPath" "$jarUrl" -f 250 | else 251 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 252 | fi 253 | 254 | else 255 | if [ "$MVNW_VERBOSE" = true ]; then 256 | echo "Falling back to using Java to download" 257 | fi 258 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 259 | # For Cygwin, switch paths to Windows format before running javac 260 | if $cygwin; then 261 | javaClass=`cygpath --path --windows "$javaClass"` 262 | fi 263 | if [ -e "$javaClass" ]; then 264 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 265 | if [ "$MVNW_VERBOSE" = true ]; then 266 | echo " - Compiling MavenWrapperDownloader.java ..." 267 | fi 268 | # Compiling the Java class 269 | ("$JAVA_HOME/bin/javac" "$javaClass") 270 | fi 271 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 272 | # Running the downloader 273 | if [ "$MVNW_VERBOSE" = true ]; then 274 | echo " - Running MavenWrapperDownloader.java ..." 275 | fi 276 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 277 | fi 278 | fi 279 | fi 280 | fi 281 | ########################################################################################## 282 | # End of extension 283 | ########################################################################################## 284 | 285 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 286 | if [ "$MVNW_VERBOSE" = true ]; then 287 | echo $MAVEN_PROJECTBASEDIR 288 | fi 289 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 290 | 291 | # For Cygwin, switch paths to Windows format before running java 292 | if $cygwin; then 293 | [ -n "$M2_HOME" ] && 294 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 295 | [ -n "$JAVA_HOME" ] && 296 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 297 | [ -n "$CLASSPATH" ] && 298 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 299 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 300 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 301 | fi 302 | 303 | # Provide a "standardized" way to retrieve the CLI args that will 304 | # work with both Windows and non-Windows executions. 305 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 306 | export MAVEN_CMD_LINE_ARGS 307 | 308 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 309 | 310 | exec "$JAVACMD" \ 311 | $MAVEN_OPTS \ 312 | $MAVEN_DEBUG_OPTS \ 313 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 314 | "-Dmaven.home=${M2_HOME}" \ 315 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 316 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 317 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* 50 | if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 124 | 125 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% ^ 162 | %JVM_CONFIG_MAVEN_PROPS% ^ 163 | %MAVEN_OPTS% ^ 164 | %MAVEN_DEBUG_OPTS% ^ 165 | -classpath %WRAPPER_JAR% ^ 166 | "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ 167 | %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 168 | if ERRORLEVEL 1 goto error 169 | goto end 170 | 171 | :error 172 | set ERROR_CODE=1 173 | 174 | :end 175 | @endlocal & set ERROR_CODE=%ERROR_CODE% 176 | 177 | if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost 178 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 179 | if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" 180 | if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" 181 | :skipRcPost 182 | 183 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 184 | if "%MAVEN_BATCH_PAUSE%"=="on" pause 185 | 186 | if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% 187 | 188 | cmd /C exit /B %ERROR_CODE% 189 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shipment-list-demo", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": {} 6 | } 7 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | org.springframework.boot 9 | spring-boot-starter-parent 10 | 3.0.1 11 | 12 | 13 | dev.ancaghenade 14 | shipment-list-demo 15 | 0.0.1-SNAPSHOT 16 | 17 | shipment-list-demo 18 | Shipment List Service 19 | 20 | 21 | 17 22 | 17 23 | 17 24 | 3.2.0 25 | 26 | UTF-8 27 | 28 | 29 | 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-web 34 | 35 | 36 | org.projectlombok 37 | lombok 38 | true 39 | 40 | 41 | software.amazon.awssdk 42 | s3 43 | 44 | 45 | software.amazon.awssdk 46 | dynamodb-enhanced 47 | 48 | 49 | software.amazon.awssdk 50 | dynamodb 51 | 52 | 53 | org.springframework.boot 54 | spring-boot-autoconfigure 55 | 56 | 57 | org.springframework.boot 58 | spring-boot-starter-logging 59 | 60 | 61 | io.awspring.cloud 62 | spring-cloud-aws-starter-sqs 63 | 64 | 65 | 66 | 67 | 68 | org.springframework.boot 69 | spring-boot-starter-test 70 | test 71 | 72 | 73 | org.json 74 | json 75 | 20220924 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | software.amazon.awssdk 84 | bom 85 | 2.20.26 86 | pom 87 | import 88 | 89 | 90 | io.awspring.cloud 91 | spring-cloud-aws-dependencies 92 | 3.0.0-RC1 93 | pom 94 | import 95 | 96 | 97 | 98 | 99 | 100 | shipment-list-demo 101 | 102 | 103 | src/main/resources 104 | true 105 | 106 | 107 | 108 | 109 | 110 | org.springframework.boot 111 | spring-boot-maven-plugin 112 | 113 | 114 | 115 | org.projectlombok 116 | lombok 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /sample-pictures/cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinyg210/shipment-list-demo/984381c916dbde13053cc53935bcf98724240053/sample-pictures/cat.jpg -------------------------------------------------------------------------------- /sample-pictures/open-package.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinyg210/shipment-list-demo/984381c916dbde13053cc53935bcf98724240053/sample-pictures/open-package.jpg -------------------------------------------------------------------------------- /sample-pictures/package.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinyg210/shipment-list-demo/984381c916dbde13053cc53935bcf98724240053/sample-pictures/package.png -------------------------------------------------------------------------------- /sample-pictures/scale.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinyg210/shipment-list-demo/984381c916dbde13053cc53935bcf98724240053/sample-pictures/scale.jpg -------------------------------------------------------------------------------- /sample-pictures/television.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinyg210/shipment-list-demo/984381c916dbde13053cc53935bcf98724240053/sample-pictures/television.jpg -------------------------------------------------------------------------------- /sample-pictures/wrong-format-file.txt: -------------------------------------------------------------------------------- 1 | This file is the wrong format.It needs to go. -------------------------------------------------------------------------------- /shipment-picture-lambda-validator/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | dev.ancaghenade 8 | shipment-picture-lambda-validator 9 | 1.0-SNAPSHOT 10 | 11 | 12 | 11 13 | 11 14 | 15 | 16 | 17 | 18 | software.amazon.awssdk 19 | lambda 20 | 21 | 22 | software.amazon.awssdk 23 | protocol-core 24 | 2.20.47 25 | 26 | 27 | software.amazon.awssdk 28 | s3 29 | 30 | 31 | software.amazon.awssdk 32 | sns 33 | 34 | 35 | com.amazonaws 36 | aws-lambda-java-core 37 | 1.2.2 38 | 39 | 40 | com.jayway.jsonpath 41 | json-path 42 | 2.7.0 43 | compile 44 | 45 | 46 | org.apache.httpcomponents 47 | httpcore 48 | 4.4.16 49 | compile 50 | 51 | 52 | 53 | 54 | net.coobird 55 | thumbnailator 56 | 0.4.19 57 | 58 | 59 | org.projectlombok 60 | lombok 61 | 1.18.22 62 | compile 63 | 64 | 65 | 66 | 67 | 68 | 69 | software.amazon.awssdk 70 | bom 71 | 2.20.47 72 | pom 73 | import 74 | 75 | 76 | 77 | 78 | 79 | shipment-picture-lambda-validator 80 | 81 | 82 | src/main/resources 83 | true 84 | 85 | 86 | 87 | 88 | 89 | org.apache.maven.plugins 90 | maven-shade-plugin 91 | 2.4.3 92 | 93 | false 94 | 95 | 96 | 97 | package 98 | 99 | shade 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /shipment-picture-lambda-validator/src/main/java/dev/ancaghenade/shipmentpicturelambdavalidator/Location.java: -------------------------------------------------------------------------------- 1 | package dev.ancaghenade.shipmentpicturelambdavalidator; 2 | 3 | import lombok.Getter; 4 | import software.amazon.awssdk.regions.Region; 5 | 6 | @Getter 7 | public enum Location { 8 | 9 | 10 | REGION(Region.US_EAST_1); 11 | 12 | private final Region region; 13 | Location(Region region) { 14 | this.region = region; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /shipment-picture-lambda-validator/src/main/java/dev/ancaghenade/shipmentpicturelambdavalidator/PropertiesProvider.java: -------------------------------------------------------------------------------- 1 | package dev.ancaghenade.shipmentpicturelambdavalidator; 2 | 3 | import java.io.FileNotFoundException; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.util.Properties; 7 | 8 | public class PropertiesProvider { 9 | 10 | InputStream inputStream; 11 | 12 | public Properties values() throws IOException { 13 | try { 14 | Properties properties = new java.util.Properties(); 15 | inputStream = getClass().getClassLoader().getResourceAsStream("config.properties"); 16 | if (inputStream != null) { 17 | properties.load(inputStream); 18 | } else { 19 | throw new FileNotFoundException("Property file not found in the classpath."); 20 | } 21 | return properties; 22 | } catch (Exception e) { 23 | System.out.println("Exception: " + e); 24 | } finally { 25 | inputStream.close(); 26 | } 27 | return null; 28 | } 29 | 30 | public String getProperty(String key) throws IOException { 31 | return values().getProperty(key); 32 | } 33 | } -------------------------------------------------------------------------------- /shipment-picture-lambda-validator/src/main/java/dev/ancaghenade/shipmentpicturelambdavalidator/S3ClientHelper.java: -------------------------------------------------------------------------------- 1 | package dev.ancaghenade.shipmentpicturelambdavalidator; 2 | 3 | import java.io.IOException; 4 | import java.net.URI; 5 | import java.util.Objects; 6 | import software.amazon.awssdk.services.s3.S3Client; 7 | 8 | public class S3ClientHelper { 9 | 10 | private static final String LOCALSTACK_HOSTNAME = System.getenv("LOCALSTACK_HOSTNAME"); 11 | 12 | public static S3Client getS3Client() throws IOException { 13 | 14 | var clientBuilder = S3Client.builder(); 15 | if (Objects.nonNull(LOCALSTACK_HOSTNAME)) { 16 | return clientBuilder 17 | .region(Location.REGION.getRegion()) 18 | .endpointOverride(URI.create(String.format("http://%s:4566", LOCALSTACK_HOSTNAME))) 19 | .forcePathStyle(true) 20 | .build(); 21 | } else { 22 | return clientBuilder.build(); 23 | } 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /shipment-picture-lambda-validator/src/main/java/dev/ancaghenade/shipmentpicturelambdavalidator/SNSClientHelper.java: -------------------------------------------------------------------------------- 1 | package dev.ancaghenade.shipmentpicturelambdavalidator; 2 | 3 | import java.net.URI; 4 | import java.util.Objects; 5 | import software.amazon.awssdk.services.sns.SnsClient; 6 | 7 | public class SNSClientHelper { 8 | 9 | private static final String LOCALSTACK_HOSTNAME = System.getenv("LOCALSTACK_HOSTNAME"); 10 | private static String snsTopicArn; 11 | 12 | public static SnsClient getSnsClient() { 13 | 14 | var clientBuilder = SnsClient.builder(); 15 | 16 | if (Objects.nonNull(LOCALSTACK_HOSTNAME)) { 17 | snsTopicArn = String.format("arn:aws:sns:%s:000000000000:update_shipment_picture_topic", 18 | Location.REGION.getRegion()); 19 | 20 | return clientBuilder 21 | .region(Location.REGION.getRegion()) 22 | .endpointOverride(URI.create(String.format("http://%s:4566", LOCALSTACK_HOSTNAME))) 23 | .build(); 24 | } else { 25 | snsTopicArn = String.format("arn:aws:sns:%s:%s:update_shipment_picture_topic", 26 | Location.REGION.getRegion(), "932043840972"); 27 | return clientBuilder.build(); 28 | } 29 | } 30 | 31 | public static String topicARN() { 32 | return snsTopicArn; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /shipment-picture-lambda-validator/src/main/java/dev/ancaghenade/shipmentpicturelambdavalidator/ServiceHandler.java: -------------------------------------------------------------------------------- 1 | package dev.ancaghenade.shipmentpicturelambdavalidator; 2 | 3 | import com.amazonaws.services.lambda.runtime.Context; 4 | import com.amazonaws.services.lambda.runtime.RequestStreamHandler; 5 | import com.jayway.jsonpath.JsonPath; 6 | import java.awt.image.BufferedImage; 7 | import java.io.ByteArrayOutputStream; 8 | import java.io.File; 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | import java.io.OutputStream; 12 | import java.util.Collections; 13 | import java.util.List; 14 | import java.util.Objects; 15 | import javax.imageio.ImageIO; 16 | import org.apache.http.entity.ContentType; 17 | import software.amazon.awssdk.core.ResponseInputStream; 18 | import software.amazon.awssdk.core.sync.RequestBody; 19 | import software.amazon.awssdk.services.s3.S3Client; 20 | import software.amazon.awssdk.services.s3.model.GetObjectRequest; 21 | import software.amazon.awssdk.services.s3.model.GetObjectResponse; 22 | import software.amazon.awssdk.services.s3.model.PutObjectRequest; 23 | import software.amazon.awssdk.services.sns.SnsClient; 24 | import software.amazon.awssdk.services.sns.model.PublishRequest; 25 | 26 | 27 | public class ServiceHandler implements RequestStreamHandler { 28 | 29 | private static final String BUCKET_NAME = System.getenv("BUCKET"); 30 | public ServiceHandler() { 31 | } 32 | 33 | @Override 34 | public void handleRequest(InputStream inputStream, OutputStream outputStream, 35 | Context context) throws IOException { 36 | var isValid = true; 37 | 38 | var s3Client = acquireS3Client(); 39 | var snsClient = acquireSnsClient(); 40 | var objectKey = getObjectKey(inputStream, context); 41 | 42 | if (Objects.isNull(objectKey)) { 43 | context.getLogger().log("Object key is null"); 44 | return; 45 | } 46 | 47 | context.getLogger().log("Object key: " + objectKey); 48 | 49 | var getObjectRequest = GetObjectRequest.builder() 50 | .bucket(BUCKET_NAME) 51 | .key(objectKey) 52 | .build(); 53 | 54 | ResponseInputStream s3ObjectResponse; 55 | try { 56 | s3ObjectResponse = s3Client.getObject( 57 | getObjectRequest); 58 | } catch (Exception e) { 59 | e.printStackTrace(); 60 | context.getLogger().log(e.getMessage()); 61 | return; 62 | } 63 | context.getLogger().log("Object fetched"); 64 | 65 | // Check if the image was already processed 66 | if (s3ObjectResponse.response().metadata().entrySet().stream().anyMatch( 67 | entry -> entry.getKey().equals("exclude-lambda") && entry.getValue().equals("true"))) { 68 | context.getLogger().log("Object already present."); 69 | return; 70 | } 71 | 72 | // Check the file extension to determine the image format 73 | if (!List.of(ContentType.IMAGE_JPEG.getMimeType(), 74 | ContentType.IMAGE_PNG.getMimeType(), 75 | ContentType.IMAGE_BMP.getMimeType()) 76 | .contains(s3ObjectResponse.response().contentType())) { 77 | isValid = false; 78 | context.getLogger().log("Object invalid due to wrong format."); 79 | 80 | } 81 | 82 | // Get the object data as a byte array 83 | var objectData = s3Client.getObject(getObjectRequest).readAllBytes(); 84 | 85 | if (!isValid) { 86 | try { 87 | File imageFile = new File("placeholder.jpg"); 88 | BufferedImage image = ImageIO.read(imageFile); 89 | 90 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 91 | ImageIO.write(image, "jpg", baos); 92 | byte[] imageBytes = baos.toByteArray(); 93 | 94 | objectKey = TextParser.replaceSufix(objectKey, "placeholder.jpg"); 95 | 96 | System.out.println("NEW IMAGE LINK: " + objectKey); 97 | 98 | var putObjectRequest = PutObjectRequest.builder() 99 | .bucket(BUCKET_NAME) 100 | .key(objectKey) 101 | .metadata(Collections.singletonMap("exclude-lambda", "true")) 102 | .build(); 103 | 104 | s3Client.putObject(putObjectRequest, RequestBody.fromBytes(imageBytes)); 105 | 106 | baos.close(); 107 | } catch (IOException e) { 108 | e.printStackTrace(); 109 | } 110 | 111 | } else { 112 | var extension = s3ObjectResponse.response().contentType(); 113 | 114 | var putObjectRequest = PutObjectRequest.builder() 115 | .bucket(BUCKET_NAME) 116 | .key(objectKey) 117 | .metadata(Collections.singletonMap("exclude-lambda", "true")) 118 | .build(); 119 | 120 | s3Client.putObject(putObjectRequest, RequestBody.fromBytes( 121 | Watermark.watermarkImage(objectData, 122 | extension.substring(extension.lastIndexOf("/") + 1)))); 123 | context.getLogger().log("Watermark has been added."); 124 | } 125 | var request = PublishRequest 126 | .builder() 127 | .message(objectKey) 128 | .topicArn(SNSClientHelper.topicARN()) 129 | .build(); 130 | snsClient.publish(request); 131 | context.getLogger().log("Published to topic: " + request.topicArn()); 132 | 133 | // Close clients 134 | s3Client.close(); 135 | snsClient.close(); 136 | 137 | } 138 | 139 | private String getObjectKey(InputStream inputStream, Context context) { 140 | try { 141 | List keys = JsonPath.read(inputStream, "$.Records[*].s3.object.key"); 142 | if (keys.iterator().hasNext()) { 143 | return keys.iterator().next(); 144 | } 145 | } catch (IOException ioe) { 146 | context.getLogger().log("caught IOException reading input stream"); 147 | } 148 | return null; 149 | } 150 | 151 | private S3Client acquireS3Client() { 152 | try { 153 | return S3ClientHelper.getS3Client(); 154 | } catch (IOException e) { 155 | throw new RuntimeException(e); 156 | } 157 | } 158 | 159 | private SnsClient acquireSnsClient() { 160 | return SNSClientHelper.getSnsClient(); 161 | } 162 | } 163 | 164 | -------------------------------------------------------------------------------- /shipment-picture-lambda-validator/src/main/java/dev/ancaghenade/shipmentpicturelambdavalidator/TextParser.java: -------------------------------------------------------------------------------- 1 | package dev.ancaghenade.shipmentpicturelambdavalidator; 2 | 3 | import java.util.regex.Pattern; 4 | 5 | public class TextParser { 6 | 7 | public static String replaceSufix(String input, String replacement) { 8 | String[] parts = input.split("-"); 9 | 10 | // Find the last UUID 11 | String lastUUID = ""; 12 | for (int i = parts.length - 1; i >= 0; i--) { 13 | if (isUUID(parts[i])) { 14 | lastUUID = parts[i]; 15 | break; 16 | } 17 | } 18 | 19 | // Extract the part before the last UUID 20 | String partBeforeLastUUID = input.substring(0, input.lastIndexOf(lastUUID) - 1); 21 | 22 | // Replace the part after the last UUID with a new string 23 | String result = partBeforeLastUUID + "-" + replacement; 24 | return result; 25 | } 26 | 27 | // Check if a string is a valid UUID 28 | public static boolean isUUID(String input) { 29 | String uuidPattern = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"; 30 | return Pattern.matches(uuidPattern, input); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /shipment-picture-lambda-validator/src/main/java/dev/ancaghenade/shipmentpicturelambdavalidator/Watermark.java: -------------------------------------------------------------------------------- 1 | package dev.ancaghenade.shipmentpicturelambdavalidator; 2 | 3 | import java.awt.AlphaComposite; 4 | import java.awt.Color; 5 | import java.awt.Font; 6 | import java.awt.Graphics2D; 7 | import java.awt.image.BufferedImage; 8 | import java.io.ByteArrayInputStream; 9 | import java.io.ByteArrayOutputStream; 10 | import java.io.IOException; 11 | import javax.imageio.ImageIO; 12 | 13 | public class Watermark { 14 | 15 | static byte[] watermarkImage(byte[] objectData, String extension) 16 | throws IOException { 17 | var originalImage = ImageIO.read(new ByteArrayInputStream(objectData)); 18 | var watermarkedImage = new BufferedImage(originalImage.getWidth(), 19 | originalImage.getHeight(), BufferedImage.TYPE_INT_RGB); 20 | var g2d = (Graphics2D) watermarkedImage.getGraphics(); 21 | g2d.drawImage(originalImage, 0, 0, null); 22 | var font = new Font("Arial", Font.BOLD, 40); 23 | var color = Color.WHITE; 24 | 25 | g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f)); 26 | 27 | g2d.setFont(font); 28 | g2d.setColor(color); 29 | g2d.rotate(Math.toRadians(45), 50, 50); 30 | g2d.drawString("Banana for scale approved!", 50, 50); 31 | 32 | var baos = new ByteArrayOutputStream(); 33 | ImageIO.write(watermarkedImage, extension, baos); 34 | baos.close(); 35 | return baos.toByteArray(); 36 | 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /shipment-picture-lambda-validator/src/main/resources/config.properties: -------------------------------------------------------------------------------- 1 | aws.region=eu-central-1 2 | environment.dev=dev 3 | credentials.access-key=test_access_key 4 | credentials.secret-key=test_secret_access_key 5 | s3.endpoint=http://localstack:4566 6 | sns.endpoint=http://localstack:4566 7 | sns.arn.dev=arn:aws:sns:eu-central-1:000000000000:update_shipment_picture_topic 8 | sns.arn.prod=arn:aws:sns:eu-central-1:932043840972:update_shipment_picture_topic 9 | -------------------------------------------------------------------------------- /shipment-picture-lambda-validator/src/main/resources/lambda_update_script.sh: -------------------------------------------------------------------------------- 1 | 2 | #you need awslocal cli installed for this 3 | 4 | awslocal lambda update-function-code --function-name shipment-picture-lambda-validator \ 5 | --zip-file fileb://target/shipment-picture-lambda-validator.jar \ 6 | --region us-east-1 7 | 8 | aws lambda update-function-code --function-name shipment-picture-lambda-validator \ 9 | --zip-file fileb://target/shipment-picture-lambda-validator.jar \ 10 | --region us-east-1 -------------------------------------------------------------------------------- /shipment-picture-lambda-validator/src/main/resources/placeholder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinyg210/shipment-list-demo/984381c916dbde13053cc53935bcf98724240053/shipment-picture-lambda-validator/src/main/resources/placeholder.jpg -------------------------------------------------------------------------------- /src/main/java/dev/ancaghenade/shipmentlistdemo/ShipmentListDemoApplication.java: -------------------------------------------------------------------------------- 1 | package dev.ancaghenade.shipmentlistdemo; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class ShipmentListDemoApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(ShipmentListDemoApplication.class, args); 11 | } 12 | 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/dev/ancaghenade/shipmentlistdemo/buckets/BucketName.java: -------------------------------------------------------------------------------- 1 | package dev.ancaghenade.shipmentlistdemo.buckets; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.context.annotation.PropertySource; 6 | import org.springframework.stereotype.Component; 7 | 8 | @Component 9 | @Configuration 10 | @PropertySource(value = "classpath:buckets.properties") 11 | public class BucketName { 12 | 13 | @Value("${shipment-picture-bucket}") 14 | private String shipmentPictureBucket; 15 | @Value("${shipment-picture-bucket-validator}") 16 | private String shipmentPictureValidatorBucket; 17 | 18 | public String getShipmentPictureBucket() { 19 | return shipmentPictureBucket; 20 | } 21 | 22 | public String getShipmentPictureValidatorBucket() { 23 | return shipmentPictureValidatorBucket; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/dev/ancaghenade/shipmentlistdemo/config/AWSClientConfig.java: -------------------------------------------------------------------------------- 1 | package dev.ancaghenade.shipmentlistdemo.config; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; 5 | import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; 6 | import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; 7 | 8 | public abstract class AWSClientConfig { 9 | 10 | @Value("${aws.credentials.access-key}") 11 | protected String awsAccessKey; 12 | 13 | @Value("${aws.credentials.secret-key}") 14 | protected String awsSecretKey; 15 | 16 | @Value("${aws.region}") 17 | protected String awsRegion; 18 | 19 | protected AwsCredentialsProvider amazonAWSCredentialsProvider() { 20 | return StaticCredentialsProvider.create(AwsBasicCredentials.create(awsAccessKey, awsSecretKey)); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/dev/ancaghenade/shipmentlistdemo/config/AmazonS3Config.java: -------------------------------------------------------------------------------- 1 | package dev.ancaghenade.shipmentlistdemo.config; 2 | 3 | 4 | import java.net.URI; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import software.amazon.awssdk.regions.Region; 9 | import software.amazon.awssdk.services.s3.S3Client; 10 | 11 | @Configuration 12 | public class AmazonS3Config extends AWSClientConfig { 13 | 14 | @Value("${aws.s3.endpoint}") 15 | private String awsS3EndPoint; 16 | 17 | @Bean 18 | public S3Client s3() { 19 | return S3Client.builder() 20 | .region(Region.of(awsRegion)) 21 | .credentialsProvider(amazonAWSCredentialsProvider()) 22 | .endpointOverride(URI.create(awsS3EndPoint)) 23 | .build(); 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /src/main/java/dev/ancaghenade/shipmentlistdemo/config/AmazonSQSConfig.java: -------------------------------------------------------------------------------- 1 | package dev.ancaghenade.shipmentlistdemo.config; 2 | 3 | import java.net.URI; 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import software.amazon.awssdk.regions.Region; 8 | import software.amazon.awssdk.services.sqs.SqsAsyncClient; 9 | 10 | @Configuration 11 | public class AmazonSQSConfig extends AWSClientConfig { 12 | 13 | @Value("${aws.sqs.endpoint}") 14 | private String awsSqsEndPoint; 15 | 16 | @Bean 17 | public SqsAsyncClient sqsClient() { 18 | return SqsAsyncClient.builder() 19 | .endpointOverride(URI.create(awsSqsEndPoint)) 20 | .credentialsProvider(amazonAWSCredentialsProvider()) 21 | .region(Region.of(awsRegion)) 22 | .build(); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/dev/ancaghenade/shipmentlistdemo/config/DynamoDBConfig.java: -------------------------------------------------------------------------------- 1 | package dev.ancaghenade.shipmentlistdemo.config; 2 | 3 | import dev.ancaghenade.shipmentlistdemo.entity.Shipment; 4 | import java.net.URI; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; 9 | import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; 10 | import software.amazon.awssdk.enhanced.dynamodb.TableSchema; 11 | import software.amazon.awssdk.regions.Region; 12 | import software.amazon.awssdk.services.dynamodb.DynamoDbClient; 13 | 14 | @Configuration 15 | public class DynamoDBConfig extends AWSClientConfig { 16 | 17 | @Value("${aws.dynamodb.endpoint}") 18 | private String awsDynamoDBEndPoint; 19 | 20 | @Bean 21 | public DynamoDbEnhancedClient dynamoDbClient() { 22 | DynamoDbClient dynamoDbClient = DynamoDbClient.builder() 23 | .region(Region.of(awsRegion)) 24 | .credentialsProvider(amazonAWSCredentialsProvider()) 25 | .endpointOverride(URI.create(awsDynamoDBEndPoint)) 26 | .build(); 27 | 28 | // using the enhanced client for mapping classes to tables 29 | return DynamoDbEnhancedClient.builder() 30 | .dynamoDbClient(dynamoDbClient) 31 | .build(); 32 | } 33 | @Bean 34 | public DynamoDbTable shipmentTable(DynamoDbEnhancedClient dynamoDbClient) { 35 | return dynamoDbClient.table("shipment", TableSchema.fromBean(Shipment.class)); 36 | } 37 | 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/dev/ancaghenade/shipmentlistdemo/controller/MessageReceiver.java: -------------------------------------------------------------------------------- 1 | package dev.ancaghenade.shipmentlistdemo.controller; 2 | 3 | import dev.ancaghenade.shipmentlistdemo.service.ShipmentService; 4 | import io.awspring.cloud.sqs.annotation.SqsListener; 5 | import java.io.IOException; 6 | import java.util.List; 7 | import java.util.concurrent.CopyOnWriteArrayList; 8 | import org.json.JSONObject; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.stereotype.Service; 13 | import org.springframework.web.bind.annotation.CrossOrigin; 14 | import org.springframework.web.bind.annotation.GetMapping; 15 | import org.springframework.web.bind.annotation.RestController; 16 | import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; 17 | 18 | @Service 19 | @RestController 20 | public class MessageReceiver { 21 | 22 | private static final Logger LOGGER = LoggerFactory.getLogger(MessageReceiver.class); 23 | private final List emitters = new CopyOnWriteArrayList<>(); 24 | 25 | private final ShipmentService shipmentService; 26 | 27 | @Autowired 28 | public MessageReceiver(ShipmentService shipmentService) { 29 | this.shipmentService = shipmentService; 30 | } 31 | 32 | @SqsListener(value = "update_shipment_picture_queue") 33 | public void loadMessagesFromQueue(String notification) { 34 | LOGGER.info("Message from queue %s" + notification); 35 | 36 | JSONObject obj = new JSONObject(notification); 37 | String message = obj.getString("Message"); 38 | String shipmentId = message.split("/")[0]; 39 | 40 | shipmentService.updateImageLink(shipmentId, message); 41 | 42 | for (var sseEmitter : emitters) { 43 | try { 44 | sseEmitter.send( 45 | shipmentId); 46 | sleep(sseEmitter); 47 | } catch (IOException e) { 48 | sseEmitter.completeWithError(e); 49 | } 50 | sseEmitter.complete(); 51 | LOGGER.info("SSE emitter complete."); 52 | } 53 | } 54 | 55 | @GetMapping(value = "/push-endpoint") 56 | @CrossOrigin(origins = "http://localhost:3000") 57 | public SseEmitter pushData() { 58 | 59 | SseEmitter emitter = new SseEmitter(Long.MAX_VALUE); 60 | 61 | emitter.onCompletion(() -> { 62 | synchronized (emitters) { 63 | emitters.remove(emitter); 64 | LOGGER.info("SseEmitter is completed"); 65 | } 66 | }); 67 | 68 | emitter.onTimeout(() -> { 69 | synchronized (emitters) { 70 | emitters.remove(emitter); 71 | } 72 | emitter.complete(); 73 | LOGGER.info("SseEmitter is timed out"); 74 | }); 75 | 76 | emitter.onError(e -> { 77 | synchronized (emitters) { 78 | emitters.remove(emitter); 79 | } 80 | }); 81 | 82 | synchronized (emitters) { 83 | emitters.add(emitter); 84 | } 85 | 86 | return emitter; 87 | } 88 | 89 | private void sleep(SseEmitter sseEmitter) { 90 | try { 91 | Thread.sleep(1000); 92 | } catch (InterruptedException e) { 93 | e.printStackTrace(); 94 | sseEmitter.completeWithError(e); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/dev/ancaghenade/shipmentlistdemo/controller/ShipmentController.java: -------------------------------------------------------------------------------- 1 | package dev.ancaghenade.shipmentlistdemo.controller; 2 | 3 | 4 | import dev.ancaghenade.shipmentlistdemo.entity.Shipment; 5 | import dev.ancaghenade.shipmentlistdemo.service.ShipmentService; 6 | import java.util.List; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.http.MediaType; 9 | import org.springframework.web.bind.annotation.CrossOrigin; 10 | import org.springframework.web.bind.annotation.DeleteMapping; 11 | import org.springframework.web.bind.annotation.GetMapping; 12 | import org.springframework.web.bind.annotation.PathVariable; 13 | import org.springframework.web.bind.annotation.PostMapping; 14 | import org.springframework.web.bind.annotation.RequestBody; 15 | import org.springframework.web.bind.annotation.RequestMapping; 16 | import org.springframework.web.bind.annotation.RequestParam; 17 | import org.springframework.web.bind.annotation.RestController; 18 | import org.springframework.web.multipart.MultipartFile; 19 | 20 | @RestController 21 | @RequestMapping("api/shipment") 22 | @CrossOrigin("http://localhost:3000") 23 | public class ShipmentController { 24 | 25 | private final ShipmentService shipmentService; 26 | 27 | @Autowired 28 | public ShipmentController(ShipmentService shipmentService) { 29 | this.shipmentService = shipmentService; 30 | } 31 | 32 | @GetMapping 33 | public List getAllShipments() { 34 | return shipmentService.getAllShipments(); 35 | } 36 | 37 | @GetMapping( 38 | path = "{shipmentId}/image/download", produces = MediaType.IMAGE_JPEG_VALUE) 39 | public byte[] downloadShipmentImage(@PathVariable("shipmentId") String shipmentId) { 40 | return shipmentService.downloadShipmentImage(shipmentId); 41 | } 42 | 43 | @DeleteMapping("/{shipmentId}") 44 | public String deleteShipment(@PathVariable("shipmentId") String shipmentId) { 45 | return shipmentService.deleteShipment(shipmentId); 46 | } 47 | 48 | @PostMapping( 49 | path = "{shipmentId}/image/upload", 50 | consumes = MediaType.MULTIPART_FORM_DATA_VALUE, 51 | produces = MediaType.APPLICATION_JSON_VALUE) 52 | public void uploadShipmentImage(@PathVariable("shipmentId") String shipmentId, 53 | @RequestParam("file") MultipartFile file) { 54 | shipmentService.uploadShipmentImage(shipmentId, file); 55 | } 56 | 57 | @PostMapping( 58 | consumes = MediaType.APPLICATION_JSON_VALUE, 59 | produces = MediaType.APPLICATION_JSON_VALUE) 60 | public void saveUpdateShipment(@RequestBody Shipment shipment) { 61 | shipmentService.saveShipment(shipment); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/dev/ancaghenade/shipmentlistdemo/entity/Address.java: -------------------------------------------------------------------------------- 1 | package dev.ancaghenade.shipmentlistdemo.entity; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbAttribute; 8 | import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; 9 | 10 | @Data 11 | @AllArgsConstructor 12 | @NoArgsConstructor 13 | @Builder 14 | @DynamoDbBean 15 | public class Address { 16 | 17 | private String postalCode; 18 | private String street; 19 | private String number; 20 | private String city; 21 | private String additionalInfo; 22 | 23 | @DynamoDbAttribute("postalCode") 24 | public String getPostalCode() { 25 | return postalCode; 26 | } 27 | 28 | public void setPostalCode(String postalCode) { 29 | this.postalCode = postalCode; 30 | } 31 | 32 | @DynamoDbAttribute("street") 33 | 34 | public String getStreet() { 35 | return street; 36 | } 37 | 38 | public void setStreet(String street) { 39 | this.street = street; 40 | } 41 | 42 | @DynamoDbAttribute("number") 43 | 44 | public String getNumber() { 45 | return number; 46 | } 47 | 48 | 49 | public void setNumber(String number) { 50 | this.number = number; 51 | } 52 | 53 | @DynamoDbAttribute("city") 54 | 55 | public String getCity() { 56 | return city; 57 | } 58 | 59 | public void setCity(String city) { 60 | this.city = city; 61 | } 62 | 63 | @DynamoDbAttribute("additionalInfo") 64 | 65 | public String getAdditionalInfo() { 66 | return additionalInfo; 67 | } 68 | 69 | public void setAdditionalInfo(String additionalInfo) { 70 | this.additionalInfo = additionalInfo; 71 | } 72 | } -------------------------------------------------------------------------------- /src/main/java/dev/ancaghenade/shipmentlistdemo/entity/Participant.java: -------------------------------------------------------------------------------- 1 | package dev.ancaghenade.shipmentlistdemo.entity; 2 | 3 | 4 | import lombok.AllArgsConstructor; 5 | import lombok.Builder; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbAttribute; 9 | import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; 10 | 11 | @Data 12 | @AllArgsConstructor 13 | @NoArgsConstructor 14 | @Builder 15 | @DynamoDbBean 16 | public class Participant { 17 | 18 | private String name; 19 | private Address address; 20 | 21 | @DynamoDbAttribute("name") 22 | 23 | public String getName() { 24 | return name; 25 | } 26 | 27 | public void setName(String name) { 28 | this.name = name; 29 | } 30 | 31 | @DynamoDbAttribute("address") 32 | 33 | public Address getAddress() { 34 | return address; 35 | } 36 | 37 | public void setAddress(Address address) { 38 | this.address = address; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/dev/ancaghenade/shipmentlistdemo/entity/Shipment.java: -------------------------------------------------------------------------------- 1 | package dev.ancaghenade.shipmentlistdemo.entity; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.EqualsAndHashCode; 7 | import lombok.NoArgsConstructor; 8 | import org.springframework.lang.NonNull; 9 | import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbAttribute; 10 | import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; 11 | import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; 12 | 13 | @Data 14 | @Builder 15 | @EqualsAndHashCode 16 | @NoArgsConstructor 17 | @AllArgsConstructor 18 | @DynamoDbBean 19 | public class Shipment { 20 | private String shipmentId; 21 | private Participant recipient; 22 | private Participant sender; 23 | private Double weight; 24 | private String imageLink; 25 | 26 | 27 | @DynamoDbPartitionKey 28 | public String getShipmentId() { 29 | return shipmentId; 30 | } 31 | 32 | public void setShipmentId(String shipmentId) { 33 | this.shipmentId = shipmentId; 34 | } 35 | 36 | @NonNull 37 | @DynamoDbAttribute("recipient") 38 | public Participant getRecipient() { 39 | return recipient; 40 | } 41 | 42 | public void setRecipient(@NonNull Participant recipient) { 43 | this.recipient = recipient; 44 | } 45 | 46 | @NonNull 47 | @DynamoDbAttribute("sender") 48 | public Participant getSender() { 49 | return sender; 50 | } 51 | 52 | public void setSender(@NonNull Participant sender) { 53 | this.sender = sender; 54 | } 55 | 56 | @DynamoDbAttribute("weight") 57 | public Double getWeight() { 58 | return weight; 59 | } 60 | 61 | public void setWeight(Double weight) { 62 | this.weight = weight; 63 | } 64 | 65 | @DynamoDbAttribute("imageLink") 66 | 67 | public String getImageLink() { 68 | return imageLink; 69 | } 70 | 71 | public void setImageLink(String imageLink) { 72 | this.imageLink = imageLink; 73 | } 74 | } -------------------------------------------------------------------------------- /src/main/java/dev/ancaghenade/shipmentlistdemo/repository/DynamoDBService.java: -------------------------------------------------------------------------------- 1 | package dev.ancaghenade.shipmentlistdemo.repository; 2 | 3 | import dev.ancaghenade.shipmentlistdemo.entity.Shipment; 4 | import java.util.List; 5 | import java.util.Objects; 6 | import java.util.Optional; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.stereotype.Repository; 9 | import software.amazon.awssdk.core.pagination.sync.SdkIterable; 10 | import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; 11 | import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; 12 | import software.amazon.awssdk.enhanced.dynamodb.Key; 13 | import software.amazon.awssdk.enhanced.dynamodb.model.ScanEnhancedRequest; 14 | 15 | @Repository 16 | public class DynamoDBService { 17 | 18 | private final DynamoDbTable shipmentTable; 19 | 20 | @Autowired 21 | public DynamoDBService(DynamoDbEnhancedClient dynamoDbClient, 22 | DynamoDbTable shipmentTable) { 23 | this.shipmentTable = shipmentTable; 24 | } 25 | 26 | public Shipment upsert(Shipment shipment) { 27 | if (Objects.isNull(shipment.getShipmentId())) { 28 | shipmentTable.putItem(shipment); 29 | } else { 30 | shipmentTable.updateItem(shipment); 31 | } 32 | return shipment; 33 | } 34 | 35 | public Optional getShipment(String shipmentId) { 36 | return Optional.ofNullable( 37 | shipmentTable.getItem(Key.builder().partitionValue(shipmentId).build())); 38 | } 39 | 40 | public String delete(String shipmentId) { 41 | shipmentTable.deleteItem(Key.builder().partitionValue(shipmentId).build()); 42 | 43 | return "Shipment has been deleted"; 44 | } 45 | 46 | public List getAllShipments() { 47 | ScanEnhancedRequest request = ScanEnhancedRequest.builder().build(); 48 | SdkIterable shipments = shipmentTable.scan(request).items(); 49 | return shipments.stream().toList(); 50 | } 51 | 52 | public void removeImageLink(String shipmentId) { 53 | Optional.ofNullable(shipmentTable.getItem(Key.builder().partitionValue(shipmentId).build())) 54 | .ifPresent(shipment -> shipment.setImageLink(null)); 55 | } 56 | 57 | public void updateImageLink(String shipmentId, String message) { 58 | Optional.ofNullable(shipmentTable.getItem(Key.builder().partitionValue(shipmentId).build())) 59 | .ifPresent(shipment -> { 60 | shipment.setImageLink(message); 61 | shipmentTable.updateItem(shipment); 62 | }); 63 | 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/dev/ancaghenade/shipmentlistdemo/repository/S3StorageService.java: -------------------------------------------------------------------------------- 1 | package dev.ancaghenade.shipmentlistdemo.repository; 2 | 3 | import dev.ancaghenade.shipmentlistdemo.buckets.BucketName; 4 | import dev.ancaghenade.shipmentlistdemo.util.FileUtil; 5 | import java.io.IOException; 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.stereotype.Service; 12 | import org.springframework.web.multipart.MultipartFile; 13 | import software.amazon.awssdk.core.exception.SdkException; 14 | import software.amazon.awssdk.core.sync.RequestBody; 15 | import software.amazon.awssdk.services.s3.S3Client; 16 | import software.amazon.awssdk.services.s3.model.DeleteObjectsRequest; 17 | import software.amazon.awssdk.services.s3.model.DeleteObjectsResponse; 18 | import software.amazon.awssdk.services.s3.model.GetObjectRequest; 19 | import software.amazon.awssdk.services.s3.model.NoSuchKeyException; 20 | import software.amazon.awssdk.services.s3.model.ObjectIdentifier; 21 | import software.amazon.awssdk.services.s3.model.PutObjectRequest; 22 | import software.amazon.awssdk.services.s3.model.S3Error; 23 | import software.amazon.awssdk.services.s3.model.S3Object; 24 | 25 | @Service 26 | public class S3StorageService { 27 | 28 | private final S3Client s3; 29 | private static final Logger LOGGER = LoggerFactory.getLogger(S3StorageService.class); 30 | 31 | private final BucketName bucketName; 32 | @Autowired 33 | public S3StorageService(S3Client s3, BucketName bucketName) { 34 | this.s3 = s3; 35 | this.bucketName = bucketName; 36 | } 37 | 38 | public void save(String path, String fileName, 39 | MultipartFile multipartFile) 40 | throws IOException { 41 | PutObjectRequest putObjectRequest = PutObjectRequest.builder() 42 | .bucket(bucketName.getShipmentPictureBucket()) 43 | .key(path + "/" + fileName) 44 | .contentType(multipartFile.getContentType()) 45 | .contentLength(multipartFile.getSize()) 46 | .build(); 47 | 48 | s3.putObject(putObjectRequest, 49 | RequestBody.fromFile(FileUtil.convertMultipartFileToFile(multipartFile))); 50 | 51 | } 52 | 53 | public byte[] download(String key) throws IOException { 54 | GetObjectRequest getObjectRequest = GetObjectRequest.builder() 55 | .bucket(bucketName.getShipmentPictureBucket()) 56 | .key(key) 57 | .build(); 58 | byte[] object = new byte[0]; 59 | try { 60 | object = s3.getObject(getObjectRequest).readAllBytes(); 61 | } catch (NoSuchKeyException noSuchKeyException) { 62 | LOGGER.warn(String.format("Could not find object: %s", noSuchKeyException.getMessage())); 63 | } 64 | return object; 65 | } 66 | 67 | public void delete(String folderPrefix) { 68 | List keysToDelete = new ArrayList<>(); 69 | s3.listObjectsV2Paginator( 70 | builder -> builder.bucket(bucketName.getShipmentPictureBucket()) 71 | .prefix(folderPrefix + "/")) 72 | .contents().stream() 73 | .map(S3Object::key) 74 | .forEach(key -> keysToDelete.add(ObjectIdentifier.builder().key(key).build())); 75 | 76 | DeleteObjectsRequest deleteRequest = DeleteObjectsRequest.builder() 77 | .bucket(bucketName.getShipmentPictureBucket()) 78 | .delete(builder -> builder.objects(keysToDelete).build()) 79 | .build(); 80 | 81 | try { 82 | DeleteObjectsResponse response = s3.deleteObjects(deleteRequest); 83 | List errors = response.errors(); 84 | if (!errors.isEmpty()) { 85 | LOGGER.error("Errors occurred while deleting objects:"); 86 | errors.forEach(error -> System.out.println("Object: " + error.key() + 87 | ", Error Code: " + error.code() + 88 | ", Error Message: " + error.message())); 89 | } else { 90 | LOGGER.info("Objects deleted successfully."); 91 | } 92 | } catch (SdkException e) { 93 | LOGGER.error("Error occurred during object deletion: " + e.getMessage()); 94 | } 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/dev/ancaghenade/shipmentlistdemo/service/ShipmentService.java: -------------------------------------------------------------------------------- 1 | package dev.ancaghenade.shipmentlistdemo.service; 2 | 3 | import static java.lang.String.format; 4 | 5 | import dev.ancaghenade.shipmentlistdemo.entity.Shipment; 6 | import dev.ancaghenade.shipmentlistdemo.repository.DynamoDBService; 7 | import dev.ancaghenade.shipmentlistdemo.repository.S3StorageService; 8 | import java.io.File; 9 | import java.io.IOException; 10 | import java.nio.file.Files; 11 | import java.util.List; 12 | import java.util.Optional; 13 | import java.util.UUID; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.stereotype.Service; 16 | import org.springframework.web.multipart.MultipartFile; 17 | 18 | @Service 19 | public class ShipmentService { 20 | 21 | 22 | private final DynamoDBService dynamoDBService; 23 | private final S3StorageService s3StorageService; 24 | 25 | 26 | @Autowired 27 | public ShipmentService(DynamoDBService dynamoDBService, S3StorageService s3StorageService) { 28 | this.dynamoDBService = dynamoDBService; 29 | this.s3StorageService = s3StorageService; 30 | } 31 | 32 | public List getAllShipments() { 33 | return dynamoDBService.getAllShipments(); 34 | } 35 | 36 | public String deleteShipment(String shipmentId) { 37 | s3StorageService.delete(shipmentId); 38 | return dynamoDBService.delete(shipmentId); 39 | } 40 | 41 | public Shipment saveShipment(Shipment shipment) { 42 | return dynamoDBService.upsert(shipment); 43 | } 44 | 45 | public void removeImageLink(String shipmentId) { 46 | dynamoDBService.removeImageLink(shipmentId); 47 | } 48 | 49 | public void uploadShipmentImage(String shipmentId, MultipartFile file) { 50 | 51 | checkIfFileIsEmpty(file); 52 | 53 | Shipment shipment = getShipment(shipmentId); 54 | 55 | String path = shipment.getShipmentId(); 56 | 57 | String fileName = format("%s-%s", UUID.randomUUID(), file.getOriginalFilename()); 58 | try { 59 | s3StorageService.save(path, fileName, file); 60 | } catch (IOException e) { 61 | throw new IllegalStateException(e); 62 | } 63 | shipment.setImageLink(format("%s/%s", path, fileName)); 64 | dynamoDBService.upsert(shipment); 65 | } 66 | 67 | 68 | public byte[] downloadShipmentImage(String shipmentId) throws IllegalStateException { 69 | Shipment shipment = dynamoDBService.getShipment(shipmentId).stream() 70 | .findFirst() 71 | .orElseThrow( 72 | () -> new IllegalStateException(format("Shipment %s was not found.", shipmentId))); 73 | 74 | try { 75 | return Optional.ofNullable(shipment.getImageLink()) 76 | .map(link -> { 77 | try { 78 | return s3StorageService.download(link); 79 | } catch (IOException e) { 80 | throw new RuntimeException(e); 81 | } 82 | }) 83 | .orElse(Files.readAllBytes(new File("src/main/resources/placeholder.jpg").toPath())); 84 | } catch (IOException e) { 85 | throw new RuntimeException(e); 86 | } 87 | } 88 | 89 | 90 | private Shipment getShipment(String shipmentId) { 91 | return dynamoDBService.getShipment(shipmentId).stream() 92 | .findFirst() 93 | .orElseThrow( 94 | () -> new IllegalStateException(format("Shipment %s was not found.", shipmentId))); 95 | } 96 | 97 | private void checkIfFileIsEmpty(MultipartFile file) { 98 | if (file.isEmpty()) { 99 | throw new IllegalStateException( 100 | "Cannot save empty file to S3. File size: [" + file.getSize() + "]"); 101 | } 102 | } 103 | 104 | 105 | public void updateImageLink(String shipmentId, String imageLink) { 106 | dynamoDBService.updateImageLink(shipmentId, imageLink); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/dev/ancaghenade/shipmentlistdemo/util/FileUtil.java: -------------------------------------------------------------------------------- 1 | package dev.ancaghenade.shipmentlistdemo.util; 2 | 3 | import java.io.File; 4 | import java.io.FileOutputStream; 5 | import java.io.IOException; 6 | import org.springframework.web.multipart.MultipartFile; 7 | 8 | public class FileUtil { 9 | 10 | public static File convertMultipartFileToFile(MultipartFile multipartFile) throws IOException { 11 | File file = File.createTempFile("temp", null); 12 | try (FileOutputStream fos = new FileOutputStream(file)) { 13 | fos.write(multipartFile.getBytes()); 14 | } 15 | return file; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/dev/ancaghenade/shipmentlistdemo/util/ResourceReader.java: -------------------------------------------------------------------------------- 1 | package dev.ancaghenade.shipmentlistdemo.util; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStreamReader; 5 | import java.io.Reader; 6 | import java.nio.charset.StandardCharsets; 7 | import org.springframework.core.io.DefaultResourceLoader; 8 | import org.springframework.core.io.Resource; 9 | import org.springframework.core.io.ResourceLoader; 10 | import org.springframework.util.FileCopyUtils; 11 | 12 | public class ResourceReader { 13 | 14 | private ResourceReader() { 15 | throw new IllegalStateException("Utility class"); 16 | } 17 | 18 | private static String asString(Resource resource) throws IOException { 19 | try (Reader reader = new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8)) { 20 | return FileCopyUtils.copyToString(reader); 21 | } 22 | } 23 | 24 | public static String readFileToString(String path) throws IOException { 25 | ResourceLoader resourceLoader = new DefaultResourceLoader(); 26 | Resource resource = resourceLoader.getResource(path); 27 | return asString(resource); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/resources/application-dev.yml: -------------------------------------------------------------------------------- 1 | aws: 2 | credentials: 3 | access-key: test_access_key 4 | secret-key: test_secret_access_key 5 | s3: 6 | endpoint: http://s3.localhost.localstack.cloud:4566/ 7 | dynamodb: 8 | endpoint: http://localhost.localstack.cloud:4566/ 9 | sqs: 10 | endpoint: http://localhost:4566/000000000000 11 | region: us-east-1 -------------------------------------------------------------------------------- /src/main/resources/application-prod.yml: -------------------------------------------------------------------------------- 1 | aws: 2 | credentials: 3 | access-key: ${AWS_ACCESS_KEY_ID} 4 | secret-key: ${AWS_SECRET_ACCESS_KEY} 5 | dynamodb: 6 | endpoint: https://dynamodb.eu-central-1.amazonaws.com 7 | s3: 8 | endpoint: https://s3.eu-central-1.amazonaws.com 9 | sqs: 10 | endpoint: https://sqs.eu-central-1.amazonaws.com 11 | region: us-east-1 -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8081 3 | 4 | # limit size of received files 5 | spring: 6 | servlet: 7 | multipart: 8 | enabled: true 9 | max-file-size: 100MB 10 | max-request-size: 100MB 11 | 12 | # log everything 13 | logging: 14 | level: 15 | root=debug: 16 | 17 | -------------------------------------------------------------------------------- /src/main/resources/buckets.properties: -------------------------------------------------------------------------------- 1 | shipment-picture-bucket=shipment-picture-bucket-concise-malamute 2 | shipment-picture-bucket-validator=shipment-picture-lambda-validator-bucket-concise-malamute 3 | -------------------------------------------------------------------------------- /src/main/resources/placeholder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinyg210/shipment-list-demo/984381c916dbde13053cc53935bcf98724240053/src/main/resources/placeholder.jpg -------------------------------------------------------------------------------- /src/main/shipment-list-frontend/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 13 | 14 | The page will reload when you make changes.\ 15 | You may also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!** 35 | 36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. 39 | 40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /src/main/shipment-list-frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shipment-list-frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@stomp/stompjs": "^7.0.0", 7 | "@testing-library/jest-dom": "^5.16.5", 8 | "@testing-library/react": "^13.4.0", 9 | "@testing-library/user-event": "^13.5.0", 10 | "axios": "^1.2.2", 11 | "react": "^18.2.0", 12 | "react-dom": "^18.2.0", 13 | "react-dropzone": "^14.2.3", 14 | "react-scripts": "^5.0.1", 15 | "react-stomp-hooks": "^2.1.0", 16 | "web-vitals": "^2.1.4", 17 | "websocket": "^1.0.34" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test", 23 | "eject": "react-scripts eject" 24 | }, 25 | "eslintConfig": { 26 | "extends": [ 27 | "react-app", 28 | "react-app/jest" 29 | ] 30 | }, 31 | "browserslist": { 32 | "production": [ 33 | ">0.2%", 34 | "not dead", 35 | "not op_mini all" 36 | ], 37 | "development": [ 38 | "last 1 chrome version", 39 | "last 1 firefox version", 40 | "last 1 safari version" 41 | ] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/shipment-list-frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinyg210/shipment-list-demo/984381c916dbde13053cc53935bcf98724240053/src/main/shipment-list-frontend/public/favicon.ico -------------------------------------------------------------------------------- /src/main/shipment-list-frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Shipment List 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/main/shipment-list-frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinyg210/shipment-list-demo/984381c916dbde13053cc53935bcf98724240053/src/main/shipment-list-frontend/public/logo192.png -------------------------------------------------------------------------------- /src/main/shipment-list-frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinyg210/shipment-list-demo/984381c916dbde13053cc53935bcf98724240053/src/main/shipment-list-frontend/public/logo512.png -------------------------------------------------------------------------------- /src/main/shipment-list-frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Shipment List", 3 | "name": "Shipment List", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/main/shipment-list-frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/main/shipment-list-frontend/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | img { 32 | width: 300px; 33 | height: 300px; 34 | object-fit: cover; 35 | border-radius: 1%; 36 | } 37 | 38 | 39 | @keyframes App-logo-spin { 40 | from { 41 | transform: rotate(0deg); 42 | } 43 | to { 44 | transform: rotate(360deg); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/shipment-list-frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import React, {useCallback, useEffect, useState} from "react"; 2 | import {useDropzone} from 'react-dropzone'; 3 | import axios from "axios"; 4 | import './App.css'; 5 | import SSEManager from './SSEManager'; 6 | import placeholder from './placeholder.jpg'; 7 | 8 | const Shipments = () => { 9 | const [shipments, setShipments] = useState([]); 10 | const [isFetchingComplete, setIsFetchingComplete] = useState(false); 11 | const [refreshKey, setRefreshKey] = useState(0); // hack0: Add refreshKey state to force refresh 12 | 13 | const fetchShipments = () => { 14 | axios.get("http://localhost:8081/api/shipment").then(res => { 15 | console.log(res); 16 | setShipments(res.data) 17 | }).then(() => { 18 | setIsFetchingComplete(true); 19 | setRefreshKey((prevKey) => prevKey + 1); // Update refreshKey 20 | 21 | }) 22 | .catch((error) => { 23 | console.log(error); 24 | }); 25 | } 26 | useEffect(() => { 27 | fetchShipments(); 28 | }, [isFetchingComplete]); 29 | 30 | function handleRemove(shipmentId) { 31 | axios.delete(`http://localhost:8081/api/shipment/${shipmentId}`) 32 | .then(res => { 33 | console.log(res.data) 34 | const newList = shipments.filter( 35 | (shipment) => shipment.shipmentId !== shipmentId); 36 | setShipments(newList); 37 | }).catch(err => { 38 | console.log(err) 39 | }); 40 | } 41 | 42 | const refreshShipmentPicture = (shipmentId) => { 43 | if(shipments.some((shp) => shp.shipmentId === shipmentId)) { 44 | setShipments(shipments); 45 | setRefreshKey((prevKey) => prevKey + 1); // Update refreshKey 46 | } 47 | } 48 | 49 | const handleSSEEvent = (data) => { 50 | if (isFetchingComplete) { 51 | refreshShipmentPicture(data); 52 | console.log("Message: " + data); 53 | } 54 | } 55 | 56 | const handleSSEError = (event) => { 57 | console.log("On error handler: " + event.target.readyState); 58 | if (event.target.readyState === EventSource.CLOSED) { 59 | console.log('eventsource closed (' + event.target.readyState + ')') 60 | } 61 | } 62 | 63 | return ( 64 |
65 | 66 | 67 | {shipments.map((shipment, index) => ( 68 |
73 |
74 | 75 | {placeholder} 78 | 79 |
80 | 81 |
87 |
88 |
89 | 90 |

Shipment ID: {shipment.shipmentId}

91 |

From: {shipment.sender.name}

92 |

Address: {shipment.sender.address.postalCode} {shipment.sender.address.street} {shipment.sender.address.number} {shipment.sender.address.city}

93 |

To: {shipment.recipient.name}

94 |

Address: {shipment.recipient.address.postalCode} {shipment.recipient.address.street} {shipment.recipient.address.number} {shipment.recipient.address.city}

95 |

Weight: {shipment.weight}

96 | 100 | 101 |
102 |
103 |
104 | )) 105 | } 106 |
); 107 | } 108 | 109 | function Dropzone( 110 | { 111 | shipmentId 112 | } 113 | ) { 114 | const onDrop = useCallback(acceptedFiles => { 115 | const file = acceptedFiles[0]; 116 | console.log(file); 117 | const formData = new FormData(); 118 | formData.append("file", file); 119 | 120 | axios.post( 121 | `http://localhost:8081/api/shipment/${shipmentId}/image/upload`, 122 | formData, 123 | { 124 | headers: { 125 | "Content-Type": "multipart/form-data" 126 | } 127 | }).then(() => { 128 | console.log("File upload succeeded.") 129 | }).catch(err => { 130 | console.log(err) 131 | }); 132 | }, []) 133 | const {getRootProps, getInputProps, isDragActive} = useDropzone({onDrop}) 134 | 135 | return ( 136 |
137 | 138 | { 139 | isDragActive ? 140 | (

Drop the image here ...

) : 141 | ( 142 |
143 |

Size (Banana for scale):

144 |
click to add new image
145 |
) 146 | 147 | } 148 |
149 | ) 150 | } 151 | 152 | function App() { 153 | return ( 154 |
157 |

Shipments you can see and edit

158 | 159 |
160 | ); 161 | } 162 | 163 | export default App; 164 | -------------------------------------------------------------------------------- /src/main/shipment-list-frontend/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /src/main/shipment-list-frontend/src/SSEManager.js: -------------------------------------------------------------------------------- 1 | import {useEffect, useRef} from "react"; 2 | 3 | const SSEManager = ({ onEvent, onError }) => { 4 | const eventSourceRef = useRef(null); 5 | 6 | useEffect(() => { 7 | // Create the SSE connection 8 | eventSourceRef.current = new EventSource('http://localhost:8081/push-endpoint'); 9 | 10 | // Event listener for SSE messages 11 | eventSourceRef.current.onmessage = (event) => { 12 | onEvent(event.data); 13 | }; 14 | 15 | // Error listener for SSE connection errors 16 | eventSourceRef.current.onerror = (error) => { 17 | onError(error); 18 | }; 19 | 20 | // Clean up the SSE connection on component unmount 21 | return () => { 22 | if (eventSourceRef.current) { 23 | eventSourceRef.current.close(); 24 | } 25 | }; 26 | }, [onEvent, onError]); 27 | 28 | return null; 29 | }; 30 | 31 | export default SSEManager; -------------------------------------------------------------------------------- /src/main/shipment-list-frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | margin-top: 100px; 4 | 5 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 6 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 7 | sans-serif; 8 | -webkit-font-smoothing: antialiased; 9 | -moz-osx-font-smoothing: grayscale; 10 | background-color: #eaeaea; 11 | } 12 | 13 | code { 14 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 15 | monospace; 16 | } 17 | body { 18 | font-family: 'Open Sans', sans-serif; 19 | background-color: #f5f5f5; 20 | 21 | } 22 | 23 | /* Headings */ 24 | h1, h2, h3 { 25 | font-family: 'Montserrat', sans-serif; 26 | font-weight: 600; 27 | } 28 | 29 | h1 { 30 | font-size: 36px; 31 | margin-bottom: 24px; 32 | } 33 | 34 | h2 { 35 | font-size: 30px; 36 | margin-bottom: 18px; 37 | } 38 | 39 | h3 { 40 | font-size: 24px; 41 | margin-bottom: 12px; 42 | } 43 | 44 | .div-main { 45 | display: flex; 46 | align-items: center; /* This centers the items vertically */ 47 | justify-content: center; 48 | } 49 | 50 | /* Paragraphs */ 51 | p { 52 | font-size: 16px; 53 | line-height: 1.5; 54 | margin-bottom: 18px; 55 | } 56 | 57 | 58 | .btn { 59 | font-family: 'Montserrat', sans-serif; 60 | font-weight: 600; 61 | border-radius: 50px; /* This line changes the button corners to round */ 62 | padding: 12px 24px; 63 | transition: background-color 0.3s ease; 64 | text-transform: uppercase; 65 | background-color: #e5e023; 66 | color: black; 67 | } 68 | 69 | .btn-primary { 70 | background-color: #4caf50; 71 | color: #ffffff; 72 | } 73 | 74 | .btn:hover { 75 | background-color: #e72525; 76 | } 77 | -------------------------------------------------------------------------------- /src/main/shipment-list-frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | const root = ReactDOM.createRoot(document.getElementById('root')); 8 | root.render( 9 | 10 | 11 | 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals(); 18 | -------------------------------------------------------------------------------- /src/main/shipment-list-frontend/src/placeholder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinyg210/shipment-list-demo/984381c916dbde13053cc53935bcf98724240053/src/main/shipment-list-frontend/src/placeholder.jpg -------------------------------------------------------------------------------- /src/main/shipment-list-frontend/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /src/main/shipment-list-frontend/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/test/java/dev/ancaghenade/shipmentlistdemo/ShipmentListDemoApplicationTests.java: -------------------------------------------------------------------------------- 1 | package dev.ancaghenade.shipmentlistdemo; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class ShipmentListDemoApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /terraform/cleanup.sh: -------------------------------------------------------------------------------- 1 | rm .terraform.lock.hcl 2 | rm -rf .terraform 3 | rm terraform.tfstate 4 | rm terraform.tfstate.backup -------------------------------------------------------------------------------- /terraform/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "shipment1": { 3 | "shipmentId": { 4 | "S": "dc3b6668-45ba-4c10-9860-95bbffaebfc1" 5 | }, 6 | "recipient": { 7 | "M": { 8 | "name": { 9 | "S": "Harry Potter" 10 | }, 11 | "address": { 12 | "M": { 13 | "postalCode": { 14 | "S": "LNDNGB" 15 | }, 16 | "street": { 17 | "S": "Privet Drive" 18 | }, 19 | "number": { 20 | "S": "4" 21 | }, 22 | "city": { 23 | "S": "Little Whinging" 24 | }, 25 | "additionalInfo": { 26 | "S": "" 27 | } 28 | } 29 | } 30 | } 31 | }, 32 | "sender": { 33 | "M": { 34 | "name": { 35 | "S": "Hermione Granger" 36 | }, 37 | "address": { 38 | "M": { 39 | "postalCode": { 40 | "S": "OXFGB" 41 | }, 42 | "street": { 43 | "S": "Grimmauld Place" 44 | }, 45 | "number": { 46 | "S": "12" 47 | }, 48 | "city": { 49 | "S": "London" 50 | }, 51 | "additionalInfo": { 52 | "S": "" 53 | } 54 | } 55 | } 56 | } 57 | }, 58 | "weight": { 59 | "N": "1.0" 60 | }, 61 | "imageLink": { 62 | "NULL": true 63 | } 64 | }, 65 | "shipment2": { 66 | "shipmentId": { 67 | "S": "f7fc2d00-5cd9-4749-b5ac-10a6f7ac0310" 68 | }, 69 | "recipient": { 70 | "M": { 71 | "name": { 72 | "S": "Walter White" 73 | }, 74 | "address": { 75 | "M": { 76 | "postalCode": { 77 | "S": "ALBQNM" 78 | }, 79 | "street": { 80 | "S": "Negra Arroyo Lane" 81 | }, 82 | "number": { 83 | "S": "308" 84 | }, 85 | "city": { 86 | "S": "Albuquerque" 87 | }, 88 | "additionalInfo": { 89 | "S": "RV storage" 90 | } 91 | } 92 | } 93 | } 94 | }, 95 | "sender": { 96 | "M": { 97 | "name": { 98 | "S": "Tony Stark" 99 | }, 100 | "address": { 101 | "M": { 102 | "postalCode": { 103 | "S": "NYCNY" 104 | }, 105 | "street": { 106 | "S": "Avenue of the Americas" 107 | }, 108 | "number": { 109 | "S": "64" 110 | }, 111 | "city": { 112 | "S": "New York City" 113 | }, 114 | "additionalInfo": { 115 | "S": "Stark Tower" 116 | } 117 | } 118 | } 119 | } 120 | }, 121 | "weight": { 122 | "N": "0.7" 123 | }, 124 | "imageLink": { 125 | "NULL": true 126 | } 127 | }, 128 | "shipment3": { 129 | "shipmentId": { 130 | "S": "3317ac4f-1f9b-4bab-a974-4aadf79d7da5" 131 | }, 132 | "recipient": { 133 | "M": { 134 | "name": { 135 | "S": "Buddy The Elf" 136 | }, 137 | "address": { 138 | "M": { 139 | "postalCode": { 140 | "S": "938746" 141 | }, 142 | "street": { 143 | "S": "Candy Cane Lane" 144 | }, 145 | "number": { 146 | "S": "1" 147 | }, 148 | "city": { 149 | "S": "North Pole" 150 | }, 151 | "additionalInfo": { 152 | "S": "Santa's Workshop" 153 | } 154 | } 155 | } 156 | } 157 | }, 158 | "sender": { 159 | "M": { 160 | "name": { 161 | "S": "The Grinch" 162 | }, 163 | "address": { 164 | "M": { 165 | "postalCode": { 166 | "S": "69869" 167 | }, 168 | "street": { 169 | "S": "Mount Crumpit" 170 | }, 171 | "number": { 172 | "S": "666" 173 | }, 174 | "city": { 175 | "S": "Whoville" 176 | }, 177 | "additionalInfo": { 178 | "S": "Cave" 179 | } 180 | } 181 | } 182 | } 183 | }, 184 | "weight": { 185 | "N": "9.0" 186 | }, 187 | "imageLink": { 188 | "NULL": true 189 | } 190 | }, 191 | "shipment4": { 192 | "shipmentId": { 193 | "S": "26c09286-a00c-11ed-a8fc-0242ac120002" 194 | }, 195 | "recipient": { 196 | "M": { 197 | "name": { 198 | "S": "Home Sweet Home" 199 | }, 200 | "address": { 201 | "M": { 202 | "postalCode": { 203 | "S": "98653" 204 | }, 205 | "street": { 206 | "S": "47th Street" 207 | }, 208 | "number": { 209 | "S": "4" 210 | }, 211 | "city": { 212 | "S": "Springfield" 213 | }, 214 | "additionalInfo": { 215 | "S": "" 216 | } 217 | } 218 | } 219 | } 220 | }, 221 | "sender": { 222 | "M": { 223 | "name": { 224 | "S": "Warehouse of Unicorns" 225 | }, 226 | "address": { 227 | "M": { 228 | "postalCode": { 229 | "S": "98653" 230 | }, 231 | "street": { 232 | "S": "47th Street" 233 | }, 234 | "number": { 235 | "S": "4" 236 | }, 237 | "city": { 238 | "S": "Townsville" 239 | }, 240 | "additionalInfo": { 241 | "S": "" 242 | } 243 | } 244 | } 245 | } 246 | }, 247 | "weight": { 248 | "N": "2.3" 249 | }, 250 | "imageLink": { 251 | "NULL": true 252 | } 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /terraform/locals.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | json_data = file("./data.json") 3 | tf_data = jsondecode(local.json_data) 4 | } -------------------------------------------------------------------------------- /terraform/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "= 4.66.1" 6 | } 7 | } 8 | } 9 | provider "aws" { 10 | region = "us-east-1" 11 | } 12 | 13 | provider "random" { 14 | version = "3.1.0" 15 | } 16 | 17 | resource "random_pet" "random_name" { 18 | length = 2 19 | separator = "-" 20 | } 21 | 22 | # S3 bucket 23 | resource "aws_s3_bucket" "shipment_picture_bucket" { 24 | bucket = "shipment-picture-bucket-${random_pet.random_name.id}" 25 | force_destroy = true 26 | lifecycle { 27 | prevent_destroy = false 28 | } 29 | } 30 | 31 | # DynamoDB table creation 32 | resource "aws_dynamodb_table" "shipment" { 33 | name = "shipment" 34 | read_capacity = 10 35 | write_capacity = 5 36 | 37 | attribute { 38 | name = "shipmentId" 39 | type = "S" 40 | } 41 | hash_key = "shipmentId" 42 | server_side_encryption { 43 | enabled = true 44 | } 45 | 46 | stream_enabled = true 47 | stream_view_type = "NEW_AND_OLD_IMAGES" 48 | } 49 | 50 | # Populate the table 51 | resource "aws_dynamodb_table_item" "shipment" { 52 | for_each = local.tf_data 53 | table_name = aws_dynamodb_table.shipment.name 54 | hash_key = "shipmentId" 55 | item = jsonencode(each.value) 56 | } 57 | 58 | # Define a bucket for the lambda zip 59 | resource "aws_s3_bucket" "lambda_code_bucket" { 60 | bucket = "shipment-picture-lambda-validator-bucket-${random_pet.random_name.id}" 61 | force_destroy = true 62 | lifecycle { 63 | prevent_destroy = false 64 | } 65 | } 66 | 67 | # Lambda source code 68 | resource "aws_s3_bucket_object" "lambda_code" { 69 | source = "../shipment-picture-lambda-validator/target/shipment-picture-lambda-validator.jar" 70 | bucket = aws_s3_bucket.lambda_code_bucket.id 71 | key = "shipment-picture-lambda-validator.jar" 72 | } 73 | 74 | # Lambda definition 75 | resource "aws_lambda_function" "shipment_picture_lambda_validator" { 76 | function_name = "shipment-picture-lambda-validator" 77 | handler = "dev.ancaghenade.shipmentpicturelambdavalidator.ServiceHandler::handleRequest" 78 | runtime = "java11" 79 | role = aws_iam_role.lambda_exec.arn 80 | s3_bucket = aws_s3_bucket.lambda_code_bucket.id 81 | s3_key = aws_s3_bucket_object.lambda_code.key 82 | memory_size = 512 83 | timeout = 60 84 | environment { 85 | variables = { 86 | BUCKET = aws_s3_bucket.shipment_picture_bucket.bucket 87 | } 88 | } 89 | } 90 | 91 | # Define trigger for S3 92 | resource "aws_s3_bucket_notification" "demo_bucket_notification" { 93 | bucket = aws_s3_bucket.shipment_picture_bucket.id 94 | lambda_function { 95 | lambda_function_arn = aws_lambda_function.shipment_picture_lambda_validator.arn 96 | events = ["s3:ObjectCreated:*"] 97 | } 98 | } 99 | 100 | # Give Lambda permission to call S3 101 | resource "aws_lambda_permission" "s3_lambda_exec_permission" { 102 | statement_id = "AllowExecutionFromS3Bucket" 103 | action = "lambda:InvokeFunction" 104 | function_name = aws_lambda_function.shipment_picture_lambda_validator.function_name 105 | principal = "s3.amazonaws.com" 106 | source_arn = aws_s3_bucket.shipment_picture_bucket.arn 107 | } 108 | 109 | # Define role to execute Lambda 110 | resource "aws_iam_role" "lambda_exec" { 111 | name = "lambda_exec_role" 112 | 113 | assume_role_policy = <