Skip to content

Latest commit

 

History

History

itemtracker_dynamodb

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 

Create a React and Spring REST application that queries DynamoDB data using the SDK for Kotlin

Overview

Heading Description
Description Discusses how to develop a Spring Boot application that queries Amazon DynamoDB data. The Spring Boot application uses the AWS SDK for Kotlin to invoke AWS services and is used by a React application that displays the data. The React application uses Cloudscape. For information, see Cloudscape.
Audience Developer (intermediate)
Updated 11/14/2023
Required skills Kotlin, Gradle, JavaScript

Purpose

You can develop a dynamic web application that tracks and reports on work items by using the following AWS services:

  • Amazon DynamoDB
  • Amazon Simple Email Service (Amazon SES)

The application you create is a decoupled React application that uses a Spring REST API to return DynamoDB data. That is, the React application interacts with a Spring API by making RESTful GET and POST requests. The Spring API uses a DynamoDbClient object to perform CRUD operations on the DynamoDB database. Then, the Spring REST API returns JSON data in an HTTP response, as shown in the following illustration.

AWS Tracking Application

Topics

  • Prerequisites
  • Understand the AWS Tracker application
  • Create an IntelliJ Kotlin project
  • Add the dependencies to your Gradle build file
  • Create the Kotlin classes
  • Create the React front end

Prerequisites

To complete the tutorial, you need the following:

  • An AWS account.
  • A Kotlin IDE (this tutorial uses the IntelliJ IDE).
  • Java 17 JDK.
  • Gradle 8.1 or higher.
  • You must also set up your development environment. For more information, see Get started with the SDK for Kotlin.

Important

  • The AWS services in this document are included in the AWS Free Tier.
  • This code has not been tested in all AWS Regions. Some AWS services are available only in specific Regions. For more information, see AWS Regional Services.
  • Running this code might result in charges to your AWS account.
  • Be sure to delete all of the resources that you create during this tutorial so that you won't be charged.

Create the DynamoDB table and add some items

Using the AWS Management Console, create an Amazon DynamoDB table named Work with a partition key named id of type String.

After creating the Work table with the id partition key, select the table in the console. Under the Actions menu, select Create item to enter more columns and values (Attributes is the term used with DynamoDB).

Because you're creating an item for the first time, define the attributes in your table and also add values. Enter the attributes and values as shown in the following table. Enter 'Open' as the value for the archive attribute. Select Create item to create your first item (row).

The Work table attributes

Attribute name What the attribute value represents
id The primary key. Enter a random string of text up to 20 characters.
date Date the work item was performed.
description Description of the work being done.
guide Name of the guide the work is for.
status Status of the work, such as 'started' or 'in review'.
username User name who performed the work item.
archive A numeric value of 0 (Open) or 1 (Closed). Indicates whether the item is active or archived.

Enter at least two more items (rows). Because you've already defined all the attributes for this example, you can select the check box for the first item you created. Then, under the Actions menu, select Duplicate item. Select Create item when you're done changing the values.

Duplicate one more item so that you have a total of three items.

The following illustration shows an example of the Work table.

AWS Tracking Application

For more information about how to use the AWS Management Console to create a DynamoDB table
and add data, see Create a table. (The table in that example is different from the table in this example.)

Now that the table is created and populated with some data, there is data to display when starting up the Spring Boot app for the REST API.

Understand the AWS Tracker React application

A user can perform the following tasks using the React application:

  • View all active items
  • View archived items that are complete
  • Add a new item
  • Convert an active item into an archived item
  • Send a report to an email recipient

The React application displays active and archive items. For example, the following illustration shows the React application displaying active data.

AWS Tracking Application

Likewise, the following illustration shows the React application displaying archived data.

AWS Tracking Application

With the React application, a user can convert an active item to an archived item by choosing the Archive item(s) button.

AWS Tracking Application

The React application also lets a user enter a new item.

AWS Tracking Application

The user can enter an email recipient into the text field and choose Send Report.

AWS Tracking Application

The application queries active items from the database and sends the data to the selected email recipient.

Create an IntelliJ project

Perform the following steps.

  1. In the IntelliJ IDE, choose File, New, Project.
  2. In the New Project dialog box, choose Kotlin.
  3. Enter the name ItemTrackerKotlinDynamoDBRest.
  4. Select Gradle Kotlin for the Build System.
  5. Select your JVM option and choose Next.
  6. Choose Finish.

Add the dependencies to your Gradle build file

At this point, you have a new project. Make sure that the build.gradle.kts file looks like the following.

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
kotlin("jvm") version "1.9.0"
application
}

