diff --git a/.github/workflows/run-e2e-tests.yml b/.github/workflows/run-e2e-tests.yml
index 4ed79ca80..a998e6b2d 100644
--- a/.github/workflows/run-e2e-tests.yml
+++ b/.github/workflows/run-e2e-tests.yml
@@ -14,9 +14,16 @@ on:
- 'powertools-idempotency/**'
- 'powertools-parameters/**'
- 'powertools-metrics/**'
+ - 'powertools-large-messages/**'
- 'pom.xml'
- '.github/workflows/**'
+ pull_request:
+ branches:
+ - main
+ paths:
+ - 'powertools-e2e-tests/**'
+
jobs:
e2e:
runs-on: ubuntu-latest
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 775074e82..5619f0bbb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,14 @@ This project follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) fo
## [Unreleased]
+## [1.16.1] - 2023-07-19
+
+* Fix: idempotency timeout bug (#1285) by @scottgerring
+* Fix: ParamManager cannot provide default SSM & Secrets providers (#1282) by @jeromevdl
+* Fix: Handle batch failures in FIFO queues correctly (#1183) by @scottgerring
+* Deps: Bump third party dependencies to the latest versions.
+
+
## [1.16.0] - 2023-06-29
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 08aeddcfe..8db303737 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,58 +1,128 @@
+
+# Table of contents
+
+- [Contributing Guidelines](#contributing-guidelines)
+ - [Reporting Bugs/Feature Requests](#reporting-bugsfeature-requests)
+ - [Contributing via Pull Requests](#contributing-via-pull-requests)
+ - [Dev setup](#dev-setup)
+ - [Local documentation](#local-documentation)
+ - [Conventions](#conventions)
+ - [General terminology and practices](#general-terminology-and-practices)
+ - [Testing definition](#testing-definition)
+ - [Finding contributions to work on](#finding-contributions-to-work-on)
+ - [Code of Conduct](#code-of-conduct)
+ - [Security issue notifications](#security-issue-notifications)
+ - [Troubleshooting](#troubleshooting)
+ - [Licensing](#licensing)
+
# Contributing Guidelines
-Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional
-documentation, we greatly value feedback and contributions from our community.
+
+Thank you for your interest in contributing to our project. Whether it's a [bug report](https://github.com/aws-powertools/powertools-lambda-java/issues/new?assignees=&labels=bug%2C+triage&projects=&template=bug_report.md&title=Bug%3A+TITLE), [new feature](https://github.com/aws-powertools/powertools-lambda-java/issues/new?assignees=&labels=feature-request%2C+triage&projects=&template=feature_request.md&title=Feature+request%3A+TITLE) or [additional documentation](https://github.com/aws-powertools/powertools-lambda-java/issues/new?assignees=&labels=documentation%2Ctriage&projects=&template=documentation_improvements.yml&title=Docs%3A+TITLE), we greatly value feedback and contributions from our community.
+
Please read through this document before submitting any issues or pull requests to ensure we have all the necessary
information to effectively respond to your bug report or contribution.
-
## Reporting Bugs/Feature Requests
-We welcome you to use the GitHub issue tracker to report bugs or suggest features.
-
-When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already
-reported the issue. Please try to include as much information as you can. Details like these are incredibly useful:
-
-* A reproducible test case or series of steps
-* The version of our code being used
-* Any modifications you've made relevant to the bug
-* Anything unusual about your environment or deployment
+We welcome you to use the GitHub issue tracker to report bugs, suggest features, or documentation improvements.
+
+[When filing an issue](https://github.com/aws-powertools/powertools-lambda-java/issues/new/choose), please check [existing open](https://github.com/aws-powertools/powertools-lambda-java/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc), or [recently closed](https://github.com/aws-powertools/powertools-lambda-java/issues?q=is%3Aissue+sort%3Aupdated-desc+is%3Aclosed) issues to make sure somebody else hasn't already reported the issue. Please try to include as much information as you can.
+
## Contributing via Pull Requests
+
Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that:
-1. You are working against the latest source on the *main* branch.
-2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already.
-3. You open an issue to discuss any significant work - we would hate for your time to be wasted.
+1. You are working on a fork. [Fork the repository](https://github.com/aws-powertools/powertools-lambda-java/fork).
+2. You are working against the latest source on the **main** branch.
+3. You've checked existing open, and recently merged pull requests to make sure someone else hasn't addressed the problem already.
+4. You've opened an [issue](https://github.com/aws-powertools/powertools-lambda-java/issues/new/choose) before you begin any implementation. We value your time and bandwidth. As such, any pull requests created on non-triaged issues might not be successful.
+
+### Dev setup
+
+We recommend using [IntelliJ IDEA](https://www.jetbrains.com/idea/) from JetBrains.
+A community version is available and largely enough for our purpose.
+
+#### Code Formatting
+
+We strongly recommend installing the CheckStyle-IDEA plugin and apply the provided [checkstyle.xml](checkstyle.xml) in order to comply with our formatting rules:
+
+1. Install the [CheckStyle-IDEA plugin](https://plugins.jetbrains.com/plugin/1065-checkstyle-idea) and restart IntelliJ.
+
+2. After installing the plugin, open the preferences (`⌘,` on macOS, or `Ctrl+Alt+S` on Windows/Linux) and search for _Code Style_. Click on the gear icon near the scheme and import checkstyle configuration. Click on "Apply" and "OK".
+
+
+3. Select the code you've created (module, package, class) and reformat code: `⌘⌥L` (macOS), or `Ctrl+Alt+L` (Windows/Linux):
+
+
+4. Apply the reformat, optimize imports, rearrange and cleanup to your code and only to java files:
+
+
+#### License headers
+All the java files should contain the licence/copyright header. You can copy paste it from the [license-header](license-header) file.
-To send us a pull request, please:
+### Creating the pull request
-1. Fork the repository.
-2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change.
-3. Ensure local tests pass.
-4. Commit to your fork using clear commit messages.
-5. Send us a pull request, answering any default questions in the pull request interface.
-6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation.
+To send us a pull request, please follow these steps:
+
+1. Create a new branch to focus on the specific change you are contributing e.g. `improv/logger-debug-sampling`
+2. Run all tests, and code baseline checks: `mvn clean verify -P build-with-spotbugs`
+3. Commit to your fork using clear commit messages.
+4. Send us a pull request with a [conventional semantic title](.github/semantic.yml), and answering any default questions in the pull request interface.
+5. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation.
GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and
[creating a pull request](https://help.github.com/articles/creating-a-pull-request/).
+### Local documentation
+
+If you work on the documentation, you may find useful to display it locally while editing, using the following command:
+
+- **Docs website**: `make docs-local-docker`
+
+## Conventions
+
+### General terminology and practices
+
+| Category | Convention |
+|-----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| **Style guide** | We use Checkstyle and Sonar to enforce beyond good practices. |
+| **Exceptions** | Specific exceptions live within the utilities themselves and use `Exception` suffix e.g. `IdempotencyKeyException`. |
+| **Git commits** | We follow [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/). We do not enforce conventional commits on contributors to lower the entry bar. Instead, we enforce a conventional PR title so our label automation and changelog are generated correctly. |
+| **API documentation** | API reference docs are generated from Javadoc which should have examples to allow developers to have what they need within their own IDE. Documentation website covers the wider usage, tips, and strive to be concise. |
+| **Documentation** | We treat it like a product. We sub-divide content aimed at getting started (80% of customers) vs advanced usage (20%). We also ensure customers know how to unit test their code when using our features. |
+
+### Testing definition
+
+We group tests in different categories
+
+| Test | When to write | Notes | Speed |
+| ----------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
+| Unit tests | Verify the smallest possible unit works. | Networking access is prohibited. Prefer Functional tests given our complexity. | Lightning fast (nsec to ms) |
+| End-to-end tests | Gain confidence that a Lambda function with our code operates as expected. | It simulates how customers configure, deploy, and run their Lambda function - Event Source configuration, IAM permissions, etc. | Slow (minutes) |
+
+**NOTE**: Unit tests are mandatory. End-to-end tests should be created for new modules.
+We apply the principle of the [test pyramid](https://martinfowler.com/articles/practical-test-pyramid.html), having a majority of unit tests to cover most cases (standard, edge, errors, ...) and generally one end-to-end test to verify the standard case in the target environment.
## Finding contributions to work on
-Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start.
+Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/help wanted/invalid/question/documentation), [looking at any 'good first issue' issues is a great place to start](https://github.com/aws-powertools/powertools-lambda-java/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22).
## Code of Conduct
+
This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
-opensource-codeofconduct@amazon.com with any additional questions or comments.
-
+ with any additional questions or comments.
## Security issue notifications
+
If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue.
+## Troubleshooting
+
## Licensing
diff --git a/checkstyle.xml b/checkstyle.xml
new file mode 100644
index 000000000..34ef98ef2
--- /dev/null
+++ b/checkstyle.xml
@@ -0,0 +1,427 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/core/logging.md b/docs/core/logging.md
index 1af206314..09714a512 100644
--- a/docs/core/logging.md
+++ b/docs/core/logging.md
@@ -11,6 +11,141 @@ Logging provides an opinionated logger with output structured as JSON.
* Log Lambda event when instructed, disabled by default, can be enabled explicitly via annotation param
* Append additional keys to structured log at any point in time
+## Install
+
+Depending on your version of Java (either Java 1.8 or 11+), the configuration slightly changes.
+
+=== "Maven Java 11+"
+
+ ```xml hl_lines="3-7 16 18 24-27"
+
+ ...
+
+ software.amazon.lambda
+ powertools-logging
+ {{ powertools.version }}
+
+ ...
+
+ ...
+
+
+
+ ...
+
+ dev.aspectj
+ aspectj-maven-plugin
+ 1.13.1
+
+ 11
+ 11
+ 11
+
+
+ software.amazon.lambda
+ powertools-logging
+
+
+
+
+
+
+ compile
+
+
+
+
+ ...
+
+
+ ```
+
+=== "Maven Java 1.8"
+
+ ```xml hl_lines="3-7 16 18 24-27"
+
+ ...
+
+ software.amazon.lambda
+ powertools-logging
+ {{ powertools.version }}
+
+ ...
+
+ ...
+
+
+
+ ...
+
+ org.codehaus.mojo
+ aspectj-maven-plugin
+ 1.14.0
+
+ 1.8
+ 1.8
+ 1.8
+
+
+ software.amazon.lambda
+ powertools-logging
+
+
+
+
+
+
+ compile
+
+
+
+
+ ...
+
+
+ ```
+
+=== "Gradle Java 11+"
+
+ ```groovy hl_lines="3 11"
+ plugins {
+ id 'java'
+ id 'io.freefair.aspectj.post-compile-weaving' version '8.1.0'
+ }
+
+ repositories {
+ mavenCentral()
+ }
+
+ dependencies {
+ aspect 'software.amazon.lambda:powertools-logging:{{ powertools.version }}'
+ }
+
+ sourceCompatibility = 11
+ targetCompatibility = 11
+ ```
+
+=== "Gradle Java 1.8"
+
+ ```groovy hl_lines="3 11"
+ plugins {
+ id 'java'
+ id 'io.freefair.aspectj.post-compile-weaving' version '6.6.3'
+ }
+
+ repositories {
+ mavenCentral()
+ }
+
+ dependencies {
+ aspect 'software.amazon.lambda:powertools-logging:{{ powertools.version }}'
+ }
+
+ sourceCompatibility = 1.8
+ targetCompatibility = 1.8
+ ```
+
+
## Initialization
Powertools for AWS Lambda (Java) extends the functionality of Log4J. Below is an example `#!xml log4j2.xml` file, with the `JsonTemplateLayout` using `#!json LambdaJsonLayout.json` configured.
@@ -104,7 +239,7 @@ to customise what is logged.
*/
public class App implements RequestHandler {
- Logger log = LogManager.getLogger();
+ Logger log = LogManager.getLogger(App.class);
@Logging
public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) {
@@ -121,7 +256,7 @@ to customise what is logged.
*/
public class AppLogEvent implements RequestHandler {
- Logger log = LogManager.getLogger();
+ Logger log = LogManager.getLogger(AppLogEvent.class);
@Logging(logEvent = true)
public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) {
@@ -180,7 +315,7 @@ You can set a Correlation ID using `correlationIdPath` attribute by passing a [J
*/
public class App implements RequestHandler {
- Logger log = LogManager.getLogger();
+ Logger log = LogManager.getLogger(App.class);
@Logging(correlationIdPath = "/headers/my_request_id_header")
public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) {
@@ -229,7 +364,7 @@ for known event sources, where either a request ID or X-Ray Trace ID are present
*/
public class App implements RequestHandler {
- Logger log = LogManager.getLogger();
+ Logger log = LogManager.getLogger(App.class);
@Logging(correlationIdPath = CorrelationIdPathConstants.API_GATEWAY_REST)
public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) {
@@ -282,7 +417,7 @@ You can append your own keys to your existing logs via `appendKey`.
*/
public class App implements RequestHandler {
- Logger log = LogManager.getLogger();
+ Logger log = LogManager.getLogger(App.class);
@Logging(logEvent = true)
public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) {
@@ -314,7 +449,7 @@ You can remove any additional key from entry using `LoggingUtils.removeKeys()`.
*/
public class App implements RequestHandler {
- Logger log = LogManager.getLogger();
+ Logger log = LogManager.getLogger(App.class);
@Logging(logEvent = true)
public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) {
@@ -349,7 +484,7 @@ this means that custom keys can be persisted across invocations. If you want all
*/
public class App implements RequestHandler {
- Logger log = LogManager.getLogger();
+ Logger log = LogManager.getLogger(App.class);
@Logging(clearState = true)
public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) {
@@ -410,7 +545,7 @@ specific fields from received event due to security.
*/
public class App implements RequestHandler {
- Logger log = LogManager.getLogger();
+ Logger log = LogManager.getLogger(App.class);
static {
ObjectMapper objectMapper = new ObjectMapper();
@@ -440,7 +575,7 @@ via `samplingRate` attribute on annotation.
*/
public class App implements RequestHandler {
- Logger log = LogManager.getLogger();
+ Logger log = LogManager.getLogger(App.class);
@Logging(samplingRate = 0.5)
public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) {
diff --git a/docs/core/metrics.md b/docs/core/metrics.md
index f84508669..e06ab6d10 100644
--- a/docs/core/metrics.md
+++ b/docs/core/metrics.md
@@ -26,6 +26,139 @@ If you're new to Amazon CloudWatch, there are two terminologies you must be awar
Metric terminology, visually explained
+## Install
+
+ Depending on your version of Java (either Java 1.8 or 11+), the configuration slightly changes.
+
+=== "Maven Java 11+"
+
+ ```xml hl_lines="3-7 16 18 24-27"
+
+ ...
+
+ software.amazon.lambda
+ powertools-metrics
+ {{ powertools.version }}
+
+ ...
+
+ ...
+
+
+
+ ...
+
+ dev.aspectj
+ aspectj-maven-plugin
+ 1.13.1
+
+ 11
+ 11
+ 11
+
+
+ software.amazon.lambda
+ powertools-metrics
+
+
+
+
+
+
+ compile
+
+
+
+
+ ...
+
+
+ ```
+
+=== "Maven Java 1.8"
+
+ ```xml hl_lines="3-7 16 18 24-27"
+
+ ...
+
+ software.amazon.lambda
+ powertools-metrics
+ {{ powertools.version }}
+
+ ...
+
+ ...
+
+
+
+ ...
+
+ org.codehaus.mojo
+ aspectj-maven-plugin
+ 1.14.0
+
+ 1.8
+ 1.8
+ 1.8
+
+
+ software.amazon.lambda
+ powertools-metrics
+
+
+
+
+
+
+ compile
+
+
+
+
+ ...
+
+
+ ```
+
+=== "Gradle Java 11+"
+
+ ```groovy hl_lines="3 11"
+ plugins {
+ id 'java'
+ id 'io.freefair.aspectj.post-compile-weaving' version '8.1.0'
+ }
+
+ repositories {
+ mavenCentral()
+ }
+
+ dependencies {
+ aspect 'software.amazon.lambda:powertools-metrics:{{ powertools.version }}'
+ }
+
+ sourceCompatibility = 11
+ targetCompatibility = 11
+ ```
+
+=== "Gradle Java 1.8"
+
+ ```groovy hl_lines="3 11"
+ plugins {
+ id 'java'
+ id 'io.freefair.aspectj.post-compile-weaving' version '6.6.3'
+ }
+
+ repositories {
+ mavenCentral()
+ }
+
+ dependencies {
+ aspect 'software.amazon.lambda:powertools-metrics:{{ powertools.version }}'
+ }
+
+ sourceCompatibility = 1.8
+ targetCompatibility = 1.8
+ ```
## Getting started
diff --git a/docs/core/tracing.md b/docs/core/tracing.md
index 13d0be292..17e81b867 100644
--- a/docs/core/tracing.md
+++ b/docs/core/tracing.md
@@ -15,7 +15,142 @@ a provides functionality to reduce the overhead of performing common tracing tas
* Better developer experience when developing with multiple threads.
* Auto patch supported modules by AWS X-Ray
-Initialization
+## Install
+
+Depending on your version of Java (either Java 1.8 or 11+), the configuration slightly changes.
+
+=== "Maven Java 11+"
+
+ ```xml hl_lines="3-7 16 18 24-27"
+
+ ...
+
+ software.amazon.lambda
+ powertools-tracing
+ {{ powertools.version }}
+
+ ...
+
+ ...
+
+
+
+ ...
+
+ dev.aspectj
+ aspectj-maven-plugin
+ 1.13.1
+
+ 11
+ 11
+ 11
+
+
+ software.amazon.lambda
+ powertools-tracing
+
+
+
+
+
+
+ compile
+
+
+
+
+ ...
+
+
+ ```
+
+=== "Maven Java 1.8"
+
+ ```xml hl_lines="3-7 16 18 24-27"
+
+ ...
+
+ software.amazon.lambda
+ powertools-tracing
+ {{ powertools.version }}
+
+ ...
+
+ ...
+
+
+
+ ...
+
+ org.codehaus.mojo
+ aspectj-maven-plugin
+ 1.14.0
+
+ 1.8
+ 1.8
+ 1.8
+
+
+ software.amazon.lambda
+ powertools-tracing
+
+
+
+
+
+
+ compile
+
+
+
+
+ ...
+
+
+ ```
+
+=== "Gradle Java 11+"
+
+ ```groovy hl_lines="3 11"
+ plugins {
+ id 'java'
+ id 'io.freefair.aspectj.post-compile-weaving' version '8.1.0'
+ }
+
+ repositories {
+ mavenCentral()
+ }
+
+ dependencies {
+ aspect 'software.amazon.lambda:powertools-tracing:{{ powertools.version }}'
+ }
+
+ sourceCompatibility = 11
+ targetCompatibility = 11
+ ```
+
+=== "Gradle Java 1.8"
+
+ ```groovy hl_lines="3 11"
+ plugins {
+ id 'java'
+ id 'io.freefair.aspectj.post-compile-weaving' version '6.6.3'
+ }
+
+ repositories {
+ mavenCentral()
+ }
+
+ dependencies {
+ aspect 'software.amazon.lambda:powertools-tracing:{{ powertools.version }}'
+ }
+
+ sourceCompatibility = 1.8
+ targetCompatibility = 1.8
+ ```
+
+
+## Initialization
Before your use this utility, your AWS Lambda function [must have permissions](https://docs.aws.amazon.com/lambda/latest/dg/services-xray.html#services-xray-permissions) to send traces to AWS X-Ray.
diff --git a/docs/index.md b/docs/index.md
index 4358b08f4..d3e487174 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -28,24 +28,63 @@ This project separates core utilities that will be available in other runtimes v
## Install
-Powertools for AWS Lambda (Java) dependencies are available in Maven Central. You can use your favourite dependency management tool to install it
-
-* [Maven](https://maven.apache.org/)
-* [Gradle](https://gradle.org)
-
-**Quick hello world examples using SAM CLI**
+**Quick hello world example using SAM CLI**
You can use [SAM](https://aws.amazon.com/serverless/sam/) to quickly setup a serverless project including Powertools for AWS Lambda (Java).
```bash
- sam init --location gh:aws-samples/cookiecutter-aws-sam-powertools-java
+sam init
+
+Which template source would you like to use?
+ 1 - AWS Quick Start Templates
+ 2 - Custom Template Location
+Choice: 1
+
+Choose an AWS Quick Start application template
+ 1 - Hello World Example
+ 2 - Data processing
+ 3 - Hello World Example with Powertools for AWS Lambda
+ 4 - Multi-step workflow
+ 5 - Scheduled task
+ 6 - Standalone function
+ 7 - Serverless API
+ 8 - Infrastructure event management
+ 9 - Lambda Response Streaming
+ 10 - Serverless Connector Hello World Example
+ 11 - Multi-step workflow with Connectors
+ 12 - Full Stack
+ 13 - Lambda EFS example
+ 14 - DynamoDB Example
+ 15 - Machine Learning
+Template: 3
+
+Which runtime would you like to use?
+ 1 - dotnet6
+ 2 - java17
+ 3 - java11
+ 4 - java8.al2
+ 5 - java8
+ 6 - nodejs18.x
+ 7 - nodejs16.x
+ 8 - nodejs14.x
+ 9 - python3.9
+ 10 - python3.8
+ 11 - python3.7
+ 12 - python3.10
+Runtime: 2, 3, 4 or 5
```
-For more information about the project and available options refer to this [repository](https://github.com/aws-samples/cookiecutter-aws-sam-powertools-java/blob/main/README.md)
+**Manual installation**
+Powertools for AWS Lambda (Java) dependencies are available in Maven Central. You can use your favourite dependency management tool to install it
-=== "Maven"
+* [Maven](https://maven.apache.org/)
+* [Gradle](https://gradle.org)
- ```xml hl_lines="3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55"
+Depending on your version of Java (either Java 1.8 or 11+), the configuration slightly changes.
+
+=== "Maven Java 11+"
+
+ ```xml
...
@@ -74,6 +113,69 @@ For more information about the project and available options refer to this [repo
dev.aspectjaspectj-maven-plugin1.13.1
+
+ 11
+ 11
+ 11
+
+
+ software.amazon.lambda
+ powertools-tracing
+
+
+ software.amazon.lambda
+ powertools-logging
+
+
+ software.amazon.lambda
+ powertools-metrics
+
+
+
+
+
+
+ compile
+
+
+
+
+ ...
+
+
+ ```
+
+=== "Maven Java 1.8"
+
+ ```xml
+
+ ...
+
+ software.amazon.lambda
+ powertools-tracing
+ {{ powertools.version }}
+
+
+ software.amazon.lambda
+ powertools-logging
+ {{ powertools.version }}
+
+
+ software.amazon.lambda
+ powertools-metrics
+ {{ powertools.version }}
+
+ ...
+
+ ...
+
+
+
+ ...
+
+ org.codehaus.mojo
+ aspectj-maven-plugin
+ 1.14.01.81.8
@@ -106,30 +208,57 @@ For more information about the project and available options refer to this [repo
```
-=== "Gradle"
+=== "Gradle Java 11+"
```groovy
- plugins{
- id 'java'
- id 'io.freefair.aspectj.post-compile-weaving' version '6.3.0'
- }
-
- repositories {
- mavenCentral()
- }
-
- dependencies {
- aspect 'software.amazon.lambda:powertools-logging:{{ powertools.version }}'
- aspect 'software.amazon.lambda:powertools-tracing:{{ powertools.version }}'
- aspect 'software.amazon.lambda:powertools-metrics:{{ powertools.version }}'
-// This dependency is needed for Java17+, please uncomment it if you are using Java17+
-// implementation 'org.aspectj:aspectjrt:1.9.19'
- }
-
- sourceCompatibility = 11
- targetCompatibility = 11
+ plugins {
+ id 'java'
+ id 'io.freefair.aspectj.post-compile-weaving' version '8.1.0'
+ }
+
+ repositories {
+ mavenCentral()
+ }
+
+ dependencies {
+ aspect 'software.amazon.lambda:powertools-logging:{{ powertools.version }}'
+ aspect 'software.amazon.lambda:powertools-tracing:{{ powertools.version }}'
+ aspect 'software.amazon.lambda:powertools-metrics:{{ powertools.version }}'
+ }
+
+ sourceCompatibility = 11
+ targetCompatibility = 11
```
+=== "Gradle Java 1.8"
+
+ ```groovy
+ plugins {
+ id 'java'
+ id 'io.freefair.aspectj.post-compile-weaving' version '6.6.3'
+ }
+
+ repositories {
+ mavenCentral()
+ }
+
+ dependencies {
+ aspect 'software.amazon.lambda:powertools-logging:{{ powertools.version }}'
+ aspect 'software.amazon.lambda:powertools-tracing:{{ powertools.version }}'
+ aspect 'software.amazon.lambda:powertools-metrics:{{ powertools.version }}'
+ }
+
+ sourceCompatibility = 1.8
+ targetCompatibility = 1.8
+ ```
+
+???+ tip "Why a different configuration?"
+ Powertools for AWS Lambda (Java) is using [AspectJ](https://eclipse.dev/aspectj/doc/released/progguide/starting.html) internally
+ to handle annotations. Recently, in order to support Java 17 we had to move to `dev.aspectj:aspectj-maven-plugin` because
+ `org.codehaus.mojo:aspectj-maven-plugin` does not support Java 17.
+ Under the hood, `org.codehaus.mojo:aspectj-maven-plugin` is based on AspectJ 1.9.7,
+ while `dev.aspectj:aspectj-maven-plugin` is based on AspectJ 1.9.8, compiled for Java 11+.
+
## Environment variables
!!! info
diff --git a/docs/media/intellij_checkstyle_1.png b/docs/media/intellij_checkstyle_1.png
new file mode 100644
index 000000000..322e24744
Binary files /dev/null and b/docs/media/intellij_checkstyle_1.png differ
diff --git a/docs/media/intellij_checkstyle_2.png b/docs/media/intellij_checkstyle_2.png
new file mode 100644
index 000000000..7fc82187b
Binary files /dev/null and b/docs/media/intellij_checkstyle_2.png differ
diff --git a/docs/media/intellij_checkstyle_3.png b/docs/media/intellij_checkstyle_3.png
new file mode 100644
index 000000000..6e08dde62
Binary files /dev/null and b/docs/media/intellij_checkstyle_3.png differ
diff --git a/docs/processes/maintainers.md b/docs/processes/maintainers.md
new file mode 100644
index 000000000..6b3d9e126
--- /dev/null
+++ b/docs/processes/maintainers.md
@@ -0,0 +1,247 @@
+---
+title: Maintainers playbook
+description: Process
+---
+
+
+
+## Overview
+
+!!! note "Please treat this content as a living document."
+
+This is document explains who the maintainers are, their responsibilities, and how they should be doing it. If you're interested in contributing,
+ see [CONTRIBUTING](https://github.com/aws-powertools/powertools-lambda-java/blob/main/CONTRIBUTING.md){target="_blank"}.
+
+## Current Maintainers
+
+| Maintainer | GitHub ID | Affiliation |
+|-----------------------|---------------------------------------------------------------------------------| ----------- |
+| Jerome Van Der Linden | [jeromevdl](https://github.com/jeromevdl){target="_blank"} | Amazon |
+| Michele Ricciardi | [mriccia](https://github.com/mriccia){target="_blank"} | Amazon |
+| Scott Gerring | [scottgerring](https://github.com/scottgerring){target="_blank"} | Amazon |
+
+## Emeritus
+
+Previous active maintainers who contributed to this project.
+
+| Maintainer | GitHub ID | Affiliation |
+|----------------|-----------------------------------------------------------------------------------------|---------------|
+| Mark Sailes | [msailes](https://github.com/msailes){target="_blank"} | Amazon |
+| Pankaj Agrawal | [pankajagrawal16](https://github.com/pankajagrawal16){target="_blank"} | Former Amazon |
+| Steve Houel | [stevehouel](https://github.com/stevehouel) | Amazon |
+
+## Labels
+
+These are the most common labels used by maintainers to triage issues, pull requests (PR), and for project management:
+
+| Label | Usage | Notes |
+|----------------------------------|---------------------------------------------------------------------------------------------------|----------------------------------------------------|
+| triage | New issues that require maintainers review | Issue template |
+| bug | Unexpected, reproducible and unintended software behavior | PR/Release automation; Doc snippets are excluded; |
+| documentation | Documentation improvements | PR/Release automation; Doc additions, fixes, etc.; |
+| duplicate | Dupe of another issue | |
+| enhancement | New or enhancements to existing features | Issue template |
+| RFC | Technical design documents related to a feature request | Issue template |
+| help wanted | Tasks you want help from anyone to move forward | Bandwidth, complex topics, etc. |
+| feature-parity | Adding features present in other Powertools for Lambda libraries | |
+| good first issue | Somewhere for new contributors to start | |
+| governance | Issues related to project governance - contributor guides, automation, etc. | |
+| question | Issues that are raised to ask questions | |
+| maven | Related to the build system | |
+| need-more-information | Missing information before making any calls | |
+| status/staged-next-release | Changes are merged and will be available once the next release is made. | |
+| status/staged-next-major-release | Contains breaking changes - merged changes will be available once the next major release is made. | |
+| blocked | Issues or PRs that are blocked for varying reasons | Timeline is uncertain |
+| priority:1 | Critical - needs urgent attention | |
+| priority:2 | High - core feature, or affects 60%+ of users | |
+| priority:3 | Neutral - not a core feature, or affects < 40% of users | |
+| priority:4 | Low - nice to have | |
+| priority:5 | Low - idea for later | |
+| invalid | This doesn't seem right | |
+| size/XS | PRs between 0-9 LOC | PR automation |
+| size/S | PRs between 10-29 LOC | PR automation |
+| size/M | PRs between 30-99 LOC | PR automation |
+| size/L | PRs between 100-499 LOC | PR automation |
+| size/XL | PRs between 500-999 LOC, often PRs that grown with feedback | PR automation |
+| size/XXL | PRs with 1K+ LOC, largely documentation related | PR automation |
+| dependencies | Changes that touch dependencies, e.g. Dependabot, etc. | PR/ automation |
+| maintenance | Address outstanding tech debt | |
+
+## Maintainer Responsibilities
+
+Maintainers are active and visible members of the community, and have
+[maintain-level permissions on a repository](https://docs.github.com/en/organizations/managing-access-to-your-organizations-repositories/repository-permission-levels-for-an-organization){target="_blank"}.
+Use those privileges to serve the community and evolve code as follows.
+
+Be aware of recurring ambiguous situations and [document them](#common-scenarios) to help your fellow maintainers.
+
+### Uphold Code of Conduct
+
+
+Model the behavior set forward by the
+[Code of Conduct](https://github.com/aws-powertools/powertools-lambda-java/blob/main/CODE_OF_CONDUCT.md){target="_blank"}
+and raise any violations to other maintainers and admins. There could be unusual circumstances where inappropriate
+behavior does not immediately fall within the [Code of Conduct](https://github.com/aws-powertools/powertools-lambda-java/blob/main/CODE_OF_CONDUCT.md){target="_blank"}.
+
+These might be nuanced and should be handled with extra care - when in doubt, do not engage and reach out to other maintainers
+and admins.
+
+### Prioritize Security
+
+Security is your number one priority. Maintainer's Github keys must be password protected securely and any reported
+security vulnerabilities are addressed before features or bugs.
+
+Note that this repository is monitored and supported 24/7 by Amazon Security, see
+[Security disclosures](https://github.com/aws-powertools/powertools-lambda-java/){target="_blank"} for details.
+
+### Review Pull Requests
+
+Review pull requests regularly, comment, suggest, reject, merge and close. Accept only high quality pull-requests.
+Provide code reviews and guidance on incoming pull requests.
+
+PRs are [labeled](#labels) based on file changes and semantic title. Pay attention to whether labels reflect the current
+state of the PR and correct accordingly.
+
+Use and enforce [semantic versioning](https://semver.org/){target="_blank" rel="nofollow"} pull request titles, as these will be used for
+[CHANGELOG](https://github.com/aws-powertools/powertools-lambda-java/blob/main/CHANGELOG.md){target="_blank"}
+and [Release notes](https://github.com/aws-powertools/powertools-lambda-java/releases) - make sure they communicate their
+intent at the human level.
+
+For issues linked to a PR, make sure `status/staged-next-release` label is applied to them when merging.
+[Upon release](#releasing-a-new-version), these issues will be notified which release version contains their change.
+
+See [Common scenarios](#common-scenarios) section for additional guidance.
+
+### Triage New Issues
+
+Manage [labels](#labels), review issues regularly, and create new labels as needed by the project. Remove `triage`
+label when you're able to confirm the validity of a request, a bug can be reproduced, etc.
+Give priority to the original author for implementation, unless it is a sensitive task that is best handled by maintainers.
+
+Make sure issues are assigned to our [board of activities](https://github.com/orgs/aws-powertools/projects/4).
+
+Use our [labels](#labels) to signal good first issues to new community members, and to set expectation that this might
+need additional feedback from the author, other customers, experienced community members and/or maintainers.
+
+Be aware of [casual contributors](https://opensource.com/article/17/10/managing-casual-contributors){target="_blank" rel="nofollow"} and recurring contributors.
+Provide the experience and attention you wish you had if you were starting in open source.
+
+See [Common scenarios](#common-scenarios) section for additional guidance.
+
+### Triage Bug Reports
+
+Be familiar with [our definition of bug](#is-that-a-bug). If it's not a bug, you can close it or adjust its title and
+labels - always communicate the reason accordingly.
+
+For bugs caused by upstream dependencies, replace `bug` with `bug-upstream` label. Ask the author whether they'd like to
+raise the issue upstream or if they prefer us to do so.
+
+Assess the impact and make the call on whether we need an emergency release. Contact other [maintainers](#current-maintainers) when in doubt.
+
+See [Common scenarios](#common-scenarios) section for additional guidance.
+
+### Triage RFCs
+
+RFC is a collaborative process to help us get to the most optimal solution given the context. Their purpose is to ensure
+everyone understands what this context is, their trade-offs, and alternative solutions that were part of the research
+before implementation begins.
+
+Make sure you ask these questions in mind when reviewing:
+
+- Does it use our [RFC template](https://github.com/aws-powertools/powertools-lambda-java/issues/new?assignees=&labels=RFC%2C+triage&projects=&template=rfc.md&title=RFC%3A+)?
+- Does it match our [Tenets](https://docs.powertools.aws.dev/lambda/java/latest/#tenets)?
+- Does the proposal address the use case? If so, is the recommended usage explicit?
+- Does it focus on the mechanics to solve the use case over fine-grained implementation details?
+- Can anyone familiar with the code base implement it?
+- If approved, are they interested in contributing? Do they need any guidance?
+- Does this significantly increase the overall project maintenance? Do we have the skills to maintain it?
+- If we can't take this use case, are there alternative projects we could recommend? Or does it call for a new project altogether?
+
+When necessary, be upfront that the time to review, approve, and implement a RFC can vary -
+see [Contribution is stuck](#contribution-is-stuck). Some RFCs may be further updated after implementation, as certain areas become clearer.
+
+Some examples using our initial and new RFC templates: #92, #94, #95, #991, #1226
+
+### Releasing a new version
+
+!!! note "The release process is currently a long, multi-step process. The team is in the process of automating at it."
+
+Firstly, make sure the commit history in the `main` branch **(1)** it's up to date, **(2)** commit messages are semantic,
+and **(3)** commit messages have their respective area, for example `feat: `, `chore: ...`).
+
+**Looks good, what's next?**
+
+Kickoff the `Prepare for maven central release` workflow with the intended rekease version. Once this has completed, it will
+draft a Pull Request named something like `chore: Prep release 1.19.0`. the PR will **(1)** roll all of the POM versions
+forward to the new release version and **(2)** release notes.
+
+Once this is done, check out the branch and clean up the release notes. These will be used both in the
+[CHANGELOG.md file](https://github.com/aws-powertools/powertools-lambda-java/blob/main/CHANGELOG.md)
+file and the [published github release information](https://github.com/aws-powertools/powertools-lambda-java/releases),
+and you can use the existing release notes to see how changes are summarized.
+
+Next, commit and push, wait for the build to complete, and merge to main. Once main has built successfully (i.e. build, tests and end-to-end tests should pass), create a
+tagged release from the Github UI, using the same release notes.
+
+Next, run the `Publish package to the Maven Central Repository` action to release the library.
+
+Finally, by hand, create a PR rolling all of the POMs onto the next snapshot version (e.g. `1.20.0-SNAPSHOT`).
+
+
+### Add Continuous Integration Checks
+
+Add integration checks that validate pull requests and pushes to ease the burden on Pull Request reviewers.
+Continuously revisit areas of improvement to reduce operational burden in all parties involved.
+
+### Negative Impact on the Project
+
+Actions that negatively impact the project will be handled by the admins, in coordination with other maintainers,
+in balance with the urgency of the issue. Examples would be
+[Code of Conduct](https://github.com/aws-powertools/powertools-lambda-java/blob/main/CODE_OF_CONDUCT.md){target="_blank"}
+violations, deliberate harmful or malicious actions, spam, monopolization, and security risks.
+
+## Common scenarios
+
+These are recurring ambiguous situations that new and existing maintainers may encounter. They serve as guidance.
+It is up to each maintainer to follow, adjust, or handle in a different manner as long as
+[our conduct is consistent](#uphold-code-of-conduct)
+
+### Contribution is stuck
+
+A contribution can get stuck often due to lack of bandwidth and language barrier. For bandwidth issues,
+check whether the author needs help. Make sure you get their permission before pushing code into their existing PR -
+do not create a new PR unless strictly necessary.
+
+For language barrier and others, offer a 1:1 chat to get them unblocked. Often times, English might not be their
+primary language, and writing in public might put them off, or come across not the way they intended to be.
+
+In other cases, you may have constrained capacity. Use `help wanted` label when you want to signal other maintainers
+and external contributors that you could use a hand to move it forward.
+
+### Insufficient feedback or information
+
+When in doubt, use the `need-more-information` label to signal more context and feedback are necessary before proceeding.
+
+### Crediting contributions
+
+We credit all contributions as part of each [release note](https://github.com/aws-powertools/powertools-lambda-java/releases){target="_blank"}
+as an automated process. If you find contributors are missing from the release note you're producing, please add them manually.
+
+### Is that a bug?
+
+A bug produces incorrect or unexpected results at runtime that differ from its intended behavior.
+Bugs must be reproducible. They directly affect customers experience at runtime despite following its recommended usage.
+
+Documentation snippets, use of internal components, or unadvertised functionalities are not considered bugs.
+
+### Mentoring contributions
+
+Always favor mentoring issue authors to contribute, unless they're not interested or the implementation is sensitive (_e.g., complexity, time to release, etc._).
+
+Make use of `help wanted` and `good first issue` to signal additional contributions the community can help.
+
+### Long running issues or PRs
+
+Try offering a 1:1 call in the attempt to get to a mutual understanding and clarify areas that maintainers could help.
+
+In the rare cases where both parties don't have the bandwidth or expertise to continue, it's best to use the `revisit-in-3-months` label. By then, see if it's possible to break the PR or issue in smaller chunks, and eventually close if there is no progress.
diff --git a/docs/utilities/batch.md b/docs/utilities/batch.md
index 0a2add081..95704b8a0 100644
--- a/docs/utilities/batch.md
+++ b/docs/utilities/batch.md
@@ -4,6 +4,9 @@ description: Utility
---
The SQS batch processing utility provides a way to handle partial failures when processing batches of messages from SQS.
+The utility handles batch processing for both
+[standard](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/standard-queues.html) and
+[FIFO](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/FIFO-queues.html) SQS queues.
**Key Features**
@@ -23,10 +26,11 @@ are returned to the queue.
## Install
-To install this utility, add the following dependency to your project.
+Depending on your version of Java (either Java 1.8 or 11+), the configuration slightly changes.
-=== "Maven"
- ```xml hl_lines="3 4 5 6 7 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36"
+=== "Maven Java 11+"
+
+ ```xml hl_lines="3-7 16 18 24-27""
...
@@ -36,6 +40,7 @@ To install this utility, add the following dependency to your project.
...
+ ...
@@ -44,6 +49,51 @@ To install this utility, add the following dependency to your project.
dev.aspectjaspectj-maven-plugin1.13.1
+
+ 11
+ 11
+ 11
+
+
+ software.amazon.lambda
+ powertools-sqs
+
+
+
+
+
+
+ compile
+
+
+
+
+ ...
+
+
+ ```
+
+=== "Maven Java 1.8"
+
+ ```xml hl_lines="3-7 16 18 24-27"
+
+ ...
+
+ software.amazon.lambda
+ powertools-sqs
+ {{ powertools.version }}
+
+ ...
+
+ ...
+
+
+
+ ...
+
+ org.codehaus.mojo
+ aspectj-maven-plugin
+ 1.14.01.81.8
@@ -68,24 +118,44 @@ To install this utility, add the following dependency to your project.
```
-=== "Gradle"
+=== "Gradle Java 11+"
- ```groovy
- plugins{
- id 'java'
- id 'io.freefair.aspectj.post-compile-weaving' version '6.3.0'
- }
+ ```groovy hl_lines="3 11"
+ plugins {
+ id 'java'
+ id 'io.freefair.aspectj.post-compile-weaving' version '8.1.0'
+ }
+
+ repositories {
+ mavenCentral()
+ }
+
+ dependencies {
+ aspect 'software.amazon.lambda:powertools-sqs:{{ powertools.version }}'
+ }
+
+ sourceCompatibility = 11 // or higher
+ targetCompatibility = 11 // or higher
+ ```
- repositories {
- mavenCentral()
- }
+=== "Gradle Java 1.8"
- dependencies {
- ...
- aspect 'software.amazon.lambda:powertools-sqs:{{ powertools.version }}'
-// This dependency is needed for Java17+, please uncomment it if you are using Java17+
-// implementation 'org.aspectj:aspectjrt:1.9.19'
- }
+ ```groovy hl_lines="3 11"
+ plugins {
+ id 'java'
+ id 'io.freefair.aspectj.post-compile-weaving' version '6.6.3'
+ }
+
+ repositories {
+ mavenCentral()
+ }
+
+ dependencies {
+ aspect 'software.amazon.lambda:powertools-sqs:{{ powertools.version }}'
+ }
+
+ sourceCompatibility = 1.8
+ targetCompatibility = 1.8
```
## IAM Permissions
@@ -110,8 +180,11 @@ Both have nearly the same behaviour when it comes to processing messages from th
* **Entire batch has been successfully processed**, where your Lambda handler returned successfully, we will let SQS delete the batch to optimize your cost
* **Entire Batch has been partially processed successfully**, where exceptions were raised within your `SqsMessageHandler` interface implementation, we will:
- **1)** Delete successfully processed messages from the queue by directly calling `sqs:DeleteMessageBatch`
- - **2)** if, non retryable exceptions occur, messages resulting in configured exceptions during processing will be immediately moved to the dead letter queue associated to the source SQS queue or deleted from the source SQS queue if `deleteNonRetryableMessageFromQueue` is set to `true`.
- - **3)** Raise `SQSBatchProcessingException` to ensure failed messages return to your SQS queue
+ - **2)** If a message with a [message group ID](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/using-messagegroupid-property.html) fails,
+ the processing of the batch will be stopped and the remainder of the messages will be returned to SQS.
+ This behaviour [is required to handle SQS FIFO queues](https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html#services-sqs-batchfailurereporting).
+ - **3)** if non retryable exceptions occur, messages resulting in configured exceptions during processing will be immediately moved to the dead letter queue associated to the source SQS queue or deleted from the source SQS queue if `deleteNonRetryableMessageFromQueue` is set to `true`.
+ - **4)** Raise `SQSBatchProcessingException` to ensure failed messages return to your SQS queue
The only difference is that **SqsUtils Utility API** will give you access to return from the processed messages if you need. Exception `SQSBatchProcessingException` thrown from the
utility will have access to both successful and failed messaged along with failure exceptions.
diff --git a/docs/utilities/idempotency.md b/docs/utilities/idempotency.md
index 2f4774c94..5392b8d4c 100644
--- a/docs/utilities/idempotency.md
+++ b/docs/utilities/idempotency.md
@@ -26,8 +26,11 @@ times with the same parameters**. This makes idempotent operations safe to retry
## Getting started
### Installation
-=== "Maven"
- ```xml hl_lines="3-7 24-27"
+Depending on your version of Java (either Java 1.8 or 11+), the configuration slightly changes.
+
+=== "Maven Java 11+"
+
+ ```xml hl_lines="3-7 16 18 24-27"
...
@@ -37,7 +40,7 @@ times with the same parameters**. This makes idempotent operations safe to retry
...
-
+ ...
@@ -46,6 +49,51 @@ times with the same parameters**. This makes idempotent operations safe to retry
dev.aspectjaspectj-maven-plugin1.13.1
+
+ 11
+ 11
+ 11
+
+
+ software.amazon.lambda
+ powertools-idempotency
+
+
+
+
+
+
+ compile
+
+
+
+
+ ...
+
+
+ ```
+
+=== "Maven Java 1.8"
+
+ ```xml hl_lines="3-7 16 18 24-27"
+
+ ...
+
+ software.amazon.lambda
+ powertools-idempotency
+ {{ powertools.version }}
+
+ ...
+
+ ...
+
+
+
+ ...
+
+ org.codehaus.mojo
+ aspectj-maven-plugin
+ 1.14.01.81.8
@@ -55,7 +103,6 @@ times with the same parameters**. This makes idempotent operations safe to retry
software.amazon.lambdapowertools-idempotency
- ...
@@ -71,6 +118,46 @@ times with the same parameters**. This makes idempotent operations safe to retry
```
+=== "Gradle Java 11+"
+
+ ```groovy hl_lines="3 11"
+ plugins {
+ id 'java'
+ id 'io.freefair.aspectj.post-compile-weaving' version '8.1.0'
+ }
+
+ repositories {
+ mavenCentral()
+ }
+
+ dependencies {
+ aspect 'software.amazon.lambda:powertools-idempotency:{{ powertools.version }}'
+ }
+
+ sourceCompatibility = 11 // or higher
+ targetCompatibility = 11 // or higher
+ ```
+
+=== "Gradle Java 1.8"
+
+ ```groovy hl_lines="3 11"
+ plugins {
+ id 'java'
+ id 'io.freefair.aspectj.post-compile-weaving' version '6.6.3'
+ }
+
+ repositories {
+ mavenCentral()
+ }
+
+ dependencies {
+ aspect 'software.amazon.lambda:powertools-idempotency:{{ powertools.version }}'
+ }
+
+ sourceCompatibility = 1.8
+ targetCompatibility = 1.8
+ ```
+
### Required resources
Before getting started, you need to create a persistent storage layer where the idempotency utility can store its state - your Lambda functions will need read and write access to it.
@@ -273,7 +360,7 @@ Imagine the function executes successfully, but the client never receives the re
!!! warning "Warning: Idempotency for JSON payloads"
The payload extracted by the `EventKeyJMESPath` is treated as a string by default, so will be sensitive to differences in whitespace even when the JSON payload itself is identical.
- To alter this behaviour, you can use the [JMESPath built-in function](utilities.md#powertools_json-function) `powertools_json()` to treat the payload as a JSON object rather than a string.
+ To alter this behaviour, you can use the [JMESPath built-in function](serialization.md#jmespath-functions) `powertools_json()` to treat the payload as a JSON object rather than a string.
=== "PaymentFunction.java"
diff --git a/docs/utilities/large_messages.md b/docs/utilities/large_messages.md
new file mode 100644
index 000000000..c4947a6e8
--- /dev/null
+++ b/docs/utilities/large_messages.md
@@ -0,0 +1,419 @@
+---
+title: Large Messages
+description: Utility
+---
+
+The large message utility handles SQS and SNS messages which have had their payloads
+offloaded to S3 if they are larger than the maximum allowed size (256 KB).
+
+!!! Notice
+ The large message utility (available in the `powertools-sqs` module for versions v1.16.1 and earlier) is now deprecated
+ and replaced by the `powertools-large-messages` described in this page.
+ You can still get the documentation [here](sqs_large_message_handling.md)
+ and the migration guide [here](#migration-from-the-sqs-large-message-utility).
+
+## Features
+
+- Automatically retrieve the content of S3 objects when SQS or SNS messages have been offloaded to S3.
+- Automatically delete the S3 Objects after processing succeeds.
+- Compatible with the batch module (with SQS).
+
+## Background
+
+```mermaid
+stateDiagram-v2
+ direction LR
+ Function : Lambda Function
+
+ state Application {
+ direction TB
+ sendMsg: sendMessage(QueueUrl, MessageBody)
+ extendLib: extended-client-lib
+ [*] --> sendMsg
+ sendMsg --> extendLib
+ state extendLib {
+ state if_big <>
+ bigMsg: MessageBody > 256KB ?
+ putObject: putObject(S3Bucket, S3Key, Body)
+ updateMsg: Update MessageBody with a pointer to S3 and add a message attribute
+ bigMsg --> if_big
+ if_big --> [*]: size(body) <= 256kb
+ if_big --> putObject: size(body) > 256kb
+ putObject --> updateMsg
+ updateMsg --> [*]
+ }
+ }
+
+ state Function {
+ direction TB
+ iterateMsgs: Iterate over messages
+ ptLargeMsg: powertools-large-messages
+ [*] --> Handler
+ Handler --> iterateMsgs
+ iterateMsgs --> ptLargeMsg
+ state ptLargeMsg {
+ state if_pointer <>
+ pointer: Message attribute for large message ?
+ normalMsg: Small message, body left unchanged
+ getObject: getObject(S3Pointer)
+ deleteObject: deleteObject(S3Pointer)
+ updateBody: Update message body with content from S3 object and remove message attribute
+ updateMD5: Update MD5 of the body and attributes (SQS only)
+ yourcode: YOUR CODE HERE!
+ pointer --> if_pointer
+ if_pointer --> normalMsg : False
+ normalMsg --> [*]
+ if_pointer --> getObject : True
+ getObject --> updateBody
+ updateBody --> updateMD5
+ updateMD5 --> yourcode
+ yourcode --> deleteObject
+ deleteObject --> [*]
+ }
+ }
+
+ [*] --> Application
+ Application --> Function : Lambda Invocation
+ Function --> [*]
+
+```
+
+SQS and SNS message payload is limited to 256KB. If you wish to send messages with a larger payload, you can leverage the
+[amazon-sqs-java-extended-client-lib](https://github.com/awslabs/amazon-sqs-java-extended-client-lib)
+or [amazon-sns-java-extended-client-lib](https://github.com/awslabs/amazon-sns-java-extended-client-lib) which
+offload the message to Amazon S3. See documentation
+([SQS](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-s3-messages.html)
+/ [SNS](https://docs.aws.amazon.com/sns/latest/dg/large-message-payloads.html))
+
+When offloaded to S3, the message contains a specific message attribute and the payload only contains a pointer to the
+S3 object (bucket and object key).
+
+This utility automatically retrieves messages which have been offloaded to S3 using the
+extended client libraries. Once a message's payload has been processed successfully, the
+utility deletes the payload from S3.
+
+This utility is compatible with
+versions *[1.1.0+](https://github.com/awslabs/amazon-sqs-java-extended-client-lib/releases/tag/1.1.0)*
+of amazon-sqs-java-extended-client-lib
+and *[1.0.0+](https://github.com/awslabs/amazon-sns-java-extended-client-lib/releases/tag/1.0.0)*
+of amazon-sns-java-extended-client-lib.
+
+## Install
+
+Depending on your version of Java (either Java 1.8 or 11+), the configuration slightly changes.
+
+=== "Maven Java 11+"
+```xml hl_lines="3-7 16 18 24-27"
+
+...
+
+software.amazon.lambda
+powertools-large-messages
+{{ powertools.version }}
+
+...
+
+...
+
+
+
+...
+
+dev.aspectj
+aspectj-maven-plugin
+1.13.1
+
+11
+11
+11
+
+
+software.amazon.lambda
+powertools-large-messages
+
+
+
+
+
+
+compile
+
+
+
+
+...
+
+
+```
+
+=== "Maven Java 1.8"
+
+ ```xml hl_lines="3-7 16 18 24-27"
+
+ ...
+
+ software.amazon.lambda
+ powertools-large-messages
+ {{ powertools.version }}
+
+ ...
+
+ ...
+
+
+
+ ...
+
+ org.codehaus.mojo
+ aspectj-maven-plugin
+ 1.14.0
+
+ 1.8
+ 1.8
+ 1.8
+
+
+ software.amazon.lambda
+ powertools-large-messages
+
+
+
+
+
+
+ compile
+
+
+
+
+ ...
+
+
+ ```
+
+=== "Gradle Java 11+"
+
+ ```groovy hl_lines="3 11"
+ plugins {
+ id 'java'
+ id 'io.freefair.aspectj.post-compile-weaving' version '8.1.0'
+ }
+
+ repositories {
+ mavenCentral()
+ }
+
+ dependencies {
+ aspect 'software.amazon.lambda:powertools-large-messages:{{ powertools.version }}'
+ }
+
+ sourceCompatibility = 11 // or higher
+ targetCompatibility = 11 // or higher
+ ```
+
+=== "Gradle Java 1.8"
+
+ ```groovy hl_lines="3 11"
+ plugins {
+ id 'java'
+ id 'io.freefair.aspectj.post-compile-weaving' version '6.6.3'
+ }
+
+ repositories {
+ mavenCentral()
+ }
+
+ dependencies {
+ aspect 'software.amazon.lambda:powertools-large-messages:{{ powertools.version }}'
+ }
+
+ sourceCompatibility = 1.8
+ targetCompatibility = 1.8
+ ```
+
+## Permissions
+
+As the utility interacts with Amazon S3, the lambda function must have the following permissions
+on the S3 bucket used for the large messages offloading:
+
+- `s3:GetObject`
+- `s3:DeleteObject`
+
+## Annotation
+
+The annotation `@LargeMessage` can be used on any method where the *first* parameter is one of:
+
+- `SQSEvent.SQSMessage`
+- `SNSEvent.SNSRecord`
+
+=== "SQS Example"
+
+ ```java hl_lines="8 13 15"
+ import software.amazon.lambda.powertools.largemessages.LargeMessage;
+
+ public class SqsMessageHandler implements RequestHandler {
+
+ @Override
+ public SQSBatchResponse handleRequest(SQSEvent event, Context context) {
+ for (SQSMessage message: event.getRecords()) {
+ processRawMessage(message, context);
+ }
+ return SQSBatchResponse.builder().build();
+ }
+
+ @LargeMessage
+ private void processRawMessage(SQSEvent.SQSMessage sqsMessage, Context context) {
+ // sqsMessage.getBody() will contain the content of the S3 object
+ }
+ }
+ ```
+
+=== "SNS Example"
+
+ ```java hl_lines="7 11 13"
+ import software.amazon.lambda.powertools.largemessages.LargeMessage;
+
+ public class SnsRecordHandler implements RequestHandler {
+
+ @Override
+ public String handleRequest(SNSEvent event, Context context) {
+ processSNSRecord(event.records.get(0)); // there are always only one message
+ return "Hello World";
+ }
+
+ @LargeMessage
+ private void processSNSRecord(SNSEvent.SNSRecord snsRecord) {
+ // snsRecord.getSNS().getMessage() will contain the content of the S3 object
+ }
+ }
+ ```
+
+When the Lambda function is invoked with a SQS or SNS event, the utility first
+checks if the content was offloaded to S3. In the case of a large message, there is a message attribute
+specifying the size of the offloaded message and the message contains a pointer to the S3 object.
+
+If this is the case, the utility will retrieve the object from S3 using the `getObject(bucket, key)` API,
+and place the content of the object in the message payload. You can then directly use the content of the message.
+If there was an error during the S3 download, the function will fail with a `LargeMessageProcessingException`.
+
+After your code is invoked and returns without error, the object is deleted from S3
+using the `deleteObject(bucket, key)` API. You can disable the deletion of S3 objects with the following configuration:
+
+=== "Don't delete S3 Objects"
+ ```java
+ @LargeMessage(deleteS3Object = false)
+ private void processRawMessage(SQSEvent.SQSMessage sqsMessage) {
+ // do something with the message
+ }
+ ```
+
+!!! tip "Use together with batch module"
+ This utility works perfectly together with the batch module (`powertools-batch`), especially for SQS:
+
+ ```java hl_lines="2 5-7 12 15 16" title="Combining batch and large message modules"
+ public class SqsBatchHandler implements RequestHandler {
+ private final BatchMessageHandler handler;
+
+ public SqsBatchHandler() {
+ handler = new BatchMessageHandlerBuilder()
+ .withSqsBatchHandler()
+ .buildWithRawMessageHandler(this::processMessage);
+ }
+
+ @Override
+ public SQSBatchResponse handleRequest(SQSEvent sqsEvent, Context context) {
+ return handler.processBatch(sqsEvent, context);
+ }
+
+ @LargeMessage
+ private void processMessage(SQSEvent.SQSMessage sqsMessage) {
+ // do something with the message
+ }
+ }
+ ```
+
+!!! tip "Use together with idempotency module"
+ This utility also works together with the idempotency module (`powertools-idempotency`).
+ You can add both the `@LargeMessage` and `@Idempotent` annotations, in any order, to the same method.
+ The `@Idempotent` takes precedence over the `@LargeMessage` annotation.
+ It means Idempotency module will use the initial raw message (containing the S3 pointer) and not the large message.
+
+ ```java hl_lines="6 23-25" title="Combining idempotency and large message modules"
+ public class SqsBatchHandler implements RequestHandler {
+
+ public SqsBatchHandler() {
+ Idempotency.config().withConfig(
+ IdempotencyConfig.builder()
+ .withEventKeyJMESPath("body") // get the body of the message for the idempotency key
+ .build())
+ .withPersistenceStore(
+ DynamoDBPersistenceStore.builder()
+ .withTableName(System.getenv("IDEMPOTENCY_TABLE"))
+ .build()
+ ).configure();
+ }
+
+ @Override
+ public SQSBatchResponse handleRequest(SQSEvent sqsEvent, Context context) {
+ for (SQSMessage message: event.getRecords()) {
+ processRawMessage(message, context);
+ }
+ return SQSBatchResponse.builder().build();
+ }
+
+ @Idempotent
+ @LargeMessage
+ private String processRawMessage(@IdempotencyKey SQSEvent.SQSMessage sqsMessage, Context context) {
+ // do something with the message
+ }
+ }
+ ```
+
+## Customizing S3 client configuration
+
+To interact with S3, the utility creates a default S3 Client :
+
+=== "Default S3 Client"
+ ```java
+ S3Client client = S3Client.builder()
+ .httpClient(UrlConnectionHttpClient.builder().build())
+ .region(Region.of(System.getenv(AWS_REGION_ENV)))
+ .build();
+ ```
+
+If you need to customize this `S3Client`, you can leverage the `LargeMessageConfig` singleton:
+
+=== "Custom S3 Client"
+ ```java hl_lines="6"
+ import software.amazon.lambda.powertools.largemessages.LargeMessage;
+
+ public class SnsRecordHandler implements RequestHandler {
+
+ public SnsRecordHandler() {
+ LargeMessageConfig.init().withS3Client(/* put your custom S3Client here */);
+ }
+
+ @Override
+ public String handleRequest(SNSEvent event, Context context) {
+ processSNSRecord(event.records.get(0));
+ return "Hello World";
+ }
+
+ @LargeMessage
+ private void processSNSRecord(SNSEvent.SNSRecord snsRecord) {
+ // snsRecord.getSNS().getMessage() will contain the content of the S3 object
+ }
+ }
+ ```
+
+## Migration from the SQS Large Message utility
+
+- Replace the dependency in maven / gradle: `powertools-sqs` ==> `powertools-large-messages`
+- Replace the annotation: `@SqsLargeMessage` ==> `@LargeMessage` (the new module handles both SQS and SNS)
+- Move the annotation away from the Lambda `handleRequest` method and put it on a method with `SQSEvent.SQSMessage`
+ or `SNSEvent.SNSRecord` as first parameter.
+- The annotation now handles a single message, contrary to the previous version that was handling the complete batch.
+ It gives more control, especially when dealing with partial failures with SQS (see the batch module).
+- The new module only provides an annotation, an equivalent to the `SqsUtils` class is not available anymore in this new version.
+
+Finally, if you are still using the `powertools-sqs` library for batch processing, consider moving to `powertools-batch` at the same time to remove the dependency on this library completely; it has been deprecated and will be removed in v2.
\ No newline at end of file
diff --git a/docs/utilities/parameters.md b/docs/utilities/parameters.md
index 7e70248d4..85d30d77e 100644
--- a/docs/utilities/parameters.md
+++ b/docs/utilities/parameters.md
@@ -16,27 +16,136 @@ It also provides a base class to create your parameter provider implementation.
* Transform parameter values from JSON or base 64 encoded strings
## Install
+Depending on your version of Java (either Java 1.8 or 11+), the configuration slightly changes.
-To install this utility, add the following dependency to your project.
+=== "Maven Java 11+"
-=== "Maven"
-
- ```xml
-
- software.amazon.lambda
- powertools-parameters
- {{ powertools.version }}
-
+ ```xml hl_lines="3-7 16 18 24-27"
+
+ ...
+
+ software.amazon.lambda
+ powertools-parameters
+ {{ powertools.version }}
+
+ ...
+
+ ...
+
+
+
+ ...
+
+ dev.aspectj
+ aspectj-maven-plugin
+ 1.13.1
+
+ 11
+ 11
+ 11
+
+
+ software.amazon.lambda
+ powertools-parameters
+
+
+
+
+
+
+ compile
+
+
+
+
+ ...
+
+
```
-=== "Gradle"
- ```groovy
- dependencies {
+=== "Maven Java 1.8"
+
+ ```xml hl_lines="3-7 16 18 24-27"
+
...
- aspect 'software.amazon.lambda:powertools-parameters:{{ powertools.version }}'
-// This dependency is needed for Java17+, please uncomment it if you are using Java17+
-// implementation 'org.aspectj:aspectjrt:1.9.19'
- }
+
+ software.amazon.lambda
+ powertools-parameters
+ {{ powertools.version }}
+
+ ...
+
+ ...
+
+
+
+ ...
+
+ org.codehaus.mojo
+ aspectj-maven-plugin
+ 1.14.0
+
+ 1.8
+ 1.8
+ 1.8
+
+
+ software.amazon.lambda
+ powertools-parameters
+
+
+
+
+
+
+ compile
+
+
+
+
+ ...
+
+
+ ```
+
+=== "Gradle Java 11+"
+
+ ```groovy hl_lines="3 11"
+ plugins {
+ id 'java'
+ id 'io.freefair.aspectj.post-compile-weaving' version '8.1.0'
+ }
+
+ repositories {
+ mavenCentral()
+ }
+
+ dependencies {
+ aspect 'software.amazon.lambda:powertools-parameters:{{ powertools.version }}'
+ }
+
+ sourceCompatibility = 11 // or higher
+ targetCompatibility = 11 // or higher
+ ```
+
+=== "Gradle Java 1.8"
+
+ ```groovy hl_lines="3 11"
+ plugins {
+ id 'java'
+ id 'io.freefair.aspectj.post-compile-weaving' version '6.6.3'
+ }
+
+ repositories {
+ mavenCentral()
+ }
+
+ dependencies {
+ aspect 'software.amazon.lambda:powertools-parameters:{{ powertools.version }}'
+ }
+
+ sourceCompatibility = 1.8
+ targetCompatibility = 1.8
```
**IAM Permissions**
diff --git a/docs/utilities/serialization.md b/docs/utilities/serialization.md
index bd654393f..b47bdbd91 100644
--- a/docs/utilities/serialization.md
+++ b/docs/utilities/serialization.md
@@ -234,14 +234,14 @@ It can also handle a collection of elements like the records of an SQS event:
| `APIGatewayV2HTTPEvent` | `body` | |
| `SNSEvent` | `Records[0].Sns.Message` | |
| `SQSEvent` | `Records[*].body` | x |
- | `ScheduledEvent` | `detail` | |
- | `ApplicationLoadBalancerRequestEvent` | `body` | |
- | `CloudWatchLogsEvent` | `powertools_base64_gzip(data)` | |
- | `CloudFormationCustomResourceEvent` | `resourceProperties` | |
- | `KinesisEvent` | `Records[*].kinesis.powertools_base64(data)` | x |
- | `KinesisFirehoseEvent` | `Records[*].powertools_base64(data)` | x |
- | `KafkaEvent` | `records[*].values[*].powertools_base64(value)` | x |
- | `ActiveMQEvent` | `messages[*].powertools_base64(data)` | x |
+| `ScheduledEvent` | `detail` | |
+| `ApplicationLoadBalancerRequestEvent` | `body` | |
+| `CloudWatchLogsEvent` | `powertools_base64_gzip(data)` | |
+| `CloudFormationCustomResourceEvent` | `resourceProperties` | |
+| `KinesisEvent` | `Records[*].kinesis.powertools_base64(data)` | x |
+| `KinesisFirehoseEvent` | `Records[*].powertools_base64(data)` | x |
+| `KafkaEvent` | `records[*].values[*].powertools_base64(value)` | x |
+| `ActiveMQEvent` | `messages[*].powertools_base64(data)` | x |
| `RabbitMQEvent` | `rmqMessagesByQueue[*].values[*].powertools_base64(data)` | x |
| `KinesisAnalyticsFirehoseInputPreprocessingEvent` | `Records[*].kinesis.powertools_base64(data)` | x |
| `KinesisAnalyticsStreamsInputPreprocessingEvent` | `Records[*].kinesis.powertools_base64(data)` | x |
diff --git a/docs/utilities/sqs_large_message_handling.md b/docs/utilities/sqs_large_message_handling.md
deleted file mode 100644
index 5f7ede095..000000000
--- a/docs/utilities/sqs_large_message_handling.md
+++ /dev/null
@@ -1,220 +0,0 @@
----
-title: SQS Large Message Handling
-description: Utility
----
-
-The large message handling utility handles SQS messages which have had their payloads
-offloaded to S3 due to them being larger than the SQS maximum.
-
-The utility automatically retrieves messages which have been offloaded to S3 using the
-[amazon-sqs-java-extended-client-lib](https://github.com/awslabs/amazon-sqs-java-extended-client-lib)
-client library. Once the message payloads have been processed successful the
-utility can delete the message payloads from S3.
-
-This utility is compatible with versions *[1.1.0+](https://github.com/awslabs/amazon-sqs-java-extended-client-lib)* of amazon-sqs-java-extended-client-lib.
-
-=== "Maven"
- ```xml
-
- com.amazonaws
- amazon-sqs-java-extended-client-lib
- 1.1.0
-
- ```
-=== "Gradle"
-
- ```groovy
- dependencies {
- implementation 'com.amazonaws:amazon-sqs-java-extended-client-lib:1.1.0'
- }
- ```
-
-## Install
-
-To install this utility, add the following dependency to your project.
-
-=== "Maven"
- ```xml hl_lines="3 4 5 6 7 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36"
-
- ...
-
- software.amazon.lambda
- powertools-sqs
- {{ powertools.version }}
-
- ...
-
-
-
-
- ...
-
- dev.aspectj
- aspectj-maven-plugin
- 1.13.1
-
- 1.8
- 1.8
- 1.8
-
-
- software.amazon.lambda
- powertools-sqs
-
-
-
-
-
-
- compile
-
-
-
-
- ...
-
-
- ```
-
-=== "Gradle"
-
- ```groovy
- plugins{
- id 'java'
- id 'io.freefair.aspectj.post-compile-weaving' version '6.3.0'
- }
-
- repositories {
- mavenCentral()
- }
-
- dependencies {
- ...
- aspect 'software.amazon.lambda:powertools-sqs:{{ powertools.version }}'
-// This dependency is needed for Java17+, please uncomment it if you are using Java17+
-// implementation 'org.aspectj:aspectjrt:1.9.19'
- }
- ```
-
-## Lambda handler
-
-The annotation `@SqsLargeMessage` should be used with the handleRequest method of a class
-which implements `com.amazonaws.services.lambda.runtime.RequestHandler` with
-`com.amazonaws.services.lambda.runtime.events.SQSEvent` as the first parameter.
-
-=== "SqsMessageHandler.java"
-
- ```java hl_lines="6"
- import software.amazon.lambda.powertools.sqs.SqsLargeMessage;
-
- public class SqsMessageHandler implements RequestHandler {
-
- @Override
- @SqsLargeMessage
- public String handleRequest(SQSEvent sqsEvent, Context context) {
- // process messages
-
- return "ok";
- }
- }
- ```
-
-`@SqsLargeMessage` creates a default S3 Client `AmazonS3 amazonS3 = AmazonS3ClientBuilder.defaultClient()`.
-
-!!! tip
- When the Lambda function is invoked with an event from SQS, each received record
- in the SQSEvent is checked to see to validate if it is offloaded to S3.
- If it does then `getObject(bucket, key)` will be called, and the payload retrieved.
- If there is an error during this process then the function will fail with a `FailedProcessingLargePayloadException` exception.
-
- If the request handler method returns without error then each payload will be
- deleted from S3 using `deleteObject(bucket, key)`
-
-To disable deletion of payloads setting the following annotation parameter:
-
-=== "Disable payload deletion"
-
- ```java hl_lines="3"
- import software.amazon.lambda.powertools.sqs.SqsLargeMessage;
-
- @SqsLargeMessage(deletePayloads=false)
- public class SqsMessageHandler implements RequestHandler {
-
- }
- ```
-
-## Utility
-
-If you want to avoid using annotation and have control over error that can happen during payload enrichment use `SqsUtils.enrichedMessageFromS3()`.
-It provides you access with a list of `SQSMessage` object enriched from S3 payload.
-
-Original `SQSEvent` object is never mutated. You can also control if the S3 payload should be deleted after successful
-processing.
-
-=== "Functional API without annotation"
-
- ```java hl_lines="9 10 11 14 15 16 17 18 19 20 21 22 27 28 29"
- import software.amazon.lambda.powertools.sqs.SqsLargeMessage;
- import software.amazon.lambda.powertools.sqs.SqsUtils;
-
- public class SqsMessageHandler implements RequestHandler {
-
- @Override
- public String handleRequest(SQSEvent sqsEvent, Context context) {
-
- Map sqsMessage = SqsUtils.enrichedMessageFromS3(sqsEvent, sqsMessages -> {
- // Some business logic
- Map someBusinessLogic = new HashMap<>();
- someBusinessLogic.put("Message", sqsMessages.get(0).getBody());
- return someBusinessLogic;
- });
-
- // Do not delete payload after processing.
- Map sqsMessage = SqsUtils.enrichedMessageFromS3(sqsEvent, false, sqsMessages -> {
- // Some business logic
- Map someBusinessLogic = new HashMap<>();
- someBusinessLogic.put("Message", sqsMessages.get(0).getBody());
- return someBusinessLogic;
- });
-
- // Better control over exception during enrichment
- try {
- // Do not delete payload after processing.
- SqsUtils.enrichedMessageFromS3(sqsEvent, false, sqsMessages -> {
- // Some business logic
- });
- } catch (FailedProcessingLargePayloadException e) {
- // handle any exception.
- }
-
- return "ok";
- }
- }
- ```
-
-## Overriding the default S3Client
-
-If you require customisations to the default S3Client, you can create your own `S3Client` and pass it to be used by utility either for
-**[SqsLargeMessage annotation](#lambda-handler)**, or **[SqsUtils Utility API](#utility)**.
-
-=== "App.java"
-
- ```java hl_lines="4 5 11"
- import software.amazon.lambda.powertools.sqs.SqsLargeMessage;
-
- static {
- SqsUtils.overrideS3Client(S3Client.builder()
- .build());
- }
-
- public class SqsMessageHandler implements RequestHandler {
-
- @Override
- @SqsLargeMessage
- public String handleRequest(SQSEvent sqsEvent, Context context) {
- // process messages
-
- return "ok";
- }
- }
- ```
\ No newline at end of file
diff --git a/docs/utilities/validation.md b/docs/utilities/validation.md
index 8733c0e77..928ffb6c8 100644
--- a/docs/utilities/validation.md
+++ b/docs/utilities/validation.md
@@ -12,20 +12,20 @@ This utility provides JSON Schema validation for payloads held within events and
* JMESPath support validate only a sub part of the event
## Install
+Depending on your version of Java (either Java 1.8 or 11+), the configuration slightly changes.
-To install this utility, add the following dependency to your project.
-
-=== "Maven"
- ```xml hl_lines="3 4 5 6 7 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36"
+=== "Maven Java 11+"
+ ```xml hl_lines="3-7 16 18 24-27"
- ...
-
- software.amazon.lambda
- powertools-validation
- {{ powertools.version }}
-
- ...
+ ...
+
+ software.amazon.lambda
+ powertools-validation
+ {{ powertools.version }}
+
+ ...
+ ...
@@ -34,6 +34,51 @@ To install this utility, add the following dependency to your project.
dev.aspectjaspectj-maven-plugin1.13.1
+
+ 11
+ 11
+ 11
+
+
+ software.amazon.lambda
+ powertools-validation
+
+
+
+
+
+
+ compile
+
+
+
+
+ ...
+
+
+ ```
+
+=== "Maven Java 1.8"
+
+ ```xml hl_lines="3-7 16 18 24-27"
+
+ ...
+
+ software.amazon.lambda
+ powertools-validation
+ {{ powertools.version }}
+
+ ...
+
+ ...
+
+
+
+ ...
+
+ org.codehaus.mojo
+ aspectj-maven-plugin
+ 1.14.01.81.8
@@ -58,25 +103,47 @@ To install this utility, add the following dependency to your project.
```
-=== "Gradle"
+=== "Gradle Java 11+"
- ```groovy
- plugins{
- id 'java'
- id 'io.freefair.aspectj.post-compile-weaving' version '6.3.0'
- }
+ ```groovy hl_lines="3 11"
+ plugins {
+ id 'java'
+ id 'io.freefair.aspectj.post-compile-weaving' version '8.1.0'
+ }
+
+ repositories {
+ mavenCentral()
+ }
+
+ dependencies {
+ aspect 'software.amazon.lambda:powertools-validation:{{ powertools.version }}'
+ }
+
+ sourceCompatibility = 11 // or higher
+ targetCompatibility = 11 // or higher
+ ```
- repositories {
- mavenCentral()
- }
+=== "Gradle Java 1.8"
- dependencies {
- aspect 'software.amazon.lambda:powertools-validation:{{ powertools.version }}'
-// This dependency is needed for Java17+, please uncomment it if you are using Java17+
-// implementation 'org.aspectj:aspectjrt:1.9.19'
- }
+ ```groovy hl_lines="3 11"
+ plugins {
+ id 'java'
+ id 'io.freefair.aspectj.post-compile-weaving' version '6.6.3'
+ }
+
+ repositories {
+ mavenCentral()
+ }
+
+ dependencies {
+ aspect 'software.amazon.lambda:powertools-validation:{{ powertools.version }}'
+ }
+
+ sourceCompatibility = 1.8
+ targetCompatibility = 1.8
```
+
## Validating events
You can validate inbound and outbound events using `@Validation` annotation.
diff --git a/examples/README.md b/examples/README.md
index f7e6fc620..b44ff2433 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -1,4 +1,73 @@
-## aws-lambda-powertools-examples
+# Powertools for AWS Lambda (Java) Examples
This directory holds example projects demoing different components of the Powertools for AWS Lambda (Java).
-Each example can be copied from its subdirectory and used independently of the rest of this repository.
\ No newline at end of file
+Each example can be copied from its subdirectory and used independently of the rest of this repository.
+
+## Examples
+
+* [powertools-examples-core](powertools-examples-core) - Demonstrates the core logging, tracing, and metrics modules
+* [powertools-examples-idempotency](powertools-examples-idempotency) - An idempotent HTTP API
+* [powertools-examples-parameters](powertools-examples-parameters) - Uses the parameters module to provide runtime parameters to a function
+* [powertools-examples-serialization](powertools-examples-serialization) - Uses the serialization module to serialize and deserialize API Gateway & SQS payloads
+* [powertools-examples-sqs](powertools-examples-sqs) - Processes SQS batch requests
+* [powertools-examples-validation](powertools-examples-validation) - Uses the validation module to validate user requests received via API Gateway
+* [powertools-examples-cloudformation](powertools-examples-cloudformation) - Deploys a Cloudformation custom resource
+
+## Working with AWS Serverless Application Model (SAM) Examples
+Many of the examples use [AWS Serverless Application Model](https://aws.amazon.com/serverless/sam/) (SAM). To get started
+with them, you can use the SAM Command Line Interface (SAM CLI) to build it and deploy an example 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 learn more about SAM,
+[check out the developer guide](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/using-sam-cli.html).
+You can use the CLI to [test events locally](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/using-sam-cli-local-invoke.html),
+and [run the application locally](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/using-sam-cli-local-start-api.html),
+amongst other things.
+
+To build and deploy an example application for the first time, run the following in your shell:
+
+```bash
+# Switch to the directory containing an example for the powertools-core module
+$ cd powertools-examples-core
+
+# Build and deploy the example
+$ sam build
+$ sam deploy --guided
+```
+
+The first command will build the source of your application. The second command will package and deploy your application to AWS, with a series of prompts:
+
+* **Stack Name**: The name of the stack to deploy to CloudFormation. This should be unique to your account and region, and a good starting point would be something matching your project name.
+* **AWS Region**: The AWS region you want to deploy your app to.
+* **Confirm changes before deploy**: If set to yes, any change sets will be shown to you before execution for manual review. If set to no, the AWS SAM CLI will automatically deploy application changes.
+* **Allow SAM CLI IAM role creation**: Many AWS SAM templates, including this example, create AWS IAM roles required for the AWS Lambda function(s) included to access AWS services. By default, these are scoped down to minimum required permissions. To deploy an AWS CloudFormation stack which creates or modified IAM roles, the `CAPABILITY_IAM` value for `capabilities` must be provided. If permission isn't provided through this prompt, to deploy this example you must explicitly pass `--capabilities CAPABILITY_IAM` to the `sam deploy` command.
+* **Save arguments to samconfig.toml**: If set to yes, your choices will be saved to a configuration file inside the project, so that in the future you can just re-run `sam deploy` without parameters to deploy changes to your application.
+
+You can find your API Gateway Endpoint URL in the output values displayed after deployment.
+
+### External examples
+
+You can find more examples in the https://github.com/aws/aws-sam-cli-app-templates project:
+
+* [Java 8 + Maven](https://github.com/aws/aws-sam-cli-app-templates/tree/master/java8/hello-pt-maven)
+* [Java 8 on Amazon Linux 2 + Maven](https://github.com/aws/aws-sam-cli-app-templates/tree/master/java8.al2/hello-pt-maven)
+* [Java 11 + Maven](https://github.com/aws/aws-sam-cli-app-templates/tree/master/java11/hello-pt-maven)
+* [Java 17 + Maven](https://github.com/aws/aws-sam-cli-app-templates/tree/master/java17/hello-pt-maven)
+* [Java 17 + Gradle](https://github.com/aws/aws-sam-cli-app-templates/tree/master/java17/hello-pt-gradle)
+
+
+### SAM - Other Tools
+
+If you prefer to use an integrated development environment (IDE) to build and test your application, you can use the AWS Toolkit.
+The AWS Toolkit is an open source plug-in for popular IDEs that uses the SAM CLI to build and deploy serverless applications on AWS. The AWS Toolkit also adds a simplified step-through debugging experience for Lambda function code. See the following links to get started.
+
+* [PyCharm](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html)
+* [IntelliJ](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html)
+* [VS Code](https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/welcome.html)
+* [Visual Studio](https://docs.aws.amazon.com/toolkit-for-visual-studio/latest/user-guide/welcome.html)
\ No newline at end of file
diff --git a/examples/pom.xml b/examples/pom.xml
index 03ef9f7bb..1d80f198b 100644
--- a/examples/pom.xml
+++ b/examples/pom.xml
@@ -1,4 +1,18 @@
+
+
@@ -19,8 +33,8 @@
powertools-examples-idempotencypowertools-examples-parameterspowertools-examples-serialization
- powertools-examples-sqspowertools-examples-validation
+ powertools-examples-cloudformation
diff --git a/examples/powertools-examples-cloudformation/README.md b/examples/powertools-examples-cloudformation/README.md
new file mode 100644
index 000000000..4b53ff0aa
--- /dev/null
+++ b/examples/powertools-examples-cloudformation/README.md
@@ -0,0 +1,27 @@
+# Powertools for AWS Lambda (Java) - Cloudformation Custom Resource Example
+
+This project contains an example of Lambda function using the CloudFormation module of Powertools for AWS Lambda in Java. For more information on this module, please refer to the [documentation](https://awslabs.github.io/aws-lambda-powertools-java/utilities/custom_resources/).
+
+## Deploy the sample application
+
+This sample can be used either with the Serverless Application Model (SAM) or with CDK.
+
+### Deploy with SAM CLI
+To deploy it using the SAM CLI, check out the instructions for getting started in [the examples directory](../README.md)
+
+### Deploy with CDK
+To use CDK you need the following tools.
+
+* CDK - [Install CDK](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html)
+* Java 8 - [Install Java 8](https://docs.aws.amazon.com/corretto/latest/corretto-8-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 this application for the first time, run the following in your shell:
+
+```bash
+cd infra/cdk
+mvn package
+cdk synth
+cdk deploy -c BucketNameParam=my-unique-bucket-20230718
+```
\ No newline at end of file
diff --git a/examples/powertools-examples-cloudformation/infra/cdk/.gitignore b/examples/powertools-examples-cloudformation/infra/cdk/.gitignore
new file mode 100644
index 000000000..1db21f162
--- /dev/null
+++ b/examples/powertools-examples-cloudformation/infra/cdk/.gitignore
@@ -0,0 +1,13 @@
+.classpath.txt
+target
+.classpath
+.project
+.idea
+.settings
+.vscode
+*.iml
+
+# CDK asset staging directory
+.cdk.staging
+cdk.out
+
diff --git a/examples/powertools-examples-cloudformation/infra/cdk/cdk.json b/examples/powertools-examples-cloudformation/infra/cdk/cdk.json
new file mode 100644
index 000000000..fe011b328
--- /dev/null
+++ b/examples/powertools-examples-cloudformation/infra/cdk/cdk.json
@@ -0,0 +1,37 @@
+{
+ "app": "mvn -e -q compile exec:java",
+ "watch": {
+ "include": [
+ "**"
+ ],
+ "exclude": [
+ "README.md",
+ "cdk*.json",
+ "target",
+ "pom.xml",
+ "src/test"
+ ]
+ },
+ "context": {
+ "@aws-cdk/aws-lambda:recognizeLayerVersion": true,
+ "@aws-cdk/core:checkSecretUsage": true,
+ "@aws-cdk/core:target-partitions": [
+ "aws",
+ "aws-cn"
+ ],
+ "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
+ "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
+ "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
+ "@aws-cdk/aws-iam:minimizePolicies": true,
+ "@aws-cdk/core:validateSnapshotRemovalPolicy": true,
+ "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
+ "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
+ "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
+ "@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
+ "@aws-cdk/core:enablePartitionLiterals": true,
+ "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
+ "@aws-cdk/aws-iam:standardizedServicePrincipals": true,
+ "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
+ "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true
+ }
+}
diff --git a/examples/powertools-examples-cloudformation/infra/cdk/pom.xml b/examples/powertools-examples-cloudformation/infra/cdk/pom.xml
new file mode 100644
index 000000000..30172fa3f
--- /dev/null
+++ b/examples/powertools-examples-cloudformation/infra/cdk/pom.xml
@@ -0,0 +1,53 @@
+
+
+ 4.0.0
+
+ com.myorg
+ powertools-examples-cloudformation-cdk
+ 0.1
+
+
+ UTF-8
+ 2.59.0
+ [10.0.0,11.0.0)
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.8.1
+
+ 8
+ 8
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 3.0.0
+
+ com.myorg.PowertoolsExamplesCloudformationCdkApp
+
+
+
+
+
+
+
+
+ software.amazon.awscdk
+ aws-cdk-lib
+ ${cdk.version}
+
+
+
+ software.constructs
+ constructs
+ ${constructs.version}
+
+
+
diff --git a/examples/powertools-examples-cloudformation/infra/cdk/src/main/java/com/myorg/PowertoolsExamplesCloudformationCdkApp.java b/examples/powertools-examples-cloudformation/infra/cdk/src/main/java/com/myorg/PowertoolsExamplesCloudformationCdkApp.java
new file mode 100644
index 000000000..f4a4d06d7
--- /dev/null
+++ b/examples/powertools-examples-cloudformation/infra/cdk/src/main/java/com/myorg/PowertoolsExamplesCloudformationCdkApp.java
@@ -0,0 +1,17 @@
+package com.myorg;
+
+import software.amazon.awscdk.App;
+import software.amazon.awscdk.StackProps;
+
+public class PowertoolsExamplesCloudformationCdkApp {
+ public static void main(final String[] args) {
+ App app = new App();
+
+ new PowertoolsExamplesCloudformationCdkStack(app, "PowertoolsExamplesCloudformationCdkStack",
+ StackProps.builder()
+ .build());
+
+ app.synth();
+ }
+}
+
diff --git a/examples/powertools-examples-cloudformation/infra/cdk/src/main/java/com/myorg/PowertoolsExamplesCloudformationCdkStack.java b/examples/powertools-examples-cloudformation/infra/cdk/src/main/java/com/myorg/PowertoolsExamplesCloudformationCdkStack.java
new file mode 100644
index 000000000..e880a3534
--- /dev/null
+++ b/examples/powertools-examples-cloudformation/infra/cdk/src/main/java/com/myorg/PowertoolsExamplesCloudformationCdkStack.java
@@ -0,0 +1,89 @@
+package com.myorg;
+
+import software.amazon.awscdk.Stack;
+import software.amazon.awscdk.*;
+import software.amazon.awscdk.services.iam.Effect;
+import software.amazon.awscdk.services.iam.PolicyStatement;
+import software.amazon.awscdk.services.iam.PolicyStatementProps;
+import software.amazon.awscdk.services.lambda.Code;
+import software.amazon.awscdk.services.lambda.Function;
+import software.amazon.awscdk.services.lambda.FunctionProps;
+import software.amazon.awscdk.services.lambda.Runtime;
+import software.amazon.awscdk.services.s3.assets.AssetOptions;
+import software.constructs.Construct;
+
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+
+import static java.util.Collections.singletonList;
+import static software.amazon.awscdk.BundlingOutput.NOT_ARCHIVED;
+
+public class PowertoolsExamplesCloudformationCdkStack extends Stack {
+
+ public static final String SAMPLE_BUCKET_NAME = "sample-bucket-name-20230315-abc123";
+
+ public PowertoolsExamplesCloudformationCdkStack(final Construct scope, final String id) {
+ this(scope, id, null);
+ }
+
+ public PowertoolsExamplesCloudformationCdkStack(final Construct scope, final String id, final StackProps props) {
+ super(scope, id, props);
+
+
+ List functionPackagingInstructions = Arrays.asList(
+ "/bin/sh",
+ "-c",
+ "mvn clean install" +
+ "&& mkdir /asset-output/lib" +
+ "&& cp target/powertools-examples-cloudformation-*.jar /asset-output/lib"
+ );
+ BundlingOptions bundlingOptions = BundlingOptions.builder()
+ .command(functionPackagingInstructions)
+ .image(Runtime.JAVA_11.getBundlingImage())
+ .volumes(singletonList(
+ // Mount local .m2 repo to avoid download all the dependencies again inside the container
+ DockerVolume.builder()
+ .hostPath(System.getProperty("user.home") + "/.m2/")
+ .containerPath("/root/.m2/")
+ .build()
+ ))
+ .user("root")
+ .outputType(NOT_ARCHIVED)
+ .build();
+
+ Function helloWorldFunction = new Function(this, "HelloWorldFunction", FunctionProps.builder()
+ .runtime(Runtime.JAVA_11)
+ .code(Code.fromAsset("../../", AssetOptions.builder().bundling(bundlingOptions)
+ .build()))
+ .handler("helloworld.App::handleRequest")
+ .memorySize(512)
+ .timeout(Duration.seconds(20))
+ .environment(Collections
+ .singletonMap("JAVA_TOOL_OPTIONS", "-XX:+TieredCompilation -XX:TieredStopAtLevel=1"))
+ .build());
+ helloWorldFunction.addToRolePolicy(new PolicyStatement(PolicyStatementProps.builder()
+ .effect(Effect.ALLOW)
+ .actions(Arrays.asList("s3:GetLifecycleConfiguration",
+ "s3:PutLifecycleConfiguration",
+ "s3:CreateBucket",
+ "s3:ListBucket",
+ "s3:DeleteBucket"))
+ .resources(singletonList("*")).build()));
+
+ String bucketName = (String) this.getNode().tryGetContext("BucketNameParam");
+
+ Map crProperties = new HashMap<>();
+ crProperties.put("BucketName", bucketName);
+ CustomResource.Builder
+ .create(this, "HelloWorldCustomResource")
+ .serviceToken(helloWorldFunction.getFunctionArn())
+ .properties(crProperties)
+ .build();
+
+ }
+}
diff --git a/examples/powertools-examples-cloudformation/infra/sam/events/create_event.json b/examples/powertools-examples-cloudformation/infra/sam/events/create_event.json
new file mode 100644
index 000000000..26ef0a03b
--- /dev/null
+++ b/examples/powertools-examples-cloudformation/infra/sam/events/create_event.json
@@ -0,0 +1,12 @@
+{
+ "RequestType": "Create",
+ "ResponseURL": "http://pre-signed-S3-url-for-response",
+ "StackId": "arn:aws:cloudformation:us-east-1:123456789012:stack/MyStack/guid",
+ "RequestId": "unique id for this create request",
+ "ResourceType": "Custom::TestResource",
+ "ResourceProperties": {
+ "BucketName": "test-bucket-20230307-1",
+ "RetentionDays" : 10,
+ "StackName": "MyStack"
+ }
+}
diff --git a/examples/powertools-examples-cloudformation/infra/sam/events/delete_event.json b/examples/powertools-examples-cloudformation/infra/sam/events/delete_event.json
new file mode 100644
index 000000000..d18fdd3e4
--- /dev/null
+++ b/examples/powertools-examples-cloudformation/infra/sam/events/delete_event.json
@@ -0,0 +1,14 @@
+{
+ "RequestType": "Delete",
+ "ResponseURL": "http://pre-signed-S3-url-for-response",
+ "StackId": "arn:aws:cloudformation:us-east-1:123456789012:stack/MyStack/guid",
+ "RequestId": "unique id for this create request",
+ "ResourceType": "Custom::TestResource",
+ "LogicalResourceId": "MyTestResource",
+ "PhysicalResourceId": "test-bucket-20230307-1",
+ "ResourceProperties": {
+ "BucketName": "test-bucket-20230307-1",
+ "RetentionDays" : 10,
+ "StackName": "MyStack"
+ }
+}
diff --git a/examples/powertools-examples-cloudformation/infra/sam/events/update_event.json b/examples/powertools-examples-cloudformation/infra/sam/events/update_event.json
new file mode 100644
index 000000000..5a5ae2e3f
--- /dev/null
+++ b/examples/powertools-examples-cloudformation/infra/sam/events/update_event.json
@@ -0,0 +1,14 @@
+{
+ "RequestType": "Update",
+ "ResponseURL": "http://pre-signed-S3-url-for-response",
+ "StackId": "arn:aws:cloudformation:us-east-1:123456789012:stack/MyStack/guid",
+ "RequestId": "unique id for this create request",
+ "ResourceType": "Custom::TestResource",
+ "LogicalResourceId": "MyTestResource",
+ "PhysicalResourceId": "test-bucket-20230307-1",
+ "ResourceProperties": {
+ "BucketName": "test-bucket-20230307-1",
+ "RetentionDays" : 100,
+ "StackName": "MyStack"
+ }
+}
diff --git a/examples/powertools-examples-cloudformation/infra/sam/template.yaml b/examples/powertools-examples-cloudformation/infra/sam/template.yaml
new file mode 100644
index 000000000..a7ce4adf1
--- /dev/null
+++ b/examples/powertools-examples-cloudformation/infra/sam/template.yaml
@@ -0,0 +1,50 @@
+AWSTemplateFormatVersion: '2010-09-09'
+Transform: AWS::Serverless-2016-10-31
+Description: >
+ powertools-examples-cloudformation
+
+ Sample SAM Template for powertools-examples-cloudformation
+
+Globals:
+ Function:
+ Timeout: 20
+
+Parameters:
+ BucketNameParam:
+ Type: String
+
+Resources:
+ HelloWorldCustomResource:
+ Type: AWS::CloudFormation::CustomResource
+ Properties:
+ ServiceToken: !GetAtt HelloWorldFunction.Arn
+ BucketName: !Ref BucketNameParam
+
+ HelloWorldFunction:
+ Type: AWS::Serverless::Function
+ Properties:
+ CodeUri: ../../
+ Handler: helloworld.App::handleRequest
+ Runtime: java11
+ Architectures:
+ - x86_64
+ MemorySize: 512
+ Policies:
+ - Statement:
+ - Sid: bucketaccess1
+ Effect: Allow
+ Action:
+ - s3:GetLifecycleConfiguration
+ - s3:PutLifecycleConfiguration
+ - s3:CreateBucket
+ - s3:ListBucket
+ - s3:DeleteBucket
+ Resource: '*'
+ Environment:
+ Variables:
+ JAVA_TOOL_OPTIONS: -XX:+TieredCompilation -XX:TieredStopAtLevel=1
+
+Outputs:
+ HelloWorldFunction:
+ Description: "Hello World Lambda Function ARN"
+ Value: !GetAtt HelloWorldFunction.Arn
diff --git a/examples/powertools-examples-sqs/pom.xml b/examples/powertools-examples-cloudformation/pom.xml
similarity index 79%
rename from examples/powertools-examples-sqs/pom.xml
rename to examples/powertools-examples-cloudformation/pom.xml
index 8e4bcae7b..7b3b79a4a 100644
--- a/examples/powertools-examples-sqs/pom.xml
+++ b/examples/powertools-examples-cloudformation/pom.xml
@@ -1,44 +1,55 @@
4.0.0
+
software.amazon.lambda.examples
- 2.0.0-SNAPSHOT
- powertools-examples-sqs
+ 1.17.0-SNAPSHOT
+ powertools-examples-cloudformationjar
- Powertools for AWS Lambda (Java) library Examples - SQS
+
+ AWS Lambda Powertools for Java library Examples - CloudFormation2.20.01.81.8true
+ 1.2.2
+ 3.11.2
+ 2.20.118
+
+
+
+ software.amazon.awssdk
+ bom
+ ${aws.sdk.version}
+ pom
+ import
+
+
+
-
- software.amazon.lambda
- powertools-logging
- ${project.version}
-
-
- software.amazon.lambda
- powertools-sqs
- ${project.version}
-
-
- software.amazon.awssdk
- url-connection-client
- 2.20.96
- com.amazonawsaws-lambda-java-core
- 1.2.2
+ ${lambda.core.version}com.amazonawsaws-lambda-java-events
- 3.11.2
+ ${lambda.events.version}
+
+
+ software.amazon.lambda
+ powertools-cloudformation
+ ${project.version}
+
+
+ software.amazon.lambda
+ powertools-logging
+ ${project.version}org.apache.logging.log4j
@@ -51,17 +62,37 @@
${log4j.version}
- com.fasterxml.jackson.datatype
- jackson-datatype-joda
- 2.15.2
+ software.amazon.awssdk
+ s3
+
+
+ software.amazon.awssdk
+ netty-nio-client
+
+
+ software.amazon.awssdk
+ apache-client
+
+
-
- junit
- junit
- 4.13.2
- test
+ software.amazon.awssdk
+ apache-client
+
+
+ commons-logging
+ commons-logging
+
+
+
+
+ org.apache.logging.log4j
+ log4j-jcl
+ ${log4j.version}
+
+
+
@@ -79,10 +110,6 @@
software.amazon.lambdapowertools-logging
-
- software.amazon.lambda
- powertools-sqs
-
@@ -165,10 +192,6 @@
software.amazon.lambdapowertools-logging
-
- software.amazon.lambda
- powertools-sqs
-
diff --git a/examples/powertools-examples-cloudformation/src/main/java/helloworld/App.java b/examples/powertools-examples-cloudformation/src/main/java/helloworld/App.java
new file mode 100644
index 000000000..ca3cb0ab7
--- /dev/null
+++ b/examples/powertools-examples-cloudformation/src/main/java/helloworld/App.java
@@ -0,0 +1,169 @@
+package helloworld;
+
+import com.amazonaws.services.lambda.runtime.Context;
+import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import software.amazon.awssdk.awscore.exception.AwsServiceException;
+import software.amazon.awssdk.core.exception.SdkClientException;
+import software.amazon.awssdk.core.waiters.WaiterResponse;
+import software.amazon.awssdk.http.apache.ApacheHttpClient;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.model.*;
+import software.amazon.awssdk.services.s3.waiters.S3Waiter;
+import software.amazon.lambda.powertools.cloudformation.AbstractCustomResourceHandler;
+import software.amazon.lambda.powertools.cloudformation.Response;
+
+import java.util.Objects;
+
+/**
+ * Handler for requests to Lambda function.
+ */
+
+public class App extends AbstractCustomResourceHandler {
+ private final static Logger log = LogManager.getLogger(App.class);
+ private final S3Client s3Client;
+
+ public App() {
+ super();
+ s3Client = S3Client.builder().httpClientBuilder(ApacheHttpClient.builder()).build();
+ }
+
+ /**
+ * This method is invoked when CloudFormation Creates the Custom Resource.
+ * In this example, the method creates an Amazon S3 Bucket with the provided `BucketName`
+ *
+ * @param cloudFormationCustomResourceEvent Create Event from CloudFormation
+ * @param context Lambda Context
+ * @return Response to send to CloudFormation
+ */
+ @Override
+ protected Response create(CloudFormationCustomResourceEvent cloudFormationCustomResourceEvent, Context context) {
+ // Validate the CloudFormation Custom Resource event
+ Objects.requireNonNull(cloudFormationCustomResourceEvent, "cloudFormationCustomResourceEvent cannot be null.");
+ Objects.requireNonNull(cloudFormationCustomResourceEvent.getResourceProperties().get("BucketName"),
+ "BucketName cannot be null.");
+
+ log.info(cloudFormationCustomResourceEvent);
+ String bucketName = (String) cloudFormationCustomResourceEvent.getResourceProperties().get("BucketName");
+ log.info("Bucket Name {}", bucketName);
+ try {
+ // Create the S3 bucket with the given bucketName
+ createBucket(bucketName);
+ // Return a successful response with the bucketName as the physicalResourceId
+ return Response.success(bucketName);
+ } catch (AwsServiceException | SdkClientException e) {
+ // In case of error, return a failed response, with the bucketName as the physicalResourceId
+ log.error(e);
+ return Response.failed(bucketName);
+ }
+ }
+
+ /**
+ * This method is invoked when CloudFormation Updates the Custom Resource.
+ * In this example, the method creates an Amazon S3 Bucket with the provided `BucketName`, if the `BucketName` differs from the previous `BucketName` (for initial creation)
+ *
+ * @param cloudFormationCustomResourceEvent Update Event from CloudFormation
+ * @param context Lambda Context
+ * @return Response to send to CloudFormation
+ */
+ @Override
+ protected Response update(CloudFormationCustomResourceEvent cloudFormationCustomResourceEvent, Context context) {
+ // Validate the CloudFormation Custom Resource event
+ Objects.requireNonNull(cloudFormationCustomResourceEvent, "cloudFormationCustomResourceEvent cannot be null.");
+ Objects.requireNonNull(cloudFormationCustomResourceEvent.getResourceProperties().get("BucketName"),
+ "BucketName cannot be null.");
+
+ log.info(cloudFormationCustomResourceEvent);
+ // Get the physicalResourceId. physicalResourceId is the value returned to CloudFormation in the Create request, and passed in on subsequent requests (e.g. UPDATE or DELETE)
+ String physicalResourceId = cloudFormationCustomResourceEvent.getPhysicalResourceId();
+ log.info("Physical Resource ID {}", physicalResourceId);
+
+ // Get the BucketName from the CloudFormation Event
+ String newBucketName = (String) cloudFormationCustomResourceEvent.getResourceProperties().get("BucketName");
+
+ // Check if the physicalResourceId equals the new BucketName
+ if (!physicalResourceId.equals(newBucketName)) {
+ // The bucket name has changed - create a new bucket
+ try {
+ // Create a new bucket with the newBucketName
+ createBucket(newBucketName);
+ // Return a successful response with the newBucketName
+ return Response.success(newBucketName);
+ } catch (AwsServiceException | SdkClientException e) {
+ log.error(e);
+ return Response.failed(newBucketName);
+ }
+ } else {
+ // Bucket name has not changed, and no changes are needed.
+ // Return a successful response with the previous physicalResourceId
+ return Response.success(physicalResourceId);
+ }
+ }
+
+ /**
+ * This method is invoked when CloudFormation Deletes the Custom Resource.
+ * NOTE: CloudFormation will DELETE a resource, if during the UPDATE a new physicalResourceId is returned.
+ * Refer to the Powertools Java Documentation for more details.
+ *
+ * @param cloudFormationCustomResourceEvent Delete Event from CloudFormation
+ * @param context Lambda Context
+ * @return Response to send to CloudFormation
+ */
+ @Override
+ protected Response delete(CloudFormationCustomResourceEvent cloudFormationCustomResourceEvent, Context context) {
+ // Validate the CloudFormation Custom Resource event
+ Objects.requireNonNull(cloudFormationCustomResourceEvent, "cloudFormationCustomResourceEvent cannot be null.");
+ Objects.requireNonNull(cloudFormationCustomResourceEvent.getPhysicalResourceId(),
+ "PhysicalResourceId cannot be null.");
+
+ log.info(cloudFormationCustomResourceEvent);
+ // Get the physicalResourceId. physicalResourceId is the value provided to CloudFormation in the Create request.
+ String bucketName = cloudFormationCustomResourceEvent.getPhysicalResourceId();
+ log.info("Bucket Name {}", bucketName);
+
+ // Check if a bucket with bucketName exists
+ if (bucketExists(bucketName)) {
+ try {
+ // If it exists, delete the bucket
+ s3Client.deleteBucket(DeleteBucketRequest.builder().bucket(bucketName).build());
+ log.info("Bucket Deleted {}", bucketName);
+ // Return a successful response with bucketName as the physicalResourceId
+ return Response.success(bucketName);
+ } catch (AwsServiceException | SdkClientException e) {
+ // Return a failed response in case of errors during the bucket deletion
+ log.error(e);
+ return Response.failed(bucketName);
+ }
+ } else {
+ // If the bucket does not exist, return a successful response with the bucketName as the physicalResourceId
+ log.info("Bucket already deleted - no action");
+ return Response.success(bucketName);
+ }
+
+ }
+
+ private boolean bucketExists(String bucketName) {
+ try {
+ HeadBucketResponse headBucketResponse =
+ s3Client.headBucket(HeadBucketRequest.builder().bucket(bucketName).build());
+ if (headBucketResponse.sdkHttpResponse().isSuccessful()) {
+ return true;
+ }
+ } catch (NoSuchBucketException e) {
+ log.info("Bucket does not exist");
+ return false;
+ }
+ return false;
+ }
+
+ private void createBucket(String bucketName) {
+ S3Waiter waiter = s3Client.waiter();
+ CreateBucketRequest createBucketRequest = CreateBucketRequest.builder().bucket(bucketName).build();
+ s3Client.createBucket(createBucketRequest);
+ WaiterResponse waiterResponse =
+ waiter.waitUntilBucketExists(HeadBucketRequest.builder().bucket(bucketName).build());
+ waiterResponse.matched().response().ifPresent(log::info);
+ log.info("Bucket Created {}", bucketName);
+ }
+}
\ No newline at end of file
diff --git a/examples/powertools-examples-cloudformation/src/main/resources/log4j2.xml b/examples/powertools-examples-cloudformation/src/main/resources/log4j2.xml
new file mode 100644
index 000000000..8e8162128
--- /dev/null
+++ b/examples/powertools-examples-cloudformation/src/main/resources/log4j2.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+ %d{dd MMM yyyy HH:mm:ss,SSS} [%p] <%X{AWSRequestId}> (%t) %c:%L: %m%n
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/powertools-examples-core/README.md b/examples/powertools-examples-core/README.md
index ba8859027..a47d0d26c 100644
--- a/examples/powertools-examples-core/README.md
+++ b/examples/powertools-examples-core/README.md
@@ -1,138 +1,47 @@
-# CoreUtilities
+# Powertools for AWS Lambda (Java) - Core Utilities Example
-This project contains source code and supporting files for a serverless application that you can deploy with the SAM CLI. It includes [Powertools for AWS Lambda (Java) for operational best practices](https://github.com/aws-powertools/powertools-lambda-java), and the following files and folders.
+This project demonstrates the Lambda for Powertools Java module - including
+[logging](https://docs.powertools.aws.dev/lambda/java/core/logging/),
+[tracing](https://docs.powertools.aws.dev/lambda/java/core/tracing/), and
+[metrics](https://docs.powertools.aws.dev/lambda/java/core/metrics/).
-- HelloWorldFunction/src/main - Code for the application's Lambda function.
-- events - Invocation events that you can use to invoke the function.
-- HelloWorldFunction/src/test - Unit tests for the application code.
-- template.yaml - A template that defines the application's AWS resources.
+It is made up of the following:
-The application uses several AWS resources, including Lambda functions and an API Gateway API. These resources are defined in the `template.yaml` file in this project. You can update the template to add AWS resources through the same deployment process that updates your application code.
-
-If you prefer to use an integrated development environment (IDE) to build and test your application, you can use the AWS Toolkit.
-The AWS Toolkit is an open source plug-in for popular IDEs that uses the SAM CLI to build and deploy serverless applications on AWS. The AWS Toolkit also adds a simplified step-through debugging experience for Lambda function code. See the following links to get started.
-
-* [PyCharm](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html)
-* [IntelliJ](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html)
-* [VS Code](https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/welcome.html)
-* [Visual Studio](https://docs.aws.amazon.com/toolkit-for-visual-studio/latest/user-guide/welcome.html)
+- [App.java](src/main/java/helloworld/App.java) - Code for the application's Lambda function.
+- [events](events) - Invocation events that you can use to invoke the function.
+- [AppTests.java](src/test/java/helloworld/AppTest.java) - Unit tests for the application code.
+- [template.yaml](template.yaml) - A template that defines the application's AWS resources.
## Deploy the sample application
-The Serverless Application Model Command Line Interface (SAM CLI) is an extension of the AWS CLI that adds functionality for building and testing Lambda applications. It uses Docker to run your functions in an Amazon Linux environment that matches Lambda. It can also emulate your application's build environment and API.
-
-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
-```
-
-The first command will build the source of your application. The second command will package and deploy your application to AWS, with a series of prompts:
-
-* **Stack Name**: The name of the stack to deploy to CloudFormation. This should be unique to your account and region, and a good starting point would be something matching your project name.
-* **AWS Region**: The AWS region you want to deploy your app to.
-* **Confirm changes before deploy**: If set to yes, any change sets will be shown to you before execution for manual review. If set to no, the AWS SAM CLI will automatically deploy application changes.
-* **Allow SAM CLI IAM role creation**: Many AWS SAM templates, including this example, create AWS IAM roles required for the AWS Lambda function(s) included to access AWS services. By default, these are scoped down to minimum required permissions. To deploy an AWS CloudFormation stack which creates or modified IAM roles, the `CAPABILITY_IAM` value for `capabilities` must be provided. If permission isn't provided through this prompt, to deploy this example you must explicitly pass `--capabilities CAPABILITY_IAM` to the `sam deploy` command.
-* **Save arguments to samconfig.toml**: If set to yes, your choices will be saved to a configuration file inside the project, so that in the future you can just re-run `sam deploy` without parameters to deploy changes to your application.
-
-You can find your API Gateway Endpoint URL in the output values displayed after deployment.
-
-## Use the SAM CLI to build and test locally
-
-Build your application with the `sam build` command.
-
-```bash
-Coreutilities$ sam build
-```
-
-The SAM CLI installs dependencies defined in `HelloWorldFunction/pom.xml`, creates a deployment package, and saves it in the `.aws-sam/build` folder.
-
-Test a single function by invoking it directly with a test event. An event is a JSON document that represents the input that the function receives from the event source. Test events are included in the `events` folder in this project.
-
-Run functions locally and invoke them with the `sam local invoke` command.
-
-```bash
-Coreutilities$ sam local invoke HelloWorldFunction --event events/event.json
-```
-
-The SAM CLI can also emulate your application's API. Use the `sam local start-api` to run the API locally on port 3000.
-
-```bash
-Coreutilities$ sam local start-api
-Coreutilities$ curl http://localhost:3000/
-```
-
-The SAM CLI reads the application template to determine the API's routes and the functions that they invoke. The `Events` property on each function's definition includes the route and method for each path.
-
-```yaml
- Events:
- HelloWorld:
- Type: Api
- Properties:
- Path: /hello
- Method: get
-```
-
-## Add a resource to your application
-The application template uses AWS Serverless Application Model (AWS SAM) to define application resources. AWS SAM is an extension of AWS CloudFormation with a simpler syntax for configuring common serverless application resources such as functions, triggers, and APIs. For resources not included in [the SAM specification](https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md), you can use standard [AWS CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html) resource types.
+This sample is based on Serverless Application Model (SAM). To deploy it, check out the instructions for getting
+started with SAM in [the examples directory](../README.md)
-## Fetch, tail, and filter Lambda function logs
+## Test the application
-To simplify troubleshooting, SAM CLI has a command called `sam logs`. `sam logs` lets you fetch logs generated by your deployed Lambda function from the command line. In addition to printing the logs on the terminal, this command has several nifty features to help you quickly find the bug.
-
-`NOTE`: This command works for all AWS Lambda functions; not just the ones you deploy using SAM.
+Once the app is deployed, you can invoke the endpoint like this:
```bash
-Coreutilities$ sam logs -n HelloWorldFunction --stack-name --tail
+ curl https://[REST-API-ID].execute-api.[REGION].amazonaws.com/Prod/hello/
```
-You can find more information and examples about filtering Lambda function logs in the [SAM CLI Documentation](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-logging.html).
-
-## Unit tests
-
-Tests are defined in the `HelloWorldFunction/src/test` folder in this project.
-
-```bash
-Coreutilities$ cd HelloWorldFunction
-HelloWorldFunction$ mvn test
-```
+The response itself isn't particularly interesting - you will get back some information about your IP address. If
+you go to the Lambda Console and locate the lambda you have deployed, then click the "Monitoring" tab you will
+be able to find:
-## Cleanup
+* **View X-Ray traces** - Display the traces captured by the traces module. These include subsegments for the
+different function calls within the example
+* **View Cloudwatch logs** - Display the structured logging output of the example
-To delete the sample application that you created, use the AWS CLI. Assuming you used your project name for the stack name, you can run the following:
+Likewise, from the CloudWatch dashboard, under **Metrics**, **all metrics**, you will find the namespaces `Another`
+and `ServerlessAirline`. The values in each of these are published by the code in
+[App.java](src/main/java/helloworld/App.java).
+You can also watch the trace information or log information using the SAM CLI:
```bash
-aws cloudformation delete-stack --stack-name
-```
-
-# Appendix
-
-## Powertools
-
-**Tracing**
-
-[Tracing utility](https://docs.powertools.aws.dev/lambda-java/core/tracing/) provides functionality to reduce the overhead of performing common tracing tasks. It traces the execution of this sample code including the response and exceptions as tracing metadata - You can visualize them in AWS X-Ray.
-
-**Logger**
-
-[Logging utility](https://docs.powertools.aws.dev/lambda-java/core/logging/) creates an opinionated application Logger with structured logging as the output, dynamically samples a percentage (samplingRate) of your logs in DEBUG mode for concurrent invocations, log incoming events as your function is invoked, and injects key information from Lambda context object into your Logger - You can visualize them in Amazon CloudWatch Logs.
-
-**Metrics**
-
-[Metrics utility](https://docs.powertools.aws.dev/lambda-java/core/metrics/) captures cold start metric of your Lambda invocation, and could add additional metrics to help you understand your application KPIs - You can visualize them in Amazon CloudWatch.
-
-## Resources
-
-See the [AWS SAM developer guide](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) for an introduction to SAM specification, the SAM CLI, and serverless application concepts.
-
-Check the [Powertools for AWS Lambda (Java)](https://docs.powertools.aws.dev/lambda-java/) for more information on how to use and configure such tools
+# Tail the logs
+sam logs --tail $MY_STACK
-Next, you can use AWS Serverless Application Repository to deploy ready to use Apps that go beyond hello world samples and learn how authors developed their applications: [AWS Serverless Application Repository main page](https://aws.amazon.com/serverless/serverlessrepo/)
+# Tail the traces
+sam traces --tail
+```
\ No newline at end of file
diff --git a/examples/powertools-examples-core/src/main/java/helloworld/App.java b/examples/powertools-examples-core/src/main/java/helloworld/App.java
index b45440114..94360cf59 100644
--- a/examples/powertools-examples-core/src/main/java/helloworld/App.java
+++ b/examples/powertools-examples-core/src/main/java/helloworld/App.java
@@ -1,12 +1,23 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
package helloworld;
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.net.URL;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.stream.Collectors;
+import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger;
+import static software.amazon.lambda.powertools.metrics.MetricsUtils.withSingleMetric;
+import static software.amazon.lambda.powertools.tracing.TracingUtils.putMetadata;
+import static software.amazon.lambda.powertools.tracing.TracingUtils.withEntitySubsegment;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
@@ -14,21 +25,23 @@
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
import com.amazonaws.xray.AWSXRay;
import com.amazonaws.xray.entities.Entity;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import software.amazon.cloudwatchlogs.emf.model.DimensionSet;
import software.amazon.cloudwatchlogs.emf.model.Unit;
-import software.amazon.lambda.powertools.logging.LoggingUtils;
import software.amazon.lambda.powertools.logging.Logging;
+import software.amazon.lambda.powertools.logging.LoggingUtils;
import software.amazon.lambda.powertools.metrics.Metrics;
import software.amazon.lambda.powertools.tracing.CaptureMode;
-import software.amazon.lambda.powertools.tracing.TracingUtils;
import software.amazon.lambda.powertools.tracing.Tracing;
-
-import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger;
-import static software.amazon.lambda.powertools.metrics.MetricsUtils.withSingleMetric;
-import static software.amazon.lambda.powertools.tracing.TracingUtils.putMetadata;
-import static software.amazon.lambda.powertools.tracing.TracingUtils.withEntitySubsegment;
+import software.amazon.lambda.powertools.tracing.TracingUtils;
/**
* Handler for requests to Lambda function.
@@ -47,10 +60,11 @@ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEv
metricsLogger().putMetric("CustomMetric1", 1, Unit.COUNT);
- withSingleMetric("CustomMetrics2", 1, Unit.COUNT, "Another", (metric) -> {
- metric.setDimensions(DimensionSet.of("AnotherService", "CustomService"));
- metric.setDimensions(DimensionSet.of("AnotherService1", "CustomService1"));
- });
+ withSingleMetric("CustomMetrics2", 1, Unit.COUNT, "Another", (metric) ->
+ {
+ metric.setDimensions(DimensionSet.of("AnotherService", "CustomService"));
+ metric.setDimensions(DimensionSet.of("AnotherService1", "CustomService1"));
+ });
LoggingUtils.appendKey("test", "willBeLogged");
@@ -62,11 +76,12 @@ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEv
TracingUtils.putAnnotation("Test", "New");
String output = String.format("{ \"message\": \"hello world\", \"location\": \"%s\" }", pageContents);
- TracingUtils.withSubsegment("loggingResponse", subsegment -> {
- String sampled = "log something out";
- log.info(sampled);
- log.info(output);
- });
+ TracingUtils.withSubsegment("loggingResponse", subsegment ->
+ {
+ String sampled = "log something out";
+ log.info(sampled);
+ log.info(output);
+ });
threadOption1();
@@ -91,10 +106,11 @@ private void threadOption1() throws InterruptedException {
private void threadOption2() throws InterruptedException {
Entity traceEntity = AWSXRay.getTraceEntity();
- Thread anotherThread = new Thread(() -> withEntitySubsegment("inlineLog", traceEntity, subsegment -> {
- String var = "somethingToProcess";
- log.info("inside threaded logging inline {}", var);
- }));
+ Thread anotherThread = new Thread(() -> withEntitySubsegment("inlineLog", traceEntity, subsegment ->
+ {
+ String var = "somethingToProcess";
+ log.info("inside threaded logging inline {}", var);
+ }));
anotherThread.start();
anotherThread.join();
}
diff --git a/examples/powertools-examples-core/src/main/java/helloworld/AppStream.java b/examples/powertools-examples-core/src/main/java/helloworld/AppStream.java
index aed048eef..401ef8c48 100644
--- a/examples/powertools-examples-core/src/main/java/helloworld/AppStream.java
+++ b/examples/powertools-examples-core/src/main/java/helloworld/AppStream.java
@@ -1,13 +1,26 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
package helloworld;
+import com.amazonaws.services.lambda.runtime.Context;
+import com.amazonaws.services.lambda.runtime.RequestStreamHandler;
+import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Map;
-
-import com.amazonaws.services.lambda.runtime.Context;
-import com.amazonaws.services.lambda.runtime.RequestStreamHandler;
-import com.fasterxml.jackson.databind.ObjectMapper;
import software.amazon.lambda.powertools.logging.Logging;
import software.amazon.lambda.powertools.metrics.Metrics;
diff --git a/examples/powertools-examples-core/src/test/java/helloworld/AppTest.java b/examples/powertools-examples-core/src/test/java/helloworld/AppTest.java
index b584ee944..70dad8d71 100644
--- a/examples/powertools-examples-core/src/test/java/helloworld/AppTest.java
+++ b/examples/powertools-examples-core/src/test/java/helloworld/AppTest.java
@@ -1,43 +1,59 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
package helloworld;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
import com.amazonaws.xray.AWSXRay;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
-import static org.junit.Assert.*;
-
public class AppTest {
- @Before
- public void setup() {
- if(null == System.getenv("LAMBDA_TASK_ROOT")) {
- AWSXRay.beginSegment("test");
+ @Before
+ public void setup() {
+ if (null == System.getenv("LAMBDA_TASK_ROOT")) {
+ AWSXRay.beginSegment("test");
+ }
}
- }
- @After
- public void tearDown() {
- if (AWSXRay.getCurrentSubsegmentOptional().isPresent()) {
- AWSXRay.endSubsegment();
+ @After
+ public void tearDown() {
+ if (AWSXRay.getCurrentSubsegmentOptional().isPresent()) {
+ AWSXRay.endSubsegment();
+ }
+
+ if (null == System.getenv("LAMBDA_TASK_ROOT")) {
+ AWSXRay.endSegment();
+ }
}
- if(null == System.getenv("LAMBDA_TASK_ROOT")) {
- AWSXRay.endSegment();
+ @Test
+ public void successfulResponse() {
+ App app = new App();
+ APIGatewayProxyResponseEvent result = app.handleRequest(null, null);
+ assertEquals(result.getStatusCode().intValue(), 200);
+ assertEquals(result.getHeaders().get("Content-Type"), "application/json");
+ String content = result.getBody();
+ assertNotNull(content);
+ assertTrue(content.contains("\"message\""));
+ assertTrue(content.contains("\"hello world\""));
+ assertTrue(content.contains("\"location\""));
}
- }
-
- @Test
- public void successfulResponse() {
- App app = new App();
- APIGatewayProxyResponseEvent result = app.handleRequest(null, null);
- assertEquals(result.getStatusCode().intValue(), 200);
- assertEquals(result.getHeaders().get("Content-Type"), "application/json");
- String content = result.getBody();
- assertNotNull(content);
- assertTrue(content.contains("\"message\""));
- assertTrue(content.contains("\"hello world\""));
- assertTrue(content.contains("\"location\""));
- }
}
diff --git a/examples/powertools-examples-idempotency/README.md b/examples/powertools-examples-idempotency/README.md
index 278484a92..6f6022054 100644
--- a/examples/powertools-examples-idempotency/README.md
+++ b/examples/powertools-examples-idempotency/README.md
@@ -1,21 +1,22 @@
-# Idempotency
+# Powertools for AWS Lambda (Java) - Idempotency Example
This project contains an example of Lambda function using the idempotency module of Powertools for AWS Lambda (Java). For more information on this module, please refer to the [documentation](https://docs.powertools.aws.dev/lambda-java/utilities/idempotency/).
+The example exposes a HTTP POST endpoint. When the user sends the address of a webpage to it, the endpoint fetches the contents of the URL and returns them to the user:
## 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.
+This sample is based on Serverless Application Model (SAM). To deploy it, check out the instructions for getting
+started with SAM in [the examples directory](../README.md)
-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:
+## Test the application
```bash
-Coreutilities$ sam build
-Coreutilities$ sam deploy --guided
+ 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"}'
```
+
+this should return the contents of the webpage, for instance:
+```json
+{ "message": "hello world", "location": "123.123.123.1" }
+```
+
+Check out [App.java](src/main/java/helloworld/App.java) to see how it works!
diff --git a/examples/powertools-examples-idempotency/pom.xml b/examples/powertools-examples-idempotency/pom.xml
index 7bdee0638..38682e164 100644
--- a/examples/powertools-examples-idempotency/pom.xml
+++ b/examples/powertools-examples-idempotency/pom.xml
@@ -1,3 +1,17 @@
+
+
4.0.0
diff --git a/examples/powertools-examples-idempotency/src/main/java/helloworld/App.java b/examples/powertools-examples-idempotency/src/main/java/helloworld/App.java
index f26877c34..ac2c7ef1b 100644
--- a/examples/powertools-examples-idempotency/src/main/java/helloworld/App.java
+++ b/examples/powertools-examples-idempotency/src/main/java/helloworld/App.java
@@ -1,9 +1,30 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
package helloworld;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
@@ -14,14 +35,6 @@
import software.amazon.lambda.powertools.logging.Logging;
import software.amazon.lambda.powertools.utilities.JsonConfig;
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.net.URL;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.stream.Collectors;
-
public class App implements RequestHandler {
private final static Logger log = LogManager.getLogger(App.class);
@@ -32,7 +45,8 @@ public App() {
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`
+ .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()
@@ -43,7 +57,11 @@ public App(DynamoDbClient client) {
}
/**
- * Try with:
+ * This is our Lambda event handler. It accepts HTTP POST requests from API gateway and returns the contents of the given URL. Requests are made idempotent
+ * by the idempotency library, and results are cached for the default 1h expiry time.
+ *
@@ -52,7 +70,7 @@ public App(DynamoDbClient client) {
*
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 ***
+ @Idempotent // The magic is here!
@Logging(logEvent = true)
public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) {
Map headers = new HashMap<>();
@@ -65,6 +83,7 @@ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEv
APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent()
.withHeaders(headers);
try {
+ // Read the 'address' field from the JSON post body
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);
@@ -81,8 +100,17 @@ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEv
}
}
- // 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)
+
+ /**
+ * Helper to retrieve the contents of the given URL and return them as a string.
+ *
+ * We could also put the @Idempotent annotation here if we only wanted this sub-operation to be idempotent. Putting
+ * it on the handler, however, reduces total execution time and saves us time!
+ *
+ * @param address The URL to fetch
+ * @return The contents of the given URL
+ * @throws IOException
+ */
private String getPageContents(String address) throws IOException {
URL url = new URL(address);
try (BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream(), "UTF-8"))) {
diff --git a/examples/powertools-examples-idempotency/src/test/java/helloworld/AppTest.java b/examples/powertools-examples-idempotency/src/test/java/helloworld/AppTest.java
index 7a5304e36..7f097906a 100644
--- a/examples/powertools-examples-idempotency/src/test/java/helloworld/AppTest.java
+++ b/examples/powertools-examples-idempotency/src/test/java/helloworld/AppTest.java
@@ -1,3 +1,17 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
package helloworld;
import com.amazonaws.services.dynamodbv2.local.main.ServerRunner;
@@ -5,6 +19,9 @@
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
import com.amazonaws.services.lambda.runtime.tests.EventLoader;
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.net.URI;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
@@ -16,23 +33,24 @@
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;
+import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition;
+import software.amazon.awssdk.services.dynamodb.model.BillingMode;
+import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest;
+import software.amazon.awssdk.services.dynamodb.model.KeySchemaElement;
+import software.amazon.awssdk.services.dynamodb.model.KeyType;
+import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType;
public class AppTest {
+ private static DynamoDbClient client;
@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[]{
+ DynamoDBProxyServer dynamoProxy = ServerRunner.createServerFromCommandLineArgs(new String[] {
"-inMemory",
"-port",
Integer.toString(port)
@@ -79,7 +97,8 @@ void setUp() {
@Test
public void testApp() {
- APIGatewayProxyResponseEvent response = app.handleRequest(EventLoader.loadApiGatewayRestEvent("event.json"), context);
+ APIGatewayProxyResponseEvent response =
+ app.handleRequest(EventLoader.loadApiGatewayRestEvent("event.json"), context);
Assertions.assertNotNull(response);
Assertions.assertTrue(response.getBody().contains("hello world"));
}
diff --git a/examples/powertools-examples-parameters/README.md b/examples/powertools-examples-parameters/README.md
index 0f843d455..a65307f69 100644
--- a/examples/powertools-examples-parameters/README.md
+++ b/examples/powertools-examples-parameters/README.md
@@ -1,21 +1,38 @@
-# Parameters
+# Powertools for AWS Lambda (Java) - Parameters Example
This project contains an example of Lambda function using the parameters module of Powertools for AWS Lambda (Java). For more information on this module, please refer to the [documentation](https://docs.powertools.aws.dev/lambda-java/utilities/parameters/).
+The example uses the [SSM Parameter Store](https://docs.powertools.aws.dev/lambda/java/utilities/parameters/#ssm-parameter-store)
+and the [Secrets Manager](https://docs.powertools.aws.dev/lambda/java/utilities/parameters/#secrets-manager) to inject
+runtime parameters into the application.
+Have a look at [ParametersFunction.java](src/main/java/org/demo/parameters/ParametersFunction.java) for the full details.
+
## 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.
+This sample is based on Serverless Application Model (SAM). To deploy it, check out the instructions for getting
+started with SAM in [the examples directory](../README.md)
-To use the SAM CLI, you need the following tools.
+## Test the application
-* 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)
+First, hit the URL of the application. You can do this with curl or your browser:
-To build and deploy your application for the first time, run the following in your shell:
+```bash
+ curl https://[REST-API-ID].execute-api.[REGION].amazonaws.com/Prod/params/
+```
+You will get your IP address back. The contents of the logs will be more interesting, and show you the values
+of the parameters injected into the handler:
```bash
-sam build
-sam deploy --guided
+sam logs --stack-name $MY_STACK_NAME --tail
+```
+
+```json
+{
+ ...
+ "thread": "main",
+ "level": "INFO",
+ "loggerName": "org.demo.parameters.ParametersFunction",
+ "message": "secretjsonobj=MyObject{id=23443, code='hk38543oj24kn796kp67bkb234gkj679l68'}\n",
+ ...
+}
```
diff --git a/examples/powertools-examples-parameters/src/main/java/org/demo/parameters/MyObject.java b/examples/powertools-examples-parameters/src/main/java/org/demo/parameters/MyObject.java
index 2cf145284..d406ae3df 100644
--- a/examples/powertools-examples-parameters/src/main/java/org/demo/parameters/MyObject.java
+++ b/examples/powertools-examples-parameters/src/main/java/org/demo/parameters/MyObject.java
@@ -1,3 +1,17 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
package org.demo.parameters;
public class MyObject {
diff --git a/examples/powertools-examples-parameters/src/main/java/org/demo/parameters/ParametersFunction.java b/examples/powertools-examples-parameters/src/main/java/org/demo/parameters/ParametersFunction.java
index 7f41e020e..5b691cfd9 100644
--- a/examples/powertools-examples-parameters/src/main/java/org/demo/parameters/ParametersFunction.java
+++ b/examples/powertools-examples-parameters/src/main/java/org/demo/parameters/ParametersFunction.java
@@ -1,15 +1,27 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
package org.demo.parameters;
+import static java.time.temporal.ChronoUnit.SECONDS;
+import static software.amazon.lambda.powertools.parameters.transform.Transformer.base64;
+import static software.amazon.lambda.powertools.parameters.transform.Transformer.json;
+
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-import software.amazon.lambda.powertools.parameters.ParamManager;
-import software.amazon.lambda.powertools.parameters.SSMProvider;
-import software.amazon.lambda.powertools.parameters.SecretsProvider;
-
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
@@ -17,13 +29,14 @@
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
-
-import static java.time.temporal.ChronoUnit.SECONDS;
-import static software.amazon.lambda.powertools.parameters.transform.Transformer.base64;
-import static software.amazon.lambda.powertools.parameters.transform.Transformer.json;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import software.amazon.lambda.powertools.parameters.ParamManager;
+import software.amazon.lambda.powertools.parameters.SSMProvider;
+import software.amazon.lambda.powertools.parameters.SecretsProvider;
public class ParametersFunction implements RequestHandler {
- private final static Logger log = LogManager.getLogger();
+ private final static Logger log = LogManager.getLogger(ParametersFunction.class);
SSMProvider ssmProvider = ParamManager.getSsmProvider();
SecretsProvider secretsProvider = ParamManager.getSecretsProvider();
@@ -34,8 +47,10 @@ public class ParametersFunction implements RequestHandler allValues = ssmProvider.getMultiple("/powertools-java/sample");
String b64value = ssmProvider.withTransformation(base64).get("/powertools-java/sample/keybase64");
- Map secretJson = secretsProvider.withTransformation(json).get("/powertools-java/userpwd", Map.class);
- MyObject secretJsonObj = secretsProvider.withMaxAge(42, SECONDS).withTransformation(json).get("/powertools-java/secretcode", MyObject.class);
+ Map secretJson =
+ secretsProvider.withTransformation(json).get("/powertools-java/userpwd", Map.class);
+ MyObject secretJsonObj = secretsProvider.withMaxAge(42, SECONDS).withTransformation(json)
+ .get("/powertools-java/secretcode", MyObject.class);
@Override
public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
@@ -72,9 +87,9 @@ public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent in
}
}
- private String getPageContents(String address) throws IOException{
+ private String getPageContents(String address) throws IOException {
URL url = new URL(address);
- try(BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream(), "UTF-8"))) {
+ try (BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream(), "UTF-8"))) {
return br.lines().collect(Collectors.joining(System.lineSeparator()));
}
}
diff --git a/examples/powertools-examples-parameters/template.yaml b/examples/powertools-examples-parameters/template.yaml
index 052cfcdc7..9d3bf8b0e 100644
--- a/examples/powertools-examples-parameters/template.yaml
+++ b/examples/powertools-examples-parameters/template.yaml
@@ -19,6 +19,9 @@ Resources:
Handler: org.demo.parameters.ParametersFunction::handleRequest
MemorySize: 512
Tracing: Active
+ Environment:
+ Variables:
+ LOG_LEVEL: INFO
Policies:
- AWSSecretsManagerGetSecretValuePolicy:
SecretArn: !Ref UserPwd
diff --git a/examples/powertools-examples-serialization/README.md b/examples/powertools-examples-serialization/README.md
index 7f70da1f2..4e3f66eb0 100644
--- a/examples/powertools-examples-serialization/README.md
+++ b/examples/powertools-examples-serialization/README.md
@@ -1,21 +1,77 @@
-# Deserialization
+# Powertools for AWS Lambda (Java) - Serialization Example
This project contains an example of Lambda function using the serialization utilities module of Powertools for AWS Lambda (Java). For more information on this module, please refer to the [documentation](https://docs.powertools.aws.dev/lambda-java/utilities/serialization/).
+The project contains two `RequestHandler`s -
+
+* [APIGatewayRequestDeserializationFunction](src/main/java/org/demo/serialization/APIGatewayRequestDeserializationFunction.java) - Uses the serialization library to deserialize an API Gateway request body
+* [SQSEventDeserializationFunction](src/main/java/org/demo/serialization/SQSEventDeserializationFunction.java) - Uses the serialization library to deserialize an SQS message body
+
+In both cases, the output of the serialized message will be printed to the function logs. The message format
+in JSON looks like this:
+
+```json
+{
+ "id":1234,
+ "name":"product",
+ "price":42
+}
+```
+
## 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.
+This sample is based on Serverless Application Model (SAM). To deploy it, check out the instructions for getting
+started with SAM in [the examples directory](../README.md)
+
+## Test the application
+
+### 1. API Gateway Endpoint
+
+To test the HTTP endpoint, we can post a product to the test URL:
+
+```bash
+curl -X POST https://[REST-API-ID].execute-api.[REGION].amazonaws.com/Prod/product/ -H "Content-Type: application/json" -d '{"id": 1234, "name": "product", "price": 42}'
+```
+
+The result will indicate that the handler has successfully deserialized the request body:
-To use the SAM CLI, you need the following tools.
+```
+Received request for productId: 1234
+```
+
+If we look at the logs using `sam logs --tail --stack-name $MY_STACK`, we will see the full deserialized request:
+
+```json
+{
+ ...
+ "level": "INFO",
+ "loggerName": "org.demo.serialization.APIGatewayRequestDeserializationFunction",
+ "message": "product=Product{id=1234, name='product', price=42.0}\n",
+ ...
+}
+```
+
+### 2. SQS Queue
+For the SQS handler, we have to send a request to our queue. We can either construct the Queue URL (see below), or
+find it from the SQS section of the AWS console.
-* 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)
+```bash
+ aws sqs send-message --queue-url "https://sqs.[REGION].amazonaws.com/[ACCOUNT-ID]/sqs-event-deserialization-queue" --message-body '{"id": 1234, "name": "product", "price": 123}'
+```
+
+Here we can find the message by filtering through the logs for messages that have come back from our SQS handler:
-To build and deploy your application for the first time, run the following in your shell:
+```bash
+sam logs --tail --stack-name $MY_STACK --filter SQS
+```
```bash
-sam build
-sam deploy --guided
+ {
+ ...
+ "level": "INFO",
+ "loggerName": "org.demo.serialization.SQSEventDeserializationFunction",
+ "message": "products=[Product{id=1234, name='product', price=42.0}]\n",
+ ...
+}
+
```
diff --git a/examples/powertools-examples-serialization/src/main/java/org/demo/serialization/APIGatewayRequestDeserializationFunction.java b/examples/powertools-examples-serialization/src/main/java/org/demo/serialization/APIGatewayRequestDeserializationFunction.java
index 8c33baed9..e70b37959 100644
--- a/examples/powertools-examples-serialization/src/main/java/org/demo/serialization/APIGatewayRequestDeserializationFunction.java
+++ b/examples/powertools-examples-serialization/src/main/java/org/demo/serialization/APIGatewayRequestDeserializationFunction.java
@@ -1,19 +1,33 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
package org.demo.serialization;
+import static software.amazon.lambda.powertools.utilities.EventDeserializer.extractDataFrom;
+
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-
import java.util.HashMap;
import java.util.Map;
-
-import static software.amazon.lambda.powertools.utilities.EventDeserializer.extractDataFrom;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
-public class APIGatewayRequestDeserializationFunction implements RequestHandler {
+public class APIGatewayRequestDeserializationFunction
+ implements RequestHandler {
private final static Logger LOGGER = LogManager.getLogger(APIGatewayRequestDeserializationFunction.class);
private static final Map HEADERS = new HashMap() {{
@@ -28,9 +42,9 @@ public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent ev
LOGGER.info("product={}\n", product);
return new APIGatewayProxyResponseEvent()
- .withHeaders(HEADERS)
- .withStatusCode(200)
- .withBody("Received request for productId: " + product.getId());
+ .withHeaders(HEADERS)
+ .withStatusCode(200)
+ .withBody("Received request for productId: " + product.getId());
}
}
diff --git a/examples/powertools-examples-serialization/src/main/java/org/demo/serialization/Product.java b/examples/powertools-examples-serialization/src/main/java/org/demo/serialization/Product.java
index fb94a99f8..25bae34f6 100644
--- a/examples/powertools-examples-serialization/src/main/java/org/demo/serialization/Product.java
+++ b/examples/powertools-examples-serialization/src/main/java/org/demo/serialization/Product.java
@@ -1,3 +1,17 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
package org.demo.serialization;
public class Product {
diff --git a/examples/powertools-examples-serialization/src/main/java/org/demo/serialization/SQSEventDeserializationFunction.java b/examples/powertools-examples-serialization/src/main/java/org/demo/serialization/SQSEventDeserializationFunction.java
index 129fe0243..36dbed074 100644
--- a/examples/powertools-examples-serialization/src/main/java/org/demo/serialization/SQSEventDeserializationFunction.java
+++ b/examples/powertools-examples-serialization/src/main/java/org/demo/serialization/SQSEventDeserializationFunction.java
@@ -1,15 +1,28 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
package org.demo.serialization;
+import static software.amazon.lambda.powertools.utilities.EventDeserializer.extractDataFrom;
+
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.SQSEvent;
+import java.util.List;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
-import java.util.List;
-
-import static software.amazon.lambda.powertools.utilities.EventDeserializer.extractDataFrom;
-
public class SQSEventDeserializationFunction implements RequestHandler {
diff --git a/examples/powertools-examples-serialization/src/test/java/org/demo/serialization/APIGatewayRequestDeserializationFunctionTest.java b/examples/powertools-examples-serialization/src/test/java/org/demo/serialization/APIGatewayRequestDeserializationFunctionTest.java
index 5d5da7ecc..ec8cdbd33 100644
--- a/examples/powertools-examples-serialization/src/test/java/org/demo/serialization/APIGatewayRequestDeserializationFunctionTest.java
+++ b/examples/powertools-examples-serialization/src/test/java/org/demo/serialization/APIGatewayRequestDeserializationFunctionTest.java
@@ -1,5 +1,21 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
package org.demo.serialization;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
@@ -8,8 +24,6 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
class APIGatewayRequestDeserializationFunctionTest {
@Mock
diff --git a/examples/powertools-examples-serialization/src/test/java/org/demo/serialization/SQSEventDeserializationFunctionTest.java b/examples/powertools-examples-serialization/src/test/java/org/demo/serialization/SQSEventDeserializationFunctionTest.java
index 6979a6868..b46af3052 100644
--- a/examples/powertools-examples-serialization/src/test/java/org/demo/serialization/SQSEventDeserializationFunctionTest.java
+++ b/examples/powertools-examples-serialization/src/test/java/org/demo/serialization/SQSEventDeserializationFunctionTest.java
@@ -1,17 +1,29 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
package org.demo.serialization;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.events.SQSEvent;
+import java.util.ArrayList;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
-import java.util.List;
-import java.util.ArrayList;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
class SQSEventDeserializationFunctionTest {
@Mock
@@ -29,7 +41,7 @@ public void shouldReturnNumberOfReceivedMessages() {
SQSEvent.SQSMessage message1 = messageWithBody("{ \"id\": 1234, \"name\": \"product\", \"price\": 42}");
SQSEvent.SQSMessage message2 = messageWithBody("{ \"id\": 12345, \"name\": \"product5\", \"price\": 45}");
SQSEvent event = new SQSEvent();
- event.setRecords(new ArrayList(){{
+ event.setRecords(new ArrayList() {{
add(message1);
add(message2);
}});
diff --git a/examples/powertools-examples-serialization/template.yaml b/examples/powertools-examples-serialization/template.yaml
index 539d2d615..f330ec146 100644
--- a/examples/powertools-examples-serialization/template.yaml
+++ b/examples/powertools-examples-serialization/template.yaml
@@ -32,7 +32,7 @@ Resources:
SQSEventDeserializationFunction:
Type: AWS::Serverless::Function
Properties:
- CodeUri: Function
+ CodeUri: .
Handler: org.demo.serialization.SQSEventDeserializationFunction::handleRequest
Policies:
- Statement:
diff --git a/examples/powertools-examples-sqs/README.md b/examples/powertools-examples-sqs/README.md
deleted file mode 100644
index 2b6da65b5..000000000
--- a/examples/powertools-examples-sqs/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-## SqsBatchProcessingDemo
-
-Demos setup of SQS Batch processing via Powertools
diff --git a/examples/powertools-examples-sqs/events/event.json b/examples/powertools-examples-sqs/events/event.json
deleted file mode 100644
index 3822fadaa..000000000
--- a/examples/powertools-examples-sqs/events/event.json
+++ /dev/null
@@ -1,63 +0,0 @@
-{
- "body": "{\"message\": \"hello world\"}",
- "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/examples/powertools-examples-sqs/src/main/java/org/demo/sqs/SqsMessageSender.java b/examples/powertools-examples-sqs/src/main/java/org/demo/sqs/SqsMessageSender.java
deleted file mode 100644
index b90c50654..000000000
--- a/examples/powertools-examples-sqs/src/main/java/org/demo/sqs/SqsMessageSender.java
+++ /dev/null
@@ -1,79 +0,0 @@
-
-package org.demo.sqs;
-
-import com.amazonaws.services.lambda.runtime.Context;
-import com.amazonaws.services.lambda.runtime.RequestHandler;
-import com.amazonaws.services.lambda.runtime.events.ScheduledEvent;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.datatype.joda.JodaModule;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient;
-import software.amazon.awssdk.services.sqs.SqsClient;
-import software.amazon.awssdk.services.sqs.model.MessageAttributeValue;
-import software.amazon.awssdk.services.sqs.model.SendMessageBatchRequest;
-import software.amazon.awssdk.services.sqs.model.SendMessageBatchRequestEntry;
-import software.amazon.awssdk.services.sqs.model.SendMessageBatchResponse;
-import software.amazon.lambda.powertools.logging.Logging;
-import software.amazon.lambda.powertools.logging.LoggingUtils;
-
-import java.security.SecureRandom;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Random;
-import java.util.stream.IntStream;
-
-import static java.util.stream.Collectors.toList;
-
-public class SqsMessageSender implements RequestHandler {
-
- private static final Logger log = LogManager.getLogger(SqsMessageSender.class);
-
- private static final SqsClient sqsClient = SqsClient.builder()
- .httpClient(UrlConnectionHttpClient.create())
- .build();
-
- private static final Random random = new SecureRandom();
-
- private static final ObjectMapper objectMapper;
-
- static {
- objectMapper = new ObjectMapper();
- objectMapper.registerModule(new JodaModule());
- LoggingUtils.defaultObjectMapper(objectMapper);
- }
-
- @Logging(logEvent = true)
- public String handleRequest(final ScheduledEvent input, final Context context) {
- String queueUrl = System.getenv("QUEUE_URL");
-
- // Push 5 messages on each invoke.
- List batchRequestEntries = IntStream.range(0, 5)
- .mapToObj(value -> {
- Map attributeValueHashMap = new HashMap<>();
- attributeValueHashMap.put("Key" + value, MessageAttributeValue.builder()
- .dataType("String")
- .stringValue("Value" + value)
- .build());
-
- byte[] array = new byte[7];
- random.nextBytes(array);
-
- return SendMessageBatchRequestEntry.builder()
- .messageAttributes(attributeValueHashMap)
- .id(input.getId() + value)
- .messageBody("Sample Message " + value)
- .build();
- }).collect(toList());
-
- SendMessageBatchResponse sendMessageBatchResponse = sqsClient.sendMessageBatch(SendMessageBatchRequest.builder()
- .queueUrl(queueUrl)
- .entries(batchRequestEntries)
- .build());
-
- log.info("Sent Message {}", sendMessageBatchResponse);
-
- return "Success";
- }
-}
\ No newline at end of file
diff --git a/examples/powertools-examples-sqs/src/main/java/org/demo/sqs/SqsPoller.java b/examples/powertools-examples-sqs/src/main/java/org/demo/sqs/SqsPoller.java
deleted file mode 100644
index bf2b7bdfe..000000000
--- a/examples/powertools-examples-sqs/src/main/java/org/demo/sqs/SqsPoller.java
+++ /dev/null
@@ -1,61 +0,0 @@
-package org.demo.sqs;
-
-import java.util.Random;
-
-import com.amazonaws.services.lambda.runtime.Context;
-import com.amazonaws.services.lambda.runtime.RequestHandler;
-import com.amazonaws.services.lambda.runtime.events.SQSEvent;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient;
-import software.amazon.awssdk.services.sqs.SqsClient;
-import software.amazon.lambda.powertools.logging.Logging;
-import software.amazon.lambda.powertools.sqs.SqsBatch;
-import software.amazon.lambda.powertools.sqs.SqsMessageHandler;
-import software.amazon.lambda.powertools.sqs.SqsUtils;
-import java.security.SecureRandom;
-
-import static com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage;
-
-/**
- * Handler for requests to Lambda function.
- */
-public class SqsPoller implements RequestHandler {
-
- Logger log = LogManager.getLogger(SqsPoller.class);
- Random random = new SecureRandom();
-
- static {
- // https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/lambda-optimize-starttime.html
- SqsUtils.overrideSqsClient(SqsClient.builder()
- .httpClient(UrlConnectionHttpClient.create())
- .build());
- }
-
- @SqsBatch(value = BatchProcessor.class, nonRetryableExceptions = {IllegalArgumentException.class})
- @Logging(logEvent = true)
- public String handleRequest(final SQSEvent input, final Context context) {
- return "Success";
- }
-
- private class BatchProcessor implements SqsMessageHandler
-
- software.amazon.lambda
- powertools-sqs
- ${project.version}
- software.amazon.lambdapowertools-tracing
@@ -229,27 +237,22 @@
- org.junit.jupiter
- junit-jupiter-api
- ${junit-jupiter.version}
- test
-
-
- org.junit.jupiter
- junit-jupiter-engine
- ${junit-jupiter.version}
- test
+ org.junit
+ junit-bom
+ ${junit.version}
+ pom
+ import
- org.junit.jupiter
- junit-jupiter-params
- ${junit-jupiter.version}
+ org.junit-pioneer
+ junit-pioneer
+ 1.9.1testorg.apache.commonscommons-lang3
- 3.12.0
+ 3.13.0test
@@ -516,6 +519,7 @@
3.1.2
+ @{argLine}
--add-opens java.base/java.util=ALL-UNNAMED
--add-opens java.base/java.lang=ALL-UNNAMED
@@ -524,6 +528,45 @@
+
+ newerThanJdk8
+
+ [9,)
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+ 3.3.0
+
+ checkstyle.xml
+ UTF-8
+ true
+ true
+ false
+
+
+
+
+
+ com.puppycrawl.tools
+ checkstyle
+ 10.12.2
+
+
+
+
+
+ check
+
+
+
+
+
+
+
diff --git a/powertools-cloudformation/pom.xml b/powertools-cloudformation/pom.xml
index 0a89445fb..336a3f94c 100644
--- a/powertools-cloudformation/pom.xml
+++ b/powertools-cloudformation/pom.xml
@@ -1,4 +1,18 @@
+
+
@@ -100,4 +114,12 @@
+
+
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+
+
+
\ No newline at end of file
diff --git a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/AbstractCustomResourceHandler.java b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/AbstractCustomResourceHandler.java
index 7d3a43069..7f5b6bb24 100644
--- a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/AbstractCustomResourceHandler.java
+++ b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/AbstractCustomResourceHandler.java
@@ -1,16 +1,29 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
package software.amazon.lambda.powertools.cloudformation;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent;
+import java.io.IOException;
+import java.util.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import software.amazon.awssdk.http.SdkHttpClient;
import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient;
-import java.io.IOException;
-import java.util.Objects;
-
/**
* Handler base class providing core functionality for sending responses to custom CloudFormation resources after
* receiving some event. Depending on the type of event, this class either invokes the crete, update, or delete method
diff --git a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java
index 39a86293b..2f020aa25 100644
--- a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java
+++ b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java
@@ -1,3 +1,17 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
package software.amazon.lambda.powertools.cloudformation;
import com.amazonaws.services.lambda.runtime.Context;
@@ -6,6 +20,13 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.node.ObjectNode;
+import java.io.IOException;
+import java.net.URI;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import software.amazon.awssdk.http.Header;
@@ -16,14 +37,6 @@
import software.amazon.awssdk.http.SdkHttpRequest;
import software.amazon.awssdk.utils.StringInputStream;
-import java.io.IOException;
-import java.net.URI;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-
/**
* Client for sending responses to AWS CloudFormation custom resources by way of a response URL, which is an Amazon S3
* pre-signed URL.
@@ -35,102 +48,6 @@
class CloudFormationResponse {
private static final Logger LOG = LoggerFactory.getLogger(CloudFormationResponse.class);
-
- /**
- * Internal representation of the payload to be sent to the event target URL. Retains all properties of the payload
- * except for "Data". This is done so that the serialization of the non-"Data" properties and the serialization of
- * the value of "Data" can be handled by separate ObjectMappers, if need be. The former properties are dictated by
- * the custom resource but the latter is dictated by the implementor of the custom resource handler.
- */
- @SuppressWarnings("unused")
- static class ResponseBody {
- static final ObjectMapper MAPPER = new ObjectMapper()
- .setPropertyNamingStrategy(PropertyNamingStrategies.UPPER_CAMEL_CASE);
- private static final String DATA_PROPERTY_NAME = "Data";
-
- private final String status;
- private final String reason;
- private final String physicalResourceId;
- private final String stackId;
- private final String requestId;
- private final String logicalResourceId;
- private final boolean noEcho;
-
- ResponseBody(CloudFormationCustomResourceEvent event,
- Response.Status responseStatus,
- String physicalResourceId,
- boolean noEcho,
- String reason) {
- Objects.requireNonNull(event, "CloudFormationCustomResourceEvent cannot be null");
-
- this.physicalResourceId = physicalResourceId;
- this.reason = reason;
- this.status = responseStatus == null ? Response.Status.SUCCESS.name() : responseStatus.name();
- this.stackId = event.getStackId();
- this.requestId = event.getRequestId();
- this.logicalResourceId = event.getLogicalResourceId();
- this.noEcho = noEcho;
- }
-
- public String getStatus() {
- return status;
- }
-
- public String getReason() {
- return reason;
- }
-
- public String getPhysicalResourceId() {
- return physicalResourceId;
- }
-
- public String getStackId() {
- return stackId;
- }
-
- public String getRequestId() {
- return requestId;
- }
-
- public String getLogicalResourceId() {
- return logicalResourceId;
- }
-
- public boolean isNoEcho() {
- return noEcho;
- }
-
- /**
- * Returns this ResponseBody as an ObjectNode with the provided JsonNode as the value of its "Data" property.
- *
- * @param dataNode the value of the "Data" property for the returned node; may be null
- * @return an ObjectNode representation of this ResponseBody and the provided dataNode
- */
- ObjectNode toObjectNode(JsonNode dataNode) {
- ObjectNode node = MAPPER.valueToTree(this);
- if (dataNode == null) {
- node.putNull(DATA_PROPERTY_NAME);
- } else {
- node.set(DATA_PROPERTY_NAME, dataNode);
- }
- return node;
- }
-
- @Override
- public String toString() {
- final StringBuffer sb = new StringBuffer("ResponseBody{");
- sb.append("status='").append(status).append('\'');
- sb.append(", reason='").append(reason).append('\'');
- sb.append(", physicalResourceId='").append(physicalResourceId).append('\'');
- sb.append(", stackId='").append(stackId).append('\'');
- sb.append(", requestId='").append(requestId).append('\'');
- sb.append(", logicalResourceId='").append(logicalResourceId).append('\'');
- sb.append(", noEcho=").append(noEcho);
- sb.append('}');
- return sb.toString();
- }
- }
-
private final SdkHttpClient client;
/**
@@ -212,7 +129,7 @@ protected Map> headers(int contentLength) {
/**
* Returns the response body as an input stream, for supplying with the HTTP request to the custom resource.
- *
+ *
* If PhysicalResourceId is null at this point it will be replaced with the Lambda LogStreamName.
*
* @throws CustomResourceResponseException if unable to generate the response stream
@@ -223,7 +140,8 @@ StringInputStream responseBodyStream(CloudFormationCustomResourceEvent event,
try {
String reason = "See the details in CloudWatch Log Stream: " + context.getLogStreamName();
if (resp == null) {
- String physicalResourceId = event.getPhysicalResourceId() != null? event.getPhysicalResourceId() : context.getLogStreamName();
+ String physicalResourceId = event.getPhysicalResourceId() != null ? event.getPhysicalResourceId() :
+ context.getLogStreamName();
ResponseBody body = new ResponseBody(event, Response.Status.SUCCESS, physicalResourceId, false, reason);
LOG.debug("ResponseBody: {}", body);
@@ -232,9 +150,11 @@ StringInputStream responseBodyStream(CloudFormationCustomResourceEvent event,
} else {
String physicalResourceId = resp.getPhysicalResourceId() != null ? resp.getPhysicalResourceId() :
- event.getPhysicalResourceId() != null? event.getPhysicalResourceId() : context.getLogStreamName();
+ event.getPhysicalResourceId() != null ? event.getPhysicalResourceId() :
+ context.getLogStreamName();
- ResponseBody body = new ResponseBody(event, resp.getStatus(), physicalResourceId, resp.isNoEcho(), reason);
+ ResponseBody body =
+ new ResponseBody(event, resp.getStatus(), physicalResourceId, resp.isNoEcho(), reason);
LOG.debug("ResponseBody: {}", body);
ObjectNode node = body.toObjectNode(resp.getJsonNode());
return new StringInputStream(node.toString());
@@ -244,4 +164,99 @@ StringInputStream responseBodyStream(CloudFormationCustomResourceEvent event,
throw new CustomResourceResponseException("Unable to generate response body.", e);
}
}
+
+ /**
+ * Internal representation of the payload to be sent to the event target URL. Retains all properties of the payload
+ * except for "Data". This is done so that the serialization of the non-"Data" properties and the serialization of
+ * the value of "Data" can be handled by separate ObjectMappers, if need be. The former properties are dictated by
+ * the custom resource but the latter is dictated by the implementor of the custom resource handler.
+ */
+ @SuppressWarnings("unused")
+ static class ResponseBody {
+ static final ObjectMapper MAPPER = new ObjectMapper()
+ .setPropertyNamingStrategy(PropertyNamingStrategies.UPPER_CAMEL_CASE);
+ private static final String DATA_PROPERTY_NAME = "Data";
+
+ private final String status;
+ private final String reason;
+ private final String physicalResourceId;
+ private final String stackId;
+ private final String requestId;
+ private final String logicalResourceId;
+ private final boolean noEcho;
+
+ ResponseBody(CloudFormationCustomResourceEvent event,
+ Response.Status responseStatus,
+ String physicalResourceId,
+ boolean noEcho,
+ String reason) {
+ Objects.requireNonNull(event, "CloudFormationCustomResourceEvent cannot be null");
+
+ this.physicalResourceId = physicalResourceId;
+ this.reason = reason;
+ this.status = responseStatus == null ? Response.Status.SUCCESS.name() : responseStatus.name();
+ this.stackId = event.getStackId();
+ this.requestId = event.getRequestId();
+ this.logicalResourceId = event.getLogicalResourceId();
+ this.noEcho = noEcho;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public String getReason() {
+ return reason;
+ }
+
+ public String getPhysicalResourceId() {
+ return physicalResourceId;
+ }
+
+ public String getStackId() {
+ return stackId;
+ }
+
+ public String getRequestId() {
+ return requestId;
+ }
+
+ public String getLogicalResourceId() {
+ return logicalResourceId;
+ }
+
+ public boolean isNoEcho() {
+ return noEcho;
+ }
+
+ /**
+ * Returns this ResponseBody as an ObjectNode with the provided JsonNode as the value of its "Data" property.
+ *
+ * @param dataNode the value of the "Data" property for the returned node; may be null
+ * @return an ObjectNode representation of this ResponseBody and the provided dataNode
+ */
+ ObjectNode toObjectNode(JsonNode dataNode) {
+ ObjectNode node = MAPPER.valueToTree(this);
+ if (dataNode == null) {
+ node.putNull(DATA_PROPERTY_NAME);
+ } else {
+ node.set(DATA_PROPERTY_NAME, dataNode);
+ }
+ return node;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuffer sb = new StringBuffer("ResponseBody{");
+ sb.append("status='").append(status).append('\'');
+ sb.append(", reason='").append(reason).append('\'');
+ sb.append(", physicalResourceId='").append(physicalResourceId).append('\'');
+ sb.append(", stackId='").append(stackId).append('\'');
+ sb.append(", requestId='").append(requestId).append('\'');
+ sb.append(", logicalResourceId='").append(logicalResourceId).append('\'');
+ sb.append(", noEcho=").append(noEcho);
+ sb.append('}');
+ return sb.toString();
+ }
+ }
}
diff --git a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CustomResourceResponseException.java b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CustomResourceResponseException.java
index ead912392..904ae9c3f 100644
--- a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CustomResourceResponseException.java
+++ b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CustomResourceResponseException.java
@@ -1,3 +1,17 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
package software.amazon.lambda.powertools.cloudformation;
/**
diff --git a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/Response.java b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/Response.java
index f388f6384..fe18000d4 100644
--- a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/Response.java
+++ b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/Response.java
@@ -1,8 +1,21 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
package software.amazon.lambda.powertools.cloudformation;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
-
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
@@ -13,119 +26,16 @@
*/
public class Response {
- /**
- * Indicates whether a response is a success or failure.
- */
- public enum Status {
- SUCCESS, FAILED
- }
-
- /**
- * For building Response instances.
- */
- public static class Builder {
- private Object value;
- private ObjectMapper objectMapper;
- private Status status;
- private String physicalResourceId;
- private boolean noEcho;
-
- private Builder() {
- }
-
- /**
- * Configures the value of this Response, typically a Map of name/value pairs.
- *
- * @param value if null, the Response will be empty
- * @return a reference to this builder
- */
- public Builder value(Object value) {
- this.value = value;
- return this;
- }
-
- /**
- * Configures a custom ObjectMapper for serializing the value object. Creates a copy of the mapper provided;
- * future mutations of the ObjectMapper made using the provided reference will not affect Response
- * serialization.
- *
- * @param objectMapper if null, a default mapper will be used
- * @return a reference to this builder
- */
- public Builder objectMapper(ObjectMapper objectMapper) {
- this.objectMapper = objectMapper == null ? null : objectMapper.copy();
- return this;
- }
-
- /**
- * Configures the status of this response.
- *
- * @param status if null, SUCCESS will be assumed
- * @return a reference to this builder
- */
- public Builder status(Status status) {
- this.status = status;
- return this;
- }
-
- /**
- * A unique identifier for the custom resource being responded to. By default, the identifier is the name of the
- * Amazon CloudWatch Logs log stream associated with the Lambda function.
- *
- * @param physicalResourceId if null, the default resource ID will be used
- * @return a reference to this builder
- */
- public Builder physicalResourceId(String physicalResourceId) {
- this.physicalResourceId = physicalResourceId;
- return this;
- }
-
- /**
- * Indicates whether to mask the output of the custom resource when it's retrieved by using the Fn::GetAtt
- * function. If set to true, values will be masked with asterisks (*****), except for information stored in the
- * these locations:
- *
- *
The Metadata template section. CloudFormation does not transform, modify, or redact any information
- * included in the Metadata section. See
- * https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/metadata-section-structure.html
- *
The Outputs template section. See
- * https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html
- *
The Metadata attribute of a resource definition. See
- * https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-metadata.html
- *
- *
- * We strongly recommend not using these mechanisms to include sensitive information, such as passwords or
- * secrets.
- *
- * For more information about using noEcho to mask sensitive information, see
- * https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/best-practices.html#creds
- *
- * By default, this value is false.
- *
- * @param noEcho when true, masks certain output
- * @return a reference to this builder
- */
- public Builder noEcho(boolean noEcho) {
- this.noEcho = noEcho;
- return this;
- }
+ private final JsonNode jsonNode;
+ private final Status status;
+ private final String physicalResourceId;
+ private final boolean noEcho;
- /**
- * Builds a Response object for the value.
- *
- * @return a Response object wrapping the initially provided value.
- */
- public Response build() {
- JsonNode node;
- if (value == null) {
- node = null;
- } else {
- ObjectMapper mapper = objectMapper != null ? objectMapper : CloudFormationResponse.ResponseBody.MAPPER;
- node = mapper.valueToTree(value);
- }
- Status responseStatus = this.status != null ? this.status : Status.SUCCESS;
- return new Response(node, responseStatus, physicalResourceId, noEcho);
- }
+ private Response(JsonNode jsonNode, Status status, String physicalResourceId, boolean noEcho) {
+ this.jsonNode = jsonNode;
+ this.status = status;
+ this.physicalResourceId = physicalResourceId;
+ this.noEcho = noEcho;
}
/**
@@ -140,14 +50,14 @@ public static Builder builder() {
/**
* Creates a failed Response with no physicalResourceId set. Powertools for AWS Lambda (Java) will set the physicalResourceId to the
* Lambda LogStreamName
- *
+ *
* The value returned for a PhysicalResourceId can change custom resource update operations. If the value returned
* is the same, it is considered a normal update. If the value returned is different, AWS CloudFormation recognizes
* the update as a replacement and sends a delete request to the old resource. For more information,
* see AWS::CloudFormation::CustomResource.
*
- * @deprecated this method is not safe. Provide a physicalResourceId.
* @return a failed Response with no value.
+ * @deprecated this method is not safe. Provide a physicalResourceId.
*/
@Deprecated
public static Response failed() {
@@ -173,14 +83,14 @@ public static Response failed(String physicalResourceId) {
/**
* Creates a successful Response with no physicalResourceId set. Powertools for AWS Lambda (Java) will set the physicalResourceId to the
* Lambda LogStreamName
- *
+ *
* The value returned for a PhysicalResourceId can change custom resource update operations. If the value returned
* is the same, it is considered a normal update. If the value returned is different, AWS CloudFormation recognizes
* the update as a replacement and sends a delete request to the old resource. For more information,
* see AWS::CloudFormation::CustomResource.
*
- * @deprecated this method is not safe. Provide a physicalResourceId.
* @return a success Response with no physicalResourceId value.
+ * @deprecated this method is not safe. Provide a physicalResourceId.
*/
@Deprecated
public static Response success() {
@@ -203,18 +113,6 @@ public static Response success(String physicalResourceId) {
return new Response(null, Status.SUCCESS, physicalResourceId, false);
}
- private final JsonNode jsonNode;
- private final Status status;
- private final String physicalResourceId;
- private final boolean noEcho;
-
- private Response(JsonNode jsonNode, Status status, String physicalResourceId, boolean noEcho) {
- this.jsonNode = jsonNode;
- this.status = status;
- this.physicalResourceId = physicalResourceId;
- this.noEcho = noEcho;
- }
-
/**
* Returns a JsonNode representation of the Response.
*
@@ -267,4 +165,119 @@ public String toString() {
.map(entry -> entry.getKey() + " = " + entry.getValue())
.collect(Collectors.joining(",", "[", "]"));
}
+
+ /**
+ * Indicates whether a response is a success or failure.
+ */
+ public enum Status {
+ SUCCESS, FAILED
+ }
+
+ /**
+ * For building Response instances.
+ */
+ public static class Builder {
+ private Object value;
+ private ObjectMapper objectMapper;
+ private Status status;
+ private String physicalResourceId;
+ private boolean noEcho;
+
+ private Builder() {
+ }
+
+ /**
+ * Configures the value of this Response, typically a Map of name/value pairs.
+ *
+ * @param value if null, the Response will be empty
+ * @return a reference to this builder
+ */
+ public Builder value(Object value) {
+ this.value = value;
+ return this;
+ }
+
+ /**
+ * Configures a custom ObjectMapper for serializing the value object. Creates a copy of the mapper provided;
+ * future mutations of the ObjectMapper made using the provided reference will not affect Response
+ * serialization.
+ *
+ * @param objectMapper if null, a default mapper will be used
+ * @return a reference to this builder
+ */
+ public Builder objectMapper(ObjectMapper objectMapper) {
+ this.objectMapper = objectMapper == null ? null : objectMapper.copy();
+ return this;
+ }
+
+ /**
+ * Configures the status of this response.
+ *
+ * @param status if null, SUCCESS will be assumed
+ * @return a reference to this builder
+ */
+ public Builder status(Status status) {
+ this.status = status;
+ return this;
+ }
+
+ /**
+ * A unique identifier for the custom resource being responded to. By default, the identifier is the name of the
+ * Amazon CloudWatch Logs log stream associated with the Lambda function.
+ *
+ * @param physicalResourceId if null, the default resource ID will be used
+ * @return a reference to this builder
+ */
+ public Builder physicalResourceId(String physicalResourceId) {
+ this.physicalResourceId = physicalResourceId;
+ return this;
+ }
+
+ /**
+ * Indicates whether to mask the output of the custom resource when it's retrieved by using the Fn::GetAtt
+ * function. If set to true, values will be masked with asterisks (*****), except for information stored in the
+ * these locations:
+ *
+ *
The Metadata template section. CloudFormation does not transform, modify, or redact any information
+ * included in the Metadata section. See
+ * https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/metadata-section-structure.html
+ *
The Outputs template section. See
+ * https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html
+ *
The Metadata attribute of a resource definition. See
+ * https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-metadata.html
+ *
+ *
+ * We strongly recommend not using these mechanisms to include sensitive information, such as passwords or
+ * secrets.
+ *
+ * For more information about using noEcho to mask sensitive information, see
+ * https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/best-practices.html#creds
+ *
+ * By default, this value is false.
+ *
+ * @param noEcho when true, masks certain output
+ * @return a reference to this builder
+ */
+ public Builder noEcho(boolean noEcho) {
+ this.noEcho = noEcho;
+ return this;
+ }
+
+ /**
+ * Builds a Response object for the value.
+ *
+ * @return a Response object wrapping the initially provided value.
+ */
+ public Response build() {
+ JsonNode node;
+ if (value == null) {
+ node = null;
+ } else {
+ ObjectMapper mapper = objectMapper != null ? objectMapper : CloudFormationResponse.ResponseBody.MAPPER;
+ node = mapper.valueToTree(value);
+ }
+ Status responseStatus = this.status != null ? this.status : Status.SUCCESS;
+ return new Response(node, responseStatus, physicalResourceId, noEcho);
+ }
+ }
}
diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/AbstractCustomResourceHandlerTest.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/AbstractCustomResourceHandlerTest.java
index d68b434d6..1e399ef6f 100644
--- a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/AbstractCustomResourceHandlerTest.java
+++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/AbstractCustomResourceHandlerTest.java
@@ -1,14 +1,18 @@
-package software.amazon.lambda.powertools.cloudformation;
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
-import com.amazonaws.services.lambda.runtime.Context;
-import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.CsvSource;
-import software.amazon.awssdk.http.SdkHttpClient;
-import software.amazon.lambda.powertools.cloudformation.Response.Status;
-
-import java.io.IOException;
+package software.amazon.lambda.powertools.cloudformation;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
@@ -21,95 +25,16 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-public class AbstractCustomResourceHandlerTest {
-
- /**
- * Bare-bones implementation that returns null for abstract methods.
- */
- static class NullCustomResourceHandler extends AbstractCustomResourceHandler {
- NullCustomResourceHandler() {
- }
-
- NullCustomResourceHandler(SdkHttpClient client) {
- super(client);
- }
-
- @Override
- protected Response create(CloudFormationCustomResourceEvent event, Context context) {
- return null;
- }
-
- @Override
- protected Response update(CloudFormationCustomResourceEvent event, Context context) {
- return null;
- }
-
- @Override
- protected Response delete(CloudFormationCustomResourceEvent event, Context context) {
- return null;
- }
- }
-
- /**
- * Uses a mocked CloudFormationResponse to avoid sending actual HTTP requests.
- */
- static class NoOpCustomResourceHandler extends NullCustomResourceHandler {
-
- NoOpCustomResourceHandler() {
- super(mock(SdkHttpClient.class));
- }
-
- @Override
- protected CloudFormationResponse buildResponseClient() {
- return mock(CloudFormationResponse.class);
- }
- }
-
- /**
- * Creates a handler that will expect the Response to be sent with an expected status. Will throw an AssertionError
- * if the method is sent with an unexpected status.
- */
- static class ExpectedStatusResourceHandler extends NoOpCustomResourceHandler {
- private final Status expectedStatus;
-
- ExpectedStatusResourceHandler(Status expectedStatus) {
- this.expectedStatus = expectedStatus;
- }
-
- @Override
- protected CloudFormationResponse buildResponseClient() {
- // create a CloudFormationResponse that fails if invoked with unexpected status
- CloudFormationResponse cfnResponse = mock(CloudFormationResponse.class);
- try {
- when(cfnResponse.send(any(), any(), argThat(resp -> resp.getStatus() != expectedStatus)))
- .thenThrow(new AssertionError("Expected response's status to be " + expectedStatus));
- } catch (IOException | CustomResourceResponseException e) {
- // this should never happen
- throw new RuntimeException("Unexpected mocking exception", e);
- }
- return cfnResponse;
- }
- }
+import com.amazonaws.services.lambda.runtime.Context;
+import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent;
+import java.io.IOException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import software.amazon.awssdk.http.SdkHttpClient;
+import software.amazon.lambda.powertools.cloudformation.Response.Status;
- /**
- * Always fails to send the response
- */
- static class FailToSendResponseHandler extends NoOpCustomResourceHandler {
- @Override
- protected CloudFormationResponse buildResponseClient() {
- CloudFormationResponse cfnResponse = mock(CloudFormationResponse.class);
- try {
- when(cfnResponse.send(any(), any()))
- .thenThrow(new IOException("Intentional send failure"));
- when(cfnResponse.send(any(), any(), any()))
- .thenThrow(new IOException("Intentional send failure"));
- } catch (IOException | CustomResourceResponseException e) {
- // this should never happen
- throw new RuntimeException("Unexpected mocking exception", e);
- }
- return cfnResponse;
- }
- }
+public class AbstractCustomResourceHandlerTest {
/**
* Builds a valid Event with the provide request type.
@@ -288,4 +213,92 @@ protected Response create(CloudFormationCustomResourceEvent event, Context conte
verify(handler, times(1))
.onSendFailure(eq(event), eq(context), isNull(), any(IOException.class));
}
+
+ /**
+ * Bare-bones implementation that returns null for abstract methods.
+ */
+ static class NullCustomResourceHandler extends AbstractCustomResourceHandler {
+ NullCustomResourceHandler() {
+ }
+
+ NullCustomResourceHandler(SdkHttpClient client) {
+ super(client);
+ }
+
+ @Override
+ protected Response create(CloudFormationCustomResourceEvent event, Context context) {
+ return null;
+ }
+
+ @Override
+ protected Response update(CloudFormationCustomResourceEvent event, Context context) {
+ return null;
+ }
+
+ @Override
+ protected Response delete(CloudFormationCustomResourceEvent event, Context context) {
+ return null;
+ }
+ }
+
+ /**
+ * Uses a mocked CloudFormationResponse to avoid sending actual HTTP requests.
+ */
+ static class NoOpCustomResourceHandler extends NullCustomResourceHandler {
+
+ NoOpCustomResourceHandler() {
+ super(mock(SdkHttpClient.class));
+ }
+
+ @Override
+ protected CloudFormationResponse buildResponseClient() {
+ return mock(CloudFormationResponse.class);
+ }
+ }
+
+ /**
+ * Creates a handler that will expect the Response to be sent with an expected status. Will throw an AssertionError
+ * if the method is sent with an unexpected status.
+ */
+ static class ExpectedStatusResourceHandler extends NoOpCustomResourceHandler {
+ private final Status expectedStatus;
+
+ ExpectedStatusResourceHandler(Status expectedStatus) {
+ this.expectedStatus = expectedStatus;
+ }
+
+ @Override
+ protected CloudFormationResponse buildResponseClient() {
+ // create a CloudFormationResponse that fails if invoked with unexpected status
+ CloudFormationResponse cfnResponse = mock(CloudFormationResponse.class);
+ try {
+ when(cfnResponse.send(any(), any(), argThat(resp -> resp.getStatus() != expectedStatus)))
+ .thenThrow(new AssertionError("Expected response's status to be " + expectedStatus));
+ } catch (IOException | CustomResourceResponseException e) {
+ // this should never happen
+ throw new RuntimeException("Unexpected mocking exception", e);
+ }
+ return cfnResponse;
+ }
+ }
+
+ /**
+ * Always fails to send the response
+ */
+ static class FailToSendResponseHandler extends NoOpCustomResourceHandler {
+ @Override
+ protected CloudFormationResponse buildResponseClient() {
+ CloudFormationResponse cfnResponse = mock(CloudFormationResponse.class);
+ try {
+ when(cfnResponse.send(any(), any()))
+ .thenThrow(new IOException("Intentional send failure"));
+ when(cfnResponse.send(any(), any(), any()))
+ .thenThrow(new IOException("Intentional send failure"));
+ } catch (IOException | CustomResourceResponseException e) {
+ // this should never happen
+ throw new RuntimeException("Unexpected mocking exception", e);
+ }
+ return cfnResponse;
+ }
+ }
}
diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationIntegrationTest.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationIntegrationTest.java
index 06463308c..ce45d3afc 100644
--- a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationIntegrationTest.java
+++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationIntegrationTest.java
@@ -1,5 +1,28 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
package software.amazon.lambda.powertools.cloudformation;
+import static com.github.tomakehurst.wiremock.client.WireMock.matchingJsonPath;
+import static com.github.tomakehurst.wiremock.client.WireMock.ok;
+import static com.github.tomakehurst.wiremock.client.WireMock.put;
+import static com.github.tomakehurst.wiremock.client.WireMock.putRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching;
+import static com.github.tomakehurst.wiremock.client.WireMock.verify;
+import static org.assertj.core.api.Assertions.assertThat;
+
import com.amazonaws.services.lambda.runtime.ClientContext;
import com.amazonaws.services.lambda.runtime.CognitoIdentity;
import com.amazonaws.services.lambda.runtime.Context;
@@ -7,6 +30,7 @@
import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent;
import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
import com.github.tomakehurst.wiremock.junit5.WireMockTest;
+import java.util.UUID;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
@@ -14,20 +38,47 @@
import software.amazon.lambda.powertools.cloudformation.handlers.PhysicalResourceIdSetHandler;
import software.amazon.lambda.powertools.cloudformation.handlers.RuntimeExceptionThrownHandler;
-import java.util.UUID;
-
-import static com.github.tomakehurst.wiremock.client.WireMock.*;
-import static org.assertj.core.api.Assertions.assertThat;
-
@WireMockTest
public class CloudFormationIntegrationTest {
public static final String PHYSICAL_RESOURCE_ID = UUID.randomUUID().toString();
public static final String LOG_STREAM_NAME = "FakeLogStreamName";
+ private static CloudFormationCustomResourceEvent updateEventWithPhysicalResourceId(int httpPort,
+ String physicalResourceId) {
+ CloudFormationCustomResourceEvent.CloudFormationCustomResourceEventBuilder builder = baseEvent(httpPort);
+
+ builder.withPhysicalResourceId(physicalResourceId);
+ builder.withRequestType("Update");
+
+ return builder.build();
+ }
+
+ private static CloudFormationCustomResourceEvent deleteEventWithPhysicalResourceId(int httpPort,
+ String physicalResourceId) {
+ CloudFormationCustomResourceEvent.CloudFormationCustomResourceEventBuilder builder = baseEvent(httpPort);
+
+ builder.withPhysicalResourceId(physicalResourceId);
+ builder.withRequestType("Delete");
+
+ return builder.build();
+ }
+
+ private static CloudFormationCustomResourceEvent.CloudFormationCustomResourceEventBuilder baseEvent(int httpPort) {
+ CloudFormationCustomResourceEvent.CloudFormationCustomResourceEventBuilder builder =
+ CloudFormationCustomResourceEvent.builder()
+ .withResponseUrl("http://localhost:" + httpPort + "/")
+ .withStackId("123")
+ .withRequestId("234")
+ .withLogicalResourceId("345");
+
+ return builder;
+ }
+
@ParameterizedTest
@ValueSource(strings = {"Update", "Delete"})
- void physicalResourceIdTakenFromRequestForUpdateOrDeleteWhenUserSpecifiesNull(String requestType, WireMockRuntimeInfo wmRuntimeInfo) {
+ void physicalResourceIdTakenFromRequestForUpdateOrDeleteWhenUserSpecifiesNull(String requestType,
+ WireMockRuntimeInfo wmRuntimeInfo) {
stubFor(put("/").willReturn(ok()));
NoPhysicalResourceIdSetHandler handler = new NoPhysicalResourceIdSetHandler();
@@ -48,7 +99,8 @@ void physicalResourceIdTakenFromRequestForUpdateOrDeleteWhenUserSpecifiesNull(St
@ParameterizedTest
@ValueSource(strings = {"Update", "Delete"})
- void physicalResourceIdDoesNotChangeWhenRuntimeExceptionThrownWhenUpdatingOrDeleting(String requestType, WireMockRuntimeInfo wmRuntimeInfo) {
+ void physicalResourceIdDoesNotChangeWhenRuntimeExceptionThrownWhenUpdatingOrDeleting(String requestType,
+ WireMockRuntimeInfo wmRuntimeInfo) {
stubFor(put("/").willReturn(ok()));
RuntimeExceptionThrownHandler handler = new RuntimeExceptionThrownHandler();
@@ -68,7 +120,7 @@ void physicalResourceIdDoesNotChangeWhenRuntimeExceptionThrownWhenUpdatingOrDele
}
@Test
- void runtimeExceptionThrownOnCreateSendsLogStreamNameAsPhysicalResourceId(WireMockRuntimeInfo wmRuntimeInfo) {
+ void runtimeExceptionThrownOnCreateSendsLogStreamNameAsPhysicalResourceId(WireMockRuntimeInfo wmRuntimeInfo) {
stubFor(put("/").willReturn(ok()));
RuntimeExceptionThrownHandler handler = new RuntimeExceptionThrownHandler();
@@ -85,7 +137,8 @@ void runtimeExceptionThrownOnCreateSendsLogStreamNameAsPhysicalResourceId(WireMo
@ParameterizedTest
@ValueSource(strings = {"Update", "Delete"})
- void physicalResourceIdSetFromRequestOnUpdateOrDeleteWhenCustomerDoesntProvideAPhysicalResourceId(String requestType, WireMockRuntimeInfo wmRuntimeInfo) {
+ void physicalResourceIdSetFromRequestOnUpdateOrDeleteWhenCustomerDoesntProvideAPhysicalResourceId(
+ String requestType, WireMockRuntimeInfo wmRuntimeInfo) {
stubFor(put("/").willReturn(ok()));
NoPhysicalResourceIdSetHandler handler = new NoPhysicalResourceIdSetHandler();
@@ -160,34 +213,6 @@ void physicalResourceIdReturnedFromFailedToCloudformation(String requestType, Wi
);
}
- private static CloudFormationCustomResourceEvent updateEventWithPhysicalResourceId(int httpPort, String physicalResourceId) {
- CloudFormationCustomResourceEvent.CloudFormationCustomResourceEventBuilder builder = baseEvent(httpPort);
-
- builder.withPhysicalResourceId(physicalResourceId);
- builder.withRequestType("Update");
-
- return builder.build();
- }
-
- private static CloudFormationCustomResourceEvent deleteEventWithPhysicalResourceId(int httpPort, String physicalResourceId) {
- CloudFormationCustomResourceEvent.CloudFormationCustomResourceEventBuilder builder = baseEvent(httpPort);
-
- builder.withPhysicalResourceId(physicalResourceId);
- builder.withRequestType("Delete");
-
- return builder.build();
- }
-
- private static CloudFormationCustomResourceEvent.CloudFormationCustomResourceEventBuilder baseEvent(int httpPort) {
- CloudFormationCustomResourceEvent.CloudFormationCustomResourceEventBuilder builder = CloudFormationCustomResourceEvent.builder()
- .withResponseUrl("http://localhost:" + httpPort + "/")
- .withStackId("123")
- .withRequestId("234")
- .withLogicalResourceId("345");
-
- return builder;
- }
-
private static class FakeContext implements Context {
@Override
public String getAwsRequestId() {
diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponseTest.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponseTest.java
index 64c313695..2e7fbcc0c 100644
--- a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponseTest.java
+++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponseTest.java
@@ -1,8 +1,33 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
package software.amazon.lambda.powertools.cloudformation;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent;
import com.fasterxml.jackson.databind.node.ObjectNode;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Optional;
import org.junit.jupiter.api.Test;
import software.amazon.awssdk.http.AbortableInputStream;
import software.amazon.awssdk.http.ExecutableHttpRequest;
@@ -13,18 +38,6 @@
import software.amazon.awssdk.utils.StringInputStream;
import software.amazon.lambda.powertools.cloudformation.CloudFormationResponse.ResponseBody;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.LinkedHashMap;
-import java.util.Map;
-import java.util.Optional;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
public class CloudFormationResponseTest {
/**
@@ -44,16 +57,17 @@ static CloudFormationResponse testableCloudFormationResponse() {
SdkHttpClient client = mock(SdkHttpClient.class);
ExecutableHttpRequest executableRequest = mock(ExecutableHttpRequest.class);
- when(client.prepareRequest(any(HttpExecuteRequest.class))).thenAnswer(args -> {
- HttpExecuteRequest request = args.getArgument(0, HttpExecuteRequest.class);
- assertThat(request.contentStreamProvider()).isPresent();
+ when(client.prepareRequest(any(HttpExecuteRequest.class))).thenAnswer(args ->
+ {
+ HttpExecuteRequest request = args.getArgument(0, HttpExecuteRequest.class);
+ assertThat(request.contentStreamProvider()).isPresent();
- InputStream inputStream = request.contentStreamProvider().get().newStream();
- HttpExecuteResponse response = mock(HttpExecuteResponse.class);
- when(response.responseBody()).thenReturn(Optional.of(AbortableInputStream.create(inputStream)));
- when(executableRequest.call()).thenReturn(response);
- return executableRequest;
- });
+ InputStream inputStream = request.contentStreamProvider().get().newStream();
+ HttpExecuteResponse response = mock(HttpExecuteResponse.class);
+ when(response.responseBody()).thenReturn(Optional.of(AbortableInputStream.create(inputStream)));
+ when(executableRequest.call()).thenReturn(response);
+ return executableRequest;
+ });
return new CloudFormationResponse(client);
}
@@ -113,7 +127,8 @@ void customPhysicalResponseId() {
String customPhysicalResourceId = "Custom-Physical-Resource-ID";
ResponseBody body = new ResponseBody(
- event, Response.Status.SUCCESS, customPhysicalResourceId, false, "See the details in CloudWatch Log Stream: " + context.getLogStreamName());
+ event, Response.Status.SUCCESS, customPhysicalResourceId, false,
+ "See the details in CloudWatch Log Stream: " + context.getLogStreamName());
assertThat(body.getPhysicalResourceId()).isEqualTo(customPhysicalResourceId);
}
@@ -122,7 +137,8 @@ void responseBodyWithNullDataNode() {
CloudFormationCustomResourceEvent event = mockCloudFormationCustomResourceEvent();
Context context = mock(Context.class);
- ResponseBody responseBody = new ResponseBody(event, Response.Status.FAILED, null, true, "See the details in CloudWatch Log Stream: " + context.getLogStreamName());
+ ResponseBody responseBody = new ResponseBody(event, Response.Status.FAILED, null, true,
+ "See the details in CloudWatch Log Stream: " + context.getLogStreamName());
String actualJson = responseBody.toObjectNode(null).toString();
String expectedJson = "{" +
@@ -146,7 +162,8 @@ void responseBodyWithNonNullDataNode() {
dataNode.put("foo", "bar");
dataNode.put("baz", 10);
- ResponseBody responseBody = new ResponseBody(event, Response.Status.FAILED, null, true, "See the details in CloudWatch Log Stream: " + context.getLogStreamName());
+ ResponseBody responseBody = new ResponseBody(event, Response.Status.FAILED, null, true,
+ "See the details in CloudWatch Log Stream: " + context.getLogStreamName());
String actualJson = responseBody.toObjectNode(dataNode).toString();
String expectedJson = "{" +
@@ -178,7 +195,8 @@ void customStatus() {
Context context = mock(Context.class);
ResponseBody body = new ResponseBody(
- event, Response.Status.FAILED, null, false, "See the details in CloudWatch Log Stream: " + context.getLogStreamName());
+ event, Response.Status.FAILED, null, false,
+ "See the details in CloudWatch Log Stream: " + context.getLogStreamName());
assertThat(body.getStatus()).isEqualTo("FAILED");
}
@@ -191,7 +209,8 @@ void reasonIncludesLogStreamName() {
when(context.getLogStreamName()).thenReturn(logStreamName);
ResponseBody body = new ResponseBody(
- event, Response.Status.SUCCESS, null, false, "See the details in CloudWatch Log Stream: " + context.getLogStreamName());
+ event, Response.Status.SUCCESS, null, false,
+ "See the details in CloudWatch Log Stream: " + context.getLogStreamName());
assertThat(body.getReason()).contains(logStreamName);
}
diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/ResponseTest.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/ResponseTest.java
index e97a1a5ba..37fe73d0f 100644
--- a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/ResponseTest.java
+++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/ResponseTest.java
@@ -1,29 +1,29 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
package software.amazon.lambda.powertools.cloudformation;
+import static org.assertj.core.api.Assertions.assertThat;
+
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
-import org.junit.jupiter.api.Test;
-
import java.util.HashMap;
import java.util.Map;
-
-import static org.assertj.core.api.Assertions.assertThat;
+import org.junit.jupiter.api.Test;
public class ResponseTest {
- static class DummyBean {
- private final Object propertyWithLongName;
-
- DummyBean(Object propertyWithLongName) {
- this.propertyWithLongName = propertyWithLongName;
- }
-
- @SuppressWarnings("unused")
- public Object getPropertyWithLongName() {
- return propertyWithLongName;
- }
- }
-
@Test
void defaultValues() {
Response response = Response.builder().build();
@@ -173,4 +173,17 @@ void failedFactoryMethod() {
assertThat(response).isNotNull();
assertThat(response.getStatus()).isEqualTo(Response.Status.FAILED);
}
+
+ static class DummyBean {
+ private final Object propertyWithLongName;
+
+ DummyBean(Object propertyWithLongName) {
+ this.propertyWithLongName = propertyWithLongName;
+ }
+
+ @SuppressWarnings("unused")
+ public Object getPropertyWithLongName() {
+ return propertyWithLongName;
+ }
+ }
}
diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/NoPhysicalResourceIdSetHandler.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/NoPhysicalResourceIdSetHandler.java
index 68d057b54..2bbda309f 100644
--- a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/NoPhysicalResourceIdSetHandler.java
+++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/NoPhysicalResourceIdSetHandler.java
@@ -1,3 +1,17 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
package software.amazon.lambda.powertools.cloudformation.handlers;
import com.amazonaws.services.lambda.runtime.Context;
diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/PhysicalResourceIdSetHandler.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/PhysicalResourceIdSetHandler.java
index 51f520a3d..c6bd56b76 100644
--- a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/PhysicalResourceIdSetHandler.java
+++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/PhysicalResourceIdSetHandler.java
@@ -1,3 +1,17 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
package software.amazon.lambda.powertools.cloudformation.handlers;
import com.amazonaws.services.lambda.runtime.Context;
@@ -17,16 +31,16 @@ public PhysicalResourceIdSetHandler(String physicalResourceId, boolean callsSucc
@Override
protected Response create(CloudFormationCustomResourceEvent event, Context context) {
- return callsSucceed? Response.success(physicalResourceId) : Response.failed(physicalResourceId);
+ return callsSucceed ? Response.success(physicalResourceId) : Response.failed(physicalResourceId);
}
@Override
protected Response update(CloudFormationCustomResourceEvent event, Context context) {
- return callsSucceed? Response.success(physicalResourceId) : Response.failed(physicalResourceId);
+ return callsSucceed ? Response.success(physicalResourceId) : Response.failed(physicalResourceId);
}
@Override
protected Response delete(CloudFormationCustomResourceEvent event, Context context) {
- return callsSucceed? Response.success(physicalResourceId) : Response.failed(physicalResourceId);
+ return callsSucceed ? Response.success(physicalResourceId) : Response.failed(physicalResourceId);
}
}
diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/RuntimeExceptionThrownHandler.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/RuntimeExceptionThrownHandler.java
index ee5be77b8..d5a11e895 100644
--- a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/RuntimeExceptionThrownHandler.java
+++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/RuntimeExceptionThrownHandler.java
@@ -1,3 +1,17 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
package software.amazon.lambda.powertools.cloudformation.handlers;
import com.amazonaws.services.lambda.runtime.Context;
diff --git a/powertools-core/pom.xml b/powertools-core/pom.xml
index d151c7b3c..c244f70fc 100644
--- a/powertools-core/pom.xml
+++ b/powertools-core/pom.xml
@@ -1,4 +1,18 @@
+
+
@@ -50,6 +64,11 @@
org.aspectjaspectjrt
+
+ org.apache.logging.log4j
+ log4j-slf4j2-impl
+ ${log4j.version}
+
@@ -82,6 +101,26 @@
assertj-coretest
+
+ org.mockito
+ mockito-inline
+ test
+
+
+
+
+ src/main/resources-filtered
+ true
+
+
+
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+
+
+
+
\ No newline at end of file
diff --git a/powertools-core/src/main/java/software/amazon/lambda/powertools/core/internal/LambdaConstants.java b/powertools-core/src/main/java/software/amazon/lambda/powertools/core/internal/LambdaConstants.java
index fe30b4928..d0f94260b 100644
--- a/powertools-core/src/main/java/software/amazon/lambda/powertools/core/internal/LambdaConstants.java
+++ b/powertools-core/src/main/java/software/amazon/lambda/powertools/core/internal/LambdaConstants.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 Amazon.com, Inc. or its affiliates.
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
@@ -11,11 +11,21 @@
* limitations under the License.
*
*/
+
package software.amazon.lambda.powertools.core.internal;
public class LambdaConstants {
public static final String LAMBDA_FUNCTION_NAME_ENV = "AWS_LAMBDA_FUNCTION_NAME";
public static final String AWS_REGION_ENV = "AWS_REGION";
+ // Also you can use AWS_LAMBDA_INITIALIZATION_TYPE to distinguish between on-demand and SnapStart initialization
+ // it's not recommended to use this env variable to initialize SDK clients or other resources.
+ @Deprecated
public static final String AWS_LAMBDA_INITIALIZATION_TYPE = "AWS_LAMBDA_INITIALIZATION_TYPE";
+ @Deprecated
public static final String ON_DEMAND = "on-demand";
+ public static final String X_AMZN_TRACE_ID = "_X_AMZN_TRACE_ID";
+ public static final String AWS_SAM_LOCAL = "AWS_SAM_LOCAL";
+ public static final String ROOT_EQUALS = "Root=";
+ public static final String POWERTOOLS_SERVICE_NAME = "POWERTOOLS_SERVICE_NAME";
+ public static final String SERVICE_UNDEFINED = "service_undefined";
}
diff --git a/powertools-core/src/main/java/software/amazon/lambda/powertools/core/internal/LambdaHandlerProcessor.java b/powertools-core/src/main/java/software/amazon/lambda/powertools/core/internal/LambdaHandlerProcessor.java
index 1cff812b8..e9e220e41 100644
--- a/powertools-core/src/main/java/software/amazon/lambda/powertools/core/internal/LambdaHandlerProcessor.java
+++ b/powertools-core/src/main/java/software/amazon/lambda/powertools/core/internal/LambdaHandlerProcessor.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 Amazon.com, Inc. or its affiliates.
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
@@ -11,31 +11,37 @@
* limitations under the License.
*
*/
+
package software.amazon.lambda.powertools.core.internal;
+import static java.util.Optional.empty;
+import static java.util.Optional.of;
+import static software.amazon.lambda.powertools.core.internal.SystemWrapper.getenv;
+
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.RequestStreamHandler;
-import org.aspectj.lang.ProceedingJoinPoint;
-
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Optional;
-
-import static java.util.Optional.empty;
-import static java.util.Optional.of;
-import static software.amazon.lambda.powertools.core.internal.SystemWrapper.getenv;
+import org.aspectj.lang.ProceedingJoinPoint;
public final class LambdaHandlerProcessor {
+
// SERVICE_NAME cannot be final for testing purposes
- private static String SERVICE_NAME = null != System.getenv("POWERTOOLS_SERVICE_NAME")
- ? System.getenv("POWERTOOLS_SERVICE_NAME") : "service_undefined";
+ private static String SERVICE_NAME = calculateServiceName();
+
private static Boolean IS_COLD_START = null;
private LambdaHandlerProcessor() {
// Hide default constructor
}
+ private static String calculateServiceName() {
+ return null != getenv(LambdaConstants.POWERTOOLS_SERVICE_NAME)
+ ? getenv(LambdaConstants.POWERTOOLS_SERVICE_NAME) : LambdaConstants.SERVICE_UNDEFINED;
+ }
+
public static boolean isHandlerMethod(final ProceedingJoinPoint pjp) {
return placedOnRequestHandler(pjp) || placedOnStreamHandler(pjp);
}
@@ -56,23 +62,24 @@ public static boolean placedOnStreamHandler(final ProceedingJoinPoint pjp) {
public static Context extractContext(final ProceedingJoinPoint pjp) {
- if (isHandlerMethod(pjp)) {
- if (placedOnRequestHandler(pjp)) {
- return (Context) pjp.getArgs()[1];
- }
-
- if (placedOnStreamHandler(pjp)) {
- return (Context) pjp.getArgs()[2];
- }
+ if (placedOnRequestHandler(pjp)) {
+ return (Context) pjp.getArgs()[1];
+ } else if (placedOnStreamHandler(pjp)) {
+ return (Context) pjp.getArgs()[2];
+ } else {
+ return null;
}
-
- return null;
}
public static String serviceName() {
return SERVICE_NAME;
}
+ // Method used for testing purposes
+ protected static void resetServiceName() {
+ SERVICE_NAME = calculateServiceName();
+ }
+
public static boolean isColdStart() {
return IS_COLD_START == null;
}
@@ -82,13 +89,13 @@ public static void coldStartDone() {
}
public static boolean isSamLocal() {
- return "true".equals(System.getenv("AWS_SAM_LOCAL"));
+ return "true".equals(getenv(LambdaConstants.AWS_SAM_LOCAL));
}
public static Optional getXrayTraceId() {
- final String X_AMZN_TRACE_ID = getenv("_X_AMZN_TRACE_ID");
- if(X_AMZN_TRACE_ID != null) {
- return of(X_AMZN_TRACE_ID.split(";")[0].replace("Root=", ""));
+ final String X_AMZN_TRACE_ID = getenv(LambdaConstants.X_AMZN_TRACE_ID);
+ if (X_AMZN_TRACE_ID != null) {
+ return of(X_AMZN_TRACE_ID.split(";")[0].replace(LambdaConstants.ROOT_EQUALS, ""));
}
return empty();
}
diff --git a/powertools-core/src/main/java/software/amazon/lambda/powertools/core/internal/SystemWrapper.java b/powertools-core/src/main/java/software/amazon/lambda/powertools/core/internal/SystemWrapper.java
index aef64378f..30f72232f 100644
--- a/powertools-core/src/main/java/software/amazon/lambda/powertools/core/internal/SystemWrapper.java
+++ b/powertools-core/src/main/java/software/amazon/lambda/powertools/core/internal/SystemWrapper.java
@@ -1,3 +1,17 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
package software.amazon.lambda.powertools.core.internal;
public class SystemWrapper {
diff --git a/powertools-core/src/main/java/software/amazon/lambda/powertools/core/internal/UserAgentConfigurator.java b/powertools-core/src/main/java/software/amazon/lambda/powertools/core/internal/UserAgentConfigurator.java
new file mode 100644
index 000000000..354305d33
--- /dev/null
+++ b/powertools-core/src/main/java/software/amazon/lambda/powertools/core/internal/UserAgentConfigurator.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package software.amazon.lambda.powertools.core.internal;
+
+import static software.amazon.lambda.powertools.core.internal.SystemWrapper.getenv;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.net.URL;
+import java.util.Properties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * Can be used to create a string that can server as a User-Agent suffix in requests made with the AWS SDK clients
+ */
+public class UserAgentConfigurator {
+
+ public static final String NA = "NA";
+ public static final String VERSION_KEY = "powertools.version";
+ public static final String PT_FEATURE_VARIABLE = "${PT_FEATURE}";
+ public static final String PT_EXEC_ENV_VARIABLE = "${PT_EXEC_ENV}";
+ public static final String VERSION_PROPERTIES_FILENAME = "version.properties";
+ public static final String AWS_EXECUTION_ENV = "AWS_EXECUTION_ENV";
+ private static final Logger LOG = LoggerFactory.getLogger(UserAgentConfigurator.class);
+ private static final String NO_OP = "no-op";
+ private static String ptVersion = getProjectVersion();
+ private static String userAgentPattern = "PT/" + PT_FEATURE_VARIABLE + "/" + ptVersion + " PTEnv/"
+ + PT_EXEC_ENV_VARIABLE;
+
+ private UserAgentConfigurator() {
+ throw new IllegalStateException("Utility class. Not meant to be instantiated");
+ }
+
+ /**
+ * Retrieves the project version from the version.properties file
+ *
+ * @return the project version
+ */
+ static String getProjectVersion() {
+ return getVersionFromProperties(VERSION_PROPERTIES_FILENAME, VERSION_KEY);
+ }
+
+
+ /**
+ * Retrieves the project version from a properties file.
+ * The file should be in the resources folder.
+ * The version is retrieved from the property with the given key.
+ *
+ * @param propertyFileName the name of the properties file
+ * @param versionKey the key of the property that contains the version
+ * @return the version of the project as configured in the given properties file
+ */
+ static String getVersionFromProperties(String propertyFileName, String versionKey) {
+
+ URL propertiesFileURI = Thread.currentThread().getContextClassLoader().getResource(propertyFileName);
+ if (propertiesFileURI != null) {
+ try (FileInputStream fis = new FileInputStream(propertiesFileURI.getPath())) {
+ Properties properties = new Properties();
+ properties.load(fis);
+ String version = properties.getProperty(versionKey);
+ if (version != null && !version.isEmpty()) {
+ return version;
+ }
+ } catch (IOException e) {
+ LOG.warn("Unable to read {} file. Using default version.", propertyFileName);
+ LOG.debug("Exception:", e);
+ }
+ }
+ return NA;
+ }
+
+ /**
+ * Retrieves the user agent string for the Powertools for AWS Lambda.
+ * It follows the pattern PT/{PT_FEATURE}/{PT_VERSION} PTEnv/{PT_EXEC_ENV}
+ * The version of the project is automatically retrieved.
+ * The PT_EXEC_ENV is automatically retrieved from the AWS_EXECUTION_ENV environment variable.
+ * If it AWS_EXECUTION_ENV is not set, PT_EXEC_ENV defaults to "NA"
+ *
+ * @param ptFeature a custom feature to be added to the user agent string (e.g. idempotency).
+ * If null or empty, the default PT_FEATURE is used.
+ * The default PT_FEATURE is "no-op".
+ * @return the user agent string
+ */
+ public static String getUserAgent(String ptFeature) {
+
+ String awsExecutionEnv = getenv(AWS_EXECUTION_ENV);
+ String ptExecEnv = awsExecutionEnv != null ? awsExecutionEnv : NA;
+ String userAgent = userAgentPattern.replace(PT_EXEC_ENV_VARIABLE, ptExecEnv);
+
+ if (ptFeature == null || ptFeature.isEmpty()) {
+ ptFeature = NO_OP;
+ }
+ return userAgent
+ .replace(PT_FEATURE_VARIABLE, ptFeature)
+ .replace(PT_EXEC_ENV_VARIABLE, ptExecEnv);
+ }
+}
diff --git a/powertools-core/src/main/resources-filtered/version.properties b/powertools-core/src/main/resources-filtered/version.properties
new file mode 100644
index 000000000..5e95fb588
--- /dev/null
+++ b/powertools-core/src/main/resources-filtered/version.properties
@@ -0,0 +1,9 @@
+# The filtered properties can have variables that are filled in by system properties or project properties.
+# See https://maven.apache.org/plugins/maven-resources-plugin/examples/filter.html
+#
+# The values are replaced before copying the resources to the main output directory. Therefore, as soon as the build phase is completed,
+# the values should have been replaced if the properties are available and if 'filtering' is set to true in the pom.xml
+#
+#
+# The ${project.version} is retrieved from the respective pom.xml property
+powertools.version=${project.version}
\ No newline at end of file
diff --git a/powertools-core/src/test/java/software/amazon/lambda/powertools/core/internal/LambdaHandlerProcessorTest.java b/powertools-core/src/test/java/software/amazon/lambda/powertools/core/internal/LambdaHandlerProcessorTest.java
index 6ed8b4160..dc8f49580 100644
--- a/powertools-core/src/test/java/software/amazon/lambda/powertools/core/internal/LambdaHandlerProcessorTest.java
+++ b/powertools-core/src/test/java/software/amazon/lambda/powertools/core/internal/LambdaHandlerProcessorTest.java
@@ -1,65 +1,250 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
package software.amazon.lambda.powertools.core.internal;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.when;
+import static software.amazon.lambda.powertools.core.internal.SystemWrapper.getenv;
+
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.RequestStreamHandler;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Optional;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.junit.jupiter.api.Test;
-
-import java.io.InputStream;
-import java.io.OutputStream;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
+import org.mockito.MockedStatic;
class LambdaHandlerProcessorTest {
+ private Signature signature = mock(Signature.class);
+ private ProceedingJoinPoint pjpMock = mock(ProceedingJoinPoint.class);
+
@Test
void isHandlerMethod_shouldRecognizeRequestHandler() {
- ProceedingJoinPoint pjpMock = mockRequestHandlerPjp();
+ Object[] args = {new Object(), mock(Context.class)};
+ ProceedingJoinPoint pjpMock = mockRequestHandlerPjp(RequestHandler.class, args);
assertThat(LambdaHandlerProcessor.isHandlerMethod(pjpMock)).isTrue();
}
@Test
void isHandlerMethod_shouldRecognizeRequestStreamHandler() {
- ProceedingJoinPoint pjpMock = mockRequestStreamHandlerPjp();
+ Object[] args = {mock(InputStream.class), mock(OutputStream.class), mock(Context.class)};
+ ProceedingJoinPoint pjpMock = mockRequestHandlerPjp(RequestStreamHandler.class, args);
assertThat(LambdaHandlerProcessor.isHandlerMethod(pjpMock)).isTrue();
}
+ @Test
+ void isHandlerMethod_shouldReturnFalse() {
+ ProceedingJoinPoint pjpMock = mockRequestHandlerPjp(Object.class, new Object[] {});
+
+ boolean isHandlerMethod = LambdaHandlerProcessor.isHandlerMethod(pjpMock);
+
+ assertThat(isHandlerMethod).isFalse();
+ }
+
@Test
void placedOnRequestHandler_shouldRecognizeRequestHandler() {
- ProceedingJoinPoint pjpMock = mockRequestHandlerPjp();
+ Object[] args = {new Object(), mock(Context.class)};
+ ProceedingJoinPoint pjpMock = mockRequestHandlerPjp(RequestHandler.class, args);
assertThat(LambdaHandlerProcessor.placedOnRequestHandler(pjpMock)).isTrue();
}
@Test
void placedOnStreamHandler_shouldRecognizeRequestStreamHandler() {
- ProceedingJoinPoint pjpMock = mockRequestStreamHandlerPjp();
+ Object[] args = {mock(InputStream.class), mock(OutputStream.class), mock(Context.class)};
+ ProceedingJoinPoint pjpMock = mockRequestHandlerPjp(RequestStreamHandler.class, args);
assertThat(LambdaHandlerProcessor.placedOnStreamHandler(pjpMock)).isTrue();
}
- private static ProceedingJoinPoint mockRequestHandlerPjp() {
- Signature signature = mock(Signature.class);
- when(signature.getDeclaringType()).thenReturn(RequestHandler.class);
- ProceedingJoinPoint pjpMock = mock(ProceedingJoinPoint.class);
+ @Test
+ void placedOnRequestHandler_shouldInvalidateOnWrongNoOfArgs() {
+ Object[] args = {new Object()};
+ ProceedingJoinPoint pjpMock = mockRequestHandlerPjp(RequestHandler.class, args);
+
+ boolean isPlacedOnRequestHandler = LambdaHandlerProcessor.placedOnRequestHandler(pjpMock);
+
+ assertThat(isPlacedOnRequestHandler).isFalse();
+ }
+
+ @Test
+ void placedOnRequestHandler_shouldInvalidateOnWrongTypeOfArgs() {
+ Object[] args = {new Object(), new Object()};
+ ProceedingJoinPoint pjpMock = mockRequestHandlerPjp(RequestHandler.class, args);
+
+ boolean isPlacedOnRequestHandler = LambdaHandlerProcessor.placedOnRequestHandler(pjpMock);
+
+ assertThat(isPlacedOnRequestHandler).isFalse();
+ }
+
+ @Test
+ void placedOnStreamHandler_shouldInvalidateOnWrongNoOfArgs() {
+ Object[] args = {new Object()};
+ ProceedingJoinPoint pjpMock = mockRequestHandlerPjp(RequestStreamHandler.class, args);
+
+ boolean isPlacedOnStreamHandler = LambdaHandlerProcessor.placedOnStreamHandler(pjpMock);
+
+ assertThat(isPlacedOnStreamHandler).isFalse();
+ }
+
+ @Test
+ void placedOnStreamHandler_shouldInvalidateOnWrongTypeOfArgs() {
+ Object[] args = {new Object(), new Object(), new Object()};
+ ProceedingJoinPoint pjpMock = mockRequestHandlerPjp(RequestStreamHandler.class, args);
+
+ boolean isPlacedOnStreamHandler = LambdaHandlerProcessor.placedOnStreamHandler(pjpMock);
+
+ assertThat(isPlacedOnStreamHandler).isFalse();
+ }
+
+ @Test
+ void placedOnStreamHandler_shouldInvalidateOnTypeOfArgs_invalidOutputStreamArg() {
+ Object[] args = {mock(InputStream.class), new Object(), mock(Context.class)};
+ ProceedingJoinPoint pjpMock = mockRequestHandlerPjp(RequestStreamHandler.class, args);
+
+ boolean isPlacedOnStreamHandler = LambdaHandlerProcessor.placedOnStreamHandler(pjpMock);
+
+ assertThat(isPlacedOnStreamHandler).isFalse();
+ }
+
+ @Test
+ void placedOnStreamHandler_shouldInvalidateOnTypeOfArgs_invalidContextArg() {
+ Object[] args = {mock(InputStream.class), mock(OutputStream.class), new Object()};
+ ProceedingJoinPoint pjpMock = mockRequestHandlerPjp(RequestStreamHandler.class, args);
+
+ boolean isPlacedOnStreamHandler = LambdaHandlerProcessor.placedOnStreamHandler(pjpMock);
+
+ assertThat(isPlacedOnStreamHandler).isFalse();
+ }
+
+ @Test
+ void getXrayTraceId_present() {
+ String traceID = "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1\"";
+ try (MockedStatic mockedSystemWrapper = mockStatic(SystemWrapper.class)) {
+ mockedSystemWrapper.when(() -> getenv(LambdaConstants.X_AMZN_TRACE_ID)).thenReturn(traceID);
+
+ Optional xRayTraceId = LambdaHandlerProcessor.getXrayTraceId();
+
+ assertThat(xRayTraceId.isPresent()).isTrue();
+ assertThat(traceID.split(";")[0].replace(LambdaConstants.ROOT_EQUALS, "")).isEqualTo(xRayTraceId.get());
+ }
+ }
+
+ @Test
+ void getXrayTraceId_notPresent() {
+ try (MockedStatic mockedSystemWrapper = mockStatic(SystemWrapper.class)) {
+ mockedSystemWrapper.when(() -> getenv(LambdaConstants.X_AMZN_TRACE_ID)).thenReturn(null);
+
+ boolean isXRayTraceIdPresent = LambdaHandlerProcessor.getXrayTraceId().isPresent();
+
+ assertThat(isXRayTraceIdPresent).isFalse();
+ }
+ }
+
+ @Test
+ void extractContext_fromRequestHandler() {
Object[] args = {new Object(), mock(Context.class)};
- when(pjpMock.getArgs()).thenReturn(args);
- when(pjpMock.getSignature()).thenReturn(signature);
- return pjpMock;
+ ProceedingJoinPoint pjpMock = mockRequestHandlerPjp(RequestHandler.class, args);
+
+ Context context = LambdaHandlerProcessor.extractContext(pjpMock);
+
+ assertThat(context).isNotNull();
}
- private static ProceedingJoinPoint mockRequestStreamHandlerPjp() {
- Signature signature = mock(Signature.class);
- when(signature.getDeclaringType()).thenReturn(RequestStreamHandler.class);
- ProceedingJoinPoint pjpMock = mock(ProceedingJoinPoint.class);
+ @Test
+ void extractContext_fromStreamRequestHandler() {
Object[] args = {mock(InputStream.class), mock(OutputStream.class), mock(Context.class)};
- when(pjpMock.getArgs()).thenReturn(args);
+ ProceedingJoinPoint pjpMock = mockRequestHandlerPjp(RequestStreamHandler.class, args);
+
+ Context context = LambdaHandlerProcessor.extractContext(pjpMock);
+
+ assertNotNull(context);
+ }
+
+ @Test
+ void extractContext_notKnownHandler() {
+ Object[] args = {new Object()};
+ ProceedingJoinPoint pjpMock = mockRequestHandlerPjp(Object.class, args);
+
+ Context context = LambdaHandlerProcessor.extractContext(pjpMock);
+
+ assertThat(context).isNull();
+ }
+
+ @Test
+ void isColdStart() {
+ boolean isColdStart = LambdaHandlerProcessor.isColdStart();
+
+ assertThat(isColdStart).isTrue();
+ }
+
+ @Test
+ void isColdStart_coldStartDone() {
+ LambdaHandlerProcessor.coldStartDone();
+
+ boolean isColdStart = LambdaHandlerProcessor.isColdStart();
+
+ assertThat(isColdStart).isFalse();
+ }
+
+ @Test
+ void isSamLocal() {
+ try (MockedStatic mockedSystemWrapper = mockStatic(SystemWrapper.class)) {
+ mockedSystemWrapper.when(() -> getenv(LambdaConstants.AWS_SAM_LOCAL)).thenReturn("true");
+
+ boolean isSamLocal = LambdaHandlerProcessor.isSamLocal();
+
+ assertThat(isSamLocal).isTrue();
+ }
+ }
+
+ @Test
+ void serviceName() {
+ try (MockedStatic mockedSystemWrapper = mockStatic(SystemWrapper.class)) {
+ String expectedServiceName = "MyService";
+ mockedSystemWrapper.when(() -> getenv(LambdaConstants.POWERTOOLS_SERVICE_NAME))
+ .thenReturn(expectedServiceName);
+
+ String actualServiceName = LambdaHandlerProcessor.serviceName();
+
+ assertThat(actualServiceName).isEqualTo(expectedServiceName);
+ }
+ }
+
+ @Test
+ void serviceName_Undefined() {
+ LambdaHandlerProcessor.resetServiceName();
+ try (MockedStatic mockedSystemWrapper = mockStatic(SystemWrapper.class)) {
+ mockedSystemWrapper.when(() -> getenv(LambdaConstants.POWERTOOLS_SERVICE_NAME)).thenReturn(null);
+
+ assertThat(LambdaHandlerProcessor.serviceName()).isEqualTo(LambdaConstants.SERVICE_UNDEFINED);
+ }
+ }
+
+ private ProceedingJoinPoint mockRequestHandlerPjp(Class handlerClass, Object[] handlerArgs) {
+ when(signature.getDeclaringType()).thenReturn(handlerClass);
+ when(pjpMock.getArgs()).thenReturn(handlerArgs);
when(pjpMock.getSignature()).thenReturn(signature);
return pjpMock;
}
diff --git a/powertools-core/src/test/java/software/amazon/lambda/powertools/core/internal/UserAgentConfiguratorTest.java b/powertools-core/src/test/java/software/amazon/lambda/powertools/core/internal/UserAgentConfiguratorTest.java
new file mode 100644
index 000000000..00110077f
--- /dev/null
+++ b/powertools-core/src/test/java/software/amazon/lambda/powertools/core/internal/UserAgentConfiguratorTest.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package software.amazon.lambda.powertools.core.internal;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mockStatic;
+import static software.amazon.lambda.powertools.core.internal.SystemWrapper.getenv;
+import static software.amazon.lambda.powertools.core.internal.UserAgentConfigurator.AWS_EXECUTION_ENV;
+import static software.amazon.lambda.powertools.core.internal.UserAgentConfigurator.VERSION_KEY;
+import static software.amazon.lambda.powertools.core.internal.UserAgentConfigurator.VERSION_PROPERTIES_FILENAME;
+import static software.amazon.lambda.powertools.core.internal.UserAgentConfigurator.getVersionFromProperties;
+
+import java.io.File;
+import java.util.Objects;
+import java.util.regex.Pattern;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+
+class UserAgentConfiguratorTest {
+
+ private static final String SEM_VER_PATTERN = "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$";
+ private static final String VERSION = UserAgentConfigurator.getProjectVersion();
+
+
+ @Test
+ void testGetVersion() {
+
+ assertThat(VERSION)
+ .isNotNull()
+ .isNotEmpty();
+ assertThat(Pattern.matches(SEM_VER_PATTERN, VERSION)).isTrue();
+ }
+
+ @Test
+ void testGetVersionFromProperties_WrongKey() {
+ String version = getVersionFromProperties(VERSION_PROPERTIES_FILENAME, "some invalid key");
+
+ assertThat(version)
+ .isNotNull()
+ .isEqualTo("NA");
+ }
+
+ @Test
+ void testGetVersionFromProperties_FileNotExist() {
+ String version = getVersionFromProperties("some file", VERSION_KEY);
+
+ assertThat(version)
+ .isNotNull()
+ .isEqualTo("NA");
+ }
+
+ @Test
+ void testGetVersionFromProperties_InvalidFile() {
+ File f = new File(Objects.requireNonNull(Thread.currentThread().getContextClassLoader()
+ .getResource("unreadable.properties")).getPath());
+ f.setReadable(false);
+
+ String version = getVersionFromProperties("unreadable.properties", VERSION_KEY);
+
+ assertThat(version).isEqualTo("NA");
+ }
+
+ @Test
+ void testGetVersionFromProperties_EmptyVersion() {
+ String version = getVersionFromProperties("test.properties", VERSION_KEY);
+
+ assertThat(version).isEqualTo("NA");
+ }
+
+ @Test
+ void testGetUserAgent() {
+ String userAgent = UserAgentConfigurator.getUserAgent("test-feature");
+
+ assertThat(userAgent)
+ .isNotNull()
+ .isEqualTo("PT/test-feature/" + VERSION + " PTEnv/NA");
+
+ }
+
+ @Test
+ void testGetUserAgent_NoFeature() {
+ String userAgent = UserAgentConfigurator.getUserAgent("");
+
+ assertThat(userAgent)
+ .isNotNull()
+ .isEqualTo("PT/no-op/" + VERSION + " PTEnv/NA");
+ }
+
+ @Test
+ void testGetUserAgent_NullFeature() {
+ String userAgent = UserAgentConfigurator.getUserAgent(null);
+
+ assertThat(userAgent)
+ .isNotNull()
+ .isEqualTo("PT/no-op/" + VERSION + " PTEnv/NA");
+ }
+
+ @Test
+ void testGetUserAgent_SetAWSExecutionEnv() {
+ try (MockedStatic mockedSystemWrapper = mockStatic(SystemWrapper.class)) {
+ mockedSystemWrapper.when(() -> getenv(AWS_EXECUTION_ENV)).thenReturn("AWS_Lambda_java8");
+ String userAgent = UserAgentConfigurator.getUserAgent("test-feature");
+
+ assertThat(userAgent)
+ .isNotNull()
+ .isEqualTo("PT/test-feature/" + VERSION + " PTEnv/AWS_Lambda_java8");
+ }
+ }
+
+}
diff --git a/powertools-core/src/test/resources/test.properties b/powertools-core/src/test/resources/test.properties
new file mode 100644
index 000000000..65756b8dd
--- /dev/null
+++ b/powertools-core/src/test/resources/test.properties
@@ -0,0 +1 @@
+powertools.version=
\ No newline at end of file
diff --git a/powertools-core/src/test/resources/unreadable.properties b/powertools-core/src/test/resources/unreadable.properties
new file mode 100644
index 000000000..42ff4693f
--- /dev/null
+++ b/powertools-core/src/test/resources/unreadable.properties
@@ -0,0 +1,2 @@
+# This is intentionally left empty
+# It used during testing and is set to un-readable to fulfil the test purposes.
\ No newline at end of file
diff --git a/powertools-e2e-tests/handlers/idempotency/pom.xml b/powertools-e2e-tests/handlers/idempotency/pom.xml
index 4e24c738c..25dfbfabf 100644
--- a/powertools-e2e-tests/handlers/idempotency/pom.xml
+++ b/powertools-e2e-tests/handlers/idempotency/pom.xml
@@ -17,6 +17,14 @@
software.amazon.lambdapowertools-idempotency
+
+ software.amazon.lambda
+ powertools-logging
+
+
+ org.apache.logging.log4j
+ log4j-slf4j2-impl
+ com.amazonawsaws-lambda-java-events
@@ -38,6 +46,10 @@
software.amazon.lambdapowertools-idempotency
+
+ software.amazon.lambda
+ powertools-logging
+
diff --git a/powertools-e2e-tests/handlers/idempotency/src/main/java/software/amazon/lambda/powertools/e2e/Function.java b/powertools-e2e-tests/handlers/idempotency/src/main/java/software/amazon/lambda/powertools/e2e/Function.java
index cc6eec4fa..e4c2f2b9a 100644
--- a/powertools-e2e-tests/handlers/idempotency/src/main/java/software/amazon/lambda/powertools/e2e/Function.java
+++ b/powertools-e2e-tests/handlers/idempotency/src/main/java/software/amazon/lambda/powertools/e2e/Function.java
@@ -1,7 +1,26 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
package software.amazon.lambda.powertools.e2e;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
+import java.util.TimeZone;
import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
@@ -9,12 +28,7 @@
import software.amazon.lambda.powertools.idempotency.IdempotencyConfig;
import software.amazon.lambda.powertools.idempotency.Idempotent;
import software.amazon.lambda.powertools.idempotency.persistence.DynamoDBPersistenceStore;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.time.format.DateTimeFormatter;
-import java.time.temporal.ChronoUnit;
-import java.util.TimeZone;
+import software.amazon.lambda.powertools.logging.Logging;
public class Function implements RequestHandler {
@@ -40,9 +54,10 @@ public Function(DynamoDbClient client) {
).configure();
}
+ @Logging(logEvent = true)
@Idempotent
public String handleRequest(Input input, Context context) {
- DateTimeFormatter dtf = DateTimeFormatter.ISO_DATE_TIME.withZone(TimeZone.getTimeZone("UTC").toZoneId());
+ DateTimeFormatter dtf = DateTimeFormatter.ISO_DATE_TIME.withZone(TimeZone.getTimeZone("UTC").toZoneId());
return dtf.format(Instant.now());
}
}
\ No newline at end of file
diff --git a/powertools-e2e-tests/handlers/idempotency/src/main/java/software/amazon/lambda/powertools/e2e/Input.java b/powertools-e2e-tests/handlers/idempotency/src/main/java/software/amazon/lambda/powertools/e2e/Input.java
index c5c2a121e..e0e4c27c9 100644
--- a/powertools-e2e-tests/handlers/idempotency/src/main/java/software/amazon/lambda/powertools/e2e/Input.java
+++ b/powertools-e2e-tests/handlers/idempotency/src/main/java/software/amazon/lambda/powertools/e2e/Input.java
@@ -1,3 +1,17 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
package software.amazon.lambda.powertools.e2e;
public class Input {
diff --git a/examples/powertools-examples-sqs/src/main/resources/log4j2.xml b/powertools-e2e-tests/handlers/idempotency/src/main/resources/log4j2.xml
similarity index 94%
rename from examples/powertools-examples-sqs/src/main/resources/log4j2.xml
rename to powertools-e2e-tests/handlers/idempotency/src/main/resources/log4j2.xml
index e1fd14cea..8925f70b9 100644
--- a/examples/powertools-examples-sqs/src/main/resources/log4j2.xml
+++ b/powertools-e2e-tests/handlers/idempotency/src/main/resources/log4j2.xml
@@ -6,11 +6,11 @@
+
+
+
-
-
-
\ No newline at end of file
diff --git a/powertools-e2e-tests/handlers/largemessage/pom.xml b/powertools-e2e-tests/handlers/largemessage/pom.xml
new file mode 100644
index 000000000..c626f5f64
--- /dev/null
+++ b/powertools-e2e-tests/handlers/largemessage/pom.xml
@@ -0,0 +1,72 @@
+
+ 4.0.0
+
+
+ software.amazon.lambda
+ e2e-test-handlers-parent
+ 1.0.0
+
+
+ e2e-test-handler-largemessage
+ jar
+ A Lambda function using Powertools for AWS Lambda (Java) large message
+
+
+
+ software.amazon.awssdk
+ dynamodb
+
+
+ software.amazon.lambda
+ powertools-large-messages
+
+
+ software.amazon.lambda
+ powertools-logging
+
+
+ com.amazonaws
+ aws-lambda-java-events
+
+
+ org.apache.logging.log4j
+ log4j-slf4j2-impl
+
+
+
+
+
+
+ dev.aspectj
+ aspectj-maven-plugin
+
+ ${maven.compiler.source}
+ ${maven.compiler.target}
+ ${maven.compiler.target}
+
+
+ software.amazon.lambda
+ powertools-large-messages
+
+
+ software.amazon.lambda
+ powertools-logging
+
+
+
+
+
+
+ compile
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+
+
+
+
diff --git a/powertools-e2e-tests/handlers/largemessage/src/main/java/software/amazon/lambda/powertools/e2e/Function.java b/powertools-e2e-tests/handlers/largemessage/src/main/java/software/amazon/lambda/powertools/e2e/Function.java
new file mode 100644
index 000000000..70f1bceea
--- /dev/null
+++ b/powertools-e2e-tests/handlers/largemessage/src/main/java/software/amazon/lambda/powertools/e2e/Function.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package software.amazon.lambda.powertools.e2e;
+
+import com.amazonaws.services.lambda.runtime.Context;
+import com.amazonaws.services.lambda.runtime.RequestHandler;
+import com.amazonaws.services.lambda.runtime.events.SQSBatchResponse;
+import com.amazonaws.services.lambda.runtime.events.SQSEvent;
+import com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage;
+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.AttributeValue;
+import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;
+import software.amazon.awssdk.utils.BinaryUtils;
+import software.amazon.awssdk.utils.Md5Utils;
+import software.amazon.lambda.powertools.largemessages.LargeMessage;
+import software.amazon.lambda.powertools.logging.Logging;
+
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+
+public class Function implements RequestHandler {
+
+ private static final String TABLE_FOR_ASYNC_TESTS = System.getenv("TABLE_FOR_ASYNC_TESTS");
+ private DynamoDbClient client;
+
+ public Function() {
+ if (client == null) {
+ client = DynamoDbClient.builder()
+ .httpClient(UrlConnectionHttpClient.builder().build())
+ .region(Region.of(System.getenv("AWS_REGION")))
+ .build();
+ }
+ }
+
+ @Logging(logEvent = true)
+ public SQSBatchResponse handleRequest(SQSEvent event, Context context) {
+ for (SQSMessage message: event.getRecords()) {
+ processRawMessage(message, context);
+ }
+ return SQSBatchResponse.builder().build();
+ }
+
+ @LargeMessage
+ private void processRawMessage(SQSMessage sqsMessage, Context context) {
+ String bodyMD5 = md5(sqsMessage.getBody());
+ if (!sqsMessage.getMd5OfBody().equals(bodyMD5)) {
+ throw new SecurityException(String.format("message digest does not match, expected %s, got %s", sqsMessage.getMd5OfBody(), bodyMD5));
+ }
+
+ Map item = new HashMap<>();
+ item.put("functionName", AttributeValue.builder().s(context.getFunctionName()).build());
+ item.put("id", AttributeValue.builder().s(sqsMessage.getMessageId()).build());
+ item.put("bodyMD5", AttributeValue.builder().s(bodyMD5).build());
+ item.put("bodySize", AttributeValue.builder().n(String.valueOf(sqsMessage.getBody().getBytes(StandardCharsets.UTF_8).length)).build());
+
+ client.putItem(PutItemRequest.builder().tableName(TABLE_FOR_ASYNC_TESTS).item(item).build());
+ }
+
+ private String md5(String message) {
+ return BinaryUtils.toHex(Md5Utils.computeMD5Hash(message.getBytes(StandardCharsets.UTF_8)));
+ }
+}
\ No newline at end of file
diff --git a/powertools-e2e-tests/handlers/largemessage/src/main/resources/log4j2.xml b/powertools-e2e-tests/handlers/largemessage/src/main/resources/log4j2.xml
new file mode 100644
index 000000000..8925f70b9
--- /dev/null
+++ b/powertools-e2e-tests/handlers/largemessage/src/main/resources/log4j2.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/powertools-e2e-tests/handlers/largemessage_idempotent/pom.xml b/powertools-e2e-tests/handlers/largemessage_idempotent/pom.xml
new file mode 100644
index 000000000..9635efd87
--- /dev/null
+++ b/powertools-e2e-tests/handlers/largemessage_idempotent/pom.xml
@@ -0,0 +1,77 @@
+
+ 4.0.0
+
+
+ software.amazon.lambda
+ e2e-test-handlers-parent
+ 1.0.0
+
+
+ e2e-test-handler-large-msg-idempotent
+ jar
+ A Lambda function using Powertools for AWS Lambda (Java) idempotency with large messages
+
+
+
+ software.amazon.lambda
+ powertools-idempotency
+
+
+ software.amazon.lambda
+ powertools-large-messages
+
+
+ software.amazon.lambda
+ powertools-logging
+
+
+ com.amazonaws
+ aws-lambda-java-events
+
+
+ org.apache.logging.log4j
+ log4j-slf4j2-impl
+
+
+
+
+
+
+
+ dev.aspectj
+ aspectj-maven-plugin
+
+ ${maven.compiler.source}
+ ${maven.compiler.target}
+ ${maven.compiler.target}
+
+
+ software.amazon.lambda
+ powertools-idempotency
+
+
+ software.amazon.lambda
+ powertools-large-messages
+
+
+ software.amazon.lambda
+ powertools-logging
+
+
+
+
+
+
+ compile
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+
+
+
+
diff --git a/powertools-e2e-tests/handlers/largemessage_idempotent/src/main/java/software/amazon/lambda/powertools/e2e/Function.java b/powertools-e2e-tests/handlers/largemessage_idempotent/src/main/java/software/amazon/lambda/powertools/e2e/Function.java
new file mode 100644
index 000000000..b9f737857
--- /dev/null
+++ b/powertools-e2e-tests/handlers/largemessage_idempotent/src/main/java/software/amazon/lambda/powertools/e2e/Function.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package software.amazon.lambda.powertools.e2e;
+
+import com.amazonaws.services.lambda.runtime.Context;
+import com.amazonaws.services.lambda.runtime.RequestHandler;
+import com.amazonaws.services.lambda.runtime.events.SQSBatchResponse;
+import com.amazonaws.services.lambda.runtime.events.SQSEvent;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.TimeZone;
+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.AttributeValue;
+import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;
+import software.amazon.awssdk.utils.BinaryUtils;
+import software.amazon.awssdk.utils.Md5Utils;
+import software.amazon.lambda.powertools.idempotency.Idempotency;
+import software.amazon.lambda.powertools.idempotency.IdempotencyConfig;
+import software.amazon.lambda.powertools.idempotency.IdempotencyKey;
+import software.amazon.lambda.powertools.idempotency.Idempotent;
+import software.amazon.lambda.powertools.idempotency.persistence.DynamoDBPersistenceStore;
+import software.amazon.lambda.powertools.largemessages.LargeMessage;
+import software.amazon.lambda.powertools.logging.Logging;
+
+public class Function implements RequestHandler {
+
+ private static final String TABLE_FOR_ASYNC_TESTS = System.getenv("TABLE_FOR_ASYNC_TESTS");
+ private final DynamoDbClient client;
+
+ public Function() {
+ this(DynamoDbClient
+ .builder()
+ .httpClient(UrlConnectionHttpClient.builder().build())
+ .region(Region.of(System.getenv("AWS_REGION")))
+ .build());
+ }
+
+ public Function(DynamoDbClient client) {
+ this.client = client;
+ Idempotency.config().withConfig(
+ IdempotencyConfig.builder()
+ .withExpiration(Duration.of(22, ChronoUnit.SECONDS))
+ .withEventKeyJMESPath("body") // get the body of the message
+ .build())
+ .withPersistenceStore(
+ DynamoDBPersistenceStore.builder()
+ .withDynamoDbClient(client)
+ .withTableName(System.getenv("IDEMPOTENCY_TABLE"))
+ .build()
+ ).configure();
+ }
+
+ @Logging(logEvent = true)
+ public SQSBatchResponse handleRequest(SQSEvent event, Context context) {
+ for (SQSEvent.SQSMessage message: event.getRecords()) {
+ processRawMessage(message, context);
+ }
+ return SQSBatchResponse.builder().build();
+ }
+
+ @Idempotent
+ @LargeMessage(deleteS3Object = false)
+ private String processRawMessage(@IdempotencyKey SQSEvent.SQSMessage sqsMessage, Context context) {
+ String bodyMD5 = md5(sqsMessage.getBody());
+ if (!sqsMessage.getMd5OfBody().equals(bodyMD5)) {
+ throw new SecurityException(String.format("message digest does not match, expected %s, got %s", sqsMessage.getMd5OfBody(), bodyMD5));
+ }
+
+ Instant now = Instant.now();
+ Map item = new HashMap<>();
+ item.put("functionName", AttributeValue.builder().s(context.getFunctionName()).build());
+ item.put("id", AttributeValue.builder().s(sqsMessage.getMessageId()).build());
+ item.put("bodyMD5", AttributeValue.builder().s(bodyMD5).build());
+ item.put("now", AttributeValue.builder().n(String.valueOf(now.getEpochSecond())).build());
+ item.put("bodySize", AttributeValue.builder().n(String.valueOf(sqsMessage.getBody().getBytes(StandardCharsets.UTF_8).length)).build());
+
+ client.putItem(PutItemRequest.builder().tableName(TABLE_FOR_ASYNC_TESTS).item(item).build());
+
+ DateTimeFormatter dtf = DateTimeFormatter.ISO_DATE_TIME.withZone(TimeZone.getTimeZone("UTC").toZoneId());
+ return dtf.format(now);
+ }
+
+ private String md5(String message) {
+ return BinaryUtils.toHex(Md5Utils.computeMD5Hash(message.getBytes(StandardCharsets.UTF_8)));
+ }
+}
\ No newline at end of file
diff --git a/powertools-e2e-tests/handlers/largemessage_idempotent/src/main/resources/log4j2.xml b/powertools-e2e-tests/handlers/largemessage_idempotent/src/main/resources/log4j2.xml
new file mode 100644
index 000000000..8925f70b9
--- /dev/null
+++ b/powertools-e2e-tests/handlers/largemessage_idempotent/src/main/resources/log4j2.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/powertools-e2e-tests/handlers/logging/src/main/java/software/amazon/lambda/powertools/e2e/Function.java b/powertools-e2e-tests/handlers/logging/src/main/java/software/amazon/lambda/powertools/e2e/Function.java
index 5a9a87109..62ebabc6e 100644
--- a/powertools-e2e-tests/handlers/logging/src/main/java/software/amazon/lambda/powertools/e2e/Function.java
+++ b/powertools-e2e-tests/handlers/logging/src/main/java/software/amazon/lambda/powertools/e2e/Function.java
@@ -1,3 +1,17 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
package software.amazon.lambda.powertools.e2e;
import com.amazonaws.services.lambda.runtime.Context;
diff --git a/powertools-e2e-tests/handlers/logging/src/main/java/software/amazon/lambda/powertools/e2e/Input.java b/powertools-e2e-tests/handlers/logging/src/main/java/software/amazon/lambda/powertools/e2e/Input.java
index 83afbbd5a..cc449922e 100644
--- a/powertools-e2e-tests/handlers/logging/src/main/java/software/amazon/lambda/powertools/e2e/Input.java
+++ b/powertools-e2e-tests/handlers/logging/src/main/java/software/amazon/lambda/powertools/e2e/Input.java
@@ -1,3 +1,17 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
package software.amazon.lambda.powertools.e2e;
import java.util.Map;
diff --git a/powertools-e2e-tests/handlers/metrics/src/main/java/software/amazon/lambda/powertools/e2e/Function.java b/powertools-e2e-tests/handlers/metrics/src/main/java/software/amazon/lambda/powertools/e2e/Function.java
index e643de9d5..d9cf575c3 100644
--- a/powertools-e2e-tests/handlers/metrics/src/main/java/software/amazon/lambda/powertools/e2e/Function.java
+++ b/powertools-e2e-tests/handlers/metrics/src/main/java/software/amazon/lambda/powertools/e2e/Function.java
@@ -1,3 +1,17 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
package software.amazon.lambda.powertools.e2e;
import com.amazonaws.services.lambda.runtime.Context;
diff --git a/powertools-e2e-tests/handlers/metrics/src/main/java/software/amazon/lambda/powertools/e2e/Input.java b/powertools-e2e-tests/handlers/metrics/src/main/java/software/amazon/lambda/powertools/e2e/Input.java
index 5ff8a7125..18c4eb747 100644
--- a/powertools-e2e-tests/handlers/metrics/src/main/java/software/amazon/lambda/powertools/e2e/Input.java
+++ b/powertools-e2e-tests/handlers/metrics/src/main/java/software/amazon/lambda/powertools/e2e/Input.java
@@ -1,3 +1,17 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
package software.amazon.lambda.powertools.e2e;
import java.util.Map;
@@ -7,6 +21,9 @@ public class Input {
private Map dimensions;
+ public Input() {
+ }
+
public Map getMetrics() {
return metrics;
}
@@ -15,10 +32,6 @@ public void setMetrics(Map metrics) {
this.metrics = metrics;
}
- public Input() {
- }
-
-
public Map getDimensions() {
return dimensions;
}
diff --git a/powertools-e2e-tests/handlers/parameters/src/main/java/software/amazon/lambda/powertools/e2e/Function.java b/powertools-e2e-tests/handlers/parameters/src/main/java/software/amazon/lambda/powertools/e2e/Function.java
index a2d6a1ba6..3a83a1b05 100644
--- a/powertools-e2e-tests/handlers/parameters/src/main/java/software/amazon/lambda/powertools/e2e/Function.java
+++ b/powertools-e2e-tests/handlers/parameters/src/main/java/software/amazon/lambda/powertools/e2e/Function.java
@@ -1,3 +1,17 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
package software.amazon.lambda.powertools.e2e;
import com.amazonaws.services.lambda.runtime.Context;
diff --git a/powertools-e2e-tests/handlers/parameters/src/main/java/software/amazon/lambda/powertools/e2e/Input.java b/powertools-e2e-tests/handlers/parameters/src/main/java/software/amazon/lambda/powertools/e2e/Input.java
index 7ea22143f..b481d25e1 100644
--- a/powertools-e2e-tests/handlers/parameters/src/main/java/software/amazon/lambda/powertools/e2e/Input.java
+++ b/powertools-e2e-tests/handlers/parameters/src/main/java/software/amazon/lambda/powertools/e2e/Input.java
@@ -1,34 +1,47 @@
-package software.amazon.lambda.powertools.e2e;
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
-import java.util.Map;
+package software.amazon.lambda.powertools.e2e;
public class Input {
private String app;
private String environment;
private String key;
- public void setApp(String app) {
- this.app = app;
- }
-
- public void setEnvironment(String environment) {
- this.environment = environment;
- }
-
- public void setKey(String key) {
- this.key = key;
- }
public String getApp() {
return app;
}
+ public void setApp(String app) {
+ this.app = app;
+ }
+
public String getEnvironment() {
return environment;
}
+ public void setEnvironment(String environment) {
+ this.environment = environment;
+ }
+
public String getKey() {
return key;
}
+ public void setKey(String key) {
+ this.key = key;
+ }
+
}
diff --git a/powertools-e2e-tests/handlers/pom.xml b/powertools-e2e-tests/handlers/pom.xml
index d24b4afb9..bf649fd01 100644
--- a/powertools-e2e-tests/handlers/pom.xml
+++ b/powertools-e2e-tests/handlers/pom.xml
@@ -20,6 +20,8 @@
3.5.01.13.13.11.0
+ 2.20.108
+ 2.20.0
@@ -32,6 +34,14 @@
+
+ software.amazon.awssdk
+ bom
+ ${aws.sdk.version}
+ pom
+ import
+
+
software.amazon.lambdapowertools-logging
@@ -57,6 +67,11 @@
powertools-parameters${lambda.powertools.version}
+
+ software.amazon.lambda
+ powertools-large-messages
+ ${lambda.powertools.version}
+ com.amazonawsaws-lambda-java-core
@@ -67,6 +82,11 @@
aws-lambda-java-events${lambda.java.events}
+
+ org.apache.logging.log4j
+ log4j-slf4j2-impl
+ ${log4j.version}
+
diff --git a/powertools-e2e-tests/handlers/tracing/src/main/java/software/amazon/lambda/powertools/e2e/Function.java b/powertools-e2e-tests/handlers/tracing/src/main/java/software/amazon/lambda/powertools/e2e/Function.java
index f7b2c5e5d..462b7c71d 100644
--- a/powertools-e2e-tests/handlers/tracing/src/main/java/software/amazon/lambda/powertools/e2e/Function.java
+++ b/powertools-e2e-tests/handlers/tracing/src/main/java/software/amazon/lambda/powertools/e2e/Function.java
@@ -1,3 +1,17 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
package software.amazon.lambda.powertools.e2e;
import com.amazonaws.services.lambda.runtime.Context;
@@ -16,13 +30,14 @@ public String handleRequest(Input input, Context context) {
}
String message = buildMessage(input.getMessage(), context.getFunctionName());
- TracingUtils.withSubsegment("internal_stuff", subsegment -> {
- try {
- Thread.sleep(100); // simulate stuff
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
- });
+ TracingUtils.withSubsegment("internal_stuff", subsegment ->
+ {
+ try {
+ Thread.sleep(100); // simulate stuff
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ });
return message;
}
diff --git a/powertools-e2e-tests/handlers/tracing/src/main/java/software/amazon/lambda/powertools/e2e/Input.java b/powertools-e2e-tests/handlers/tracing/src/main/java/software/amazon/lambda/powertools/e2e/Input.java
index 29cf618ba..92078d0b3 100644
--- a/powertools-e2e-tests/handlers/tracing/src/main/java/software/amazon/lambda/powertools/e2e/Input.java
+++ b/powertools-e2e-tests/handlers/tracing/src/main/java/software/amazon/lambda/powertools/e2e/Input.java
@@ -1,3 +1,17 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
package software.amazon.lambda.powertools.e2e;
public class Input {
diff --git a/powertools-e2e-tests/pom.xml b/powertools-e2e-tests/pom.xml
index 1215bb66d..29f6cafaa 100644
--- a/powertools-e2e-tests/pom.xml
+++ b/powertools-e2e-tests/pom.xml
@@ -1,4 +1,18 @@
+
+
@@ -16,8 +30,8 @@
1.81.8
- 10.2.62
- 2.85.0
+ 10.2.69
+ 2.89.0true
@@ -37,6 +51,13 @@
test
+
+ software.amazon.awssdk
+ dynamodb
+ ${aws.sdk.version}
+ test
+
+
software.amazon.awssdkcloudwatch
@@ -51,6 +72,20 @@
test
+
+ software.amazon.awssdk
+ sqs
+ ${aws.sdk.version}
+ test
+
+
+
+ com.amazonaws
+ amazon-sqs-java-extended-client-lib
+ 2.0.3
+ test
+
+
software.amazon.awssdkurl-connection-client
@@ -63,6 +98,12 @@
test
+
+ commons-io
+ commons-io
+ 2.13.0
+
+
org.junit.jupiterjunit-jupiter-engine
@@ -133,6 +174,10 @@
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+ org.apache.maven.pluginsmaven-compiler-plugin
@@ -164,6 +209,7 @@
+ 1**/*E2ET.java
diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/IdempotencyE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/IdempotencyE2ET.java
index 6923c3caa..242d1a2db 100644
--- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/IdempotencyE2ET.java
+++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/IdempotencyE2ET.java
@@ -1,5 +1,26 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
package software.amazon.lambda.powertools;
+import static software.amazon.lambda.powertools.testutils.Infrastructure.FUNCTION_NAME_OUTPUT;
+import static software.amazon.lambda.powertools.testutils.lambda.LambdaInvoker.invokeFunction;
+
+import java.time.Year;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
@@ -8,13 +29,6 @@
import software.amazon.lambda.powertools.testutils.Infrastructure;
import software.amazon.lambda.powertools.testutils.lambda.InvocationResult;
-import java.time.Year;
-import java.util.Collections;
-import java.util.UUID;
-import java.util.concurrent.TimeUnit;
-
-import static software.amazon.lambda.powertools.testutils.lambda.LambdaInvoker.invokeFunction;
-
public class IdempotencyE2ET {
private static Infrastructure infrastructure;
private static String functionName;
@@ -27,15 +41,16 @@ public static void setup() {
.testName(IdempotencyE2ET.class.getSimpleName())
.pathToFunction("idempotency")
.idempotencyTable("idempo" + random)
- .environmentVariables(Collections.singletonMap("IDEMPOTENCY_TABLE", "idempo" + random))
.build();
- functionName = infrastructure.deploy();
+ Map outputs = infrastructure.deploy();
+ functionName = outputs.get(FUNCTION_NAME_OUTPUT);
}
@AfterAll
public static void tearDown() {
- if (infrastructure != null)
+ if (infrastructure != null) {
infrastructure.destroy();
+ }
}
@Test
diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LargeMessageE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LargeMessageE2ET.java
new file mode 100644
index 000000000..2d9f74135
--- /dev/null
+++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LargeMessageE2ET.java
@@ -0,0 +1,167 @@
+package software.amazon.lambda.powertools;
+
+import com.amazon.sqs.javamessaging.AmazonSQSExtendedClient;
+import com.amazon.sqs.javamessaging.ExtendedClientConfiguration;
+import org.apache.commons.io.IOUtils;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import software.amazon.awssdk.http.SdkHttpClient;
+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.AttributeValue;
+import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest;
+import software.amazon.awssdk.services.dynamodb.model.QueryRequest;
+import software.amazon.awssdk.services.dynamodb.model.QueryResponse;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.sqs.SqsClient;
+import software.amazon.awssdk.services.sqs.model.SendMessageRequest;
+import software.amazon.lambda.powertools.testutils.Infrastructure;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static software.amazon.lambda.powertools.testutils.Infrastructure.FUNCTION_NAME_OUTPUT;
+
+public class LargeMessageE2ET {
+
+ private static final Logger LOG = LoggerFactory.getLogger(LargeMessageE2ET.class);
+ private static final SdkHttpClient httpClient = UrlConnectionHttpClient.builder().build();
+ private static final Region region = Region.of(System.getProperty("AWS_DEFAULT_REGION", "eu-west-1"));
+
+ private static final S3Client s3Client = S3Client.builder()
+ .httpClient(httpClient)
+ .region(region)
+ .build();
+ private static final DynamoDbClient dynamoDbClient = DynamoDbClient.builder()
+ .httpClient(httpClient)
+ .region(region)
+ .build();
+
+ private static Infrastructure infrastructure;
+ private static String functionName;
+ private static String bucketName;
+ private static String queueUrl;
+ private static String tableName;
+ private String messageId;
+
+ @BeforeAll
+ @Timeout(value = 5, unit = TimeUnit.MINUTES)
+ public static void setup() {
+ String random = UUID.randomUUID().toString().substring(0, 6);
+ bucketName = "largemessagebucket" + random;
+ String queueName = "largemessagequeue" + random;
+
+ infrastructure = Infrastructure.builder()
+ .testName(LargeMessageE2ET.class.getSimpleName())
+ .queue(queueName)
+ .largeMessagesBucket(bucketName)
+ .pathToFunction("largemessage")
+ .timeoutInSeconds(60)
+ .build();
+
+ Map outputs = infrastructure.deploy();
+ functionName = outputs.get(FUNCTION_NAME_OUTPUT);
+ queueUrl = outputs.get("QueueURL");
+ tableName = outputs.get("TableNameForAsyncTests");
+
+ LOG.info("Testing '" + LargeMessageE2ET.class.getSimpleName() + "'");
+ }
+
+ @AfterEach
+ public void reset() {
+ if (messageId != null) {
+ Map itemToDelete = new HashMap<>();
+ itemToDelete.put("functionName", AttributeValue.builder().s(functionName).build());
+ itemToDelete.put("id", AttributeValue.builder().s(messageId).build());
+ dynamoDbClient.deleteItem(DeleteItemRequest.builder().tableName(tableName).key(itemToDelete).build());
+ messageId = null;
+ }
+ }
+
+ @AfterAll
+ public static void tearDown() {
+ if (infrastructure != null)
+ infrastructure.destroy();
+ }
+
+ @Test
+ public void bigSQSMessageOffloadedToS3_shouldLoadFromS3() throws IOException, InterruptedException {
+ // given
+ final ExtendedClientConfiguration extendedClientConfig =
+ new ExtendedClientConfiguration()
+ .withPayloadSupportEnabled(s3Client, bucketName);
+ AmazonSQSExtendedClient client = new AmazonSQSExtendedClient(SqsClient.builder().httpClient(httpClient).build(), extendedClientConfig);
+ InputStream inputStream = this.getClass().getResourceAsStream("/large_sqs_message.txt");
+ String bigMessage = IOUtils.toString(inputStream, StandardCharsets.UTF_8);
+
+ // when
+ client.sendMessage(SendMessageRequest
+ .builder()
+ .queueUrl(queueUrl)
+ .messageBody(bigMessage)
+ .build());
+
+ Thread.sleep(30000); // wait for function to be executed
+
+ // then
+ QueryRequest request = QueryRequest
+ .builder()
+ .tableName(tableName)
+ .keyConditionExpression("functionName = :func")
+ .expressionAttributeValues(Collections.singletonMap(":func", AttributeValue.builder().s(functionName).build()))
+ .build();
+ QueryResponse response = dynamoDbClient.query(request);
+ List
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+
diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/Constants.java b/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/Constants.java
index 28c6f58aa..0d19fa7a9 100644
--- a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/Constants.java
+++ b/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/Constants.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 Amazon.com, Inc. or its affiliates.
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
@@ -11,6 +11,7 @@
* limitations under the License.
*
*/
+
package software.amazon.lambda.powertools.idempotency;
public class Constants {
diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/Idempotency.java b/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/Idempotency.java
index ce652791b..6da826c45 100644
--- a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/Idempotency.java
+++ b/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/Idempotency.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 Amazon.com, Inc. or its affiliates.
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
@@ -11,6 +11,7 @@
* limitations under the License.
*
*/
+
package software.amazon.lambda.powertools.idempotency;
import com.amazonaws.services.lambda.runtime.Context;
@@ -38,29 +39,6 @@ public class Idempotency {
private Idempotency() {
}
- public IdempotencyConfig getConfig() {
- return config;
- }
-
- public BasePersistenceStore getPersistenceStore() {
- if (persistenceStore == null) {
- throw new IllegalStateException("Persistence Store is null, did you call 'configure()'?");
- }
- return persistenceStore;
- }
-
- private void setConfig(IdempotencyConfig config) {
- this.config = config;
- }
-
- private void setPersistenceStore(BasePersistenceStore persistenceStore) {
- this.persistenceStore = persistenceStore;
- }
-
- private static class Holder {
- private final static Idempotency instance = new Idempotency();
- }
-
public static Idempotency getInstance() {
return Holder.instance;
}
@@ -84,6 +62,29 @@ public static Config config() {
return new Config();
}
+ public IdempotencyConfig getConfig() {
+ return config;
+ }
+
+ private void setConfig(IdempotencyConfig config) {
+ this.config = config;
+ }
+
+ public BasePersistenceStore getPersistenceStore() {
+ if (persistenceStore == null) {
+ throw new IllegalStateException("Persistence Store is null, did you call 'configure()'?");
+ }
+ return persistenceStore;
+ }
+
+ private void setPersistenceStore(BasePersistenceStore persistenceStore) {
+ this.persistenceStore = persistenceStore;
+ }
+
+ private static class Holder {
+ private final static Idempotency instance = new Idempotency();
+ }
+
public static class Config {
private IdempotencyConfig config;
@@ -94,7 +95,8 @@ public static class Config {
*/
public void configure() {
if (store == null) {
- throw new IllegalStateException("Persistence Layer is null, configure one with 'withPersistenceStore()'");
+ throw new IllegalStateException(
+ "Persistence Layer is null, configure one with 'withPersistenceStore()'");
}
if (config == null) {
config = IdempotencyConfig.builder().build();
diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/IdempotencyConfig.java b/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/IdempotencyConfig.java
index 28f20ffa9..58d0a7f5b 100644
--- a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/IdempotencyConfig.java
+++ b/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/IdempotencyConfig.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 Amazon.com, Inc. or its affiliates.
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
@@ -11,12 +11,12 @@
* limitations under the License.
*
*/
+
package software.amazon.lambda.powertools.idempotency;
import com.amazonaws.services.lambda.runtime.Context;
-import software.amazon.lambda.powertools.idempotency.internal.cache.LRUCache;
-
import java.time.Duration;
+import software.amazon.lambda.powertools.idempotency.internal.cache.LRUCache;
/**
* Configuration of the idempotency feature. Use the {@link Builder} to create an instance.
@@ -31,7 +31,9 @@ public class IdempotencyConfig {
private final String hashFunction;
private Context lambdaContext;
- private IdempotencyConfig(String eventKeyJMESPath, String payloadValidationJMESPath, boolean throwOnNoIdempotencyKey, boolean useLocalCache, int localCacheMaxItems, long expirationInSeconds, String hashFunction) {
+ private IdempotencyConfig(String eventKeyJMESPath, String payloadValidationJMESPath,
+ boolean throwOnNoIdempotencyKey, boolean useLocalCache, int localCacheMaxItems,
+ long expirationInSeconds, String hashFunction) {
this.localCacheMaxItems = localCacheMaxItems;
this.useLocalCache = useLocalCache;
this.expirationInSeconds = expirationInSeconds;
@@ -41,6 +43,15 @@ private IdempotencyConfig(String eventKeyJMESPath, String payloadValidationJMESP
this.hashFunction = hashFunction;
}
+ /**
+ * Create a builder that can be used to configure and create a {@link IdempotencyConfig}.
+ *
+ * @return a new instance of {@link Builder}
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
public int getLocalCacheMaxItems() {
return localCacheMaxItems;
}
@@ -69,24 +80,14 @@ public String getHashFunction() {
return hashFunction;
}
-
- /**
- * Create a builder that can be used to configure and create a {@link IdempotencyConfig}.
- *
- * @return a new instance of {@link Builder}
- */
- public static Builder builder() {
- return new Builder();
+ public Context getLambdaContext() {
+ return lambdaContext;
}
public void setLambdaContext(Context lambdaContext) {
this.lambdaContext = lambdaContext;
}
- public Context getLambdaContext() {
- return lambdaContext;
- }
-
public static class Builder {
private int localCacheMaxItems = 256;
@@ -107,6 +108,7 @@ public static class Builder {
*
+ *
* @return an instance of {@link IdempotencyConfig}.
*/
public IdempotencyConfig build() {
@@ -124,16 +126,15 @@ public IdempotencyConfig build() {
* A JMESPath expression to extract the idempotency key from the event record.
* See https://jmespath.org/ for more details.
* Common paths are:
- *
powertools_json(body) for APIGatewayProxyRequestEvent and APIGatewayV2HTTPEvent
- *
Records[*].powertools_json(body) for SQSEvent
- *
Records[0].Sns.Message | powertools_json(@) for SNSEvent
- *
detail for ScheduledEvent (EventBridge / CloudWatch events)
- *
Records[*].kinesis.powertools_json(powertools_base64(data)) for KinesisEvent
- *
Records[*].powertools_json(powertools_base64(data)) for KinesisFirehoseEvent
- *
...
+ *
powertools_json(body) for APIGatewayProxyRequestEvent and APIGatewayV2HTTPEvent
+ *
Records[*].powertools_json(body) for SQSEvent
+ *
Records[0].Sns.Message | powertools_json(@) for SNSEvent
+ *
detail for ScheduledEvent (EventBridge / CloudWatch events)
+ *
Records[*].kinesis.powertools_json(powertools_base64(data)) for KinesisEvent
+ *
Records[*].powertools_json(powertools_base64(data)) for KinesisFirehoseEvent
+ *
...
*
*
- *
* @param eventKeyJMESPath path of the key in the Lambda event
* @return the instance of the builder (to chain operations)
*/
diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/IdempotencyKey.java b/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/IdempotencyKey.java
index 92a0a3d49..4f63b10f5 100644
--- a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/IdempotencyKey.java
+++ b/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/IdempotencyKey.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 Amazon.com, Inc. or its affiliates.
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
@@ -11,6 +11,7 @@
* limitations under the License.
*
*/
+
package software.amazon.lambda.powertools.idempotency;
import java.lang.annotation.ElementType;
diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/Idempotent.java b/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/Idempotent.java
index e7cace1fb..6ca40a0e1 100644
--- a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/Idempotent.java
+++ b/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/Idempotent.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 Amazon.com, Inc. or its affiliates.
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
@@ -11,10 +11,10 @@
* limitations under the License.
*
*/
+
package software.amazon.lambda.powertools.idempotency;
import com.amazonaws.services.lambda.runtime.Context;
-
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyAlreadyInProgressException.java b/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyAlreadyInProgressException.java
index 3d5ee93c5..dc87f422b 100644
--- a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyAlreadyInProgressException.java
+++ b/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyAlreadyInProgressException.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 Amazon.com, Inc. or its affiliates.
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
@@ -11,6 +11,7 @@
* limitations under the License.
*
*/
+
package software.amazon.lambda.powertools.idempotency.exceptions;
/**
diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyConfigurationException.java b/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyConfigurationException.java
index 0d3844641..9e85f4b5f 100644
--- a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyConfigurationException.java
+++ b/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyConfigurationException.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 Amazon.com, Inc. or its affiliates.
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
@@ -11,6 +11,7 @@
* limitations under the License.
*
*/
+
package software.amazon.lambda.powertools.idempotency.exceptions;
/**
diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyInconsistentStateException.java b/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyInconsistentStateException.java
index 40c90dcab..e41e30e84 100644
--- a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyInconsistentStateException.java
+++ b/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyInconsistentStateException.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 Amazon.com, Inc. or its affiliates.
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
@@ -11,6 +11,7 @@
* limitations under the License.
*
*/
+
package software.amazon.lambda.powertools.idempotency.exceptions;
/**
diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyItemAlreadyExistsException.java b/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyItemAlreadyExistsException.java
index 088db59c0..ba7da69bf 100644
--- a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyItemAlreadyExistsException.java
+++ b/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyItemAlreadyExistsException.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 Amazon.com, Inc. or its affiliates.
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
@@ -11,6 +11,7 @@
* limitations under the License.
*
*/
+
package software.amazon.lambda.powertools.idempotency.exceptions;
/**
diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyItemNotFoundException.java b/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyItemNotFoundException.java
index afae2554e..420829363 100644
--- a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyItemNotFoundException.java
+++ b/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyItemNotFoundException.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 Amazon.com, Inc. or its affiliates.
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
@@ -11,15 +11,16 @@
* limitations under the License.
*
*/
+
package software.amazon.lambda.powertools.idempotency.exceptions;
/**
* Exception thrown when the item was not found in the persistence store.
*/
-public class IdempotencyItemNotFoundException extends RuntimeException{
+public class IdempotencyItemNotFoundException extends RuntimeException {
private static final long serialVersionUID = 4818288566747993032L;
public IdempotencyItemNotFoundException(String idempotencyKey) {
- super("Item with idempotency key "+ idempotencyKey + " not found");
+ super("Item with idempotency key " + idempotencyKey + " not found");
}
}
diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyKeyException.java b/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyKeyException.java
index 7259dff0f..29b8bd2ec 100644
--- a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyKeyException.java
+++ b/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyKeyException.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 Amazon.com, Inc. or its affiliates.
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
@@ -11,6 +11,7 @@
* limitations under the License.
*
*/
+
package software.amazon.lambda.powertools.idempotency.exceptions;
/**
diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyPersistenceLayerException.java b/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyPersistenceLayerException.java
index fa49b746c..bfdd2d792 100644
--- a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyPersistenceLayerException.java
+++ b/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyPersistenceLayerException.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 Amazon.com, Inc. or its affiliates.
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
@@ -11,6 +11,7 @@
* limitations under the License.
*
*/
+
package software.amazon.lambda.powertools.idempotency.exceptions;
/**
diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyValidationException.java b/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyValidationException.java
index 5aee228eb..cdb2bb6a7 100644
--- a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyValidationException.java
+++ b/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyValidationException.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 Amazon.com, Inc. or its affiliates.
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
@@ -11,6 +11,7 @@
* limitations under the License.
*
*/
+
package software.amazon.lambda.powertools.idempotency.exceptions;
import software.amazon.lambda.powertools.idempotency.IdempotencyConfig;
diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/internal/IdempotencyHandler.java b/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/internal/IdempotencyHandler.java
index 5ce723f04..2875ab3d1 100644
--- a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/internal/IdempotencyHandler.java
+++ b/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/internal/IdempotencyHandler.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 Amazon.com, Inc. or its affiliates.
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
@@ -11,26 +11,32 @@
* limitations under the License.
*
*/
+
package software.amazon.lambda.powertools.idempotency.internal;
+import static software.amazon.lambda.powertools.idempotency.persistence.DataRecord.Status.EXPIRED;
+import static software.amazon.lambda.powertools.idempotency.persistence.DataRecord.Status.INPROGRESS;
+
import com.amazonaws.services.lambda.runtime.Context;
import com.fasterxml.jackson.databind.JsonNode;
+import java.time.Instant;
+import java.util.OptionalInt;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import software.amazon.lambda.powertools.idempotency.Idempotency;
-import software.amazon.lambda.powertools.idempotency.exceptions.*;
+import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyAlreadyInProgressException;
+import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyInconsistentStateException;
+import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemAlreadyExistsException;
+import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemNotFoundException;
+import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyKeyException;
+import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyPersistenceLayerException;
+import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyValidationException;
import software.amazon.lambda.powertools.idempotency.persistence.BasePersistenceStore;
import software.amazon.lambda.powertools.idempotency.persistence.DataRecord;
import software.amazon.lambda.powertools.utilities.JsonConfig;
-import java.time.Instant;
-import java.util.OptionalInt;
-
-import static software.amazon.lambda.powertools.idempotency.persistence.DataRecord.Status.EXPIRED;
-import static software.amazon.lambda.powertools.idempotency.persistence.DataRecord.Status.INPROGRESS;
-
/**
* Internal class that will handle the Idempotency, and use the {@link software.amazon.lambda.powertools.idempotency.persistence.PersistenceStore}
* to store the result of previous calls.
@@ -90,7 +96,9 @@ private Object processIdempotency() throws Throwable {
} catch (IdempotencyKeyException ike) {
throw ike;
} catch (Exception e) {
- throw new IdempotencyPersistenceLayerException("Failed to save in progress record to idempotency store. If you believe this is a Powertools for AWS Lambda (Java) bug, please open an issue.", e);
+ throw new IdempotencyPersistenceLayerException(
+ "Failed to save in progress record to idempotency store. If you believe this is a Powertools for AWS Lambda (Java) bug, please open an issue.",
+ e);
}
return getFunctionResponse();
}
@@ -121,11 +129,14 @@ private DataRecord getIdempotencyRecord() {
} catch (IdempotencyItemNotFoundException e) {
// This code path will only be triggered if the record is removed between saveInProgress and getRecord
LOG.debug("An existing idempotency record was deleted before we could fetch it");
- throw new IdempotencyInconsistentStateException("saveInProgress and getRecord return inconsistent results", e);
+ throw new IdempotencyInconsistentStateException("saveInProgress and getRecord return inconsistent results",
+ e);
} catch (IdempotencyValidationException | IdempotencyKeyException vke) {
throw vke;
} catch (Exception e) {
- throw new IdempotencyPersistenceLayerException("Failed to get record from idempotency store. If you believe this is a Powertools for AWS Lambda (Java) bug, please open an issue.", e);
+ throw new IdempotencyPersistenceLayerException(
+ "Failed to get record from idempotency store. If you believe this is a Powertools for AWS Lambda (Java) bug, please open an issue.",
+ e);
}
}
@@ -144,19 +155,24 @@ private Object handleForStatus(DataRecord record) {
if (INPROGRESS.equals(record.getStatus())) {
if (record.getInProgressExpiryTimestamp().isPresent()
&& record.getInProgressExpiryTimestamp().getAsLong() < Instant.now().toEpochMilli()) {
- throw new IdempotencyInconsistentStateException("Item should have been expired in-progress because it already time-outed.");
+ throw new IdempotencyInconsistentStateException(
+ "Item should have been expired in-progress because it already time-outed.");
}
- throw new IdempotencyAlreadyInProgressException("Execution already in progress with idempotency key: " + record.getIdempotencyKey());
+ throw new IdempotencyAlreadyInProgressException(
+ "Execution already in progress with idempotency key: " + record.getIdempotencyKey());
}
Class> returnType = ((MethodSignature) pjp.getSignature()).getReturnType();
try {
- LOG.debug("Response for key '{}' retrieved from idempotency store, skipping the function", record.getIdempotencyKey());
- if (returnType.equals(String.class))
+ LOG.debug("Response for key '{}' retrieved from idempotency store, skipping the function",
+ record.getIdempotencyKey());
+ if (returnType.equals(String.class)) {
return record.getResponseData();
+ }
return JsonConfig.get().getObjectMapper().reader().readValue(record.getResponseData(), returnType);
} catch (Exception e) {
- throw new IdempotencyPersistenceLayerException("Unable to get function response as " + returnType.getSimpleName(), e);
+ throw new IdempotencyPersistenceLayerException(
+ "Unable to get function response as " + returnType.getSimpleName(), e);
}
}
@@ -172,7 +188,9 @@ private Object getFunctionResponse() throws Throwable {
} catch (IdempotencyKeyException ke) {
throw ke;
} catch (Exception e) {
- throw new IdempotencyPersistenceLayerException("Failed to delete record from idempotency store. If you believe this is a Powertools for AWS Lambda (Java) bug, please open an issue.", e);
+ throw new IdempotencyPersistenceLayerException(
+ "Failed to delete record from idempotency store. If you believe this is a Powertools for AWS Lambda (Java) bug, please open an issue.",
+ e);
}
throw handlerException;
}
@@ -180,7 +198,9 @@ private Object getFunctionResponse() throws Throwable {
try {
persistenceStore.saveSuccess(data, response, Instant.now());
} catch (Exception e) {
- throw new IdempotencyPersistenceLayerException("Failed to update record state to success in idempotency store. If you believe this is a Powertools for AWS Lambda (Java) bug, please open an issue.", e);
+ throw new IdempotencyPersistenceLayerException(
+ "Failed to update record state to success in idempotency store. If you believe this is a Powertools for AWS Lambda (Java) bug, please open an issue.",
+ e);
}
return response;
}
diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/internal/IdempotentAspect.java b/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/internal/IdempotentAspect.java
index dc2703e64..989e88eb7 100644
--- a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/internal/IdempotentAspect.java
+++ b/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/internal/IdempotentAspect.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 Amazon.com, Inc. or its affiliates.
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
@@ -11,15 +11,21 @@
* limitations under the License.
*
*/
+
package software.amazon.lambda.powertools.idempotency.internal;
+import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.placedOnRequestHandler;
+
+import com.amazonaws.services.lambda.runtime.Context;
import com.fasterxml.jackson.databind.JsonNode;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.DeclarePrecedence;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
-import com.amazonaws.services.lambda.runtime.Context;
import software.amazon.lambda.powertools.idempotency.Constants;
import software.amazon.lambda.powertools.idempotency.Idempotency;
import software.amazon.lambda.powertools.idempotency.IdempotencyKey;
@@ -27,16 +33,13 @@
import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyConfigurationException;
import software.amazon.lambda.powertools.utilities.JsonConfig;
-import java.lang.annotation.Annotation;
-import java.lang.reflect.Method;
-
-import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.placedOnRequestHandler;
-
/**
* Aspect that handles the {@link Idempotent} annotation.
* It uses the {@link IdempotencyHandler} to actually do the job.
*/
@Aspect
+// Idempotency annotation should come first before large message
+@DeclarePrecedence("software.amazon.lambda.powertools.idempotency.internal.IdempotentAspect, *")
public class IdempotentAspect {
@SuppressWarnings({"EmptyMethod"})
@Pointcut("@annotation(idempotent)")
@@ -48,19 +51,21 @@ public Object around(ProceedingJoinPoint pjp,
Idempotent idempotent) throws Throwable {
String idempotencyDisabledEnv = System.getenv().get(Constants.IDEMPOTENCY_DISABLED_ENV);
- if (idempotencyDisabledEnv != null && !idempotencyDisabledEnv.equalsIgnoreCase("false")) {
+ if (idempotencyDisabledEnv != null && !"false".equalsIgnoreCase(idempotencyDisabledEnv)) {
return pjp.proceed(pjp.getArgs());
}
Method method = ((MethodSignature) pjp.getSignature()).getMethod();
if (method.getReturnType().equals(void.class)) {
- throw new IdempotencyConfigurationException("The annotated method doesn't return anything. Unable to perform idempotency on void return type");
+ throw new IdempotencyConfigurationException(
+ "The annotated method doesn't return anything. Unable to perform idempotency on void return type");
}
boolean isHandler = placedOnRequestHandler(pjp);
JsonNode payload = getPayload(pjp, method, isHandler);
if (payload == null) {
- throw new IdempotencyConfigurationException("Unable to get payload from the method. Ensure there is at least one parameter or that you use @IdempotencyKey");
+ throw new IdempotencyConfigurationException(
+ "Unable to get payload from the method. Ensure there is at least one parameter or that you use @IdempotencyKey");
}
Context lambdaContext;
@@ -76,7 +81,8 @@ public Object around(ProceedingJoinPoint pjp,
/**
* Retrieve the payload from the annotated method parameters
- * @param pjp joinPoint
+ *
+ * @param pjp joinPoint
* @param method the annotated method
* @return the payload used for idempotency
*/
diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/internal/cache/LRUCache.java b/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/internal/cache/LRUCache.java
index a017c211a..b57ad2977 100644
--- a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/internal/cache/LRUCache.java
+++ b/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/internal/cache/LRUCache.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 Amazon.com, Inc. or its affiliates.
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
@@ -11,6 +11,7 @@
* limitations under the License.
*
*/
+
package software.amazon.lambda.powertools.idempotency.internal.cache;
import java.util.LinkedHashMap;
@@ -19,6 +20,7 @@
/**
* Implementation of a simple LRU Cache based on a {@link LinkedHashMap}
* See here.
+ *
* @param Type of the keys
* @param Types of the values
*/
diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/BasePersistenceStore.java b/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/BasePersistenceStore.java
index c79068d1a..f58b276fd 100644
--- a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/BasePersistenceStore.java
+++ b/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/BasePersistenceStore.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 Amazon.com, Inc. or its affiliates.
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
@@ -11,23 +11,15 @@
* limitations under the License.
*
*/
+
package software.amazon.lambda.powertools.idempotency.persistence;
+import static software.amazon.lambda.powertools.core.internal.LambdaConstants.LAMBDA_FUNCTION_NAME_ENV;
+
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectWriter;
import io.burt.jmespath.Expression;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import software.amazon.awssdk.utils.StringUtils;
-import software.amazon.lambda.powertools.idempotency.IdempotencyConfig;
-import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemAlreadyExistsException;
-import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemNotFoundException;
-import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyKeyException;
-import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyValidationException;
-import software.amazon.lambda.powertools.idempotency.internal.cache.LRUCache;
-import software.amazon.lambda.powertools.utilities.JsonConfig;
-
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
@@ -42,8 +34,16 @@
import java.util.Spliterators;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
-
-import static software.amazon.lambda.powertools.core.internal.LambdaConstants.LAMBDA_FUNCTION_NAME_ENV;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import software.amazon.awssdk.utils.StringUtils;
+import software.amazon.lambda.powertools.idempotency.IdempotencyConfig;
+import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemAlreadyExistsException;
+import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemNotFoundException;
+import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyKeyException;
+import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyValidationException;
+import software.amazon.lambda.powertools.idempotency.internal.cache.LRUCache;
+import software.amazon.lambda.powertools.utilities.JsonConfig;
/**
* Persistence layer that will store the idempotency result.
@@ -53,7 +53,7 @@
public abstract class BasePersistenceStore implements PersistenceStore {
private static final Logger LOG = LoggerFactory.getLogger(BasePersistenceStore.class);
-
+ protected boolean payloadValidationEnabled = false;
private String functionName = "";
private boolean configured = false;
private long expirationInSeconds = 60 * 60; // 1 hour default
@@ -61,7 +61,6 @@ public abstract class BasePersistenceStore implements PersistenceStore {
private LRUCache cache;
private String eventKeyJMESPath;
private Expression eventKeyCompiledJMESPath;
- protected boolean payloadValidationEnabled = false;
private Expression validationKeyJMESPath;
private boolean throwOnNoIdempotencyKey = false;
private String hashFunctionName;
@@ -130,7 +129,8 @@ public void saveSuccess(JsonNode data, Object result, Instant now) {
responseJson,
getHashedPayload(data)
);
- LOG.debug("Function successfully executed. Saving record to persistence store with idempotency key: {}", record.getIdempotencyKey());
+ LOG.debug("Function successfully executed. Saving record to persistence store with idempotency key: {}",
+ record.getIdempotencyKey());
updateRecord(record);
saveToCache(record);
} catch (JsonProcessingException e) {
@@ -145,7 +145,8 @@ public void saveSuccess(JsonNode data, Object result, Instant now) {
* @param data Payload
* @param now
*/
- public void saveInProgress(JsonNode data, Instant now, OptionalInt remainingTimeInMs) throws IdempotencyItemAlreadyExistsException {
+ public void saveInProgress(JsonNode data, Instant now, OptionalInt remainingTimeInMs)
+ throws IdempotencyItemAlreadyExistsException {
Optional hashedIdempotencyKey = getHashedIdempotencyKey(data);
if (!hashedIdempotencyKey.isPresent()) {
// missing idempotency key => non-idempotent transaction, we do not store the data, simply return
@@ -159,7 +160,8 @@ public void saveInProgress(JsonNode data, Instant now, OptionalInt remainingTime
OptionalLong inProgressExpirationMsTimestamp = OptionalLong.empty();
if (remainingTimeInMs.isPresent()) {
- inProgressExpirationMsTimestamp = OptionalLong.of(now.plus(remainingTimeInMs.getAsInt(), ChronoUnit.MILLIS).toEpochMilli());
+ inProgressExpirationMsTimestamp =
+ OptionalLong.of(now.plus(remainingTimeInMs.getAsInt(), ChronoUnit.MILLIS).toEpochMilli());
}
DataRecord record = new DataRecord(
@@ -205,7 +207,8 @@ public void deleteRecord(JsonNode data, Throwable throwable) {
* @throws IdempotencyValidationException Payload doesn't match the stored record for the given idempotency key
* @throws IdempotencyItemNotFoundException Exception thrown if no record exists in persistence store with the idempotency key
*/
- public DataRecord getRecord(JsonNode data, Instant now) throws IdempotencyValidationException, IdempotencyItemNotFoundException {
+ public DataRecord getRecord(JsonNode data, Instant now)
+ throws IdempotencyValidationException, IdempotencyItemNotFoundException {
Optional hashedIdempotencyKey = getHashedIdempotencyKey(data);
if (!hashedIdempotencyKey.isPresent()) {
// missing idempotency key => non-idempotent transaction, we do not get the data, simply return nothing
@@ -255,7 +258,9 @@ private Optional getHashedIdempotencyKey(JsonNode data) {
private boolean isMissingIdemPotencyKey(JsonNode data) {
if (data.isContainerNode()) {
- Stream> stream = StreamSupport.stream(Spliterators.spliteratorUnknownSize(data.fields(), Spliterator.ORDERED), false);
+ Stream> stream =
+ StreamSupport.stream(Spliterators.spliteratorUnknownSize(data.fields(), Spliterator.ORDERED),
+ false);
return stream.allMatch(e -> e.getValue().isNull());
}
return data.isNull();
@@ -302,7 +307,9 @@ String generateHash(JsonNode data) {
node = data.decimalValue();
} else if (data.isBoolean()) {
node = data.asBoolean();
- } else node = data; // anything else
+ } else {
+ node = data; // anything else
+ }
MessageDigest hashAlgorithm = getHashAlgorithm();
byte[] digest = hashAlgorithm.digest(node.toString().getBytes(StandardCharsets.UTF_8));
@@ -356,17 +363,20 @@ private long getExpiryEpochSecond(Instant now) {
* @param dataRecord DataRecord to save in cache
*/
private void saveToCache(DataRecord dataRecord) {
- if (!useLocalCache)
+ if (!useLocalCache) {
return;
- if (dataRecord.getStatus().equals(DataRecord.Status.INPROGRESS))
+ }
+ if (dataRecord.getStatus().equals(DataRecord.Status.INPROGRESS)) {
return;
+ }
cache.put(dataRecord.getIdempotencyKey(), dataRecord);
}
private DataRecord retrieveFromCache(String idempotencyKey, Instant now) {
- if (!useLocalCache)
+ if (!useLocalCache) {
return null;
+ }
DataRecord record = cache.get(idempotencyKey);
if (record != null) {
@@ -380,8 +390,9 @@ private DataRecord retrieveFromCache(String idempotencyKey, Instant now) {
}
private void deleteFromCache(String idempotencyKey) {
- if (!useLocalCache)
+ if (!useLocalCache) {
return;
+ }
cache.remove(idempotencyKey);
}
diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/DataRecord.java b/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/DataRecord.java
index 934ec3d09..9af7c6a53 100644
--- a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/DataRecord.java
+++ b/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/DataRecord.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 Amazon.com, Inc. or its affiliates.
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
@@ -11,14 +11,13 @@
* limitations under the License.
*
*/
-package software.amazon.lambda.powertools.idempotency.persistence;
-import software.amazon.lambda.powertools.idempotency.IdempotencyConfig;
+package software.amazon.lambda.powertools.idempotency.persistence;
import java.time.Instant;
import java.util.Objects;
-import java.util.OptionalInt;
import java.util.OptionalLong;
+import software.amazon.lambda.powertools.idempotency.IdempotencyConfig;
/**
* Data Class for idempotency records. This is actually the item that will be stored in the persistence layer.
@@ -26,12 +25,34 @@
public class DataRecord {
private final String idempotencyKey;
private final String status;
+
+ /**
+ * This field is controlling how long the result of the idempotent
+ * event is cached. It is stored in _seconds since epoch_.
+ *
+ * DynamoDB's TTL mechanism is used to remove the record once the
+ * expiry has been reached, and subsequent execution of the request
+ * will be permitted. The user must configure this on their table.
+ */
private final long expiryTimestamp;
private final String responseData;
private final String payloadHash;
+
+ /**
+ * The in-progress field is set to the remaining lambda execution time
+ * when the record is created.
+ * This field is stored in _milliseconds since epoch_.
+ *
+ * This ensures that:
+ *
+ * 1/ other concurrently executing requests are blocked from starting
+ * 2/ if a lambda times out, subsequent requests will be allowed again, despite
+ * the fact that the idempotency record is already in the table
+ */
private final OptionalLong inProgressExpiryTimestamp;
- public DataRecord(String idempotencyKey, Status status, long expiryTimestamp, String responseData, String payloadHash) {
+ public DataRecord(String idempotencyKey, Status status, long expiryTimestamp, String responseData,
+ String payloadHash) {
this.idempotencyKey = idempotencyKey;
this.status = status.toString();
this.expiryTimestamp = expiryTimestamp;
@@ -40,7 +61,8 @@ public DataRecord(String idempotencyKey, Status status, long expiryTimestamp, St
this.inProgressExpiryTimestamp = OptionalLong.empty();
}
- public DataRecord(String idempotencyKey, Status status, long expiryTimestamp, String responseData, String payloadHash, OptionalLong inProgressExpiryTimestamp) {
+ public DataRecord(String idempotencyKey, Status status, long expiryTimestamp, String responseData,
+ String payloadHash, OptionalLong inProgressExpiryTimestamp) {
this.idempotencyKey = idempotencyKey;
this.status = status.toString();
this.expiryTimestamp = expiryTimestamp;
@@ -89,8 +111,12 @@ public String getPayloadHash() {
@Override
public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
DataRecord record = (DataRecord) o;
return expiryTimestamp == record.expiryTimestamp
&& idempotencyKey.equals(record.idempotencyKey)
diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/DynamoDBPersistenceStore.java b/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/DynamoDBPersistenceStore.java
index 47ddf4c5c..d7301b149 100644
--- a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/DynamoDBPersistenceStore.java
+++ b/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/DynamoDBPersistenceStore.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 Amazon.com, Inc. or its affiliates.
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
@@ -11,20 +11,24 @@
* limitations under the License.
*
*/
+
package software.amazon.lambda.powertools.idempotency.persistence;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider;
+import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
+import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption;
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.DynamoDbClientBuilder;
-import software.amazon.awssdk.services.dynamodb.model.*;
import software.amazon.awssdk.utils.StringUtils;
+import software.amazon.lambda.powertools.core.internal.UserAgentConfigurator;
import software.amazon.lambda.powertools.idempotency.Constants;
import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemAlreadyExistsException;
import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemNotFoundException;
+import static software.amazon.lambda.powertools.core.internal.LambdaConstants.AWS_REGION_ENV;
+import static software.amazon.lambda.powertools.core.internal.LambdaConstants.LAMBDA_FUNCTION_NAME_ENV;
+import static software.amazon.lambda.powertools.idempotency.persistence.DataRecord.Status.INPROGRESS;
import java.time.Instant;
import java.util.AbstractMap;
@@ -34,11 +38,13 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;
-import static software.amazon.lambda.powertools.core.internal.LambdaConstants.AWS_LAMBDA_INITIALIZATION_TYPE;
-import static software.amazon.lambda.powertools.core.internal.LambdaConstants.AWS_REGION_ENV;
-import static software.amazon.lambda.powertools.core.internal.LambdaConstants.LAMBDA_FUNCTION_NAME_ENV;
-import static software.amazon.lambda.powertools.core.internal.LambdaConstants.ON_DEMAND;
-import static software.amazon.lambda.powertools.idempotency.persistence.DataRecord.Status.INPROGRESS;
+import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
+import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException;
+import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest;
+import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
+import software.amazon.awssdk.services.dynamodb.model.GetItemResponse;
+import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;
+import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest;
/**
* DynamoDB version of the {@link PersistenceStore}. Will store idempotency data in DynamoDB.
@@ -47,6 +53,7 @@
public class DynamoDBPersistenceStore extends BasePersistenceStore implements PersistenceStore {
private static final Logger LOG = LoggerFactory.getLogger(DynamoDBPersistenceStore.class);
+ public static final String IDEMPOTENCY = "idempotency";
private final String tableName;
private final String keyAttr;
@@ -87,20 +94,12 @@ private DynamoDBPersistenceStore(String tableName,
this.dynamoDbClient = client;
} else {
String idempotencyDisabledEnv = System.getenv().get(Constants.IDEMPOTENCY_DISABLED_ENV);
- if (idempotencyDisabledEnv == null || idempotencyDisabledEnv.equalsIgnoreCase("false")) {
- DynamoDbClientBuilder ddbBuilder = DynamoDbClient.builder()
+ if (idempotencyDisabledEnv == null || "false".equalsIgnoreCase(idempotencyDisabledEnv)) {
+ this.dynamoDbClient = DynamoDbClient.builder()
.httpClient(UrlConnectionHttpClient.builder().build())
- .region(Region.of(System.getenv(AWS_REGION_ENV)));
-
- // AWS_LAMBDA_INITIALIZATION_TYPE has two values on-demand and snap-start
- // when using snap-start mode, the env var creds provider isn't used and causes a fatal error if set
- // fall back to the default provider chain if the mode is anything other than on-demand.
- String initializationType = System.getenv().get(AWS_LAMBDA_INITIALIZATION_TYPE);
- if (initializationType != null && initializationType.equals(ON_DEMAND)) {
- ddbBuilder.credentialsProvider(EnvironmentVariableCredentialsProvider.create());
- }
-
- this.dynamoDbClient = ddbBuilder.build();
+ .overrideConfiguration(ClientOverrideConfiguration.builder().putAdvancedOption(SdkAdvancedClientOption.USER_AGENT_SUFFIX, UserAgentConfigurator.getUserAgent(IDEMPOTENCY)).build())
+ .region(Region.of(System.getenv(AWS_REGION_ENV)))
+ .build();
} else {
// we do not want to create a DynamoDbClient if idempotency is disabled
// null is ok as idempotency won't be called
@@ -109,6 +108,10 @@ private DynamoDBPersistenceStore(String tableName,
}
}
+ public static Builder builder() {
+ return new Builder();
+ }
+
@Override
public DataRecord getRecord(String idempotencyKey) throws IdempotencyItemNotFoundException {
GetItemResponse response = dynamoDbClient.getItem(
@@ -126,6 +129,19 @@ public DataRecord getRecord(String idempotencyKey) throws IdempotencyItemNotFoun
return itemToRecord(response.item());
}
+ /**
+ * Store's the given idempotency record in the DDB store. If there
+ * is an existing record that has expired - either due to the
+ * cache expiry or due to the in_progress_expiry - the record
+ * will be overwritten and the idempotent operation can continue.
+ *
+ * Note: This method writes only expiry and status information - not
+ * the results of the operation itself.
+ *
+ * @param record DataRecord instance to store
+ * @param now
+ * @throws IdempotencyItemAlreadyExistsException
+ */
@Override
public void putRecord(DataRecord record, Instant now) throws IdempotencyItemAlreadyExistsException {
Map item = new HashMap<>(getKey(record.getIdempotencyKey()));
@@ -133,7 +149,9 @@ public void putRecord(DataRecord record, Instant now) throws IdempotencyItemAlre
item.put(this.statusAttr, AttributeValue.builder().s(record.getStatus().toString()).build());
if (record.getInProgressExpiryTimestamp().isPresent()) {
- item.put(this.inProgressExpiryAttr, AttributeValue.builder().n(String.valueOf(record.getInProgressExpiryTimestamp().getAsLong())).build());
+ item.put(this.inProgressExpiryAttr,
+ AttributeValue.builder().n(String.valueOf(record.getInProgressExpiryTimestamp().getAsLong()))
+ .build());
}
if (this.payloadValidationEnabled) {
@@ -151,8 +169,12 @@ public void putRecord(DataRecord record, Instant now) throws IdempotencyItemAlre
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
Map expressionAttributeValues = Stream.of(
- new AbstractMap.SimpleEntry<>(":now", AttributeValue.builder().n(String.valueOf(now.getEpochSecond())).build()),
- new AbstractMap.SimpleEntry<>(":inprogress", AttributeValue.builder().s(INPROGRESS.toString()).build())
+ new AbstractMap.SimpleEntry<>(":now",
+ AttributeValue.builder().n(String.valueOf(now.getEpochSecond())).build()),
+ new AbstractMap.SimpleEntry<>(":now_milliseconds",
+ AttributeValue.builder().n(String.valueOf(now.toEpochMilli())).build()),
+ new AbstractMap.SimpleEntry<>(":inprogress",
+ AttributeValue.builder().s(INPROGRESS.toString()).build())
).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
@@ -160,14 +182,16 @@ public void putRecord(DataRecord record, Instant now) throws IdempotencyItemAlre
PutItemRequest.builder()
.tableName(tableName)
.item(item)
- .conditionExpression("attribute_not_exists(#id) OR #expiry < :now OR (attribute_exists(#in_progress_expiry) AND #in_progress_expiry < :now AND #status = :inprogress)")
+ .conditionExpression(
+ "attribute_not_exists(#id) OR #expiry < :now OR (attribute_exists(#in_progress_expiry) AND #in_progress_expiry < :now_milliseconds AND #status = :inprogress)")
.expressionAttributeNames(expressionAttributeNames)
.expressionAttributeValues(expressionAttributeValues)
.build()
);
} catch (ConditionalCheckFailedException e) {
LOG.debug("Failed to put record for already existing idempotency key: {}", record.getIdempotencyKey());
- throw new IdempotencyItemAlreadyExistsException("Failed to put record for already existing idempotency key: " + record.getIdempotencyKey(), e);
+ throw new IdempotencyItemAlreadyExistsException(
+ "Failed to put record for already existing idempotency key: " + record.getIdempotencyKey(), e);
}
}
@@ -183,15 +207,19 @@ public void updateRecord(DataRecord record) {
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
Map expressionAttributeValues = Stream.of(
- new AbstractMap.SimpleEntry<>(":response_data", AttributeValue.builder().s(record.getResponseData()).build()),
- new AbstractMap.SimpleEntry<>(":expiry", AttributeValue.builder().n(String.valueOf(record.getExpiryTimestamp())).build()),
- new AbstractMap.SimpleEntry<>(":status", AttributeValue.builder().s(record.getStatus().toString()).build()))
+ new AbstractMap.SimpleEntry<>(":response_data",
+ AttributeValue.builder().s(record.getResponseData()).build()),
+ new AbstractMap.SimpleEntry<>(":expiry",
+ AttributeValue.builder().n(String.valueOf(record.getExpiryTimestamp())).build()),
+ new AbstractMap.SimpleEntry<>(":status",
+ AttributeValue.builder().s(record.getStatus().toString()).build()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
if (payloadValidationEnabled) {
updateExpression += ", #validation_key = :validation_key";
expressionAttributeNames.put("#validation_key", this.validationAttr);
- expressionAttributeValues.put(":validation_key", AttributeValue.builder().s(record.getPayloadHash()).build());
+ expressionAttributeValues.put(":validation_key",
+ AttributeValue.builder().s(record.getPayloadHash()).build());
}
dynamoDbClient.updateItem(UpdateItemRequest.builder()
@@ -241,16 +269,14 @@ private DataRecord itemToRecord(Map item) {
// data and validation payload may be null
AttributeValue data = item.get(this.dataAttr);
AttributeValue validation = item.get(this.validationAttr);
- return new DataRecord(item.get(sortKeyAttr != null ? sortKeyAttr: keyAttr).s(),
+ return new DataRecord(item.get(sortKeyAttr != null ? sortKeyAttr : keyAttr).s(),
DataRecord.Status.valueOf(item.get(this.statusAttr).s()),
Long.parseLong(item.get(this.expiryAttr).n()),
data != null ? data.s() : null,
validation != null ? validation.s() : null,
- item.get(this.inProgressExpiryAttr) != null ? OptionalLong.of(Long.parseLong(item.get(this.inProgressExpiryAttr).n())) : OptionalLong.empty());
- }
-
- public static Builder builder() {
- return new Builder();
+ item.get(this.inProgressExpiryAttr) != null ?
+ OptionalLong.of(Long.parseLong(item.get(this.inProgressExpiryAttr).n())) :
+ OptionalLong.empty());
}
/**
@@ -287,7 +313,8 @@ public DynamoDBPersistenceStore build() {
if (StringUtils.isEmpty(tableName)) {
throw new IllegalArgumentException("Table name is not specified");
}
- return new DynamoDBPersistenceStore(tableName, keyAttr, staticPkValue, sortKeyAttr, expiryAttr, inProgressExpiryAttr, statusAttr, dataAttr, validationAttr, dynamoDbClient);
+ return new DynamoDBPersistenceStore(tableName, keyAttr, staticPkValue, sortKeyAttr, expiryAttr,
+ inProgressExpiryAttr, statusAttr, dataAttr, validationAttr, dynamoDbClient);
}
/**
diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/PersistenceStore.java b/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/PersistenceStore.java
index d199c99b5..c058b592e 100644
--- a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/PersistenceStore.java
+++ b/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/PersistenceStore.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 Amazon.com, Inc. or its affiliates.
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
@@ -11,13 +11,13 @@
* limitations under the License.
*
*/
+
package software.amazon.lambda.powertools.idempotency.persistence;
+import java.time.Instant;
import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemAlreadyExistsException;
import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemNotFoundException;
-import java.time.Instant;
-
/**
* Persistence layer that will store the idempotency result.
* In order to provide another implementation, extends {@link BasePersistenceStore}.
@@ -26,6 +26,7 @@ public interface PersistenceStore {
/**
* Retrieve item from persistence store using idempotency key and return it as a DataRecord instance.
+ *
* @param idempotencyKey the key of the record
* @return DataRecord representation of existing record found in persistence store
* @throws IdempotencyItemNotFoundException Exception thrown if no record exists in persistence store with the idempotency key
@@ -34,6 +35,7 @@ public interface PersistenceStore {
/**
* Add a DataRecord to persistence store if it does not already exist with that key
+ *
* @param record DataRecord instance
* @param now
* @throws IdempotencyItemAlreadyExistsException if a non-expired entry already exists.
@@ -42,12 +44,14 @@ public interface PersistenceStore {
/**
* Update item in persistence store
+ *
* @param record DataRecord instance
*/
void updateRecord(DataRecord record);
/**
* Remove item from persistence store
+ *
* @param idempotencyKey the key of the record
*/
void deleteRecord(String idempotencyKey);
diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/DynamoDBConfig.java b/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/DynamoDBConfig.java
index 38678322c..66ddb53ac 100644
--- a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/DynamoDBConfig.java
+++ b/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/DynamoDBConfig.java
@@ -1,7 +1,24 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
package software.amazon.lambda.powertools.idempotency;
import com.amazonaws.services.dynamodbv2.local.main.ServerRunner;
import com.amazonaws.services.dynamodbv2.local.server.DynamoDBProxyServer;
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.net.URI;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
@@ -9,11 +26,14 @@
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;
+import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition;
+import software.amazon.awssdk.services.dynamodb.model.BillingMode;
+import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest;
+import software.amazon.awssdk.services.dynamodb.model.DescribeTableRequest;
+import software.amazon.awssdk.services.dynamodb.model.DescribeTableResponse;
+import software.amazon.awssdk.services.dynamodb.model.KeySchemaElement;
+import software.amazon.awssdk.services.dynamodb.model.KeyType;
+import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType;
public class DynamoDBConfig {
protected static final String TABLE_NAME = "idempotency_table";
@@ -24,7 +44,7 @@ public class DynamoDBConfig {
public static void setupDynamo() {
int port = getFreePort();
try {
- dynamoProxy = ServerRunner.createServerFromCommandLineArgs(new String[]{
+ dynamoProxy = ServerRunner.createServerFromCommandLineArgs(new String[] {
"-inMemory",
"-port",
Integer.toString(port)
@@ -51,7 +71,8 @@ public static void setupDynamo() {
.billingMode(BillingMode.PAY_PER_REQUEST)
.build());
- DescribeTableResponse response = client.describeTable(DescribeTableRequest.builder().tableName(TABLE_NAME).build());
+ DescribeTableResponse response =
+ client.describeTable(DescribeTableRequest.builder().tableName(TABLE_NAME).build());
if (response == null) {
throw new RuntimeException("Table was not created within expected time");
}
diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/IdempotencyTest.java b/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/IdempotencyTest.java
index a782d9613..c94fec3db 100644
--- a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/IdempotencyTest.java
+++ b/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/IdempotencyTest.java
@@ -1,6 +1,22 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
package software.amazon.lambda.powertools.idempotency;
+import static org.assertj.core.api.Assertions.assertThat;
+
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
import com.amazonaws.services.lambda.runtime.tests.EventLoader;
@@ -11,8 +27,6 @@
import software.amazon.awssdk.services.dynamodb.model.ScanRequest;
import software.amazon.lambda.powertools.idempotency.handlers.IdempotencyFunction;
-import static org.assertj.core.api.Assertions.assertThat;
-
public class IdempotencyTest extends DynamoDBConfig {
@Mock
@@ -27,12 +41,14 @@ void setUp() {
public void endToEndTest() {
IdempotencyFunction function = new IdempotencyFunction(client);
- APIGatewayProxyResponseEvent response = function.handleRequest(EventLoader.loadApiGatewayRestEvent("apigw_event2.json"), context);
+ APIGatewayProxyResponseEvent response =
+ function.handleRequest(EventLoader.loadApiGatewayRestEvent("apigw_event2.json"), context);
assertThat(function.handlerExecuted).isTrue();
function.handlerExecuted = false;
- APIGatewayProxyResponseEvent response2 = function.handleRequest(EventLoader.loadApiGatewayRestEvent("apigw_event2.json"), context);
+ APIGatewayProxyResponseEvent response2 =
+ function.handleRequest(EventLoader.loadApiGatewayRestEvent("apigw_event2.json"), context);
assertThat(function.handlerExecuted).isFalse();
assertThat(response).isEqualTo(response2);
diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyEnabledFunction.java b/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyEnabledFunction.java
index 6c39dc6de..f12edc87e 100644
--- a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyEnabledFunction.java
+++ b/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyEnabledFunction.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 Amazon.com, Inc. or its affiliates.
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
@@ -11,6 +11,7 @@
* limitations under the License.
*
*/
+
package software.amazon.lambda.powertools.idempotency.handlers;
import com.amazonaws.services.lambda.runtime.Context;
diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyFunction.java b/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyFunction.java
index c60336b81..76c36ae9f 100644
--- a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyFunction.java
+++ b/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyFunction.java
@@ -1,9 +1,30 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
package software.amazon.lambda.powertools.idempotency.handlers;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
@@ -13,16 +34,8 @@
import software.amazon.lambda.powertools.idempotency.persistence.DynamoDBPersistenceStore;
import software.amazon.lambda.powertools.utilities.JsonConfig;
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.net.URL;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.stream.Collectors;
-
public class IdempotencyFunction implements RequestHandler {
- private final static Logger LOG = LogManager.getLogger();
+ private final static Logger LOG = LogManager.getLogger(IdempotencyFunction.class);
public boolean handlerExecuted = false;
diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunction.java b/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunction.java
index f3c1bdbc9..34e3eb319 100644
--- a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunction.java
+++ b/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunction.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 Amazon.com, Inc. or its affiliates.
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
@@ -11,6 +11,7 @@
* limitations under the License.
*
*/
+
package software.amazon.lambda.powertools.idempotency.handlers;
import com.amazonaws.services.lambda.runtime.Context;
@@ -38,14 +39,14 @@ public Basket handleRequest(Product input, Context context) {
if (registerContext) {
Idempotency.registerLambdaContext(context);
}
-
+
return createBasket("fake", input);
}
@Idempotent
private Basket createBasket(@IdempotencyKey String magicProduct, Product p) {
called = true;
- Basket b = new Basket(p);
+ Basket b = new Basket(p);
b.add(new Product(0, magicProduct, 0));
return b;
}
diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunctionInternalKey.java b/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunctionInternalKey.java
index 566db6727..3ae500341 100644
--- a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunctionInternalKey.java
+++ b/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunctionInternalKey.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 Amazon.com, Inc. or its affiliates.
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
@@ -11,6 +11,7 @@
* limitations under the License.
*
*/
+
package software.amazon.lambda.powertools.idempotency.handlers;
import com.amazonaws.services.lambda.runtime.Context;
diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunctionInvalid.java b/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunctionInvalid.java
index 4c82bff15..42e438798 100644
--- a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunctionInvalid.java
+++ b/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunctionInvalid.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 Amazon.com, Inc. or its affiliates.
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
@@ -11,6 +11,7 @@
* limitations under the License.
*
*/
+
package software.amazon.lambda.powertools.idempotency.handlers;
import com.amazonaws.services.lambda.runtime.Context;
@@ -33,7 +34,7 @@ public Basket handleRequest(Product input, Context context) {
@Idempotent
private Basket createBasket(String magicProduct, Product p) {
- Basket b = new Basket(p);
+ Basket b = new Basket(p);
b.add(new Product(0, magicProduct, 0));
return b;
}
diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunctionVoid.java b/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunctionVoid.java
index a6b89fc8d..384ed5e86 100644
--- a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunctionVoid.java
+++ b/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunctionVoid.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 Amazon.com, Inc. or its affiliates.
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
@@ -11,6 +11,7 @@
* limitations under the License.
*
*/
+
package software.amazon.lambda.powertools.idempotency.handlers;
import com.amazonaws.services.lambda.runtime.Context;
@@ -28,7 +29,7 @@ public class IdempotencyInternalFunctionVoid implements RequestHandler function.handleRequest(p, context)).isInstanceOf(IdempotencyAlreadyInProgressException.class);
+ assertThatThrownBy(() -> function.handleRequest(p, context)).isInstanceOf(
+ IdempotencyAlreadyInProgressException.class);
}
@Test
- public void secondCall_inProgress_lambdaTimeout_timeoutExpired_shouldThrowInconsistentState() throws JsonProcessingException {
+ public void secondCall_inProgress_lambdaTimeout_timeoutExpired_shouldThrowInconsistentState()
+ throws JsonProcessingException {
// GIVEN
Idempotency.config()
.withPersistenceStore(store)
@@ -195,7 +210,8 @@ public void secondCall_inProgress_lambdaTimeout_timeoutExpired_shouldThrowIncons
Product p = new Product(42, "fake product", 12);
Basket b = new Basket(p);
- OptionalLong timestampInThePast = OptionalLong.of(Instant.now().toEpochMilli() - 100); // timeout expired 100ms ago
+ OptionalLong timestampInThePast =
+ OptionalLong.of(Instant.now().toEpochMilli() - 100); // timeout expired 100ms ago
DataRecord record = new DataRecord(
"42",
DataRecord.Status.INPROGRESS,
@@ -207,7 +223,8 @@ public void secondCall_inProgress_lambdaTimeout_timeoutExpired_shouldThrowIncons
// THEN
IdempotencyEnabledFunction function = new IdempotencyEnabledFunction();
- assertThatThrownBy(() -> function.handleRequest(p, context)).isInstanceOf(IdempotencyInconsistentStateException.class);
+ assertThatThrownBy(() -> function.handleRequest(p, context)).isInstanceOf(
+ IdempotencyInconsistentStateException.class);
}
@Test
@@ -278,7 +295,8 @@ public void idempotencyOnSubMethodAnnotated_firstCall_shouldPutInStore() {
ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(Basket.class);
verify(store).saveSuccess(any(), resultCaptor.capture(), any());
- assertThat(resultCaptor.getValue().getProducts()).contains(basket.getProducts().get(0), new Product(0, "fake", 0));
+ assertThat(resultCaptor.getValue().getProducts()).contains(basket.getProducts().get(0),
+ new Product(0, "fake", 0));
}
@Test
@@ -305,11 +323,13 @@ public void idempotencyOnSubMethodAnnotated_firstCall_contextNotRegistered_shoul
ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(Basket.class);
verify(store).saveSuccess(any(), resultCaptor.capture(), any());
- assertThat(resultCaptor.getValue().getProducts()).contains(basket.getProducts().get(0), new Product(0, "fake", 0));
+ assertThat(resultCaptor.getValue().getProducts()).contains(basket.getProducts().get(0),
+ new Product(0, "fake", 0));
}
@Test
- public void idempotencyOnSubMethodAnnotated_secondCall_notExpired_shouldGetFromStore() throws JsonProcessingException {
+ public void idempotencyOnSubMethodAnnotated_secondCall_notExpired_shouldGetFromStore()
+ throws JsonProcessingException {
// GIVEN
Idempotency.config()
.withPersistenceStore(store)
@@ -354,7 +374,8 @@ public void idempotencyOnSubMethodAnnotated_keyJMESPath_shouldPutInStoreWithKey(
ArgumentCaptor recordCaptor = ArgumentCaptor.forClass(DataRecord.class);
verify(persistenceStore).putRecord(recordCaptor.capture(), any());
// a1d0c6e83f027327d8461063f4ac58a6 = MD5(42)
- assertThat(recordCaptor.getValue().getIdempotencyKey()).isEqualTo("testFunction.createBasket#a1d0c6e83f027327d8461063f4ac58a6");
+ assertThat(recordCaptor.getValue().getIdempotencyKey()).isEqualTo(
+ "testFunction.createBasket#a1d0c6e83f027327d8461063f4ac58a6");
}
@Test
@@ -369,7 +390,8 @@ public void idempotencyOnSubMethodNotAnnotated_shouldThrowException() {
Product p = new Product(42, "fake product", 12);
// THEN
- assertThatThrownBy(() -> function.handleRequest(p, context)).isInstanceOf(IdempotencyConfigurationException.class);
+ assertThatThrownBy(() -> function.handleRequest(p, context)).isInstanceOf(
+ IdempotencyConfigurationException.class);
}
@Test
@@ -384,7 +406,8 @@ public void idempotencyOnSubMethodVoid_shouldThrowException() {
Product p = new Product(42, "fake product", 12);
// THEN
- assertThatThrownBy(() -> function.handleRequest(p, context)).isInstanceOf(IdempotencyConfigurationException.class);
+ assertThatThrownBy(() -> function.handleRequest(p, context)).isInstanceOf(
+ IdempotencyConfigurationException.class);
}
}
diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/internal/cache/LRUCacheTest.java b/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/internal/cache/LRUCacheTest.java
index 3d2f7c7e3..8854be1f2 100644
--- a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/internal/cache/LRUCacheTest.java
+++ b/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/internal/cache/LRUCacheTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 Amazon.com, Inc. or its affiliates.
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
@@ -11,12 +11,13 @@
* limitations under the License.
*
*/
-package software.amazon.lambda.powertools.idempotency.internal.cache;
-import org.junit.jupiter.api.Test;
+package software.amazon.lambda.powertools.idempotency.internal.cache;
import static org.assertj.core.api.Assertions.assertThat;
+import org.junit.jupiter.api.Test;
+
public class LRUCacheTest {
@Test
diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/model/Basket.java b/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/model/Basket.java
index 304fd3810..a17bb8abf 100644
--- a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/model/Basket.java
+++ b/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/model/Basket.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 Amazon.com, Inc. or its affiliates.
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
@@ -11,6 +11,7 @@
* limitations under the License.
*
*/
+
package software.amazon.lambda.powertools.idempotency.model;
import java.util.ArrayList;
@@ -21,19 +22,19 @@
public class Basket {
private List products = new ArrayList<>();
- public List getProducts() {
- return products;
+ public Basket() {
}
- public void setProducts(List products) {
- this.products = products;
+ public Basket(Product... p) {
+ products.addAll(Arrays.asList(p));
}
- public Basket() {
+ public List getProducts() {
+ return products;
}
- public Basket( Product ...p){
- products.addAll(Arrays.asList(p));
+ public void setProducts(List products) {
+ this.products = products;
}
public void add(Product product) {
@@ -42,8 +43,12 @@ public void add(Product product) {
@Override
public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
Basket basket = (Basket) o;
return products.equals(basket.products);
}
diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/model/Product.java b/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/model/Product.java
index 1c66c584d..7fa191d82 100644
--- a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/model/Product.java
+++ b/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/model/Product.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 Amazon.com, Inc. or its affiliates.
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
@@ -11,6 +11,7 @@
* limitations under the License.
*
*/
+
package software.amazon.lambda.powertools.idempotency.model;
import java.util.Objects;
@@ -57,8 +58,12 @@ public void setPrice(double price) {
@Override
public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
Product product = (Product) o;
return id == product.id && Double.compare(product.price, price) == 0 && Objects.equals(name, product.name);
}
diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/BasePersistenceStoreTest.java b/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/BasePersistenceStoreTest.java
index 6b58fa8a5..67bc5aa22 100644
--- a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/BasePersistenceStoreTest.java
+++ b/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/BasePersistenceStoreTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 Amazon.com, Inc. or its affiliates.
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
@@ -11,13 +11,21 @@
* limitations under the License.
*
*/
+
package software.amazon.lambda.powertools.idempotency.persistence;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.tests.EventLoader;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.node.DoubleNode;
import com.fasterxml.jackson.databind.node.TextNode;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.OptionalInt;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import software.amazon.lambda.powertools.idempotency.IdempotencyConfig;
@@ -29,14 +37,6 @@
import software.amazon.lambda.powertools.idempotency.model.Product;
import software.amazon.lambda.powertools.utilities.JsonConfig;
-import java.time.Duration;
-import java.time.Instant;
-import java.time.temporal.ChronoUnit;
-import java.util.OptionalInt;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-
public class BasePersistenceStoreTest {
private DataRecord dr;
@@ -53,7 +53,8 @@ public void setup() {
@Override
public DataRecord getRecord(String idempotencyKey) throws IdempotencyItemNotFoundException {
status = 0;
- return new DataRecord(idempotencyKey, DataRecord.Status.INPROGRESS, Instant.now().plus(3600, ChronoUnit.SECONDS).getEpochSecond(), "Response", validationHash);
+ return new DataRecord(idempotencyKey, DataRecord.Status.INPROGRESS,
+ Instant.now().plus(3600, ChronoUnit.SECONDS).getEpochSecond(), "Response", validationHash);
}
@Override
@@ -84,7 +85,8 @@ public void saveInProgress_defaultConfig() {
persistenceStore.configure(IdempotencyConfig.builder().build(), null);
Instant now = Instant.now();
- persistenceStore.saveInProgress(JsonConfig.get().getObjectMapper().valueToTree(event), now, OptionalInt.empty());
+ persistenceStore.saveInProgress(JsonConfig.get().getObjectMapper().valueToTree(event), now,
+ OptionalInt.empty());
assertThat(dr.getStatus()).isEqualTo(DataRecord.Status.INPROGRESS);
assertThat(dr.getExpiryTimestamp()).isEqualTo(now.plus(3600, ChronoUnit.SECONDS).getEpochSecond());
assertThat(dr.getResponseData()).isNull();
@@ -101,13 +103,15 @@ public void saveInProgress_withRemainingTime() {
int lambdaTimeoutMs = 30000;
Instant now = Instant.now();
- persistenceStore.saveInProgress(JsonConfig.get().getObjectMapper().valueToTree(event), now, OptionalInt.of(lambdaTimeoutMs));
+ persistenceStore.saveInProgress(JsonConfig.get().getObjectMapper().valueToTree(event), now,
+ OptionalInt.of(lambdaTimeoutMs));
assertThat(dr.getStatus()).isEqualTo(DataRecord.Status.INPROGRESS);
assertThat(dr.getExpiryTimestamp()).isEqualTo(now.plus(3600, ChronoUnit.SECONDS).getEpochSecond());
assertThat(dr.getResponseData()).isNull();
assertThat(dr.getIdempotencyKey()).isEqualTo("testFunction#7b40f56c086de5aa91dc467456329ed2");
assertThat(dr.getPayloadHash()).isEqualTo("");
- assertThat(dr.getInProgressExpiryTimestamp().orElse(-1)).isEqualTo(now.plus(lambdaTimeoutMs, ChronoUnit.MILLIS).toEpochMilli());
+ assertThat(dr.getInProgressExpiryTimestamp().orElse(-1)).isEqualTo(
+ now.plus(lambdaTimeoutMs, ChronoUnit.MILLIS).toEpochMilli());
assertThat(status).isEqualTo(1);
}
@@ -119,7 +123,8 @@ public void saveInProgress_jmespath() {
.build(), "myfunc");
Instant now = Instant.now();
- persistenceStore.saveInProgress(JsonConfig.get().getObjectMapper().valueToTree(event), now, OptionalInt.empty());
+ persistenceStore.saveInProgress(JsonConfig.get().getObjectMapper().valueToTree(event), now,
+ OptionalInt.empty());
assertThat(dr.getStatus()).isEqualTo(DataRecord.Status.INPROGRESS);
assertThat(dr.getExpiryTimestamp()).isEqualTo(now.plus(3600, ChronoUnit.SECONDS).getEpochSecond());
assertThat(dr.getResponseData()).isNull();
@@ -136,7 +141,9 @@ public void saveInProgress_jmespath_NotFound_shouldThrowException() {
.withThrowOnNoIdempotencyKey(true) // should throw
.build(), "");
Instant now = Instant.now();
- assertThatThrownBy(() -> persistenceStore.saveInProgress(JsonConfig.get().getObjectMapper().valueToTree(event), now, OptionalInt.empty()))
+ assertThatThrownBy(
+ () -> persistenceStore.saveInProgress(JsonConfig.get().getObjectMapper().valueToTree(event), now,
+ OptionalInt.empty()))
.isInstanceOf(IdempotencyKeyException.class)
.hasMessageContaining("No data found to create a hashed idempotency key");
assertThat(status).isEqualTo(-1);
@@ -149,7 +156,8 @@ public void saveInProgress_jmespath_NotFound_shouldNotPersist() {
.withEventKeyJMESPath("unavailable")
.build(), "");
Instant now = Instant.now();
- persistenceStore.saveInProgress(JsonConfig.get().getObjectMapper().valueToTree(event), now, OptionalInt.empty());
+ persistenceStore.saveInProgress(JsonConfig.get().getObjectMapper().valueToTree(event), now,
+ OptionalInt.empty());
assertThat(dr).isNull();
assertThat(status).isEqualTo(-1);
}
@@ -170,7 +178,9 @@ public void saveInProgress_withLocalCache_NotExpired_ShouldThrowException() {
now.plus(3600, ChronoUnit.SECONDS).getEpochSecond(),
null, null)
);
- assertThatThrownBy(() -> persistenceStore.saveInProgress(JsonConfig.get().getObjectMapper().valueToTree(event), now, OptionalInt.empty()))
+ assertThatThrownBy(
+ () -> persistenceStore.saveInProgress(JsonConfig.get().getObjectMapper().valueToTree(event), now,
+ OptionalInt.empty()))
.isInstanceOf(IdempotencyItemAlreadyExistsException.class);
assertThat(status).isEqualTo(-1);
}
@@ -192,7 +202,8 @@ public void saveInProgress_withLocalCache_Expired_ShouldRemoveFromCache() {
now.minus(3, ChronoUnit.SECONDS).getEpochSecond(),
null, null)
);
- persistenceStore.saveInProgress(JsonConfig.get().getObjectMapper().valueToTree(event), now, OptionalInt.empty());
+ persistenceStore.saveInProgress(JsonConfig.get().getObjectMapper().valueToTree(event), now,
+ OptionalInt.empty());
assertThat(dr.getStatus()).isEqualTo(DataRecord.Status.INPROGRESS);
assertThat(cache).isEmpty();
assertThat(status).isEqualTo(1);
@@ -250,7 +261,8 @@ public void saveSuccess_withCacheEnabled_shouldSaveInCache() throws JsonProcessi
//
@Test
- public void getRecord_shouldReturnRecordFromPersistence() throws IdempotencyItemNotFoundException, IdempotencyValidationException {
+ public void getRecord_shouldReturnRecordFromPersistence()
+ throws IdempotencyItemNotFoundException, IdempotencyValidationException {
APIGatewayProxyRequestEvent event = EventLoader.loadApiGatewayRestEvent("apigw_event.json");
LRUCache cache = new LRUCache<>(2);
persistenceStore.configure(IdempotencyConfig.builder().build(), "myfunc", cache);
@@ -264,7 +276,8 @@ public void getRecord_shouldReturnRecordFromPersistence() throws IdempotencyItem
}
@Test
- public void getRecord_cacheEnabledNotExpired_shouldReturnRecordFromCache() throws IdempotencyItemNotFoundException, IdempotencyValidationException {
+ public void getRecord_cacheEnabledNotExpired_shouldReturnRecordFromCache()
+ throws IdempotencyItemNotFoundException, IdempotencyValidationException {
APIGatewayProxyRequestEvent event = EventLoader.loadApiGatewayRestEvent("apigw_event.json");
LRUCache cache = new LRUCache<>(2);
persistenceStore.configure(IdempotencyConfig.builder()
@@ -287,7 +300,8 @@ public void getRecord_cacheEnabledNotExpired_shouldReturnRecordFromCache() throw
}
@Test
- public void getRecord_cacheEnabledExpired_shouldReturnRecordFromPersistence() throws IdempotencyItemNotFoundException, IdempotencyValidationException {
+ public void getRecord_cacheEnabledExpired_shouldReturnRecordFromPersistence()
+ throws IdempotencyItemNotFoundException, IdempotencyValidationException {
APIGatewayProxyRequestEvent event = EventLoader.loadApiGatewayRestEvent("apigw_event.json");
LRUCache cache = new LRUCache<>(2);
persistenceStore.configure(IdempotencyConfig.builder()
@@ -314,14 +328,15 @@ public void getRecord_cacheEnabledExpired_shouldReturnRecordFromPersistence() th
public void getRecord_invalidPayload_shouldThrowValidationException() {
APIGatewayProxyRequestEvent event = EventLoader.loadApiGatewayRestEvent("apigw_event.json");
persistenceStore.configure(IdempotencyConfig.builder()
- .withEventKeyJMESPath("powertools_json(body).id")
- .withPayloadValidationJMESPath("powertools_json(body).message")
- .build(),
+ .withEventKeyJMESPath("powertools_json(body).id")
+ .withPayloadValidationJMESPath("powertools_json(body).message")
+ .build(),
"myfunc");
this.validationHash = "different hash"; // "Lambda rocks" ==> 70c24d88041893f7fbab4105b76fd9e1
- assertThatThrownBy(() -> persistenceStore.getRecord(JsonConfig.get().getObjectMapper().valueToTree(event), Instant.now()))
+ assertThatThrownBy(
+ () -> persistenceStore.getRecord(JsonConfig.get().getObjectMapper().valueToTree(event), Instant.now()))
.isInstanceOf(IdempotencyValidationException.class);
}
@@ -348,7 +363,8 @@ public void deleteRecord_cacheEnabled_shouldDeleteRecordFromCache() {
.withUseLocalCache(true).build(), null, cache);
cache.put("testFunction#7b40f56c086de5aa91dc467456329ed2",
- new DataRecord("testFunction#7b40f56c086de5aa91dc467456329ed2", DataRecord.Status.COMPLETED, 123, null, null));
+ new DataRecord("testFunction#7b40f56c086de5aa91dc467456329ed2", DataRecord.Status.COMPLETED, 123, null,
+ null));
persistenceStore.deleteRecord(JsonConfig.get().getObjectMapper().valueToTree(event), new ArithmeticException());
assertThat(status).isEqualTo(3);
assertThat(cache).isEmpty();
@@ -385,7 +401,8 @@ public void generateHashDouble_shouldGenerateMd5ofDouble() {
@Test
public void generateHashString_withSha256Algorithm_shouldGenerateSha256ofString() {
persistenceStore.configure(IdempotencyConfig.builder().withHashFunction("SHA-256").build(), null);
- String expectedHash = "e6139efa88ef3337e901e826e6f327337f414860fb499d9f26eefcff21d719af"; // SHA-256(Lambda rocks)
+ String expectedHash =
+ "e6139efa88ef3337e901e826e6f327337f414860fb499d9f26eefcff21d719af"; // SHA-256(Lambda rocks)
String generatedHash = persistenceStore.generateHash(new TextNode("Lambda rocks"));
assertThat(generatedHash).isEqualTo(expectedHash);
}
diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/DynamoDBPersistenceStoreTest.java b/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/DynamoDBPersistenceStoreTest.java
index 86e35cd33..b19cebfe1 100644
--- a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/DynamoDBPersistenceStoreTest.java
+++ b/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/DynamoDBPersistenceStoreTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 Amazon.com, Inc. or its affiliates.
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
@@ -11,28 +11,39 @@
* limitations under the License.
*
*/
+
package software.amazon.lambda.powertools.idempotency.persistence;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junitpioneer.jupiter.SetEnvironmentVariable;
-import software.amazon.awssdk.services.dynamodb.model.*;
+import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition;
+import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
+import software.amazon.awssdk.services.dynamodb.model.BillingMode;
+import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest;
+import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest;
+import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest;
+import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
+import software.amazon.awssdk.services.dynamodb.model.KeySchemaElement;
+import software.amazon.awssdk.services.dynamodb.model.KeyType;
+import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;
+import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType;
+import software.amazon.awssdk.services.dynamodb.model.ScanRequest;
import software.amazon.lambda.powertools.idempotency.Constants;
import software.amazon.lambda.powertools.idempotency.DynamoDBConfig;
import software.amazon.lambda.powertools.idempotency.IdempotencyConfig;
import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemAlreadyExistsException;
import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemNotFoundException;
-import java.time.Instant;
-import java.time.temporal.ChronoUnit;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-
/**
* These test are using DynamoDBLocal and sqlite, see https://nickolasfisher.com/blog/Configuring-an-In-Memory-DynamoDB-instance-with-Java-for-Integration-Testing
* NOTE: on a Mac with Apple Chipset, you need to use the Oracle JDK x86 64-bit
@@ -51,7 +62,8 @@ public void putRecord_shouldCreateRecordInDynamoDB() throws IdempotencyItemAlrea
dynamoDBPersistenceStore.putRecord(new DataRecord("key", DataRecord.Status.COMPLETED, expiry, null, null), now);
key = Collections.singletonMap("id", AttributeValue.builder().s("key").build());
- Map item = client.getItem(GetItemRequest.builder().tableName(TABLE_NAME).key(key).build()).item();
+ Map item =
+ client.getItem(GetItemRequest.builder().tableName(TABLE_NAME).key(key).build()).item();
assertThat(item).isNotNull();
assertThat(item.get("status").s()).isEqualTo("COMPLETED");
assertThat(item.get("expiration").n()).isEqualTo(String.valueOf(expiry));
@@ -81,7 +93,8 @@ public void putRecord_shouldCreateRecordInDynamoDB_IfPreviousExpired() {
), now);
// THEN: an item is inserted
- Map itemInDb = client.getItem(GetItemRequest.builder().tableName(TABLE_NAME).key(key).build()).item();
+ Map itemInDb =
+ client.getItem(GetItemRequest.builder().tableName(TABLE_NAME).key(key).build()).item();
assertThat(itemInDb).isNotNull();
assertThat(itemInDb.get("status").s()).isEqualTo("INPROGRESS");
assertThat(itemInDb.get("expiration").n()).isEqualTo(String.valueOf(expiry2));
@@ -95,7 +108,7 @@ public void putRecord_shouldCreateRecordInDynamoDB_IfLambdaWasInProgressAndTimed
Map item = new HashMap<>(key);
Instant now = Instant.now();
long expiry = now.plus(30, ChronoUnit.SECONDS).getEpochSecond();
- long progressExpiry = now.minus(30, ChronoUnit.SECONDS).getEpochSecond();
+ long progressExpiry = now.minus(30, ChronoUnit.SECONDS).toEpochMilli();
item.put("expiration", AttributeValue.builder().n(String.valueOf(expiry)).build());
item.put("status", AttributeValue.builder().s(DataRecord.Status.INPROGRESS.toString()).build());
item.put("data", AttributeValue.builder().s("Fake Data").build());
@@ -113,7 +126,8 @@ public void putRecord_shouldCreateRecordInDynamoDB_IfLambdaWasInProgressAndTimed
), now);
// THEN: an item is inserted
- Map itemInDb = client.getItem(GetItemRequest.builder().tableName(TABLE_NAME).key(key).build()).item();
+ Map itemInDb =
+ client.getItem(GetItemRequest.builder().tableName(TABLE_NAME).key(key).build()).item();
assertThat(itemInDb).isNotNull();
assertThat(itemInDb.get("status").s()).isEqualTo("INPROGRESS");
assertThat(itemInDb.get("expiration").n()).isEqualTo(String.valueOf(expiry2));
@@ -144,7 +158,8 @@ public void putRecord_shouldThrowIdempotencyItemAlreadyExistsException_IfRecordA
).isInstanceOf(IdempotencyItemAlreadyExistsException.class);
// THEN: item was not updated, retrieve the initial one
- Map itemInDb = client.getItem(GetItemRequest.builder().tableName(TABLE_NAME).key(key).build()).item();
+ Map itemInDb =
+ client.getItem(GetItemRequest.builder().tableName(TABLE_NAME).key(key).build()).item();
assertThat(itemInDb).isNotNull();
assertThat(itemInDb.get("status").s()).isEqualTo("COMPLETED");
assertThat(itemInDb.get("expiration").n()).isEqualTo(String.valueOf(expiry));
@@ -152,14 +167,14 @@ public void putRecord_shouldThrowIdempotencyItemAlreadyExistsException_IfRecordA
}
@Test
- public void putRecord_shouldThrowIdempotencyItemAlreadyExistsException_IfRecordAlreadyExistAndProgressNotExpiredAfterLambdaTimedOut() {
+ public void putRecord_shouldBlockUpdate_IfRecordAlreadyExistAndProgressNotExpiredAfterLambdaTimedOut() {
key = Collections.singletonMap("id", AttributeValue.builder().s("key").build());
// GIVEN: Insert a fake item with same id
Map item = new HashMap<>(key);
Instant now = Instant.now();
long expiry = now.plus(30, ChronoUnit.SECONDS).getEpochSecond(); // not expired
- long progressExpiry = now.plus(30, ChronoUnit.SECONDS).getEpochSecond(); // not expired
+ long progressExpiry = now.plus(30, ChronoUnit.SECONDS).toEpochMilli(); // not expired
item.put("expiration", AttributeValue.builder().n(String.valueOf(expiry)).build());
item.put("status", AttributeValue.builder().s(DataRecord.Status.INPROGRESS.toString()).build());
item.put("data", AttributeValue.builder().s("Fake Data").build());
@@ -172,13 +187,14 @@ public void putRecord_shouldThrowIdempotencyItemAlreadyExistsException_IfRecordA
new DataRecord("key",
DataRecord.Status.INPROGRESS,
expiry2,
- null,
+ "Fake Data 2",
null
- ), now)
- ).isInstanceOf(IdempotencyItemAlreadyExistsException.class);
+ ), now))
+ .isInstanceOf(IdempotencyItemAlreadyExistsException.class);
// THEN: item was not updated, retrieve the initial one
- Map itemInDb = client.getItem(GetItemRequest.builder().tableName(TABLE_NAME).key(key).build()).item();
+ Map itemInDb =
+ client.getItem(GetItemRequest.builder().tableName(TABLE_NAME).key(key).build()).item();
assertThat(itemInDb).isNotNull();
assertThat(itemInDb.get("status").s()).isEqualTo("INPROGRESS");
assertThat(itemInDb.get("expiration").n()).isEqualTo(String.valueOf(expiry));
@@ -217,7 +233,8 @@ public void getRecord_shouldReturnExistingRecord() throws IdempotencyItemNotFoun
@Test
public void getRecord_shouldThrowException_whenRecordIsAbsent() {
- assertThatThrownBy(() -> dynamoDBPersistenceStore.getRecord("key")).isInstanceOf(IdempotencyItemNotFoundException.class);
+ assertThatThrownBy(() -> dynamoDBPersistenceStore.getRecord("key")).isInstanceOf(
+ IdempotencyItemNotFoundException.class);
}
//
@@ -237,7 +254,8 @@ public void updateRecord_shouldUpdateRecord() {
item.put("status", AttributeValue.builder().s(DataRecord.Status.INPROGRESS.toString()).build());
client.putItem(PutItemRequest.builder().tableName(TABLE_NAME).item(item).build());
// enable payload validation
- dynamoDBPersistenceStore.configure(IdempotencyConfig.builder().withPayloadValidationJMESPath("path").build(), null);
+ dynamoDBPersistenceStore.configure(IdempotencyConfig.builder().withPayloadValidationJMESPath("path").build(),
+ null);
// WHEN
expiry = now.plus(3600, ChronoUnit.SECONDS).getEpochSecond();
@@ -245,7 +263,8 @@ public void updateRecord_shouldUpdateRecord() {
dynamoDBPersistenceStore.updateRecord(record);
// THEN
- Map itemInDb = client.getItem(GetItemRequest.builder().tableName(TABLE_NAME).key(key).build()).item();
+ Map itemInDb =
+ client.getItem(GetItemRequest.builder().tableName(TABLE_NAME).key(key).build()).item();
assertThat(itemInDb.get("status").s()).isEqualTo("COMPLETED");
assertThat(itemInDb.get("expiration").n()).isEqualTo(String.valueOf(expiry));
assertThat(itemInDb.get("data").s()).isEqualTo("Fake result");
@@ -290,8 +309,10 @@ public void endToEndWithCustomAttrNamesAndSortKey() throws IdempotencyItemNotFou
KeySchemaElement.builder().keyType(KeyType.RANGE).attributeName("sortkey").build()
)
.attributeDefinitions(
- AttributeDefinition.builder().attributeName("key").attributeType(ScalarAttributeType.S).build(),
- AttributeDefinition.builder().attributeName("sortkey").attributeType(ScalarAttributeType.S).build()
+ AttributeDefinition.builder().attributeName("key").attributeType(ScalarAttributeType.S)
+ .build(),
+ AttributeDefinition.builder().attributeName("sortkey").attributeType(ScalarAttributeType.S)
+ .build()
)
.billingMode(BillingMode.PAY_PER_REQUEST)
.build());
@@ -323,7 +344,8 @@ public void endToEndWithCustomAttrNamesAndSortKey() throws IdempotencyItemNotFou
customKey.put("key", AttributeValue.builder().s("pk").build());
customKey.put("sortkey", AttributeValue.builder().s("mykey").build());
- Map itemInDb = client.getItem(GetItemRequest.builder().tableName(TABLE_NAME_CUSTOM).key(customKey).build()).item();
+ Map itemInDb =
+ client.getItem(GetItemRequest.builder().tableName(TABLE_NAME_CUSTOM).key(customKey).build()).item();
// GET
DataRecord recordInDb = persistenceStore.getRecord("mykey");
diff --git a/powertools-sqs/pom.xml b/powertools-large-messages/pom.xml
similarity index 54%
rename from powertools-sqs/pom.xml
rename to powertools-large-messages/pom.xml
index 881c35a78..a9ded60c5 100644
--- a/powertools-sqs/pom.xml
+++ b/powertools-large-messages/pom.xml
@@ -1,23 +1,36 @@
+
+
4.0.0
- powertools-sqs
- jar
+A suite of utilities for AWS Lambda Functions that makes handling large messages in SQS and SNS easier.
- powertools-parentsoftware.amazon.lambda
- 2.0.0-SNAPSHOT
+ powertools-parent
+ 1.17.0-SNAPSHOT
- Powertools for AWS Lambda (Java) library SQS
-
- A suite of utilities for AWS Lambda Functions that makes tracing with AWS X-Ray, structured logging and creating custom metrics asynchronously easier.
-
- https://aws.amazon.com/lambda/
+ powertools-large-messages
+ jar
+
+ Powertools for AWS Lambda (Java) library Large messages
+
GitHub Issueshttps://github.com/aws-powertools/powertools-lambda-java/issues
@@ -45,34 +58,48 @@
software.amazon.lambdapowertools-core
+
+ org.aspectj
+ aspectjrt
+ com.amazonaws
- aws-lambda-java-core
+ aws-lambda-java-eventssoftware.amazon.payloadoffloadingpayloadoffloading-common
- com.amazonaws
- aws-lambda-java-events
+ com.fasterxml.jackson.core
+ jackson-coresoftware.amazon.awssdk
- sqs
+ sdk-coresoftware.amazon.awssdk
- s3
+ utils
- com.fasterxml.jackson.core
- jackson-databind
+ software.amazon.awssdk
+ s3
+
+
+ software.amazon.awssdk
+ netty-nio-client
+
+
+ software.amazon.awssdk
+ apache-client
+
+
-
- org.aspectj
- aspectjrt
+ software.amazon.awssdk
+ url-connection-client
+ ${aws.sdk.version}
@@ -87,23 +114,23 @@
test
- org.junit.jupiter
- junit-jupiter-params
+ org.junit-pioneer
+ junit-pioneertest
- org.apache.commons
- commons-lang3
+ org.mockito
+ mockito-coretestorg.mockito
- mockito-core
+ mockito-inlinetest
- org.aspectj
- aspectjweaver
+ org.apache.commons
+ commons-lang3test
@@ -111,6 +138,29 @@
assertj-coretest
+
+ org.apache.logging.log4j
+ log4j-slf4j2-impl
+ test
+
+
+
+
+ maven-surefire-plugin
+ ${maven-surefire-plugin.version}
+
+
+ eu-central-1
+
+
+
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+
+
+
+
\ No newline at end of file
diff --git a/powertools-large-messages/src/main/java/software/amazon/lambda/powertools/largemessages/LargeMessage.java b/powertools-large-messages/src/main/java/software/amazon/lambda/powertools/largemessages/LargeMessage.java
new file mode 100644
index 000000000..758d7eb45
--- /dev/null
+++ b/powertools-large-messages/src/main/java/software/amazon/lambda/powertools/largemessages/LargeMessage.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package software.amazon.lambda.powertools.largemessages;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ *
Use this annotation to handle large messages (> 256 KB) from SQS or SNS.
+ * When large messages are sent to an SQS Queue or SNS Topic, they are offloaded to S3 and only a reference is passed in the message/record.
+ *
+ *
{@code @LargeMessage} automatically retrieves and deletes messages
+ * which have been offloaded to S3 using the {@code amazon-sqs-java-extended-client-lib} or {@code amazon-sns-java-extended-client-lib}
+ * client libraries.
+ *
+ *
This version of the {@code @LargeMessage} is compatible with version
+ * 1.1.0+ of {@code amazon-sqs-java-extended-client-lib} / {@code amazon-sns-java-extended-client-lib}.
+ *
+ *
Put this annotation on a method where the first parameter is either a {@link com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage} or {@link com.amazonaws.services.lambda.runtime.events.SNSEvent.SNSRecord}.
+ *
+ * SQS:
+ *
+ * @LargeMessage
+ * private void processRawMessage(SQSMessage sqsMessage, Context context) {
+ * // sqsMessage.getBody() will contain the content of the S3 Object
+ * }
+ *
+ * SNS:
+ *
+ * @LargeMessage
+ * private void processMessage(SNSRecord snsRecord) {
+ * // snsRecord.getSNS().getMessage() will contain the content of the S3 Object
+ * }
+ *
+ *
+ *
+ *
To disable the deletion of S3 objects, you can configure the {@code deleteS3Object} option to false (default is true):
+ *
+ * @LargeMessage(deleteS3Object = false)
+ *
+ *
+ *
+ *
Note 1: Retrieving payloads and deleting objects from S3 will increase the duration of the
+ * Lambda function.
+ *
Note 2: Make sure to configure your function with enough memory to be able to retrieve S3 objects.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface LargeMessage {
+
+ /**
+ * Specify if S3 objects must be deleted after being processed (default = true)
+ Alternatively you might consider using S3 lifecycle policies to remove the payloads automatically after a period of time.
+ */
+ boolean deleteS3Object() default true;
+}
diff --git a/powertools-large-messages/src/main/java/software/amazon/lambda/powertools/largemessages/LargeMessageConfig.java b/powertools-large-messages/src/main/java/software/amazon/lambda/powertools/largemessages/LargeMessageConfig.java
new file mode 100644
index 000000000..fb8ea9b15
--- /dev/null
+++ b/powertools-large-messages/src/main/java/software/amazon/lambda/powertools/largemessages/LargeMessageConfig.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package software.amazon.lambda.powertools.largemessages;
+
+import static software.amazon.lambda.powertools.core.internal.LambdaConstants.AWS_REGION_ENV;
+
+import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.S3ClientBuilder;
+
+/**
+ * Singleton instance for Large Message Config. We need this to provide a way to customize the S3 client configuration used by the annotation.
+ *
+ * Optional: Use it in your Lambda constructor to pass a custom {@link S3Client} to the {@link software.amazon.lambda.powertools.largemessages.internal.LargeMessageProcessor}
+ *
+ * If you don't use this, a default S3Client will be created.
+ *
+ */
+public class LargeMessageConfig {
+
+ private static final LargeMessageConfig INSTANCE = new LargeMessageConfig();
+ private S3Client s3Client;
+
+ private LargeMessageConfig() {
+ }
+
+ /**
+ * Retrieve the singleton instance (you generally don't need to use this one, used internally by the library)
+ *
+ * @return the singleton instance
+ */
+ public static LargeMessageConfig get() {
+ return INSTANCE;
+ }
+
+ /**
+ * Initialize the singleton instance
+ *
+ * @return the singleton instance
+ */
+ public static LargeMessageConfig init() {
+ return INSTANCE;
+ }
+
+ public void withS3Client(S3Client s3Client) {
+ if (this.s3Client == null) {
+ this.s3Client = s3Client;
+ }
+ }
+
+ // For tests purpose
+ void resetS3Client() {
+ this.s3Client = null;
+ }
+
+ // Getter needs to initialize if not done with setter
+ public S3Client getS3Client() {
+ if (this.s3Client == null) {
+ S3ClientBuilder s3ClientBuilder = S3Client.builder()
+ .httpClient(UrlConnectionHttpClient.builder().build())
+ .region(Region.of(System.getenv(AWS_REGION_ENV)));
+ this.s3Client = s3ClientBuilder.build();
+ }
+ return this.s3Client;
+ }
+}
diff --git a/powertools-large-messages/src/main/java/software/amazon/lambda/powertools/largemessages/LargeMessageProcessingException.java b/powertools-large-messages/src/main/java/software/amazon/lambda/powertools/largemessages/LargeMessageProcessingException.java
new file mode 100644
index 000000000..20b19230a
--- /dev/null
+++ b/powertools-large-messages/src/main/java/software/amazon/lambda/powertools/largemessages/LargeMessageProcessingException.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package software.amazon.lambda.powertools.largemessages;
+
+/**
+ * Exception that occurs when the utility fails to retrieve the content from S3
+ */
+public class LargeMessageProcessingException extends RuntimeException {
+ public LargeMessageProcessingException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public LargeMessageProcessingException(String message) {
+ super(message);
+ }
+}
diff --git a/powertools-large-messages/src/main/java/software/amazon/lambda/powertools/largemessages/internal/LargeMessageAspect.java b/powertools-large-messages/src/main/java/software/amazon/lambda/powertools/largemessages/internal/LargeMessageAspect.java
new file mode 100644
index 000000000..861193203
--- /dev/null
+++ b/powertools-large-messages/src/main/java/software/amazon/lambda/powertools/largemessages/internal/LargeMessageAspect.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package software.amazon.lambda.powertools.largemessages.internal;
+
+import java.util.Optional;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import software.amazon.lambda.powertools.largemessages.LargeMessage;
+
+/**
+ * Handle {@link LargeMessage} annotations.
+ */
+@Aspect
+public class LargeMessageAspect {
+
+ private static final Logger LOG = LoggerFactory.getLogger(LargeMessageAspect.class);
+
+ @SuppressWarnings({"EmptyMethod"})
+ @Pointcut("@annotation(largeMessage)")
+ public void callAt(LargeMessage largeMessage) {
+ }
+
+ @Around(value = "callAt(largeMessage) && execution(@LargeMessage * *.*(..))", argNames = "pjp,largeMessage")
+ public Object around(ProceedingJoinPoint pjp,
+ LargeMessage largeMessage) throws Throwable {
+ Object[] proceedArgs = pjp.getArgs();
+
+ // we need a message to process
+ if (proceedArgs.length == 0) {
+ LOG.warn("@LargeMessage annotation is placed on a method without any message to process, proceeding");
+ return pjp.proceed(proceedArgs);
+ }
+
+ Object message = proceedArgs[0];
+ Optional> largeMessageProcessor = LargeMessageProcessorFactory.get(message);
+
+ if (!largeMessageProcessor.isPresent()) {
+ LOG.warn("@LargeMessage annotation is placed on a method with unsupported message type [{}], proceeding", message.getClass());
+ return pjp.proceed(proceedArgs);
+ }
+
+ return largeMessageProcessor.get().process(pjp, largeMessage.deleteS3Object());
+ }
+
+}
diff --git a/powertools-large-messages/src/main/java/software/amazon/lambda/powertools/largemessages/internal/LargeMessageProcessor.java b/powertools-large-messages/src/main/java/software/amazon/lambda/powertools/largemessages/internal/LargeMessageProcessor.java
new file mode 100644
index 000000000..f0e89e631
--- /dev/null
+++ b/powertools-large-messages/src/main/java/software/amazon/lambda/powertools/largemessages/internal/LargeMessageProcessor.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package software.amazon.lambda.powertools.largemessages.internal;
+
+import static java.lang.String.format;
+
+import java.nio.charset.StandardCharsets;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import software.amazon.awssdk.core.exception.SdkException;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.lambda.powertools.largemessages.LargeMessageConfig;
+import software.amazon.lambda.powertools.largemessages.LargeMessageProcessingException;
+import software.amazon.payloadoffloading.S3BackedPayloadStore;
+import software.amazon.payloadoffloading.S3Dao;
+
+/**
+ * Abstract processor for Large Messages. Handle the download from S3 and replace the actual S3 pointer with the content
+ * of the S3 Object leveraging the payloadoffloading library.
+ *
+ * @param any message type that support Large Messages with S3 pointers
+ * ({@link com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage} and {@link com.amazonaws.services.lambda.runtime.events.SNSEvent.SNSRecord} at the moment)
+ */
+abstract class LargeMessageProcessor {
+ protected static final String RESERVED_ATTRIBUTE_NAME = "ExtendedPayloadSize";
+ private static final Logger LOG = LoggerFactory.getLogger(LargeMessageProcessor.class);
+
+ private final S3Client s3Client = LargeMessageConfig.get().getS3Client();
+ private final S3BackedPayloadStore payloadStore = new S3BackedPayloadStore(new S3Dao(s3Client), "DUMMY");
+
+ public Object process(ProceedingJoinPoint pjp, boolean deleteS3Object) throws Throwable {
+ Object[] proceedArgs = pjp.getArgs();
+ T message = (T) proceedArgs[0];
+
+ if (!isLargeMessage(message)) {
+ LOG.warn("Not a large message, proceeding");
+ return pjp.proceed(proceedArgs);
+ }
+
+ String payloadPointer = getMessageContent(message);
+ if (payloadPointer == null) {
+ LOG.warn("No content in the message, proceeding");
+ return pjp.proceed(proceedArgs);
+ }
+ // legacy attribute (sqs only)
+ payloadPointer = payloadPointer.replace("com.amazon.sqs.javamessaging.MessageS3Pointer", "software.amazon.payloadoffloading.PayloadS3Pointer");
+
+ if (LOG.isInfoEnabled()) {
+ LOG.info("Large message [{}]: retrieving content from S3", getMessageId(message));
+ }
+
+ String s3ObjectContent = getS3ObjectContent(payloadPointer);
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Large message [{}] retrieved in S3 [{}]: {}KB", getMessageId(message), payloadPointer,
+ s3ObjectContent.getBytes(StandardCharsets.UTF_8).length / 1024);
+ }
+
+ updateMessageContent(message, s3ObjectContent);
+ removeLargeMessageAttributes(message);
+
+ Object response = pjp.proceed(proceedArgs);
+
+ if (deleteS3Object) {
+ if (LOG.isInfoEnabled()) {
+ LOG.info("Large message [{}]: deleting object from S3", getMessageId(message));
+ }
+ deleteS3Object(payloadPointer);
+ }
+
+ return response;
+ }
+
+ /**
+ * Retrieve the message id
+ *
+ * @param message the message itself
+ * @return the id of the message (String format)
+ */
+ protected abstract String getMessageId(T message);
+
+ /**
+ * Retrieve the content of the message (ex: body of an SQSMessage)
+ *
+ * @param message the message itself
+ * @return the content of the message (String format)
+ */
+ protected abstract String getMessageContent(T message);
+
+ /**
+ * Update the message content of the message (ex: body of an SQSMessage)
+ *
+ * @param message the message itself
+ * @param messageContent the new content of the message (String format)
+ */
+ protected abstract void updateMessageContent(T message, String messageContent);
+
+ /**
+ * Check if the message is actually a large message (indicator in message attributes)
+ *
+ * @param message the message itself
+ * @return true if the message is a large message
+ */
+ protected abstract boolean isLargeMessage(T message);
+
+ /**
+ * Remove the large message indicator (in message attributes)
+ *
+ * @param message the message itself
+ */
+ protected abstract void removeLargeMessageAttributes(T message);
+
+ private String getS3ObjectContent(String payloadPointer) {
+ try {
+ return payloadStore.getOriginalPayload(payloadPointer);
+ } catch (SdkException e) {
+ throw new LargeMessageProcessingException(format("Failed processing S3 record [%s]", payloadPointer), e);
+ }
+ }
+
+ private void deleteS3Object(String payloadPointer) {
+ try {
+ payloadStore.deleteOriginalPayload(payloadPointer);
+ } catch (SdkException e) {
+ throw new LargeMessageProcessingException(format("Failed deleting S3 record [%s]", payloadPointer), e);
+ }
+ }
+
+}
diff --git a/powertools-large-messages/src/main/java/software/amazon/lambda/powertools/largemessages/internal/LargeMessageProcessorFactory.java b/powertools-large-messages/src/main/java/software/amazon/lambda/powertools/largemessages/internal/LargeMessageProcessorFactory.java
new file mode 100644
index 000000000..26c33738a
--- /dev/null
+++ b/powertools-large-messages/src/main/java/software/amazon/lambda/powertools/largemessages/internal/LargeMessageProcessorFactory.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package software.amazon.lambda.powertools.largemessages.internal;
+
+import com.amazonaws.services.lambda.runtime.events.SNSEvent.SNSRecord;
+import com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage;
+import java.util.Optional;
+
+class LargeMessageProcessorFactory {
+
+ private LargeMessageProcessorFactory() {
+ // not intended to be instantiated
+ }
+
+ public static Optional> get(Object message) {
+ if (message instanceof SQSMessage) {
+ return Optional.of(new LargeSQSMessageProcessor());
+ } else if (message instanceof SNSRecord) {
+ return Optional.of(new LargeSNSMessageProcessor());
+ } else {
+ return Optional.empty();
+ }
+ }
+}
diff --git a/powertools-large-messages/src/main/java/software/amazon/lambda/powertools/largemessages/internal/LargeSNSMessageProcessor.java b/powertools-large-messages/src/main/java/software/amazon/lambda/powertools/largemessages/internal/LargeSNSMessageProcessor.java
new file mode 100644
index 000000000..1ed7f5eaa
--- /dev/null
+++ b/powertools-large-messages/src/main/java/software/amazon/lambda/powertools/largemessages/internal/LargeSNSMessageProcessor.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package software.amazon.lambda.powertools.largemessages.internal;
+
+import com.amazonaws.services.lambda.runtime.events.SNSEvent.MessageAttribute;
+import com.amazonaws.services.lambda.runtime.events.SNSEvent.SNSRecord;
+import java.util.HashMap;
+import java.util.Map;
+
+class LargeSNSMessageProcessor extends LargeMessageProcessor {
+
+ @Override
+ protected String getMessageId(SNSRecord message) {
+ return message.getSNS().getMessageId();
+ }
+
+ @Override
+ protected String getMessageContent(SNSRecord message) {
+ return message.getSNS().getMessage();
+ }
+
+ @Override
+ protected void updateMessageContent(SNSRecord message, String messageContent) {
+ message.getSNS().setMessage(messageContent);
+ }
+
+ @Override
+ protected boolean isLargeMessage(SNSRecord message) {
+ Map msgAttributes = message.getSNS().getMessageAttributes();
+ return msgAttributes != null && msgAttributes.containsKey(RESERVED_ATTRIBUTE_NAME);
+ }
+
+ @Override
+ protected void removeLargeMessageAttributes(SNSRecord message) {
+ // message.getSNS().getMessageAttributes() does not support remove operation, copy to new map
+ Map newAttributes = new HashMap<>(message.getSNS().getMessageAttributes());
+ newAttributes.remove(RESERVED_ATTRIBUTE_NAME);
+ message.getSNS().setMessageAttributes(newAttributes);
+ }
+}
diff --git a/powertools-large-messages/src/main/java/software/amazon/lambda/powertools/largemessages/internal/LargeSQSMessageProcessor.java b/powertools-large-messages/src/main/java/software/amazon/lambda/powertools/largemessages/internal/LargeSQSMessageProcessor.java
new file mode 100644
index 000000000..18c99e300
--- /dev/null
+++ b/powertools-large-messages/src/main/java/software/amazon/lambda/powertools/largemessages/internal/LargeSQSMessageProcessor.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package software.amazon.lambda.powertools.largemessages.internal;
+
+import com.amazonaws.services.lambda.runtime.events.SQSEvent.MessageAttribute;
+import com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import software.amazon.awssdk.utils.BinaryUtils;
+import software.amazon.awssdk.utils.Md5Utils;
+
+class LargeSQSMessageProcessor extends LargeMessageProcessor {
+
+ private static final Logger LOG = LoggerFactory.getLogger(LargeSQSMessageProcessor.class);
+ private static final String LEGACY_RESERVED_ATTRIBUTE_NAME = "SQSLargePayloadSize";
+ private static final int INTEGER_SIZE_IN_BYTES = 4;
+ private static final byte STRING_TYPE_FIELD_INDEX = 1;
+ private static final byte BINARY_TYPE_FIELD_INDEX = 2;
+ private static final byte STRING_LIST_TYPE_FIELD_INDEX = 3;
+ private static final byte BINARY_LIST_TYPE_FIELD_INDEX = 4;
+
+ @Override
+ protected String getMessageId(SQSMessage message) {
+ return message.getMessageId();
+ }
+
+ @Override
+ protected String getMessageContent(SQSMessage message) {
+ return message.getBody();
+ }
+
+ @Override
+ protected void updateMessageContent(SQSMessage message, String messageContent) {
+ message.setBody(messageContent);
+ // we update the MD5 digest so it doesn't look tempered
+ message.setMd5OfBody(calculateMessageBodyMd5(messageContent).orElse(message.getMd5OfBody()));
+ }
+
+ @Override
+ protected boolean isLargeMessage(SQSMessage message) {
+ Map msgAttributes = message.getMessageAttributes();
+ return msgAttributes != null && (msgAttributes.containsKey(RESERVED_ATTRIBUTE_NAME) || msgAttributes.containsKey(LEGACY_RESERVED_ATTRIBUTE_NAME));
+ }
+
+ @Override
+ protected void removeLargeMessageAttributes(SQSMessage message) {
+ // message.getMessageAttributes() does not support remove operation, copy to new map
+ Map newAttributes = new HashMap<>(message.getMessageAttributes());
+ newAttributes.remove(RESERVED_ATTRIBUTE_NAME);
+ newAttributes.remove(LEGACY_RESERVED_ATTRIBUTE_NAME);
+ message.setMessageAttributes(newAttributes);
+ // we update the MD5 digest so it doesn't look tempered
+ message.setMd5OfMessageAttributes(calculateMessageAttributesMd5(newAttributes).orElse(message.getMd5OfMessageAttributes()));
+ }
+
+ /**
+ * Compute the MD5 of the message body.
+ * Inspired from {@code software.amazon.awssdk.services.sqs.internal.MessageMD5ChecksumInterceptor}.
+ * package protected for testing purpose.
+ *
+ * @param messageBody body of the SQS Message
+ * @return the MD5 digest of the SQS Message body (or empty in case of error)
+ */
+ static Optional calculateMessageBodyMd5(String messageBody) {
+ byte[] expectedMd5;
+ try {
+ expectedMd5 = Md5Utils.computeMD5Hash(messageBody.getBytes(StandardCharsets.UTF_8));
+ } catch (Exception e) {
+ LOG.warn("Unable to calculate the MD5 hash of the message body. ", e);
+ return Optional.empty();
+ }
+ return Optional.of(BinaryUtils.toHex(expectedMd5));
+ }
+
+ /**
+ * Compute the MD5 of the message attributes.
+ * Inspired from {@code software.amazon.awssdk.services.sqs.internal.MessageMD5ChecksumInterceptor}.
+ * package protected for testing purpose.
+ *
+ * @param messageAttributes attributes of the SQS Message
+ * @return the MD5 digest of the SQS Message attributes (or empty in case of error)
+ */
+ @SuppressWarnings("squid:S4790") // MD5 algorithm is used by SQS, we must use MD5
+ static Optional calculateMessageAttributesMd5(final Map messageAttributes) {
+ List sortedAttributeNames = new ArrayList<>(messageAttributes.keySet());
+ Collections.sort(sortedAttributeNames);
+
+ MessageDigest md5Digest;
+ try {
+ md5Digest = MessageDigest.getInstance("MD5");
+
+ for (String attrName : sortedAttributeNames) {
+ MessageAttribute attrValue = messageAttributes.get(attrName);
+
+ // Encoded Name
+ updateLengthAndBytes(md5Digest, attrName);
+
+ // Encoded Type
+ updateLengthAndBytes(md5Digest, attrValue.getDataType());
+
+ // Encoded Value
+ if (attrValue.getStringValue() != null) {
+ md5Digest.update(STRING_TYPE_FIELD_INDEX);
+ updateLengthAndBytes(md5Digest, attrValue.getStringValue());
+ } else if (attrValue.getBinaryValue() != null) {
+ md5Digest.update(BINARY_TYPE_FIELD_INDEX);
+ updateLengthAndBytes(md5Digest, attrValue.getBinaryValue());
+ } else if (attrValue.getStringListValues() != null &&
+ attrValue.getStringListValues().size() > 0) {
+ md5Digest.update(STRING_LIST_TYPE_FIELD_INDEX);
+ for (String strListMember : attrValue.getStringListValues()) {
+ updateLengthAndBytes(md5Digest, strListMember);
+ }
+ } else if (attrValue.getBinaryListValues() != null &&
+ attrValue.getBinaryListValues().size() > 0) {
+ md5Digest.update(BINARY_LIST_TYPE_FIELD_INDEX);
+ for (ByteBuffer byteListMember : attrValue.getBinaryListValues()) {
+ updateLengthAndBytes(md5Digest, byteListMember);
+ }
+ }
+ }
+ } catch (Exception e) {
+ LOG.warn("Unable to calculate the MD5 hash of the message attributes. ", e);
+ return Optional.empty();
+ }
+
+ return Optional.of(BinaryUtils.toHex(md5Digest.digest()));
+ }
+
+ /**
+ * Update the digest using a sequence of bytes that consists of the length (in 4 bytes) of the
+ * input String and the actual utf8-encoded byte values.
+ */
+ private static void updateLengthAndBytes(MessageDigest digest, String str) {
+ byte[] utf8Encoded = str.getBytes(StandardCharsets.UTF_8);
+ ByteBuffer lengthBytes = ByteBuffer.allocate(INTEGER_SIZE_IN_BYTES).putInt(utf8Encoded.length);
+ digest.update(lengthBytes.array());
+ digest.update(utf8Encoded);
+ }
+
+ /**
+ * Update the digest using a sequence of bytes that consists of the length (in 4 bytes) of the
+ * input ByteBuffer and all the bytes it contains.
+ */
+ private static void updateLengthAndBytes(MessageDigest digest, ByteBuffer binaryValue) {
+ ByteBuffer readOnlyBuffer = binaryValue.asReadOnlyBuffer();
+ int size = readOnlyBuffer.remaining();
+ ByteBuffer lengthBytes = ByteBuffer.allocate(INTEGER_SIZE_IN_BYTES).putInt(size);
+ digest.update(lengthBytes.array());
+ digest.update(readOnlyBuffer);
+ }
+}
diff --git a/powertools-large-messages/src/test/java/software/amazon/lambda/powertools/largemessages/LargeMessageConfigTest.java b/powertools-large-messages/src/test/java/software/amazon/lambda/powertools/largemessages/LargeMessageConfigTest.java
new file mode 100644
index 000000000..b6bcaf6b5
--- /dev/null
+++ b/powertools-large-messages/src/test/java/software/amazon/lambda/powertools/largemessages/LargeMessageConfigTest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package software.amazon.lambda.powertools.largemessages;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.s3.S3Client;
+
+public class LargeMessageConfigTest {
+
+ @BeforeEach
+ public void setup() {
+ LargeMessageConfig.get().resetS3Client();
+ }
+
+ @AfterEach
+ public void tearDown() {
+ LargeMessageConfig.get().resetS3Client();
+ }
+
+ @Test
+ public void singleton_shouldNotChangeWhenCalledMultipleTimes() {
+ LargeMessageConfig.init().withS3Client(S3Client.builder().region(Region.US_EAST_1).build());
+ LargeMessageConfig config = LargeMessageConfig.get();
+
+ LargeMessageConfig.init().withS3Client(null);
+ LargeMessageConfig config2 = LargeMessageConfig.get();
+
+ assertThat(config2).isEqualTo(config);
+ }
+
+ @Test
+ public void singletonWithDefaultClient_shouldNotChangeWhenCalledMultipleTimes() {
+ S3Client s3Client = LargeMessageConfig.get().getS3Client();
+
+ LargeMessageConfig.init().withS3Client(S3Client.create());
+ S3Client s3Client2 = LargeMessageConfig.get().getS3Client();
+
+ assertThat(s3Client2).isEqualTo(s3Client);
+ }
+}
diff --git a/powertools-large-messages/src/test/java/software/amazon/lambda/powertools/largemessages/internal/LargeMessageAspectTest.java b/powertools-large-messages/src/test/java/software/amazon/lambda/powertools/largemessages/internal/LargeMessageAspectTest.java
new file mode 100644
index 000000000..95dfd445a
--- /dev/null
+++ b/powertools-large-messages/src/test/java/software/amazon/lambda/powertools/largemessages/internal/LargeMessageAspectTest.java
@@ -0,0 +1,333 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package software.amazon.lambda.powertools.largemessages.internal;
+
+import static java.lang.String.format;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+import static org.mockito.MockitoAnnotations.openMocks;
+import static software.amazon.lambda.powertools.largemessages.internal.LargeSQSMessageProcessor.calculateMessageAttributesMd5;
+import static software.amazon.lambda.powertools.largemessages.internal.LargeSQSMessageProcessor.calculateMessageBodyMd5;
+
+import com.amazonaws.services.lambda.runtime.Context;
+import com.amazonaws.services.lambda.runtime.events.KinesisEvent.KinesisEventRecord;
+import com.amazonaws.services.lambda.runtime.events.SNSEvent;
+import com.amazonaws.services.lambda.runtime.events.SNSEvent.SNS;
+import com.amazonaws.services.lambda.runtime.events.SNSEvent.SNSRecord;
+import com.amazonaws.services.lambda.runtime.events.SQSEvent.MessageAttribute;
+import com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage;
+import java.io.ByteArrayInputStream;
+import java.lang.reflect.Field;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Consumer;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import software.amazon.awssdk.core.ResponseInputStream;
+import software.amazon.awssdk.http.AbortableInputStream;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
+import software.amazon.awssdk.services.s3.model.GetObjectRequest;
+import software.amazon.awssdk.services.s3.model.GetObjectResponse;
+import software.amazon.awssdk.services.s3.model.S3Exception;
+import software.amazon.lambda.powertools.largemessages.LargeMessage;
+import software.amazon.lambda.powertools.largemessages.LargeMessageConfig;
+import software.amazon.lambda.powertools.largemessages.LargeMessageProcessingException;
+
+public class LargeMessageAspectTest {
+
+ private static final String BIG_MSG = "A biiiiiiiig message";
+ private static final String BIG_MSG_MD5 = "919ebd392d8cb7161f95cb612a903d42";
+
+ private static final String BUCKET_NAME = "bucketname";
+ private static final String BUCKET_KEY = "c71eb2ae-37e0-4265-8909-32f4153faddf";
+
+ private static final String BIG_MESSAGE_BODY = "[\"software.amazon.payloadoffloading.PayloadS3Pointer\", {\"s3BucketName\":\"" + BUCKET_NAME + "\", \"s3Key\":\"" + BUCKET_KEY + "\"}]";
+
+ @Mock
+ private S3Client s3Client;
+ @Mock
+ private Context context;
+
+ @BeforeEach
+ public void init() throws NoSuchFieldException, IllegalAccessException {
+ openMocks(this);
+ setupContext();
+ // need to clean the s3Client with introspection (singleton)
+ Field client = LargeMessageConfig.class.getDeclaredField("s3Client");
+ client.setAccessible(true);
+ client.set(LargeMessageConfig.get(), null);
+ LargeMessageConfig.init().withS3Client(s3Client);
+ }
+
+ @LargeMessage
+ private String processSQSMessage(SQSMessage sqsMessage, Context context) {
+ return sqsMessage.getBody();
+ }
+
+ @LargeMessage
+ private String processSQSMessageWithMd5Checks(SQSMessage transformedMessage, String initialBodyMD5, String initialAttributesMD5) {
+ assertThat(transformedMessage.getMd5OfBody()).isNotEqualTo(initialBodyMD5);
+ assertThat(transformedMessage.getMd5OfBody()).isEqualTo(BIG_MSG_MD5);
+
+ assertThat(transformedMessage.getMessageAttributes()).hasSize(3);
+
+ assertThat(transformedMessage.getMd5OfMessageAttributes()).isNotEqualTo(initialAttributesMD5);
+ return transformedMessage.getBody();
+ }
+
+ @LargeMessage
+ private String processSNSMessageWithoutContext(SNSRecord snsRecord) {
+ return snsRecord.getSNS().getMessage();
+ }
+
+ @LargeMessage(deleteS3Object = false)
+ private String processSQSMessageNoDelete(SQSMessage sqsMessage, Context context) {
+ return sqsMessage.getBody();
+ }
+
+ @LargeMessage
+ private String processKinesisMessage(KinesisEventRecord kinesisEventRecord) {
+ return kinesisEventRecord.getEventID();
+ }
+
+ @LargeMessage
+ private String processNoMessage() {
+ return "Hello World";
+ }
+
+ @Test
+ public void testLargeSQSMessageWithDefaultDeletion() {
+ // given
+ when(s3Client.getObject(any(GetObjectRequest.class))).thenReturn(s3ObjectWithLargeMessage());
+ SQSMessage sqsMessage = sqsMessageWithBody(BIG_MESSAGE_BODY, true);
+
+ // when
+ String message = processSQSMessage(sqsMessage, context);
+
+ // then
+ assertThat(message).isEqualTo(BIG_MSG);
+ ArgumentCaptor delete = ArgumentCaptor.forClass(DeleteObjectRequest.class);
+ verify(s3Client).deleteObject(delete.capture());
+ Assertions.assertThat(delete.getValue())
+ .satisfies((Consumer) deleteObjectRequest -> {
+ assertThat(deleteObjectRequest.bucket())
+ .isEqualTo(BUCKET_NAME);
+
+ assertThat(deleteObjectRequest.key())
+ .isEqualTo(BUCKET_KEY);
+ });
+ }
+
+ @Test
+ public void testLargeSQSMessage_shouldChangeMd5OfBodyAndAttributes() {
+ // given
+ when(s3Client.getObject(any(GetObjectRequest.class))).thenReturn(s3ObjectWithLargeMessage());
+
+ MessageAttribute stringListAttribute = new MessageAttribute();
+ stringListAttribute.setStringListValues(Collections.singletonList("customAttributeValue"));
+ stringListAttribute.setDataType("StringList");
+
+ MessageAttribute binAttribute = new MessageAttribute();
+ binAttribute.setBinaryValue(ByteBuffer.wrap("customAttributeValue".getBytes(StandardCharsets.UTF_8)));
+ binAttribute.setDataType("Binary");
+
+ MessageAttribute listBinAttribute = new MessageAttribute();
+ listBinAttribute.setBinaryListValues(Collections.singletonList(ByteBuffer.wrap("customAttributeValue".getBytes(StandardCharsets.UTF_8))));
+ listBinAttribute.setDataType("BinaryList");
+
+ Map attrs = new HashMap<>();
+ attrs.put("stringListAttribute", stringListAttribute);
+ attrs.put("binAttribute", binAttribute);
+ attrs.put("listBinAttribute", listBinAttribute);
+ SQSMessage sqsMessage = sqsMessageWithBody(BIG_MESSAGE_BODY, true, attrs);
+
+ // when
+ String message = processSQSMessageWithMd5Checks(sqsMessage, sqsMessage.getMd5OfBody(), sqsMessage.getMd5OfMessageAttributes());
+
+ // then
+ assertThat(message).isEqualTo(BIG_MSG);
+ }
+
+ @Test
+ public void testLargeSNSMessageWithDefaultDeletion() {
+ // given
+ when(s3Client.getObject(any(GetObjectRequest.class))).thenReturn(s3ObjectWithLargeMessage());
+ SNSRecord snsRecord = snsRecordWithMessage(BIG_MESSAGE_BODY, true);
+
+ //when
+ String message = processSNSMessageWithoutContext(snsRecord);
+
+ // then
+ assertThat(message).isEqualTo(BIG_MSG);
+ ArgumentCaptor delete = ArgumentCaptor.forClass(DeleteObjectRequest.class);
+ verify(s3Client).deleteObject(delete.capture());
+ Assertions.assertThat(delete.getValue())
+ .satisfies((Consumer) deleteObjectRequest -> {
+ assertThat(deleteObjectRequest.bucket())
+ .isEqualTo(BUCKET_NAME);
+
+ assertThat(deleteObjectRequest.key())
+ .isEqualTo(BUCKET_KEY);
+ });
+ }
+
+ @Test
+ public void testLargeSQSMessageWithNoDeletion_shouldNotDelete() {
+ // given
+ when(s3Client.getObject(any(GetObjectRequest.class))).thenReturn(s3ObjectWithLargeMessage());
+ SQSMessage sqsMessage = sqsMessageWithBody(BIG_MESSAGE_BODY, true);
+
+ // when
+ String message = processSQSMessageNoDelete(sqsMessage, context);
+
+ // then
+ assertThat(message).isEqualTo(BIG_MSG);
+ verify(s3Client, never()).deleteObject(any(DeleteObjectRequest.class));
+ }
+
+ @Test
+ public void testKinesisMessage_shouldProceedWithoutS3() {
+ // given
+ KinesisEventRecord kinesisEventRecord = new KinesisEventRecord();
+ kinesisEventRecord.setEventID("kinesis_id1234567890");
+
+ // when
+ String message = processKinesisMessage(kinesisEventRecord);
+
+ // then
+ assertThat(message).isEqualTo("kinesis_id1234567890");
+ verifyNoInteractions(s3Client);
+ }
+
+ @Test
+ public void testNoMessage_shouldProceedWithoutS3() {
+ // when
+ String message = processNoMessage();
+
+ // then
+ assertThat(message).isEqualTo("Hello World");
+ verifyNoInteractions(s3Client);
+ }
+
+ @Test
+ public void testSmallMessage_shouldProceedWithoutS3() {
+ // given
+ SQSMessage sqsMessage = sqsMessageWithBody("This is small message", false);
+
+ // when
+ String message = processSQSMessage(sqsMessage, context);
+
+ // then
+ assertThat(message)
+ .isEqualTo("This is small message");
+ verifyNoInteractions(s3Client);
+ }
+
+ @Test
+ public void testNullMessage_shouldProceedWithoutS3() {
+ // given
+ SQSMessage sqsMessage = sqsMessageWithBody(null, true);
+
+ // when
+ String message = processSQSMessage(sqsMessage, context);
+
+ // then
+ assertThat(message).isNull();
+ verifyNoInteractions(s3Client);
+ }
+
+ @Test
+ public void testGetS3ObjectException_shouldThrowLargeMessageProcessingException() {
+ // given
+ when(s3Client.getObject(any(GetObjectRequest.class))).thenThrow(S3Exception.create("Permission denied", new Exception("User is not allowed to access bucket " + BUCKET_NAME)));
+ SQSMessage sqsMessage = sqsMessageWithBody(BIG_MESSAGE_BODY, true);
+
+ // when / then
+ assertThatThrownBy(() -> processSQSMessage(sqsMessage, context))
+ .isInstanceOf(LargeMessageProcessingException.class)
+ .hasMessage(format("Failed processing S3 record [%s]", BIG_MESSAGE_BODY));
+ }
+
+ @Test
+ public void testDeleteS3ObjectException_shouldThrowLargeMessageProcessingException() {
+ // given
+ when(s3Client.getObject(any(GetObjectRequest.class))).thenReturn(s3ObjectWithLargeMessage());
+ when(s3Client.deleteObject(any(DeleteObjectRequest.class))).thenThrow(S3Exception.create("Permission denied", new Exception("User is not allowed to access bucket " + BUCKET_NAME)));
+ SQSMessage sqsMessage = sqsMessageWithBody(BIG_MESSAGE_BODY, true);
+
+ // when / then
+ assertThatThrownBy(() -> processSQSMessage(sqsMessage, context))
+ .isInstanceOf(LargeMessageProcessingException.class)
+ .hasMessage(format("Failed deleting S3 record [%s]", BIG_MESSAGE_BODY));
+ }
+
+ private ResponseInputStream s3ObjectWithLargeMessage() {
+ return new ResponseInputStream<>(GetObjectResponse.builder().build(), AbortableInputStream.create(new ByteArrayInputStream(BIG_MSG.getBytes())));
+ }
+
+ private SQSMessage sqsMessageWithBody(String messageBody, boolean largeMessage) {
+ return sqsMessageWithBody(messageBody, largeMessage, null);
+ }
+
+ private SQSMessage sqsMessageWithBody(String messageBody, boolean largeMessage, Map optionalAttributes) {
+ SQSMessage sqsMessage = new SQSMessage();
+ sqsMessage.setBody(messageBody);
+ if (messageBody != null) {
+ sqsMessage.setMd5OfBody(calculateMessageBodyMd5(messageBody).orElseThrow(() -> new RuntimeException("Unable to md5 body " + messageBody)));
+ }
+
+ if (largeMessage) {
+ Map attributeMap = new HashMap<>();
+ if (optionalAttributes != null) {
+ attributeMap.putAll(optionalAttributes);
+ }
+ MessageAttribute payloadAttribute = new MessageAttribute();
+ payloadAttribute.setStringValue("300450");
+ payloadAttribute.setDataType("Number");
+ attributeMap.put(LargeMessageProcessor.RESERVED_ATTRIBUTE_NAME, payloadAttribute);
+
+ sqsMessage.setMessageAttributes(attributeMap);
+ sqsMessage.setMd5OfMessageAttributes(calculateMessageAttributesMd5(attributeMap).orElseThrow(() -> new RuntimeException("Unable to md5 attributes " + attributeMap)));
+ }
+ return sqsMessage;
+ }
+
+ private SNSRecord snsRecordWithMessage(String messageBody, boolean largeMessage) {
+ SNS sns = new SNS().withMessage(messageBody);
+ if (largeMessage) {
+ sns.setMessageAttributes(Collections.singletonMap(LargeMessageProcessor.RESERVED_ATTRIBUTE_NAME, new SNSEvent.MessageAttribute()));
+ }
+ return new SNSRecord().withSns(sns);
+ }
+
+ private void setupContext() {
+ when(context.getFunctionName()).thenReturn("testFunction");
+ when(context.getInvokedFunctionArn()).thenReturn("testArn");
+ when(context.getFunctionVersion()).thenReturn("1");
+ when(context.getMemoryLimitInMB()).thenReturn(1024);
+ }
+}
diff --git a/powertools-large-messages/src/test/java/software/amazon/lambda/powertools/largemessages/internal/LargeMessageProcessorFactoryTest.java b/powertools-large-messages/src/test/java/software/amazon/lambda/powertools/largemessages/internal/LargeMessageProcessorFactoryTest.java
new file mode 100644
index 000000000..3011c8189
--- /dev/null
+++ b/powertools-large-messages/src/test/java/software/amazon/lambda/powertools/largemessages/internal/LargeMessageProcessorFactoryTest.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package software.amazon.lambda.powertools.largemessages.internal;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.amazonaws.services.lambda.runtime.events.KinesisEvent;
+import com.amazonaws.services.lambda.runtime.events.SNSEvent;
+import com.amazonaws.services.lambda.runtime.events.SQSEvent;
+import org.junit.jupiter.api.Test;
+
+public class LargeMessageProcessorFactoryTest {
+
+ @Test
+ public void createLargeSQSMessageProcessor() {
+ assertThat(LargeMessageProcessorFactory.get(new SQSEvent.SQSMessage()))
+ .isPresent()
+ .get()
+ .isInstanceOf(LargeSQSMessageProcessor.class);
+ }
+
+ @Test
+ public void createLargeSNSMessageProcessor() {
+ assertThat(LargeMessageProcessorFactory.get(new SNSEvent.SNSRecord()))
+ .isPresent()
+ .get()
+ .isInstanceOf(LargeSNSMessageProcessor.class);
+ }
+
+ @Test
+ public void createUnknownMessageProcessor() {
+ assertThat(LargeMessageProcessorFactory.get(new KinesisEvent.KinesisEventRecord())).isNotPresent();
+ }
+}
diff --git a/powertools-logging/pom.xml b/powertools-logging/pom.xml
index 32e3d33f6..e13a88e57 100644
--- a/powertools-logging/pom.xml
+++ b/powertools-logging/pom.xml
@@ -1,4 +1,18 @@
+
+
@@ -127,4 +141,12 @@
+
+
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+
+
+
\ No newline at end of file
diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/CorrelationIdPathConstants.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/CorrelationIdPathConstants.java
index e0d24b8a5..ce43c9aa0 100644
--- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/CorrelationIdPathConstants.java
+++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/CorrelationIdPathConstants.java
@@ -1,3 +1,17 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
package software.amazon.lambda.powertools.logging;
/**
@@ -11,7 +25,7 @@ public class CorrelationIdPathConstants {
/**
* To use when function is expecting API Gateway HTTP API Request event
*/
- public static final String API_GATEWAY_HTTP = "/requestContext/requestId";
+ public static final String API_GATEWAY_HTTP = "/requestContext/requestId";
/**
* To use when function is expecting Application Load balancer Request event
*/
@@ -19,5 +33,5 @@ public class CorrelationIdPathConstants {
/**
* To use when function is expecting Event Bridge Request event
*/
- public static final String EVENT_BRIDGE = "/id";
+ public static final String EVENT_BRIDGE = "/id";
}
diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/Logging.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/Logging.java
index b86b800b7..9932eb700 100644
--- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/Logging.java
+++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/Logging.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 Amazon.com, Inc. or its affiliates.
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
@@ -11,6 +11,7 @@
* limitations under the License.
*
*/
+
package software.amazon.lambda.powertools.logging;
import java.lang.annotation.ElementType;
@@ -70,7 +71,8 @@
/**
* Json Pointer path to extract correlation id from.
- * @see
+ *
+ * @see
*/
String correlationIdPath() default "";
diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/LoggingUtils.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/LoggingUtils.java
index f23e274d4..6e11573cc 100644
--- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/LoggingUtils.java
+++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/LoggingUtils.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 Amazon.com, Inc. or its affiliates.
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
@@ -11,18 +11,18 @@
* limitations under the License.
*
*/
+
package software.amazon.lambda.powertools.logging;
-import java.util.Map;
+import static java.util.Arrays.asList;
import com.fasterxml.jackson.databind.ObjectMapper;
+import java.util.Map;
import org.apache.logging.log4j.ThreadContext;
-import static java.util.Arrays.asList;
-
/**
* A class of helper functions to add additional functionality to Logging.
- *
+ *