kotlin/usecases/serverless_rds/README.md
| Heading | Description |
|---|---|
| Description | Discusses how to develop a Spring Boot application that queries Amazon Aurora Serverless 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 |
You can develop a dynamic web application that tracks and reports on work items by using the following AWS services:
The application you create is a decoupled React application that uses a Spring REST API to return Aurora Serverless data. That is, the React application interacts with a Spring API by making RESTful GET and POST requests. The Spring API uses an RdsDataClient object to perform CRUD operations on the Aurora Serverless database. Then, the Spring REST API returns JSON data in an HTTP response, as shown in the following illustration.
Note: You can only use the RdsDataClient object for an Aurora Serverless DB cluster or Aurora PostgreSQL. For more information, see Using the Data API for Aurora Serverless.
To complete the tutorial, you need the following:
You can use one of the following ways to create the required AWS resources:
Create an Aurora Serverless database named jobs. Next, create a table named Work that contains the following fields:
The following figure shows the Work table in the Amazon RDS console.
For more information, see Creating an Aurora Serverless v1 DB cluster.
To successfully connect to the database using the RdsDataClient object, set up an AWS Secrets Manager secret to use for authentication. For more information, see Rotate Amazon RDS database credentials automatically with AWS Secrets Manager.
To use the RdsDataClient object, you must have the following two Amazon Resource Name (ARN) values:
Note: You must set up inbound rules for the security group to connect to the database. You can set up an inbound rule for your development environment. Setting up an inbound rule essentially means enabling an IP address to use the database. After you set up the inbound rules, you can connect to the database from the REST endpoint. For information about setting up security group inbound rules, see Controlling access with security groups.
Using the AWS Cloud Development Kit (AWS CDK), you can set up the resources required for this tutorial. For more information, see the AWS CDK instructions.
A user can perform the following tasks using the React application:
The React application displays active and archive items. For example, the following illustration shows the React application displaying active data.
Likewise, the following illustration shows the React application displaying archived data.
With the React application, a user can convert an active item to an archived item by choosing the Archive item(s) button.
The React application also lets a user enter a new item.
The user can enter an email recipient into the text field and choose Send Report.
The application queries active items from the database and sends the data to the selected email recipient.
Perform these steps.
At this point, you have a new project. Confirm 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("aws.sdk.kotlin:rdsdata: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")
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")
testImplementation("org.springframework.boot:spring-boot-starter-test:2.7.5")
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "17"
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
Create a new package in the main/kotlin folder named com.example.demo. The following Kotlin classes go into this package.
Note: The MessageResource class is located in the DemoApplication file.
The following Kotlin code represents the DemoApplication class. This is the entry point into a Spring Boot application.
package com.example.demo
import kotlinx.coroutines.runBlocking
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 DemoApplication
fun main(args: Array<String>) {
runApplication<DemoApplication>(*args)
}
@CrossOrigin(origins = ["*"])
@RestController
class MessageResource {
// Add a new item.
@PostMapping("api/items")
fun addItems(@RequestBody payLoad: Map<String, Any>): String = runBlocking {
val wi = WorkItemRepository()
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 = wi.injestNewSubmission(myWork)
return@runBlocking "Item $id added successfully!"
}
// Retrieve items.
@GetMapping("api/items")
fun getItems(@RequestParam(required = false) archived: String?): MutableList<WorkItem> = runBlocking {
val wi = WorkItemRepository()
val list: MutableList<WorkItem>
if (archived != null) {
list = wi.getItemsDataSQL(archived)
} else {
list = wi.getItemsDataSQL("")
}
return@runBlocking list
}
// Flip an item from Active to Archive.
@PutMapping("api/items/{id}:archive")
fun modUser(@PathVariable id: String): String = runBlocking {
val wi = WorkItemRepository()
wi.flipItemArchive(id)
return@runBlocking id
}
@PostMapping("api/items:report")
@ResponseStatus(value = HttpStatus.NO_CONTENT)
fun sendReport(@RequestBody body: Map<String, String>) = runBlocking {
val wi = WorkItemRepository()
val email = body.get("email")
val sendMsg = SendMessage()
val xml = wi.getItemsDataSQLReport("0")
try {
if (email != null) {
sendMsg.send(email, xml)
}
} catch (e: IOException) {
e.stackTrace
}
return@runBlocking
}
}
The following Kotlin code represents the WorkItemRepository class. You are required to specify ARN values for Secrets Manager and the Amazon Aurora Serverless database (as discussed in the Create the resources section). Without both of these values, your code won't work. To use the RDSDataClient, you must create an ExecuteStatementRequest object and specify both ARN values, the database name, and the SQL statement.
Also notice the use of SqlParameter when using SQL statements. For example, in the injestNewSubmission method, you build a list of SqlParameter objects that are used to add a new record to the database.
package com.example.demo
import aws.sdk.kotlin.services.rdsdata.RdsDataClient
import aws.sdk.kotlin.services.rdsdata.model.ExecuteStatementRequest
import aws.sdk.kotlin.services.rdsdata.model.Field
import aws.sdk.kotlin.services.rdsdata.model.SqlParameter
import org.springframework.stereotype.Component
import org.w3c.dom.Document
import java.io.StringWriter
import java.sql.Date
import java.text.SimpleDateFormat
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
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
@Component
class WorkItemRepository {
private val secretArnVal = "<Enter value>"
private val resourceArnVal = "<Enter value>"
fun param(nameVal: String, valueVal: String): SqlParameter {
val myPar = SqlParameter {
name = nameVal
value = Field.StringValue(valueVal)
}
return myPar
}
// Archive the specific item.
suspend fun flipItemArchive(id: String): String {
val sqlStatement: String
val arc = "1"
// Specify the SQL statement to query data.
sqlStatement = "update work set archive = (:arch) where idwork =(:id);"
val parametersVal = listOf(
param("arch", arc),
param("id", id)
)
val sqlRequest = ExecuteStatementRequest {
secretArn = secretArnVal
sql = sqlStatement
database = "jobs"
resourceArn = resourceArnVal
parameters = parametersVal
}
RdsDataClient { region = "us-east-1" }.use { rdsDataClient ->
rdsDataClient.executeStatement(sqlRequest)
}
return id
}
// Get items from the database.
suspend fun getItemsDataSQL(statusVal: String): MutableList<WorkItem> {
val records = mutableListOf<WorkItem>()
val sqlStatement: String
val sqlRequest: ExecuteStatementRequest
val isArc: String
if (statusVal.compareTo("true") == 0) {
sqlStatement = "SELECT idwork, date, description, guide, status, username, archive " +
"FROM work WHERE archive = :arch ;"
isArc = "1"
val parametersVal = listOf(param("arch", isArc))
sqlRequest = ExecuteStatementRequest {
secretArn = secretArnVal
sql = sqlStatement
database = "jobs"
resourceArn = resourceArnVal
parameters = parametersVal
}
} else if (statusVal.compareTo("false") == 0) {
sqlStatement = "SELECT idwork, date, description, guide, status, username, archive " +
"FROM work WHERE archive = :arch ;"
isArc = "0"
val parametersVal = listOf(param("arch", isArc))
sqlRequest = ExecuteStatementRequest {
secretArn = secretArnVal
sql = sqlStatement
database = "jobs"
resourceArn = resourceArnVal
parameters = parametersVal
}
} else {
sqlStatement = "SELECT idwork, date, description, guide, status, username, archive FROM work ;"
sqlRequest = ExecuteStatementRequest {
secretArn = secretArnVal
sql = sqlStatement
database = "jobs"
resourceArn = resourceArnVal
}
}
RdsDataClient { region = "us-east-1" }.use { rdsDataClient ->
val response = rdsDataClient.executeStatement(sqlRequest)
val dataList: List<List<Field>>? = response.records
// Process records using Kotlin collection operations.
dataList?.forEach { record ->
val workItem = WorkItem().apply {
id = record[0].toString().substringAfter("=").substringBefore(')')
date = record[1].toString().substringAfter("=").substringBefore(')')
description = record[2].toString().substringAfter("=").substringBefore(')')
guide = record[3].toString().substringAfter("=").substringBefore(')')
status = record[4].toString().substringAfter("=").substringBefore(')')
name = record[5].toString().substringAfter("=").substringBefore(')')
archived = record[6].toString().substringAfter("=").substringBefore(')').toBoolean()
}
records.add(workItem)
}
}
return records
}
// Inject a new submission.
suspend fun injestNewSubmission(item: WorkItem): String {
val arc = "0"
val name = item.name.toString()
val guide = item.guide.toString()
val description = item.description.toString()
val status = item.status.toString()
// Generate the work item ID.
val uuid = UUID.randomUUID()
val workId = uuid.toString()
// Date conversion.
val dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss")
val now = LocalDateTime.now()
val sDate1 = dtf.format(now)
val date1 = SimpleDateFormat("yyyy/MM/dd").parse(sDate1)
val sqlDate = Date(date1.time)
// Inject an item into the database.
val sqlStatement =
"INSERT INTO work (idwork, username, date, description, guide, status, archive) VALUES" +
"(:idwork, :username, :date, :description, :guide, :status, :arch);"
val parametersVal = listOf(
param("arch", arc),
param("username", name),
param("status", status),
param("date", sqlDate.toString()),
param("description", description),
param("guide", guide),
param("idwork", workId)
)
val sqlRequest = ExecuteStatementRequest {
secretArn = secretArnVal
sql = sqlStatement
database = "jobs"
resourceArn = resourceArnVal
parameters = parametersVal
}
RdsDataClient { region = "us-east-1" }.use { rdsDataClient ->
rdsDataClient.executeStatement(sqlRequest)
}
return workId
}
// Get Items data for the content that is sent using Amazon SES.
suspend fun getItemsDataSQLReport(arch: String): String? {
val records = mutableListOf<WorkItem>()
val sqlStatement = "SELECT idwork, date, description, guide, status, username, archive FROM work WHERE archive = :arch ;"
val parametersVal = listOf(param("arch", arch))
val sqlRequest = ExecuteStatementRequest {
secretArn = secretArnVal
sql = sqlStatement
database = "jobs"
resourceArn = resourceArnVal
parameters = parametersVal
}
RdsDataClient { region = "us-east-1" }.use { rdsDataClient ->
val response = rdsDataClient.executeStatement(sqlRequest)
val dataList: List<List<Field>>? = response.records
// Process records using Kotlin collection operations.
dataList?.forEach { record ->
val workItem = WorkItem().apply {
id = record[0].toString().substringAfter("=").substringBefore(')')
date = record[1].toString().substringAfter("=").substringBefore(')')
description = record[2].toString().substringAfter("=").substringBefore(')')
guide = record[3].toString().substringAfter("=").substringBefore(')')
status = record[4].toString().substringAfter("=").substringBefore(')')
name = record[5].toString().substringAfter("=").substringBefore(')')
archived = record[6].toString().substringAfter("=").substringBefore(')').toBoolean()
}
records.add(workItem)
}
}
return convertToString(toXml(records))
}
// Convert Work data into XML to use in the report.
fun toXml(itemList: List<WorkItem>): Document? {
try {
val factory = DocumentBuilderFactory.newInstance()
val builder = factory.newDocumentBuilder()
val doc = builder.newDocument()
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
}
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
}
}
The SendMessage class uses the SesClient 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.
The following Kotlin code represents the WorkItem class.
package com.example.demo
class WorkItem {
var id: String? = null
var name: String? = null
var guide: String? = null
var date: String? = null
var description: String? = null
var status: String? = null
var archived: Boolean? = null
}
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.
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.
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.
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.
In the config.json file, you must make sure that the BASE_URL value references your Spring application.
{
"BASE_URL": "http://localhost:8080/api"
}
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.