group = "me.scmacdon"
version = "1.0-SNAPSHOT"

java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

buildscript {
repositories {
maven("https://plugins.gradle.org/m2/")
}
dependencies {
classpath("org.jlleitschuh.gradle:ktlint-gradle:10.3.0")
}
}

repositories {
mavenCentral()
}
apply(plugin = "org.jlleitschuh.gradle.ktlint")
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web:2.7.5")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.3")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation("javax.mail:javax.mail-api:1.6.2")
implementation("com.sun.mail:javax.mail:1.6.2")
implementation("aws.sdk.kotlin:dynamodb:0.33.1-beta")
implementation("aws.sdk.kotlin:ses:0.33.1-beta")
implementation("aws.smithy.kotlin:http-client-engine-okhttp:0.28.0")
implementation("aws.smithy.kotlin:http-client-engine-crt:0.28.0")
testImplementation("org.springframework.boot:spring-boot-starter-test:2.7.3")
}

tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "17"
}
}

tasks.withType<Test> {
useJUnitPlatform()
}

Create the Kotlin classes

Create a new package in the main/kotlin folder named com.aws.rest. The following Kotlin classes go into this package.

  • App - Used as the base class and Controller for the Spring Boot application
  • DynamoDBService - Uses the DynamoDbClient object to perform CRUD operations on the database
  • SendMessage - Uses the SesClient object to send email messages
  • WorkItem - Represents the application model

Note: The MessageResource class is located in the App file.

App class

The following Kotlin code represents the App class. This is the entry point into a Spring boot application.

// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package com.aws.rest

import kotlinx.coroutines.runBlocking
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.CrossOrigin
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestController
import java.io.IOException

@SpringBootApplication
open class App

fun main(args: Array<String>) {
    runApplication<App>(*args)
}

@CrossOrigin(origins = ["*"])
@RestController
class MessageResource {

    @Autowired
    private lateinit var dbService: DynamoDBService

    @Autowired
    private lateinit var sendMsg: SendMessage

    // Add a new item to the Amazon DynamoDB database.
    @PostMapping("api/items")
    fun addItems(@RequestBody payLoad: Map<String, Any>): String = runBlocking {
        val nameVal = "user"
        val guideVal = payLoad.get("guide").toString()
        val descriptionVal = payLoad.get("description").toString()
        val statusVal = payLoad.get("status").toString()

        // Create a Work Item object.
        val myWork = WorkItem()
        myWork.guide = guideVal
        myWork.description = descriptionVal
        myWork.status = statusVal
        myWork.name = nameVal
        val id = dbService.putItemInTable(myWork)
        return@runBlocking "Item $id added successfully!"
    }

    // Retrieve items.
    @GetMapping("api/items")
    fun getItems(@RequestParam(required = false) archived: String?): MutableList<WorkItem> = runBlocking {
        val list: MutableList<WorkItem>
        if (archived == "false") {
            list = dbService.getOpenItems(false)
        } else if (archived == "true") {
            list = dbService.getOpenItems(true)
        } else {
            list = dbService.getAllItems()
        }
        return@runBlocking list
    }

    // Flip an item from Active to Archive.
    @PutMapping("api/items/{id}:archive")
    @ResponseStatus(value = HttpStatus.NO_CONTENT)
    fun modUser(@PathVariable id: String) = runBlocking {
        dbService.archiveItemEC(id)
        return@runBlocking
    }

    @PostMapping("api/items:report")
    @ResponseStatus(value = HttpStatus.NO_CONTENT)
    fun sendReport(@RequestBody body: Map<String, String>) = runBlocking {
        val email = body.get("email")
        val xml = dbService.getOpenReport(false)
        try {
            if (email != null) {
                sendMsg.send(email, xml)
            }
        } catch (e: IOException) {
            e.stackTrace
        }
        return@runBlocking
    }
}


DynamoDBService class

The following Kotlin code represents the DynamoDBService class that uses the DynamoDbClient client to perform operations on the Amazon DynamoDB Work table. For example, the getAllItems method returns all items in the Work table. Notice the getOpenItems method uses a filterExpression to query either active or archive items. This represents how you can filter DynamoDB items when using the AWS SDK for Kotlin.

