kotlin/usecases/subpub_app/README.md
You can create a web application that has subscription and publish functionality by using the Amazon Simple Notification Service (Amazon SNS) and the AWS SDK for Kotlin. The application created in this AWS tutorial is a Spring Boot web application that lets a user subscribe to an Amazon SNS topic by entering a valid email address. A user can enter many emails and all of them are subscribed to the given SNS topic (once the email recipients confirm the subscription). The user can publish a message that results in all subscribed emails receiving the message.
Note: Amazon SNS is a managed service that provides message delivery from publishers to subscribers (also known as producers and consumers). For more information, see What is Amazon SNS?
To complete the tutorial, you need the following:
Note: Make sure that you have installed the Kotlin plug-in for IntelliJ.
Create an Amazon SNS topic that you use in the Kotlin code. You need to reference the topic's ARN value in the Kotlin code. For information, see Creating an Amazon SNS topic.
To subscribe to an Amazon SNS topic, the user enters a valid email address into the web application.
The specified email address recieves an email message that lets the recipient confirm the subscription.
Once the email recipient accepts the confirmation, that email is subscribed to the specific SNS topic and recieves published messages. To publish a message, a user enters the message into the web applicaiton and then chooses the Publish button.
This application lets a user specify the language of the message that is sent. For example, the user can select French from the dropdown field and then the message appears in that language to all subscribed users.
This example application lets you view all of the subscribed email recipients by choosing the List Subscriptions button, as shown in the following illustration.
Perform these steps.
At this point, you have a new project named SpringKotlinSubPub. Ensure that the gradle build file resembles the following code.
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:sns:0.33.1-beta")
implementation("aws.sdk.kotlin:translate: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.4")
implementation("org.springframework.boot:spring-boot-starter-thymeleaf:2.7.4")
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("net.sourceforge.jexcelapi:jxl:2.6.10")
implementation("commons-io:commons-io:2.10.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 a package in the main/kotlin folder named com.aws.kotlin. The Kotlin classes go into this package.
Create these Kotlin classes:
Note: The MessageResource class is located in the SubApplication file.
The following Kotlin code represents the SubApplication and the MessageResource classes. Notice that the SubApplication uses the @SpringBootApplication annotation while the MessageResource class uses the @Controller annotation. In addition, the Spring Controller uses runBlocking and @runBlocking. Both are required and part of Kotlin Coroutine functionality. For more information, see Coroutines basics.
package com.aws.kotlin
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.stereotype.Controller
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestMethod
import org.springframework.web.bind.annotation.ResponseBody
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
@SpringBootApplication
open class SubApplication
fun main(args: Array<String>) {
runApplication<SubApplication>(*args)
}
@Controller
class MessageResource {
@Autowired
var sns: SnsService? = null
@GetMapping("/")
fun root(): String? {
return "index"
}
@GetMapping("/subscribe")
fun add(): String? {
return "sub"
}
@RequestMapping(value = ["/delSub"], method = [RequestMethod.POST])
@ResponseBody
fun delSub(request: HttpServletRequest, response: HttpServletResponse?): String? = runBlocking {
val email = request.getParameter("email")
sns?.unSubEmail(email)
return@runBlocking "$email was successfully deleted!"
}
@RequestMapping(value = ["/addEmail"], method = [RequestMethod.POST])
@ResponseBody
fun addItems(request: HttpServletRequest, response: HttpServletResponse?): String? = runBlocking {
val email = request.getParameter("email")
return@runBlocking sns?.subEmail(email)
}
@RequestMapping(value = ["/addMessage"], method = [RequestMethod.POST])
@ResponseBody
fun addMessage(request: HttpServletRequest, response: HttpServletResponse?): String? = runBlocking {
val body = request.getParameter("body")
val lang = request.getParameter("lang")
return@runBlocking sns?.pubTopic(body,lang)
}
@RequestMapping(value = ["/getSubs"], method = [RequestMethod.GET])
@ResponseBody
fun getSubs(request: HttpServletRequest?, response: HttpServletResponse?): String? = runBlocking{
return@runBlocking sns?.getAllSubscriptions()
}
}
The following Kotlin code represents the SnsService class. This class uses the Kotlin SNS API to interact with Amazon SNS. For example, the subEmail method uses the email address to subscribe to the Amazon SNS topic. Likewise, the unSubEmail method unsubscibes from the Amazon SNS topic. The pubTopic publishes a message.
package com.aws.kotlin
import aws.sdk.kotlin.services.sns.SnsClient
import aws.sdk.kotlin.services.sns.model.ListSubscriptionsByTopicRequest
import aws.sdk.kotlin.services.sns.model.PublishRequest
import aws.sdk.kotlin.services.sns.model.SubscribeRequest
import aws.sdk.kotlin.services.sns.model.UnsubscribeRequest
import aws.sdk.kotlin.services.translate.TranslateClient
import aws.sdk.kotlin.services.translate.model.TranslateTextRequest
import org.springframework.stereotype.Component
import org.w3c.dom.Document
import java.io.StringWriter
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.parsers.ParserConfigurationException
import javax.xml.transform.TransformerConfigurationException
import javax.xml.transform.TransformerException
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult
@Component
class SnsService {
var topicArnVal = "<Enter your topic ARN>"
// Create a Subscription.
suspend fun subEmail(email: String?): String? {
val request = SubscribeRequest {
protocol = "email"
endpoint = email
returnSubscriptionArn = true
topicArn = topicArnVal
}
SnsClient { region = "us-west-2" }.use { snsClient ->
val result = snsClient.subscribe(request)
return result.subscriptionArn
}
}
suspend fun pubTopic(messageVal: String, lang: String): String {
val translateClient = TranslateClient { region = "us-east-1" }
val body: String
if (lang.compareTo("English") == 0) {
body = messageVal
} else if (lang.compareTo("French") == 0) {
val textRequest = TranslateTextRequest {
sourceLanguageCode = "en"
targetLanguageCode = "fr"
text = messageVal
}
val textResponse = translateClient.translateText(textRequest)
body = textResponse.translatedText.toString()
} else {
val textRequest = TranslateTextRequest {
sourceLanguageCode = "en"
targetLanguageCode = "es"
text = messageVal
}
val textResponse = translateClient.translateText(textRequest)
body = textResponse.translatedText.toString()
}
val request = PublishRequest {
message = body
topicArn = topicArnVal
}
SnsClient { region = "us-west-2" }.use { snsClient ->
val result = snsClient.publish(request)
return "{$result.messageId.toString()} message sent successfully in $lang."
}
}
suspend fun unSubEmail(emailEndpoint: String) {
val subscriptionArnVal = getTopicArnValue(emailEndpoint)
val request = UnsubscribeRequest {
subscriptionArn = subscriptionArnVal
}
SnsClient { region = "us-west-2" }.use { snsClient ->
snsClient.unsubscribe(request)
}
}
// Returns the Sub Amazon Resource Name (ARN) based on the given endpoint used for unSub.
suspend fun getTopicArnValue(endpoint: String): String? {
var subArn: String
val request = ListSubscriptionsByTopicRequest {
topicArn = topicArnVal
}
SnsClient { region = "us-west-2" }.use { snsClient ->
val response = snsClient.listSubscriptionsByTopic(request)
response.subscriptions?.forEach { sub ->
if (sub.endpoint?.compareTo(endpoint) == 0) {
subArn = sub.subscriptionArn.toString()
return subArn
}
}
return ""
}
}
suspend fun getAllSubscriptions(): String? {
val subList = mutableListOf<String>()
val request = ListSubscriptionsByTopicRequest {
topicArn = topicArnVal
}
SnsClient { region = "us-west-2" }.use { snsClient ->
val response = snsClient.listSubscriptionsByTopic(request)
response.subscriptions?.forEach { sub ->
subList.add(sub.endpoint.toString())
}
return convertToString(toXml(subList))
}
}
// Convert the list to XML to pass back to the view.
private fun toXml(subsList: List<String>): Document? {
try {
val factory = DocumentBuilderFactory.newInstance()
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)
val builder = factory.newDocumentBuilder()
val doc = builder.newDocument()
// Start building the XML.
val root = doc.createElement("Subs")
doc.appendChild(root)
// Iterate through the collection.
for (sub in subsList) {
val item = doc.createElement("Sub")
root.appendChild(item)
// Set email.
val email = doc.createElement("email")
email.appendChild(doc.createTextNode(sub))
item.appendChild(email)
}
return doc
} catch (e: ParserConfigurationException) {
e.printStackTrace()
}
return null
}
private fun convertToString(xml: Document?): String? {
try {
val transformerFactory = getSecureTransformerFactory()
val transformer = transformerFactory?.newTransformer()
val result = StreamResult(StringWriter())
val source = DOMSource(xml)
if (transformer != null) {
transformer.transform(source, result)
}
return result.writer.toString()
} catch (ex: TransformerException) {
ex.printStackTrace()
}
return null
}
private fun getSecureTransformerFactory(): TransformerFactory? {
val transformerFactory: TransformerFactory = TransformerFactory.newInstance()
try {
transformerFactory.setFeature(javax.xml.XMLConstants.FEATURE_SECURE_PROCESSING, true)
} catch (e: TransformerConfigurationException) {
e.printStackTrace()
}
return transformerFactory
}
}
Note: Make sure that you assign the SNS topic ARN to the topicArn data member. Otherwise, your code does not work.
At this point, you have created all of the Java files required for this example application. Now create HTML files that are required for the application's view. Under the resource folder, create a templates folder, and then create the following HTML files:
The index.html file is the application's home view.
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script th:src="|https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js|"></script>
<link rel="stylesheet" th:href="|https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css|"/>
<link rel="stylesheet" href="../public/css/styles.css" th:href="@{/css/styles.css}" />
<link rel="icon" href="../public/img/favicon.ico" th:href="@{/img/favicon.ico}" />
<title>AWS Job Posting Example</title>
</head>
<body>
<header th:replace="layout :: site-header"/>
<div class="container">
<h3>Welcome to the Amazon Simple Notification Service example app</h3>
<p>Now is: <b th:text="${execInfo.now.time}"></b></p>
<p>The Amazon Simple Notification Service example uses multiple AWS Services and the Java V2 API. Perform these steps:<p>
<ol>
<li>You can subscribe to a SNS topic by choosing the <i>Manage Subscriptions</i> menu item.</li>
<li>Enter a valid email address and then choose <i>Subscribe</i>.</li>
<li>The sample application subscribes to the endpoint by using the SNS Java API V2.</li>
<li>You can view all the email addresses that have subscribed by choosing the <i>List Subscriptions</i> menu item.</li>
<li>You can unSubscribe by entering the email address and choosing <i>UnSubscribe</i>. </li>
<li>You can publish a message by entering a message and choosing <i>Publish</i>.
<li>All subscribed email recipients will receive the published message.</li>
</ol>
<div>
</body>
</html>
### layout.html
The following code represents the **layout.html** file that represents the application's menu.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:fragment="site-head">
<meta charset="UTF-8" />
<link rel="icon" href="../public/img/favicon.ico" th:href="@{/img/favicon.ico}" />
<script th:src="|https://code.jquery.com/jquery-1.12.4.min.js|"></script>
<meta th:include="this :: head" th:remove="tag"/>
</head>
<header th:fragment="site-header">
<a href="index.html" th:href="@{/}"></a>
<a href="#" style="color: white" th:href="@{/}">Home</a>
<a href="#" style="color: white" th:href="@{/subscribe}">Manage Subscriptions</a>
</header>
</html>
### add.html
The **sub.html** file is the application's view that manages Amazon SNS Subscriptions.
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org" lang="">
<head>
<meta charset="UTF-8" />
<title>Subscription</title>
<script th:src="|https://code.jquery.com/jquery-1.12.4.min.js|"></script>
<script th:src="|https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js|"></script>
<link rel="stylesheet" th:href="|https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css|"/>
<script src="../public/js/contact_me.js" th:src="@{/js/contact_me.js}"></script>
<link rel="stylesheet" href="../public/css/styles.css" th:href="@{/css/styles.css}" />
</head>
<body>
<header th:replace="layout :: site-header"/>
<div class="container">
<p>Now is: <b th:text="${execInfo.now.time}"></b></p>
<div class="row">
<div class="col">
<h4>Enter an email address<h3>
<input type="email" class="form-control" id="inputEmail1" aria-describedby="emailHelp" placeholder="Enter email">
<div class="clearfix mt-40">
<!-- Button trigger modal -->
<button type="button" onclick="subEmail() "class="btn btn-primary" >
Subscribe
</button>
<button type="button" class="btn btn-primary" onclick="getSubs()">
List Subscriptions
</button>
<button type="button" onclick="delSub()" class="btn btn-primary" >
UnSubscribe
</button>
<!-- Modal -->
<div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLongTitle" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLongTitle">SNS Email Subscriptions</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<hr style="width:50%;text-align:left;margin-left:0">
<h4>Enter a message to publish</h4>
<div class="col-lg-12 mx-auto">
<div class="control-group">
<div class="form-group floating-label-form-group controls mb-0 pb-2">
<textarea class="form-control" id="body" rows="5" placeholder="Body" required="required" data-validation-required-message="Please enter a description."></textarea>
<p class="help-block text-danger"></p>
</div>
</div>
<div>
<label for="lang">Select a Language:</label>
<select name="lang" id="lang">
<option>English</option>
<option>French</option>
<option>Spanish</option>
</select>
</div>
<button type="submit" class="btn btn-primary btn-xl" id="SendButton">Publish</button>
</div>
</div>
</body>
</html
This application has a contact_me.js file that is used to send HTTP requests to the Spring Controller using AJAX. Place this file in the resources\public\js folder.
$(function() {
$("#SendButton" ).click(function($e) {
var body = $('#body').val();
var lang = $('#lang option:selected').text();
if (body == '' ){
alert("Please enter text");
return;
}
$.ajax('/addMessage', {
type: 'POST',
data: 'lang=' + lang+'&body=' + body,
success: function (data, status, xhr) {
alert(data)
$('#body').val("");
},
error: function (jqXhr, textStatus, errorMessage) {
$('p').append('Error' + errorMessage);
}
});
} );
} );
function subEmail(){
var mail = $('#inputEmail1').val();
var result = validate(mail)
if (result == false) {
alert (mail + " is not valid. Please specify a valid email");
return;
}
// Valid email, post to the server
$.ajax('/addEmail', {
type: 'POST', // http method
data: 'email=' + mail, // data to submit
success: function (data, status, xhr) {
$('#inputEmail1').val("")
alert("Subscription validation is "+data);
},
error: function (jqXhr, textStatus, errorMessage) {
$('p').append('Error' + errorMessage);
}
});
}
function validateEmail(email) {
const re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(email);
}
function validate(email) {
const $result = $("#result");
if (validateEmail(email)) {
return true ;
} else {
return false ;
}
}
function getSubs(){
$.ajax('/getSubs', {
type: 'GET', // http method
success: function (data, status, xhr) {
$('.modal-body').empty();
var xml = data ;
$(xml).find('Sub').each(function () {
var $field = $(this);
var email = $field.find('email').text();
// Append this data to the main list.
$('.modal-body').append("<p><b>"+email+"</b></p>");
});
$("#myModal").modal();
},
error: function (jqXhr, textStatus, errorMessage) {
$('p').append('Error' + errorMessage);
}
});
}
function delSub(event) {
var mail = $('#inputEmail1').val();
var result = validate(mail)
if (result == false) {
alert (mail + " is not valid. Please specify a valid email");
return;
}
$.ajax('/delSub', {
type: 'POST', // http method
data: 'email=' + mail, // data to submit
success: function (data, status, xhr) {
alert(data);
},
error: function (jqXhr, textStatus, errorMessage) {
$('p').append('Error' + errorMessage);
}
});
}
This application uses a CSS file named styles.css file that is used for the menu. Place this file in the resources\public\css folder.
body>header {
background: #000;
padding: 5px;
}
body>header>a>img, body>header a {
display: inline-block;
vertical-align: middle;
padding: 0px 5px;
font-size: 1.2em;
}
body>footer {
background: #eee;
padding: 5px;
margin: 10px 0;
text-align: center;
}
#logged-in-info {
float: right;
margin-top: 18px;
}
#logged-in-info form {
display: inline-block;
margin-right: 10px;
}
Using the IntelliJ IDE, you can run your application. The first time you run the Spring Boot application, you can run the application by clicking the run icon in the Spring Boot main class, as shown in this illustration.
To access the application, open your browser and enter the URL for your application. You will see the Home page for your application.
Congratulations! You have created a Spring Boot application that contains subscription and publish functionality. As stated at the beginning of this tutorial, be sure to terminate all of the resources you create while going through this tutorial to ensure that you’re not charged.