{
+ private final static Logger log = LogManager.getLogger();
+
+ public App() {
+ this(null);
+ }
+
+ public App(DynamoDbClient client) {
+ Idempotency.config().withConfig(
+ IdempotencyConfig.builder()
+ .withEventKeyJMESPath("powertools_json(body).address") // will retrieve the address field in the body which is a string transformed to json with `powertools_json`
+ .build())
+ .withPersistenceStore(
+ DynamoDBPersistenceStore.builder()
+ .withDynamoDbClient(client)
+ .withTableName(System.getenv("IDEMPOTENCY_TABLE"))
+ .build()
+ ).configure();
+ }
+
+ /**
+ * Try with:
+ *
+ * curl -X POST https://[REST-API-ID].execute-api.[REGION].amazonaws.com/Prod/helloidem/ -H "Content-Type: application/json" -d '{"address": "https://checkip.amazonaws.com"}'
+ *
+ *
+ * - First call will execute the handleRequest normally, and store the response in the idempotency table (Look into DynamoDB)
+ * - Second call (and next ones) will retrieve from the cache (if cache is enabled, which is by default) or from the store, the handler won't be called. Until the expiration happens (by default 1 hour).
+ *
+ */
+ @Idempotent // *** THE MAGIC IS HERE ***
+ @Logging(logEvent = true)
+ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) {
+ Map headers = new HashMap<>();
+
+ headers.put("Content-Type", "application/json");
+ headers.put("Access-Control-Allow-Origin", "*");
+ headers.put("Access-Control-Allow-Methods", "GET, OPTIONS");
+ headers.put("Access-Control-Allow-Headers", "*");
+
+ APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent()
+ .withHeaders(headers);
+ try {
+ String address = JsonConfig.get().getObjectMapper().readTree(input.getBody()).get("address").asText();
+ final String pageContents = this.getPageContents(address);
+ String output = String.format("{ \"message\": \"hello world\", \"location\": \"%s\" }", pageContents);
+
+ log.info("ip is {}", pageContents);
+ return response
+ .withStatusCode(200)
+ .withBody(output);
+
+ } catch (IOException e) {
+ return response
+ .withBody("{}")
+ .withStatusCode(500);
+ }
+ }
+
+ // we could also put the @Idempotent annotation here, but using it on the handler avoids executing the handler (cost reduction).
+ // Use it on other methods to handle multiple items (with SQS batch processing for example)
+ private String getPageContents(String address) throws IOException {
+ URL url = new URL(address);
+ try (BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream()))) {
+ return br.lines().collect(Collectors.joining(System.lineSeparator()));
+ }
+ }
+}
\ No newline at end of file
diff --git a/java/Idempotency/Function/src/test/java/helloworld/AppTest.java b/java/Idempotency/Function/src/test/java/helloworld/AppTest.java
new file mode 100644
index 0000000..7a5304e
--- /dev/null
+++ b/java/Idempotency/Function/src/test/java/helloworld/AppTest.java
@@ -0,0 +1,86 @@
+package helloworld;
+
+import com.amazonaws.services.dynamodbv2.local.main.ServerRunner;
+import com.amazonaws.services.dynamodbv2.local.server.DynamoDBProxyServer;
+import com.amazonaws.services.lambda.runtime.Context;
+import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
+import com.amazonaws.services.lambda.runtime.tests.EventLoader;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
+import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
+import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
+import software.amazon.awssdk.services.dynamodb.model.*;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.net.URI;
+
+public class AppTest {
+ @Mock
+ private Context context;
+ private App app;
+ private static DynamoDbClient client;
+
+ @BeforeAll
+ public static void setupDynamoLocal() {
+ int port = getFreePort();
+ try {
+ DynamoDBProxyServer dynamoProxy = ServerRunner.createServerFromCommandLineArgs(new String[]{
+ "-inMemory",
+ "-port",
+ Integer.toString(port)
+ });
+ dynamoProxy.start();
+ } catch (Exception e) {
+ throw new RuntimeException();
+ }
+
+ client = DynamoDbClient.builder()
+ .httpClient(UrlConnectionHttpClient.builder().build())
+ .region(Region.EU_WEST_1)
+ .endpointOverride(URI.create("http://localhost:" + port))
+ .credentialsProvider(StaticCredentialsProvider.create(
+ AwsBasicCredentials.create("FAKE", "FAKE")))
+ .build();
+
+ client.createTable(CreateTableRequest.builder()
+ .tableName("idempotency")
+ .keySchema(KeySchemaElement.builder().keyType(KeyType.HASH).attributeName("id").build())
+ .attributeDefinitions(
+ AttributeDefinition.builder().attributeName("id").attributeType(ScalarAttributeType.S).build()
+ )
+ .billingMode(BillingMode.PAY_PER_REQUEST)
+ .build());
+ }
+
+ private static int getFreePort() {
+ try {
+ ServerSocket socket = new ServerSocket(0);
+ int port = socket.getLocalPort();
+ socket.close();
+ return port;
+ } catch (IOException ioe) {
+ throw new RuntimeException(ioe);
+ }
+ }
+
+ @BeforeEach
+ void setUp() {
+ MockitoAnnotations.openMocks(this);
+ app = new App(client);
+ }
+
+ @Test
+ public void testApp() {
+ APIGatewayProxyResponseEvent response = app.handleRequest(EventLoader.loadApiGatewayRestEvent("event.json"), context);
+ Assertions.assertNotNull(response);
+ Assertions.assertTrue(response.getBody().contains("hello world"));
+ }
+}
diff --git a/java/Idempotency/Function/src/test/resources/event.json b/java/Idempotency/Function/src/test/resources/event.json
new file mode 100644
index 0000000..fd7f5ac
--- /dev/null
+++ b/java/Idempotency/Function/src/test/resources/event.json
@@ -0,0 +1,63 @@
+{
+ "body": "{\"address\": \"https://checkip.amazonaws.com\"}",
+ "resource": "/{proxy+}",
+ "path": "/path/to/resource",
+ "httpMethod": "POST",
+ "isBase64Encoded": false,
+ "queryStringParameters": {
+ "foo": "bar"
+ },
+ "pathParameters": {
+ "proxy": "/path/to/resource"
+ },
+ "stageVariables": {
+ "baz": "qux"
+ },
+ "headers": {
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
+ "Accept-Encoding": "gzip, deflate, sdch",
+ "Accept-Language": "en-US,en;q=0.8",
+ "Cache-Control": "max-age=0",
+ "CloudFront-Forwarded-Proto": "https",
+ "CloudFront-Is-Desktop-Viewer": "true",
+ "CloudFront-Is-Mobile-Viewer": "false",
+ "CloudFront-Is-SmartTV-Viewer": "false",
+ "CloudFront-Is-Tablet-Viewer": "false",
+ "CloudFront-Viewer-Country": "US",
+ "Host": "1234567890.execute-api.us-east-1.amazonaws.com",
+ "Upgrade-Insecure-Requests": "1",
+ "User-Agent": "Custom User Agent String",
+ "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)",
+ "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==",
+ "X-Forwarded-For": "127.0.0.1, 127.0.0.2",
+ "X-Forwarded-Port": "443",
+ "X-Forwarded-Proto": "https"
+ },
+ "requestContext": {
+ "accountId": "123456789012",
+ "resourceId": "123456",
+ "stage": "prod",
+ "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
+ "requestTime": "09/Apr/2015:12:34:56 +0000",
+ "requestTimeEpoch": 1428582896000,
+ "identity": {
+ "cognitoIdentityPoolId": null,
+ "accountId": null,
+ "cognitoIdentityId": null,
+ "caller": null,
+ "accessKey": null,
+ "sourceIp": "127.0.0.1",
+ "cognitoAuthenticationType": null,
+ "cognitoAuthenticationProvider": null,
+ "userArn": null,
+ "userAgent": "Custom User Agent String",
+ "user": null
+ },
+ "path": "/prod/path/to/resource",
+ "resourcePath": "/{proxy+}",
+ "httpMethod": "POST",
+ "apiId": "1234567890",
+ "protocol": "HTTP/1.1"
+ }
+ }
+
\ No newline at end of file
diff --git a/java/Idempotency/README.md b/java/Idempotency/README.md
new file mode 100644
index 0000000..a919f0c
--- /dev/null
+++ b/java/Idempotency/README.md
@@ -0,0 +1,21 @@
+# Idempotency
+
+This project contains an example of Lambda function using the idempotency module of Lambda Powertools for Java. For more information on this module, please refer to the [documentation](https://awslabs.github.io/aws-lambda-powertools-java/utilities/idempotency/).
+
+## Deploy the sample application
+
+This sample is based on Serverless Application Model (SAM) and you can use the SAM Command Line Interface (SAM CLI) to build it and deploy it to AWS.
+
+To use the SAM CLI, you need the following tools.
+
+* SAM CLI - [Install the SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html)
+* Java11 - [Install the Java 11](https://docs.aws.amazon.com/corretto/latest/corretto-11-ug/downloads-list.html)
+* Maven - [Install Maven](https://maven.apache.org/install.html)
+* Docker - [Install Docker community edition](https://hub.docker.com/search/?type=edition&offering=community)
+
+To build and deploy your application for the first time, run the following in your shell:
+
+```bash
+Coreutilities$ sam build
+Coreutilities$ sam deploy --guided
+```
diff --git a/java/Idempotency/template.yaml b/java/Idempotency/template.yaml
new file mode 100644
index 0000000..d0708c6
--- /dev/null
+++ b/java/Idempotency/template.yaml
@@ -0,0 +1,59 @@
+AWSTemplateFormatVersion: '2010-09-09'
+Transform: AWS::Serverless-2016-10-31
+Description: >
+ Idempotency demo
+
+Globals:
+ Function:
+ Timeout: 20
+ Runtime: java11
+ MemorySize: 512
+ Tracing: Active
+ Environment:
+ Variables:
+ POWERTOOLS_LOG_LEVEL: INFO
+ POWERTOOLS_LOGGER_SAMPLE_RATE: 0.1
+ POWERTOOLS_LOGGER_LOG_EVENT: true
+
+Resources:
+ IdempotencyTable:
+ Type: AWS::DynamoDB::Table
+ Properties:
+ AttributeDefinitions:
+ - AttributeName: id
+ AttributeType: S
+ KeySchema:
+ - AttributeName: id
+ KeyType: HASH
+ TimeToLiveSpecification:
+ AttributeName: expiration
+ Enabled: true
+ BillingMode: PAY_PER_REQUEST
+
+ IdempotencyFunction:
+ Type: AWS::Serverless::Function
+ Properties:
+ CodeUri: Function
+ Handler: helloworld.App::handleRequest
+ Policies:
+ - DynamoDBCrudPolicy:
+ TableName: !Ref IdempotencyTable
+ Environment:
+ Variables:
+ POWERTOOLS_SERVICE_NAME: idempotency
+ IDEMPOTENCY_TABLE: !Ref IdempotencyTable
+ Events:
+ HelloWorld:
+ Type: Api
+ Properties:
+ Path: /helloidem
+ Method: post
+
+Outputs:
+ HelloWorldApi:
+ Description: "API Gateway endpoint URL for Prod stage for Idempotent function"
+ Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/helloidem/"
+ HelloWorldFunction:
+ Description: "Idempotent Lambda Function ARN"
+ Value: !GetAtt IdempotencyFunction.Arn
+
diff --git a/manifest.json b/manifest.json
index bb35f06..8e0eaf3 100644
--- a/manifest.json
+++ b/manifest.json
@@ -13,6 +13,13 @@
"dependencyManager": "maven",
"appTemplate": "CoreUtilities",
"javaVersion": "11"
+ },
+ {
+ "directory": "java/Idempotency/Function",
+ "displayName": "Demos setup of an idempotent Lambda function using Powertools",
+ "dependencyManager": "maven",
+ "appTemplate": "Idempotency",
+ "javaVersion": "11"
}
]
}
\ No newline at end of file