package com.aws.rest
import aws.sdk.kotlin.services.dynamodb.DynamoDbClient
import aws.sdk.kotlin.services.dynamodb.model.AttributeAction
import aws.sdk.kotlin.services.dynamodb.model.AttributeValue
import aws.sdk.kotlin.services.dynamodb.model.AttributeValueUpdate
import aws.sdk.kotlin.services.dynamodb.model.PutItemRequest
import aws.sdk.kotlin.services.dynamodb.model.ScanRequest
import aws.sdk.kotlin.services.dynamodb.model.UpdateItemRequest
import org.springframework.stereotype.Component
import org.w3c.dom.Document
import java.io.StringWriter
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.UUID
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.parsers.ParserConfigurationException
import javax.xml.transform.TransformerException
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult
import kotlin.collections.HashMap

/*
Before running this code example, create a DynamoDB table named Work with a primary key named id.
*/
@Component
class DynamoDBService {

    // Archive an item.
    suspend fun archiveItemEC(id: String) {
        val tableNameVal = "Work"
        val itemKey = mutableMapOf<String, AttributeValue>()
        itemKey["id"] = AttributeValue.S(id)

        val updatedValues = mutableMapOf<String, AttributeValueUpdate>()
        updatedValues["archive"] = AttributeValueUpdate {
            value = AttributeValue.N("1")
            action = AttributeAction.Put
        }

        val request = UpdateItemRequest {
            tableName = tableNameVal
            key = itemKey
            attributeUpdates = updatedValues
        }

        DynamoDbClient { region = "us-east-1" }.use { dynamoDBClient ->
            dynamoDBClient.updateItem(request)
        }
    }

    // Get items from the DynamoDB table.
    suspend fun getOpenItems(myArc: Boolean): MutableList<WorkItem> {
        val tableNameVal = "Work"
        val myList = mutableListOf<WorkItem>()
        val myMap = HashMap<String, String>()
        myMap.put("#archive2", "archive")
        val myExMap = mutableMapOf<String, AttributeValue>()
        if (myArc) {
            myExMap.put(":val", AttributeValue.N("1"))
        } else {
            myExMap.put(":val", AttributeValue.N("0"))
        }

        val scanRequest = ScanRequest {
            expressionAttributeNames = myMap
            expressionAttributeValues = myExMap
            tableName = tableNameVal
            filterExpression = "#archive2 = :val"
        }

        DynamoDbClient { region = "us-east-1" }.use { dynamoDBClient ->
            val response = dynamoDBClient.scan(scanRequest)
            for (item in response.items!!) {
                val keys = item.keys
                val myItem = WorkItem()

                for (key in keys) {
                    when (key) {
                        "date" -> {
                            myItem.date = splitMyString(item[key].toString())
                        }

                        "status" -> {
                            myItem.status = splitMyString(item[key].toString())
                        }

                        "username" -> {
                            myItem.name = "user"
                        }

                        "archive" -> {
                            myItem.arc = splitMyString(item[key].toString())
                        }

                        "description" -> {
                            myItem.description = splitMyString(item[key].toString())
                        }
                        "id" -> {
                            myItem.id = splitMyString(item[key].toString())
                        }
                        else -> {
                            myItem.guide = splitMyString(item[key].toString())
                            myList.add(myItem)
                        }
                    }
                }
            }
            return myList
        }
    }

    // Get items from the DynamoDB table.
    suspend fun getAllItems(): MutableList<WorkItem> {
        val tableNameVal = "Work"
        val myList = mutableListOf<WorkItem>()
        val scanRequest = ScanRequest {
           tableName = tableNameVal
       }

        DynamoDbClient { region = "us-east-1" }.use { dynamoDBClient ->
            val response = dynamoDBClient.scan(scanRequest)
            for (item in response.items!!) {
                val keys = item.keys
                val myItem = WorkItem()

                for (key in keys) {
                    when (key) {
                        "date" -> {
                            myItem.date = splitMyString(item[key].toString())
                        }

                        "status" -> {
                            myItem.status = splitMyString(item[key].toString())
                        }

                        "username" -> {
                            myItem.name = "user"
                        }

                        "archive" -> {
                            myItem.arc = splitMyString(item[key].toString())
                        }

                        "description" -> {
                            myItem.description = splitMyString(item[key].toString())
                        }
                        "id" -> {
                            myItem.id = splitMyString(item[key].toString())
                        }
                        else -> {
                            myItem.guide = splitMyString(item[key].toString())
                            myList.add(myItem)
                        }
                    }
                }
            }
            return myList
        }
    }



    // Get items to go into the email report.
    suspend fun getOpenReport(myArc: Boolean): String? {
        val tableNameVal = "Work"
        val myList = mutableListOf<WorkItem>()
        val myMap = HashMap<String, String>()
        myMap.put("#archive2", "archive")
        val myExMap = mutableMapOf<String, AttributeValue>()
        if (myArc) {
            myExMap.put(":val", AttributeValue.N("1"))
        } else {
            myExMap.put(":val", AttributeValue.N("0"))
        }

        val scanRequest = ScanRequest {
            expressionAttributeNames = myMap
            expressionAttributeValues = myExMap
            tableName = tableNameVal
            filterExpression = "#archive2 = :val"
        }

        DynamoDbClient { region = "us-east-1" }.use { dynamoDBClient ->
            val response = dynamoDBClient.scan(scanRequest)
            for (item in response.items!!) {
                val keys = item.keys
                val myItem = WorkItem()

                for (key in keys) {
                    when (key) {
                        "date" -> {
                            myItem.date = splitMyString(item[key].toString())
                        }

                        "status" -> {
                            myItem.status = splitMyString(item[key].toString())
                        }

                        "username" -> {
                            myItem.name = "user"
                        }

                        "archive" -> {
                            myItem.arc = splitMyString(item[key].toString())
                        }

                        "description" -> {
                            myItem.description = splitMyString(item[key].toString())
                        }
                        "id" -> {
                            myItem.id = splitMyString(item[key].toString())
                        }
                        else -> {
                            myItem.guide = splitMyString(item[key].toString())
                            myList.add(myItem)
                        }
                    }
                }
            }
            return toXml(myList)?.let { convertToString(it) }
        }
    }

    // Put an item into a DynamoDB table.
    suspend fun putItemInTable(itemOb: WorkItem): String {
        val tableNameVal = "Work"

        // Get all the values to store in the DynamoDB table.
        val myGuid = UUID.randomUUID().toString()
        val user = itemOb.name
        val desc = itemOb.description
        val status = itemOb.status
        val guide = itemOb.guide

        val date = Calendar.getInstance().time
        val formatter = SimpleDateFormat.getDateTimeInstance()
        val formatedDate = formatter.format(date)

        // Add the data to the DynamoDB table.
        val itemValues = mutableMapOf<String, AttributeValue>()
        itemValues["id"] = AttributeValue.S(myGuid)
        itemValues["username"] = AttributeValue.S(user.toString())
        itemValues["archive"] = AttributeValue.N("0")
        itemValues["date"] = AttributeValue.S(formatedDate)
        itemValues["description"] = AttributeValue.S(desc.toString())
        itemValues["guide"] = AttributeValue.S(guide.toString())
        itemValues["status"] = AttributeValue.S(status.toString())

        val request = PutItemRequest {
            tableName = tableNameVal
            item = itemValues
        }

        DynamoDbClient { region = "us-east-1" }.use { dynamoDBClient ->
            dynamoDBClient.putItem(request)
            return myGuid
        }
    }
}

// Split the item[key] value.
fun splitMyString(str: String): String {
    val del1 = "="
    val del2 = ")"
    val parts = str.split(del1, del2)
    val myVal = parts[1]
    return myVal
}

// Convert Work item data into XML to pass back to the view.
private fun toXml(itemList: MutableList<WorkItem>): Document? {
    try {
        val factory = DocumentBuilderFactory.newInstance()
        val builder = factory.newDocumentBuilder()
        val doc = builder.newDocument()

        // Start building the XML.
        val root = doc.createElement("Items")
        doc.appendChild(root)

        // Get the elements from the collection.
        val custCount = itemList.size

        // Iterate through the collection.
        for (index in 0 until custCount) {
            // Get the WorkItem object from the collection.
            val myItem = itemList[index]
            val item = doc.createElement("Item")
            root.appendChild(item)

            // Set Id.
            val id = doc.createElement("Id")
            id.appendChild(doc.createTextNode(myItem.id))
            item.appendChild(id)

            // Set Name.
            val name = doc.createElement("Name")
            name.appendChild(doc.createTextNode(myItem.name))
            item.appendChild(name)

            // Set Date.
            val date = doc.createElement("Date")
            date.appendChild(doc.createTextNode(myItem.date))
            item.appendChild(date)

            // Set Description.
            val desc = doc.createElement("Description")
            desc.appendChild(doc.createTextNode(myItem.description))
            item.appendChild(desc)

            // Set Guide.
            val guide = doc.createElement("Guide")
            guide.appendChild(doc.createTextNode(myItem.guide))
            item.appendChild(guide)

            // Set Status.
            val status = doc.createElement("Status")
            status.appendChild(doc.createTextNode(myItem.status))
            item.appendChild(status)
        }
        return doc
    } catch (e: ParserConfigurationException) {
        e.printStackTrace()
    }
    return null
}

private fun convertToString(xml: Document): String? {
    try {
        val transformer = TransformerFactory.newInstance().newTransformer()
        val result = StreamResult(StringWriter())
        val source = DOMSource(xml)
        transformer.transform(source, result)
        return result.writer.toString()
    } catch (ex: TransformerException) {
        ex.printStackTrace()
    }
    return null
}

SendMessage class

The SendMessage class uses the SesClient client to send an email message.

Before you can send the email message, the email address that you're sending it to must be verified. For more information, see Verifying an email address.

The following Kotlin code represents the SendMessage class.

package com.example.demo

import kotlin.system.exitProcess
import aws.sdk.kotlin.services.ses.SesClient
import aws.sdk.kotlin.services.ses.model.SesException
import aws.sdk.kotlin.services.ses.model.Destination
import aws.sdk.kotlin.services.ses.model.Content
import aws.sdk.kotlin.services.ses.model.Body
import aws.sdk.kotlin.services.ses.model.Message
import aws.sdk.kotlin.services.ses.model.SendEmailRequest

class SendMessage {

    suspend fun send(
        recipient: String,
        strValue: String?
    ) {
        val sesClient = SesClient { region = "us-east-1" }
        // The HTML body of the email.
        val bodyHTML = ("<html>" + "<head></head>" + "<body>" + "<h1>Amazon RDS Items!</h1>"
                + "<textarea>$strValue</textarea>" + "</body>" + "</html>")

        val destinationOb = Destination {
            toAddresses = listOf(recipient)
        }

        val contentOb = Content {
            data = bodyHTML
        }

        val subOb = Content {
            data = "Item Report"
        }

        val bodyOb= Body {
            html = contentOb
        }

        val msgOb = Message {
            subject = subOb
            body = bodyOb
        }

        val emailRequest = SendEmailRequest {
            destination = destinationOb
            message = msgOb
            source = "<Enter email>"
        }

        try {
            println("Attempting to send an email through Amazon SES using the AWS SDK for Kotlin...")
            sesClient.sendEmail(emailRequest)

        } catch (e: SesException) {
            println(e.message)
            sesClient.close()
            exitProcess(0)
        }
    }
}

Note: You must update the email sender address with a verified email address. Otherwise, the email is not sent. For more information, see Verifying email addresses in Amazon SES.

WorkItem class

The following Kotlin code represents the WorkItem class.

package com.example.demo

class WorkItem {
    var id: String? = null
    var arc: String? = null
    var name: String? = null
    var guide: String? = null
    var date: String? = null
    var description: String? = null
    var status: String? = null
}

Run the application

Using the IntelliJ IDE, you can run your Spring REST API. The first time you run it, choose the run icon in the main class. The Spring API supports the following URLs.

  • /api/items - A GET request that returns all data items from the Work table
  • /api/items?archived=true - A GET request that returns either active or archive data items from the Work table
  • /api/items/{id}:archive - A PUT request that converts the specified data item to an archived item
  • /api/items - A POST request that adds a new item to the database
  • api/items:report - A POST request that creates a report of active items and emails the report

Note: The React application created in the next section consumes all of the preceding URLs.

Confirm that the Spring REST API works by viewing the Active items. Enter the following URL into a browser.

http://localhost:8080/api/items

The following illustration shows the JSON data returned from the Spring REST API.

AWS Tracking Application

Using cURL Commands

You can also utilize cURL commands to invoke the functionality of this application.

You can retrieve items by executing the following cURL command:

   curl -X GET http://localhost:8080/api/items

Likewise, you can send a report by executing the following cURL command:

   curl -X POST -H "Content-Type: application/json" -d "{\"email\":\"<email address>\"}" http://localhost:8080/api/items:report

Note: Make sure that you specify a valid email address.

Create the React front end

You can create the React application that consumes the JSON data returned from the Spring REST API. To create the React application, download files from the following GitHub repository. Included in this repository are instructions on how to set up the project. To access the GitHub location, see Work item tracker web client.

Update BASE_URL

In the config.json file, you must make sure that the BASE_URL value references your Spring application.

{
  "BASE_URL": "http://localhost:8080/api"
}

Next steps

Congratulations, you have created a decoupled React application that consumes data from a Spring REST API. The Spring REST API uses the AWS SDK for Java (v2) to invoke AWS services. As stated at the beginning of this tutorial, be sure to delete all of the resources that you create during this tutorial so that you won't continue to be charged.