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". +![](docs/media/intellij_checkstyle_1.png) + +3. Select the code you've created (module, package, class) and reformat code: `⌘⌥L` (macOS), or `Ctrl+Alt+L` (Windows/Linux): +![](docs/media/intellij_checkstyle_2.png) + +4. Apply the reformat, optimize imports, rearrange and cleanup to your code and only to java files: +![](docs/media/intellij_checkstyle_3.png) + +#### 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.aspectj aspectj-maven-plugin 1.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.0 1.8 1.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.aspectj aspectj-maven-plugin 1.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.0 1.8 1.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.aspectj aspectj-maven-plugin 1.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.0 1.8 1.8 @@ -55,7 +103,6 @@ times with the same parameters**. This makes idempotent operations safe to retry software.amazon.lambda powertools-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.aspectj aspectj-maven-plugin 1.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.0 1.8 1.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-idempotency powertools-examples-parameters powertools-examples-serialization - powertools-examples-sqs powertools-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-cloudformation jar - Powertools for AWS Lambda (Java) library Examples - SQS + + AWS Lambda Powertools for Java library Examples - CloudFormation 2.20.0 1.8 1.8 true + 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.amazonaws aws-lambda-java-core - 1.2.2 + ${lambda.core.version} com.amazonaws aws-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.lambda powertools-logging - - software.amazon.lambda - powertools-sqs - @@ -165,10 +192,6 @@ software.amazon.lambda powertools-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. + *

+ * You can test the endpoint like this: + * *

      *     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"}'
      * 
@@ -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 { - @Override - public String process(SQSMessage message) { - log.info("Processing message with id {}", message.getMessageId()); - - int nextInt = random.nextInt(100); - - if(nextInt <= 10) { - log.info("Randomly picked message with id {} as business validation failure.", message.getMessageId()); - throw new IllegalArgumentException("Failed business validation. No point of retrying. Move me to DLQ." + message.getMessageId()); - } - - if(nextInt > 90) { - log.info("Randomly picked message with id {} as intermittent failure.", message.getMessageId()); - throw new RuntimeException("Failed due to intermittent issue. Will be sent back for retry." + message.getMessageId()); - } - - return "Success"; - } - } -} \ No newline at end of file diff --git a/examples/powertools-examples-sqs/template.yaml b/examples/powertools-examples-sqs/template.yaml deleted file mode 100644 index 50327de18..000000000 --- a/examples/powertools-examples-sqs/template.yaml +++ /dev/null @@ -1,148 +0,0 @@ -AWSTemplateFormatVersion: '2010-09-09' -Transform: AWS::Serverless-2016-10-31 -Description: > - sqs batch processing demo - -Globals: - Function: - Timeout: 20 - Runtime: java11 - MemorySize: 512 - Tracing: Active - Environment: - Variables: - # Powertools for AWS Lambda (Java) env vars: https://docs.powertools.aws.dev/lambda/java/#environment-variables - POWERTOOLS_LOG_LEVEL: INFO - POWERTOOLS_LOGGER_SAMPLE_RATE: 0.1 - POWERTOOLS_LOGGER_LOG_EVENT: true - -Resources: - CustomerKey: - Type: AWS::KMS::Key - Properties: - Description: KMS key for encrypted queues - Enabled: true - KeyPolicy: - Version: '2012-10-17' - Statement: - - Sid: Enable IAM User Permissions - Effect: Allow - Principal: - AWS: !Sub 'arn:aws:iam::${AWS::AccountId}:root' - Action: 'kms:*' - Resource: '*' - - Sid: Allow use of the key - Effect: Allow - Principal: - Service: lambda.amazonaws.com - Action: - - kms:Decrypt - - kms:GenerateDataKey - Resource: '*' - - CustomerKeyAlias: - Type: AWS::KMS::Alias - Properties: - AliasName: alias/sqs-key - TargetKeyId: !Ref CustomerKey - - DemoDlqSqsQueue: - Type: AWS::SQS::Queue - Properties: - KmsMasterKeyId: !Ref CustomerKey - - DemoSqsQueue: - Type: AWS::SQS::Queue - Properties: - RedrivePolicy: - deadLetterTargetArn: - Fn::GetAtt: - - "DemoDlqSqsQueue" - - "Arn" - maxReceiveCount: 2 - KmsMasterKeyId: !Ref CustomerKey - - DemoSQSSenderFunction: - Type: AWS::Serverless::Function - Properties: - CodeUri: . - Handler: org.demo.sqs.SqsMessageSender::handleRequest - Environment: - Variables: - POWERTOOLS_SERVICE_NAME: sqs-demo - QUEUE_URL: !Ref DemoSqsQueue - Policies: - - Statement: - - Sid: SQSSendMessageBatch - Effect: Allow - Action: - - sqs:SendMessageBatch - - sqs:SendMessage - Resource: !GetAtt DemoSqsQueue.Arn - - Sid: SQSKMSKey - Effect: Allow - Action: - - kms:GenerateDataKey - - kms:Decrypt - Resource: !GetAtt CustomerKey.Arn - Events: - CWSchedule: - Type: Schedule - Properties: - Schedule: 'rate(5 minutes)' - Name: !Join ["-", ["message-producer-schedule", !Select [0, !Split [-, !Select [2, !Split [/, !Ref AWS::StackId ]]]]]] - Description: Produce message to SQS via a Lambda function - Enabled: true - - DemoSQSConsumerFunction: - Type: AWS::Serverless::Function - Properties: - CodeUri: . - Handler: org.demo.sqs.SqsPoller::handleRequest - Environment: - Variables: - POWERTOOLS_SERVICE_NAME: sqs-demo - Policies: - - Statement: - - Sid: SQSDeleteGetAttribute - Effect: Allow - Action: - - sqs:DeleteMessageBatch - - sqs:GetQueueAttributes - Resource: !GetAtt DemoSqsQueue.Arn - - Sid: SQSSendMessageBatch - Effect: Allow - Action: - - sqs:SendMessageBatch - - sqs:SendMessage - Resource: !GetAtt DemoDlqSqsQueue.Arn - - Sid: SQSKMSKey - Effect: Allow - Action: - - kms:GenerateDataKey - - kms:Decrypt - Resource: !GetAtt CustomerKey.Arn - Events: - MySQSEvent: - Type: SQS - Properties: - Queue: !GetAtt DemoSqsQueue.Arn - BatchSize: 2 - MaximumBatchingWindowInSeconds: 300 - -Outputs: - DemoSqsQueue: - Description: "ARN for main SQS queue" - Value: !GetAtt DemoSqsQueue.Arn - DemoDlqSqsQueue: - Description: "ARN for DLQ" - Value: !GetAtt DemoDlqSqsQueue.Arn - DemoSQSSenderFunction: - Description: "Sender SQS Lambda Function ARN" - Value: !GetAtt DemoSQSSenderFunction.Arn - DemoSQSConsumerFunction: - Description: "Consumer SQS Lambda Function ARN" - Value: !GetAtt DemoSQSConsumerFunction.Arn - DemoSQSConsumerFunctionRole: - Description: "Implicit IAM Role created for SQS Lambda Function ARN" - Value: !GetAtt DemoSQSConsumerFunctionRole.Arn diff --git a/examples/powertools-examples-validation/README.md b/examples/powertools-examples-validation/README.md index 39afc48b9..3f6790b0c 100644 --- a/examples/powertools-examples-validation/README.md +++ b/examples/powertools-examples-validation/README.md @@ -1,21 +1,26 @@ -# Validation +# Powertools for AWS Lambda (Java) - Validation Example -This project contains an example of Lambda function using the validation 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/validation/). +This project contains an example of Lambda function using the validation 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/validation/). + +The handler [InboundValidation](src/main/java/org/demo/validation/InboundValidation.java) validates incoming HTTP requests +received from the API gateway against [schema.json](src/main/resources/schema.json). ## 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) +To test the validation, we can POST a JSON object shaped like our schema: +```bash + curl -X POST https://[REST-API-ID].execute-api.[REGION].amazonaws.com/Prod/hello/ -H "Content-Type: application/json" -d '{"address": "https://checkip.amazonaws.com"}' +``` -To build and deploy your application for the first time, run the following in your shell: +If we break the schema - for instance, by removing one of the compulsory fields, +we will get an error back from our API and will see a `ValidationException` in the logs: ```bash -sam build -sam deploy --guided + sam logs --tail --stack-name $MY_STACK ``` diff --git a/examples/powertools-examples-validation/pom.xml b/examples/powertools-examples-validation/pom.xml index 5d0774284..b3947513f 100644 --- a/examples/powertools-examples-validation/pom.xml +++ b/examples/powertools-examples-validation/pom.xml @@ -1,3 +1,17 @@ + + 4.0.0 diff --git a/examples/powertools-examples-validation/src/main/java/org/demo/validation/InboundValidation.java b/examples/powertools-examples-validation/src/main/java/org/demo/validation/InboundValidation.java index 89ec538c9..d3b8e51e4 100644 --- a/examples/powertools-examples-validation/src/main/java/org/demo/validation/InboundValidation.java +++ b/examples/powertools-examples-validation/src/main/java/org/demo/validation/InboundValidation.java @@ -1,11 +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 org.demo.validation; 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 software.amazon.lambda.powertools.validation.Validation; - import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; @@ -13,6 +25,7 @@ import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; +import software.amazon.lambda.powertools.validation.Validation; /** * Request handler for Lambda function which demonstrates validation of request message. @@ -20,7 +33,8 @@ public class InboundValidation implements RequestHandler { @Validation(inboundSchema = "classpath:/schema.json") - public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent apiGatewayProxyRequestEvent, Context context) { + public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent apiGatewayProxyRequestEvent, + Context context) { Map headers = new HashMap<>(); headers.put("Content-Type", "application/json"); diff --git a/examples/powertools-examples-validation/src/test/java/org/demo/validation/InboundValidationTest.java b/examples/powertools-examples-validation/src/test/java/org/demo/validation/InboundValidationTest.java index af47d3d87..d5e6de313 100644 --- a/examples/powertools-examples-validation/src/test/java/org/demo/validation/InboundValidationTest.java +++ b/examples/powertools-examples-validation/src/test/java/org/demo/validation/InboundValidationTest.java @@ -1,5 +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 org.demo.validation; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; @@ -9,9 +26,6 @@ import org.mockito.MockitoAnnotations; import software.amazon.lambda.powertools.validation.ValidationException; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - public class InboundValidationTest { @Mock diff --git a/license-header b/license-header new file mode 100644 index 000000000..5669f143f --- /dev/null +++ b/license-header @@ -0,0 +1,13 @@ +/* + * 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. + * + */ diff --git a/mkdocs.yml b/mkdocs.yml index 7fa3701b1..329e60dac 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -13,11 +13,13 @@ nav: - Utilities: - utilities/idempotency.md - utilities/parameters.md - - utilities/sqs_large_message_handling.md + - utilities/large_messages.md - utilities/batch.md - utilities/validation.md - utilities/custom_resources.md - utilities/serialization.md + - Processes: + - processes/maintainers.md theme: name: material diff --git a/pom.xml b/pom.xml index 37c2d813f..d1ff8d652 100644 --- a/pom.xml +++ b/pom.xml @@ -1,4 +1,18 @@ + + @@ -31,13 +45,12 @@ powertools-serialization powertools-logging powertools-tracing - powertools-sqs powertools-metrics powertools-parameters powertools-validation - powertools-test-suite powertools-cloudformation powertools-idempotency + powertools-large-messages powertools-e2e-tests examples @@ -60,7 +73,7 @@ 2.20.0 2.15.2 1.9.7 - 2.20.96 + 2.20.118 2.14.0 2.1.3 UTF-8 @@ -75,7 +88,7 @@ 3.5.0 3.3.0 3.1.0 - 5.9.3 + 5.10.0 1.0.6 0.5.1 @@ -104,11 +117,6 @@ powertools-logging ${project.version} - - software.amazon.lambda - powertools-sqs - ${project.version} - software.amazon.lambda powertools-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.1 test org.apache.commons commons-lang3 - 3.12.0 + 3.13.0 test @@ -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.aspectj aspectjrt + + org.apache.logging.log4j + log4j-slf4j2-impl + ${log4j.version} + @@ -82,6 +101,26 @@ assertj-core test + + 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.lambda powertools-idempotency + + software.amazon.lambda + powertools-logging + + + org.apache.logging.log4j + log4j-slf4j2-impl + com.amazonaws aws-lambda-java-events @@ -38,6 +46,10 @@ software.amazon.lambda powertools-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.0 1.13.1 3.11.0 + 2.20.108 + 2.20.0 @@ -32,6 +34,14 @@ + + software.amazon.awssdk + bom + ${aws.sdk.version} + pom + import + + software.amazon.lambda powertools-logging @@ -57,6 +67,11 @@ powertools-parameters ${lambda.powertools.version} + + software.amazon.lambda + powertools-large-messages + ${lambda.powertools.version} + com.amazonaws aws-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.8 1.8 - 10.2.62 - 2.85.0 + 10.2.69 + 2.89.0 true @@ -37,6 +51,13 @@ test + + software.amazon.awssdk + dynamodb + ${aws.sdk.version} + test + + software.amazon.awssdk cloudwatch @@ -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.awssdk url-connection-client @@ -63,6 +98,12 @@ test + + commons-io + commons-io + 2.13.0 + + org.junit.jupiter junit-jupiter-engine @@ -133,6 +174,10 @@ + + org.apache.maven.plugins + maven-checkstyle-plugin + org.apache.maven.plugins maven-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> items = response.items(); + assertThat(items).hasSize(1); + messageId = items.get(0).get("id").s(); + assertThat(Integer.valueOf(items.get(0).get("bodySize").n())).isEqualTo(300977); + assertThat(items.get(0).get("bodyMD5").s()).isEqualTo("22bde5e7b05fa80bc7be45bdd4bc6c75"); + } + + @Test + public void smallSQSMessage_shouldNotReadFromS3() throws IOException, InterruptedException { + // given + final ExtendedClientConfiguration extendedClientConfig = + new ExtendedClientConfiguration() + .withPayloadSupportEnabled(s3Client, bucketName); + AmazonSQSExtendedClient client = new AmazonSQSExtendedClient(SqsClient.builder().httpClient(httpClient).build(), extendedClientConfig); + String message = "Hello World"; + + // when + client.sendMessage(SendMessageRequest + .builder() + .queueUrl(queueUrl) + .messageBody(message) + .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> items = response.items(); + assertThat(items).hasSize(1); + messageId = items.get(0).get("id").s(); + assertThat(Integer.valueOf(items.get(0).get("bodySize").n())).isEqualTo(message.getBytes(StandardCharsets.UTF_8).length); + assertThat(items.get(0).get("bodyMD5").s()).isEqualTo("b10a8db164e0754105b7a99be72e3fe5"); + } +} diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LargeMessageIdempotentE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LargeMessageIdempotentE2ET.java new file mode 100644 index 000000000..986d22a2f --- /dev/null +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LargeMessageIdempotentE2ET.java @@ -0,0 +1,191 @@ +/* + * 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 org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.AfterAll; +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.core.sync.RequestBody; +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.QueryRequest; +import software.amazon.awssdk.services.dynamodb.model.QueryResponse; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.sqs.SqsClient; +import software.amazon.awssdk.services.sqs.model.MessageAttributeValue; +import software.amazon.awssdk.services.sqs.model.SendMessageRequest; +import software.amazon.lambda.powertools.testutils.Infrastructure; + +public class LargeMessageIdempotentE2ET { + + private static final Logger LOG = LoggerFactory.getLogger(LargeMessageIdempotentE2ET.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(); + // cannot use the extended library as it will create different S3 objects (we need to have the same for Idempotency) + private static final SqsClient sqsClient = SqsClient.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; + + + @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(LargeMessageIdempotentE2ET.class.getSimpleName()) + .pathToFunction("largemessage_idempotent") + .idempotencyTable("idempo" + random) + .queue(queueName) + .largeMessagesBucket(bucketName) + .build(); + + Map outputs = infrastructure.deploy(); + + functionName = outputs.get(Infrastructure.FUNCTION_NAME_OUTPUT); + queueUrl = outputs.get("QueueURL"); + tableName = outputs.get("TableNameForAsyncTests"); + + LOG.info("Testing '" + LargeMessageIdempotentE2ET.class.getSimpleName() + "'"); + } + + @AfterAll + public static void tearDown() { + if (infrastructure != null) { + infrastructure.destroy(); + } + } + + @Test + public void test_ttlNotExpired_doesNotInsertInDDB_ttlExpired_insertInDDB() throws InterruptedException, + IOException { + int waitMs = 10000; + + // GIVEN + InputStream inputStream = this.getClass().getResourceAsStream("/large_sqs_message.txt"); + String bigMessage = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + + // upload manually to S3 + String key = UUID.randomUUID().toString(); + s3Client.putObject(PutObjectRequest.builder() + .bucket(bucketName) + .key(key) + .build(), RequestBody.fromString(bigMessage)); + + // WHEN + SendMessageRequest messageRequest = SendMessageRequest.builder() + .queueUrl(queueUrl) + .messageBody(String.format( + "[\"software.amazon.payloadoffloading.PayloadS3Pointer\",{\"s3BucketName\":\"%s\",\"s3Key\":\"%s\"}]", + bucketName, key)) + .messageAttributes(Collections.singletonMap("SQSLargePayloadSize", MessageAttributeValue.builder() + .stringValue("300977") + .dataType("Number") + .build())) + .build(); + + // First invocation + // send message to SQS with the good pointer and metadata + sqsClient.sendMessage(messageRequest); + Thread.sleep(waitMs); // wait for the function to be invoked & 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> items = response.items(); + assertThat(items).hasSize(1); + assertThat(Integer.valueOf(items.get(0).get("bodySize").n())).isEqualTo(300977); + assertThat(items.get(0).get("bodyMD5").s()).isEqualTo("22bde5e7b05fa80bc7be45bdd4bc6c75"); + long timeOfInvocation1 = Long.parseLong(items.get(0).get("now").n()); + + // WHEN + // Second invocation + // send the same message before ttl expires + sqsClient.sendMessage(messageRequest); + Thread.sleep(waitMs); // wait for the function to be invoked & executed + + // THEN + response = dynamoDbClient.query(request); + items = response.items(); + assertThat(items).hasSize(1); // we should have the same number of items (idempotency working) + assertThat(Integer.valueOf(items.get(0).get("bodySize").n())).isEqualTo(300977); + assertThat(items.get(0).get("bodyMD5").s()).isEqualTo("22bde5e7b05fa80bc7be45bdd4bc6c75"); + long timeOfInvocation2 = Long.parseLong(items.get(0).get("now").n()); + assertThat(timeOfInvocation2).isEqualTo(timeOfInvocation1); // should be the same as first invocation + + // WHEN + // waiting for TTL to expire + Thread.sleep(24000); + + // Third invocation + // send the same message again + sqsClient.sendMessage(messageRequest); + Thread.sleep(waitMs); // wait for the function to be invoked + + // THEN + response = dynamoDbClient.query(request); + items = response.items(); + assertThat(items).hasSize(2); // not idempotent anymore, function should put a new item in DDB + assertThat(Integer.valueOf(items.get(0).get("bodySize").n())).isEqualTo(300977); + assertThat(items.get(0).get("bodyMD5").s()).isEqualTo("22bde5e7b05fa80bc7be45bdd4bc6c75"); + assertThat(Integer.valueOf(items.get(1).get("bodySize").n())).isEqualTo(300977); + assertThat(items.get(1).get("bodyMD5").s()).isEqualTo("22bde5e7b05fa80bc7be45bdd4bc6c75"); + long timeOfInvocation3 = Long.parseLong(items.get(0).get("now").n()); + long timeOfInvocation4 = Long.parseLong(items.get(1).get("now").n()); + assertThat(timeOfInvocation3).isNotEqualTo(timeOfInvocation4); // should be different (not idempotent anymore) + + } +} diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LoggingE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LoggingE2ET.java index 15c5eb935..f958970d8 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LoggingE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LoggingE2ET.java @@ -1,8 +1,32 @@ +/* + * 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 org.assertj.core.api.Assertions.assertThat; +import static software.amazon.lambda.powertools.testutils.Infrastructure.FUNCTION_NAME_OUTPUT; +import static software.amazon.lambda.powertools.testutils.lambda.LambdaInvoker.invokeFunction; +import static software.amazon.lambda.powertools.testutils.logging.InvocationLogs.Level.INFO; + import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -10,15 +34,6 @@ import software.amazon.lambda.powertools.testutils.Infrastructure; import software.amazon.lambda.powertools.testutils.lambda.InvocationResult; -import java.util.UUID; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static org.assertj.core.api.Assertions.assertThat; -import static software.amazon.lambda.powertools.testutils.lambda.LambdaInvoker.invokeFunction; -import static software.amazon.lambda.powertools.testutils.logging.InvocationLogs.Level.INFO; - public class LoggingE2ET { private static final ObjectMapper objectMapper = new ObjectMapper(); @@ -33,26 +48,28 @@ public static void setup() { .testName(LoggingE2ET.class.getSimpleName()) .pathToFunction("logging") .environmentVariables( - Stream.of(new String[][]{ + Stream.of(new String[][] { {"POWERTOOLS_LOG_LEVEL", "INFO"}, {"POWERTOOLS_SERVICE_NAME", LoggingE2ET.class.getSimpleName()} }) .collect(Collectors.toMap(data -> data[0], data -> data[1]))) .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 public void test_logInfoWithAdditionalKeys() throws JsonProcessingException { // GIVEN String orderId = UUID.randomUUID().toString(); - String event = "{\"message\":\"New Order\", \"keys\":{\"orderId\":\"" + orderId +"\"}}"; + String event = "{\"message\":\"New Order\", \"keys\":{\"orderId\":\"" + orderId + "\"}}"; // WHEN InvocationResult invocationResult1 = invokeFunction(functionName, event); diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/MetricsE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/MetricsE2ET.java index 4b8d7ea5a..80673b995 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/MetricsE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/MetricsE2ET.java @@ -1,26 +1,41 @@ +/* + * 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 org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.Timeout; -import software.amazon.lambda.powertools.testutils.Infrastructure; -import software.amazon.lambda.powertools.testutils.lambda.InvocationResult; -import software.amazon.lambda.powertools.testutils.metrics.MetricsFetcher; +import static org.assertj.core.api.Assertions.assertThat; +import static software.amazon.lambda.powertools.testutils.Infrastructure.FUNCTION_NAME_OUTPUT; +import static software.amazon.lambda.powertools.testutils.lambda.LambdaInvoker.invokeFunction; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import java.util.stream.Stream; - -import static org.assertj.core.api.Assertions.assertThat; -import static software.amazon.lambda.powertools.testutils.lambda.LambdaInvoker.invokeFunction; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import software.amazon.lambda.powertools.testutils.Infrastructure; +import software.amazon.lambda.powertools.testutils.lambda.InvocationResult; +import software.amazon.lambda.powertools.testutils.metrics.MetricsFetcher; public class MetricsE2ET { - private static final String namespace = "MetricsE2ENamespace_"+UUID.randomUUID(); - private static final String service = "MetricsE2EService_"+UUID.randomUUID(); + private static final String namespace = "MetricsE2ENamespace_" + UUID.randomUUID(); + private static final String service = "MetricsE2EService_" + UUID.randomUUID(); private static Infrastructure infrastructure; private static String functionName; @@ -31,43 +46,56 @@ public static void setup() { .testName(MetricsE2ET.class.getSimpleName()) .pathToFunction("metrics") .environmentVariables( - Stream.of(new String[][]{ + Stream.of(new String[][] { {"POWERTOOLS_METRICS_NAMESPACE", namespace}, {"POWERTOOLS_SERVICE_NAME", service} }) .collect(Collectors.toMap(data -> data[0], data -> data[1]))) .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 - public void test_recordMetrics() { + public void test_recordMetrics() { // GIVEN - String event1 = "{ \"metrics\": {\"orders\": 1, \"products\": 4}, \"dimensions\": { \"Environment\": \"test\"} }"; + String event1 = + "{ \"metrics\": {\"orders\": 1, \"products\": 4}, \"dimensions\": { \"Environment\": \"test\"} }"; // WHEN InvocationResult invocationResult = invokeFunction(functionName, event1); // THEN MetricsFetcher metricsFetcher = new MetricsFetcher(); - List coldStart = metricsFetcher.fetchMetrics(invocationResult.getStart(), invocationResult.getEnd(), 60, namespace, "ColdStart", Stream.of(new String[][]{ - {"FunctionName", functionName}, - {"Service", service}} - ).collect(Collectors.toMap(data -> data[0], data -> data[1]))); + List coldStart = + metricsFetcher.fetchMetrics(invocationResult.getStart(), invocationResult.getEnd(), 60, namespace, + "ColdStart", Stream.of(new String[][] { + {"FunctionName", functionName}, + {"Service", service}} + ).collect(Collectors.toMap(data -> data[0], data -> data[1]))); assertThat(coldStart.get(0)).isEqualTo(1); - List orderMetrics = metricsFetcher.fetchMetrics(invocationResult.getStart(), invocationResult.getEnd(), 60, namespace, "orders", Collections.singletonMap("Environment", "test")); + List orderMetrics = + metricsFetcher.fetchMetrics(invocationResult.getStart(), invocationResult.getEnd(), 60, namespace, + "orders", Collections.singletonMap("Environment", "test")); assertThat(orderMetrics.get(0)).isEqualTo(1); - List productMetrics = metricsFetcher.fetchMetrics(invocationResult.getStart(), invocationResult.getEnd(), 60, namespace, "products", Collections.singletonMap("Environment", "test")); + List productMetrics = + metricsFetcher.fetchMetrics(invocationResult.getStart(), invocationResult.getEnd(), 60, namespace, + "products", Collections.singletonMap("Environment", "test")); assertThat(productMetrics.get(0)).isEqualTo(4); - orderMetrics = metricsFetcher.fetchMetrics(invocationResult.getStart(), invocationResult.getEnd(), 60, namespace, "orders", Collections.singletonMap("Service", service)); + orderMetrics = + metricsFetcher.fetchMetrics(invocationResult.getStart(), invocationResult.getEnd(), 60, namespace, + "orders", Collections.singletonMap("Service", service)); assertThat(orderMetrics.get(0)).isEqualTo(1); - productMetrics = metricsFetcher.fetchMetrics(invocationResult.getStart(), invocationResult.getEnd(), 60, namespace, "products", Collections.singletonMap("Service", service)); + productMetrics = + metricsFetcher.fetchMetrics(invocationResult.getStart(), invocationResult.getEnd(), 60, namespace, + "products", Collections.singletonMap("Service", service)); assertThat(productMetrics.get(0)).isEqualTo(4); } } diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ParametersE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ParametersE2ET.java index dbebe721f..9582f9f23 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ParametersE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ParametersE2ET.java @@ -1,9 +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; -import org.junit.jupiter.api.*; -import software.amazon.lambda.powertools.testutils.AppConfig; -import software.amazon.lambda.powertools.testutils.Infrastructure; -import software.amazon.lambda.powertools.testutils.lambda.InvocationResult; +import static org.assertj.core.api.Assertions.assertThat; +import static software.amazon.lambda.powertools.testutils.Infrastructure.FUNCTION_NAME_OUTPUT; +import static software.amazon.lambda.powertools.testutils.lambda.LambdaInvoker.invokeFunction; import java.util.HashMap; import java.util.Map; @@ -11,23 +24,29 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import java.util.stream.Stream; - -import static org.assertj.core.api.Assertions.assertThat; -import static software.amazon.lambda.powertools.testutils.lambda.LambdaInvoker.invokeFunction; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.Timeout; +import software.amazon.lambda.powertools.testutils.AppConfig; +import software.amazon.lambda.powertools.testutils.Infrastructure; +import software.amazon.lambda.powertools.testutils.lambda.InvocationResult; @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class ParametersE2ET { + private final AppConfig appConfig; private Infrastructure infrastructure; private String functionName; - private final AppConfig appConfig; public ParametersE2ET() { String appName = UUID.randomUUID().toString(); - Map params = new HashMap<>(); + Map params = new HashMap<>(); params.put("key1", "value1"); params.put("key2", "value2"); appConfig = new AppConfig(appName, "e2etest", params); } + @BeforeAll @Timeout(value = 5, unit = TimeUnit.MINUTES) public void setup() { @@ -36,24 +55,26 @@ public void setup() { .pathToFunction("parameters") .appConfig(appConfig) .environmentVariables( - Stream.of(new String[][]{ + Stream.of(new String[][] { {"POWERTOOLS_LOG_LEVEL", "INFO"}, {"POWERTOOLS_SERVICE_NAME", ParametersE2ET.class.getSimpleName()} }) .collect(Collectors.toMap(data -> data[0], data -> data[1]))) .build(); - functionName = infrastructure.deploy(); + Map outputs = infrastructure.deploy(); + functionName = outputs.get(FUNCTION_NAME_OUTPUT); } @AfterAll public void tearDown() { - if (infrastructure != null) + if (infrastructure != null) { infrastructure.destroy(); + } } @Test public void test_getAppConfigValue() { - for (Map.EntryconfigKey: appConfig.getConfigurationValues().entrySet()) { + for (Map.Entry configKey : appConfig.getConfigurationValues().entrySet()) { // Arrange String event1 = "{" + diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/TracingE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/TracingE2ET.java index 1f002fe60..0827d91ae 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/TracingE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/TracingE2ET.java @@ -1,5 +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 software.amazon.lambda.powertools; +import static org.assertj.core.api.Assertions.assertThat; +import static software.amazon.lambda.powertools.testutils.Infrastructure.FUNCTION_NAME_OUTPUT; +import static software.amazon.lambda.powertools.testutils.lambda.LambdaInvoker.invokeFunction; + +import java.util.Collections; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -10,16 +32,8 @@ import software.amazon.lambda.powertools.testutils.tracing.Trace; import software.amazon.lambda.powertools.testutils.tracing.TraceFetcher; -import java.util.Collections; -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.lambda.LambdaInvoker.invokeFunction; - public class TracingE2ET { - private static final String service = "TracingE2EService_" + UUID.randomUUID(); // "TracingE2EService_e479fb27-422b-4107-9f8c-086c62e1cd12"; + private static final String service = "TracingE2EService_" + UUID.randomUUID(); private static Infrastructure infrastructure; private static String functionName; @@ -33,13 +47,15 @@ public static void setup() { .tracing(true) .environmentVariables(Collections.singletonMap("POWERTOOLS_SERVICE_NAME", service)) .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 @@ -77,7 +93,8 @@ public void test_tracing() { sub = handleRequest.getSubsegments().get(1); assertThat(sub.getName()).isIn("## internal_stuff", "## buildMessage"); - SubSegment buildMessage = handleRequest.getSubsegments().stream().filter(subSegment -> subSegment.getName().equals("## buildMessage")).findFirst().orElse(null); + SubSegment buildMessage = handleRequest.getSubsegments().stream() + .filter(subSegment -> subSegment.getName().equals("## buildMessage")).findFirst().orElse(null); assertThat(buildMessage).isNotNull(); assertThat(buildMessage.getAnnotations()).hasSize(1); assertThat(buildMessage.getAnnotations().get("message")).isEqualTo(message); diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/AppConfig.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/AppConfig.java index c87f4ac48..4229af040 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/AppConfig.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/AppConfig.java @@ -1,12 +1,25 @@ +/* + * 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.testutils; -import java.util.HashMap; import java.util.Map; /** * Defines configuration used to setup an AppConfig * deployment when the infrastructure is rolled out. - * + *

    * All fields are non-nullable. */ public class AppConfig { diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/Infrastructure.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/Infrastructure.java index 59035af7c..996f49bd4 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/Infrastructure.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/Infrastructure.java @@ -1,33 +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.testutils; +import static java.util.Collections.singletonList; + import com.fasterxml.jackson.databind.JsonNode; +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.yaml.snakeyaml.Yaml; +import software.amazon.awscdk.App; +import software.amazon.awscdk.BundlingOptions; +import software.amazon.awscdk.BundlingOutput; +import software.amazon.awscdk.CfnOutput; +import software.amazon.awscdk.DockerVolume; +import software.amazon.awscdk.Duration; +import software.amazon.awscdk.RemovalPolicy; import software.amazon.awscdk.Stack; -import software.amazon.awscdk.*; import software.amazon.awscdk.cxapi.CloudAssembly; -import software.amazon.awscdk.services.appconfig.*; +import software.amazon.awscdk.services.appconfig.CfnApplication; +import software.amazon.awscdk.services.appconfig.CfnConfigurationProfile; +import software.amazon.awscdk.services.appconfig.CfnDeployment; +import software.amazon.awscdk.services.appconfig.CfnDeploymentStrategy; +import software.amazon.awscdk.services.appconfig.CfnEnvironment; +import software.amazon.awscdk.services.appconfig.CfnHostedConfigurationVersion; import software.amazon.awscdk.services.dynamodb.Attribute; import software.amazon.awscdk.services.dynamodb.AttributeType; import software.amazon.awscdk.services.dynamodb.BillingMode; import software.amazon.awscdk.services.dynamodb.Table; -import software.amazon.awscdk.services.groundstation.CfnConfig; import software.amazon.awscdk.services.iam.PolicyStatement; -import software.amazon.awscdk.services.iam.ServicePrincipal; import software.amazon.awscdk.services.lambda.Code; import software.amazon.awscdk.services.lambda.Function; -import software.amazon.awscdk.services.lambda.Permission; import software.amazon.awscdk.services.lambda.Tracing; +import software.amazon.awscdk.services.lambda.eventsources.SqsEventSource; import software.amazon.awscdk.services.logs.LogGroup; import software.amazon.awscdk.services.logs.RetentionDays; +import software.amazon.awscdk.services.s3.Bucket; +import software.amazon.awscdk.services.s3.LifecycleRule; import software.amazon.awscdk.services.s3.assets.AssetOptions; +import software.amazon.awscdk.services.sqs.DeadLetterQueue; +import software.amazon.awscdk.services.sqs.Queue; import software.amazon.awssdk.core.waiters.WaiterResponse; import software.amazon.awssdk.http.SdkHttpClient; import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.cloudformation.CloudFormationClient; -import software.amazon.awssdk.services.cloudformation.model.*; +import software.amazon.awssdk.services.cloudformation.model.Capability; +import software.amazon.awssdk.services.cloudformation.model.CreateStackRequest; +import software.amazon.awssdk.services.cloudformation.model.DeleteStackRequest; +import software.amazon.awssdk.services.cloudformation.model.DescribeStacksRequest; +import software.amazon.awssdk.services.cloudformation.model.DescribeStacksResponse; +import software.amazon.awssdk.services.cloudformation.model.OnFailure; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.ListObjectsV2Request; import software.amazon.awssdk.services.s3.model.ListObjectsV2Response; @@ -36,14 +80,6 @@ import software.amazon.awssdk.utils.StringUtils; import software.amazon.lambda.powertools.utilities.JsonConfig; -import java.io.File; -import java.io.IOException; -import java.nio.file.Paths; -import java.util.*; -import java.util.stream.Collectors; - -import static java.util.Collections.singletonList; - /** * This class is in charge of bootstrapping the infrastructure for the tests. *
    @@ -55,6 +91,7 @@ * and the CloudFormation stack is created (with the SDK `createStack`) */ public class Infrastructure { + public static final String FUNCTION_NAME_OUTPUT = "functionName"; private static final Logger LOG = LoggerFactory.getLogger(Infrastructure.class); private final String stackName; @@ -71,12 +108,13 @@ public class Infrastructure { private final String account; private final String idempotencyTable; private final AppConfig appConfig; - + private final SdkHttpClient httpClient; + private final String queue; + private final String largeMessagesBucket; private String functionName; private Object cfnTemplate; private String cfnAssetDirectory; - private final SdkHttpClient httpClient; private Infrastructure(Builder builder) { this.stackName = builder.stackName; @@ -87,6 +125,8 @@ private Infrastructure(Builder builder) { this.pathToFunction = builder.pathToFunction; this.idempotencyTable = builder.idemPotencyTable; this.appConfig = builder.appConfig; + this.queue = builder.queue; + this.largeMessagesBucket = builder.largeMessagesBucket; this.app = new App(); this.stack = createStackWithLambda(); @@ -110,11 +150,16 @@ private Infrastructure(Builder builder) { .build(); } + public static Builder builder() { + return new Builder(); + } + /** * Use the CloudFormation SDK to create the stack + * * @return the name of the function deployed part of the stack */ - public String deploy() { + public Map deploy() { uploadAssets(); LOG.info("Deploying '" + stackName + "' on account " + account); cfn.createStack(CreateStackRequest.builder() @@ -124,13 +169,17 @@ public String deploy() { .onFailure(OnFailure.ROLLBACK) .capabilities(Capability.CAPABILITY_IAM) .build()); - WaiterResponse waiterResponse = cfn.waiter().waitUntilStackCreateComplete(DescribeStacksRequest.builder().stackName(stackName).build()); + WaiterResponse waiterResponse = + cfn.waiter().waitUntilStackCreateComplete(DescribeStacksRequest.builder().stackName(stackName).build()); if (waiterResponse.matched().response().isPresent()) { - LOG.info("Stack " + waiterResponse.matched().response().get().stacks().get(0).stackName() + " successfully deployed"); + software.amazon.awssdk.services.cloudformation.model.Stack deployedStack = waiterResponse.matched().response().get().stacks().get(0); + LOG.info("Stack " + deployedStack.stackName() + " successfully deployed"); + Map outputs = new HashMap<>(); + deployedStack.outputs().forEach(output -> outputs.put(output.outputKey(), output.outputValue())); + return outputs; } else { throw new RuntimeException("Failed to create stack"); } - return functionName; } /** @@ -141,96 +190,13 @@ public void destroy() { cfn.deleteStack(DeleteStackRequest.builder().stackName(stackName).build()); } - public static Builder builder() { - return new Builder(); - } - - public static class Builder { - public long timeoutInSeconds = 30; - public String pathToFunction; - public String testName; - public AppConfig appConfig; - private String stackName; - private boolean tracing = false; - private JavaRuntime runtime; - private Map environmentVariables = new HashMap<>(); - private String idemPotencyTable; - - private Builder() { - getJavaRuntime(); - } - - /** - * Retrieve the java runtime to use for the lambda function. - */ - private void getJavaRuntime() { - String javaVersion = System.getenv("JAVA_VERSION"); // must be set in GitHub actions - if (javaVersion == null) { - throw new IllegalArgumentException("JAVA_VERSION is not set"); - } - if (javaVersion.startsWith("8")) { - runtime = JavaRuntime.JAVA8AL2; - } else if (javaVersion.startsWith("11")) { - runtime = JavaRuntime.JAVA11; - } else if (javaVersion.startsWith("17")) { - runtime = JavaRuntime.JAVA17; - } else { - throw new IllegalArgumentException("Unsupported Java version " + javaVersion); - } - LOG.debug("Java Version set to {}, using runtime {}", javaVersion, runtime.getRuntime()); - } - - public Infrastructure build() { - Objects.requireNonNull(testName, "testName must not be null"); - - String uuid = UUID.randomUUID().toString().replace("-", "").substring(0, 12); - stackName = testName + "-" + uuid; - - Objects.requireNonNull(pathToFunction, "pathToFunction must not be null"); - return new Infrastructure(this); - } - - public Builder testName(String testName) { - this.testName = testName; - return this; - } - - public Builder pathToFunction(String pathToFunction) { - this.pathToFunction = pathToFunction; - return this; - } - - public Builder tracing(boolean tracing) { - this.tracing = tracing; - return this; - } - - public Builder idempotencyTable(String tableName) { - this.idemPotencyTable = tableName; - return this; - } - - public Builder appConfig(AppConfig app) { - this.appConfig = app; - return this; - } - - public Builder environmentVariables(Map environmentVariables) { - this.environmentVariables = environmentVariables; - return this; - } - - public Builder timeoutInSeconds(long timeoutInSeconds) { - this.timeoutInSeconds = timeoutInSeconds; - return this; - } - } - /** * Build the CDK Stack containing the required resources (Lambda function, LogGroup, DDB Table) + * * @return the CDK stack */ private Stack createStackWithLambda() { + boolean createTableForAsyncTests = false; Stack stack = new Stack(app, stackName); List packagingInstruction = Arrays.asList( "/bin/sh", @@ -258,8 +224,12 @@ private Stack createStackWithLambda() { .outputType(BundlingOutput.ARCHIVED); functionName = stackName + "-function"; + CfnOutput.Builder.create(stack, FUNCTION_NAME_OUTPUT) + .value(functionName) + .build(); - LOG.debug("Building Lambda function with command "+ packagingInstruction.stream().collect(Collectors.joining(" ", "[", "]"))); + LOG.debug("Building Lambda function with command " + + packagingInstruction.stream().collect(Collectors.joining(" ", "[", "]"))); Function function = Function.Builder .create(stack, functionName) .code(Code.fromAsset("handlers/", AssetOptions.builder() @@ -292,15 +262,48 @@ private Stack createStackWithLambda() { .tableName(idempotencyTable) .timeToLiveAttribute("expiration") .build(); + function.addEnvironment("IDEMPOTENCY_TABLE", idempotencyTable); table.grantReadWriteData(function); } + if (!StringUtils.isEmpty(queue)) { + Queue sqsQueue = Queue.Builder + .create(stack, "SQSQueue") + .queueName(queue) + .visibilityTimeout(Duration.seconds(timeout * 6)) + .retentionPeriod(Duration.seconds(timeout * 6)) + .build(); + DeadLetterQueue.builder() + .queue(sqsQueue) + .maxReceiveCount(1) // do not retry in case of error + .build(); + sqsQueue.grantConsumeMessages(function); + SqsEventSource sqsEventSource = SqsEventSource.Builder.create(sqsQueue).enabled(true).batchSize(1).build(); + function.addEventSource(sqsEventSource); + CfnOutput.Builder + .create(stack, "QueueURL") + .value(sqsQueue.getQueueUrl()) + .build(); + createTableForAsyncTests = true; + } + + if (!StringUtils.isEmpty(largeMessagesBucket)) { + Bucket offloadBucket = Bucket.Builder + .create(stack, "LargeMessagesOffloadBucket") + .removalPolicy(RemovalPolicy.RETAIN) // autodelete does not work without cdk deploy + .bucketName(largeMessagesBucket) + .build(); + // instead of autodelete, have a lifecycle rule to delete files after a day + LifecycleRule.builder().expiration(Duration.days(1)).enabled(true).build(); + offloadBucket.grantReadWrite(function); + } + if (appConfig != null) { - CfnApplication app = CfnApplication.Builder - .create(stack, "AppConfigApp") - .name(appConfig.getApplication()) - .build(); + CfnApplication app = CfnApplication.Builder + .create(stack, "AppConfigApp") + .name(appConfig.getApplication()) + .build(); CfnEnvironment environment = CfnEnvironment.Builder .create(stack, "AppConfigEnvironment") @@ -308,7 +311,7 @@ private Stack createStackWithLambda() { .name(appConfig.getEnvironment()) .build(); - // Create a fast deployment strategy so we don't have to wait ages + // Create a fast deployment strategy, so we don't have to wait ages CfnDeploymentStrategy fastDeployment = CfnDeploymentStrategy.Builder .create(stack, "AppConfigDeployment") .name("FastDeploymentStrategy") @@ -327,7 +330,7 @@ private Stack createStackWithLambda() { ); CfnDeployment previousDeployment = null; - for (Map.Entry entry : appConfig.getConfigurationValues().entrySet()) { + for (Map.Entry entry : appConfig.getConfigurationValues().entrySet()) { CfnConfigurationProfile configProfile = CfnConfigurationProfile.Builder .create(stack, "AppConfigProfileFor" + entry.getKey()) .applicationId(app.getRef()) @@ -354,11 +357,25 @@ private Stack createStackWithLambda() { // We need to chain the deployments to keep CFN happy if (previousDeployment != null) { - deployment.addDependsOn(previousDeployment); + deployment.addDependency(previousDeployment); } previousDeployment = deployment; } } + if (createTableForAsyncTests) { + Table table = Table.Builder + .create(stack, "TableForAsyncTests") + .billingMode(BillingMode.PAY_PER_REQUEST) + .removalPolicy(RemovalPolicy.DESTROY) + .partitionKey(Attribute.builder().name("functionName").type(AttributeType.STRING).build()) + .sortKey(Attribute.builder().name("id").type(AttributeType.STRING).build()) + .build(); + + table.grantReadWriteData(function); + function.addEnvironment("TABLE_FOR_ASYNC_TESTS", table.getTableName()); + CfnOutput.Builder.create(stack, "TableNameForAsyncTests").value(table.getTableName()).build(); + } + return stack; } @@ -376,43 +393,145 @@ private void synthesize() { */ private void uploadAssets() { Map assets = findAssets(); - assets.forEach((objectKey, asset) -> { - if (!asset.assetPath.endsWith(".jar")) { - return; - } - ListObjectsV2Response objects = s3.listObjectsV2(ListObjectsV2Request.builder().bucket(asset.bucketName).build()); - if (objects.contents().stream().anyMatch(o -> o.key().equals(objectKey))) { - LOG.debug("Asset already exists, skipping"); - return; - } - LOG.info("Uploading asset " + objectKey + " to " + asset.bucketName); - s3.putObject(PutObjectRequest.builder().bucket(asset.bucketName).key(objectKey).build(), Paths.get(cfnAssetDirectory, asset.assetPath)); - }); + assets.forEach((objectKey, asset) -> + { + if (!asset.assetPath.endsWith(".jar")) { + return; + } + ListObjectsV2Response objects = + s3.listObjectsV2(ListObjectsV2Request.builder().bucket(asset.bucketName).build()); + if (objects.contents().stream().anyMatch(o -> o.key().equals(objectKey))) { + LOG.debug("Asset already exists, skipping"); + return; + } + LOG.info("Uploading asset " + objectKey + " to " + asset.bucketName); + s3.putObject(PutObjectRequest.builder().bucket(asset.bucketName).key(objectKey).build(), + Paths.get(cfnAssetDirectory, asset.assetPath)); + }); } /** * Reading the cdk assets.json file to retrieve the list of assets to push to S3 + * * @return a map of assets */ private Map findAssets() { Map assets = new HashMap<>(); try { - JsonNode jsonNode = JsonConfig.get().getObjectMapper().readTree(new File(cfnAssetDirectory, stackName + ".assets.json")); + JsonNode jsonNode = JsonConfig.get().getObjectMapper() + .readTree(new File(cfnAssetDirectory, stackName + ".assets.json")); JsonNode files = jsonNode.get("files"); - files.iterator().forEachRemaining(file -> { - String assetPath = file.get("source").get("path").asText(); - String assetPackaging = file.get("source").get("packaging").asText(); - String bucketName = file.get("destinations").get("current_account-current_region").get("bucketName").asText(); - String objectKey = file.get("destinations").get("current_account-current_region").get("objectKey").asText(); - Asset asset = new Asset(assetPath, assetPackaging, bucketName.replace("${AWS::AccountId}", account).replace("${AWS::Region}", region.toString())); - assets.put(objectKey, asset); - }); + files.iterator().forEachRemaining(file -> + { + String assetPath = file.get("source").get("path").asText(); + String assetPackaging = file.get("source").get("packaging").asText(); + String bucketName = + file.get("destinations").get("current_account-current_region").get("bucketName").asText(); + String objectKey = + file.get("destinations").get("current_account-current_region").get("objectKey").asText(); + Asset asset = new Asset(assetPath, assetPackaging, bucketName.replace("${AWS::AccountId}", account) + .replace("${AWS::Region}", region.toString())); + assets.put(objectKey, asset); + }); } catch (IOException e) { throw new RuntimeException(e); } return assets; } + public static class Builder { + public long timeoutInSeconds = 30; + public String pathToFunction; + public String testName; + public AppConfig appConfig; + private String largeMessagesBucket; + private String stackName; + private boolean tracing = false; + private JavaRuntime runtime; + private Map environmentVariables = new HashMap<>(); + private String idemPotencyTable; + private String queue; + + private Builder() { + getJavaRuntime(); + } + + /** + * Retrieve the java runtime to use for the lambda function. + */ + private void getJavaRuntime() { + String javaVersion = System.getenv("JAVA_VERSION"); // must be set in GitHub actions + if (javaVersion == null) { + throw new IllegalArgumentException("JAVA_VERSION is not set"); + } + if (javaVersion.startsWith("8")) { + runtime = JavaRuntime.JAVA8AL2; + } else if (javaVersion.startsWith("11")) { + runtime = JavaRuntime.JAVA11; + } else if (javaVersion.startsWith("17")) { + runtime = JavaRuntime.JAVA17; + } else { + throw new IllegalArgumentException("Unsupported Java version " + javaVersion); + } + LOG.debug("Java Version set to {}, using runtime {}", javaVersion, runtime.getRuntime()); + } + + public Infrastructure build() { + Objects.requireNonNull(testName, "testName must not be null"); + + String uuid = UUID.randomUUID().toString().replace("-", "").substring(0, 12); + stackName = testName + "-" + uuid; + + Objects.requireNonNull(pathToFunction, "pathToFunction must not be null"); + return new Infrastructure(this); + } + + public Builder testName(String testName) { + this.testName = testName; + return this; + } + + public Builder pathToFunction(String pathToFunction) { + this.pathToFunction = pathToFunction; + return this; + } + + public Builder tracing(boolean tracing) { + this.tracing = tracing; + return this; + } + + public Builder idempotencyTable(String tableName) { + this.idemPotencyTable = tableName; + return this; + } + + public Builder appConfig(AppConfig app) { + this.appConfig = app; + return this; + } + + public Builder environmentVariables(Map environmentVariables) { + this.environmentVariables = environmentVariables; + return this; + } + + public Builder timeoutInSeconds(long timeoutInSeconds) { + this.timeoutInSeconds = timeoutInSeconds; + return this; + } + + public Builder queue(String queue) { + this.queue = queue; + return this; + } + + public Builder largeMessagesBucket(String largeMessagesBucket) { + this.largeMessagesBucket = largeMessagesBucket; + return this; + } + } + private static class Asset { private final String assetPath; private final String assetPackaging; diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/JavaRuntime.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/JavaRuntime.java index dce97538f..c50fcab84 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/JavaRuntime.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/JavaRuntime.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.testutils; import software.amazon.awscdk.services.lambda.Runtime; diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/lambda/InvocationResult.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/lambda/InvocationResult.java index 168fec71b..b91840b8e 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/lambda/InvocationResult.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/lambda/InvocationResult.java @@ -1,10 +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 software.amazon.lambda.powertools.testutils.lambda; +import java.time.Instant; import software.amazon.awssdk.services.lambda.model.InvokeResponse; import software.amazon.lambda.powertools.testutils.logging.InvocationLogs; -import java.time.Instant; - public class InvocationResult { private final InvocationLogs logs; @@ -21,6 +34,7 @@ public InvocationResult(InvokeResponse response, Instant start, Instant end) { this.start = start; this.end = end; } + public InvocationLogs getLogs() { return logs; } diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/lambda/LambdaInvoker.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/lambda/LambdaInvoker.java index ecde1042e..cf45076bf 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/lambda/LambdaInvoker.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/lambda/LambdaInvoker.java @@ -1,5 +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 software.amazon.lambda.powertools.testutils.lambda; +import static java.time.temporal.ChronoUnit.MINUTES; + +import java.time.Clock; +import java.time.Instant; import software.amazon.awssdk.core.SdkBytes; import software.amazon.awssdk.http.SdkHttpClient; import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; @@ -9,18 +27,13 @@ import software.amazon.awssdk.services.lambda.model.InvokeResponse; import software.amazon.awssdk.services.lambda.model.LogType; -import java.time.Clock; -import java.time.Instant; - -import static java.time.temporal.ChronoUnit.MINUTES; - public class LambdaInvoker { 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 LambdaClient lambda = LambdaClient.builder() .httpClient(httpClient) - .region(region) - .build(); + .region(region) + .build(); public static InvocationResult invokeFunction(String functionName, String input) { SdkBytes payload = SdkBytes.fromUtf8String(input); diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/logging/InvocationLogs.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/logging/InvocationLogs.java index 1ae1cfad7..cd63d308a 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/logging/InvocationLogs.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/logging/InvocationLogs.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.testutils.logging; import java.nio.charset.StandardCharsets; @@ -35,6 +49,7 @@ public String[] getAllLogs() { /** * Return only logs from function, exclude START, END, and REPORT and other elements generated by Lambda service + * * @return only logs generated by the function */ public String[] getFunctionLogs() { @@ -44,7 +59,8 @@ public String[] getFunctionLogs() { public String[] getFunctionLogs(Level level) { String[] filtered = getFunctionLogs(); - return Arrays.stream(filtered).filter(log -> log.contains("\"level\":\""+level.getLevel()+"\"")).toArray(String[]::new); + return Arrays.stream(filtered).filter(log -> log.contains("\"level\":\"" + level.getLevel() + "\"")) + .toArray(String[]::new); } public enum Level { diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/metrics/MetricsFetcher.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/metrics/MetricsFetcher.java index eb3cd63a4..349a9acc1 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/metrics/MetricsFetcher.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/metrics/MetricsFetcher.java @@ -1,25 +1,44 @@ +/* + * 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.testutils.metrics; +import static java.time.Duration.ofSeconds; + import com.evanlennick.retry4j.CallExecutor; import com.evanlennick.retry4j.CallExecutorBuilder; import com.evanlennick.retry4j.Status; import com.evanlennick.retry4j.config.RetryConfig; import com.evanlennick.retry4j.config.RetryConfigBuilder; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; 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.cloudwatch.CloudWatchClient; -import software.amazon.awssdk.services.cloudwatch.model.*; - -import java.time.Instant; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.Callable; - -import static java.time.Duration.ofSeconds; +import software.amazon.awssdk.services.cloudwatch.model.Dimension; +import software.amazon.awssdk.services.cloudwatch.model.GetMetricDataRequest; +import software.amazon.awssdk.services.cloudwatch.model.GetMetricDataResponse; +import software.amazon.awssdk.services.cloudwatch.model.Metric; +import software.amazon.awssdk.services.cloudwatch.model.MetricDataQuery; +import software.amazon.awssdk.services.cloudwatch.model.MetricStat; +import software.amazon.awssdk.services.cloudwatch.model.StandardUnit; /** * Class in charge of retrieving the actual metrics of a Lambda execution on CloudWatch @@ -37,6 +56,7 @@ public class MetricsFetcher { /** * Retrieve the metric values from start to end. Different parameters are required (see {@link CloudWatchClient#getMetricData} for more info). * Use a retry mechanism as metrics may not be available instantaneously after a function runs. + * * @param start * @param end * @param period @@ -45,37 +65,41 @@ public class MetricsFetcher { * @param dimensions * @return */ - public List fetchMetrics(Instant start, Instant end, int period, String namespace, String metricName, Map dimensions) { + public List fetchMetrics(Instant start, Instant end, int period, String namespace, String metricName, + Map dimensions) { List dimensionsList = new ArrayList<>(); - if (dimensions != null) + if (dimensions != null) { dimensions.forEach((key, value) -> dimensionsList.add(Dimension.builder().name(key).value(value).build())); + } - Callable> callable = () -> { - LOG.debug("Get Metrics for namespace {}, start {}, end {}, metric {}, dimensions {}", namespace, start, end, metricName, dimensionsList); - GetMetricDataResponse metricData = cloudwatch.getMetricData(GetMetricDataRequest.builder() - .startTime(start) - .endTime(end) - .metricDataQueries(MetricDataQuery.builder() - .id(metricName.toLowerCase()) - .metricStat(MetricStat.builder() - .unit(StandardUnit.COUNT) - .metric(Metric.builder() - .namespace(namespace) - .metricName(metricName) - .dimensions(dimensionsList) - .build()) - .period(period) - .stat("Sum") - .build()) - .returnData(true) - .build()) - .build()); - List values = metricData.metricDataResults().get(0).values(); - if (values == null || values.isEmpty()) { - throw new Exception("No data found for metric " + metricName); - } - return values; - }; + Callable> callable = () -> + { + LOG.debug("Get Metrics for namespace {}, start {}, end {}, metric {}, dimensions {}", namespace, start, + end, metricName, dimensionsList); + GetMetricDataResponse metricData = cloudwatch.getMetricData(GetMetricDataRequest.builder() + .startTime(start) + .endTime(end) + .metricDataQueries(MetricDataQuery.builder() + .id(metricName.toLowerCase()) + .metricStat(MetricStat.builder() + .unit(StandardUnit.COUNT) + .metric(Metric.builder() + .namespace(namespace) + .metricName(metricName) + .dimensions(dimensionsList) + .build()) + .period(period) + .stat("Sum") + .build()) + .returnData(true) + .build()) + .build()); + List values = metricData.metricDataResults().get(0).values(); + if (values == null || values.isEmpty()) { + throw new Exception("No data found for metric " + metricName); + } + return values; + }; RetryConfig retryConfig = new RetryConfigBuilder() .withMaxNumberOfTries(10) @@ -85,9 +109,10 @@ public List fetchMetrics(Instant start, Instant end, int period, String .build(); CallExecutor> callExecutor = new CallExecutorBuilder>() .config(retryConfig) - .afterFailedTryListener(s -> { - LOG.warn(s.getLastExceptionThatCausedRetry().getMessage() + ", attempts: " + s.getTotalTries()); - }) + .afterFailedTryListener(s -> + { + LOG.warn(s.getLastExceptionThatCausedRetry().getMessage() + ", attempts: " + s.getTotalTries()); + }) .build(); Status> status = callExecutor.execute(callable); return status.getResult(); diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/tracing/SegmentDocument.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/tracing/SegmentDocument.java index 08f4bf7d8..5654b9876 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/tracing/SegmentDocument.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/tracing/SegmentDocument.java @@ -1,7 +1,20 @@ +/* + * 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.testutils.tracing; import com.fasterxml.jackson.annotation.JsonSetter; - import java.time.Duration; import java.util.ArrayList; import java.util.List; @@ -92,7 +105,7 @@ public boolean hasSubsegments() { return !subsegments.isEmpty(); } - public static class SubSegment{ + public static class SubSegment { private String id; private String name; diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/tracing/Trace.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/tracing/Trace.java index 15026a9d1..7298957aa 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/tracing/Trace.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/tracing/Trace.java @@ -1,9 +1,22 @@ -package software.amazon.lambda.powertools.testutils.tracing; +/* + * 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 software.amazon.lambda.powertools.testutils.tracing.SegmentDocument.SubSegment; +package software.amazon.lambda.powertools.testutils.tracing; import java.util.ArrayList; import java.util.List; +import software.amazon.lambda.powertools.testutils.tracing.SegmentDocument.SubSegment; public class Trace { private final List subsegments = new ArrayList<>(); diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/tracing/TraceFetcher.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/tracing/TraceFetcher.java index e7cd13640..dc63987fd 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/tracing/TraceFetcher.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/tracing/TraceFetcher.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 software.amazon.lambda.powertools.testutils.tracing; +import static java.time.Duration.ofSeconds; + import com.evanlennick.retry4j.CallExecutor; import com.evanlennick.retry4j.CallExecutorBuilder; import com.evanlennick.retry4j.Status; @@ -8,15 +24,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; -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.xray.XRayClient; -import software.amazon.awssdk.services.xray.model.*; -import software.amazon.lambda.powertools.testutils.tracing.SegmentDocument.SubSegment; - import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Arrays; @@ -24,25 +31,42 @@ import java.util.List; import java.util.concurrent.Callable; import java.util.stream.Collectors; - -import static java.time.Duration.ofSeconds; +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.xray.XRayClient; +import software.amazon.awssdk.services.xray.model.BatchGetTracesRequest; +import software.amazon.awssdk.services.xray.model.BatchGetTracesResponse; +import software.amazon.awssdk.services.xray.model.GetTraceSummariesRequest; +import software.amazon.awssdk.services.xray.model.GetTraceSummariesResponse; +import software.amazon.awssdk.services.xray.model.TimeRangeType; +import software.amazon.awssdk.services.xray.model.TraceSummary; +import software.amazon.lambda.powertools.testutils.tracing.SegmentDocument.SubSegment; /** * Class in charge of retrieving the actual traces of a Lambda execution on X-Ray */ public class TraceFetcher { - private static final ObjectMapper MAPPER = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + private static final ObjectMapper MAPPER = + new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); private static final Logger LOG = LoggerFactory.getLogger(TraceFetcher.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 XRayClient xray = XRayClient.builder() + .httpClient(httpClient) + .region(region) + .build(); private final Instant start; private final Instant end; private final String filterExpression; private final List excludedSegments; /** - * @param start beginning of the time slot to search in - * @param end end of the time slot to search in + * @param start beginning of the time slot to search in + * @param end end of the time slot to search in * @param filterExpression eventual filter for the search * @param excludedSegments list of segment to exclude from the search */ @@ -64,10 +88,11 @@ public static Builder builder() { * @return traces */ public Trace fetchTrace() { - Callable callable = () -> { - List traceIds = getTraceIds(); - return getTrace(traceIds); - }; + Callable callable = () -> + { + List traceIds = getTraceIds(); + return getTrace(traceIds); + }; RetryConfig retryConfig = new RetryConfigBuilder() .withMaxNumberOfTries(10) @@ -77,7 +102,10 @@ public Trace fetchTrace() { .build(); CallExecutor callExecutor = new CallExecutorBuilder() .config(retryConfig) - .afterFailedTryListener(s -> {LOG.warn(s.getLastExceptionThatCausedRetry().getMessage() + ", attempts: " + s.getTotalTries());}) + .afterFailedTryListener(s -> + { + LOG.warn(s.getLastExceptionThatCausedRetry().getMessage() + ", attempts: " + s.getTotalTries()); + }) .build(); Status status = callExecutor.execute(callable); return status.getResult(); @@ -85,6 +113,7 @@ public Trace fetchTrace() { /** * Retrieve traces from trace ids. + * * @param traceIds * @return */ @@ -96,43 +125,49 @@ private Trace getTrace(List traceIds) { throw new RuntimeException("No trace found"); } Trace traceRes = new Trace(); - tracesResponse.traces().forEach(trace -> { - if (trace.hasSegments()) { - trace.segments().forEach(segment -> { - try { - SegmentDocument document = MAPPER.readValue(segment.document(), SegmentDocument.class); - if (document.getOrigin().equals("AWS::Lambda::Function")) { - if (document.hasSubsegments()) { - getNestedSubSegments(document.getSubsegments(), traceRes, Collections.emptyList()); + tracesResponse.traces().forEach(trace -> + { + if (trace.hasSegments()) { + trace.segments().forEach(segment -> + { + try { + SegmentDocument document = MAPPER.readValue(segment.document(), SegmentDocument.class); + if (document.getOrigin().equals("AWS::Lambda::Function")) { + if (document.hasSubsegments()) { + getNestedSubSegments(document.getSubsegments(), traceRes, + Collections.emptyList()); + } + } + } catch (JsonProcessingException e) { + LOG.error("Failed to parse segment document: " + e.getMessage()); + throw new RuntimeException(e); } - } - } catch (JsonProcessingException e) { - LOG.error("Failed to parse segment document: " + e.getMessage()); - throw new RuntimeException(e); - } - }); - } - }); + }); + } + }); return traceRes; } private void getNestedSubSegments(List subsegments, Trace traceRes, List idsToIgnore) { - subsegments.forEach(subsegment -> { - List subSegmentIdsToIgnore = Collections.emptyList(); - if (!excludedSegments.contains(subsegment.getName()) && !idsToIgnore.contains(subsegment.getId())) { - traceRes.addSubSegment(subsegment); + subsegments.forEach(subsegment -> + { + List subSegmentIdsToIgnore = Collections.emptyList(); + if (!excludedSegments.contains(subsegment.getName()) && !idsToIgnore.contains(subsegment.getId())) { + traceRes.addSubSegment(subsegment); + if (subsegment.hasSubsegments()) { + subSegmentIdsToIgnore = subsegment.getSubsegments().stream().map(SubSegment::getId) + .collect(Collectors.toList()); + } + } if (subsegment.hasSubsegments()) { - subSegmentIdsToIgnore = subsegment.getSubsegments().stream().map(SubSegment::getId).collect(Collectors.toList()); + getNestedSubSegments(subsegment.getSubsegments(), traceRes, subSegmentIdsToIgnore); } - } - if (subsegment.hasSubsegments()) { - getNestedSubSegments(subsegment.getSubsegments(), traceRes, subSegmentIdsToIgnore); - } - }); + }); } /** * Use the X-Ray SDK to retrieve the trace ids corresponding to a specific function during a specific time slot + * * @return a list of trace ids */ private List getTraceIds() { @@ -146,20 +181,14 @@ private List getTraceIds() { if (!traceSummaries.hasTraceSummaries()) { throw new RuntimeException("No trace id found"); } - List traceIds = traceSummaries.traceSummaries().stream().map(TraceSummary::id).collect(Collectors.toList()); + List traceIds = + traceSummaries.traceSummaries().stream().map(TraceSummary::id).collect(Collectors.toList()); if (traceIds.isEmpty()) { throw new RuntimeException("No trace id found"); } return traceIds; } - 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 XRayClient xray = XRayClient.builder() - .httpClient(httpClient) - .region(region) - .build(); - public static class Builder { private Instant start; private Instant end; @@ -167,12 +196,15 @@ public static class Builder { private List excludedSegments = Arrays.asList("Initialization", "Invocation", "Overhead"); public TraceFetcher build() { - if (filterExpression == null) + if (filterExpression == null) { throw new IllegalArgumentException("filterExpression or functionName is required"); - if (start == null) + } + if (start == null) { throw new IllegalArgumentException("start is required"); - if (end == null) + } + if (end == null) { end = start.plus(1, ChronoUnit.MINUTES); + } LOG.debug("Looking for traces from {} to {} with filter {}", start, end, filterExpression); return new TraceFetcher(start, end, filterExpression, excludedSegments); } @@ -194,6 +226,7 @@ public Builder filterExpression(String filterExpression) { /** * "Initialization", "Invocation", "Overhead" are excluded by default + * * @param excludedSegments * @return */ @@ -203,7 +236,8 @@ public Builder excludeSegments(List excludedSegments) { } public Builder functionName(String functionName) { - this.filterExpression = String.format("service(id(name: \"%s\", type: \"AWS::Lambda::Function\"))", functionName); + this.filterExpression = + String.format("service(id(name: \"%s\", type: \"AWS::Lambda::Function\"))", functionName); return this; } } diff --git a/powertools-e2e-tests/src/test/resources/large_sqs_message.txt b/powertools-e2e-tests/src/test/resources/large_sqs_message.txt new file mode 100644 index 000000000..102216e83 --- /dev/null +++ b/powertools-e2e-tests/src/test/resources/large_sqs_message.txt @@ -0,0 +1,977 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam in nibh eget ante viverra mattis. Etiam elit est, pharetra efficitur fermentum at, accumsan id eros. Aenean accumsan nisi facilisis tempor suscipit. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nam magna erat, tincidunt eu placerat at, molestie at sem. In sit amet cursus mauris. Etiam ullamcorper lacus nisi, et porta metus semper id. Nulla mauris nunc, pharetra at facilisis a, consequat id metus. Sed convallis ligula non felis vulputate finibus. Curabitur nisl nulla, pharetra in justo a, dapibus ultricies dui. Morbi ultricies sollicitudin purus, quis pellentesque leo rhoncus sed. Curabitur sed eros quis lacus vulputate pretium. Vestibulum vitae urna nec arcu vulputate efficitur. Cras venenatis sodales dolor non ullamcorper. + +Donec eu placerat magna. Vestibulum justo lacus, luctus ac luctus sit amet, dapibus at orci. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec eu tortor ac justo pharetra hendrerit. Phasellus ornare lacinia vestibulum. Maecenas diam orci, molestie rhoncus fringilla vitae, aliquam eu purus. Maecenas vehicula felis dui, vel dapibus odio lobortis quis. Cras a velit non nisl efficitur tempor. + +Suspendisse magna enim, ultrices et elementum ut, venenatis ut purus. Curabitur convallis vel tortor id rhoncus. Pellentesque varius arcu non imperdiet eleifend. Vestibulum fringilla aliquet vehicula. Nullam et lorem ullamcorper, tincidunt ante eget, egestas ex. Donec laoreet, justo id tincidunt egestas, lectus diam faucibus tortor, nec venenatis felis leo a urna. In at tortor semper augue finibus ornare et vitae erat. Praesent vulputate, dui sed pulvinar porta, justo tortor feugiat tellus, eget accumsan velit turpis at orci. Nulla vitae velit facilisis augue sagittis fringilla nec sagittis nunc. + +Duis vel elementum odio, id eleifend mauris. Nam in ultrices nulla. Quisque pharetra blandit risus eget lacinia. Nulla mattis mi felis, a ultrices nisi egestas vulputate. Donec a augue ac ex laoreet semper at porta ante. Vestibulum arcu ipsum, vehicula vel mollis lobortis, ullamcorper sed augue. Ut viverra faucibus nunc, sit amet sodales diam gravida sollicitudin. Fusce finibus quam sed ornare consequat. Suspendisse viverra ultricies dui in volutpat. Maecenas blandit erat in rhoncus placerat. Phasellus eu nisi rutrum, bibendum nunc eu, viverra ex. Praesent sollicitudin mi sit amet dictum convallis. Vivamus purus ligula, interdum at tincidunt sit amet, pellentesque ut elit. Aenean vitae ultrices ex. In facilisis lectus est, nec vulputate diam tristique vel. In gravida tincidunt ex et viverra. + +Pellentesque mi justo, dignissim a nisl quis, tempor dictum lacus. Nam tristique varius dolor tincidunt pharetra. Nullam iaculis est sed quam dignissim cursus. Duis sit amet vehicula nisi, sed consectetur velit. Nullam non tellus nec dolor venenatis porta quis at quam. In pharetra id orci sed faucibus. Cras et malesuada erat. + +Quisque fringilla commodo metus nec feugiat. Donec et est urna. Maecenas ut magna dui. Vivamus eu sem venenatis, porta mi at, efficitur tortor. In lacinia hendrerit elit, in faucibus nulla ultrices et. Mauris accumsan nec orci et vestibulum. Aliquam eget justo velit. Mauris fringilla ullamcorper enim, in elementum enim eleifend a. Nulla id libero ac arcu tristique ultrices. Maecenas mattis orci vitae nisl laoreet auctor. + +Quisque id aliquam orci. Nunc molestie vitae quam eu consectetur. Vivamus molestie venenatis est, nec lacinia odio faucibus eget. Etiam ornare eu leo eget posuere. Quisque elementum ligula at euismod placerat. Maecenas lobortis leo diam, vel egestas odio iaculis ac. Integer dapibus mi metus. Sed at eleifend ligula, ullamcorper vestibulum eros. Suspendisse dignissim metus in vulputate iaculis. Nam rhoncus felis sit amet velit scelerisque semper. Ut sed augue eget nulla laoreet scelerisque. + +Aenean convallis ipsum id turpis gravida, et elementum ante facilisis. Donec vitae arcu id mauris mollis hendrerit. Mauris imperdiet cursus nibh at laoreet. Sed sit amet enim magna. Mauris blandit nisi quis arcu sodales, eget consequat orci euismod. Curabitur id bibendum diam. Ut pharetra tellus nec enim tristique, id efficitur eros accumsan. Suspendisse potenti. Praesent vel porttitor risus. Nulla vitae ex nec ex hendrerit euismod non mattis arcu. Aenean eget velit massa. Pellentesque venenatis arcu mauris, sit amet scelerisque sem lobortis ornare. Vivamus auctor porta eros, eget blandit lorem semper non. Nunc sed nibh commodo, hendrerit nunc at, malesuada nisl. Aliquam pretium leo sed iaculis vehicula. Vivamus interdum porta augue. + +Suspendisse potenti. Aenean sodales vehicula erat, vel sollicitudin eros eleifend id. Suspendisse hendrerit malesuada est, imperdiet tristique ex aliquet ac. Vivamus ultricies placerat lorem, ac placerat lectus sollicitudin ac. Etiam consequat odio vel bibendum pretium. Integer elementum nisl magna. Nam et vehicula risus, sed mollis tortor. Ut in molestie turpis. Nam luctus, elit molestie varius luctus, lacus ligula ullamcorper elit, non interdum ipsum neque non nibh. Curabitur vulputate facilisis suscipit. Sed vitae augue eu ex luctus lacinia ac vitae quam. Quisque non nibh ex. Praesent gravida efficitur dui, a vulputate nulla ultrices vitae. Duis libero dolor, facilisis in tempus vitae, pharetra non libero. Sed urna leo, placerat quis neque at, interdum placerat odio. Interdum et malesuada fames ac ante ipsum primis in faucibus. + +Vivamus in hendrerit elit, quis egestas sem. Sed tempus dolor finibus massa ultrices, sed tristique quam semper. Pellentesque euismod molestie facilisis. Vivamus sit amet ultrices elit. Ut eleifend, libero eu molestie maximus, ipsum elit pulvinar tortor, et aliquet nulla orci eu est. Nullam pretium, ligula at faucibus gravida, velit mauris pulvinar felis, sit amet tincidunt magna dui ut quam. Phasellus porttitor eu nunc id maximus. Suspendisse volutpat suscipit porta. + +Integer dictum augue purus, sed cursus eros blandit id. Vestibulum non nibh viverra, molestie lectus eu, vestibulum justo. Nullam a libero non nibh dignissim posuere id vel orci. Nulla viverra lorem eget condimentum scelerisque. Donec porta nunc sed sapien pretium dapibus. In hac habitasse platea dictumst. Suspendisse sit amet ligula elementum, accumsan ipsum vel, imperdiet massa. Fusce scelerisque non erat ac bibendum. Pellentesque consequat vehicula euismod. Morbi sapien nisl, ultrices ut scelerisque consectetur, ornare et orci. Maecenas efficitur eros a venenatis rutrum. Aenean bibendum dui in enim luctus posuere. Donec placerat porta eros eu dignissim. Fusce nulla velit, sodales eget nibh gravida, tincidunt venenatis urna. Aenean condimentum, massa in fringilla lobortis, turpis felis lacinia metus, sit amet facilisis nisl quam aliquet diam. Praesent fringilla vitae arcu nec lobortis. + +In tincidunt nisi ac dictum cursus. Sed dolor purus, posuere vel dolor vel, semper tempor elit. Integer nec scelerisque tellus, sit amet dictum magna. Donec hendrerit aliquet libero, a varius velit dapibus quis. Donec lectus turpis, egestas vel vestibulum vitae, iaculis quis eros. Maecenas id convallis metus. Duis quis tellus iaculis, lobortis massa vitae, condimentum eros. Pellentesque scelerisque nisl id hendrerit tempor. Donec quam augue, maximus eu porta ut, venenatis a ante. Curabitur dictum et nibh sed auctor. Nam et consequat odio. In vitae magna a dui porta pellentesque quis id magna. Sed eu nunc bibendum, imperdiet nunc sit amet, cursus diam. Vivamus in odio vel nibh sollicitudin molestie. Morbi sit amet leo et odio congue pharetra. Aliquam quis suscipit orci. + +Donec at ornare orci. Suspendisse nec tellus elit. Nam erat urna, laoreet at elit sed, tristique interdum ligula. Nullam dapibus orci sed lorem convallis fringilla. Cras urna ipsum, tristique maximus nisl quis, auctor mattis quam. Donec finibus consectetur lectus et interdum. Aenean posuere lectus vel turpis auctor finibus. Praesent molestie lectus ipsum, et tristique quam porttitor ac. Suspendisse aliquam pretium tortor, id egestas erat. Nulla mollis, orci at semper vulputate, nisl est pharetra diam, sit amet vulputate diam augue a sem. Donec in mauris felis. + +Nunc eleifend at elit a volutpat. Integer semper quis quam at faucibus. Donec pretium purus vitae erat pretium posuere. Sed ac aliquet nulla. Nam ut sagittis mauris. Sed quis sagittis risus, id pretium urna. Nam sagittis elementum ornare. Aenean ligula sapien, congue eget mi quis, viverra commodo nibh. Suspendisse non iaculis magna, nec volutpat neque. Donec eu dapibus eros. + +Nam sit amet iaculis massa. Nullam vel laoreet tellus, id cursus tortor. In hac habitasse platea dictumst. Curabitur in urna risus. Proin dui sem, interdum accumsan cursus ut, dignissim in purus. Nunc vitae consequat arcu. Proin volutpat elementum quam in posuere. Pellentesque luctus arcu in ornare tempor. Cras est turpis, viverra vel consectetur ac, dictum a quam. Donec sagittis tortor sed volutpat accumsan. Curabitur a tellus vitae arcu pretium viverra vel in ligula. Pellentesque turpis augue, porta vitae quam id, fermentum congue leo. Sed nec fermentum turpis. Nulla vehicula vulputate sem eu consequat. In tincidunt nisi et mi maximus, non facilisis justo porttitor. Proin eu sapien aliquam, accumsan nunc non, pulvinar leo. + +Nam vitae commodo sem. Ut laoreet in quam in suscipit. Nullam at faucibus diam. Fusce ut purus at ex lobortis elementum vitae quis sapien. Nam tempus consectetur ex, sed luctus felis. Donec porttitor mi dui, non luctus quam consectetur eu. Ut a egestas diam. Etiam sodales, nisl vitae gravida pulvinar, libero est condimentum tellus, vitae ullamcorper tortor justo a urna. Maecenas ac velit accumsan, laoreet leo eget, elementum libero. Maecenas dictum tincidunt blandit. Proin sed bibendum urna. Phasellus fermentum tincidunt tempor. Mauris iaculis, mi ac fermentum interdum, nibh odio pellentesque nunc, vel scelerisque sapien sem id purus. + +Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Maecenas imperdiet sit amet lectus at hendrerit. Vivamus luctus, elit non lacinia hendrerit, risus velit finibus nulla, quis sagittis ipsum diam ut odio. Sed mauris sapien, vehicula efficitur gravida sed, gravida in lectus. Fusce eget consectetur turpis, in fermentum neque. Nullam turpis turpis, feugiat a accumsan in, euismod a augue. Vestibulum nec neque libero. Phasellus eget efficitur turpis, fermentum molestie nunc. Sed consequat elit urna. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. + +Nunc egestas consequat blandit. Donec quis porta sapien. Sed augue nunc, efficitur vitae tellus sed, finibus bibendum sem. Fusce id sem quis quam pharetra ultrices. Phasellus non convallis velit. Quisque erat tellus, pretium eget porta in, ornare a arcu. Aenean nec lectus lorem. Suspendisse dolor lectus, ullamcorper ornare lorem a, consequat lobortis elit. Nunc dignissim est nec gravida facilisis. Proin faucibus erat vel eros volutpat, in vulputate neque sodales. + +Nunc vitae imperdiet ipsum, faucibus vehicula magna. Nulla nec nisl sapien. Curabitur et dui eget tortor efficitur accumsan. Aenean in pellentesque erat. Nam condimentum neque pulvinar, aliquam nisl eu, mollis lorem. Integer rutrum arcu eget felis semper euismod. Quisque vestibulum vel diam a tempor. Aenean mollis, tellus sit amet pretium pulvinar, nulla dolor ornare ligula, eget dignissim orci tortor ut sapien. Nam ut ipsum id lectus venenatis tempus vel non ex. Suspendisse blandit vitae nunc vitae porta. Suspendisse tincidunt est sit amet ultricies consectetur. Nulla fermentum hendrerit ex, vehicula rhoncus arcu lobortis ut. Vestibulum fermentum ornare diam at pellentesque. Praesent nunc lorem, porta et magna nec, sodales commodo justo. Duis aliquam sapien et rutrum tempus. Vestibulum malesuada felis eu ligula posuere luctus. + +Maecenas lacinia, lectus eget rhoncus aliquam, tortor est gravida sapien, vel aliquam arcu erat et magna. Praesent fringilla leo eget neque posuere imperdiet. In porttitor elit non enim gravida euismod. Aliquam tempus, orci at interdum dapibus, mauris lacus egestas sapien, ac ullamcorper ex nibh sed orci. Quisque iaculis enim et lectus egestas, malesuada posuere lectus interdum. Sed dignissim neque vel turpis dictum ornare. Vestibulum suscipit consequat maximus. + +Ut et ante sit amet leo rutrum volutpat. Sed malesuada quis sapien et ornare. Aliquam ac ex enim. Curabitur vel quam et orci posuere feugiat. Pellentesque nec metus eget sapien eleifend tincidunt non sit amet arcu. Cras posuere metus eget risus varius fermentum. Nullam orci eros, efficitur nec sapien nec, pretium laoreet erat. Nam gravida purus mauris, ac viverra orci hendrerit sed. Aenean ligula massa, posuere id faucibus vitae, malesuada quis augue. Morbi consectetur mattis mi, quis sodales diam bibendum eget. Aliquam sagittis neque at feugiat posuere. Donec gravida lectus a lectus fringilla tincidunt. Vivamus volutpat dui et turpis condimentum, ut tincidunt tortor lacinia. Ut laoreet, ante in pellentesque sodales, elit mauris scelerisque dui, quis tincidunt quam massa ac enim. Nulla porta porta sapien vel sollicitudin. + +Phasellus sed lectus posuere, mollis ante non, feugiat odio. Aenean a quam id mi ornare dapibus. Donec venenatis ipsum non velit accumsan, id elementum dolor imperdiet. Phasellus lacinia erat diam, sit amet convallis lectus ornare in. Curabitur lorem ligula, maximus eu varius quis, auctor quis odio. Etiam in orci porttitor, sodales ipsum at, rhoncus turpis. Nulla eget luctus nisi. Duis convallis est eget ligula fringilla viverra. Duis nec sapien quis dui luctus ornare sit amet quis erat. Quisque nec justo dui. + +Sed eleifend, tellus quis laoreet rhoncus, leo turpis imperdiet turpis, pretium varius odio tortor et leo. Morbi ut mi fringilla, porttitor sem ac, accumsan ante. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum cursus dolor accumsan dolor aliquet dignissim. Duis sed feugiat erat. Donec sed arcu accumsan, ornare nisl eget, lobortis nibh. Praesent pulvinar quis justo et hendrerit. + +Sed velit nibh, efficitur ut leo sed, eleifend iaculis nisl. Mauris ac diam euismod, luctus enim maximus, tincidunt sem. Ut tempus magna vitae blandit bibendum. Sed sapien tortor, ultrices et justo vel, pharetra faucibus dui. Vivamus et vestibulum arcu. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Mauris tincidunt suscipit risus, vitae varius felis commodo in. Nullam semper tortor dolor, sit amet pharetra odio interdum hendrerit. Pellentesque sed justo eros. Cras quam purus, eleifend id tellus molestie, eleifend finibus lorem. Donec a volutpat libero, at vehicula tellus. Vivamus tellus orci, pharetra in nisi vitae, tincidunt dapibus nunc. + +Suspendisse ultricies sem laoreet quam molestie ullamcorper. Sed auctor sodales metus, eget convallis eros ultrices quis. Duis rutrum mi a tortor iaculis posuere. Suspendisse potenti. Pellentesque dictum faucibus dui a vestibulum. Donec elit nisl, aliquet at dolor non, viverra dignissim felis. Donec mi elit, condimentum sit amet magna id, efficitur sodales arcu. Interdum et malesuada fames ac ante ipsum primis in faucibus. Pellentesque est elit, porttitor eget bibendum nec, auctor nec nulla. Pellentesque dignissim nisl vel orci commodo, ut ullamcorper justo viverra. + +Suspendisse pharetra gravida turpis sit amet efficitur. Nulla mattis tortor eget pharetra ultrices. Ut a turpis maximus, pharetra justo non, tempor quam. Nam et volutpat lectus. Vestibulum congue turpis augue, quis eleifend nulla euismod in. Vestibulum sed erat tempus, porttitor diam vel, elementum metus. Aenean facilisis molestie ante, sit amet ornare lacus mollis sed. Maecenas rutrum urna nec ex malesuada consequat. Nunc interdum pellentesque nisl sed viverra. Nam nec fermentum ipsum. Nulla facilisi. Maecenas congue dolor erat, a fermentum enim congue vitae. Integer metus libero, feugiat at eleifend eu, iaculis nec leo. Praesent eleifend convallis leo, eu posuere urna elementum eu. + +Aliquam dapibus dolor vel urna luctus venenatis. Suspendisse potenti. Donec vitae tellus nisi. Pellentesque vel neque dignissim, ornare tellus sed, sodales metus. Aliquam vitae maximus tortor. Nullam mattis, odio id porttitor euismod, est leo aliquet massa, bibendum pulvinar justo orci a magna. Morbi ut nisl congue, porttitor erat non, gravida turpis. In nulla risus, ullamcorper quis suscipit vel, tristique ut nulla. In malesuada odio augue, ac hendrerit nunc mattis a. Nam in quam ut elit ullamcorper mattis. Sed fringilla tempus felis, id pretium tortor varius non. Aenean quis sem quis risus mattis posuere vel quis nibh. + +Sed pulvinar commodo dui sit amet malesuada. Quisque porta tellus placerat congue efficitur. Cras id blandit enim. Praesent a urna felis. Aliquam ornare, nisl eget iaculis tempor, ligula mi vehicula dolor, ut eleifend massa massa vel est. Fusce venenatis laoreet lobortis. Proin tempus aliquet nunc ut porttitor. In est mauris, blandit eu mattis vitae, aliquam a orci. Cras vitae nulla nec ex cursus blandit. Aliquam fermentum, erat in aliquet dictum, metus urna fermentum quam, non varius magna lectus non urna. + +Donec faucibus nisl nunc. Integer rutrum dui enim, malesuada semper turpis pulvinar eget. Fusce consectetur ipsum tellus, sed maximus lorem auctor in. Morbi nec est in quam ultricies hendrerit. Sed aliquam sollicitudin elementum. Aenean aliquam ex sit amet dignissim venenatis. Duis malesuada leo nisi, id accumsan turpis pretium id. Sed ornare magna non pharetra pharetra. Ut auctor dolor neque, nec bibendum odio viverra in. Nulla convallis interdum condimentum. Proin vestibulum turpis in lorem ultrices, bibendum tristique ligula faucibus. In tincidunt auctor sem, vitae fringilla neque pulvinar eu. Etiam commodo pellentesque enim. Phasellus feugiat non sapien eget sollicitudin. Etiam consequat efficitur lacus vel maximus. Suspendisse vitae elementum diam. + +Mauris urna velit, efficitur at viverra at, interdum a velit. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Aenean bibendum sagittis massa in interdum. Mauris facilisis dui eget ipsum euismod ultrices. Donec sit amet lorem iaculis, condimentum velit quis, lacinia justo. Integer faucibus metus sed eros semper, in congue tortor venenatis. Proin tincidunt maximus enim, accumsan facilisis tellus gravida eu. Phasellus interdum aliquam ante, sit amet rhoncus nisl ornare at. Suspendisse libero eros, cursus quis fermentum nec, tempor eget lectus. Aenean ullamcorper augue lacus, non iaculis dui rutrum id. Aliquam erat volutpat. + +Morbi hendrerit fermentum sodales. Proin rutrum congue auctor. Mauris pellentesque elit non velit condimentum tincidunt a sit amet velit. Etiam accumsan ante id neque commodo vulputate. Aenean nec mattis neque. Aliquam tempor urna quis nisl convallis congue. Praesent vitae porttitor ante. Mauris mattis vestibulum ante, nec auctor augue. Praesent sem leo, accumsan eu fermentum egestas, iaculis ac nulla. + +Etiam vel nisi congue, varius neque id, volutpat quam. Fusce placerat arcu hendrerit orci condimentum vehicula. Integer sem ex, facilisis et lacinia a, condimentum at ante. Morbi condimentum diam tellus, non lobortis arcu pellentesque sit amet. Phasellus imperdiet augue eget sollicitudin mollis. Vivamus sollicitudin arcu aliquam lectus tristique, in consequat diam egestas. Suspendisse aliquet non velit id feugiat. Sed eu est fringilla, gravida nulla ac, dignissim tortor. Phasellus pellentesque nisl non venenatis sagittis. Etiam facilisis nulla quis lorem scelerisque vehicula id at ex. Duis in risus enim. In eu vulputate massa, vel dignissim odio. Praesent ut mi tellus. In hac habitasse platea dictumst. + +Nunc malesuada turpis in fermentum lobortis. Donec blandit eu orci non laoreet. Suspendisse posuere blandit tortor, in accumsan velit cursus in. Integer suscipit justo nulla, in viverra orci suscipit et. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum a pulvinar lacus. Vestibulum mattis nisi vel nunc convallis maximus. Fusce aliquam, erat in volutpat gravida, elit odio feugiat ante, nec varius enim leo eget nisl. Donec sit amet ligula lobortis, sollicitudin dolor quis, blandit augue. Duis at interdum sem. Nullam eleifend ligula urna, vitae sollicitudin purus cursus nec. + +In commodo risus eu justo hendrerit, ut posuere mi eleifend. Suspendisse sollicitudin odio sem. Vestibulum at dapibus dui, vel dictum nisi. In hac habitasse platea dictumst. Sed ac pharetra ex. Ut ultrices augue ut vulputate condimentum. Phasellus convallis arcu tortor, ac tincidunt justo cursus vitae. Mauris dignissim dapibus imperdiet. Nullam id quam eget mauris cursus molestie finibus eu enim. Fusce laoreet orci eu nunc fermentum tincidunt. Pellentesque vitae ex ac nunc porta mattis sed ut ligula. Phasellus erat dui, consequat et lacinia vel, blandit nec dui. Donec ipsum magna, rhoncus ac viverra vitae, feugiat vel ligula. + +Cras ut nisl sed elit dictum semper a at magna. Aliquam laoreet viverra velit vel lobortis. Nam tempor lorem sit amet purus tincidunt accumsan. Vestibulum et vestibulum ligula. Donec sit amet neque faucibus leo rutrum semper. Maecenas scelerisque, lacus et lobortis congue, purus quam euismod risus, et mattis orci nisl eget est. In hac habitasse platea dictumst. Pellentesque eros velit, sollicitudin quis ante vel, blandit maximus mi. Suspendisse at eros id quam vulputate ornare. Duis placerat tellus vel odio ultrices, ac feugiat enim placerat. Vestibulum ut massa mattis, commodo orci euismod, cursus lacus. Suspendisse potenti. Sed venenatis cursus neque, at lacinia erat ornare et. Sed rutrum, dui at porttitor hendrerit, lacus magna fringilla quam, id mollis elit leo ut ante. Praesent vel diam sed urna mollis laoreet eget ut risus. + +Mauris varius odio lectus, sit amet consequat nulla sollicitudin sed. Suspendisse commodo rhoncus enim vitae pellentesque. Aliquam vulputate sollicitudin aliquet. Mauris interdum interdum orci, non varius sem ornare ut. Sed vel nibh nunc. Duis tincidunt quam quis lectus egestas, eu ultricies ante posuere. Aenean in mauris eros. Suspendisse potenti. Vivamus sit amet augue at velit accumsan fermentum. Phasellus vel dui sit amet felis convallis sodales. + +Nunc mattis vitae sapien ut dignissim. Nam fermentum sit amet massa eu accumsan. In ut ipsum sit amet ante pellentesque accumsan. Nulla egestas eros eget lacus rhoncus pharetra. Nam pellentesque laoreet ex in lobortis. Vivamus congue tincidunt molestie. Integer vel turpis augue. Cras vel molestie quam. Etiam vel mauris ut tellus finibus consequat. + +Curabitur velit erat, vestibulum blandit massa a, aliquam elementum tellus. Pellentesque id nisl tempor lorem condimentum dapibus. Quisque ut lorem at orci elementum dapibus quis ut enim. Praesent venenatis congue arcu eu sagittis. Nunc nunc massa, posuere ac gravida id, scelerisque nec velit. Suspendisse non lacus a orci dapibus congue. Sed leo velit, facilisis sed egestas ut, vehicula at turpis. Praesent a finibus felis. Quisque est mi, pellentesque sit amet varius eu, gravida id est. + +Donec auctor gravida urna ac suscipit. Etiam placerat ipsum vitae ante congue, at fringilla lectus ullamcorper. Donec viverra lacus ut erat posuere, dignissim porta purus tempor. Duis eget enim quis diam vehicula pulvinar. Donec tempus eros velit, sit amet pharetra ligula placerat in. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas sit amet pretium tellus, non dictum ligula. Nullam a ex purus. Vestibulum id ex rhoncus, pretium eros vel, consequat tellus. Vivamus lacinia dolor nec ipsum aliquam, euismod fermentum sem efficitur. Nulla facilisi. Pellentesque pharetra lacinia augue, et eleifend lorem aliquet eu. Ut ut porta ante. Duis ut auctor nisi, id porttitor erat. Proin sollicitudin sem quis justo sodales aliquam. Nulla pharetra gravida arcu vel tempus. + +Maecenas nec imperdiet nulla, vitae fringilla diam. Mauris maximus aliquet tellus at congue. Aliquam in dictum elit. Sed pretium mattis lectus, non pellentesque enim dignissim vel. Sed quis elementum diam, a porttitor enim. Cras cursus eros nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus ornare sapien vel diam vestibulum, at commodo metus lobortis. Sed euismod iaculis scelerisque. Pellentesque venenatis malesuada dolor, at tempus eros venenatis sit amet. Fusce a massa non libero blandit suscipit. + +Nulla facilisi. Sed nec leo nisl. Proin condimentum nunc at risus semper, in laoreet dui congue. Integer in dolor vitae ex maximus porttitor. In efficitur vulputate metus, ac egestas diam pharetra ac. Sed tristique tempus ligula dignissim convallis. Ut tincidunt laoreet fringilla. Praesent nunc est, lacinia tristique odio in, varius rhoncus ipsum. Vivamus bibendum justo at metus ullamcorper imperdiet. Quisque elit nibh, rhoncus a placerat eget, vehicula quis ante. + +Morbi fermentum turpis enim, eu interdum dui interdum sit amet. Sed vulputate lacus nec ligula gravida euismod. Duis in nisl fringilla, sollicitudin diam ac, laoreet tortor. Sed eget porttitor augue. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Sed eu enim convallis augue vulputate convallis. Praesent laoreet, leo at ornare luctus, nisi felis semper sapien, sit amet sodales augue mauris eu lorem. Fusce ornare volutpat malesuada. Curabitur vehicula, eros id fringilla placerat, tellus elit venenatis lorem, ac imperdiet purus ex blandit felis. Nunc suscipit elementum risus ac dictum. Aliquam bibendum dignissim ipsum, sit amet posuere sem viverra ut. Vestibulum egestas in ex ac volutpat. + +Donec ut faucibus velit, nec suscipit metus. Nullam dui ligula, commodo eget est venenatis, ullamcorper porttitor lectus. Suspendisse dictum, metus vitae commodo ultricies, enim quam pretium enim, a efficitur tortor purus sed ipsum. Nam sit amet lacus vel elit consectetur ultrices. Vestibulum ac nibh in metus porttitor vestibulum. In blandit et est non efficitur. Aenean vestibulum tortor sit amet mattis semper. Praesent sit amet turpis ac neque malesuada tincidunt nec nec urna. Mauris posuere elit nisl, ac ullamcorper nisi pulvinar in. Nulla ac elit in neque ullamcorper facilisis sed et dui. + +Quisque molestie euismod dui, non scelerisque arcu dictum a. Donec eu nulla nisl. Aliquam neque orci, ultrices in nunc vitae, sollicitudin eleifend augue. Etiam at mattis velit, a efficitur dui. Nam bibendum enim non elit tristique aliquet. Vestibulum eget nibh lacus. Pellentesque id sapien dui. Nullam urna leo, faucibus et efficitur vel, ullamcorper iaculis mi. Donec sit amet bibendum justo, id dapibus sem. Pellentesque dignissim varius tellus nec egestas. Praesent eu orci vel ipsum pharetra maximus. Cras nisl ligula, sollicitudin in ligula vel, posuere sagittis eros. Phasellus porta tristique mauris vel eleifend. Mauris sit amet volutpat ipsum, sed vestibulum orci. + +Ut rutrum augue quis rhoncus tincidunt. Aenean sollicitudin lacus at erat varius ullamcorper. Integer vitae orci vel nisi vulputate cursus eget nec ex. Mauris elementum, augue ac accumsan convallis, libero leo porta ante, sit amet elementum felis ligula non enim. Ut venenatis semper posuere. Vestibulum nec nisl nisi. Ut a sapien ac orci finibus dictum sed at ex. Phasellus ac lorem nisl. Quisque vitae tempor lacus. Vestibulum in mauris diam. Proin mattis ligula vitae ipsum bibendum, at dapibus nunc placerat. Nam iaculis justo in accumsan tristique. + +Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Mauris pretium velit et tristique pellentesque. Nunc in sapien a purus congue rutrum. Nam placerat risus in ante rutrum rutrum. In in ligula magna. Duis orci ante, vehicula elementum consectetur vitae, lobortis vitae arcu. Ut feugiat tempus metus quis sollicitudin. Etiam cursus venenatis augue at fermentum. + +Vestibulum id vehicula massa. Proin ut ligula et sapien placerat tristique non nec nibh. Etiam non lacus placerat, molestie ex eu, venenatis elit. Sed posuere, ante et ullamcorper luctus, elit lectus blandit tortor, nec consequat massa elit a tortor. Fusce urna felis, porttitor at ultrices vel, eleifend porta ipsum. Pellentesque nisl nibh, molestie sit amet sem eu, dapibus pharetra nulla. Phasellus viverra augue eu augue volutpat dignissim. Integer bibendum faucibus varius. Curabitur non turpis purus. + +Sed pretium eros nisl, sed ullamcorper magna faucibus quis. Etiam ornare euismod tellus, vitae accumsan eros bibendum ut. Morbi a ex sed risus faucibus rutrum et ut urna. Nullam non tortor commodo, facilisis massa non, tristique metus. Curabitur placerat, arcu in egestas ullamcorper, nisl nisl luctus felis, ac dapibus erat velit sed erat. Proin sagittis felis vitae sem posuere semper. Vivamus fermentum eu nisi ac facilisis. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque pellentesque lacinia ex tempus lacinia. Aliquam erat volutpat. Nam aliquet mattis risus non pretium. Suspendisse potenti. + +Suspendisse et sapien ornare, elementum erat vel, ultrices nisi. Curabitur interdum dignissim tincidunt. Cras eget est a velit consectetur finibus et sed nisl. Curabitur vestibulum semper posuere. Aliquam porta diam commodo nulla tempus imperdiet. Sed id dictum neque. Ut sit amet risus aliquet, dignissim velit sed, tristique ipsum. Nam a sapien id magna commodo tincidunt quis ac tellus. Nunc nec pellentesque orci. In justo purus, vulputate quis urna a, fringilla blandit sem. Cras porttitor quam vitae metus vehicula faucibus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Ut eleifend orci lectus, vitae semper eros hendrerit id. Fusce nec tellus condimentum, vehicula elit quis, gravida erat. Maecenas volutpat interdum velit id vestibulum. + +Pellentesque id metus metus. Nullam ac metus non nisi facilisis aliquet quis a sem. Morbi cursus consectetur aliquam. Morbi id sapien nibh. Etiam tempus tempor tempor. Nam scelerisque condimentum purus. Proin nec cursus eros, in condimentum dolor. Nam in sagittis urna. Ut eget ornare erat. Vivamus nec elit ut sapien tristique aliquet a nec urna. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Maecenas varius pellentesque diam. + +Ut eget libero iaculis urna porta iaculis vel ac eros. Sed sed mi et libero porta consequat a in libero. Sed in imperdiet ipsum, sit amet ultricies dui. Curabitur sit amet consectetur eros. Praesent nunc tellus, feugiat ac laoreet consectetur, accumsan nec magna. Praesent at elementum orci. Donec dapibus venenatis libero, id tincidunt libero euismod ac. + +Aenean sit amet ex placerat, molestie sapien a, volutpat turpis. Vivamus elementum lacinia nisi a convallis. Donec a sagittis turpis. Curabitur pulvinar laoreet dolor id consequat. Aliquam aliquam feugiat magna, vitae sagittis lorem mattis ut. Donec sed auctor nulla. Ut convallis, neque vitae faucibus efficitur, nisi justo pharetra odio, vitae tristique purus lorem et nisi. Quisque eleifend aliquam arcu nec maximus. Nunc mauris elit, finibus nec gravida non, maximus nec nibh. Sed eu ipsum in arcu gravida maximus. Maecenas massa dolor, ullamcorper eget odio eu, congue ornare leo. Suspendisse venenatis ultricies imperdiet. Nam finibus orci vitae sagittis finibus. Pellentesque sit amet dapibus diam. Nam eu nunc elit. + +Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Etiam euismod turpis nec urna lobortis, ut malesuada ex suscipit. Fusce sed viverra risus. Pellentesque tristique nulla pulvinar tincidunt accumsan. Nullam vitae nibh imperdiet erat rutrum pellentesque et sit amet quam. Aenean laoreet ultricies hendrerit. Duis elementum tellus a feugiat vulputate. Aliquam aliquam enim placerat dolor viverra, non suscipit lorem ultrices. + +Proin interdum vitae lorem quis viverra. Praesent nec tempor dolor, vitae sagittis turpis. Etiam sit amet imperdiet sapien, ac laoreet arcu. Proin aliquam, risus nec maximus dapibus, dui diam elementum dolor, vitae tempor augue lorem vel justo. Aenean ac egestas dolor. Ut aliquet, felis at fringilla venenatis, leo nisl ullamcorper ex, vitae pellentesque turpis orci quis lectus. Nullam vitae suscipit metus. Integer congue, ante in lacinia rhoncus, erat lorem interdum elit, egestas suscipit lectus nunc non orci. Nunc vel viverra quam. Mauris sit amet sodales tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam quis sollicitudin libero. In nec venenatis orci. + +Integer non quam fringilla, tristique nulla id, gravida arcu. Aenean scelerisque lacinia magna. Praesent nunc sem, lobortis non convallis rhoncus, rutrum vitae ante. Sed et orci ut erat viverra bibendum a et lectus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Fusce sit amet eros eget dolor pharetra vestibulum. Nullam vestibulum, massa dictum finibus egestas, orci nulla pulvinar sem, nec tempor libero lacus eu ante. Nam lacus erat, gravida eu placerat sit amet, facilisis sed urna. Suspendisse efficitur felis non ornare dictum. Nam sit amet nulla enim. Nulla eget scelerisque est. Suspendisse lacinia velit arcu, id lobortis felis facilisis in. + +Suspendisse potenti. Nulla porttitor metus ut nunc dictum tristique. Cras sit amet tortor eget ligula tristique efficitur. Ut at nisi id purus imperdiet laoreet. Sed sit amet malesuada urna. In nunc dolor, luctus ac condimentum ut, dapibus vel metus. Suspendisse pretium urna eget libero convallis vestibulum. Integer ut mauris hendrerit ex posuere euismod sed sed odio. Nulla egestas libero sit amet magna venenatis faucibus. Pellentesque semper vestibulum elit, et pretium felis scelerisque non. Suspendisse aliquet leo arcu, sed dapibus ex semper non. Donec lacinia dictum dignissim. Maecenas ipsum nunc, aliquet eget consectetur sit amet, aliquet vitae odio. + +Proin consectetur blandit feugiat. Nulla ac dictum quam. Vivamus suscipit scelerisque ipsum, vitae consequat neque sagittis id. Donec eu augue hendrerit, varius ipsum at, cursus massa. Morbi id augue id ipsum porttitor mollis. Phasellus nec libero eu arcu finibus dapibus. Nullam nec pharetra ante. Aenean sit amet urna eget justo tempus pharetra ut nec mi. Pellentesque viverra, ligula nec elementum ornare, ante elit eleifend enim, nec dignissim ligula elit in nibh. Vestibulum facilisis, felis eu condimentum dapibus, ante risus tincidunt urna, ut posuere dui augue ut ante. + +Nam arcu lacus, accumsan ac dolor in, egestas euismod est. Sed consectetur mauris et enim tincidunt semper. Donec sit amet pellentesque diam. Fusce viverra arcu a placerat fermentum. Nullam euismod dui eget egestas ultrices. Duis euismod viverra tortor eget eleifend. Pellentesque vitae neque dapibus, bibendum mi eu, auctor velit. + +Pellentesque congue consectetur turpis, eget auctor dui suscipit vel. Aliquam a sollicitudin turpis. Praesent nec blandit tortor. Suspendisse non nunc at urna tincidunt elementum. Integer eu elit nec urna maximus blandit quis at tortor. Nulla laoreet elit a purus tempus, et tincidunt lorem sagittis. Aenean semper erat eu neque hendrerit ornare. Cras posuere lorem nec orci vulputate finibus. Fusce tempor ex ac lacus gravida venenatis. + +Phasellus laoreet, libero vel fermentum euismod, sem diam accumsan quam, vitae gravida arcu est at sem. In bibendum, felis at viverra congue, felis nunc fringilla libero, ut scelerisque erat massa ut neque. Suspendisse potenti. Cras faucibus dolor vel tortor blandit, quis imperdiet magna semper. Nullam ut urna dapibus, vulputate est a, hendrerit purus. Vivamus vestibulum nisi a tempus placerat. Morbi vitae ultricies lacus. + +Nunc vel efficitur urna. Fusce bibendum suscipit mauris, quis imperdiet nisl bibendum non. Nam sed nisi imperdiet, pellentesque sem sed, pellentesque diam. Praesent luctus feugiat odio, eget fermentum lectus ullamcorper non. Phasellus pulvinar lectus sed ligula semper lobortis. Pellentesque fermentum ultricies fermentum. Quisque id turpis vel orci rutrum rhoncus id vel metus. Vivamus ex leo, accumsan eu luctus sit amet, tincidunt vitae erat. Sed eu mollis odio, ut ultricies lacus. Duis enim odio, pellentesque non velit vel, aliquam blandit erat. Mauris feugiat felis fermentum purus blandit, id rhoncus lorem tempus. Integer cursus, nunc vitae laoreet commodo, nunc lacus venenatis orci, quis eleifend risus est nec quam. Nulla tincidunt nunc ac purus tincidunt, eu molestie ipsum sollicitudin. + +Vestibulum ac varius velit, non suscipit nulla. Vestibulum a sagittis tortor. Suspendisse vestibulum felis quam, eget blandit nulla dictum vel. Praesent eu tellus ut lacus facilisis ultrices. Etiam fringilla nulla nec leo viverra faucibus. Sed nec sollicitudin nisl. Aenean libero massa, ornare sed elit ut, interdum fermentum arcu. + +Aliquam maximus diam et elit dapibus, eget condimentum sapien finibus. Etiam gravida nunc dapibus facilisis feugiat. Sed quis massa ligula. Nunc eleifend dolor lacus, at dapibus nisi vulputate eget. Etiam vel euismod urna, quis molestie nibh. Vestibulum bibendum erat non dictum sollicitudin. Suspendisse sed placerat lacus. Cras sollicitudin quam justo, a condimentum urna feugiat a. Quisque mauris libero, ultricies at ligula a, facilisis euismod ante. + +Morbi hendrerit maximus sapien ut auctor. Nullam nisi erat, aliquam ac metus eu, fermentum laoreet augue. Nam ultricies mi at vulputate laoreet. In ipsum est, blandit sit amet lorem id, egestas aliquam odio. Praesent venenatis lobortis mi. Aenean eu lacus consequat, mattis enim et, pellentesque leo. Duis convallis facilisis placerat. Donec porta, enim eget placerat congue, nisi augue faucibus odio, et fringilla purus elit vitae felis. + +Nulla et tellus a libero pellentesque eleifend vitae mattis nisi. Nunc placerat neque et sapien lobortis, aliquet pharetra nunc vulputate. Suspendisse potenti. Etiam ullamcorper lacinia varius. Suspendisse sit amet malesuada magna. Nam molestie mi nec diam tempus laoreet. Suspendisse urna tellus, scelerisque vitae purus et, dapibus fermentum felis. + +Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec scelerisque eget neque sed hendrerit. Donec at ligula augue. Donec vulputate massa nunc, a feugiat nunc elementum ut. Donec pulvinar libero sit amet ante faucibus sagittis. Mauris quis diam ultrices, tristique sapien nec, tempus urna. Morbi hendrerit odio sit amet leo lacinia, non egestas diam condimentum. Suspendisse efficitur sapien eget elit euismod tristique. Duis posuere vestibulum risus id sodales. Ut eget mi in urna rutrum molestie non nec dolor. Curabitur sollicitudin pharetra lacus, in sodales tortor tempor vitae. Nullam rhoncus arcu vel euismod varius. + +Aenean malesuada, purus quis volutpat sollicitudin, lectus risus lacinia mi, a facilisis ante lacus a dolor. Aenean vel aliquam tortor. Vivamus ornare, tellus at malesuada suscipit, quam dolor egestas erat, id convallis enim augue a enim. Aenean ultricies sodales placerat. Vivamus vel erat ac mi vulputate commodo nec eget urna. Suspendisse potenti. Mauris quis dapibus est, id tristique libero. + +Suspendisse ex diam, imperdiet quis dui id, interdum sodales elit. Maecenas at nunc ligula. Etiam imperdiet convallis magna sit amet tristique. Proin hendrerit laoreet magna, eget malesuada elit rhoncus et. Curabitur eget odio imperdiet, molestie sapien pretium, efficitur turpis. Aliquam sed congue arcu. Pellentesque vitae mauris id neque pulvinar tempor. Donec lobortis tellus id gravida dictum. Nulla et varius ex. Vestibulum pharetra eu quam dapibus finibus. Nullam accumsan sagittis turpis. Mauris fermentum orci et tortor tempus, ac tristique odio sollicitudin. Duis nec elit et magna imperdiet molestie eget vel ante. + +Suspendisse tempus augue nec massa molestie aliquam. Pellentesque vestibulum, erat sit amet fringilla fermentum, sapien lorem tempor felis, at efficitur augue erat quis nisi. Proin nec felis sagittis, sollicitudin turpis eu, fringilla leo. Vestibulum at augue nec dui vestibulum aliquam a at metus. Duis ullamcorper eleifend bibendum. Maecenas viverra mauris lacus, et consequat nisl pharetra eu. Praesent rutrum diam eu mi aliquet, non pellentesque est suscipit. Vestibulum massa leo, blandit eget mi at, cursus faucibus nunc. Praesent nec bibendum libero, vitae cursus libero. Sed consequat vitae eros nec maximus. + +Pellentesque et tortor eget lectus feugiat venenatis. Integer ornare nisl quam, nec imperdiet justo finibus at. Morbi malesuada, ante sit amet rutrum tempor, risus lorem tristique urna, egestas varius purus ex ut sem. Etiam sit amet feugiat sapien. Etiam quis risus commodo, eleifend velit quis, tincidunt enim. Mauris fermentum lacinia velit quis efficitur. Nunc sit amet lacus sit amet urna viverra feugiat. Nullam sagittis rhoncus suscipit. Aliquam eu sem ligula. Sed sodales risus et tortor lacinia tristique. Nulla massa augue, malesuada sit amet euismod ac, euismod placerat nulla. Sed lobortis ex nec lacus facilisis, nec semper mauris ultrices. Quisque ornare non felis vitae pellentesque. Donec mattis ut magna sed imperdiet. Integer viverra tempus feugiat. + +Donec laoreet aliquam imperdiet. Fusce justo neque, dictum vitae fringilla vitae, euismod sed augue. Fusce sodales, lectus a congue bibendum, ante eros pharetra tortor, eu sollicitudin libero ipsum sit amet felis. Curabitur sed condimentum quam, in iaculis ante. Ut feugiat dictum odio, vitae hendrerit lacus volutpat sed. Sed scelerisque et metus nec gravida. Mauris mattis porttitor magna, ut maximus justo sagittis vitae. Donec at metus ut leo gravida vehicula in sed diam. Vestibulum dignissim suscipit sagittis. Nam varius et arcu sit amet lacinia. Sed eget elit rhoncus, elementum dolor a, vehicula orci. Nam vitae tellus sapien. Phasellus massa lorem, commodo quis placerat in, semper faucibus tortor. + +Vivamus id dolor mi. Curabitur sagittis posuere quam in mollis. Phasellus finibus ipsum vel elit tincidunt, a bibendum ligula vulputate. Suspendisse quis purus nisl. Integer mi sem, posuere quis nisi a, consequat consequat magna. Vestibulum bibendum mauris in risus auctor fringilla vel at lacus. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Vivamus venenatis mauris at mattis eleifend. Donec tempus ipsum ac nisl convallis ultrices. + +Nullam eu nibh ut erat vehicula accumsan. Vivamus vitae faucibus libero. In in luctus odio. Nulla nec enim pharetra, fermentum erat in, tempor turpis. Sed ac magna suscipit, dignissim elit id, faucibus libero. Maecenas placerat in dolor et faucibus. Sed maximus purus dolor, non egestas massa rutrum non. Nunc ut rhoncus lacus. Sed sit amet dolor eu sapien sagittis molestie. Aenean ipsum erat, rutrum a diam et, varius porttitor ligula. Phasellus fermentum fermentum felis. + +Phasellus odio massa, lacinia ac hendrerit vestibulum, placerat sit amet erat. Praesent eget justo scelerisque, congue est in, pharetra mi. Etiam blandit turpis dui, a faucibus ipsum viverra vitae. Praesent sed scelerisque arcu. Cras nec venenatis ipsum. In et tempus metus. Pellentesque cursus quis nibh quis porttitor. Duis leo lacus, pretium eget rutrum eu, tincidunt lobortis tellus. Cras in erat tristique arcu faucibus luctus sit amet tempus erat. Nullam placerat, est a interdum iaculis, purus justo convallis arcu, at mattis erat sem tempor velit. Curabitur sapien sapien, tempor eget sodales vitae, blandit ac libero. Proin eget sem et arcu malesuada convallis non a est. + +Ut id sagittis urna. Morbi est mauris, molestie quis magna ut, convallis porta justo. Etiam in vulputate sem. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nullam consectetur enim nisl, sit amet pulvinar turpis elementum at. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum convallis cursus libero vel feugiat. + +Curabitur vel scelerisque metus. Etiam aliquet faucibus quam, vel aliquet est mattis quis. Pellentesque pulvinar sodales augue, a dignissim sem tristique vitae. Pellentesque dolor quam, pharetra vitae rhoncus ut, blandit at elit. Sed dignissim diam turpis, ac condimentum justo iaculis vel. Donec facilisis eros sit amet est dapibus, vel pellentesque eros consequat. Integer euismod mollis metus, vel rutrum risus imperdiet ac. Fusce semper dolor sit amet mi congue accumsan. Maecenas sagittis magna justo, vel varius ante scelerisque id. Fusce at urna sapien. Aliquam dui sapien, facilisis eu magna laoreet, feugiat tincidunt elit. Fusce iaculis sit amet erat id dignissim. Suspendisse sollicitudin laoreet consequat. Pellentesque eu mollis quam, vel dapibus libero. Duis mattis tortor vitae orci vehicula, et viverra ipsum lobortis. Quisque iaculis sapien est, id sagittis tortor rutrum at. + +Duis ut condimentum risus, et venenatis nulla. Praesent urna ex, faucibus eu mollis nec, lobortis vitae neque. Vivamus a malesuada tellus. Quisque bibendum dolor nec massa porta, nec malesuada magna auctor. Phasellus non auctor turpis, et bibendum risus. Ut pellentesque justo non neque convallis, ut imperdiet diam sagittis. Nunc arcu nisi, rutrum sagittis neque elementum, finibus consequat felis. Proin euismod elit eu urna tristique ultrices. + +Curabitur id hendrerit ipsum. Aliquam tortor nisi, dignissim vel sodales a, ultricies a ipsum. Etiam commodo arcu sed volutpat varius. Proin vehicula lacinia tempor. Vestibulum feugiat nibh eu ante tempor efficitur. Etiam eleifend a orci eu euismod. Cras a tortor risus. Cras nec urna non urna malesuada maximus vel et ipsum. In ullamcorper porttitor dolor, at fringilla tortor tristique ac. Nulla gravida tortor nec dolor convallis, accumsan sollicitudin dui luctus. Vestibulum euismod vestibulum semper. Nam semper lacus dolor, vitae rhoncus nisi blandit nec. Phasellus turpis dolor, posuere sed sodales sit amet, interdum ut arcu. + +Sed blandit nulla et sem vulputate, et semper lacus posuere. Proin velit lorem, sagittis tincidunt aliquet vitae, fermentum sed orci. Ut interdum ultrices dolor eu tempor. Quisque pretium vulputate dui at blandit. Aenean pretium urna sed purus dapibus aliquam. Mauris tristique augue pretium magna scelerisque, vel cursus sapien porttitor. Nullam neque justo, aliquam non neque id, lobortis facilisis nibh. Duis molestie dui eu augue tristique porttitor. Sed posuere justo massa, efficitur vulputate erat posuere non. Cras quis condimentum tellus. + +Etiam eleifend ipsum ut justo viverra porttitor. Nam bibendum enim nec ligula vestibulum, vel fringilla velit posuere. Praesent leo enim, varius sit amet nulla ut, sodales sollicitudin nunc. Phasellus hendrerit varius ex quis tempus. Suspendisse maximus laoreet enim, ut bibendum tortor. Ut non magna vehicula augue condimentum scelerisque. Aenean efficitur elit vel rhoncus euismod. Vestibulum sit amet sollicitudin dui. Quisque ac diam nibh. Nullam quam enim, volutpat eu turpis et, posuere consectetur neque. + +Ut tincidunt semper dictum. Etiam varius enim sit amet metus consequat malesuada in at ligula. Cras nisl dui, tincidunt a urna et, porttitor rhoncus velit. Mauris interdum imperdiet lectus ac mollis. Aliquam sollicitudin ornare mi, a varius nulla faucibus aliquet. Nunc fermentum tempor ullamcorper. Pellentesque laoreet libero condimentum pellentesque pharetra. Nullam sed eros vel erat vestibulum dictum. Nulla nec interdum dui, quis finibus nunc. + +Sed ac turpis in mi ultricies finibus sit amet et diam. Pellentesque sagittis non ligula et convallis. Suspendisse interdum est aliquet erat rhoncus semper. Donec pretium turpis ante, vel posuere mauris facilisis vel. Vivamus nibh ligula, pharetra sit amet hendrerit nec, vulputate ut sapien. Nulla ullamcorper condimentum metus vitae imperdiet. Ut eget ex purus. Nulla dapibus malesuada pharetra. Praesent sit amet ornare turpis. Nulla pulvinar felis eget arcu mollis vulputate. Vestibulum eget semper felis. Donec viverra justo id mi tempor, a mollis ipsum porttitor. Maecenas aliquet volutpat ante eget tempor. Duis ipsum nisi, scelerisque quis metus vitae, blandit faucibus nisl. Mauris rutrum nisi sed lorem dictum ultricies. + +Nulla dictum arcu augue, aliquet ornare ligula suscipit ut. Integer libero erat, bibendum quis arcu at, consequat luctus sem. Ut ut erat tincidunt, porta erat efficitur, bibendum neque. Maecenas eget convallis sapien. Nullam facilisis ex et lorem fringilla, ut convallis leo dictum. Duis porttitor, ex eu venenatis faucibus, erat augue dapibus leo, sit amet scelerisque neque dui sed arcu. Praesent at diam nec sem sodales condimentum. Vivamus vehicula urna tellus, a semper ipsum cursus id. Duis auctor enim erat, laoreet volutpat dui rhoncus maximus. Suspendisse pellentesque euismod lacus, at auctor purus. Suspendisse volutpat imperdiet diam, id laoreet est egestas at. + +Fusce lobortis ex vel condimentum convallis. Vivamus fermentum convallis dolor quis rutrum. Donec tempus ornare maximus. Mauris quam neque, dignissim rutrum maximus sed, volutpat sed lectus. Donec id augue gravida, pretium purus eu, sagittis tellus. Maecenas tincidunt gravida felis nec pulvinar. Fusce turpis urna, sodales non rhoncus nec, sodales ac ipsum. Suspendisse risus felis, imperdiet id malesuada fermentum, ultricies et erat. Suspendisse potenti. Pellentesque tellus ipsum, dapibus eget pellentesque eleifend, venenatis sed risus. Ut sed mollis nisl. Aliquam lobortis aliquam ipsum, et ornare magna bibendum ut. Proin quis justo vitae neque tincidunt maximus ultricies et purus. Nullam non semper turpis. Quisque non mauris odio. + +Donec commodo tempus egestas. Vestibulum at diam vel mi tincidunt finibus. Donec aliquam magna id eros tristique finibus. Cras ut enim sapien. Proin gravida risus a nisi dapibus, ac vulputate nunc laoreet. Donec at leo vel nulla iaculis feugiat sed et ante. Nulla a arcu at ligula sollicitudin semper sed eget est. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Ut dolor magna, luctus id elit ut, interdum viverra lacus. Aenean sed tempor metus. Aenean arcu dui, eleifend quis est sed, luctus lacinia nulla. Morbi id luctus libero. Maecenas sodales leo eu sapien maximus porta. Praesent gravida augue eu ligula pretium cursus. + +Duis aliquam luctus tortor, eu facilisis quam sodales sed. Phasellus feugiat venenatis lorem, in scelerisque est efficitur quis. Cras quis nisl nisl. Quisque magna felis, tempus nec pharetra et, tempor id ligula. Pellentesque sed dignissim velit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris convallis iaculis posuere. Nullam orci velit, venenatis eget enim quis, molestie consequat dolor. Nulla egestas risus vestibulum sagittis blandit. Nullam dui lacus, tempus euismod sapien a, eleifend sodales justo. In id tortor neque. Vivamus faucibus ante ac odio vestibulum, vitae condimentum nibh consectetur. In rutrum hendrerit est, sit amet mollis ex aliquet tempor. + +Donec non tincidunt sem. Nullam dignissim felis nibh, et accumsan felis tristique sed. Maecenas id massa erat. Ut justo ligula, aliquet eu condimentum a, aliquet eget ex. Phasellus et neque eu nisl consectetur ullamcorper. Sed gravida efficitur nisi, vitae posuere nisi tincidunt sed. Duis vitae molestie metus, vitae finibus urna. Integer at lacus vitae turpis convallis feugiat a dignissim libero. In dolor nibh, aliquam eu malesuada in, tincidunt vitae nisi. Nullam at lectus risus. Integer vitae ligula interdum, congue nibh id, tincidunt dolor. + +Mauris risus quam, mollis eu consequat vitae, molestie sit amet risus. Vestibulum congue vulputate nibh sit amet ullamcorper. Nullam elit justo, hendrerit euismod elit nec, gravida tincidunt ipsum. Aenean tellus tortor, commodo vitae cursus vitae, finibus quis mi. Nam in tellus scelerisque, cursus magna a, elementum tellus. Praesent ut lacinia justo. Donec consectetur, mi nec faucibus viverra, nisl justo suscipit est, in consequat ex urna vitae orci. Fusce molestie feugiat ex sed dictum. Phasellus interdum eros dapibus sodales volutpat. Morbi elementum sapien ante, quis sagittis justo fermentum at. Maecenas vestibulum massa quis eleifend rhoncus. Praesent auctor ex at urna pellentesque facilisis. + +Duis tincidunt porta quam, in commodo orci dapibus interdum. Donec mattis erat ante, nec faucibus magna pharetra id. Aliquam dignissim ultricies auctor. Duis quis ex mi. Phasellus ipsum mi, volutpat quis augue nec, dapibus aliquet lectus. Aenean id eros mi. Fusce rhoncus iaculis magna, commodo commodo nibh pellentesque ut. Donec consectetur lacinia erat ut luctus. Mauris efficitur erat vitae sapien fringilla sollicitudin. Maecenas a egestas ipsum. Aenean placerat congue augue, ut condimentum lorem accumsan in. Morbi ac quam nunc. + +Nunc posuere faucibus semper. Integer at convallis ex. Duis a purus molestie, dignissim mi quis, consequat nulla. Proin tempus congue ligula, sit amet ultricies enim consequat ut. Nunc ac est at nisl euismod cursus. Pellentesque vulputate molestie ipsum. Praesent varius, lacus non lobortis blandit, nibh lacus scelerisque nisi, sit amet interdum ligula tellus ac purus. Vestibulum sed quam tempor, pharetra tellus ullamcorper, tincidunt est. Nulla tempus tortor nec erat tempus eleifend pellentesque eget purus. Duis gravida dui justo, ac commodo ipsum mollis et. Nullam vitae nibh enim. Ut sodales mi eu diam sagittis, quis luctus tortor euismod. + +Maecenas a augue ac augue varius rhoncus. Fusce nunc turpis, mattis eu iaculis a, dapibus nec purus. Pellentesque imperdiet sit amet purus a blandit. Morbi augue tellus, venenatis nec tortor in, consectetur tincidunt nulla. Aenean purus libero, volutpat quis neque vitae, mattis efficitur eros. Donec sodales venenatis augue, sed faucibus magna accumsan convallis. Pellentesque efficitur elementum imperdiet. Cras at condimentum leo. Curabitur feugiat ut nisi facilisis commodo. + +Sed commodo sapien quam, quis vulputate orci lobortis nec. Aenean luctus dictum ipsum, non consequat sapien molestie vel. Proin blandit libero tortor, vitae efficitur tortor hendrerit at. Sed eleifend eu velit eu tempor. Nunc auctor tincidunt mollis. Ut molestie, erat ut lobortis convallis, odio felis sollicitudin elit, vitae ultricies lorem neque et dui. Sed venenatis tempus ullamcorper. Integer eget elementum nulla. Maecenas a placerat ipsum. Etiam sagittis sagittis rhoncus. Aliquam faucibus magna id lacus pharetra, nec interdum lacus sodales. + +Mauris ipsum sem, venenatis non elit in, feugiat pretium lacus. Fusce eget tincidunt dolor. Nam pharetra lacus vitae diam bibendum, ac placerat tortor aliquet. Sed eget gravida orci. Ut in orci lorem. Morbi condimentum ligula dolor, in dictum mi vestibulum sed. Suspendisse eu velit finibus, tincidunt dolor malesuada, mattis est. Morbi iaculis sapien vitae metus facilisis fringilla. Donec sed volutpat neque. Mauris ipsum metus, venenatis cursus mattis quis, convallis ultricies purus. Aliquam quam magna, sagittis at mi quis, lacinia iaculis turpis. Praesent viverra, ex nec euismod lacinia, urna risus pretium odio, sit amet mattis metus eros non lectus. + +Nam sed vehicula est, ac aliquet mi. Aenean iaculis placerat orci, ut lobortis dui vestibulum convallis. Vestibulum eleifend ante sed lorem interdum lobortis ut vel tortor. Sed auctor id nisl sed bibendum. Fusce maximus vulputate mauris, tempus sollicitudin nunc efficitur vitae. Nulla pellentesque molestie leo, quis hendrerit enim. In vel dapibus nisi. Nam at dui quis eros porttitor volutpat in id magna. Maecenas quis quam a justo cursus aliquet viverra sit amet mauris. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Ut hendrerit est at augue pretium condimentum at ut velit. Suspendisse non gravida metus. + +Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Praesent lacinia commodo elit, sed fringilla ipsum imperdiet ut. Proin a dictum ante. Suspendisse potenti. Praesent ac nulla volutpat, ultricies diam vel, auctor risus. Nullam aliquam elit vel quam suscipit molestie vel ac dolor. Ut pharetra finibus elit, id vulputate ex dignissim in. Ut ac finibus urna, a luctus magna. + +Fusce suscipit convallis eros nec blandit. Cras quis lectus nibh. Donec pulvinar rhoncus pulvinar. Vivamus nulla nisi, vestibulum vel neque et, dictum gravida ex. Vestibulum in arcu vitae nibh auctor rhoncus ac in massa. Sed semper enim ac dui sollicitudin porttitor. Etiam ante erat, laoreet sed dolor eget, cursus commodo lorem. Curabitur maximus tincidunt nulla at molestie. Phasellus ultrices felis a cursus facilisis. Nullam a semper tortor. Nulla ac vulputate justo. Mauris maximus nisi quam, elementum eleifend nulla condimentum nec. Aenean congue tincidunt mi, id mollis orci blandit vitae. Integer placerat orci at nisl vestibulum lacinia. + +Sed egestas posuere egestas. Quisque pulvinar velit commodo felis accumsan, a consequat purus laoreet. Ut at arcu at augue sodales rhoncus. Ut non nisl dui. Sed sed nulla quis orci eleifend auctor ut et sem. Aliquam erat volutpat. Donec egestas tincidunt leo in interdum. Donec varius odio nulla, id porttitor erat venenatis ut. Nulla ac nisl ut enim placerat congue. Fusce consequat purus eget risus luctus finibus. Donec in blandit odio. Sed vestibulum nisl ut diam vestibulum volutpat. Donec magna quam, tempor eget velit quis, blandit tristique lacus. Pellentesque et orci dui. + +Integer tempor mollis purus ut volutpat. Pellentesque efficitur cursus neque in ullamcorper. Integer ac tincidunt lacus, at venenatis tellus. Curabitur convallis commodo enim a convallis. Aenean sagittis sodales nibh, sed eleifend diam ultricies at. Vestibulum erat mauris, lobortis ac tempor a, viverra non sem. Nulla non ipsum mollis, dignissim elit a, laoreet enim. + +In laoreet purus eget orci rhoncus viverra. Quisque vel metus quis nulla hendrerit viverra. In consectetur velit vitae purus vestibulum, in hendrerit odio sollicitudin. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla ut est sed ligula tincidunt fermentum nec ac nulla. Nam in sagittis velit. Vivamus vel dapibus erat, ullamcorper sollicitudin metus. + +Quisque pulvinar nulla eget mi mattis, ut convallis nulla ullamcorper. Vivamus placerat mauris vel elementum aliquet. Sed ac accumsan eros. Vivamus vitae rhoncus urna. Fusce gravida cursus varius. In posuere finibus leo cursus molestie. Interdum et malesuada fames ac ante ipsum primis in faucibus. Mauris vulputate mi ut nunc finibus convallis. Praesent luctus erat eu urna facilisis convallis. + +Nam efficitur tempus augue varius porttitor. Pellentesque scelerisque dolor scelerisque, dignissim massa eget, iaculis nibh. Praesent viverra molestie dui a pulvinar. Mauris malesuada mattis felis, vitae sagittis metus pellentesque viverra. Phasellus faucibus congue blandit. Pellentesque mattis, justo sed imperdiet rutrum, metus metus porta nisi, vel malesuada lorem massa at dolor. Nullam nisi eros, tristique quis mollis ac, hendrerit nec leo. Praesent viverra molestie vestibulum. Nullam eu aliquam risus, at dignissim diam. Quisque blandit fermentum erat, sed ullamcorper augue pellentesque in. Integer eleifend libero non lacus sagittis auctor. Aliquam volutpat interdum accumsan. + +Etiam in eros porta, bibendum lacus eget, feugiat orci. Integer massa dui, commodo ac luctus nec, ornare id velit. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Duis dignissim scelerisque ex. Aliquam tempus cursus nibh, sed scelerisque libero sagittis ut. Morbi finibus vestibulum nulla, nec aliquam urna venenatis vulputate. Pellentesque ultricies, lorem a dignissim bibendum, augue nisi eleifend libero, cursus ultrices nulla purus a nunc. Quisque dictum pulvinar turpis vitae finibus. Etiam eget ultrices diam. Sed commodo enim ante, fermentum rhoncus tortor tincidunt ac. Suspendisse fermentum aliquet erat non posuere. Aenean vel dapibus lorem. + +Aenean lorem libero, rutrum vel egestas vel, pellentesque ut ante. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum felis diam, pellentesque sit amet ultricies sed, vestibulum in leo. Morbi aliquet, quam ut fringilla sollicitudin, justo risus suscipit mi, in vulputate neque erat ut turpis. Duis auctor dui non urna sagittis dictum. Vestibulum nec felis vel sapien congue volutpat a a orci. Phasellus tincidunt libero ac lorem feugiat, ac elementum lectus eleifend. Praesent sit amet est id massa convallis congue. Integer ac nunc eget orci pharetra vehicula. Mauris et ultricies diam. Nunc et pellentesque lacus, eget bibendum sapien. Morbi condimentum mi ac metus euismod convallis. Proin consequat rhoncus varius. Maecenas feugiat elementum vulputate. + +Vestibulum tempor feugiat dapibus. Ut ornare in augue non euismod. Nulla faucibus gravida condimentum. Curabitur pharetra dui non lorem sagittis iaculis. Quisque vitae nibh magna. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Fusce quis pharetra sem, sed aliquet lectus. + +Fusce volutpat tempor tincidunt. Pellentesque ultricies imperdiet tincidunt. Integer porta dictum lorem, non luctus tellus bibendum sit amet. Quisque posuere felis et est dapibus, commodo rutrum leo molestie. Fusce sodales felis sed sem pharetra pretium. Praesent nec sollicitudin nisl. Donec volutpat convallis elementum. Duis id libero augue. Nulla facilisi. + +Nulla viverra, urna nec efficitur vulputate, metus odio lacinia purus, in sagittis elit erat vel massa. Fusce ligula leo, finibus non felis dignissim, dictum ullamcorper odio. Phasellus vel eros eu sem venenatis sagittis a nec nulla. Pellentesque sapien elit, lobortis quis dictum et, maximus vitae metus. Curabitur quis aliquam eros. Quisque cursus condimentum urna vel efficitur. Etiam aliquet lorem ut varius suscipit. In viverra molestie felis vitae vehicula. Curabitur cursus, nunc sodales faucibus ullamcorper, urna magna dapibus velit, accumsan blandit mi quam nec justo. Donec ac dui lorem. Sed eu sagittis nulla. Suspendisse ut ante lacus. Fusce non tristique felis. Nulla convallis consectetur arcu, semper egestas urna efficitur quis. Nulla ac turpis at leo pretium maximus. Morbi cursus diam ac purus imperdiet, non commodo tellus cursus. + +Mauris ut eros suscipit, suscipit nisi vel, porttitor sapien. Morbi in nulla sapien. Cras maximus pretium justo, vel eleifend urna vulputate sed. Aenean egestas nisl ex. Curabitur sed pharetra ligula. Nulla blandit lorem id mauris tincidunt, at elementum risus aliquet. Quisque id interdum dui. + +Aenean nec elit condimentum ex viverra hendrerit. Pellentesque blandit nibh elit, a ultrices orci vestibulum vitae. Donec ultricies malesuada justo ac luctus. Pellentesque laoreet nunc a sem varius, ac dapibus ligula volutpat. Aenean sem felis, aliquam nec ullamcorper quis, ullamcorper non ante. Pellentesque libero leo, sagittis nec tempus a, tincidunt eget risus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec elementum massa vel diam suscipit suscipit. Suspendisse eget neque porta, cursus orci eget, imperdiet libero. + +Vivamus faucibus vulputate sapien, non pulvinar ex sodales vitae. Maecenas consequat est urna, id faucibus eros dictum at. Vestibulum tortor mi, porta vitae sagittis sed, convallis quis enim. Vestibulum gravida justo pulvinar sem ornare, efficitur dictum neque varius. Nullam egestas congue tellus vitae interdum. Nam lectus erat, porta vitae lorem non, mollis semper velit. Nulla vestibulum tincidunt elit. Phasellus sed vulputate leo. + +Sed efficitur quam ac iaculis volutpat. Praesent nec feugiat ex. Aenean at ligula iaculis, imperdiet lacus et, condimentum magna. Integer euismod leo id mauris condimentum, quis semper lacus blandit. Sed volutpat neque urna, sit amet ullamcorper est dignissim non. Vestibulum tristique sollicitudin risus, sed hendrerit massa finibus id. Vestibulum sit amet risus non eros vehicula malesuada. Nam facilisis lacus sed quam pulvinar, at fermentum lectus tincidunt. + +Vivamus laoreet sem nec odio blandit, ut ornare sem egestas. Suspendisse potenti. Nulla aliquam pretium volutpat. Maecenas a orci suscipit, aliquam eros a, maximus urna. Aliquam eu gravida justo, et hendrerit justo. Suspendisse a commodo lorem, quis viverra nisl. Nulla vel pellentesque nisl. Nulla ligula tellus, vehicula sit amet turpis in, sodales tincidunt tellus. Proin ut nibh sed ipsum porta dignissim vel sed mauris. Interdum et malesuada fames ac ante ipsum primis in faucibus. In hac habitasse platea dictumst. Vestibulum efficitur nulla rutrum, accumsan lacus at, dignissim tortor. Morbi egestas metus ut nibh tincidunt posuere at sed elit. Integer eget hendrerit nulla. + +Donec sagittis sagittis tempus. Proin iaculis neque vehicula commodo faucibus. Pellentesque erat ante, vehicula sed efficitur vitae, varius non sem. Mauris pellentesque, felis nec egestas scelerisque, nulla nunc fringilla arcu, feugiat fringilla quam neque in elit. Nullam ut ex odio. Mauris enim risus, consequat at suscipit at, pulvinar ut arcu. Fusce mollis sem sed tellus tempus pellentesque. In pharetra, libero sed tristique vestibulum, massa velit egestas risus, at mollis augue lorem sit amet turpis. Mauris risus dui, sagittis vitae congue ut, condimentum ut augue. Quisque non sollicitudin purus, sit amet consectetur sapien. + +Sed scelerisque ipsum sed augue varius, sit amet ultricies purus posuere. Etiam vehicula at nunc in posuere. Cras eget risus a sapien mollis facilisis. Morbi vel purus auctor, faucibus mauris non, malesuada leo. Donec venenatis consectetur libero in pretium. Ut efficitur molestie metus id scelerisque. Nunc dolor justo, pharetra vel sagittis vitae, placerat ut justo. Donec tempor fermentum est semper auctor. Nullam tincidunt risus non risus elementum varius. Suspendisse euismod augue metus, nec pellentesque enim sagittis ac. Morbi et lectus quis justo commodo varius commodo vel risus. In bibendum purus in erat ullamcorper, sit amet dignissim sapien scelerisque. Quisque tempus elementum rutrum. + +In viverra sollicitudin pretium. Sed finibus scelerisque sollicitudin. Nulla lobortis purus in lectus iaculis, a ornare ex accumsan. Morbi malesuada mollis placerat. In hac habitasse platea dictumst. Pellentesque sed dui quis odio scelerisque molestie eu nec libero. Proin viverra magna ligula, at luctus lacus posuere sed. Nullam non congue mi. Praesent non mattis neque, sit amet tempus diam. Mauris eget cursus lacus, id dictum mi. + +Sed semper velit in vehicula tristique. In lobortis est augue, et maximus tellus iaculis ut. Proin quis ex maximus erat iaculis imperdiet. Curabitur ultrices turpis ac nibh congue ornare. Fusce a aliquet felis, nec sodales tellus. Nunc mauris ante, feugiat et facilisis at, pharetra ultricies dui. Integer felis metus, porttitor ac ipsum vitae, placerat varius ex. Morbi in dui a nunc aliquam posuere. Nulla tempus tortor ac lorem consectetur sodales. Praesent sit amet placerat diam. Curabitur sodales mauris ante, et tincidunt lacus ultricies quis. Nulla eu finibus nisl, sit amet volutpat leo. Donec ligula enim, feugiat non aliquet nec, congue interdum lorem. Pellentesque a nisi blandit, semper massa sit amet, auctor eros. + +Aenean ligula libero, commodo a erat at, tristique facilisis quam. Sed congue fringilla lacus, vel iaculis quam suscipit ornare. Curabitur non pretium mi. Donec condimentum odio dui, id malesuada ipsum porta ac. Aliquam imperdiet cursus ullamcorper. Duis cursus tincidunt nibh sit amet cursus. Aliquam urna orci, sodales ac ipsum at, placerat lobortis neque. Sed sit amet convallis felis, vestibulum finibus arcu. Phasellus venenatis egestas felis, id aliquam turpis iaculis non. Proin a lacus ut felis malesuada sodales. Duis elementum, eros ac placerat bibendum, quam nisl varius mauris, vel venenatis neque augue et justo. Nunc varius sem velit, vel tempus mi aliquet id. Fusce purus massa, mollis id nulla non, dignissim cursus massa. Sed sollicitudin interdum felis, nec eleifend nisl feugiat eget. Nulla rutrum id odio ut consectetur. Maecenas fermentum turpis vitae libero ullamcorper suscipit. + +Vestibulum auctor lectus ligula, non sollicitudin odio maximus nec. Cras faucibus, felis sed suscipit tempor, risus lorem consectetur est, eget pulvinar nibh dui eu dolor. Vestibulum pellentesque, ex aliquam vulputate laoreet, libero lorem rhoncus risus, vitae congue velit justo vitae nisi. Nullam placerat venenatis tortor, sit amet lacinia augue placerat nec. Quisque vulputate lectus vel pulvinar condimentum. Nullam at libero iaculis, mattis purus vel, tincidunt diam. Suspendisse eu ullamcorper eros. Nulla id eros odio. Ut enim leo, ultricies quis mauris sed, interdum commodo felis. Etiam enim lacus, scelerisque a interdum eget, mollis vel tellus. Phasellus vel vulputate orci. Nulla ornare leo sed arcu rhoncus convallis. Duis libero arcu, pulvinar ut tellus vitae, aliquam euismod magna. Donec semper nisi eget nulla tempus, sed condimentum massa sollicitudin. + +Suspendisse bibendum ante vitae ullamcorper semper. Praesent dui est, elementum nec lacus in, pellentesque accumsan sapien. Cras quis urna at lacus posuere commodo eget nec arcu. Nunc varius ante quis ligula rhoncus, ut dignissim metus commodo. Integer volutpat gravida nunc eget faucibus. Quisque imperdiet, mauris vitae cursus malesuada, lacus mauris tempus sem, ut accumsan purus lectus quis nisi. Vestibulum aliquam dui sed nisl laoreet, id posuere quam imperdiet. Aliquam ultricies non enim vel tempor. Donec sed feugiat justo, vel rutrum enim. Phasellus lorem quam, dapibus a tincidunt vulputate, pellentesque non lorem. + +Nunc quam diam, condimentum sit amet enim semper, hendrerit laoreet magna. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nullam et pulvinar leo, ac fermentum lectus. Sed erat ante, mattis ac tempor vitae, euismod at tortor. Ut sagittis fringilla scelerisque. Ut pulvinar molestie leo eu faucibus. Sed quis efficitur purus. Pellentesque nibh nisi, porta id orci id, sagittis sodales tellus. Ut venenatis velit a lectus lacinia, at ultrices nisi commodo. Vivamus eros orci, ornare vel ullamcorper elementum, posuere id ligula. Aliquam non massa pellentesque, semper ipsum scelerisque, dapibus leo. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nulla pretium leo diam, sit amet bibendum lacus tempus quis. Interdum et malesuada fames ac ante ipsum primis in faucibus. Integer quis pulvinar nibh. + +Mauris sed eros nisi. Cras malesuada, turpis vitae laoreet porta, sapien odio maximus nulla, ut efficitur mauris odio tincidunt elit. Nam ut urna eros. Sed at vestibulum risus. Nulla luctus pulvinar rhoncus. Pellentesque maximus ligula a est ullamcorper, sed tempus tortor ultrices. Curabitur ligula odio, mollis sit amet risus quis, tempor auctor magna. Suspendisse vel quam quis lorem commodo facilisis. Donec eu ipsum suscipit, molestie dui sed, fringilla dui. Proin placerat ex a lorem sodales convallis. Sed molestie quam mi. + +Fusce laoreet nec dolor id bibendum. Nunc condimentum vulputate massa lacinia fermentum. Donec maximus cursus eros sed porta. Nunc eu porta turpis. Nulla cursus et nisl eu elementum. Ut rutrum scelerisque aliquet. In tellus erat, rhoncus id tempus vitae, vestibulum non quam. + +Vivamus luctus elit a efficitur dapibus. Praesent magna mi, pellentesque sed accumsan dapibus, blandit at lectus. Praesent sodales erat diam. Pellentesque placerat lobortis enim, a dapibus ligula molestie ut. Phasellus dui augue, suscipit ac urna non, iaculis eleifend augue. Maecenas sed elit iaculis, pretium ligula lacinia, aliquam libero. Nulla sem mi, pellentesque viverra lorem vel, luctus vulputate augue. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Quisque fermentum nunc ex, at lobortis turpis congue vitae. Suspendisse iaculis elementum enim commodo facilisis. Vestibulum non neque tellus. Proin vitae sodales urna. Mauris interdum purus et neque tempus, vitae mattis libero finibus. Suspendisse iaculis condimentum nibh, eu mattis enim vehicula a. + +Fusce dictum urna non lectus ullamcorper vestibulum id in elit. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Nunc fermentum odio non ante sollicitudin posuere. Praesent vulputate quam nec magna euismod pellentesque. Etiam tempor nunc eget velit volutpat eleifend. Mauris sodales nunc ullamcorper, gravida purus eget, rutrum sem. Nam a sapien rhoncus, scelerisque lacus ac, condimentum ipsum. Pellentesque suscipit, ligula ut rhoncus ullamcorper, ligula augue rutrum lorem, eget aliquam risus tortor eget metus. Curabitur scelerisque bibendum purus vel vulputate. Morbi et odio at neque sodales suscipit. Nam at pretium nulla. In scelerisque vulputate suscipit. Nulla facilisi. Pellentesque eu sem eu ligula efficitur viverra. Pellentesque gravida consequat urna id bibendum. + +Nulla consectetur facilisis est nec aliquam. Proin hendrerit magna suscipit ante accumsan hendrerit. Nulla ut sem id neque tempor vehicula. Sed eget massa eget sem placerat tincidunt. Ut eleifend, justo sit amet egestas lacinia, ex velit pharetra nulla, nec aliquet elit neque ut turpis. Quisque in gravida velit. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed euismod malesuada convallis. Ut ultricies, magna et blandit fringilla, dolor dolor maximus sapien, in aliquet augue est vel odio. Nulla commodo elit massa, euismod tempor turpis aliquet eget. Morbi non odio et tortor egestas ultrices. Etiam semper, ipsum et dictum pharetra, eros purus bibendum lacus, vel laoreet lacus sem vehicula nibh. Nunc enim nulla, pulvinar sit amet lobortis sed, porttitor molestie risus. Morbi tincidunt diam mi, nec auctor sem rutrum at. + +Donec pellentesque odio odio, eu condimentum risus consectetur quis. Vivamus ullamcorper, mauris non semper rutrum, dui risus suscipit tellus, ut tempor velit risus vitae nibh. Duis tempor nibh at tristique rutrum. Cras congue nisl at sem sollicitudin efficitur. Aenean auctor purus vel libero fermentum elementum. Mauris convallis orci id interdum accumsan. Aliquam at metus risus. Sed accumsan, quam id dignissim congue, velit risus eleifend ligula, ut fringilla elit erat at neque. Aliquam tempor, quam ut ultrices volutpat, orci lectus vulputate turpis, eget pharetra purus nisi non lorem. Ut condimentum convallis justo, a volutpat eros. Sed tempus turpis leo, in convallis est fringilla sed. Vivamus eu laoreet tellus. Quisque ullamcorper ullamcorper leo. Nam fermentum egestas facilisis. Nulla egestas ligula feugiat urna molestie, faucibus convallis erat accumsan. Cras pellentesque ipsum lectus, vitae luctus dolor suscipit nec. + +Vivamus vel nibh sed mi tincidunt cursus quis facilisis tellus. Donec eget est eu velit pretium molestie. Maecenas lacinia risus turpis. Sed tristique id risus sit amet venenatis. Duis maximus, metus ac molestie convallis, quam ex dapibus sem, at rutrum metus nisi quis lectus. Suspendisse sodales in nisi at hendrerit. Maecenas suscipit lobortis vulputate. Nunc convallis sit amet elit eget porta. Mauris tincidunt massa in augue finibus iaculis. Vestibulum imperdiet, orci vel bibendum laoreet, ligula quam mollis lacus, a eleifend nisi tellus vitae ipsum. Cras non ante sollicitudin, auctor dolor id, condimentum tellus. Morbi malesuada leo nec lectus scelerisque, nec interdum lacus ornare. Integer ultrices ligula nunc, sed blandit urna aliquam eget. Proin consequat viverra ex non rhoncus. Phasellus id nibh at tortor sodales blandit. Donec dignissim ipsum vel ligula malesuada rhoncus. + +Nullam mauris sem, dapibus ac nisl at, hendrerit faucibus nisi. Mauris dictum fermentum pellentesque. Praesent in gravida odio. Vestibulum pharetra iaculis est quis tincidunt. Sed venenatis rhoncus lacus, non porta ante vulputate at. Duis fringilla ipsum at urna euismod molestie. Duis a porttitor ante. Mauris pharetra elit et metus auctor, a eleifend sapien porta. Vivamus ut tincidunt purus, lobortis euismod tellus. Nullam non nulla gravida, laoreet tellus ac, placerat urna. + +Sed justo sapien, scelerisque nec nisl vel, efficitur aliquet purus. Phasellus eget posuere nisl. Integer porta vel purus nec ornare. Pellentesque rutrum in nulla at hendrerit. Nullam elementum sodales volutpat. Duis tempus, purus sagittis auctor ullamcorper, dui erat hendrerit nulla, id finibus eros dui quis risus. Quisque posuere nunc quis augue placerat lacinia. Nunc quis lorem vulputate, tincidunt enim nec, rhoncus odio. Ut porttitor porta velit ut vulputate. Duis convallis sagittis magna nec gravida. Ut eu luctus sem, non placerat erat. Vivamus viverra ut ligula ac finibus. Cras ligula urna, tincidunt id risus eu, dapibus viverra lorem. Aliquam erat volutpat. Sed dolor nisi, faucibus non ligula faucibus, laoreet bibendum diam. Vivamus sagittis lacus ut condimentum tincidunt. + +Proin tristique mi ullamcorper dolor accumsan mollis. Nulla facilisi. Pellentesque iaculis mattis augue ac rutrum. Mauris in nulla at ligula fringilla pulvinar. Nam commodo ornare nibh ac viverra. Maecenas eget augue a risus mattis ullamcorper non at nibh. Aenean nec erat diam. Pellentesque augue nisl, venenatis mollis dolor non, bibendum malesuada leo. Pellentesque elementum purus ornare mauris iaculis porttitor. Sed mollis tempor dui eu elementum. Donec porttitor tellus non mattis gravida. Mauris venenatis suscipit convallis. Sed dolor sapien, pellentesque et tellus a, tempus cursus magna. Nunc lobortis leo magna, at maximus orci ultrices eget. Nullam vulputate dictum nisi, vel egestas orci. + +Nam nibh mi, ornare sit amet nibh vel, lobortis lacinia leo. Ut egestas mi vel nunc luctus vestibulum. Nullam id tempus felis, eget convallis erat. Aliquam venenatis ut tellus id fringilla. Aliquam erat volutpat. Sed vehicula, odio nec mollis dapibus, ligula sapien consequat risus, ut volutpat diam neque ut neque. Mauris venenatis libero vitae sem elementum, sit amet maximus massa varius. Maecenas malesuada mi id leo euismod, eu maximus sapien vehicula. Nulla facilisi. Duis nec venenatis ante, non pulvinar lacus. In sollicitudin sit amet velit suscipit egestas. Maecenas a lectus euismod, dictum nulla at, malesuada lectus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec lorem massa, laoreet non elit nec, sollicitudin blandit felis. + +Cras dignissim fermentum tellus ut fringilla. Maecenas maximus eros pretium sagittis venenatis. Maecenas id enim ac est semper efficitur ac hendrerit leo. Cras eu arcu tincidunt risus pellentesque aliquam. Pellentesque vel sodales libero, non rhoncus enim. Integer vulputate vulputate libero, eu consectetur eros euismod vitae. Fusce magna nibh, vehicula sed turpis eget, malesuada ultricies sem. Sed convallis sem nisl, rhoncus mollis mauris bibendum ut. Pellentesque vitae massa a justo dapibus laoreet. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Sed mollis egestas nisl vel ultricies. Phasellus sit amet varius leo, sed vehicula sapien. Etiam in urna eget neque aliquet ultricies in nec quam. Aliquam at feugiat elit. + +Maecenas placerat nisi ultrices neque pulvinar, et ultrices ante tempus. Vestibulum laoreet quam lacus, eget commodo magna dictum consectetur. Aliquam erat volutpat. Aenean tincidunt ipsum sit amet justo aliquet tempus. Quisque blandit sit amet est facilisis dictum. Sed non consequat dui. Integer purus diam, gravida quis tortor lobortis, iaculis vehicula sem. Morbi pellentesque est elit, vitae interdum elit laoreet et. Vestibulum in blandit ante, eu blandit velit. Morbi sagittis eu est eu vulputate. Nullam ac mi feugiat nibh feugiat suscipit. Sed interdum tincidunt efficitur. Integer dictum sem erat, ut pharetra libero lacinia in. Mauris at hendrerit odio, a facilisis lacus. Donec blandit massa ac nisi ultrices faucibus. + +Aenean tempus id ligula at mollis. Cras id dui magna. Integer id neque tincidunt, rhoncus nunc et, laoreet nibh. Pellentesque consectetur odio ligula, et consectetur tortor scelerisque non. Cras quis ex pharetra mi ornare lobortis. Phasellus fringilla interdum felis, vel aliquam ligula. Mauris cursus varius turpis in iaculis. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Praesent eget mi id nulla semper dapibus. + +Vivamus lacus augue, eleifend dignissim ipsum eu, interdum accumsan massa. Nam id quam vel ligula consectetur volutpat id eget eros. Aliquam sed felis velit. Duis egestas velit ac dolor blandit, ut elementum mi tincidunt. Integer varius nisl a sapien pellentesque, et pellentesque ante elementum. Fusce ac orci interdum, faucibus nisl ut, sagittis nisi. Vestibulum sed diam eu magna congue ornare. Integer dignissim ligula sit amet mauris pretium vulputate. Duis ac tincidunt magna. Sed vehicula, ante nec vulputate venenatis, mauris odio blandit dolor, vitae lacinia nibh lorem et metus. + +Sed non sapien vel lorem tempor semper ac ac tellus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas auctor ante sit amet suscipit vulputate. In ac cursus turpis. Donec eu sem bibendum, blandit est nec, blandit tortor. Pellentesque scelerisque justo vitae magna cursus, vitae egestas libero interdum. Pellentesque vitae ligula vel mauris vehicula convallis. Nunc ornare lectus sit amet lorem aliquet dignissim. Cras nec ligula pretium, semper nulla a, pulvinar lacus. Sed ac augue bibendum, posuere ipsum eu, venenatis quam. Sed elementum nunc quis odio pellentesque, a vestibulum ex congue. Cras id malesuada arcu. Mauris ut egestas nulla, sed tempus ligula. Donec ac elit ut nisi pulvinar elementum. Suspendisse id auctor lectus, vitae ultricies lacus. + +Vestibulum pulvinar ornare cursus. Curabitur accumsan sollicitudin mi in vestibulum. Vestibulum vulputate tincidunt luctus. Suspendisse potenti. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Phasellus a mattis neque, id malesuada odio. Aliquam id auctor magna. Vestibulum quis nibh lacinia, tincidunt felis sed, vestibulum ex. + +Donec placerat ipsum diam, vel imperdiet velit eleifend ac. Quisque dapibus erat non dui convallis eleifend. Praesent pellentesque felis id suscipit rutrum. Donec quis lacinia turpis. Nulla justo eros, fringilla vel fringilla in, euismod sollicitudin arcu. In ut lobortis arcu, at rhoncus elit. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. + +Pellentesque iaculis quam nec interdum eleifend. Fusce mauris mauris, imperdiet sollicitudin volutpat eu, sollicitudin blandit leo. Vestibulum hendrerit diam gravida erat imperdiet, blandit dignissim dui tempor. Phasellus viverra ante quis aliquam finibus. Duis sed condimentum augue, nec sollicitudin dui. Ut ullamcorper metus ac ligula accumsan, ac fringilla metus gravida. Morbi rhoncus est nunc, at lacinia ex fringilla sit amet. Integer lobortis ultricies nisi vitae accumsan. Nulla nec sagittis ante. Mauris bibendum nisi ut magna vulputate maximus. Praesent in elit eu metus dignissim cursus. Ut a tellus felis. Mauris eget ornare diam. + +Suspendisse potenti. Nam nec tortor ex. Duis eu elementum ligula. Pellentesque efficitur sodales orci, vitae sollicitudin odio tempor ac. Nam lacus ante, lobortis vitae lobortis eu, eleifend et velit. Nulla et lectus eu nibh tristique malesuada a sit amet felis. Proin molestie, lectus vitae sagittis malesuada, massa velit finibus est, id vestibulum dolor felis eget lectus. Phasellus varius pellentesque neque, a malesuada ex mattis eget. Nulla facilisi. Phasellus interdum placerat lobortis. Sed et odio tristique, viverra libero et, sagittis diam. Proin tristique lectus id bibendum maximus. Integer ornare euismod ligula nec malesuada. Nulla facilisi. Nullam bibendum hendrerit pharetra. + +Duis pharetra auctor felis, eget aliquet justo commodo at. Donec quis nibh non arcu sodales efficitur. Aenean lorem sapien, tincidunt eu sapien nec, convallis tempor diam. Curabitur volutpat, felis ut congue euismod, lacus nisl euismod ipsum, vel consectetur orci nibh sed ligula. Curabitur consectetur vitae mauris sed tempor. Fusce vel pretium nibh, et tristique dolor. Aliquam semper a ante non finibus. Praesent euismod urna augue, at feugiat orci commodo ut. Duis imperdiet purus non augue cursus gravida. Maecenas sodales purus et sollicitudin venenatis. Nam ultrices lorem lectus, ut pretium turpis hendrerit eget. Vestibulum vel lacinia lectus, at dignissim diam. Vivamus ut tortor ac tortor blandit finibus. Etiam porttitor tortor sit amet elit lacinia gravida. Pellentesque pretium, orci vitae tempor vehicula, nisi tellus tincidunt sapien, id egestas felis quam a nunc. Pellentesque sed vestibulum nisl. + +Mauris congue, justo vel dapibus pretium, ipsum augue consectetur leo, at aliquam ipsum neque non mauris. Nam sollicitudin in urna eu blandit. Aliquam erat volutpat. Morbi eget interdum ante. Pellentesque congue gravida arcu, eget ornare ante elementum nec. Integer a auctor dolor, in varius sem. Etiam dictum nibh magna, at volutpat nunc blandit eu. Curabitur at sem ipsum. + +Nulla porttitor erat eget volutpat iaculis. Pellentesque egestas, nisl vel ultrices efficitur, ex magna condimentum urna, in tincidunt massa turpis eget metus. Etiam vitae magna elementum, fermentum justo ut, pretium velit. Aliquam aliquet venenatis malesuada. Quisque consequat enim metus, pellentesque blandit leo rhoncus id. Vivamus vestibulum, est in condimentum mollis, ligula eros blandit lorem, vitae dignissim lectus mi eget nulla. Cras posuere leo at neque convallis, sed pretium massa ultrices. Morbi tempus porttitor turpis. Nam vitae mauris et massa venenatis tincidunt. Quisque vestibulum lacus ligula, in sodales felis elementum vel. Sed a tellus condimentum, sodales nisi id, aliquet elit. + +Vestibulum ac ex eu turpis lacinia condimentum nec eget ipsum. Nulla non imperdiet erat, at malesuada lorem. Praesent efficitur imperdiet augue vel laoreet. Sed dignissim, ante non porta feugiat, enim nunc rhoncus lorem, eu luctus turpis risus sit amet orci. Vivamus bibendum pharetra venenatis. In at gravida nisi. Vestibulum pulvinar sapien ac augue scelerisque, vitae blandit nibh malesuada. Nunc vitae est faucibus, accumsan risus a, laoreet urna. Cras imperdiet lacus in augue facilisis dignissim. Duis sed nisi quis diam mollis rutrum. + +Vestibulum eget egestas neque. Praesent viverra, velit quis porttitor euismod, sem libero venenatis mauris, id congue urna nulla sed lorem. Nulla mollis metus nec diam malesuada aliquam. Duis ac risus nunc. Cras sollicitudin urna nunc, id sodales quam gravida sit amet. Fusce in vulputate orci, in venenatis lorem. Donec a sagittis ipsum. Quisque consequat sapien tellus, sed efficitur lacus aliquam eu. Interdum et malesuada fames ac ante ipsum primis in faucibus. Aenean in neque at augue elementum commodo pellentesque eget ligula. Nullam condimentum efficitur tincidunt. Phasellus posuere tincidunt odio sed facilisis. Aenean eu risus at est euismod facilisis. Curabitur elit purus, malesuada quis blandit id, rutrum vitae dui. Praesent porta rutrum erat, ullamcorper viverra nunc. Cras ut velit dui. + +Etiam posuere pulvinar mi at ullamcorper. Pellentesque finibus, tellus non convallis commodo, orci nibh dapibus nisl, at aliquam purus nulla eget dui. Praesent fringilla urna nec nulla pellentesque, nec rhoncus turpis ultricies. Sed laoreet velit pellentesque libero varius, ac interdum urna viverra. Phasellus sed consectetur massa. Morbi quis velit nec ipsum varius tempor. Proin id sodales felis. Aliquam lacinia risus quis ligula condimentum sodales. Nulla vel arcu aliquet neque iaculis aliquet. Cras sed lorem eu turpis tincidunt sodales. Sed pulvinar elementum ligula, nec faucibus nisl. Fusce nec tellus eget dui tempor sagittis. In vitae enim in ex viverra commodo. Duis est erat, fringilla ac semper a, dapibus in tortor. + +Maecenas commodo vulputate iaculis. Aliquam non facilisis est. Donec pellentesque vitae nibh nec volutpat. In commodo metus placerat lorem commodo, non lacinia nibh bibendum. In viverra rhoncus erat. Mauris nec nisl blandit, elementum justo nec, accumsan libero. Aliquam elementum, velit et ullamcorper convallis, turpis lorem elementum lorem, quis consequat tortor eros ut erat. Curabitur et enim quis felis vulputate congue et vel purus. Sed elementum interdum ipsum, sed tincidunt arcu scelerisque et. + +Aenean interdum elementum mauris ut porta. Mauris vel purus ac odio vulputate pulvinar at quis odio. In turpis turpis, convallis in augue id, elementum vulputate lorem. Sed tincidunt fermentum vulputate. Nunc ipsum ipsum, molestie vel convallis id, pharetra vel arcu. Maecenas vel dui elit. Sed blandit dolor sit amet risus commodo faucibus. Duis rhoncus felis arcu, vel aliquam nisi faucibus sed. Suspendisse cursus eget nunc ut bibendum. Fusce non ligula risus. Curabitur vitae cursus metus, quis fringilla diam. Phasellus turpis ante, pulvinar ac turpis tristique, sollicitudin congue lectus. Proin quis ipsum at ipsum euismod euismod. + +Nunc ut fermentum nunc. Donec id commodo lacus, at hendrerit justo. Donec cursus purus sodales nunc commodo, quis bibendum elit hendrerit. Quisque quis pellentesque nibh, ac vulputate neque. Aenean non placerat felis, eget feugiat ligula. Vivamus a eros accumsan, cursus neque at, ultricies magna. Fusce tincidunt tellus vitae mi rutrum laoreet sed quis ligula. Nullam ullamcorper ligula ligula, vel fringilla metus aliquet a. Morbi aliquet, mauris vel interdum venenatis, ex arcu venenatis tortor, at tincidunt dui ipsum et arcu. Nulla blandit gravida nulla ac iaculis. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Sed ullamcorper lectus in sapien lobortis, eget posuere massa varius. + +Cras lacinia nunc faucibus mauris placerat pretium eget non sapien. Morbi viverra bibendum posuere. Aliquam accumsan sagittis dolor non iaculis. Sed nunc odio, lobortis in dolor ac, rutrum fermentum velit. In venenatis, velit in molestie semper, est magna condimentum dui, aliquam auctor lorem nulla vel ligula. Morbi vehicula turpis turpis, quis mollis justo tempus vitae. Proin luctus lacus in mauris porttitor aliquet. Duis vitae nunc ex. Nullam eget erat vitae nulla iaculis rhoncus. Sed lacus dui, suscipit eu leo vitae, tincidunt dignissim risus. Praesent ut massa ut arcu sagittis consequat. Sed sit amet tincidunt turpis. Nulla bibendum, felis eget posuere dictum, libero mi tristique elit, at venenatis neque elit ac quam. Curabitur nisi sem, scelerisque nec nunc ac, pulvinar pharetra odio. Curabitur egestas pellentesque arcu sed suscipit. In mattis dolor vel dui mollis feugiat. + +Sed commodo, dui ac vestibulum dictum, tellus libero tincidunt lacus, viverra commodo est felis vitae urna. Proin tincidunt neque vel turpis eleifend laoreet. Vestibulum sagittis, tortor sed iaculis consequat, urna ante sagittis est, ac ullamcorper lorem nibh dignissim odio. Nam arcu mi, cursus et blandit nec, aliquam ut nulla. Aenean quis iaculis tellus, eu egestas augue. Etiam pretium eget nisi quis iaculis. Aliquam sed convallis eros. Ut bibendum rhoncus lacus, in vestibulum dui ultricies id. Fusce vestibulum, mauris ut tempor consequat, dolor nisl pellentesque elit, in porta arcu elit vel ante. Vivamus at nisl est. Etiam nec blandit tortor, at pulvinar orci. Proin semper dapibus tincidunt. Phasellus lobortis enim ullamcorper dolor tempor cursus. + +Mauris a libero in enim gravida aliquet ac sit amet nibh. Vestibulum ac neque posuere, blandit libero ac, vehicula enim. Aliquam auctor iaculis eros sit amet molestie. Quisque faucibus turpis et massa tristique, nec dapibus mauris aliquet. Proin blandit aliquet mauris, non tincidunt odio blandit vel. Curabitur a nibh in eros commodo tincidunt eu et libero. Curabitur sit amet dapibus ex, in condimentum magna. Sed eu sem sem. Nunc tellus dolor, rutrum eu mauris nec, congue feugiat purus. Fusce tempor, neque vitae bibendum imperdiet, dolor ipsum condimentum urna, et egestas quam tortor in ex. Aliquam velit magna, commodo hendrerit sagittis sed, feugiat eget erat. Nunc quis ullamcorper velit, eu consequat augue. Nam arcu mauris, condimentum sit amet magna in, finibus scelerisque nunc. Mauris erat est, hendrerit ac accumsan eget, facilisis ut nisl. Quisque dignissim arcu quis diam tincidunt tristique. Sed rhoncus nisl non enim fermentum, a lobortis dolor consectetur. + +Sed eget condimentum ligula. Vestibulum vitae cursus eros. Donec elementum sapien magna, posuere iaculis sem ultrices lobortis. Morbi eu bibendum lectus. Suspendisse ante eros, ullamcorper ac viverra eget, pellentesque sed sapien. Duis sit amet tincidunt dui, vitae lobortis purus. Sed venenatis tincidunt volutpat. Vivamus a nisl ac elit consectetur semper ut eu libero. Proin id cursus ex. In hac habitasse platea dictumst. Aenean sed nisi vitae odio venenatis pulvinar vitae ac risus. Sed varius magna ut erat luctus vehicula. + +Nunc non ex eget purus blandit faucibus at quis velit. Donec quis mi vestibulum, facilisis nisi quis, tincidunt turpis. Sed bibendum metus sed consectetur mollis. Maecenas fermentum, erat finibus pulvinar lacinia, ex risus dictum sem, ut vestibulum augue diam vel diam. Donec ac massa non nibh pretium laoreet eget in orci. Nulla placerat eleifend mi, pretium vestibulum diam condimentum vitae. Nunc odio turpis, feugiat vitae turpis eget, pellentesque commodo turpis. Etiam sapien purus, consequat nec mi eget, consectetur efficitur neque. Phasellus porttitor sapien sit amet nunc semper, vel bibendum nibh finibus. Ut ac imperdiet ex, eu congue felis. In posuere nisi felis. Mauris tempus pretium mauris, ac viverra nunc hendrerit id. Sed fermentum nec sem ac pulvinar. Integer dictum velit eget congue venenatis. + +Cras eros dolor, venenatis ac dictum sed, dignissim nec sem. Curabitur tempor erat quis pretium interdum. Nunc vestibulum justo nisi, sit amet sagittis tellus consequat vel. Donec pharetra nunc vitae consequat eleifend. Quisque ut mauris quis nunc volutpat consectetur. Nam a suscipit ligula, at gravida libero. Vestibulum blandit, tellus sed bibendum volutpat, libero tortor convallis nisl, sit amet placerat lorem dolor nec libero. Integer blandit libero elit, ut congue ex euismod congue. Nulla sodales justo id eros condimentum faucibus. + +Proin quam ante, hendrerit at bibendum eu, pharetra at lectus. Proin finibus arcu id nisl aliquam dapibus. Fusce a suscipit nisl. Ut placerat ultrices nibh nec efficitur. Vestibulum vitae interdum magna. Donec lobortis finibus risus, et luctus ipsum efficitur euismod. Quisque dictum diam et venenatis euismod. Cras dictum molestie aliquet. Vestibulum imperdiet quam eget diam malesuada, quis pulvinar odio dignissim. Curabitur sapien elit, iaculis vitae justo eget, pretium malesuada elit. Donec gravida molestie tincidunt. Nullam at commodo ipsum. Sed nisi erat, tincidunt ultricies interdum consequat, pretium mollis ipsum. Vivamus in erat consectetur, sodales nibh at, porta nunc. Mauris semper, dui vitae condimentum tempus, mauris justo volutpat urna, ac ullamcorper tortor dolor in sem. + +Aenean leo ligula, egestas at vestibulum ac, porta vel mauris. Curabitur at lorem non mauris fringilla venenatis vel eu arcu. Donec posuere eleifend diam. Duis aliquet justo lacus, eu egestas erat eleifend sed. Sed non cursus urna. Sed pretium purus id blandit varius. Mauris turpis mi, lobortis eu tellus sit amet, maximus venenatis felis. Proin non varius enim, aliquet finibus velit. Praesent posuere pharetra ipsum eget varius. Phasellus non fermentum magna, ut iaculis augue. Praesent ut nisl nunc. + +Sed ligula tellus, interdum at sapien ut, dictum pellentesque nisl. Duis fringilla, sem nec elementum dapibus, nisl tellus maximus velit, eu varius sem nisl feugiat eros. Maecenas quis viverra lacus. Nulla nec commodo ex, nec placerat enim. Curabitur ultrices sagittis fringilla. Vestibulum sit amet enim sagittis, condimentum nisl sit amet, pulvinar orci. Ut at erat finibus erat bibendum convallis. Quisque euismod magna eget leo facilisis hendrerit. Cras venenatis, nisi quis sollicitudin volutpat, metus enim vestibulum nunc, at mollis leo leo nec est. Fusce fermentum tristique feugiat. Suspendisse sem est, condimentum ut ante at, egestas convallis ante. Vestibulum dictum, ex nec imperdiet laoreet, magna est ullamcorper augue, vel molestie quam odio vel ante. Donec dui dui, posuere a neque ac, condimentum vehicula magna. Curabitur suscipit, risus vitae bibendum posuere, mauris lorem viverra est, at tristique arcu quam quis leo. Donec sed posuere neque. Suspendisse iaculis aliquet condimentum. + +Morbi tempus condimentum diam eget bibendum. Aliquam varius magna quis lectus interdum, in elementum ligula tempor. Morbi vitae lorem sapien. Morbi luctus consectetur eros non aliquet. Pellentesque vestibulum sem sed ante accumsan faucibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Etiam ac suscipit justo. Nunc efficitur lectus eu arcu venenatis, vel accumsan ex suscipit. Vestibulum egestas ultricies tellus, et interdum enim pretium eu. Aenean rutrum est tincidunt rhoncus molestie. Phasellus hendrerit tellus et laoreet varius. Integer efficitur felis magna, nec sollicitudin arcu sollicitudin in. Curabitur non feugiat odio. Mauris nisi odio, luctus quis dolor sed, tristique luctus ex. Nullam libero enim, facilisis ut venenatis et, vulputate sed purus. + +Mauris a odio ut leo porttitor ultrices a et ex. Pellentesque vestibulum lacinia faucibus. In pellentesque eget augue at feugiat. Integer finibus augue dolor, in luctus lorem rutrum vitae. Donec pharetra lectus ac purus sollicitudin, vel tristique mauris pretium. Cras porttitor mi eu lectus consequat, a ultrices felis venenatis. In condimentum turpis in velit mattis laoreet. Aliquam sed mauris id nulla ultrices convallis vel vel velit. Suspendisse ut arcu finibus justo fermentum fringilla. Etiam in malesuada nisl. In hac habitasse platea dictumst. Duis a est lacinia, pretium dui eget, condimentum turpis. Integer ac placerat augue. Donec et eros felis. + +Integer cursus magna id quam sagittis consectetur. Aliquam erat volutpat. Quisque ullamcorper nisl nec massa dapibus facilisis vitae at nunc. Donec laoreet, libero in elementum tempus, enim odio porttitor felis, venenatis fermentum augue velit eu urna. Ut at ullamcorper enim. In molestie, velit et blandit maximus, erat nunc laoreet quam, vel finibus est mauris non sapien. Donec et dictum lorem. Nunc vestibulum, dolor eget tempus maximus, elit eros aliquam velit, vitae mattis mauris lectus ac tortor. Donec rutrum justo orci, id dictum metus vestibulum a. Etiam bibendum ipsum convallis, lacinia turpis vitae, dictum tortor. In velit dolor, scelerisque sed molestie nec, volutpat a turpis. Cras posuere commodo erat ut gravida. Quisque ante ipsum, volutpat a tellus non, dignissim ornare elit. Pellentesque sed porttitor dui, luctus rhoncus purus. In vitae ante pulvinar, consectetur orci quis, tempus velit. Curabitur tempus ligula id sapien ornare rhoncus. + +Maecenas eu nibh et elit accumsan blandit a at erat. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Etiam commodo feugiat condimentum. In non ante ut mauris eleifend lobortis. Phasellus eleifend vitae metus et semper. Nam sit amet rhoncus diam. Nunc molestie libero sed erat volutpat consequat. + +Aenean eget tristique odio. Vivamus quam tellus, dignissim sed faucibus sed, sagittis ut elit. Maecenas in ullamcorper sem. In at ipsum accumsan lectus vestibulum commodo nec non leo. Aliquam at suscipit felis. Nunc non egestas tortor. Donec sit amet eleifend eros. + +Sed eleifend nisi velit, in egestas erat condimentum eu. Pellentesque a tincidunt urna. Interdum et malesuada fames ac ante ipsum primis in faucibus. Vestibulum dapibus mauris vitae elit auctor, id venenatis sem consectetur. Vivamus non leo pulvinar, blandit libero ut, vehicula arcu. Nullam elementum ex enim, at mattis massa pharetra eu. Nunc nulla magna, lobortis a magna sit amet, laoreet fermentum justo. Curabitur aliquam sollicitudin posuere. Aenean semper porta dictum. Mauris accumsan non nisi nec faucibus augue. + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam in nibh eget ante viverra mattis. Etiam elit est, pharetra efficitur fermentum at, accumsan id eros. Aenean accumsan nisi facilisis tempor suscipit. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nam magna erat, tincidunt eu placerat at, molestie at sem. In sit amet cursus mauris. Etiam ullamcorper lacus nisi, et porta metus semper id. Nulla mauris nunc, pharetra at facilisis a, consequat id metus. Sed convallis ligula non felis vulputate finibus. Curabitur nisl nulla, pharetra in justo a, dapibus ultricies dui. Morbi ultricies sollicitudin purus, quis pellentesque leo rhoncus sed. Curabitur sed eros quis lacus vulputate pretium. Vestibulum vitae urna nec arcu vulputate efficitur. Cras venenatis sodales dolor non ullamcorper. + +Donec eu placerat magna. Vestibulum justo lacus, luctus ac luctus sit amet, dapibus at orci. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec eu tortor ac justo pharetra hendrerit. Phasellus ornare lacinia vestibulum. Maecenas diam orci, molestie rhoncus fringilla vitae, aliquam eu purus. Maecenas vehicula felis dui, vel dapibus odio lobortis quis. Cras a velit non nisl efficitur tempor. + +Suspendisse magna enim, ultrices et elementum ut, venenatis ut purus. Curabitur convallis vel tortor id rhoncus. Pellentesque varius arcu non imperdiet eleifend. Vestibulum fringilla aliquet vehicula. Nullam et lorem ullamcorper, tincidunt ante eget, egestas ex. Donec laoreet, justo id tincidunt egestas, lectus diam faucibus tortor, nec venenatis felis leo a urna. In at tortor semper augue finibus ornare et vitae erat. Praesent vulputate, dui sed pulvinar porta, justo tortor feugiat tellus, eget accumsan velit turpis at orci. Nulla vitae velit facilisis augue sagittis fringilla nec sagittis nunc. + +Duis vel elementum odio, id eleifend mauris. Nam in ultrices nulla. Quisque pharetra blandit risus eget lacinia. Nulla mattis mi felis, a ultrices nisi egestas vulputate. Donec a augue ac ex laoreet semper at porta ante. Vestibulum arcu ipsum, vehicula vel mollis lobortis, ullamcorper sed augue. Ut viverra faucibus nunc, sit amet sodales diam gravida sollicitudin. Fusce finibus quam sed ornare consequat. Suspendisse viverra ultricies dui in volutpat. Maecenas blandit erat in rhoncus placerat. Phasellus eu nisi rutrum, bibendum nunc eu, viverra ex. Praesent sollicitudin mi sit amet dictum convallis. Vivamus purus ligula, interdum at tincidunt sit amet, pellentesque ut elit. Aenean vitae ultrices ex. In facilisis lectus est, nec vulputate diam tristique vel. In gravida tincidunt ex et viverra. + +Pellentesque mi justo, dignissim a nisl quis, tempor dictum lacus. Nam tristique varius dolor tincidunt pharetra. Nullam iaculis est sed quam dignissim cursus. Duis sit amet vehicula nisi, sed consectetur velit. Nullam non tellus nec dolor venenatis porta quis at quam. In pharetra id orci sed faucibus. Cras et malesuada erat. + +Quisque fringilla commodo metus nec feugiat. Donec et est urna. Maecenas ut magna dui. Vivamus eu sem venenatis, porta mi at, efficitur tortor. In lacinia hendrerit elit, in faucibus nulla ultrices et. Mauris accumsan nec orci et vestibulum. Aliquam eget justo velit. Mauris fringilla ullamcorper enim, in elementum enim eleifend a. Nulla id libero ac arcu tristique ultrices. Maecenas mattis orci vitae nisl laoreet auctor. + +Quisque id aliquam orci. Nunc molestie vitae quam eu consectetur. Vivamus molestie venenatis est, nec lacinia odio faucibus eget. Etiam ornare eu leo eget posuere. Quisque elementum ligula at euismod placerat. Maecenas lobortis leo diam, vel egestas odio iaculis ac. Integer dapibus mi metus. Sed at eleifend ligula, ullamcorper vestibulum eros. Suspendisse dignissim metus in vulputate iaculis. Nam rhoncus felis sit amet velit scelerisque semper. Ut sed augue eget nulla laoreet scelerisque. + +Aenean convallis ipsum id turpis gravida, et elementum ante facilisis. Donec vitae arcu id mauris mollis hendrerit. Mauris imperdiet cursus nibh at laoreet. Sed sit amet enim magna. Mauris blandit nisi quis arcu sodales, eget consequat orci euismod. Curabitur id bibendum diam. Ut pharetra tellus nec enim tristique, id efficitur eros accumsan. Suspendisse potenti. Praesent vel porttitor risus. Nulla vitae ex nec ex hendrerit euismod non mattis arcu. Aenean eget velit massa. Pellentesque venenatis arcu mauris, sit amet scelerisque sem lobortis ornare. Vivamus auctor porta eros, eget blandit lorem semper non. Nunc sed nibh commodo, hendrerit nunc at, malesuada nisl. Aliquam pretium leo sed iaculis vehicula. Vivamus interdum porta augue. + +Suspendisse potenti. Aenean sodales vehicula erat, vel sollicitudin eros eleifend id. Suspendisse hendrerit malesuada est, imperdiet tristique ex aliquet ac. Vivamus ultricies placerat lorem, ac placerat lectus sollicitudin ac. Etiam consequat odio vel bibendum pretium. Integer elementum nisl magna. Nam et vehicula risus, sed mollis tortor. Ut in molestie turpis. Nam luctus, elit molestie varius luctus, lacus ligula ullamcorper elit, non interdum ipsum neque non nibh. Curabitur vulputate facilisis suscipit. Sed vitae augue eu ex luctus lacinia ac vitae quam. Quisque non nibh ex. Praesent gravida efficitur dui, a vulputate nulla ultrices vitae. Duis libero dolor, facilisis in tempus vitae, pharetra non libero. Sed urna leo, placerat quis neque at, interdum placerat odio. Interdum et malesuada fames ac ante ipsum primis in faucibus. + +Vivamus in hendrerit elit, quis egestas sem. Sed tempus dolor finibus massa ultrices, sed tristique quam semper. Pellentesque euismod molestie facilisis. Vivamus sit amet ultrices elit. Ut eleifend, libero eu molestie maximus, ipsum elit pulvinar tortor, et aliquet nulla orci eu est. Nullam pretium, ligula at faucibus gravida, velit mauris pulvinar felis, sit amet tincidunt magna dui ut quam. Phasellus porttitor eu nunc id maximus. Suspendisse volutpat suscipit porta. + +Integer dictum augue purus, sed cursus eros blandit id. Vestibulum non nibh viverra, molestie lectus eu, vestibulum justo. Nullam a libero non nibh dignissim posuere id vel orci. Nulla viverra lorem eget condimentum scelerisque. Donec porta nunc sed sapien pretium dapibus. In hac habitasse platea dictumst. Suspendisse sit amet ligula elementum, accumsan ipsum vel, imperdiet massa. Fusce scelerisque non erat ac bibendum. Pellentesque consequat vehicula euismod. Morbi sapien nisl, ultrices ut scelerisque consectetur, ornare et orci. Maecenas efficitur eros a venenatis rutrum. Aenean bibendum dui in enim luctus posuere. Donec placerat porta eros eu dignissim. Fusce nulla velit, sodales eget nibh gravida, tincidunt venenatis urna. Aenean condimentum, massa in fringilla lobortis, turpis felis lacinia metus, sit amet facilisis nisl quam aliquet diam. Praesent fringilla vitae arcu nec lobortis. + +In tincidunt nisi ac dictum cursus. Sed dolor purus, posuere vel dolor vel, semper tempor elit. Integer nec scelerisque tellus, sit amet dictum magna. Donec hendrerit aliquet libero, a varius velit dapibus quis. Donec lectus turpis, egestas vel vestibulum vitae, iaculis quis eros. Maecenas id convallis metus. Duis quis tellus iaculis, lobortis massa vitae, condimentum eros. Pellentesque scelerisque nisl id hendrerit tempor. Donec quam augue, maximus eu porta ut, venenatis a ante. Curabitur dictum et nibh sed auctor. Nam et consequat odio. In vitae magna a dui porta pellentesque quis id magna. Sed eu nunc bibendum, imperdiet nunc sit amet, cursus diam. Vivamus in odio vel nibh sollicitudin molestie. Morbi sit amet leo et odio congue pharetra. Aliquam quis suscipit orci. + +Donec at ornare orci. Suspendisse nec tellus elit. Nam erat urna, laoreet at elit sed, tristique interdum ligula. Nullam dapibus orci sed lorem convallis fringilla. Cras urna ipsum, tristique maximus nisl quis, auctor mattis quam. Donec finibus consectetur lectus et interdum. Aenean posuere lectus vel turpis auctor finibus. Praesent molestie lectus ipsum, et tristique quam porttitor ac. Suspendisse aliquam pretium tortor, id egestas erat. Nulla mollis, orci at semper vulputate, nisl est pharetra diam, sit amet vulputate diam augue a sem. Donec in mauris felis. + +Nunc eleifend at elit a volutpat. Integer semper quis quam at faucibus. Donec pretium purus vitae erat pretium posuere. Sed ac aliquet nulla. Nam ut sagittis mauris. Sed quis sagittis risus, id pretium urna. Nam sagittis elementum ornare. Aenean ligula sapien, congue eget mi quis, viverra commodo nibh. Suspendisse non iaculis magna, nec volutpat neque. Donec eu dapibus eros. + +Nam sit amet iaculis massa. Nullam vel laoreet tellus, id cursus tortor. In hac habitasse platea dictumst. Curabitur in urna risus. Proin dui sem, interdum accumsan cursus ut, dignissim in purus. Nunc vitae consequat arcu. Proin volutpat elementum quam in posuere. Pellentesque luctus arcu in ornare tempor. Cras est turpis, viverra vel consectetur ac, dictum a quam. Donec sagittis tortor sed volutpat accumsan. Curabitur a tellus vitae arcu pretium viverra vel in ligula. Pellentesque turpis augue, porta vitae quam id, fermentum congue leo. Sed nec fermentum turpis. Nulla vehicula vulputate sem eu consequat. In tincidunt nisi et mi maximus, non facilisis justo porttitor. Proin eu sapien aliquam, accumsan nunc non, pulvinar leo. + +Nam vitae commodo sem. Ut laoreet in quam in suscipit. Nullam at faucibus diam. Fusce ut purus at ex lobortis elementum vitae quis sapien. Nam tempus consectetur ex, sed luctus felis. Donec porttitor mi dui, non luctus quam consectetur eu. Ut a egestas diam. Etiam sodales, nisl vitae gravida pulvinar, libero est condimentum tellus, vitae ullamcorper tortor justo a urna. Maecenas ac velit accumsan, laoreet leo eget, elementum libero. Maecenas dictum tincidunt blandit. Proin sed bibendum urna. Phasellus fermentum tincidunt tempor. Mauris iaculis, mi ac fermentum interdum, nibh odio pellentesque nunc, vel scelerisque sapien sem id purus. + +Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Maecenas imperdiet sit amet lectus at hendrerit. Vivamus luctus, elit non lacinia hendrerit, risus velit finibus nulla, quis sagittis ipsum diam ut odio. Sed mauris sapien, vehicula efficitur gravida sed, gravida in lectus. Fusce eget consectetur turpis, in fermentum neque. Nullam turpis turpis, feugiat a accumsan in, euismod a augue. Vestibulum nec neque libero. Phasellus eget efficitur turpis, fermentum molestie nunc. Sed consequat elit urna. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. + +Nunc egestas consequat blandit. Donec quis porta sapien. Sed augue nunc, efficitur vitae tellus sed, finibus bibendum sem. Fusce id sem quis quam pharetra ultrices. Phasellus non convallis velit. Quisque erat tellus, pretium eget porta in, ornare a arcu. Aenean nec lectus lorem. Suspendisse dolor lectus, ullamcorper ornare lorem a, consequat lobortis elit. Nunc dignissim est nec gravida facilisis. Proin faucibus erat vel eros volutpat, in vulputate neque sodales. + +Nunc vitae imperdiet ipsum, faucibus vehicula magna. Nulla nec nisl sapien. Curabitur et dui eget tortor efficitur accumsan. Aenean in pellentesque erat. Nam condimentum neque pulvinar, aliquam nisl eu, mollis lorem. Integer rutrum arcu eget felis semper euismod. Quisque vestibulum vel diam a tempor. Aenean mollis, tellus sit amet pretium pulvinar, nulla dolor ornare ligula, eget dignissim orci tortor ut sapien. Nam ut ipsum id lectus venenatis tempus vel non ex. Suspendisse blandit vitae nunc vitae porta. Suspendisse tincidunt est sit amet ultricies consectetur. Nulla fermentum hendrerit ex, vehicula rhoncus arcu lobortis ut. Vestibulum fermentum ornare diam at pellentesque. Praesent nunc lorem, porta et magna nec, sodales commodo justo. Duis aliquam sapien et rutrum tempus. Vestibulum malesuada felis eu ligula posuere luctus. + +Maecenas lacinia, lectus eget rhoncus aliquam, tortor est gravida sapien, vel aliquam arcu erat et magna. Praesent fringilla leo eget neque posuere imperdiet. In porttitor elit non enim gravida euismod. Aliquam tempus, orci at interdum dapibus, mauris lacus egestas sapien, ac ullamcorper ex nibh sed orci. Quisque iaculis enim et lectus egestas, malesuada posuere lectus interdum. Sed dignissim neque vel turpis dictum ornare. Vestibulum suscipit consequat maximus. + +Ut et ante sit amet leo rutrum volutpat. Sed malesuada quis sapien et ornare. Aliquam ac ex enim. Curabitur vel quam et orci posuere feugiat. Pellentesque nec metus eget sapien eleifend tincidunt non sit amet arcu. Cras posuere metus eget risus varius fermentum. Nullam orci eros, efficitur nec sapien nec, pretium laoreet erat. Nam gravida purus mauris, ac viverra orci hendrerit sed. Aenean ligula massa, posuere id faucibus vitae, malesuada quis augue. Morbi consectetur mattis mi, quis sodales diam bibendum eget. Aliquam sagittis neque at feugiat posuere. Donec gravida lectus a lectus fringilla tincidunt. Vivamus volutpat dui et turpis condimentum, ut tincidunt tortor lacinia. Ut laoreet, ante in pellentesque sodales, elit mauris scelerisque dui, quis tincidunt quam massa ac enim. Nulla porta porta sapien vel sollicitudin. + +Phasellus sed lectus posuere, mollis ante non, feugiat odio. Aenean a quam id mi ornare dapibus. Donec venenatis ipsum non velit accumsan, id elementum dolor imperdiet. Phasellus lacinia erat diam, sit amet convallis lectus ornare in. Curabitur lorem ligula, maximus eu varius quis, auctor quis odio. Etiam in orci porttitor, sodales ipsum at, rhoncus turpis. Nulla eget luctus nisi. Duis convallis est eget ligula fringilla viverra. Duis nec sapien quis dui luctus ornare sit amet quis erat. Quisque nec justo dui. + +Sed eleifend, tellus quis laoreet rhoncus, leo turpis imperdiet turpis, pretium varius odio tortor et leo. Morbi ut mi fringilla, porttitor sem ac, accumsan ante. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum cursus dolor accumsan dolor aliquet dignissim. Duis sed feugiat erat. Donec sed arcu accumsan, ornare nisl eget, lobortis nibh. Praesent pulvinar quis justo et hendrerit. + +Sed velit nibh, efficitur ut leo sed, eleifend iaculis nisl. Mauris ac diam euismod, luctus enim maximus, tincidunt sem. Ut tempus magna vitae blandit bibendum. Sed sapien tortor, ultrices et justo vel, pharetra faucibus dui. Vivamus et vestibulum arcu. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Mauris tincidunt suscipit risus, vitae varius felis commodo in. Nullam semper tortor dolor, sit amet pharetra odio interdum hendrerit. Pellentesque sed justo eros. Cras quam purus, eleifend id tellus molestie, eleifend finibus lorem. Donec a volutpat libero, at vehicula tellus. Vivamus tellus orci, pharetra in nisi vitae, tincidunt dapibus nunc. + +Suspendisse ultricies sem laoreet quam molestie ullamcorper. Sed auctor sodales metus, eget convallis eros ultrices quis. Duis rutrum mi a tortor iaculis posuere. Suspendisse potenti. Pellentesque dictum faucibus dui a vestibulum. Donec elit nisl, aliquet at dolor non, viverra dignissim felis. Donec mi elit, condimentum sit amet magna id, efficitur sodales arcu. Interdum et malesuada fames ac ante ipsum primis in faucibus. Pellentesque est elit, porttitor eget bibendum nec, auctor nec nulla. Pellentesque dignissim nisl vel orci commodo, ut ullamcorper justo viverra. + +Suspendisse pharetra gravida turpis sit amet efficitur. Nulla mattis tortor eget pharetra ultrices. Ut a turpis maximus, pharetra justo non, tempor quam. Nam et volutpat lectus. Vestibulum congue turpis augue, quis eleifend nulla euismod in. Vestibulum sed erat tempus, porttitor diam vel, elementum metus. Aenean facilisis molestie ante, sit amet ornare lacus mollis sed. Maecenas rutrum urna nec ex malesuada consequat. Nunc interdum pellentesque nisl sed viverra. Nam nec fermentum ipsum. Nulla facilisi. Maecenas congue dolor erat, a fermentum enim congue vitae. Integer metus libero, feugiat at eleifend eu, iaculis nec leo. Praesent eleifend convallis leo, eu posuere urna elementum eu. + +Aliquam dapibus dolor vel urna luctus venenatis. Suspendisse potenti. Donec vitae tellus nisi. Pellentesque vel neque dignissim, ornare tellus sed, sodales metus. Aliquam vitae maximus tortor. Nullam mattis, odio id porttitor euismod, est leo aliquet massa, bibendum pulvinar justo orci a magna. Morbi ut nisl congue, porttitor erat non, gravida turpis. In nulla risus, ullamcorper quis suscipit vel, tristique ut nulla. In malesuada odio augue, ac hendrerit nunc mattis a. Nam in quam ut elit ullamcorper mattis. Sed fringilla tempus felis, id pretium tortor varius non. Aenean quis sem quis risus mattis posuere vel quis nibh. + +Sed pulvinar commodo dui sit amet malesuada. Quisque porta tellus placerat congue efficitur. Cras id blandit enim. Praesent a urna felis. Aliquam ornare, nisl eget iaculis tempor, ligula mi vehicula dolor, ut eleifend massa massa vel est. Fusce venenatis laoreet lobortis. Proin tempus aliquet nunc ut porttitor. In est mauris, blandit eu mattis vitae, aliquam a orci. Cras vitae nulla nec ex cursus blandit. Aliquam fermentum, erat in aliquet dictum, metus urna fermentum quam, non varius magna lectus non urna. + +Donec faucibus nisl nunc. Integer rutrum dui enim, malesuada semper turpis pulvinar eget. Fusce consectetur ipsum tellus, sed maximus lorem auctor in. Morbi nec est in quam ultricies hendrerit. Sed aliquam sollicitudin elementum. Aenean aliquam ex sit amet dignissim venenatis. Duis malesuada leo nisi, id accumsan turpis pretium id. Sed ornare magna non pharetra pharetra. Ut auctor dolor neque, nec bibendum odio viverra in. Nulla convallis interdum condimentum. Proin vestibulum turpis in lorem ultrices, bibendum tristique ligula faucibus. In tincidunt auctor sem, vitae fringilla neque pulvinar eu. Etiam commodo pellentesque enim. Phasellus feugiat non sapien eget sollicitudin. Etiam consequat efficitur lacus vel maximus. Suspendisse vitae elementum diam. + +Mauris urna velit, efficitur at viverra at, interdum a velit. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Aenean bibendum sagittis massa in interdum. Mauris facilisis dui eget ipsum euismod ultrices. Donec sit amet lorem iaculis, condimentum velit quis, lacinia justo. Integer faucibus metus sed eros semper, in congue tortor venenatis. Proin tincidunt maximus enim, accumsan facilisis tellus gravida eu. Phasellus interdum aliquam ante, sit amet rhoncus nisl ornare at. Suspendisse libero eros, cursus quis fermentum nec, tempor eget lectus. Aenean ullamcorper augue lacus, non iaculis dui rutrum id. Aliquam erat volutpat. + +Morbi hendrerit fermentum sodales. Proin rutrum congue auctor. Mauris pellentesque elit non velit condimentum tincidunt a sit amet velit. Etiam accumsan ante id neque commodo vulputate. Aenean nec mattis neque. Aliquam tempor urna quis nisl convallis congue. Praesent vitae porttitor ante. Mauris mattis vestibulum ante, nec auctor augue. Praesent sem leo, accumsan eu fermentum egestas, iaculis ac nulla. + +Etiam vel nisi congue, varius neque id, volutpat quam. Fusce placerat arcu hendrerit orci condimentum vehicula. Integer sem ex, facilisis et lacinia a, condimentum at ante. Morbi condimentum diam tellus, non lobortis arcu pellentesque sit amet. Phasellus imperdiet augue eget sollicitudin mollis. Vivamus sollicitudin arcu aliquam lectus tristique, in consequat diam egestas. Suspendisse aliquet non velit id feugiat. Sed eu est fringilla, gravida nulla ac, dignissim tortor. Phasellus pellentesque nisl non venenatis sagittis. Etiam facilisis nulla quis lorem scelerisque vehicula id at ex. Duis in risus enim. In eu vulputate massa, vel dignissim odio. Praesent ut mi tellus. In hac habitasse platea dictumst. + +Nunc malesuada turpis in fermentum lobortis. Donec blandit eu orci non laoreet. Suspendisse posuere blandit tortor, in accumsan velit cursus in. Integer suscipit justo nulla, in viverra orci suscipit et. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum a pulvinar lacus. Vestibulum mattis nisi vel nunc convallis maximus. Fusce aliquam, erat in volutpat gravida, elit odio feugiat ante, nec varius enim leo eget nisl. Donec sit amet ligula lobortis, sollicitudin dolor quis, blandit augue. Duis at interdum sem. Nullam eleifend ligula urna, vitae sollicitudin purus cursus nec. + +In commodo risus eu justo hendrerit, ut posuere mi eleifend. Suspendisse sollicitudin odio sem. Vestibulum at dapibus dui, vel dictum nisi. In hac habitasse platea dictumst. Sed ac pharetra ex. Ut ultrices augue ut vulputate condimentum. Phasellus convallis arcu tortor, ac tincidunt justo cursus vitae. Mauris dignissim dapibus imperdiet. Nullam id quam eget mauris cursus molestie finibus eu enim. Fusce laoreet orci eu nunc fermentum tincidunt. Pellentesque vitae ex ac nunc porta mattis sed ut ligula. Phasellus erat dui, consequat et lacinia vel, blandit nec dui. Donec ipsum magna, rhoncus ac viverra vitae, feugiat vel ligula. + +Cras ut nisl sed elit dictum semper a at magna. Aliquam laoreet viverra velit vel lobortis. Nam tempor lorem sit amet purus tincidunt accumsan. Vestibulum et vestibulum ligula. Donec sit amet neque faucibus leo rutrum semper. Maecenas scelerisque, lacus et lobortis congue, purus quam euismod risus, et mattis orci nisl eget est. In hac habitasse platea dictumst. Pellentesque eros velit, sollicitudin quis ante vel, blandit maximus mi. Suspendisse at eros id quam vulputate ornare. Duis placerat tellus vel odio ultrices, ac feugiat enim placerat. Vestibulum ut massa mattis, commodo orci euismod, cursus lacus. Suspendisse potenti. Sed venenatis cursus neque, at lacinia erat ornare et. Sed rutrum, dui at porttitor hendrerit, lacus magna fringilla quam, id mollis elit leo ut ante. Praesent vel diam sed urna mollis laoreet eget ut risus. + +Mauris varius odio lectus, sit amet consequat nulla sollicitudin sed. Suspendisse commodo rhoncus enim vitae pellentesque. Aliquam vulputate sollicitudin aliquet. Mauris interdum interdum orci, non varius sem ornare ut. Sed vel nibh nunc. Duis tincidunt quam quis lectus egestas, eu ultricies ante posuere. Aenean in mauris eros. Suspendisse potenti. Vivamus sit amet augue at velit accumsan fermentum. Phasellus vel dui sit amet felis convallis sodales. + +Nunc mattis vitae sapien ut dignissim. Nam fermentum sit amet massa eu accumsan. In ut ipsum sit amet ante pellentesque accumsan. Nulla egestas eros eget lacus rhoncus pharetra. Nam pellentesque laoreet ex in lobortis. Vivamus congue tincidunt molestie. Integer vel turpis augue. Cras vel molestie quam. Etiam vel mauris ut tellus finibus consequat. + +Curabitur velit erat, vestibulum blandit massa a, aliquam elementum tellus. Pellentesque id nisl tempor lorem condimentum dapibus. Quisque ut lorem at orci elementum dapibus quis ut enim. Praesent venenatis congue arcu eu sagittis. Nunc nunc massa, posuere ac gravida id, scelerisque nec velit. Suspendisse non lacus a orci dapibus congue. Sed leo velit, facilisis sed egestas ut, vehicula at turpis. Praesent a finibus felis. Quisque est mi, pellentesque sit amet varius eu, gravida id est. + +Donec auctor gravida urna ac suscipit. Etiam placerat ipsum vitae ante congue, at fringilla lectus ullamcorper. Donec viverra lacus ut erat posuere, dignissim porta purus tempor. Duis eget enim quis diam vehicula pulvinar. Donec tempus eros velit, sit amet pharetra ligula placerat in. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas sit amet pretium tellus, non dictum ligula. Nullam a ex purus. Vestibulum id ex rhoncus, pretium eros vel, consequat tellus. Vivamus lacinia dolor nec ipsum aliquam, euismod fermentum sem efficitur. Nulla facilisi. Pellentesque pharetra lacinia augue, et eleifend lorem aliquet eu. Ut ut porta ante. Duis ut auctor nisi, id porttitor erat. Proin sollicitudin sem quis justo sodales aliquam. Nulla pharetra gravida arcu vel tempus. + +Maecenas nec imperdiet nulla, vitae fringilla diam. Mauris maximus aliquet tellus at congue. Aliquam in dictum elit. Sed pretium mattis lectus, non pellentesque enim dignissim vel. Sed quis elementum diam, a porttitor enim. Cras cursus eros nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus ornare sapien vel diam vestibulum, at commodo metus lobortis. Sed euismod iaculis scelerisque. Pellentesque venenatis malesuada dolor, at tempus eros venenatis sit amet. Fusce a massa non libero blandit suscipit. + +Nulla facilisi. Sed nec leo nisl. Proin condimentum nunc at risus semper, in laoreet dui congue. Integer in dolor vitae ex maximus porttitor. In efficitur vulputate metus, ac egestas diam pharetra ac. Sed tristique tempus ligula dignissim convallis. Ut tincidunt laoreet fringilla. Praesent nunc est, lacinia tristique odio in, varius rhoncus ipsum. Vivamus bibendum justo at metus ullamcorper imperdiet. Quisque elit nibh, rhoncus a placerat eget, vehicula quis ante. + +Morbi fermentum turpis enim, eu interdum dui interdum sit amet. Sed vulputate lacus nec ligula gravida euismod. Duis in nisl fringilla, sollicitudin diam ac, laoreet tortor. Sed eget porttitor augue. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Sed eu enim convallis augue vulputate convallis. Praesent laoreet, leo at ornare luctus, nisi felis semper sapien, sit amet sodales augue mauris eu lorem. Fusce ornare volutpat malesuada. Curabitur vehicula, eros id fringilla placerat, tellus elit venenatis lorem, ac imperdiet purus ex blandit felis. Nunc suscipit elementum risus ac dictum. Aliquam bibendum dignissim ipsum, sit amet posuere sem viverra ut. Vestibulum egestas in ex ac volutpat. + +Donec ut faucibus velit, nec suscipit metus. Nullam dui ligula, commodo eget est venenatis, ullamcorper porttitor lectus. Suspendisse dictum, metus vitae commodo ultricies, enim quam pretium enim, a efficitur tortor purus sed ipsum. Nam sit amet lacus vel elit consectetur ultrices. Vestibulum ac nibh in metus porttitor vestibulum. In blandit et est non efficitur. Aenean vestibulum tortor sit amet mattis semper. Praesent sit amet turpis ac neque malesuada tincidunt nec nec urna. Mauris posuere elit nisl, ac ullamcorper nisi pulvinar in. Nulla ac elit in neque ullamcorper facilisis sed et dui. + +Quisque molestie euismod dui, non scelerisque arcu dictum a. Donec eu nulla nisl. Aliquam neque orci, ultrices in nunc vitae, sollicitudin eleifend augue. Etiam at mattis velit, a efficitur dui. Nam bibendum enim non elit tristique aliquet. Vestibulum eget nibh lacus. Pellentesque id sapien dui. Nullam urna leo, faucibus et efficitur vel, ullamcorper iaculis mi. Donec sit amet bibendum justo, id dapibus sem. Pellentesque dignissim varius tellus nec egestas. Praesent eu orci vel ipsum pharetra maximus. Cras nisl ligula, sollicitudin in ligula vel, posuere sagittis eros. Phasellus porta tristique mauris vel eleifend. Mauris sit amet volutpat ipsum, sed vestibulum orci. + +Ut rutrum augue quis rhoncus tincidunt. Aenean sollicitudin lacus at erat varius ullamcorper. Integer vitae orci vel nisi vulputate cursus eget nec ex. Mauris elementum, augue ac accumsan convallis, libero leo porta ante, sit amet elementum felis ligula non enim. Ut venenatis semper posuere. Vestibulum nec nisl nisi. Ut a sapien ac orci finibus dictum sed at ex. Phasellus ac lorem nisl. Quisque vitae tempor lacus. Vestibulum in mauris diam. Proin mattis ligula vitae ipsum bibendum, at dapibus nunc placerat. Nam iaculis justo in accumsan tristique. + +Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Mauris pretium velit et tristique pellentesque. Nunc in sapien a purus congue rutrum. Nam placerat risus in ante rutrum rutrum. In in ligula magna. Duis orci ante, vehicula elementum consectetur vitae, lobortis vitae arcu. Ut feugiat tempus metus quis sollicitudin. Etiam cursus venenatis augue at fermentum. + +Vestibulum id vehicula massa. Proin ut ligula et sapien placerat tristique non nec nibh. Etiam non lacus placerat, molestie ex eu, venenatis elit. Sed posuere, ante et ullamcorper luctus, elit lectus blandit tortor, nec consequat massa elit a tortor. Fusce urna felis, porttitor at ultrices vel, eleifend porta ipsum. Pellentesque nisl nibh, molestie sit amet sem eu, dapibus pharetra nulla. Phasellus viverra augue eu augue volutpat dignissim. Integer bibendum faucibus varius. Curabitur non turpis purus. + +Sed pretium eros nisl, sed ullamcorper magna faucibus quis. Etiam ornare euismod tellus, vitae accumsan eros bibendum ut. Morbi a ex sed risus faucibus rutrum et ut urna. Nullam non tortor commodo, facilisis massa non, tristique metus. Curabitur placerat, arcu in egestas ullamcorper, nisl nisl luctus felis, ac dapibus erat velit sed erat. Proin sagittis felis vitae sem posuere semper. Vivamus fermentum eu nisi ac facilisis. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque pellentesque lacinia ex tempus lacinia. Aliquam erat volutpat. Nam aliquet mattis risus non pretium. Suspendisse potenti. + +Suspendisse et sapien ornare, elementum erat vel, ultrices nisi. Curabitur interdum dignissim tincidunt. Cras eget est a velit consectetur finibus et sed nisl. Curabitur vestibulum semper posuere. Aliquam porta diam commodo nulla tempus imperdiet. Sed id dictum neque. Ut sit amet risus aliquet, dignissim velit sed, tristique ipsum. Nam a sapien id magna commodo tincidunt quis ac tellus. Nunc nec pellentesque orci. In justo purus, vulputate quis urna a, fringilla blandit sem. Cras porttitor quam vitae metus vehicula faucibus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Ut eleifend orci lectus, vitae semper eros hendrerit id. Fusce nec tellus condimentum, vehicula elit quis, gravida erat. Maecenas volutpat interdum velit id vestibulum. + +Pellentesque id metus metus. Nullam ac metus non nisi facilisis aliquet quis a sem. Morbi cursus consectetur aliquam. Morbi id sapien nibh. Etiam tempus tempor tempor. Nam scelerisque condimentum purus. Proin nec cursus eros, in condimentum dolor. Nam in sagittis urna. Ut eget ornare erat. Vivamus nec elit ut sapien tristique aliquet a nec urna. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Maecenas varius pellentesque diam. + +Ut eget libero iaculis urna porta iaculis vel ac eros. Sed sed mi et libero porta consequat a in libero. Sed in imperdiet ipsum, sit amet ultricies dui. Curabitur sit amet consectetur eros. Praesent nunc tellus, feugiat ac laoreet consectetur, accumsan nec magna. Praesent at elementum orci. Donec dapibus venenatis libero, id tincidunt libero euismod ac. + +Aenean sit amet ex placerat, molestie sapien a, volutpat turpis. Vivamus elementum lacinia nisi a convallis. Donec a sagittis turpis. Curabitur pulvinar laoreet dolor id consequat. Aliquam aliquam feugiat magna, vitae sagittis lorem mattis ut. Donec sed auctor nulla. Ut convallis, neque vitae faucibus efficitur, nisi justo pharetra odio, vitae tristique purus lorem et nisi. Quisque eleifend aliquam arcu nec maximus. Nunc mauris elit, finibus nec gravida non, maximus nec nibh. Sed eu ipsum in arcu gravida maximus. Maecenas massa dolor, ullamcorper eget odio eu, congue ornare leo. Suspendisse venenatis ultricies imperdiet. Nam finibus orci vitae sagittis finibus. Pellentesque sit amet dapibus diam. Nam eu nunc elit. + +Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Etiam euismod turpis nec urna lobortis, ut malesuada ex suscipit. Fusce sed viverra risus. Pellentesque tristique nulla pulvinar tincidunt accumsan. Nullam vitae nibh imperdiet erat rutrum pellentesque et sit amet quam. Aenean laoreet ultricies hendrerit. Duis elementum tellus a feugiat vulputate. Aliquam aliquam enim placerat dolor viverra, non suscipit lorem ultrices. + +Proin interdum vitae lorem quis viverra. Praesent nec tempor dolor, vitae sagittis turpis. Etiam sit amet imperdiet sapien, ac laoreet arcu. Proin aliquam, risus nec maximus dapibus, dui diam elementum dolor, vitae tempor augue lorem vel justo. Aenean ac egestas dolor. Ut aliquet, felis at fringilla venenatis, leo nisl ullamcorper ex, vitae pellentesque turpis orci quis lectus. Nullam vitae suscipit metus. Integer congue, ante in lacinia rhoncus, erat lorem interdum elit, egestas suscipit lectus nunc non orci. Nunc vel viverra quam. Mauris sit amet sodales tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam quis sollicitudin libero. In nec venenatis orci. + +Integer non quam fringilla, tristique nulla id, gravida arcu. Aenean scelerisque lacinia magna. Praesent nunc sem, lobortis non convallis rhoncus, rutrum vitae ante. Sed et orci ut erat viverra bibendum a et lectus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Fusce sit amet eros eget dolor pharetra vestibulum. Nullam vestibulum, massa dictum finibus egestas, orci nulla pulvinar sem, nec tempor libero lacus eu ante. Nam lacus erat, gravida eu placerat sit amet, facilisis sed urna. Suspendisse efficitur felis non ornare dictum. Nam sit amet nulla enim. Nulla eget scelerisque est. Suspendisse lacinia velit arcu, id lobortis felis facilisis in. + +Suspendisse potenti. Nulla porttitor metus ut nunc dictum tristique. Cras sit amet tortor eget ligula tristique efficitur. Ut at nisi id purus imperdiet laoreet. Sed sit amet malesuada urna. In nunc dolor, luctus ac condimentum ut, dapibus vel metus. Suspendisse pretium urna eget libero convallis vestibulum. Integer ut mauris hendrerit ex posuere euismod sed sed odio. Nulla egestas libero sit amet magna venenatis faucibus. Pellentesque semper vestibulum elit, et pretium felis scelerisque non. Suspendisse aliquet leo arcu, sed dapibus ex semper non. Donec lacinia dictum dignissim. Maecenas ipsum nunc, aliquet eget consectetur sit amet, aliquet vitae odio. + +Proin consectetur blandit feugiat. Nulla ac dictum quam. Vivamus suscipit scelerisque ipsum, vitae consequat neque sagittis id. Donec eu augue hendrerit, varius ipsum at, cursus massa. Morbi id augue id ipsum porttitor mollis. Phasellus nec libero eu arcu finibus dapibus. Nullam nec pharetra ante. Aenean sit amet urna eget justo tempus pharetra ut nec mi. Pellentesque viverra, ligula nec elementum ornare, ante elit eleifend enim, nec dignissim ligula elit in nibh. Vestibulum facilisis, felis eu condimentum dapibus, ante risus tincidunt urna, ut posuere dui augue ut ante. + +Nam arcu lacus, accumsan ac dolor in, egestas euismod est. Sed consectetur mauris et enim tincidunt semper. Donec sit amet pellentesque diam. Fusce viverra arcu a placerat fermentum. Nullam euismod dui eget egestas ultrices. Duis euismod viverra tortor eget eleifend. Pellentesque vitae neque dapibus, bibendum mi eu, auctor velit. + +Pellentesque congue consectetur turpis, eget auctor dui suscipit vel. Aliquam a sollicitudin turpis. Praesent nec blandit tortor. Suspendisse non nunc at urna tincidunt elementum. Integer eu elit nec urna maximus blandit quis at tortor. Nulla laoreet elit a purus tempus, et tincidunt lorem sagittis. Aenean semper erat eu neque hendrerit ornare. Cras posuere lorem nec orci vulputate finibus. Fusce tempor ex ac lacus gravida venenatis. + +Phasellus laoreet, libero vel fermentum euismod, sem diam accumsan quam, vitae gravida arcu est at sem. In bibendum, felis at viverra congue, felis nunc fringilla libero, ut scelerisque erat massa ut neque. Suspendisse potenti. Cras faucibus dolor vel tortor blandit, quis imperdiet magna semper. Nullam ut urna dapibus, vulputate est a, hendrerit purus. Vivamus vestibulum nisi a tempus placerat. Morbi vitae ultricies lacus. + +Nunc vel efficitur urna. Fusce bibendum suscipit mauris, quis imperdiet nisl bibendum non. Nam sed nisi imperdiet, pellentesque sem sed, pellentesque diam. Praesent luctus feugiat odio, eget fermentum lectus ullamcorper non. Phasellus pulvinar lectus sed ligula semper lobortis. Pellentesque fermentum ultricies fermentum. Quisque id turpis vel orci rutrum rhoncus id vel metus. Vivamus ex leo, accumsan eu luctus sit amet, tincidunt vitae erat. Sed eu mollis odio, ut ultricies lacus. Duis enim odio, pellentesque non velit vel, aliquam blandit erat. Mauris feugiat felis fermentum purus blandit, id rhoncus lorem tempus. Integer cursus, nunc vitae laoreet commodo, nunc lacus venenatis orci, quis eleifend risus est nec quam. Nulla tincidunt nunc ac purus tincidunt, eu molestie ipsum sollicitudin. + +Vestibulum ac varius velit, non suscipit nulla. Vestibulum a sagittis tortor. Suspendisse vestibulum felis quam, eget blandit nulla dictum vel. Praesent eu tellus ut lacus facilisis ultrices. Etiam fringilla nulla nec leo viverra faucibus. Sed nec sollicitudin nisl. Aenean libero massa, ornare sed elit ut, interdum fermentum arcu. + +Aliquam maximus diam et elit dapibus, eget condimentum sapien finibus. Etiam gravida nunc dapibus facilisis feugiat. Sed quis massa ligula. Nunc eleifend dolor lacus, at dapibus nisi vulputate eget. Etiam vel euismod urna, quis molestie nibh. Vestibulum bibendum erat non dictum sollicitudin. Suspendisse sed placerat lacus. Cras sollicitudin quam justo, a condimentum urna feugiat a. Quisque mauris libero, ultricies at ligula a, facilisis euismod ante. + +Morbi hendrerit maximus sapien ut auctor. Nullam nisi erat, aliquam ac metus eu, fermentum laoreet augue. Nam ultricies mi at vulputate laoreet. In ipsum est, blandit sit amet lorem id, egestas aliquam odio. Praesent venenatis lobortis mi. Aenean eu lacus consequat, mattis enim et, pellentesque leo. Duis convallis facilisis placerat. Donec porta, enim eget placerat congue, nisi augue faucibus odio, et fringilla purus elit vitae felis. + +Nulla et tellus a libero pellentesque eleifend vitae mattis nisi. Nunc placerat neque et sapien lobortis, aliquet pharetra nunc vulputate. Suspendisse potenti. Etiam ullamcorper lacinia varius. Suspendisse sit amet malesuada magna. Nam molestie mi nec diam tempus laoreet. Suspendisse urna tellus, scelerisque vitae purus et, dapibus fermentum felis. + +Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec scelerisque eget neque sed hendrerit. Donec at ligula augue. Donec vulputate massa nunc, a feugiat nunc elementum ut. Donec pulvinar libero sit amet ante faucibus sagittis. Mauris quis diam ultrices, tristique sapien nec, tempus urna. Morbi hendrerit odio sit amet leo lacinia, non egestas diam condimentum. Suspendisse efficitur sapien eget elit euismod tristique. Duis posuere vestibulum risus id sodales. Ut eget mi in urna rutrum molestie non nec dolor. Curabitur sollicitudin pharetra lacus, in sodales tortor tempor vitae. Nullam rhoncus arcu vel euismod varius. + +Aenean malesuada, purus quis volutpat sollicitudin, lectus risus lacinia mi, a facilisis ante lacus a dolor. Aenean vel aliquam tortor. Vivamus ornare, tellus at malesuada suscipit, quam dolor egestas erat, id convallis enim augue a enim. Aenean ultricies sodales placerat. Vivamus vel erat ac mi vulputate commodo nec eget urna. Suspendisse potenti. Mauris quis dapibus est, id tristique libero. + +Suspendisse ex diam, imperdiet quis dui id, interdum sodales elit. Maecenas at nunc ligula. Etiam imperdiet convallis magna sit amet tristique. Proin hendrerit laoreet magna, eget malesuada elit rhoncus et. Curabitur eget odio imperdiet, molestie sapien pretium, efficitur turpis. Aliquam sed congue arcu. Pellentesque vitae mauris id neque pulvinar tempor. Donec lobortis tellus id gravida dictum. Nulla et varius ex. Vestibulum pharetra eu quam dapibus finibus. Nullam accumsan sagittis turpis. Mauris fermentum orci et tortor tempus, ac tristique odio sollicitudin. Duis nec elit et magna imperdiet molestie eget vel ante. + +Suspendisse tempus augue nec massa molestie aliquam. Pellentesque vestibulum, erat sit amet fringilla fermentum, sapien lorem tempor felis, at efficitur augue erat quis nisi. Proin nec felis sagittis, sollicitudin turpis eu, fringilla leo. Vestibulum at augue nec dui vestibulum aliquam a at metus. Duis ullamcorper eleifend bibendum. Maecenas viverra mauris lacus, et consequat nisl pharetra eu. Praesent rutrum diam eu mi aliquet, non pellentesque est suscipit. Vestibulum massa leo, blandit eget mi at, cursus faucibus nunc. Praesent nec bibendum libero, vitae cursus libero. Sed consequat vitae eros nec maximus. + +Pellentesque et tortor eget lectus feugiat venenatis. Integer ornare nisl quam, nec imperdiet justo finibus at. Morbi malesuada, ante sit amet rutrum tempor, risus lorem tristique urna, egestas varius purus ex ut sem. Etiam sit amet feugiat sapien. Etiam quis risus commodo, eleifend velit quis, tincidunt enim. Mauris fermentum lacinia velit quis efficitur. Nunc sit amet lacus sit amet urna viverra feugiat. Nullam sagittis rhoncus suscipit. Aliquam eu sem ligula. Sed sodales risus et tortor lacinia tristique. Nulla massa augue, malesuada sit amet euismod ac, euismod placerat nulla. Sed lobortis ex nec lacus facilisis, nec semper mauris ultrices. Quisque ornare non felis vitae pellentesque. Donec mattis ut magna sed imperdiet. Integer viverra tempus feugiat. + +Donec laoreet aliquam imperdiet. Fusce justo neque, dictum vitae fringilla vitae, euismod sed augue. Fusce sodales, lectus a congue bibendum, ante eros pharetra tortor, eu sollicitudin libero ipsum sit amet felis. Curabitur sed condimentum quam, in iaculis ante. Ut feugiat dictum odio, vitae hendrerit lacus volutpat sed. Sed scelerisque et metus nec gravida. Mauris mattis porttitor magna, ut maximus justo sagittis vitae. Donec at metus ut leo gravida vehicula in sed diam. Vestibulum dignissim suscipit sagittis. Nam varius et arcu sit amet lacinia. Sed eget elit rhoncus, elementum dolor a, vehicula orci. Nam vitae tellus sapien. Phasellus massa lorem, commodo quis placerat in, semper faucibus tortor. + +Vivamus id dolor mi. Curabitur sagittis posuere quam in mollis. Phasellus finibus ipsum vel elit tincidunt, a bibendum ligula vulputate. Suspendisse quis purus nisl. Integer mi sem, posuere quis nisi a, consequat consequat magna. Vestibulum bibendum mauris in risus auctor fringilla vel at lacus. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Vivamus venenatis mauris at mattis eleifend. Donec tempus ipsum ac nisl convallis ultrices. + +Nullam eu nibh ut erat vehicula accumsan. Vivamus vitae faucibus libero. In in luctus odio. Nulla nec enim pharetra, fermentum erat in, tempor turpis. Sed ac magna suscipit, dignissim elit id, faucibus libero. Maecenas placerat in dolor et faucibus. Sed maximus purus dolor, non egestas massa rutrum non. Nunc ut rhoncus lacus. Sed sit amet dolor eu sapien sagittis molestie. Aenean ipsum erat, rutrum a diam et, varius porttitor ligula. Phasellus fermentum fermentum felis. + +Phasellus odio massa, lacinia ac hendrerit vestibulum, placerat sit amet erat. Praesent eget justo scelerisque, congue est in, pharetra mi. Etiam blandit turpis dui, a faucibus ipsum viverra vitae. Praesent sed scelerisque arcu. Cras nec venenatis ipsum. In et tempus metus. Pellentesque cursus quis nibh quis porttitor. Duis leo lacus, pretium eget rutrum eu, tincidunt lobortis tellus. Cras in erat tristique arcu faucibus luctus sit amet tempus erat. Nullam placerat, est a interdum iaculis, purus justo convallis arcu, at mattis erat sem tempor velit. Curabitur sapien sapien, tempor eget sodales vitae, blandit ac libero. Proin eget sem et arcu malesuada convallis non a est. + +Ut id sagittis urna. Morbi est mauris, molestie quis magna ut, convallis porta justo. Etiam in vulputate sem. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nullam consectetur enim nisl, sit amet pulvinar turpis elementum at. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum convallis cursus libero vel feugiat. + +Curabitur vel scelerisque metus. Etiam aliquet faucibus quam, vel aliquet est mattis quis. Pellentesque pulvinar sodales augue, a dignissim sem tristique vitae. Pellentesque dolor quam, pharetra vitae rhoncus ut, blandit at elit. Sed dignissim diam turpis, ac condimentum justo iaculis vel. Donec facilisis eros sit amet est dapibus, vel pellentesque eros consequat. Integer euismod mollis metus, vel rutrum risus imperdiet ac. Fusce semper dolor sit amet mi congue accumsan. Maecenas sagittis magna justo, vel varius ante scelerisque id. Fusce at urna sapien. Aliquam dui sapien, facilisis eu magna laoreet, feugiat tincidunt elit. Fusce iaculis sit amet erat id dignissim. Suspendisse sollicitudin laoreet consequat. Pellentesque eu mollis quam, vel dapibus libero. Duis mattis tortor vitae orci vehicula, et viverra ipsum lobortis. Quisque iaculis sapien est, id sagittis tortor rutrum at. + +Duis ut condimentum risus, et venenatis nulla. Praesent urna ex, faucibus eu mollis nec, lobortis vitae neque. Vivamus a malesuada tellus. Quisque bibendum dolor nec massa porta, nec malesuada magna auctor. Phasellus non auctor turpis, et bibendum risus. Ut pellentesque justo non neque convallis, ut imperdiet diam sagittis. Nunc arcu nisi, rutrum sagittis neque elementum, finibus consequat felis. Proin euismod elit eu urna tristique ultrices. + +Curabitur id hendrerit ipsum. Aliquam tortor nisi, dignissim vel sodales a, ultricies a ipsum. Etiam commodo arcu sed volutpat varius. Proin vehicula lacinia tempor. Vestibulum feugiat nibh eu ante tempor efficitur. Etiam eleifend a orci eu euismod. Cras a tortor risus. Cras nec urna non urna malesuada maximus vel et ipsum. In ullamcorper porttitor dolor, at fringilla tortor tristique ac. Nulla gravida tortor nec dolor convallis, accumsan sollicitudin dui luctus. Vestibulum euismod vestibulum semper. Nam semper lacus dolor, vitae rhoncus nisi blandit nec. Phasellus turpis dolor, posuere sed sodales sit amet, interdum ut arcu. + +Sed blandit nulla et sem vulputate, et semper lacus posuere. Proin velit lorem, sagittis tincidunt aliquet vitae, fermentum sed orci. Ut interdum ultrices dolor eu tempor. Quisque pretium vulputate dui at blandit. Aenean pretium urna sed purus dapibus aliquam. Mauris tristique augue pretium magna scelerisque, vel cursus sapien porttitor. Nullam neque justo, aliquam non neque id, lobortis facilisis nibh. Duis molestie dui eu augue tristique porttitor. Sed posuere justo massa, efficitur vulputate erat posuere non. Cras quis condimentum tellus. + +Etiam eleifend ipsum ut justo viverra porttitor. Nam bibendum enim nec ligula vestibulum, vel fringilla velit posuere. Praesent leo enim, varius sit amet nulla ut, sodales sollicitudin nunc. Phasellus hendrerit varius ex quis tempus. Suspendisse maximus laoreet enim, ut bibendum tortor. Ut non magna vehicula augue condimentum scelerisque. Aenean efficitur elit vel rhoncus euismod. Vestibulum sit amet sollicitudin dui. Quisque ac diam nibh. Nullam quam enim, volutpat eu turpis et, posuere consectetur neque. + +Ut tincidunt semper dictum. Etiam varius enim sit amet metus consequat malesuada in at ligula. Cras nisl dui, tincidunt a urna et, porttitor rhoncus velit. Mauris interdum imperdiet lectus ac mollis. Aliquam sollicitudin ornare mi, a varius nulla faucibus aliquet. Nunc fermentum tempor ullamcorper. Pellentesque laoreet libero condimentum pellentesque pharetra. Nullam sed eros vel erat vestibulum dictum. Nulla nec interdum dui, quis finibus nunc. + +Sed ac turpis in mi ultricies finibus sit amet et diam. Pellentesque sagittis non ligula et convallis. Suspendisse interdum est aliquet erat rhoncus semper. Donec pretium turpis ante, vel posuere mauris facilisis vel. Vivamus nibh ligula, pharetra sit amet hendrerit nec, vulputate ut sapien. Nulla ullamcorper condimentum metus vitae imperdiet. Ut eget ex purus. Nulla dapibus malesuada pharetra. Praesent sit amet ornare turpis. Nulla pulvinar felis eget arcu mollis vulputate. Vestibulum eget semper felis. Donec viverra justo id mi tempor, a mollis ipsum porttitor. Maecenas aliquet volutpat ante eget tempor. Duis ipsum nisi, scelerisque quis metus vitae, blandit faucibus nisl. Mauris rutrum nisi sed lorem dictum ultricies. + +Nulla dictum arcu augue, aliquet ornare ligula suscipit ut. Integer libero erat, bibendum quis arcu at, consequat luctus sem. Ut ut erat tincidunt, porta erat efficitur, bibendum neque. Maecenas eget convallis sapien. Nullam facilisis ex et lorem fringilla, ut convallis leo dictum. Duis porttitor, ex eu venenatis faucibus, erat augue dapibus leo, sit amet scelerisque neque dui sed arcu. Praesent at diam nec sem sodales condimentum. Vivamus vehicula urna tellus, a semper ipsum cursus id. Duis auctor enim erat, laoreet volutpat dui rhoncus maximus. Suspendisse pellentesque euismod lacus, at auctor purus. Suspendisse volutpat imperdiet diam, id laoreet est egestas at. + +Fusce lobortis ex vel condimentum convallis. Vivamus fermentum convallis dolor quis rutrum. Donec tempus ornare maximus. Mauris quam neque, dignissim rutrum maximus sed, volutpat sed lectus. Donec id augue gravida, pretium purus eu, sagittis tellus. Maecenas tincidunt gravida felis nec pulvinar. Fusce turpis urna, sodales non rhoncus nec, sodales ac ipsum. Suspendisse risus felis, imperdiet id malesuada fermentum, ultricies et erat. Suspendisse potenti. Pellentesque tellus ipsum, dapibus eget pellentesque eleifend, venenatis sed risus. Ut sed mollis nisl. Aliquam lobortis aliquam ipsum, et ornare magna bibendum ut. Proin quis justo vitae neque tincidunt maximus ultricies et purus. Nullam non semper turpis. Quisque non mauris odio. + +Donec commodo tempus egestas. Vestibulum at diam vel mi tincidunt finibus. Donec aliquam magna id eros tristique finibus. Cras ut enim sapien. Proin gravida risus a nisi dapibus, ac vulputate nunc laoreet. Donec at leo vel nulla iaculis feugiat sed et ante. Nulla a arcu at ligula sollicitudin semper sed eget est. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Ut dolor magna, luctus id elit ut, interdum viverra lacus. Aenean sed tempor metus. Aenean arcu dui, eleifend quis est sed, luctus lacinia nulla. Morbi id luctus libero. Maecenas sodales leo eu sapien maximus porta. Praesent gravida augue eu ligula pretium cursus. + +Duis aliquam luctus tortor, eu facilisis quam sodales sed. Phasellus feugiat venenatis lorem, in scelerisque est efficitur quis. Cras quis nisl nisl. Quisque magna felis, tempus nec pharetra et, tempor id ligula. Pellentesque sed dignissim velit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris convallis iaculis posuere. Nullam orci velit, venenatis eget enim quis, molestie consequat dolor. Nulla egestas risus vestibulum sagittis blandit. Nullam dui lacus, tempus euismod sapien a, eleifend sodales justo. In id tortor neque. Vivamus faucibus ante ac odio vestibulum, vitae condimentum nibh consectetur. In rutrum hendrerit est, sit amet mollis ex aliquet tempor. + +Donec non tincidunt sem. Nullam dignissim felis nibh, et accumsan felis tristique sed. Maecenas id massa erat. Ut justo ligula, aliquet eu condimentum a, aliquet eget ex. Phasellus et neque eu nisl consectetur ullamcorper. Sed gravida efficitur nisi, vitae posuere nisi tincidunt sed. Duis vitae molestie metus, vitae finibus urna. Integer at lacus vitae turpis convallis feugiat a dignissim libero. In dolor nibh, aliquam eu malesuada in, tincidunt vitae nisi. Nullam at lectus risus. Integer vitae ligula interdum, congue nibh id, tincidunt dolor. + +Mauris risus quam, mollis eu consequat vitae, molestie sit amet risus. Vestibulum congue vulputate nibh sit amet ullamcorper. Nullam elit justo, hendrerit euismod elit nec, gravida tincidunt ipsum. Aenean tellus tortor, commodo vitae cursus vitae, finibus quis mi. Nam in tellus scelerisque, cursus magna a, elementum tellus. Praesent ut lacinia justo. Donec consectetur, mi nec faucibus viverra, nisl justo suscipit est, in consequat ex urna vitae orci. Fusce molestie feugiat ex sed dictum. Phasellus interdum eros dapibus sodales volutpat. Morbi elementum sapien ante, quis sagittis justo fermentum at. Maecenas vestibulum massa quis eleifend rhoncus. Praesent auctor ex at urna pellentesque facilisis. + +Duis tincidunt porta quam, in commodo orci dapibus interdum. Donec mattis erat ante, nec faucibus magna pharetra id. Aliquam dignissim ultricies auctor. Duis quis ex mi. Phasellus ipsum mi, volutpat quis augue nec, dapibus aliquet lectus. Aenean id eros mi. Fusce rhoncus iaculis magna, commodo commodo nibh pellentesque ut. Donec consectetur lacinia erat ut luctus. Mauris efficitur erat vitae sapien fringilla sollicitudin. Maecenas a egestas ipsum. Aenean placerat congue augue, ut condimentum lorem accumsan in. Morbi ac quam nunc. + +Nunc posuere faucibus semper. Integer at convallis ex. Duis a purus molestie, dignissim mi quis, consequat nulla. Proin tempus congue ligula, sit amet ultricies enim consequat ut. Nunc ac est at nisl euismod cursus. Pellentesque vulputate molestie ipsum. Praesent varius, lacus non lobortis blandit, nibh lacus scelerisque nisi, sit amet interdum ligula tellus ac purus. Vestibulum sed quam tempor, pharetra tellus ullamcorper, tincidunt est. Nulla tempus tortor nec erat tempus eleifend pellentesque eget purus. Duis gravida dui justo, ac commodo ipsum mollis et. Nullam vitae nibh enim. Ut sodales mi eu diam sagittis, quis luctus tortor euismod. + +Maecenas a augue ac augue varius rhoncus. Fusce nunc turpis, mattis eu iaculis a, dapibus nec purus. Pellentesque imperdiet sit amet purus a blandit. Morbi augue tellus, venenatis nec tortor in, consectetur tincidunt nulla. Aenean purus libero, volutpat quis neque vitae, mattis efficitur eros. Donec sodales venenatis augue, sed faucibus magna accumsan convallis. Pellentesque efficitur elementum imperdiet. Cras at condimentum leo. Curabitur feugiat ut nisi facilisis commodo. + +Sed commodo sapien quam, quis vulputate orci lobortis nec. Aenean luctus dictum ipsum, non consequat sapien molestie vel. Proin blandit libero tortor, vitae efficitur tortor hendrerit at. Sed eleifend eu velit eu tempor. Nunc auctor tincidunt mollis. Ut molestie, erat ut lobortis convallis, odio felis sollicitudin elit, vitae ultricies lorem neque et dui. Sed venenatis tempus ullamcorper. Integer eget elementum nulla. Maecenas a placerat ipsum. Etiam sagittis sagittis rhoncus. Aliquam faucibus magna id lacus pharetra, nec interdum lacus sodales. + +Mauris ipsum sem, venenatis non elit in, feugiat pretium lacus. Fusce eget tincidunt dolor. Nam pharetra lacus vitae diam bibendum, ac placerat tortor aliquet. Sed eget gravida orci. Ut in orci lorem. Morbi condimentum ligula dolor, in dictum mi vestibulum sed. Suspendisse eu velit finibus, tincidunt dolor malesuada, mattis est. Morbi iaculis sapien vitae metus facilisis fringilla. Donec sed volutpat neque. Mauris ipsum metus, venenatis cursus mattis quis, convallis ultricies purus. Aliquam quam magna, sagittis at mi quis, lacinia iaculis turpis. Praesent viverra, ex nec euismod lacinia, urna risus pretium odio, sit amet mattis metus eros non lectus. + +Nam sed vehicula est, ac aliquet mi. Aenean iaculis placerat orci, ut lobortis dui vestibulum convallis. Vestibulum eleifend ante sed lorem interdum lobortis ut vel tortor. Sed auctor id nisl sed bibendum. Fusce maximus vulputate mauris, tempus sollicitudin nunc efficitur vitae. Nulla pellentesque molestie leo, quis hendrerit enim. In vel dapibus nisi. Nam at dui quis eros porttitor volutpat in id magna. Maecenas quis quam a justo cursus aliquet viverra sit amet mauris. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Ut hendrerit est at augue pretium condimentum at ut velit. Suspendisse non gravida metus. + +Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Praesent lacinia commodo elit, sed fringilla ipsum imperdiet ut. Proin a dictum ante. Suspendisse potenti. Praesent ac nulla volutpat, ultricies diam vel, auctor risus. Nullam aliquam elit vel quam suscipit molestie vel ac dolor. Ut pharetra finibus elit, id vulputate ex dignissim in. Ut ac finibus urna, a luctus magna. + +Fusce suscipit convallis eros nec blandit. Cras quis lectus nibh. Donec pulvinar rhoncus pulvinar. Vivamus nulla nisi, vestibulum vel neque et, dictum gravida ex. Vestibulum in arcu vitae nibh auctor rhoncus ac in massa. Sed semper enim ac dui sollicitudin porttitor. Etiam ante erat, laoreet sed dolor eget, cursus commodo lorem. Curabitur maximus tincidunt nulla at molestie. Phasellus ultrices felis a cursus facilisis. Nullam a semper tortor. Nulla ac vulputate justo. Mauris maximus nisi quam, elementum eleifend nulla condimentum nec. Aenean congue tincidunt mi, id mollis orci blandit vitae. Integer placerat orci at nisl vestibulum lacinia. + +Sed egestas posuere egestas. Quisque pulvinar velit commodo felis accumsan, a consequat purus laoreet. Ut at arcu at augue sodales rhoncus. Ut non nisl dui. Sed sed nulla quis orci eleifend auctor ut et sem. Aliquam erat volutpat. Donec egestas tincidunt leo in interdum. Donec varius odio nulla, id porttitor erat venenatis ut. Nulla ac nisl ut enim placerat congue. Fusce consequat purus eget risus luctus finibus. Donec in blandit odio. Sed vestibulum nisl ut diam vestibulum volutpat. Donec magna quam, tempor eget velit quis, blandit tristique lacus. Pellentesque et orci dui. + +Integer tempor mollis purus ut volutpat. Pellentesque efficitur cursus neque in ullamcorper. Integer ac tincidunt lacus, at venenatis tellus. Curabitur convallis commodo enim a convallis. Aenean sagittis sodales nibh, sed eleifend diam ultricies at. Vestibulum erat mauris, lobortis ac tempor a, viverra non sem. Nulla non ipsum mollis, dignissim elit a, laoreet enim. + +In laoreet purus eget orci rhoncus viverra. Quisque vel metus quis nulla hendrerit viverra. In consectetur velit vitae purus vestibulum, in hendrerit odio sollicitudin. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla ut est sed ligula tincidunt fermentum nec ac nulla. Nam in sagittis velit. Vivamus vel dapibus erat, ullamcorper sollicitudin metus. + +Quisque pulvinar nulla eget mi mattis, ut convallis nulla ullamcorper. Vivamus placerat mauris vel elementum aliquet. Sed ac accumsan eros. Vivamus vitae rhoncus urna. Fusce gravida cursus varius. In posuere finibus leo cursus molestie. Interdum et malesuada fames ac ante ipsum primis in faucibus. Mauris vulputate mi ut nunc finibus convallis. Praesent luctus erat eu urna facilisis convallis. + +Nam efficitur tempus augue varius porttitor. Pellentesque scelerisque dolor scelerisque, dignissim massa eget, iaculis nibh. Praesent viverra molestie dui a pulvinar. Mauris malesuada mattis felis, vitae sagittis metus pellentesque viverra. Phasellus faucibus congue blandit. Pellentesque mattis, justo sed imperdiet rutrum, metus metus porta nisi, vel malesuada lorem massa at dolor. Nullam nisi eros, tristique quis mollis ac, hendrerit nec leo. Praesent viverra molestie vestibulum. Nullam eu aliquam risus, at dignissim diam. Quisque blandit fermentum erat, sed ullamcorper augue pellentesque in. Integer eleifend libero non lacus sagittis auctor. Aliquam volutpat interdum accumsan. + +Etiam in eros porta, bibendum lacus eget, feugiat orci. Integer massa dui, commodo ac luctus nec, ornare id velit. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Duis dignissim scelerisque ex. Aliquam tempus cursus nibh, sed scelerisque libero sagittis ut. Morbi finibus vestibulum nulla, nec aliquam urna venenatis vulputate. Pellentesque ultricies, lorem a dignissim bibendum, augue nisi eleifend libero, cursus ultrices nulla purus a nunc. Quisque dictum pulvinar turpis vitae finibus. Etiam eget ultrices diam. Sed commodo enim ante, fermentum rhoncus tortor tincidunt ac. Suspendisse fermentum aliquet erat non posuere. Aenean vel dapibus lorem. + +Aenean lorem libero, rutrum vel egestas vel, pellentesque ut ante. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum felis diam, pellentesque sit amet ultricies sed, vestibulum in leo. Morbi aliquet, quam ut fringilla sollicitudin, justo risus suscipit mi, in vulputate neque erat ut turpis. Duis auctor dui non urna sagittis dictum. Vestibulum nec felis vel sapien congue volutpat a a orci. Phasellus tincidunt libero ac lorem feugiat, ac elementum lectus eleifend. Praesent sit amet est id massa convallis congue. Integer ac nunc eget orci pharetra vehicula. Mauris et ultricies diam. Nunc et pellentesque lacus, eget bibendum sapien. Morbi condimentum mi ac metus euismod convallis. Proin consequat rhoncus varius. Maecenas feugiat elementum vulputate. + +Vestibulum tempor feugiat dapibus. Ut ornare in augue non euismod. Nulla faucibus gravida condimentum. Curabitur pharetra dui non lorem sagittis iaculis. Quisque vitae nibh magna. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Fusce quis pharetra sem, sed aliquet lectus. + +Fusce volutpat tempor tincidunt. Pellentesque ultricies imperdiet tincidunt. Integer porta dictum lorem, non luctus tellus bibendum sit amet. Quisque posuere felis et est dapibus, commodo rutrum leo molestie. Fusce sodales felis sed sem pharetra pretium. Praesent nec sollicitudin nisl. Donec volutpat convallis elementum. Duis id libero augue. Nulla facilisi. + +Nulla viverra, urna nec efficitur vulputate, metus odio lacinia purus, in sagittis elit erat vel massa. Fusce ligula leo, finibus non felis dignissim, dictum ullamcorper odio. Phasellus vel eros eu sem venenatis sagittis a nec nulla. Pellentesque sapien elit, lobortis quis dictum et, maximus vitae metus. Curabitur quis aliquam eros. Quisque cursus condimentum urna vel efficitur. Etiam aliquet lorem ut varius suscipit. In viverra molestie felis vitae vehicula. Curabitur cursus, nunc sodales faucibus ullamcorper, urna magna dapibus velit, accumsan blandit mi quam nec justo. Donec ac dui lorem. Sed eu sagittis nulla. Suspendisse ut ante lacus. Fusce non tristique felis. Nulla convallis consectetur arcu, semper egestas urna efficitur quis. Nulla ac turpis at leo pretium maximus. Morbi cursus diam ac purus imperdiet, non commodo tellus cursus. + +Mauris ut eros suscipit, suscipit nisi vel, porttitor sapien. Morbi in nulla sapien. Cras maximus pretium justo, vel eleifend urna vulputate sed. Aenean egestas nisl ex. Curabitur sed pharetra ligula. Nulla blandit lorem id mauris tincidunt, at elementum risus aliquet. Quisque id interdum dui. + +Aenean nec elit condimentum ex viverra hendrerit. Pellentesque blandit nibh elit, a ultrices orci vestibulum vitae. Donec ultricies malesuada justo ac luctus. Pellentesque laoreet nunc a sem varius, ac dapibus ligula volutpat. Aenean sem felis, aliquam nec ullamcorper quis, ullamcorper non ante. Pellentesque libero leo, sagittis nec tempus a, tincidunt eget risus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec elementum massa vel diam suscipit suscipit. Suspendisse eget neque porta, cursus orci eget, imperdiet libero. + +Vivamus faucibus vulputate sapien, non pulvinar ex sodales vitae. Maecenas consequat est urna, id faucibus eros dictum at. Vestibulum tortor mi, porta vitae sagittis sed, convallis quis enim. Vestibulum gravida justo pulvinar sem ornare, efficitur dictum neque varius. Nullam egestas congue tellus vitae interdum. Nam lectus erat, porta vitae lorem non, mollis semper velit. Nulla vestibulum tincidunt elit. Phasellus sed vulputate leo. + +Sed efficitur quam ac iaculis volutpat. Praesent nec feugiat ex. Aenean at ligula iaculis, imperdiet lacus et, condimentum magna. Integer euismod leo id mauris condimentum, quis semper lacus blandit. Sed volutpat neque urna, sit amet ullamcorper est dignissim non. Vestibulum tristique sollicitudin risus, sed hendrerit massa finibus id. Vestibulum sit amet risus non eros vehicula malesuada. Nam facilisis lacus sed quam pulvinar, at fermentum lectus tincidunt. + +Vivamus laoreet sem nec odio blandit, ut ornare sem egestas. Suspendisse potenti. Nulla aliquam pretium volutpat. Maecenas a orci suscipit, aliquam eros a, maximus urna. Aliquam eu gravida justo, et hendrerit justo. Suspendisse a commodo lorem, quis viverra nisl. Nulla vel pellentesque nisl. Nulla ligula tellus, vehicula sit amet turpis in, sodales tincidunt tellus. Proin ut nibh sed ipsum porta dignissim vel sed mauris. Interdum et malesuada fames ac ante ipsum primis in faucibus. In hac habitasse platea dictumst. Vestibulum efficitur nulla rutrum, accumsan lacus at, dignissim tortor. Morbi egestas metus ut nibh tincidunt posuere at sed elit. Integer eget hendrerit nulla. + +Donec sagittis sagittis tempus. Proin iaculis neque vehicula commodo faucibus. Pellentesque erat ante, vehicula sed efficitur vitae, varius non sem. Mauris pellentesque, felis nec egestas scelerisque, nulla nunc fringilla arcu, feugiat fringilla quam neque in elit. Nullam ut ex odio. Mauris enim risus, consequat at suscipit at, pulvinar ut arcu. Fusce mollis sem sed tellus tempus pellentesque. In pharetra, libero sed tristique vestibulum, massa velit egestas risus, at mollis augue lorem sit amet turpis. Mauris risus dui, sagittis vitae congue ut, condimentum ut augue. Quisque non sollicitudin purus, sit amet consectetur sapien. + +Sed scelerisque ipsum sed augue varius, sit amet ultricies purus posuere. Etiam vehicula at nunc in posuere. Cras eget risus a sapien mollis facilisis. Morbi vel purus auctor, faucibus mauris non, malesuada leo. Donec venenatis consectetur libero in pretium. Ut efficitur molestie metus id scelerisque. Nunc dolor justo, pharetra vel sagittis vitae, placerat ut justo. Donec tempor fermentum est semper auctor. Nullam tincidunt risus non risus elementum varius. Suspendisse euismod augue metus, nec pellentesque enim sagittis ac. Morbi et lectus quis justo commodo varius commodo vel risus. In bibendum purus in erat ullamcorper, sit amet dignissim sapien scelerisque. Quisque tempus elementum rutrum. + +In viverra sollicitudin pretium. Sed finibus scelerisque sollicitudin. Nulla lobortis purus in lectus iaculis, a ornare ex accumsan. Morbi malesuada mollis placerat. In hac habitasse platea dictumst. Pellentesque sed dui quis odio scelerisque molestie eu nec libero. Proin viverra magna ligula, at luctus lacus posuere sed. Nullam non congue mi. Praesent non mattis neque, sit amet tempus diam. Mauris eget cursus lacus, id dictum mi. + +Sed semper velit in vehicula tristique. In lobortis est augue, et maximus tellus iaculis ut. Proin quis ex maximus erat iaculis imperdiet. Curabitur ultrices turpis ac nibh congue ornare. Fusce a aliquet felis, nec sodales tellus. Nunc mauris ante, feugiat et facilisis at, pharetra ultricies dui. Integer felis metus, porttitor ac ipsum vitae, placerat varius ex. Morbi in dui a nunc aliquam posuere. Nulla tempus tortor ac lorem consectetur sodales. Praesent sit amet placerat diam. Curabitur sodales mauris ante, et tincidunt lacus ultricies quis. Nulla eu finibus nisl, sit amet volutpat leo. Donec ligula enim, feugiat non aliquet nec, congue interdum lorem. Pellentesque a nisi blandit, semper massa sit amet, auctor eros. + +Aenean ligula libero, commodo a erat at, tristique facilisis quam. Sed congue fringilla lacus, vel iaculis quam suscipit ornare. Curabitur non pretium mi. Donec condimentum odio dui, id malesuada ipsum porta ac. Aliquam imperdiet cursus ullamcorper. Duis cursus tincidunt nibh sit amet cursus. Aliquam urna orci, sodales ac ipsum at, placerat lobortis neque. Sed sit amet convallis felis, vestibulum finibus arcu. Phasellus venenatis egestas felis, id aliquam turpis iaculis non. Proin a lacus ut felis malesuada sodales. Duis elementum, eros ac placerat bibendum, quam nisl varius mauris, vel venenatis neque augue et justo. Nunc varius sem velit, vel tempus mi aliquet id. Fusce purus massa, mollis id nulla non, dignissim cursus massa. Sed sollicitudin interdum felis, nec eleifend nisl feugiat eget. Nulla rutrum id odio ut consectetur. Maecenas fermentum turpis vitae libero ullamcorper suscipit. + +Vestibulum auctor lectus ligula, non sollicitudin odio maximus nec. Cras faucibus, felis sed suscipit tempor, risus lorem consectetur est, eget pulvinar nibh dui eu dolor. Vestibulum pellentesque, ex aliquam vulputate laoreet, libero lorem rhoncus risus, vitae congue velit justo vitae nisi. Nullam placerat venenatis tortor, sit amet lacinia augue placerat nec. Quisque vulputate lectus vel pulvinar condimentum. Nullam at libero iaculis, mattis purus vel, tincidunt diam. Suspendisse eu ullamcorper eros. Nulla id eros odio. Ut enim leo, ultricies quis mauris sed, interdum commodo felis. Etiam enim lacus, scelerisque a interdum eget, mollis vel tellus. Phasellus vel vulputate orci. Nulla ornare leo sed arcu rhoncus convallis. Duis libero arcu, pulvinar ut tellus vitae, aliquam euismod magna. Donec semper nisi eget nulla tempus, sed condimentum massa sollicitudin. + +Suspendisse bibendum ante vitae ullamcorper semper. Praesent dui est, elementum nec lacus in, pellentesque accumsan sapien. Cras quis urna at lacus posuere commodo eget nec arcu. Nunc varius ante quis ligula rhoncus, ut dignissim metus commodo. Integer volutpat gravida nunc eget faucibus. Quisque imperdiet, mauris vitae cursus malesuada, lacus mauris tempus sem, ut accumsan purus lectus quis nisi. Vestibulum aliquam dui sed nisl laoreet, id posuere quam imperdiet. Aliquam ultricies non enim vel tempor. Donec sed feugiat justo, vel rutrum enim. Phasellus lorem quam, dapibus a tincidunt vulputate, pellentesque non lorem. + +Nunc quam diam, condimentum sit amet enim semper, hendrerit laoreet magna. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nullam et pulvinar leo, ac fermentum lectus. Sed erat ante, mattis ac tempor vitae, euismod at tortor. Ut sagittis fringilla scelerisque. Ut pulvinar molestie leo eu faucibus. Sed quis efficitur purus. Pellentesque nibh nisi, porta id orci id, sagittis sodales tellus. Ut venenatis velit a lectus lacinia, at ultrices nisi commodo. Vivamus eros orci, ornare vel ullamcorper elementum, posuere id ligula. Aliquam non massa pellentesque, semper ipsum scelerisque, dapibus leo. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nulla pretium leo diam, sit amet bibendum lacus tempus quis. Interdum et malesuada fames ac ante ipsum primis in faucibus. Integer quis pulvinar nibh. + +Mauris sed eros nisi. Cras malesuada, turpis vitae laoreet porta, sapien odio maximus nulla, ut efficitur mauris odio tincidunt elit. Nam ut urna eros. Sed at vestibulum risus. Nulla luctus pulvinar rhoncus. Pellentesque maximus ligula a est ullamcorper, sed tempus tortor ultrices. Curabitur ligula odio, mollis sit amet risus quis, tempor auctor magna. Suspendisse vel quam quis lorem commodo facilisis. Donec eu ipsum suscipit, molestie dui sed, fringilla dui. Proin placerat ex a lorem sodales convallis. Sed molestie quam mi. + +Fusce laoreet nec dolor id bibendum. Nunc condimentum vulputate massa lacinia fermentum. Donec maximus cursus eros sed porta. Nunc eu porta turpis. Nulla cursus et nisl eu elementum. Ut rutrum scelerisque aliquet. In tellus erat, rhoncus id tempus vitae, vestibulum non quam. + +Vivamus luctus elit a efficitur dapibus. Praesent magna mi, pellentesque sed accumsan dapibus, blandit at lectus. Praesent sodales erat diam. Pellentesque placerat lobortis enim, a dapibus ligula molestie ut. Phasellus dui augue, suscipit ac urna non, iaculis eleifend augue. Maecenas sed elit iaculis, pretium ligula lacinia, aliquam libero. Nulla sem mi, pellentesque viverra lorem vel, luctus vulputate augue. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Quisque fermentum nunc ex, at lobortis turpis congue vitae. Suspendisse iaculis elementum enim commodo facilisis. Vestibulum non neque tellus. Proin vitae sodales urna. Mauris interdum purus et neque tempus, vitae mattis libero finibus. Suspendisse iaculis condimentum nibh, eu mattis enim vehicula a. + +Fusce dictum urna non lectus ullamcorper vestibulum id in elit. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Nunc fermentum odio non ante sollicitudin posuere. Praesent vulputate quam nec magna euismod pellentesque. Etiam tempor nunc eget velit volutpat eleifend. Mauris sodales nunc ullamcorper, gravida purus eget, rutrum sem. Nam a sapien rhoncus, scelerisque lacus ac, condimentum ipsum. Pellentesque suscipit, ligula ut rhoncus ullamcorper, ligula augue rutrum lorem, eget aliquam risus tortor eget metus. Curabitur scelerisque bibendum purus vel vulputate. Morbi et odio at neque sodales suscipit. Nam at pretium nulla. In scelerisque vulputate suscipit. Nulla facilisi. Pellentesque eu sem eu ligula efficitur viverra. Pellentesque gravida consequat urna id bibendum. + +Nulla consectetur facilisis est nec aliquam. Proin hendrerit magna suscipit ante accumsan hendrerit. Nulla ut sem id neque tempor vehicula. Sed eget massa eget sem placerat tincidunt. Ut eleifend, justo sit amet egestas lacinia, ex velit pharetra nulla, nec aliquet elit neque ut turpis. Quisque in gravida velit. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed euismod malesuada convallis. Ut ultricies, magna et blandit fringilla, dolor dolor maximus sapien, in aliquet augue est vel odio. Nulla commodo elit massa, euismod tempor turpis aliquet eget. Morbi non odio et tortor egestas ultrices. Etiam semper, ipsum et dictum pharetra, eros purus bibendum lacus, vel laoreet lacus sem vehicula nibh. Nunc enim nulla, pulvinar sit amet lobortis sed, porttitor molestie risus. Morbi tincidunt diam mi, nec auctor sem rutrum at. + +Donec pellentesque odio odio, eu condimentum risus consectetur quis. Vivamus ullamcorper, mauris non semper rutrum, dui risus suscipit tellus, ut tempor velit risus vitae nibh. Duis tempor nibh at tristique rutrum. Cras congue nisl at sem sollicitudin efficitur. Aenean auctor purus vel libero fermentum elementum. Mauris convallis orci id interdum accumsan. Aliquam at metus risus. Sed accumsan, quam id dignissim congue, velit risus eleifend ligula, ut fringilla elit erat at neque. Aliquam tempor, quam ut ultrices volutpat, orci lectus vulputate turpis, eget pharetra purus nisi non lorem. Ut condimentum convallis justo, a volutpat eros. Sed tempus turpis leo, in convallis est fringilla sed. Vivamus eu laoreet tellus. Quisque ullamcorper ullamcorper leo. Nam fermentum egestas facilisis. Nulla egestas ligula feugiat urna molestie, faucibus convallis erat accumsan. Cras pellentesque ipsum lectus, vitae luctus dolor suscipit nec. + +Vivamus vel nibh sed mi tincidunt cursus quis facilisis tellus. Donec eget est eu velit pretium molestie. Maecenas lacinia risus turpis. Sed tristique id risus sit amet venenatis. Duis maximus, metus ac molestie convallis, quam ex dapibus sem, at rutrum metus nisi quis lectus. Suspendisse sodales in nisi at hendrerit. Maecenas suscipit lobortis vulputate. Nunc convallis sit amet elit eget porta. Mauris tincidunt massa in augue finibus iaculis. Vestibulum imperdiet, orci vel bibendum laoreet, ligula quam mollis lacus, a eleifend nisi tellus vitae ipsum. Cras non ante sollicitudin, auctor dolor id, condimentum tellus. Morbi malesuada leo nec lectus scelerisque, nec interdum lacus ornare. Integer ultrices ligula nunc, sed blandit urna aliquam eget. Proin consequat viverra ex non rhoncus. Phasellus id nibh at tortor sodales blandit. Donec dignissim ipsum vel ligula malesuada rhoncus. + +Nullam mauris sem, dapibus ac nisl at, hendrerit faucibus nisi. Mauris dictum fermentum pellentesque. Praesent in gravida odio. Vestibulum pharetra iaculis est quis tincidunt. Sed venenatis rhoncus lacus, non porta ante vulputate at. Duis fringilla ipsum at urna euismod molestie. Duis a porttitor ante. Mauris pharetra elit et metus auctor, a eleifend sapien porta. Vivamus ut tincidunt purus, lobortis euismod tellus. Nullam non nulla gravida, laoreet tellus ac, placerat urna. + +Sed justo sapien, scelerisque nec nisl vel, efficitur aliquet purus. Phasellus eget posuere nisl. Integer porta vel purus nec ornare. Pellentesque rutrum in nulla at hendrerit. Nullam elementum sodales volutpat. Duis tempus, purus sagittis auctor ullamcorper, dui erat hendrerit nulla, id finibus eros dui quis risus. Quisque posuere nunc quis augue placerat lacinia. Nunc quis lorem vulputate, tincidunt enim nec, rhoncus odio. Ut porttitor porta velit ut vulputate. Duis convallis sagittis magna nec gravida. Ut eu luctus sem, non placerat erat. Vivamus viverra ut ligula ac finibus. Cras ligula urna, tincidunt id risus eu, dapibus viverra lorem. Aliquam erat volutpat. Sed dolor nisi, faucibus non ligula faucibus, laoreet bibendum diam. Vivamus sagittis lacus ut condimentum tincidunt. + +Proin tristique mi ullamcorper dolor accumsan mollis. Nulla facilisi. Pellentesque iaculis mattis augue ac rutrum. Mauris in nulla at ligula fringilla pulvinar. Nam commodo ornare nibh ac viverra. Maecenas eget augue a risus mattis ullamcorper non at nibh. Aenean nec erat diam. Pellentesque augue nisl, venenatis mollis dolor non, bibendum malesuada leo. Pellentesque elementum purus ornare mauris iaculis porttitor. Sed mollis tempor dui eu elementum. Donec porttitor tellus non mattis gravida. Mauris venenatis suscipit convallis. Sed dolor sapien, pellentesque et tellus a, tempus cursus magna. Nunc lobortis leo magna, at maximus orci ultrices eget. Nullam vulputate dictum nisi, vel egestas orci. + +Nam nibh mi, ornare sit amet nibh vel, lobortis lacinia leo. Ut egestas mi vel nunc luctus vestibulum. Nullam id tempus felis, eget convallis erat. Aliquam venenatis ut tellus id fringilla. Aliquam erat volutpat. Sed vehicula, odio nec mollis dapibus, ligula sapien consequat risus, ut volutpat diam neque ut neque. Mauris venenatis libero vitae sem elementum, sit amet maximus massa varius. Maecenas malesuada mi id leo euismod, eu maximus sapien vehicula. Nulla facilisi. Duis nec venenatis ante, non pulvinar lacus. In sollicitudin sit amet velit suscipit egestas. Maecenas a lectus euismod, dictum nulla at, malesuada lectus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec lorem massa, laoreet non elit nec, sollicitudin blandit felis. + +Cras dignissim fermentum tellus ut fringilla. Maecenas maximus eros pretium sagittis venenatis. Maecenas id enim ac est semper efficitur ac hendrerit leo. Cras eu arcu tincidunt risus pellentesque aliquam. Pellentesque vel sodales libero, non rhoncus enim. Integer vulputate vulputate libero, eu consectetur eros euismod vitae. Fusce magna nibh, vehicula sed turpis eget, malesuada ultricies sem. Sed convallis sem nisl, rhoncus mollis mauris bibendum ut. Pellentesque vitae massa a justo dapibus laoreet. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Sed mollis egestas nisl vel ultricies. Phasellus sit amet varius leo, sed vehicula sapien. Etiam in urna eget neque aliquet ultricies in nec quam. Aliquam at feugiat elit. + +Maecenas placerat nisi ultrices neque pulvinar, et ultrices ante tempus. Vestibulum laoreet quam lacus, eget commodo magna dictum consectetur. Aliquam erat volutpat. Aenean tincidunt ipsum sit amet justo aliquet tempus. Quisque blandit sit amet est facilisis dictum. Sed non consequat dui. Integer purus diam, gravida quis tortor lobortis, iaculis vehicula sem. Morbi pellentesque est elit, vitae interdum elit laoreet et. Vestibulum in blandit ante, eu blandit velit. Morbi sagittis eu est eu vulputate. Nullam ac mi feugiat nibh feugiat suscipit. Sed interdum tincidunt efficitur. Integer dictum sem erat, ut pharetra libero lacinia in. Mauris at hendrerit odio, a facilisis lacus. Donec blandit massa ac nisi ultrices faucibus. + +Aenean tempus id ligula at mollis. Cras id dui magna. Integer id neque tincidunt, rhoncus nunc et, laoreet nibh. Pellentesque consectetur odio ligula, et consectetur tortor scelerisque non. Cras quis ex pharetra mi ornare lobortis. Phasellus fringilla interdum felis, vel aliquam ligula. Mauris cursus varius turpis in iaculis. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Praesent eget mi id nulla semper dapibus. + +Vivamus lacus augue, eleifend dignissim ipsum eu, interdum accumsan massa. Nam id quam vel ligula consectetur volutpat id eget eros. Aliquam sed felis velit. Duis egestas velit ac dolor blandit, ut elementum mi tincidunt. Integer varius nisl a sapien pellentesque, et pellentesque ante elementum. Fusce ac orci interdum, faucibus nisl ut, sagittis nisi. Vestibulum sed diam eu magna congue ornare. Integer dignissim ligula sit amet mauris pretium vulputate. Duis ac tincidunt magna. Sed vehicula, ante nec vulputate venenatis, mauris odio blandit dolor, vitae lacinia nibh lorem et metus. + +Sed non sapien vel lorem tempor semper ac ac tellus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas auctor ante sit amet suscipit vulputate. In ac cursus turpis. Donec eu sem bibendum, blandit est nec, blandit tortor. Pellentesque scelerisque justo vitae magna cursus, vitae egestas libero interdum. Pellentesque vitae ligula vel mauris vehicula convallis. Nunc ornare lectus sit amet lorem aliquet dignissim. Cras nec ligula pretium, semper nulla a, pulvinar lacus. Sed ac augue bibendum, posuere ipsum eu, venenatis quam. Sed elementum nunc quis odio pellentesque, a vestibulum ex congue. Cras id malesuada arcu. Mauris ut egestas nulla, sed tempus ligula. Donec ac elit ut nisi pulvinar elementum. Suspendisse id auctor lectus, vitae ultricies lacus. + +Vestibulum pulvinar ornare cursus. Curabitur accumsan sollicitudin mi in vestibulum. Vestibulum vulputate tincidunt luctus. Suspendisse potenti. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Phasellus a mattis neque, id malesuada odio. Aliquam id auctor magna. Vestibulum quis nibh lacinia, tincidunt felis sed, vestibulum ex. + +Donec placerat ipsum diam, vel imperdiet velit eleifend ac. Quisque dapibus erat non dui convallis eleifend. Praesent pellentesque felis id suscipit rutrum. Donec quis lacinia turpis. Nulla justo eros, fringilla vel fringilla in, euismod sollicitudin arcu. In ut lobortis arcu, at rhoncus elit. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. + +Pellentesque iaculis quam nec interdum eleifend. Fusce mauris mauris, imperdiet sollicitudin volutpat eu, sollicitudin blandit leo. Vestibulum hendrerit diam gravida erat imperdiet, blandit dignissim dui tempor. Phasellus viverra ante quis aliquam finibus. Duis sed condimentum augue, nec sollicitudin dui. Ut ullamcorper metus ac ligula accumsan, ac fringilla metus gravida. Morbi rhoncus est nunc, at lacinia ex fringilla sit amet. Integer lobortis ultricies nisi vitae accumsan. Nulla nec sagittis ante. Mauris bibendum nisi ut magna vulputate maximus. Praesent in elit eu metus dignissim cursus. Ut a tellus felis. Mauris eget ornare diam. + +Suspendisse potenti. Nam nec tortor ex. Duis eu elementum ligula. Pellentesque efficitur sodales orci, vitae sollicitudin odio tempor ac. Nam lacus ante, lobortis vitae lobortis eu, eleifend et velit. Nulla et lectus eu nibh tristique malesuada a sit amet felis. Proin molestie, lectus vitae sagittis malesuada, massa velit finibus est, id vestibulum dolor felis eget lectus. Phasellus varius pellentesque neque, a malesuada ex mattis eget. Nulla facilisi. Phasellus interdum placerat lobortis. Sed et odio tristique, viverra libero et, sagittis diam. Proin tristique lectus id bibendum maximus. Integer ornare euismod ligula nec malesuada. Nulla facilisi. Nullam bibendum hendrerit pharetra. + +Duis pharetra auctor felis, eget aliquet justo commodo at. Donec quis nibh non arcu sodales efficitur. Aenean lorem sapien, tincidunt eu sapien nec, convallis tempor diam. Curabitur volutpat, felis ut congue euismod, lacus nisl euismod ipsum, vel consectetur orci nibh sed ligula. Curabitur consectetur vitae mauris sed tempor. Fusce vel pretium nibh, et tristique dolor. Aliquam semper a ante non finibus. Praesent euismod urna augue, at feugiat orci commodo ut. Duis imperdiet purus non augue cursus gravida. Maecenas sodales purus et sollicitudin venenatis. Nam ultrices lorem lectus, ut pretium turpis hendrerit eget. Vestibulum vel lacinia lectus, at dignissim diam. Vivamus ut tortor ac tortor blandit finibus. Etiam porttitor tortor sit amet elit lacinia gravida. Pellentesque pretium, orci vitae tempor vehicula, nisi tellus tincidunt sapien, id egestas felis quam a nunc. Pellentesque sed vestibulum nisl. + +Mauris congue, justo vel dapibus pretium, ipsum augue consectetur leo, at aliquam ipsum neque non mauris. Nam sollicitudin in urna eu blandit. Aliquam erat volutpat. Morbi eget interdum ante. Pellentesque congue gravida arcu, eget ornare ante elementum nec. Integer a auctor dolor, in varius sem. Etiam dictum nibh magna, at volutpat nunc blandit eu. Curabitur at sem ipsum. + +Nulla porttitor erat eget volutpat iaculis. Pellentesque egestas, nisl vel ultrices efficitur, ex magna condimentum urna, in tincidunt massa turpis eget metus. Etiam vitae magna elementum, fermentum justo ut, pretium velit. Aliquam aliquet venenatis malesuada. Quisque consequat enim metus, pellentesque blandit leo rhoncus id. Vivamus vestibulum, est in condimentum mollis, ligula eros blandit lorem, vitae dignissim lectus mi eget nulla. Cras posuere leo at neque convallis, sed pretium massa ultrices. Morbi tempus porttitor turpis. Nam vitae mauris et massa venenatis tincidunt. Quisque vestibulum lacus ligula, in sodales felis elementum vel. Sed a tellus condimentum, sodales nisi id, aliquet elit. + +Vestibulum ac ex eu turpis lacinia condimentum nec eget ipsum. Nulla non imperdiet erat, at malesuada lorem. Praesent efficitur imperdiet augue vel laoreet. Sed dignissim, ante non porta feugiat, enim nunc rhoncus lorem, eu luctus turpis risus sit amet orci. Vivamus bibendum pharetra venenatis. In at gravida nisi. Vestibulum pulvinar sapien ac augue scelerisque, vitae blandit nibh malesuada. Nunc vitae est faucibus, accumsan risus a, laoreet urna. Cras imperdiet lacus in augue facilisis dignissim. Duis sed nisi quis diam mollis rutrum. + +Vestibulum eget egestas neque. Praesent viverra, velit quis porttitor euismod, sem libero venenatis mauris, id congue urna nulla sed lorem. Nulla mollis metus nec diam malesuada aliquam. Duis ac risus nunc. Cras sollicitudin urna nunc, id sodales quam gravida sit amet. Fusce in vulputate orci, in venenatis lorem. Donec a sagittis ipsum. Quisque consequat sapien tellus, sed efficitur lacus aliquam eu. Interdum et malesuada fames ac ante ipsum primis in faucibus. Aenean in neque at augue elementum commodo pellentesque eget ligula. Nullam condimentum efficitur tincidunt. Phasellus posuere tincidunt odio sed facilisis. Aenean eu risus at est euismod facilisis. Curabitur elit purus, malesuada quis blandit id, rutrum vitae dui. Praesent porta rutrum erat, ullamcorper viverra nunc. Cras ut velit dui. + +Etiam posuere pulvinar mi at ullamcorper. Pellentesque finibus, tellus non convallis commodo, orci nibh dapibus nisl, at aliquam purus nulla eget dui. Praesent fringilla urna nec nulla pellentesque, nec rhoncus turpis ultricies. Sed laoreet velit pellentesque libero varius, ac interdum urna viverra. Phasellus sed consectetur massa. Morbi quis velit nec ipsum varius tempor. Proin id sodales felis. Aliquam lacinia risus quis ligula condimentum sodales. Nulla vel arcu aliquet neque iaculis aliquet. Cras sed lorem eu turpis tincidunt sodales. Sed pulvinar elementum ligula, nec faucibus nisl. Fusce nec tellus eget dui tempor sagittis. In vitae enim in ex viverra commodo. Duis est erat, fringilla ac semper a, dapibus in tortor. + +Maecenas commodo vulputate iaculis. Aliquam non facilisis est. Donec pellentesque vitae nibh nec volutpat. In commodo metus placerat lorem commodo, non lacinia nibh bibendum. In viverra rhoncus erat. Mauris nec nisl blandit, elementum justo nec, accumsan libero. Aliquam elementum, velit et ullamcorper convallis, turpis lorem elementum lorem, quis consequat tortor eros ut erat. Curabitur et enim quis felis vulputate congue et vel purus. Sed elementum interdum ipsum, sed tincidunt arcu scelerisque et. + +Aenean interdum elementum mauris ut porta. Mauris vel purus ac odio vulputate pulvinar at quis odio. In turpis turpis, convallis in augue id, elementum vulputate lorem. Sed tincidunt fermentum vulputate. Nunc ipsum ipsum, molestie vel convallis id, pharetra vel arcu. Maecenas vel dui elit. Sed blandit dolor sit amet risus commodo faucibus. Duis rhoncus felis arcu, vel aliquam nisi faucibus sed. Suspendisse cursus eget nunc ut bibendum. Fusce non ligula risus. Curabitur vitae cursus metus, quis fringilla diam. Phasellus turpis ante, pulvinar ac turpis tristique, sollicitudin congue lectus. Proin quis ipsum at ipsum euismod euismod. + +Nunc ut fermentum nunc. Donec id commodo lacus, at hendrerit justo. Donec cursus purus sodales nunc commodo, quis bibendum elit hendrerit. Quisque quis pellentesque nibh, ac vulputate neque. Aenean non placerat felis, eget feugiat ligula. Vivamus a eros accumsan, cursus neque at, ultricies magna. Fusce tincidunt tellus vitae mi rutrum laoreet sed quis ligula. Nullam ullamcorper ligula ligula, vel fringilla metus aliquet a. Morbi aliquet, mauris vel interdum venenatis, ex arcu venenatis tortor, at tincidunt dui ipsum et arcu. Nulla blandit gravida nulla ac iaculis. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Sed ullamcorper lectus in sapien lobortis, eget posuere massa varius. + +Cras lacinia nunc faucibus mauris placerat pretium eget non sapien. Morbi viverra bibendum posuere. Aliquam accumsan sagittis dolor non iaculis. Sed nunc odio, lobortis in dolor ac, rutrum fermentum velit. In venenatis, velit in molestie semper, est magna condimentum dui, aliquam auctor lorem nulla vel ligula. Morbi vehicula turpis turpis, quis mollis justo tempus vitae. Proin luctus lacus in mauris porttitor aliquet. Duis vitae nunc ex. Nullam eget erat vitae nulla iaculis rhoncus. Sed lacus dui, suscipit eu leo vitae, tincidunt dignissim risus. Praesent ut massa ut arcu sagittis consequat. Sed sit amet tincidunt turpis. Nulla bibendum, felis eget posuere dictum, libero mi tristique elit, at venenatis neque elit ac quam. Curabitur nisi sem, scelerisque nec nunc ac, pulvinar pharetra odio. Curabitur egestas pellentesque arcu sed suscipit. In mattis dolor vel dui mollis feugiat. + +Sed commodo, dui ac vestibulum dictum, tellus libero tincidunt lacus, viverra commodo est felis vitae urna. Proin tincidunt neque vel turpis eleifend laoreet. Vestibulum sagittis, tortor sed iaculis consequat, urna ante sagittis est, ac ullamcorper lorem nibh dignissim odio. Nam arcu mi, cursus et blandit nec, aliquam ut nulla. Aenean quis iaculis tellus, eu egestas augue. Etiam pretium eget nisi quis iaculis. Aliquam sed convallis eros. Ut bibendum rhoncus lacus, in vestibulum dui ultricies id. Fusce vestibulum, mauris ut tempor consequat, dolor nisl pellentesque elit, in porta arcu elit vel ante. Vivamus at nisl est. Etiam nec blandit tortor, at pulvinar orci. Proin semper dapibus tincidunt. Phasellus lobortis enim ullamcorper dolor tempor cursus. + +Mauris a libero in enim gravida aliquet ac sit amet nibh. Vestibulum ac neque posuere, blandit libero ac, vehicula enim. Aliquam auctor iaculis eros sit amet molestie. Quisque faucibus turpis et massa tristique, nec dapibus mauris aliquet. Proin blandit aliquet mauris, non tincidunt odio blandit vel. Curabitur a nibh in eros commodo tincidunt eu et libero. Curabitur sit amet dapibus ex, in condimentum magna. Sed eu sem sem. Nunc tellus dolor, rutrum eu mauris nec, congue feugiat purus. Fusce tempor, neque vitae bibendum imperdiet, dolor ipsum condimentum urna, et egestas quam tortor in ex. Aliquam velit magna, commodo hendrerit sagittis sed, feugiat eget erat. Nunc quis ullamcorper velit, eu consequat augue. Nam arcu mauris, condimentum sit amet magna in, finibus scelerisque nunc. Mauris erat est, hendrerit ac accumsan eget, facilisis ut nisl. Quisque dignissim arcu quis diam tincidunt tristique. Sed rhoncus nisl non enim fermentum, a lobortis dolor consectetur. + +Sed eget condimentum ligula. Vestibulum vitae cursus eros. Donec elementum sapien magna, posuere iaculis sem ultrices lobortis. Morbi eu bibendum lectus. Suspendisse ante eros, ullamcorper ac viverra eget, pellentesque sed sapien. Duis sit amet tincidunt dui, vitae lobortis purus. Sed venenatis tincidunt volutpat. Vivamus a nisl ac elit consectetur semper ut eu libero. Proin id cursus ex. In hac habitasse platea dictumst. Aenean sed nisi vitae odio venenatis pulvinar vitae ac risus. Sed varius magna ut erat luctus vehicula. + +Nunc non ex eget purus blandit faucibus at quis velit. Donec quis mi vestibulum, facilisis nisi quis, tincidunt turpis. Sed bibendum metus sed consectetur mollis. Maecenas fermentum, erat finibus pulvinar lacinia, ex risus dictum sem, ut vestibulum augue diam vel diam. Donec ac massa non nibh pretium laoreet eget in orci. Nulla placerat eleifend mi, pretium vestibulum diam condimentum vitae. Nunc odio turpis, feugiat vitae turpis eget, pellentesque commodo turpis. Etiam sapien purus, consequat nec mi eget, consectetur efficitur neque. Phasellus porttitor sapien sit amet nunc semper, vel bibendum nibh finibus. Ut ac imperdiet ex, eu congue felis. In posuere nisi felis. Mauris tempus pretium mauris, ac viverra nunc hendrerit id. Sed fermentum nec sem ac pulvinar. Integer dictum velit eget congue venenatis. + +Cras eros dolor, venenatis ac dictum sed, dignissim nec sem. Curabitur tempor erat quis pretium interdum. Nunc vestibulum justo nisi, sit amet sagittis tellus consequat vel. Donec pharetra nunc vitae consequat eleifend. Quisque ut mauris quis nunc volutpat consectetur. Nam a suscipit ligula, at gravida libero. Vestibulum blandit, tellus sed bibendum volutpat, libero tortor convallis nisl, sit amet placerat lorem dolor nec libero. Integer blandit libero elit, ut congue ex euismod congue. Nulla sodales justo id eros condimentum faucibus. + +Proin quam ante, hendrerit at bibendum eu, pharetra at lectus. Proin finibus arcu id nisl aliquam dapibus. Fusce a suscipit nisl. Ut placerat ultrices nibh nec efficitur. Vestibulum vitae interdum magna. Donec lobortis finibus risus, et luctus ipsum efficitur euismod. Quisque dictum diam et venenatis euismod. Cras dictum molestie aliquet. Vestibulum imperdiet quam eget diam malesuada, quis pulvinar odio dignissim. Curabitur sapien elit, iaculis vitae justo eget, pretium malesuada elit. Donec gravida molestie tincidunt. Nullam at commodo ipsum. Sed nisi erat, tincidunt ultricies interdum consequat, pretium mollis ipsum. Vivamus in erat consectetur, sodales nibh at, porta nunc. Mauris semper, dui vitae condimentum tempus, mauris justo volutpat urna, ac ullamcorper tortor dolor in sem. + +Aenean leo ligula, egestas at vestibulum ac, porta vel mauris. Curabitur at lorem non mauris fringilla venenatis vel eu arcu. Donec posuere eleifend diam. Duis aliquet justo lacus, eu egestas erat eleifend sed. Sed non cursus urna. Sed pretium purus id blandit varius. Mauris turpis mi, lobortis eu tellus sit amet, maximus venenatis felis. Proin non varius enim, aliquet finibus velit. Praesent posuere pharetra ipsum eget varius. Phasellus non fermentum magna, ut iaculis augue. Praesent ut nisl nunc. + +Sed ligula tellus, interdum at sapien ut, dictum pellentesque nisl. Duis fringilla, sem nec elementum dapibus, nisl tellus maximus velit, eu varius sem nisl feugiat eros. Maecenas quis viverra lacus. Nulla nec commodo ex, nec placerat enim. Curabitur ultrices sagittis fringilla. Vestibulum sit amet enim sagittis, condimentum nisl sit amet, pulvinar orci. Ut at erat finibus erat bibendum convallis. Quisque euismod magna eget leo facilisis hendrerit. Cras venenatis, nisi quis sollicitudin volutpat, metus enim vestibulum nunc, at mollis leo leo nec est. Fusce fermentum tristique feugiat. Suspendisse sem est, condimentum ut ante at, egestas convallis ante. Vestibulum dictum, ex nec imperdiet laoreet, magna est ullamcorper augue, vel molestie quam odio vel ante. Donec dui dui, posuere a neque ac, condimentum vehicula magna. Curabitur suscipit, risus vitae bibendum posuere, mauris lorem viverra est, at tristique arcu quam quis leo. Donec sed posuere neque. Suspendisse iaculis aliquet condimentum. + +Morbi tempus condimentum diam eget bibendum. Aliquam varius magna quis lectus interdum, in elementum ligula tempor. Morbi vitae lorem sapien. Morbi luctus consectetur eros non aliquet. Pellentesque vestibulum sem sed ante accumsan faucibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Etiam ac suscipit justo. Nunc efficitur lectus eu arcu venenatis, vel accumsan ex suscipit. Vestibulum egestas ultricies tellus, et interdum enim pretium eu. Aenean rutrum est tincidunt rhoncus molestie. Phasellus hendrerit tellus et laoreet varius. Integer efficitur felis magna, nec sollicitudin arcu sollicitudin in. Curabitur non feugiat odio. Mauris nisi odio, luctus quis dolor sed, tristique luctus ex. Nullam libero enim, facilisis ut venenatis et, vulputate sed purus. + +Mauris a odio ut leo porttitor ultrices a et ex. Pellentesque vestibulum lacinia faucibus. In pellentesque eget augue at feugiat. Integer finibus augue dolor, in luctus lorem rutrum vitae. Donec pharetra lectus ac purus sollicitudin, vel tristique mauris pretium. Cras porttitor mi eu lectus consequat, a ultrices felis venenatis. In condimentum turpis in velit mattis laoreet. Aliquam sed mauris id nulla ultrices convallis vel vel velit. Suspendisse ut arcu finibus justo fermentum fringilla. Etiam in malesuada nisl. In hac habitasse platea dictumst. Duis a est lacinia, pretium dui eget, condimentum turpis. Integer ac placerat augue. Donec et eros felis. + +Integer cursus magna id quam sagittis consectetur. Aliquam erat volutpat. Quisque ullamcorper nisl nec massa dapibus facilisis vitae at nunc. Donec laoreet, libero in elementum tempus, enim odio porttitor felis, venenatis fermentum augue velit eu urna. Ut at ullamcorper enim. In molestie, velit et blandit maximus, erat nunc laoreet quam, vel finibus est mauris non sapien. Donec et dictum lorem. Nunc vestibulum, dolor eget tempus maximus, elit eros aliquam velit, vitae mattis mauris lectus ac tortor. Donec rutrum justo orci, id dictum metus vestibulum a. Etiam bibendum ipsum convallis, lacinia turpis vitae, dictum tortor. In velit dolor, scelerisque sed molestie nec, volutpat a turpis. Cras posuere commodo erat ut gravida. Quisque ante ipsum, volutpat a tellus non, dignissim ornare elit. Pellentesque sed porttitor dui, luctus rhoncus purus. In vitae ante pulvinar, consectetur orci quis, tempus velit. Curabitur tempus ligula id sapien ornare rhoncus. + +Maecenas eu nibh et elit accumsan blandit a at erat. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Etiam commodo feugiat condimentum. In non ante ut mauris eleifend lobortis. Phasellus eleifend vitae metus et semper. Nam sit amet rhoncus diam. Nunc molestie libero sed erat volutpat consequat. + +Aenean eget tristique odio. Vivamus quam tellus, dignissim sed faucibus sed, sagittis ut elit. Maecenas in ullamcorper sem. In at ipsum accumsan lectus vestibulum commodo nec non leo. Aliquam at suscipit felis. Nunc non egestas tortor. Donec sit amet eleifend eros. + +Sed eleifend nisi velit, in egestas erat condimentum eu. Pellentesque a tincidunt urna. Interdum et malesuada fames ac ante ipsum primis in faucibus. Vestibulum dapibus mauris vitae elit auctor, id venenatis sem consectetur. Vivamus non leo pulvinar, blandit libero ut, vehicula arcu. Nullam elementum ex enim, at mattis massa pharetra eu. Nunc nulla magna, lobortis a magna sit amet, laoreet fermentum justo. Curabitur aliquam sollicitudin posuere. Aenean semper porta dictum. Mauris accumsan non nisi nec faucibus augue. + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam in nibh eget ante viverra mattis. Etiam elit est, pharetra efficitur fermentum at, accumsan id eros. Aenean accumsan nisi facilisis tempor suscipit. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nam magna erat, tincidunt eu placerat at, molestie at sem. In sit amet cursus mauris. Etiam ullamcorper lacus nisi, et porta metus semper id. Nulla mauris nunc, pharetra at facilisis a, consequat id metus. Sed convallis ligula non felis vulputate finibus. Curabitur nisl nulla, pharetra in justo a, dapibus ultricies dui. Morbi ultricies sollicitudin purus, quis pellentesque leo rhoncus sed. Curabitur sed eros quis lacus vulputate pretium. Vestibulum vitae urna nec arcu vulputate efficitur. Cras venenatis sodales dolor non ullamcorper. + +Donec eu placerat magna. Vestibulum justo lacus, luctus ac luctus sit amet, dapibus at orci. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec eu tortor ac justo pharetra hendrerit. Phasellus ornare lacinia vestibulum. Maecenas diam orci, molestie rhoncus fringilla vitae, aliquam eu purus. Maecenas vehicula felis dui, vel dapibus odio lobortis quis. Cras a velit non nisl efficitur tempor. + +Suspendisse magna enim, ultrices et elementum ut, venenatis ut purus. Curabitur convallis vel tortor id rhoncus. Pellentesque varius arcu non imperdiet eleifend. Vestibulum fringilla aliquet vehicula. Nullam et lorem ullamcorper, tincidunt ante eget, egestas ex. Donec laoreet, justo id tincidunt egestas, lectus diam faucibus tortor, nec venenatis felis leo a urna. In at tortor semper augue finibus ornare et vitae erat. Praesent vulputate, dui sed pulvinar porta, justo tortor feugiat tellus, eget accumsan velit turpis at orci. Nulla vitae velit facilisis augue sagittis fringilla nec sagittis nunc. + +Duis vel elementum odio, id eleifend mauris. Nam in ultrices nulla. Quisque pharetra blandit risus eget lacinia. Nulla mattis mi felis, a ultrices nisi egestas vulputate. Donec a augue ac ex laoreet semper at porta ante. Vestibulum arcu ipsum, vehicula vel mollis lobortis, ullamcorper sed augue. Ut viverra faucibus nunc, sit amet sodales diam gravida sollicitudin. Fusce finibus quam sed ornare consequat. Suspendisse viverra ultricies dui in volutpat. Maecenas blandit erat in rhoncus placerat. Phasellus eu nisi rutrum, bibendum nunc eu, viverra ex. Praesent sollicitudin mi sit amet dictum convallis. Vivamus purus ligula, interdum at tincidunt sit amet, pellentesque ut elit. Aenean vitae ultrices ex. In facilisis lectus est, nec vulputate diam tristique vel. In gravida tincidunt ex et viverra. + +Pellentesque mi justo, dignissim a nisl quis, tempor dictum lacus. Nam tristique varius dolor tincidunt pharetra. Nullam iaculis est sed quam dignissim cursus. Duis sit amet vehicula nisi, sed consectetur velit. Nullam non tellus nec dolor venenatis porta quis at quam. In pharetra id orci sed faucibus. Cras et malesuada erat. + +Quisque fringilla commodo metus nec feugiat. Donec et est urna. Maecenas ut magna dui. Vivamus eu sem venenatis, porta mi at, efficitur tortor. In lacinia hendrerit elit, in faucibus nulla ultrices et. Mauris accumsan nec orci et vestibulum. Aliquam eget justo velit. Mauris fringilla ullamcorper enim, in elementum enim eleifend a. Nulla id libero ac arcu tristique ultrices. Maecenas mattis orci vitae nisl laoreet auctor. + +Quisque id aliquam orci. Nunc molestie vitae quam eu consectetur. Vivamus molestie venenatis est, nec lacinia odio faucibus eget. Etiam ornare eu leo eget posuere. Quisque elementum ligula at euismod placerat. Maecenas lobortis leo diam, vel egestas odio iaculis ac. Integer dapibus mi metus. Sed at eleifend ligula, ullamcorper vestibulum eros. Suspendisse dignissim metus in vulputate iaculis. Nam rhoncus felis sit amet velit scelerisque semper. Ut sed augue eget nulla laoreet scelerisque. + +Aenean convallis ipsum id turpis gravida, et elementum ante facilisis. Donec vitae arcu id mauris mollis hendrerit. Mauris imperdiet cursus nibh at laoreet. Sed sit amet enim magna. Mauris blandit nisi quis arcu sodales, eget consequat orci euismod. Curabitur id bibendum diam. Ut pharetra tellus nec enim tristique, id efficitur eros accumsan. Suspendisse potenti. Praesent vel porttitor risus. Nulla vitae ex nec ex hendrerit euismod non mattis arcu. Aenean eget velit massa. Pellentesque venenatis arcu mauris, sit amet scelerisque sem lobortis ornare. Vivamus auctor porta eros, eget blandit lorem semper non. Nunc sed nibh commodo, hendrerit nunc at, malesuada nisl. Aliquam pretium leo sed iaculis vehicula. Vivamus interdum porta augue. + +Suspendisse potenti. Aenean sodales vehicula erat, vel sollicitudin eros eleifend id. Suspendisse hendrerit malesuada est, imperdiet tristique ex aliquet ac. Vivamus ultricies placerat lorem, ac placerat lectus sollicitudin ac. Etiam consequat odio vel bibendum pretium. Integer elementum nisl magna. Nam et vehicula risus, sed mollis tortor. Ut in molestie turpis. Nam luctus, elit molestie varius luctus, lacus ligula ullamcorper elit, non interdum ipsum neque non nibh. Curabitur vulputate facilisis suscipit. Sed vitae augue eu ex luctus lacinia ac vitae quam. Quisque non nibh ex. Praesent gravida efficitur dui, a vulputate nulla ultrices vitae. Duis libero dolor, facilisis in tempus vitae, pharetra non libero. Sed urna leo, placerat quis neque at, interdum placerat odio. Interdum et malesuada fames ac ante ipsum primis in faucibus. + +Vivamus in hendrerit elit, quis egestas sem. Sed tempus dolor finibus massa ultrices, sed tristique quam semper. Pellentesque euismod molestie facilisis. Vivamus sit amet ultrices elit. Ut eleifend, libero eu molestie maximus, ipsum elit pulvinar tortor, et aliquet nulla orci eu est. Nullam pretium, ligula at faucibus gravida, velit mauris pulvinar felis, sit amet tincidunt magna dui ut quam. Phasellus porttitor eu nunc id maximus. Suspendisse volutpat suscipit porta. + +Integer dictum augue purus, sed cursus eros blandit id. Vestibulum non nibh viverra, molestie lectus eu, vestibulum justo. Nullam a libero non nibh dignissim posuere id vel orci. Nulla viverra lorem eget condimentum scelerisque. Donec porta nunc sed sapien pretium dapibus. In hac habitasse platea dictumst. Suspendisse sit amet ligula elementum, accumsan ipsum vel, imperdiet massa. Fusce scelerisque non erat ac bibendum. Pellentesque consequat vehicula euismod. Morbi sapien nisl, ultrices ut scelerisque consectetur, ornare et orci. Maecenas efficitur eros a venenatis rutrum. Aenean bibendum dui in enim luctus posuere. Donec placerat porta eros eu dignissim. Fusce nulla velit, sodales eget nibh gravida, tincidunt venenatis urna. Aenean condimentum, massa in fringilla lobortis, turpis felis lacinia metus, sit amet facilisis nisl quam aliquet diam. Praesent fringilla vitae arcu nec lobortis. + +In tincidunt nisi ac dictum cursus. Sed dolor purus, posuere vel dolor vel, semper tempor elit. Integer nec scelerisque tellus, sit amet dictum magna. Donec hendrerit aliquet libero, a varius velit dapibus quis. Donec lectus turpis, egestas vel vestibulum vitae, iaculis quis eros. Maecenas id convallis metus. Duis quis tellus iaculis, lobortis massa vitae, condimentum eros. Pellentesque scelerisque nisl id hendrerit tempor. Donec quam augue, maximus eu porta ut, venenatis a ante. Curabitur dictum et nibh sed auctor. Nam et consequat odio. In vitae magna a dui porta pellentesque quis id magna. Sed eu nunc bibendum, imperdiet nunc sit amet, cursus diam. Vivamus in odio vel nibh sollicitudin molestie. Morbi sit amet leo et odio congue pharetra. Aliquam quis suscipit orci. + +Donec at ornare orci. Suspendisse nec tellus elit. Nam erat urna, laoreet at elit sed, tristique interdum ligula. Nullam dapibus orci sed lorem convallis fringilla. Cras urna ipsum, tristique maximus nisl quis, auctor mattis quam. Donec finibus consectetur lectus et interdum. Aenean posuere lectus vel turpis auctor finibus. Praesent molestie lectus ipsum, et tristique quam porttitor ac. Suspendisse aliquam pretium tortor, id egestas erat. Nulla mollis, orci at semper vulputate, nisl est pharetra diam, sit amet vulputate diam augue a sem. Donec in mauris felis. + +Nunc eleifend at elit a volutpat. Integer semper quis quam at faucibus. Donec pretium purus vitae erat pretium posuere. Sed ac aliquet nulla. Nam ut sagittis mauris. Sed quis sagittis risus, id pretium urna. Nam sagittis elementum ornare. Aenean ligula sapien, congue eget mi quis, viverra commodo nibh. Suspendisse non iaculis magna, nec volutpat neque. Donec eu dapibus eros. + +Nam sit amet iaculis massa. Nullam vel laoreet tellus, id cursus tortor. In hac habitasse platea dictumst. Curabitur in urna risus. Proin dui sem, interdum accumsan cursus ut, dignissim in purus. Nunc vitae consequat arcu. Proin volutpat elementum quam in posuere. Pellentesque luctus arcu in ornare tempor. Cras est turpis, viverra vel consectetur ac, dictum a quam. Donec sagittis tortor sed volutpat accumsan. Curabitur a tellus vitae arcu pretium viverra vel in ligula. Pellentesque turpis augue, porta vitae quam id, fermentum congue leo. Sed nec fermentum turpis. Nulla vehicula vulputate sem eu consequat. In tincidunt nisi et mi maximus, non facilisis justo porttitor. Proin eu sapien aliquam, accumsan nunc non, pulvinar leo. + +Nam vitae commodo sem. Ut laoreet in quam in suscipit. Nullam at faucibus diam. Fusce ut purus at ex lobortis elementum vitae quis sapien. Nam tempus consectetur ex, sed luctus felis. Donec porttitor mi dui, non luctus quam consectetur eu. Ut a egestas diam. Etiam sodales, nisl vitae gravida pulvinar, libero est condimentum tellus, vitae ullamcorper tortor justo a urna. Maecenas ac velit accumsan, laoreet leo eget, elementum libero. Maecenas dictum tincidunt blandit. Proin sed bibendum urna. Phasellus fermentum tincidunt tempor. Mauris iaculis, mi ac fermentum interdum, nibh odio pellentesque nunc, vel scelerisque sapien sem id purus. + +Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Maecenas imperdiet sit amet lectus at hendrerit. Vivamus luctus, elit non lacinia hendrerit, risus velit finibus nulla, quis sagittis ipsum diam ut odio. Sed mauris sapien, vehicula efficitur gravida sed, gravida in lectus. Fusce eget consectetur turpis, in fermentum neque. Nullam turpis turpis, feugiat a accumsan in, euismod a augue. Vestibulum nec neque libero. Phasellus eget efficitur turpis, fermentum molestie nunc. Sed consequat elit urna. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. + +Nunc egestas consequat blandit. Donec quis porta sapien. Sed augue nunc, efficitur vitae tellus sed, finibus bibendum sem. Fusce id sem quis quam pharetra ultrices. Phasellus non convallis velit. Quisque erat tellus, pretium eget porta in, ornare a arcu. Aenean nec lectus lorem. Suspendisse dolor lectus, ullamcorper ornare lorem a, consequat lobortis elit. Nunc dignissim est nec gravida facilisis. Proin faucibus erat vel eros volutpat, in vulputate neque sodales. + +Nunc vitae imperdiet ipsum, faucibus vehicula magna. Nulla nec nisl sapien. Curabitur et dui eget tortor efficitur accumsan. Aenean in pellentesque erat. Nam condimentum neque pulvinar, aliquam nisl eu, mollis lorem. Integer rutrum arcu eget felis semper euismod. Quisque vestibulum vel diam a tempor. Aenean mollis, tellus sit amet pretium pulvinar, nulla dolor ornare ligula, eget dignissim orci tortor ut sapien. Nam ut ipsum id lectus venenatis tempus vel non ex. Suspendisse blandit vitae nunc vitae porta. Suspendisse tincidunt est sit amet ultricies consectetur. Nulla fermentum hendrerit ex, vehicula rhoncus arcu lobortis ut. Vestibulum fermentum ornare diam at pellentesque. Praesent nunc lorem, porta et magna nec, sodales commodo justo. Duis aliquam sapien et rutrum tempus. Vestibulum malesuada felis eu ligula posuere luctus. + +Maecenas lacinia, lectus eget rhoncus aliquam, tortor est gravida sapien, vel aliquam arcu erat et magna. Praesent fringilla leo eget neque posuere imperdiet. In porttitor elit non enim gravida euismod. Aliquam tempus, orci at interdum dapibus, mauris lacus egestas sapien, ac ullamcorper ex nibh sed orci. Quisque iaculis enim et lectus egestas, malesuada posuere lectus interdum. Sed dignissim neque vel turpis dictum ornare. Vestibulum suscipit consequat maximus. + +Ut et ante sit amet leo rutrum volutpat. Sed malesuada quis sapien et ornare. Aliquam ac ex enim. Curabitur vel quam et orci posuere feugiat. Pellentesque nec metus eget sapien eleifend tincidunt non sit amet arcu. Cras posuere metus eget risus varius fermentum. Nullam orci eros, efficitur nec sapien nec, pretium laoreet erat. Nam gravida purus mauris, ac viverra orci hendrerit sed. Aenean ligula massa, posuere id faucibus vitae, malesuada quis augue. Morbi consectetur mattis mi, quis sodales diam bibendum eget. Aliquam sagittis neque at feugiat posuere. Donec gravida lectus a lectus fringilla tincidunt. Vivamus volutpat dui et turpis condimentum, ut tincidunt tortor lacinia. Ut laoreet, ante in pellentesque sodales, elit mauris scelerisque dui, quis tincidunt quam massa ac enim. Nulla porta porta sapien vel sollicitudin. + +Phasellus sed lectus posuere, mollis ante non, feugiat odio. Aenean a quam id mi ornare dapibus. Donec venenatis ipsum non velit accumsan, id elementum dolor imperdiet. Phasellus lacinia erat diam, sit amet convallis lectus ornare in. Curabitur lorem ligula, maximus eu varius quis, auctor quis odio. Etiam in orci porttitor, sodales ipsum at, rhoncus turpis. Nulla eget luctus nisi. Duis convallis est eget ligula fringilla viverra. Duis nec sapien quis dui luctus ornare sit amet quis erat. Quisque nec justo dui. + +Sed eleifend, tellus quis laoreet rhoncus, leo turpis imperdiet turpis, pretium varius odio tortor et leo. Morbi ut mi fringilla, porttitor sem ac, accumsan ante. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum cursus dolor accumsan dolor aliquet dignissim. Duis sed feugiat erat. Donec sed arcu accumsan, ornare nisl eget, lobortis nibh. Praesent pulvinar quis justo et hendrerit. + +Sed velit nibh, efficitur ut leo sed, eleifend iaculis nisl. Mauris ac diam euismod, luctus enim maximus, tincidunt sem. Ut tempus magna vitae blandit bibendum. Sed sapien tortor, ultrices et justo vel, pharetra faucibus dui. Vivamus et vestibulum arcu. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Mauris tincidunt suscipit risus, vitae varius felis commodo in. Nullam semper tortor dolor, sit amet pharetra odio interdum hendrerit. Pellentesque sed justo eros. Cras quam purus, eleifend id tellus molestie, eleifend finibus lorem. Donec a volutpat libero, at vehicula tellus. Vivamus tellus orci, pharetra in nisi vitae, tincidunt dapibus nunc. + +Suspendisse ultricies sem laoreet quam molestie ullamcorper. Sed auctor sodales metus, eget convallis eros ultrices quis. Duis rutrum mi a tortor iaculis posuere. Suspendisse potenti. Pellentesque dictum faucibus dui a vestibulum. Donec elit nisl, aliquet at dolor non, viverra dignissim felis. Donec mi elit, condimentum sit amet magna id, efficitur sodales arcu. Interdum et malesuada fames ac ante ipsum primis in faucibus. Pellentesque est elit, porttitor eget bibendum nec, auctor nec nulla. Pellentesque dignissim nisl vel orci commodo, ut ullamcorper justo viverra. + +Suspendisse pharetra gravida turpis sit amet efficitur. Nulla mattis tortor eget pharetra ultrices. Ut a turpis maximus, pharetra justo non, tempor quam. Nam et volutpat lectus. Vestibulum congue turpis augue, quis eleifend nulla euismod in. Vestibulum sed erat tempus, porttitor diam vel, elementum metus. Aenean facilisis molestie ante, sit amet ornare lacus mollis sed. Maecenas rutrum urna nec ex malesuada consequat. Nunc interdum pellentesque nisl sed viverra. Nam nec fermentum ipsum. Nulla facilisi. Maecenas congue dolor erat, a fermentum enim congue vitae. Integer metus libero, feugiat at eleifend eu, iaculis nec leo. Praesent eleifend convallis leo, eu posuere urna elementum eu. + +Aliquam dapibus dolor vel urna luctus venenatis. Suspendisse potenti. Donec vitae tellus nisi. Pellentesque vel neque dignissim, ornare tellus sed, sodales metus. Aliquam vitae maximus tortor. Nullam mattis, odio id porttitor euismod, est leo aliquet massa, bibendum pulvinar justo orci a magna. Morbi ut nisl congue, porttitor erat non, gravida turpis. In nulla risus, ullamcorper quis suscipit vel, tristique ut nulla. In malesuada odio augue, ac hendrerit nunc mattis a. Nam in quam ut elit ullamcorper mattis. Sed fringilla tempus felis, id pretium tortor varius non. Aenean quis sem quis risus mattis posuere vel quis nibh. + +Sed pulvinar commodo dui sit amet malesuada. Quisque porta tellus placerat congue efficitur. Cras id blandit enim. Praesent a urna felis. Aliquam ornare, nisl eget iaculis tempor, ligula mi vehicula dolor, ut eleifend massa massa vel est. Fusce venenatis laoreet lobortis. Proin tempus aliquet nunc ut porttitor. In est mauris, blandit eu mattis vitae, aliquam a orci. Cras vitae nulla nec ex cursus blandit. Aliquam fermentum, erat in aliquet dictum, metus urna fermentum quam, non varius magna lectus non urna. + +Donec faucibus nisl nunc. Integer rutrum dui enim, malesuada semper turpis pulvinar eget. Fusce consectetur ipsum tellus, sed maximus lorem auctor in. Morbi nec est in quam ultricies hendrerit. Sed aliquam sollicitudin elementum. Aenean aliquam ex sit amet dignissim venenatis. Duis malesuada leo nisi, id accumsan turpis pretium id. Sed ornare magna non pharetra pharetra. Ut auctor dolor neque, nec bibendum odio viverra in. Nulla convallis interdum condimentum. Proin vestibulum turpis in lorem ultrices, bibendum tristique ligula faucibus. In tincidunt auctor sem, vitae fringilla neque pulvinar eu. Etiam commodo pellentesque enim. Phasellus feugiat non sapien eget sollicitudin. Etiam consequat efficitur lacus vel maximus. Suspendisse vitae elementum diam. + +Mauris urna velit, efficitur at viverra at, interdum a velit. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Aenean bibendum sagittis massa in interdum. Mauris facilisis dui eget ipsum euismod ultrices. Donec sit amet lorem iaculis, condimentum velit quis, lacinia justo. Integer faucibus metus sed eros semper, in congue tortor venenatis. Proin tincidunt maximus enim, accumsan facilisis tellus gravida eu. Phasellus interdum aliquam ante, sit amet rhoncus nisl ornare at. Suspendisse libero eros, cursus quis fermentum nec, tempor eget lectus. Aenean ullamcorper augue lacus, non iaculis dui rutrum id. Aliquam erat volutpat. + +Morbi hendrerit fermentum sodales. Proin rutrum congue auctor. Mauris pellentesque elit non velit condimentum tincidunt a sit amet velit. Etiam accumsan ante id neque commodo vulputate. Aenean nec mattis neque. Aliquam tempor urna quis nisl convallis congue. Praesent vitae porttitor ante. Mauris mattis vestibulum ante, nec auctor augue. Praesent sem leo, accumsan eu fermentum egestas, iaculis ac nulla. + +Etiam vel nisi congue, varius neque id, volutpat quam. Fusce placerat arcu hendrerit orci condimentum vehicula. Integer sem ex, facilisis et lacinia a, condimentum at ante. Morbi condimentum diam tellus, non lobortis arcu pellentesque sit amet. Phasellus imperdiet augue eget sollicitudin mollis. Vivamus sollicitudin arcu aliquam lectus tristique, in consequat diam egestas. Suspendisse aliquet non velit id feugiat. Sed eu est fringilla, gravida nulla ac, dignissim tortor. Phasellus pellentesque nisl non venenatis sagittis. Etiam facilisis nulla quis lorem scelerisque vehicula id at ex. Duis in risus enim. In eu vulputate massa, vel dignissim odio. Praesent ut mi tellus. In hac habitasse platea dictumst. + +Nunc malesuada turpis in fermentum lobortis. Donec blandit eu orci non laoreet. Suspendisse posuere blandit tortor, in accumsan velit cursus in. Integer suscipit justo nulla, in viverra orci suscipit et. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum a pulvinar lacus. Vestibulum mattis nisi vel nunc convallis maximus. Fusce aliquam, erat in volutpat gravida, elit odio feugiat ante, nec varius enim leo eget nisl. Donec sit amet ligula lobortis, sollicitudin dolor quis, blandit augue. Duis at interdum sem. Nullam eleifend ligula urna, vitae sollicitudin purus cursus nec. + +In commodo risus eu justo hendrerit, ut posuere mi eleifend. Suspendisse sollicitudin odio sem. Vestibulum at dapibus dui, vel dictum nisi. In hac habitasse platea dictumst. Sed ac pharetra ex. Ut ultrices augue ut vulputate condimentum. Phasellus convallis arcu tortor, ac tincidunt justo cursus vitae. Mauris dignissim dapibus imperdiet. Nullam id quam eget mauris cursus molestie finibus eu enim. Fusce laoreet orci eu nunc fermentum tincidunt. Pellentesque vitae ex ac nunc porta mattis sed ut ligula. Phasellus erat dui, consequat et lacinia vel, blandit nec dui. Donec ipsum magna, rhoncus ac viverra vitae, feugiat vel ligula. + +Cras ut nisl sed elit dictum semper a at magna. Aliquam laoreet viverra velit vel lobortis. Nam tempor lorem sit amet purus tincidunt accumsan. Vestibulum et vestibulum ligula. Donec sit amet neque faucibus leo rutrum semper. Maecenas scelerisque, lacus et lobortis congue, purus quam euismod risus, et mattis orci nisl eget est. In hac habitasse platea dictumst. Pellentesque eros velit, sollicitudin quis ante vel, blandit maximus mi. Suspendisse at eros id quam vulputate ornare. Duis placerat tellus vel odio ultrices, ac feugiat enim placerat. Vestibulum ut massa mattis, commodo orci euismod, cursus lacus. Suspendisse potenti. Sed venenatis cursus neque, at lacinia erat ornare et. Sed rutrum, dui at porttitor hendrerit, lacus magna fringilla quam, id mollis elit leo ut ante. Praesent vel diam sed urna mollis laoreet eget ut risus. + +Mauris varius odio lectus, sit amet consequat nulla sollicitudin sed. Suspendisse commodo rhoncus enim vitae pellentesque. Aliquam vulputate sollicitudin aliquet. Mauris interdum interdum orci, non varius sem ornare ut. Sed vel nibh nunc. Duis tincidunt quam quis lectus egestas, eu ultricies ante posuere. Aenean in mauris eros. Suspendisse potenti. Vivamus sit amet augue at velit accumsan fermentum. Phasellus vel dui sit amet felis convallis sodales. + +Nunc mattis vitae sapien ut dignissim. Nam fermentum sit amet massa eu accumsan. In ut ipsum sit amet ante pellentesque accumsan. Nulla egestas eros eget lacus rhoncus pharetra. Nam pellentesque laoreet ex in lobortis. Vivamus congue tincidunt molestie. Integer vel turpis augue. Cras vel molestie quam. Etiam vel mauris ut tellus finibus consequat. + +Curabitur velit erat, vestibulum blandit massa a, aliquam elementum tellus. Pellentesque id nisl tempor lorem condimentum dapibus. Quisque ut lorem at orci elementum dapibus quis ut enim. Praesent venenatis congue arcu eu sagittis. Nunc nunc massa, posuere ac gravida id, scelerisque nec velit. Suspendisse non lacus a orci dapibus congue. Sed leo velit, facilisis sed egestas ut, vehicula at turpis. Praesent a finibus felis. Quisque est mi, pellentesque sit amet varius eu, gravida id est. + +Donec auctor gravida urna ac suscipit. Etiam placerat ipsum vitae ante congue, at fringilla lectus ullamcorper. Donec viverra lacus ut erat posuere, dignissim porta purus tempor. Duis eget enim quis diam vehicula pulvinar. Donec tempus eros velit, sit amet pharetra ligula placerat in. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas sit amet pretium tellus, non dictum ligula. Nullam a ex purus. Vestibulum id ex rhoncus, pretium eros vel, consequat tellus. Vivamus lacinia dolor nec ipsum aliquam, euismod fermentum sem efficitur. Nulla facilisi. Pellentesque pharetra lacinia augue, et eleifend lorem aliquet eu. Ut ut porta ante. Duis ut auctor nisi, id porttitor erat. Proin sollicitudin sem quis justo sodales aliquam. Nulla pharetra gravida arcu vel tempus. + +Maecenas nec imperdiet nulla, vitae fringilla diam. Mauris maximus aliquet tellus at congue. Aliquam in dictum elit. Sed pretium mattis lectus, non pellentesque enim dignissim vel. Sed quis elementum diam, a porttitor enim. Cras cursus eros nisl. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus ornare sapien vel diam vestibulum, at commodo metus lobortis. Sed euismod iaculis scelerisque. Pellentesque venenatis malesuada dolor, at tempus eros venenatis sit amet. Fusce a massa non libero blandit suscipit. + +Nulla facilisi. Sed nec leo nisl. Proin condimentum nunc at risus semper, in laoreet dui congue. Integer in dolor vitae ex maximus porttitor. In efficitur vulputate metus, ac egestas diam pharetra ac. Sed tristique tempus ligula dignissim convallis. Ut tincidunt laoreet fringilla. Praesent nunc est, lacinia tristique odio in, varius rhoncus ipsum. Vivamus bibendum justo at metus ullamcorper imperdiet. Quisque elit nibh, rhoncus a placerat eget, vehicula quis ante. + +Morbi fermentum turpis enim, eu interdum dui interdum sit amet. Sed vulputate lacus nec ligula gravida euismod. Duis in nisl fringilla, sollicitudin diam ac, laoreet tortor. Sed eget porttitor augue. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Sed eu enim convallis augue vulputate convallis. Praesent laoreet, leo at ornare luctus, nisi felis semper sapien, sit amet sodales augue mauris eu lorem. Fusce ornare volutpat malesuada. Curabitur vehicula, eros id fringilla placerat, tellus elit venenatis lorem, ac imperdiet purus ex blandit felis. Nunc suscipit elementum risus ac dictum. Aliquam bibendum dignissim ipsum, sit amet posuere sem viverra ut. Vestibulum egestas in ex ac volutpat. + +Donec ut faucibus velit, nec suscipit metus. Nullam dui ligula, commodo eget est venenatis, ullamcorper porttitor lectus. Suspendisse dictum, metus vitae commodo ultricies, enim quam pretium enim, a efficitur tortor purus sed ipsum. Nam sit amet lacus vel elit consectetur ultrices. Vestibulum ac nibh in metus porttitor vestibulum. In blandit et est non efficitur. Aenean vestibulum tortor sit amet mattis semper. Praesent sit amet turpis ac neque malesuada tincidunt nec nec urna. Mauris posuere elit nisl, ac ullamcorper nisi pulvinar in. Nulla ac elit in neque ullamcorper facilisis sed et dui. + +Quisque molestie euismod dui, non scelerisque arcu dictum a. Donec eu nulla nisl. Aliquam neque orci, ultrices in nunc vitae, sollicitudin eleifend augue. Etiam at mattis velit, a efficitur dui. Nam bibendum enim non elit tristique aliquet. Vestibulum eget nibh lacus. Pellentesque id sapien dui. Nullam urna leo, faucibus et efficitur vel, ullamcorper iaculis mi. Donec sit amet bibendum justo, id dapibus sem. Pellentesque dignissim varius tellus nec egestas. Praesent eu orci vel ipsum pharetra maximus. Cras nisl ligula, sollicitudin in ligula vel, posuere sagittis eros. Phasellus porta tristique mauris vel eleifend. Mauris sit amet volutpat ipsum, sed vestibulum orci. + +Ut rutrum augue quis rhoncus tincidunt. Aenean sollicitudin lacus at erat varius ullamcorper. Integer vitae orci vel nisi vulputate cursus eget nec ex. Mauris elementum, augue ac accumsan convallis, libero leo porta ante, sit amet elementum felis ligula non enim. Ut venenatis semper posuere. Vestibulum nec nisl nisi. Ut a sapien ac orci finibus dictum sed at ex. Phasellus ac lorem nisl. Quisque vitae tempor lacus. Vestibulum in mauris diam. Proin mattis ligula vitae ipsum bibendum, at dapibus nunc placerat. Nam iaculis justo in accumsan tristique. + +Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Mauris pretium velit et tristique pellentesque. Nunc in sapien a purus congue rutrum. Nam placerat risus in ante rutrum rutrum. In in ligula magna. Duis orci ante, vehicula elementum consectetur vitae, lobortis vitae arcu. Ut feugiat tempus metus quis sollicitudin. Etiam cursus venenatis augue at fermentum. + +Vestibulum id vehicula massa. Proin ut ligula et sapien placerat tristique non nec nibh. Etiam non lacus placerat, molestie ex eu, venenatis elit. Sed posuere, ante et ullamcorper luctus, elit lectus blandit tortor, nec consequat massa elit a tortor. Fusce urna felis, porttitor at ultrices vel, eleifend porta ipsum. Pellentesque nisl nibh, molestie sit amet sem eu, dapibus pharetra nulla. Phasellus viverra augue eu augue volutpat dignissim. Integer bibendum faucibus varius. Curabitur non turpis purus. + +Sed pretium eros nisl, sed ullamcorper magna faucibus quis. Etiam ornare euismod tellus, vitae accumsan eros bibendum ut. Morbi a ex sed risus faucibus rutrum et ut urna. Nullam non tortor commodo, facilisis massa non, tristique metus. Curabitur placerat, arcu in egestas ullamcorper, nisl nisl luctus felis, ac dapibus erat velit sed erat. Proin sagittis felis vitae sem posuere semper. Vivamus fermentum eu nisi ac facilisis. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque pellentesque lacinia ex tempus lacinia. Aliquam erat volutpat. Nam aliquet mattis risus non pretium. Suspendisse potenti. + +Suspendisse et sapien ornare, elementum erat vel, ultrices nisi. Curabitur interdum dignissim tincidunt. Cras eget est a velit consectetur finibus et sed nisl. Curabitur vestibulum semper posuere. Aliquam porta diam commodo nulla tempus imperdiet. Sed id dictum neque. Ut sit amet risus aliquet, dignissim velit sed, tristique ipsum. Nam a sapien id magna commodo tincidunt quis ac tellus. Nunc nec pellentesque orci. In justo purus, vulputate quis urna a, fringilla blandit sem. Cras porttitor quam vitae metus vehicula faucibus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Ut eleifend orci lectus, vitae semper eros hendrerit id. Fusce nec tellus condimentum, vehicula elit quis, gravida erat. Maecenas volutpat interdum velit id vestibulum. + +Pellentesque id metus metus. Nullam ac metus non nisi facilisis aliquet quis a sem. Morbi cursus consectetur aliquam. Morbi id sapien nibh. Etiam tempus tempor tempor. Nam scelerisque condimentum purus. Proin nec cursus eros, in condimentum dolor. Nam in sagittis urna. Ut eget ornare erat. Vivamus nec elit ut sapien tristique aliquet a nec urna. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Maecenas varius pellentesque diam. + +Ut eget libero iaculis urna porta iaculis vel ac eros. Sed sed mi et libero porta consequat a in libero. Sed in imperdiet ipsum, sit amet ultricies dui. Curabitur sit amet consectetur eros. Praesent nunc tellus, feugiat ac laoreet consectetur, accumsan nec magna. Praesent at elementum orci. Donec dapibus venenatis libero, id tincidunt libero euismod ac. + +Aenean sit amet ex placerat, molestie sapien a, volutpat turpis. Vivamus elementum lacinia nisi a convallis. Donec a sagittis turpis. Curabitur pulvinar laoreet dolor id consequat. Aliquam aliquam feugiat magna, vitae sagittis lorem mattis ut. Donec sed auctor nulla. Ut convallis, neque vitae faucibus efficitur, nisi justo pharetra odio, vitae tristique purus lorem et nisi. Quisque eleifend aliquam arcu nec maximus. Nunc mauris elit, finibus nec gravida non, maximus nec nibh. Sed eu ipsum in arcu gravida maximus. Maecenas massa dolor, ullamcorper eget odio eu, congue ornare leo. Suspendisse venenatis ultricies imperdiet. Nam finibus orci vitae sagittis finibus. Pellentesque sit amet dapibus diam. Nam eu nunc elit. + +Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Etiam euismod turpis nec urna lobortis, ut malesuada ex suscipit. Fusce sed viverra risus. Pellentesque tristique nulla pulvinar tincidunt accumsan. Nullam vitae nibh imperdiet erat rutrum pellentesque et sit amet quam. Aenean laoreet ultricies hendrerit. Duis elementum tellus a feugiat vulputate. Aliquam aliquam enim placerat dolor viverra, non suscipit lorem ultrices. + +Proin interdum vitae lorem quis viverra. Praesent nec tempor dolor, vitae sagittis turpis. Etiam sit amet imperdiet sapien, ac laoreet arcu. Proin aliquam, risus nec maximus dapibus, dui diam elementum dolor, vitae tempor augue lorem vel justo. Aenean ac egestas dolor. Ut aliquet, felis at fringilla venenatis, leo nisl ullamcorper ex, vitae pellentesque turpis orci quis lectus. Nullam vitae suscipit metus. Integer congue, ante in lacinia rhoncus, erat lorem interdum elit, egestas suscipit lectus nunc non orci. Nunc vel viverra quam. Mauris sit amet sodales tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam quis sollicitudin libero. In nec venenatis orci. + +Integer non quam fringilla, tristique nulla id, gravida arcu. Aenean scelerisque lacinia magna. Praesent nunc sem, lobortis non convallis rhoncus, rutrum vitae ante. Sed et orci ut erat viverra bibendum a et lectus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Fusce sit amet eros eget dolor pharetra vestibulum. Nullam vestibulum, massa dictum finibus egestas, orci nulla pulvinar sem, nec tempor libero lacus eu ante. Nam lacus erat, gravida eu placerat sit amet, facilisis sed urna. Suspendisse efficitur felis non ornare dictum. Nam sit amet nulla enim. Nulla eget scelerisque est. Suspendisse lacinia velit arcu, id lobortis felis facilisis in. + +Suspendisse potenti. Nulla porttitor metus ut nunc dictum tristique. Cras sit amet tortor eget ligula tristique efficitur. Ut at nisi id purus imperdiet laoreet. Sed sit amet malesuada urna. In nunc dolor, luctus ac condimentum ut, dapibus vel metus. Suspendisse pretium urna eget libero convallis vestibulum. Integer ut mauris hendrerit ex posuere euismod sed sed odio. Nulla egestas libero sit amet magna venenatis faucibus. Pellentesque semper vestibulum elit, et pretium felis scelerisque non. Suspendisse aliquet leo arcu, sed dapibus ex semper non. Donec lacinia dictum dignissim. Maecenas ipsum nunc, aliquet eget consectetur sit amet, aliquet vitae odio. + +Proin consectetur blandit feugiat. Nulla ac dictum quam. Vivamus suscipit scelerisque ipsum, vitae consequat neque sagittis id. Donec eu augue hendrerit, varius ipsum at, cursus massa. Morbi id augue id ipsum porttitor mollis. Phasellus nec libero eu arcu finibus dapibus. Nullam nec pharetra ante. Aenean sit amet urna eget justo tempus pharetra ut nec mi. Pellentesque viverra, ligula nec elementum ornare, ante elit eleifend enim, nec dignissim ligula elit in nibh. Vestibulum facilisis, felis eu condimentum dapibus, ante risus tincidunt urna, ut posuere dui augue ut ante. + +Nam arcu lacus, accumsan ac dolor in, egestas euismod est. Sed consectetur mauris et enim tincidunt semper. Donec sit amet pellentesque diam. Fusce viverra arcu a placerat fermentum. Nullam euismod dui eget egestas ultrices. Duis euismod viverra tortor eget eleifend. Pellentesque vitae neque dapibus, bibendum mi eu, auctor velit. + +Pellentesque congue consectetur turpis, eget auctor dui suscipit vel. Aliquam a sollicitudin turpis. Praesent nec blandit tortor. Suspendisse non nunc at urna tincidunt elementum. Integer eu elit nec urna maximus blandit quis at tortor. Nulla laoreet elit a purus tempus, et tincidunt lorem sagittis. Aenean semper erat eu neque hendrerit ornare. Cras posuere lorem nec orci vulputate finibus. Fusce tempor ex ac lacus gravida venenatis. + +Phasellus laoreet, libero vel fermentum euismod, sem diam accumsan quam, vitae gravida arcu est at sem. In bibendum, felis at viverra congue, felis nunc fringilla libero, ut scelerisque erat massa ut neque. Suspendisse potenti. Cras faucibus dolor vel tortor blandit, quis imperdiet magna semper. Nullam ut urna dapibus, vulputate est a, hendrerit purus. Vivamus vestibulum nisi a tempus placerat. Morbi vitae ultricies lacus. + +Nunc vel efficitur urna. Fusce bibendum suscipit mauris, quis imperdiet nisl bibendum non. Nam sed nisi imperdiet, pellentesque sem sed, pellentesque diam. Praesent luctus feugiat odio, eget fermentum lectus ullamcorper non. Phasellus pulvinar lectus sed ligula semper lobortis. Pellentesque fermentum ultricies fermentum. Quisque id turpis vel orci rutrum rhoncus id vel metus. Vivamus ex leo, accumsan eu luctus sit amet, tincidunt vitae erat. Sed eu mollis odio, ut ultricies lacus. Duis enim odio, pellentesque non velit vel, aliquam blandit erat. Mauris feugiat felis fermentum purus blandit, id rhoncus lorem tempus. Integer cursus, nunc vitae laoreet commodo, nunc lacus venenatis orci, quis eleifend risus est nec quam. Nulla tincidunt nunc ac purus tincidunt, eu molestie ipsum sollicitudin. + +Vestibulum ac varius velit, non suscipit nulla. Vestibulum a sagittis tortor. Suspendisse vestibulum felis quam, eget blandit nulla dictum vel. Praesent eu tellus ut lacus facilisis ultrices. Etiam fringilla nulla nec leo viverra faucibus. Sed nec sollicitudin nisl. Aenean libero massa, ornare sed elit ut, interdum fermentum arcu. + +Aliquam maximus diam et elit dapibus, eget condimentum sapien finibus. Etiam gravida nunc dapibus facilisis feugiat. Sed quis massa ligula. Nunc eleifend dolor lacus, at dapibus nisi vulputate eget. Etiam vel euismod urna, quis molestie nibh. Vestibulum bibendum erat non dictum sollicitudin. Suspendisse sed placerat lacus. Cras sollicitudin quam justo, a condimentum urna feugiat a. Quisque mauris libero, ultricies at ligula a, facilisis euismod ante. + +Morbi hendrerit maximus sapien ut auctor. Nullam nisi erat, aliquam ac metus eu, fermentum laoreet augue. Nam ultricies mi at vulputate laoreet. In ipsum est, blandit sit amet lorem id, egestas aliquam odio. Praesent venenatis lobortis mi. Aenean eu lacus consequat, mattis enim et, pellentesque leo. Duis convallis facilisis placerat. Donec porta, enim eget placerat congue, nisi augue faucibus odio, et fringilla purus elit vitae felis. + +Nulla et tellus a libero pellentesque eleifend vitae mattis nisi. Nunc placerat neque et sapien lobortis, aliquet pharetra nunc vulputate. Suspendisse potenti. Etiam ullamcorper lacinia varius. Suspendisse sit amet malesuada magna. Nam molestie mi nec diam tempus laoreet. Suspendisse urna tellus, scelerisque vitae purus et, dapibus fermentum felis. + +Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec scelerisque eget neque sed hendrerit. Donec at ligula augue. Donec vulputate massa nunc, a feugiat nunc elementum ut. Donec pulvinar libero sit amet ante faucibus sagittis. Mauris quis diam ultrices, tristique sapien nec, tempus urna. Morbi hendrerit odio sit amet leo lacinia, non egestas diam condimentum. Suspendisse efficitur sapien eget elit euismod tristique. Duis posuere vestibulum risus id sodales. Ut eget mi in urna rutrum molestie non nec dolor. Curabitur sollicitudin pharetra lacus, in sodales tortor tempor vitae. Nullam rhoncus arcu vel euismod varius. + +Aenean malesuada, purus quis volutpat sollicitudin, lectus risus lacinia mi, a facilisis ante lacus a dolor. Aenean vel aliquam tortor. Vivamus ornare, tellus at malesuada suscipit, quam dolor egestas erat, id convallis enim augue a enim. Aenean ultricies sodales placerat. Vivamus vel erat ac mi vulputate commodo nec eget urna. Suspendisse potenti. Mauris quis dapibus est, id tristique libero. + +Suspendisse ex diam, imperdiet quis dui id, interdum sodales elit. Maecenas at nunc ligula. Etiam imperdiet convallis magna sit amet tristique. Proin hendrerit laoreet magna, eget malesuada elit rhoncus et. Curabitur eget odio imperdiet, molestie sapien pretium, efficitur turpis. Aliquam sed congue arcu. Pellentesque vitae mauris id neque pulvinar tempor. Donec lobortis tellus id gravida dictum. Nulla et varius ex. Vestibulum pharetra eu quam dapibus finibus. Nullam accumsan sagittis turpis. Mauris fermentum orci et tortor tempus, ac tristique odio sollicitudin. Duis nec elit et magna imperdiet molestie eget vel ante. + +Suspendisse tempus augue nec massa molestie aliquam. Pellentesque vestibulum, erat sit amet fringilla fermentum, sapien lorem tempor felis, at efficitur augue erat quis nisi. Proin nec felis sagittis, sollicitudin turpis eu, fringilla leo. Vestibulum at augue nec dui vestibulum aliquam a at metus. Duis ullamcorper eleifend bibendum. Maecenas viverra mauris lacus, et consequat nisl pharetra eu. Praesent rutrum diam eu mi aliquet, non pellentesque est suscipit. Vestibulum massa leo, blandit eget mi at, cursus faucibus nunc. Praesent nec bibendum libero, vitae cursus libero. Sed consequat vitae eros nec maximus. + +Pellentesque et tortor eget lectus feugiat venenatis. Integer ornare nisl quam, nec imperdiet justo finibus at. Morbi malesuada, ante sit amet rutrum tempor, risus lorem tristique urna, egestas varius purus ex ut sem. Etiam sit amet feugiat sapien. Etiam quis risus commodo, eleifend velit quis, tincidunt enim. Mauris fermentum lacinia velit quis efficitur. Nunc sit amet lacus sit amet urna viverra feugiat. Nullam sagittis rhoncus suscipit. Aliquam eu sem ligula. Sed sodales risus et tortor lacinia tristique. Nulla massa augue, malesuada sit amet euismod ac, euismod placerat nulla. Sed lobortis ex nec lacus facilisis, nec semper mauris ultrices. Quisque ornare non felis vitae pellentesque. Donec mattis ut magna sed imperdiet. Integer viverra tempus feugiat. + +Donec laoreet aliquam imperdiet. Fusce justo neque, dictum vitae fringilla vitae, euismod sed augue. Fusce sodales, lectus a congue bibendum, ante eros pharetra tortor, eu sollicitudin libero ipsum sit amet felis. Curabitur sed condimentum quam, in iaculis ante. Ut feugiat dictum odio, vitae hendrerit lacus volutpat sed. Sed scelerisque et metus nec gravida. Mauris mattis porttitor magna, ut maximus justo sagittis vitae. Donec at metus ut leo gravida vehicula in sed diam. Vestibulum dignissim suscipit sagittis. Nam varius et arcu sit amet lacinia. Sed eget elit rhoncus, elementum dolor a, vehicula orci. Nam vitae tellus sapien. Phasellus massa lorem, commodo quis placerat in, semper faucibus tortor. + +Vivamus id dolor mi. Curabitur sagittis posuere quam in mollis. Phasellus finibus ipsum vel elit tincidunt, a bibendum ligula vulputate. Suspendisse quis purus nisl. Integer mi sem, posuere quis nisi a, consequat consequat magna. Vestibulum bibendum mauris in risus auctor fringilla vel at lacus. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Vivamus venenatis mauris at mattis eleifend. Donec tempus ipsum ac nisl convallis ultrices. + +Nullam eu nibh ut erat vehicula accumsan. Vivamus vitae faucibus libero. In in luctus odio. Nulla nec enim pharetra, fermentum erat in, tempor turpis. Sed ac magna suscipit, dignissim elit id, faucibus libero. Maecenas placerat in dolor et faucibus. Sed maximus purus dolor, non egestas massa rutrum non. Nunc ut rhoncus lacus. Sed sit amet dolor eu sapien sagittis molestie. Aenean ipsum erat, rutrum a diam et, varius porttitor ligula. Phasellus fermentum fermentum felis. + +Phasellus odio massa, lacinia ac hendrerit vestibulum, placerat sit amet erat. Praesent eget justo scelerisque, congue est in, pharetra mi. Etiam blandit turpis dui, a faucibus ipsum viverra vitae. Praesent sed scelerisque arcu. Cras nec venenatis ipsum. In et tempus metus. Pellentesque cursus quis nibh quis porttitor. Duis leo lacus, pretium eget rutrum eu, tincidunt lobortis tellus. Cras in erat tristique arcu faucibus luctus sit amet tempus erat. Nullam placerat, est a interdum iaculis, purus justo convallis arcu, at mattis erat sem tempor velit. Curabitur sapien sapien, tempor eget sodales vitae, blandit ac libero. Proin eget sem et arcu malesuada convallis non a est. + +Ut id sagittis urna. Morbi est mauris, molestie quis magna ut, convallis porta justo. Etiam in vulputate sem. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nullam consectetur enim nisl, sit amet pulvinar turpis elementum at. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum convallis cursus libero vel feugiat. + +Curabitur vel scelerisque metus. Etiam aliquet faucibus quam, vel aliquet est mattis quis. Pellentesque pulvinar sodales augue, a dignissim sem tristique vitae. Pellentesque dolor quam, pharetra vitae rhoncus ut, blandit at elit. Sed dignissim diam turpis, ac condimentum justo iaculis vel. Donec facilisis eros sit amet est dapibus, vel pellentesque eros consequat. Integer euismod mollis metus, vel rutrum risus imperdiet ac. Fusce semper dolor sit amet mi congue accumsan. Maecenas sagittis magna justo, vel varius ante scelerisque id. Fusce at urna sapien. Aliquam dui sapien, facilisis eu magna laoreet, feugiat tincidunt elit. Fusce iaculis sit amet erat id dignissim. Suspendisse sollicitudin laoreet consequat. Pellentesque eu mollis quam, vel dapibus libero. Duis mattis tortor vitae orci vehicula, et viverra ipsum lobortis. Quisque iaculis sapien est, id sagittis tortor rutrum at. + +Duis ut condimentum risus, et venenatis nulla. Praesent urna ex, faucibus eu mollis nec, lobortis vitae neque. Vivamus a malesuada tellus. Quisque bibendum dolor nec massa porta, nec malesuada magna auctor. Phasellus non auctor turpis, et bibendum risus. Ut pellentesque justo non neque convallis, ut imperdiet diam sagittis. Nunc arcu nisi, rutrum sagittis neque elementum, finibus consequat felis. Proin euismod elit eu urna tristique ultrices. + +Curabitur id hendrerit ipsum. Aliquam tortor nisi, dignissim vel sodales a, ultricies a ipsum. Etiam commodo arcu sed volutpat varius. Proin vehicula lacinia tempor. Vestibulum feugiat nibh eu ante tempor efficitur. Etiam eleifend a orci eu euismod. Cras a tortor risus. Cras nec urna non urna malesuada maximus vel et ipsum. In ullamcorper porttitor dolor, at fringilla tortor tristique ac. Nulla gravida tortor nec dolor convallis, accumsan sollicitudin dui luctus. Vestibulum euismod vestibulum semper. Nam semper lacus dolor, vitae rhoncus nisi blandit nec. Phasellus turpis dolor, posuere sed sodales sit amet, interdum ut arcu. + +Sed blandit nulla et sem vulputate, et semper lacus posuere. Proin velit lorem, sagittis tincidunt aliquet vitae, fermentum sed orci. Ut interdum ultrices dolor eu tempor. Quisque pretium vulputate dui at blandit. Aenean pretium urna sed purus dapibus aliquam. Mauris tristique augue pretium magna scelerisque, vel cursus sapien porttitor. Nullam neque justo, aliquam non neque id, lobortis facilisis nibh. Duis molestie dui eu augue tristique porttitor. Sed posuere justo massa, efficitur vulputate erat posuere non. Cras quis condimentum tellus. + +Etiam eleifend ipsum ut justo viverra porttitor. Nam bibendum enim nec ligula vestibulum, vel fringilla velit posuere. Praesent leo enim, varius sit amet nulla ut, sodales sollicitudin nunc. Phasellus hendrerit varius ex quis tempus. Suspendisse maximus laoreet enim, ut bibendum tortor. Ut non magna vehicula augue condimentum scelerisque. Aenean efficitur elit vel rhoncus euismod. Vestibulum sit amet sollicitudin dui. Quisque ac diam nibh. Nullam quam enim, volutpat eu turpis et, posuere consectetur neque. + +Ut tincidunt semper dictum. Etiam varius enim sit amet metus consequat malesuada in at ligula. Cras nisl dui, tincidunt a urna et, porttitor rhoncus velit. Mauris interdum imperdiet lectus ac mollis. Aliquam sollicitudin ornare mi, a varius nulla faucibus aliquet. Nunc fermentum tempor ullamcorper. Pellentesque laoreet libero condimentum pellentesque pharetra. Nullam sed eros vel erat vestibulum dictum. Nulla nec interdum dui, quis finibus nunc. + +Sed ac turpis in mi ultricies finibus sit amet et diam. Pellentesque sagittis non ligula et convallis. Suspendisse interdum est aliquet erat rhoncus semper. Donec pretium turpis ante, vel posuere mauris facilisis vel. Vivamus nibh ligula, pharetra sit amet hendrerit nec, vulputate ut sapien. Nulla ullamcorper condimentum metus vitae imperdiet. Ut eget ex purus. Nulla dapibus malesuada pharetra. Praesent sit amet ornare turpis. Nulla pulvinar felis eget arcu mollis vulputate. Vestibulum eget semper felis. Donec viverra justo id mi tempor, a mollis ipsum porttitor. Maecenas aliquet volutpat ante eget tempor. Duis ipsum nisi, scelerisque quis metus vitae, blandit faucibus nisl. Mauris rutrum nisi sed lorem dictum ultricies. + +Nulla dictum arcu augue, aliquet ornare ligula suscipit ut. Integer libero erat, bibendum quis arcu at, consequat luctus sem. Ut ut erat tincidunt, porta erat efficitur, bibendum neque. Maecenas eget convallis sapien. Nullam facilisis ex et lorem fringilla, ut convallis leo dictum. Duis porttitor, ex eu venenatis faucibus, erat augue dapibus leo, sit amet scelerisque neque dui sed arcu. Praesent at diam nec sem sodales condimentum. Vivamus vehicula urna tellus, a semper ipsum cursus id. Duis auctor enim erat, laoreet volutpat dui rhoncus maximus. Suspendisse pellentesque euismod lacus, at auctor purus. Suspendisse volutpat imperdiet diam, id laoreet est egestas at. + +Fusce lobortis ex vel condimentum convallis. Vivamus fermentum convallis dolor quis rutrum. Donec tempus ornare maximus. Mauris quam neque, dignissim rutrum maximus sed, volutpat sed lectus. Donec id augue gravida, pretium purus eu, sagittis tellus. Maecenas tincidunt gravida felis nec pulvinar. Fusce turpis urna, sodales non rhoncus nec, sodales ac ipsum. Suspendisse risus felis, imperdiet id malesuada fermentum, ultricies et erat. Suspendisse potenti. Pellentesque tellus ipsum, dapibus eget pellentesque eleifend, venenatis sed risus. Ut sed mollis nisl. Aliquam lobortis aliquam ipsum, et ornare magna bibendum ut. Proin quis justo vitae neque tincidunt maximus ultricies et purus. Nullam non semper turpis. Quisque non mauris odio. + +Donec commodo tempus egestas. Vestibulum at diam vel mi tincidunt finibus. Donec aliquam magna id eros tristique finibus. Cras ut enim sapien. Proin gravida risus a nisi dapibus, ac vulputate nunc laoreet. Donec at leo vel nulla iaculis feugiat sed et ante. Nulla a arcu at ligula sollicitudin semper sed eget est. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Ut dolor magna, luctus id elit ut, interdum viverra lacus. Aenean sed tempor metus. Aenean arcu dui, eleifend quis est sed, luctus lacinia nulla. Morbi id luctus libero. Maecenas sodales leo eu sapien maximus porta. Praesent gravida augue eu ligula pretium cursus. + +Duis aliquam luctus tortor, eu facilisis quam sodales sed. Phasellus feugiat venenatis lorem, in scelerisque est efficitur quis. Cras quis nisl nisl. Quisque magna felis, tempus nec pharetra et, tempor id ligula. Pellentesque sed dignissim velit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris convallis iaculis posuere. Nullam orci velit, venenatis eget enim quis, molestie consequat dolor. Nulla egestas risus vestibulum sagittis blandit. Nullam dui lacus, tempus euismod sapien a, eleifend sodales justo. In id tortor neque. Vivamus faucibus ante ac odio vestibulum, vitae condimentum nibh consectetur. In rutrum hendrerit est, sit amet mollis ex aliquet tempor. + +Donec non tincidunt sem. Nullam dignissim felis nibh, et accumsan felis tristique sed. Maecenas id massa erat. Ut justo ligula, aliquet eu condimentum a, aliquet eget ex. Phasellus et neque eu nisl consectetur ullamcorper. Sed gravida efficitur nisi, vitae posuere nisi tincidunt sed. Duis vitae molestie metus, vitae finibus urna. Integer at lacus vitae turpis convallis feugiat a dignissim libero. In dolor nibh, aliquam eu malesuada in, tincidunt vitae nisi. Nullam at lectus risus. Integer vitae ligula interdum, congue nibh id, tincidunt dolor. + +Mauris risus quam, mollis eu consequat vitae, molestie sit amet risus. Vestibulum congue vulputate nibh sit amet ullamcorper. Nullam elit justo, hendrerit euismod elit nec, gravida tincidunt ipsum. Aenean tellus tortor, commodo vitae cursus vitae, finibus quis mi. Nam in tellus scelerisque, cursus magna a, elementum tellus. Praesent ut lacinia justo. Donec consectetur, mi nec faucibus viverra, nisl justo suscipit est, in consequat ex urna vitae orci. Fusce molestie feugiat ex sed dictum. Phasellus interdum eros dapibus sodales volutpat. Morbi elementum sapien ante, quis sagittis justo fermentum at. Maecenas vestibulum massa quis eleifend rhoncus. Praesent auctor ex at urna pellentesque facilisis. + +Duis tincidunt porta quam, in commodo orci dapibus interdum. Donec mattis erat ante, nec faucibus magna pharetra id. Aliquam dignissim ultricies auctor. Duis quis ex mi. Phasellus ipsum mi, volutpat quis augue nec, dapibus aliquet lectus. Aenean id eros mi. Fusce rhoncus iaculis magna, commodo commodo nibh pellentesque ut. Donec consectetur lacinia erat ut luctus. Mauris efficitur erat vitae sapien fringilla sollicitudin. Maecenas a egestas ipsum. Aenean placerat congue augue, ut condimentum lorem accumsan in. Morbi ac quam nunc. + +Nunc posuere faucibus semper. Integer at convallis ex. Duis a purus molestie, dignissim mi quis, consequat nulla. Proin tempus congue ligula, sit amet ultricies enim consequat ut. Nunc ac est at nisl euismod cursus. Pellentesque vulputate molestie ipsum. Praesent varius, lacus non lobortis blandit, nibh lacus scelerisque nisi, sit amet interdum ligula tellus ac purus. Vestibulum sed quam tempor, pharetra tellus ullamcorper, tincidunt est. Nulla tempus tortor nec erat tempus eleifend pellentesque eget purus. Duis gravida dui justo, ac commodo ipsum mollis et. Nullam vitae nibh enim. Ut sodales mi eu diam sagittis, quis luctus tortor euismod. + +Maecenas a augue ac augue varius rhoncus. Fusce nunc turpis, mattis eu iaculis a, dapibus nec purus. Pellentesque imperdiet sit amet purus a blandit. Morbi augue tellus, venenatis nec tortor in, consectetur tincidunt nulla. Aenean purus libero, volutpat quis neque vitae, mattis efficitur eros. Donec sodales venenatis augue, sed faucibus magna accumsan convallis. Pellentesque efficitur elementum imperdiet. Cras at condimentum leo. Curabitur feugiat ut nisi facilisis commodo. + +Sed commodo sapien quam, quis vulputate orci lobortis nec. Aenean luctus dictum ipsum, non consequat sapien molestie vel. Proin blandit libero tortor, vitae efficitur tortor hendrerit at. Sed eleifend eu velit eu tempor. Nunc auctor tincidunt mollis. Ut molestie, erat ut lobortis convallis, odio felis sollicitudin elit, vitae ultricies lorem neque et dui. Sed venenatis tempus ullamcorper. Integer eget elementum nulla. Maecenas a placerat ipsum. Etiam sagittis sagittis rhoncus. Aliquam faucibus magna id lacus pharetra, nec interdum lacus sodales. + +Mauris ipsum sem, venenatis non elit in, feugiat pretium lacus. Fusce eget tincidunt dolor. Nam pharetra lacus vitae diam bibendum, ac placerat tortor aliquet. Sed eget gravida orci. Ut in orci lorem. Morbi condimentum ligula dolor, in dictum mi vestibulum sed. Suspendisse eu velit finibus, tincidunt dolor malesuada, mattis est. Morbi iaculis sapien vitae metus facilisis fringilla. Donec sed volutpat neque. Mauris ipsum metus, venenatis cursus mattis quis, convallis ultricies purus. Aliquam quam magna, sagittis at mi quis, lacinia iaculis turpis. Praesent viverra, ex nec euismod lacinia, urna risus pretium odio, sit amet mattis metus eros non lectus. + +Nam sed vehicula est, ac aliquet mi. Aenean iaculis placerat orci, ut lobortis dui vestibulum convallis. Vestibulum eleifend ante sed lorem interdum lobortis ut vel tortor. Sed auctor id nisl sed bibendum. Fusce maximus vulputate mauris, tempus sollicitudin nunc efficitur vitae. Nulla pellentesque molestie leo, quis hendrerit enim. In vel dapibus nisi. Nam at dui quis eros porttitor volutpat in id magna. Maecenas quis quam a justo cursus aliquet viverra sit amet mauris. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Ut hendrerit est at augue pretium condimentum at ut velit. Suspendisse non gravida metus. + +Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Praesent lacinia commodo elit, sed fringilla ipsum imperdiet ut. Proin a dictum ante. Suspendisse potenti. Praesent ac nulla volutpat, ultricies diam vel, auctor risus. Nullam aliquam elit vel quam suscipit molestie vel ac dolor. Ut pharetra finibus elit, id vulputate ex dignissim in. Ut ac finibus urna, a luctus magna. + +Fusce suscipit convallis eros nec blandit. Cras quis lectus nibh. Donec pulvinar rhoncus pulvinar. Vivamus nulla nisi, vestibulum vel neque et, dictum gravida ex. Vestibulum in arcu vitae nibh auctor rhoncus ac in massa. Sed semper enim ac dui sollicitudin porttitor. Etiam ante erat, laoreet sed dolor eget, cursus commodo lorem. Curabitur maximus tincidunt nulla at molestie. Phasellus ultrices felis a cursus facilisis. Nullam a semper tortor. Nulla ac vulputate justo. Mauris maximus nisi quam, elementum eleifend nulla condimentum nec. Aenean congue tincidunt mi, id mollis orci blandit vitae. Integer placerat orci at nisl vestibulum lacinia. + +Sed egestas posuere egestas. Quisque pulvinar velit commodo felis accumsan, a consequat purus laoreet. Ut at arcu at augue sodales rhoncus. Ut non nisl dui. Sed sed nulla quis orci eleifend auctor ut et sem. Aliquam erat volutpat. Donec egestas tincidunt leo in interdum. Donec varius odio nulla, id porttitor erat venenatis ut. Nulla ac nisl ut enim placerat congue. Fusce consequat purus eget risus luctus finibus. Donec in blandit odio. Sed vestibulum nisl ut diam vestibulum volutpat. Donec magna quam, tempor eget velit quis, blandit tristique lacus. Pellentesque et orci dui. + +Integer tempor mollis purus ut volutpat. Pellentesque efficitur cursus neque in ullamcorper. Integer ac tincidunt lacus, at venenatis tellus. Curabitur convallis commodo enim a convallis. Aenean sagittis sodales nibh, sed eleifend diam ultricies at. Vestibulum erat mauris, lobortis ac tempor a, viverra non sem. Nulla non ipsum mollis, dignissim elit a, laoreet enim. + +In laoreet purus eget orci rhoncus viverra. Quisque vel metus quis nulla hendrerit viverra. In consectetur velit vitae purus vestibulum, in hendrerit odio sollicitudin. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla ut est sed ligula tincidunt fermentum nec ac nulla. Nam in sagittis velit. Vivamus vel dapibus erat, ullamcorper sollicitudin metus. + +Quisque pulvinar nulla eget mi mattis, ut convallis nulla ullamcorper. Vivamus placerat mauris vel elementum aliquet. Sed ac accumsan eros. Vivamus vitae rhoncus urna. Fusce gravida cursus varius. In posuere finibus leo cursus molestie. Interdum et malesuada fames ac ante ipsum primis in faucibus. Mauris vulputate mi ut nunc finibus convallis. Praesent luctus erat eu urna facilisis convallis. + +Nam efficitur tempus augue varius porttitor. Pellentesque scelerisque dolor scelerisque, dignissim massa eget, iaculis nibh. Praesent viverra molestie dui a pulvinar. Mauris malesuada mattis felis, vitae sagittis metus pellentesque viverra. Phasellus faucibus congue blandit. Pellentesque mattis, justo sed imperdiet rutrum, metus metus porta nisi, vel malesuada lorem massa at dolor. Nullam nisi eros, tristique quis mollis ac, hendrerit nec leo. Praesent viverra molestie vestibulum. Nullam eu aliquam risus, at dignissim diam. Quisque blandit fermentum erat, sed ullamcorper augue pellentesque in. Integer eleifend libero non lacus sagittis auctor. Aliquam volutpat interdum accumsan. + +Etiam in eros porta, bibendum lacus eget, feugiat orci. Integer massa dui, commodo ac luctus nec, ornare id velit. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Duis dignissim scelerisque ex. Aliquam tempus cursus nibh, sed scelerisque libero sagittis ut. Morbi finibus vestibulum nulla, nec aliquam urna venenatis vulputate. Pellentesque ultricies, lorem a dignissim bibendum, augue nisi eleifend libero, cursus ultrices nulla purus a nunc. Quisque dictum pulvinar turpis vitae finibus. Etiam eget ultrices diam. Sed commodo enim ante, fermentum rhoncus tortor tincidunt ac. Suspendisse fermentum aliquet erat non posuere. Aenean vel dapibus lorem. + +Aenean lorem libero, rutrum vel egestas vel, pellentesque ut ante. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum felis diam, pellentesque sit amet ultricies sed, vestibulum in leo. Morbi aliquet, quam ut fringilla sollicitudin, justo risus suscipit mi, in vulputate neque erat ut turpis. Duis auctor dui non urna sagittis dictum. Vestibulum nec felis vel sapien congue volutpat a a orci. Phasellus tincidunt libero ac lorem feugiat, ac elementum lectus eleifend. Praesent sit amet est id massa convallis congue. Integer ac nunc eget orci pharetra vehicula. Mauris et ultricies diam. Nunc et pellentesque lacus, eget bibendum sapien. Morbi condimentum mi ac metus euismod convallis. Proin consequat rhoncus varius. Maecenas feugiat elementum vulputate. + +Vestibulum tempor feugiat dapibus. Ut ornare in augue non euismod. Nulla faucibus gravida condimentum. Curabitur pharetra dui non lorem sagittis iaculis. Quisque vitae nibh magna. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Fusce quis pharetra sem, sed aliquet lectus. + +Fusce volutpat tempor tincidunt. Pellentesque ultricies imperdiet tincidunt. Integer porta dictum lorem, non luctus tellus bibendum sit amet. Quisque posuere felis et est dapibus, commodo rutrum leo molestie. Fusce sodales felis sed sem pharetra pretium. Praesent nec sollicitudin nisl. Donec volutpat convallis elementum. Duis id libero augue. Nulla facilisi. + +Nulla viverra, urna nec efficitur vulputate, metus odio lacinia purus, in sagittis elit erat vel massa. Fusce ligula leo, finibus non felis dignissim, dictum ullamcorper odio. Phasellus vel eros eu sem venenatis sagittis a nec nulla. Pellentesque sapien elit, lobortis quis dictum et, maximus vitae metus. Curabitur quis aliquam eros. Quisque cursus condimentum urna vel efficitur. Etiam aliquet lorem ut varius suscipit. In viverra molestie felis vitae vehicula. Curabitur cursus, nunc sodales faucibus ullamcorper, urna magna dapibus velit, accumsan blandit mi quam nec justo. Donec ac dui lorem. Sed eu sagittis nulla. Suspendisse ut ante lacus. Fusce non tristique felis. Nulla convallis consectetur arcu, semper egestas urna efficitur quis. Nulla ac turpis at leo pretium maximus. Morbi cursus diam ac purus imperdiet, non commodo tellus cursus. + +Mauris ut eros suscipit, suscipit nisi vel, porttitor sapien. Morbi in nulla sapien. Cras maximus pretium justo, vel eleifend urna vulputate sed. Aenean egestas nisl ex. Curabitur sed pharetra ligula. Nulla blandit lorem id mauris tincidunt, at elementum risus aliquet. Quisque id interdum dui. + +Aenean nec elit condimentum ex viverra hendrerit. Pellentesque blandit nibh elit, a ultrices orci vestibulum vitae. Donec ultricies malesuada justo ac luctus. Pellentesque laoreet nunc a sem varius, ac dapibus ligula volutpat. Aenean sem felis, aliquam nec ullamcorper quis, ullamcorper non ante. Pellentesque libero leo, sagittis nec tempus a, tincidunt eget risus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec elementum massa vel diam suscipit suscipit. Suspendisse eget neque porta, cursus orci eget, imperdiet libero. + +Vivamus faucibus vulputate sapien, non pulvinar ex sodales vitae. Maecenas consequat est urna, id faucibus eros dictum at. Vestibulum tortor mi, porta vitae sagittis sed, convallis quis enim. Vestibulum gravida justo pulvinar sem ornare, efficitur dictum neque varius. Nullam egestas congue tellus vitae interdum. Nam lectus erat, porta vitae lorem non, mollis semper velit. Nulla vestibulum tincidunt elit. Phasellus sed vulputate leo. + +Sed efficitur quam ac iaculis volutpat. Praesent nec feugiat ex. Aenean at ligula iaculis, imperdiet lacus et, condimentum magna. Integer euismod leo id mauris condimentum, quis semper lacus blandit. Sed volutpat neque urna, sit amet ullamcorper est dignissim non. Vestibulum tristique sollicitudin risus, sed hendrerit massa finibus id. Vestibulum sit amet risus non eros vehicula malesuada. Nam facilisis lacus sed quam pulvinar, at fermentum lectus tincidunt. + +Vivamus laoreet sem nec odio blandit, ut ornare sem egestas. Suspendisse potenti. Nulla aliquam pretium volutpat. Maecenas a orci suscipit, aliquam eros a, maximus urna. Aliquam eu gravida justo, et hendrerit justo. Suspendisse a commodo lorem, quis viverra nisl. Nulla vel pellentesque nisl. Nulla ligula tellus, vehicula sit amet turpis in, sodales tincidunt tellus. Proin ut nibh sed ipsum porta dignissim vel sed mauris. Interdum et malesuada fames ac ante ipsum primis in faucibus. In hac habitasse platea dictumst. Vestibulum efficitur nulla rutrum, accumsan lacus at, dignissim tortor. Morbi egestas metus ut nibh tincidunt posuere at sed elit. Integer eget hendrerit nulla. + +Donec sagittis sagittis tempus. Proin iaculis neque vehicula commodo faucibus. Pellentesque erat ante, vehicula sed efficitur vitae, varius non sem. Mauris pellentesque, felis nec egestas scelerisque, nulla nunc fringilla arcu, feugiat fringilla quam neque in elit. Nullam ut ex odio. Mauris enim risus, consequat at suscipit at, pulvinar ut arcu. Fusce mollis sem sed tellus tempus pellentesque. In pharetra, libero sed tristique vestibulum, massa velit egestas risus, at mollis augue lorem sit amet turpis. Mauris risus dui, sagittis vitae congue ut, condimentum ut augue. Quisque non sollicitudin purus, sit amet consectetur sapien. + +Sed scelerisque ipsum sed augue varius, sit amet ultricies purus posuere. Etiam vehicula at nunc in posuere. Cras eget risus a sapien mollis facilisis. Morbi vel purus auctor, faucibus mauris non, malesuada leo. Donec venenatis consectetur libero in pretium. Ut efficitur molestie metus id scelerisque. Nunc dolor justo, pharetra vel sagittis vitae, placerat ut justo. Donec tempor fermentum est semper auctor. Nullam tincidunt risus non risus elementum varius. Suspendisse euismod augue metus, nec pellentesque enim sagittis ac. Morbi et lectus quis justo commodo varius commodo vel risus. In bibendum purus in erat ullamcorper, sit amet dignissim sapien scelerisque. Quisque tempus elementum rutrum. + +In viverra sollicitudin pretium. Sed finibus scelerisque sollicitudin. Nulla lobortis purus in lectus iaculis, a ornare ex accumsan. Morbi malesuada mollis placerat. In hac habitasse platea dictumst. Pellentesque sed dui quis odio scelerisque molestie eu nec libero. Proin viverra magna ligula, at luctus lacus posuere sed. Nullam non congue mi. Praesent non mattis neque, sit amet tempus diam. Mauris eget cursus lacus, id dictum mi. + +Sed semper velit in vehicula tristique. In lobortis est augue, et maximus tellus iaculis ut. Proin quis ex maximus erat iaculis imperdiet. Curabitur ultrices turpis ac nibh congue ornare. Fusce a aliquet felis, nec sodales tellus. Nunc mauris ante, feugiat et facilisis at, pharetra ultricies dui. Integer felis metus, porttitor ac ipsum vitae, placerat varius ex. Morbi in dui a nunc aliquam posuere. Nulla tempus tortor ac lorem consectetur sodales. Praesent sit amet placerat diam. Curabitur sodales mauris ante, et tincidunt lacus ultricies quis. Nulla eu finibus nisl, sit amet volutpat leo. Donec ligula enim, feugiat non aliquet nec, congue interdum lorem. Pellentesque a nisi blandit, semper massa sit amet, auctor eros. + +Aenean ligula libero, commodo a erat at, tristique facilisis quam. Sed congue fringilla lacus, vel iaculis quam suscipit ornare. Curabitur non pretium mi. Donec condimentum odio dui, id malesuada ipsum porta ac. Aliquam imperdiet cursus ullamcorper. Duis cursus tincidunt nibh sit amet cursus. Aliquam urna orci, sodales ac ipsum at, placerat lobortis neque. Sed sit amet convallis felis, vestibulum finibus arcu. Phasellus venenatis egestas felis, id aliquam turpis iaculis non. Proin a lacus ut felis malesuada sodales. Duis elementum, eros ac placerat bibendum, quam nisl varius mauris, vel venenatis neque augue et justo. Nunc varius sem velit, vel tempus mi aliquet id. Fusce purus massa, mollis id nulla non, dignissim cursus massa. Sed sollicitudin interdum felis, nec eleifend nisl feugiat eget. Nulla rutrum id odio ut consectetur. Maecenas fermentum turpis vitae libero ullamcorper suscipit. + +Vestibulum auctor lectus ligula, non sollicitudin odio maximus nec. Cras faucibus, felis sed suscipit tempor, risus lorem consectetur est, eget pulvinar nibh dui eu dolor. Vestibulum pellentesque, ex aliquam vulputate laoreet, libero lorem rhoncus risus, vitae congue velit justo vitae nisi. Nullam placerat venenatis tortor, sit amet lacinia augue placerat nec. Quisque vulputate lectus vel pulvinar condimentum. Nullam at libero iaculis, mattis purus vel, tincidunt diam. Suspendisse eu ullamcorper eros. Nulla id eros odio. Ut enim leo, ultricies quis mauris sed, interdum commodo felis. Etiam enim lacus, scelerisque a interdum eget, mollis vel tellus. Phasellus vel vulputate orci. Nulla ornare leo sed arcu rhoncus convallis. Duis libero arcu, pulvinar ut tellus vitae, aliquam euismod magna. Donec semper nisi eget nulla tempus, sed condimentum massa sollicitudin. + +Suspendisse bibendum ante vitae ullamcorper semper. Praesent dui est, elementum nec lacus in, pellentesque accumsan sapien. Cras quis urna at lacus posuere commodo eget nec arcu. Nunc varius ante quis ligula rhoncus, ut dignissim metus commodo. Integer volutpat gravida nunc eget faucibus. Quisque imperdiet, mauris vitae cursus malesuada, lacus mauris tempus sem, ut accumsan purus lectus quis nisi. Vestibulum aliquam dui sed nisl laoreet, id posuere quam imperdiet. Aliquam ultricies non enim vel tempor. Donec sed feugiat justo, vel rutrum enim. Phasellus lorem quam, dapibus a tincidunt vulputate, pellentesque non lorem. + +Nunc quam diam, condimentum sit amet enim semper, hendrerit laoreet magna. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nullam et pulvinar leo, ac fermentum lectus. Sed erat ante, mattis ac tempor vitae, euismod at tortor. Ut sagittis fringilla scelerisque. Ut pulvinar molestie leo eu faucibus. Sed quis efficitur purus. Pellentesque nibh nisi, porta id orci id, sagittis sodales tellus. Ut venenatis velit a lectus lacinia, at ultrices nisi commodo. Vivamus eros orci, ornare vel ullamcorper elementum, posuere id ligula. Aliquam non massa pellentesque, semper ipsum scelerisque, dapibus leo. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nulla pretium leo diam, sit amet bibendum lacus tempus quis. Interdum et malesuada fames ac ante ipsum primis in faucibus. Integer quis pulvinar nibh. + +Mauris sed eros nisi. Cras malesuada, turpis vitae laoreet porta, sapien odio maximus nulla, ut efficitur mauris odio tincidunt elit. Nam ut urna eros. Sed at vestibulum risus. Nulla luctus pulvinar rhoncus. Pellentesque maximus ligula a est ullamcorper, sed tempus tortor ultrices. Curabitur ligula odio, mollis sit amet risus quis, tempor auctor magna. Suspendisse vel quam quis lorem commodo facilisis. Donec eu ipsum suscipit, molestie dui sed, fringilla dui. Proin placerat ex a lorem sodales convallis. Sed molestie quam mi. + +Fusce laoreet nec dolor id bibendum. Nunc condimentum vulputate massa lacinia fermentum. Donec maximus cursus eros sed porta. Nunc eu porta turpis. Nulla cursus et nisl eu elementum. Ut rutrum scelerisque aliquet. In tellus erat, rhoncus id tempus vitae, vestibulum non quam. + +Vivamus luctus elit a efficitur dapibus. Praesent magna mi, pellentesque sed accumsan dapibus, blandit at lectus. Praesent sodales erat diam. Pellentesque placerat lobortis enim, a dapibus ligula molestie ut. Phasellus dui augue, suscipit ac urna non, iaculis eleifend augue. Maecenas sed elit iaculis, pretium ligula lacinia, aliquam libero. Nulla sem mi, pellentesque viverra lorem vel, luctus vulputate augue. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Quisque fermentum nunc ex, at lobortis turpis congue vitae. Suspendisse iaculis elementum enim commodo facilisis. Vestibulum non neque tellus. Proin vitae sodales urna. Mauris interdum purus et neque tempus, vitae mattis libero finibus. Suspendisse iaculis condimentum nibh, eu mattis enim vehicula a. + +Fusce dictum urna non lectus ullamcorper vestibulum id in elit. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Nunc fermentum odio non ante sollicitudin posuere. Praesent vulputate quam nec magna euismod pellentesque. Etiam tempor nunc eget velit volutpat eleifend. Mauris sodales nunc ullamcorper, gravida purus eget, rutrum sem. Nam a sapien rhoncus, scelerisque lacus ac, condimentum ipsum. Pellentesque suscipit, ligula ut rhoncus ullamcorper, ligula augue rutrum lorem, eget aliquam risus tortor eget metus. Curabitur scelerisque bibendum purus vel vulputate. Morbi et odio at neque sodales suscipit. Nam at pretium nulla. In scelerisque vulputate suscipit. Nulla facilisi. Pellentesque eu sem eu ligula efficitur viverra. Pellentesque gravida consequat urna id bibendum. + +Nulla consectetur facilisis est nec aliquam. Proin hendrerit magna suscipit ante accumsan hendrerit. Nulla ut sem id neque tempor vehicula. Sed eget massa eget sem placerat tincidunt. Ut eleifend, justo sit amet egestas lacinia, ex velit pharetra nulla, nec aliquet elit neque ut turpis. Quisque in gravida velit. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed euismod malesuada convallis. Ut ultricies, magna et blandit fringilla, dolor dolor maximus sapien, in aliquet augue est vel odio. Nulla commodo elit massa, euismod tempor turpis aliquet eget. Morbi non odio et tortor egestas ultrices. Etiam semper, ipsum et dictum pharetra, eros purus bibendum lacus, vel laoreet lacus sem vehicula nibh. Nunc enim nulla, pulvinar sit amet lobortis sed, porttitor molestie risus. Morbi tincidunt diam mi, nec auctor sem rutrum at. + +Donec pellentesque odio odio, eu condimentum risus consectetur quis. Vivamus ullamcorper, mauris non semper rutrum, dui risus suscipit tellus, ut tempor velit risus vitae nibh. Duis tempor nibh at tristique rutrum. Cras congue nisl at sem sollicitudin efficitur. Aenean auctor purus vel libero fermentum elementum. Mauris convallis orci id interdum accumsan. Aliquam at metus risus. Sed accumsan, quam id dignissim congue, velit risus eleifend ligula, ut fringilla elit erat at neque. Aliquam tempor, quam ut ultrices volutpat, orci lectus vulputate turpis, eget pharetra purus nisi non lorem. Ut condimentum convallis justo, a volutpat eros. Sed tempus turpis leo, in convallis est fringilla sed. Vivamus eu laoreet tellus. Quisque ullamcorper ullamcorper leo. Nam fermentum egestas facilisis. Nulla egestas ligula feugiat urna molestie, faucibus convallis erat accumsan. Cras pellentesque ipsum lectus, vitae luctus dolor suscipit nec. + +Vivamus vel nibh sed mi tincidunt cursus quis facilisis tellus. Donec eget est eu velit pretium molestie. Maecenas lacinia risus turpis. Sed tristique id risus sit amet venenatis. Duis maximus, metus ac molestie convallis, quam ex dapibus sem, at rutrum metus nisi quis lectus. Suspendisse sodales in nisi at hendrerit. Maecenas suscipit lobortis vulputate. Nunc convallis sit amet elit eget porta. Mauris tincidunt massa in augue finibus iaculis. Vestibulum imperdiet, orci vel bibendum laoreet, ligula quam mollis lacus, a eleifend nisi tellus vitae ipsum. Cras non ante sollicitudin, auctor dolor id, condimentum tellus. Morbi malesuada leo nec lectus scelerisque, nec interdum lacus ornare. Integer ultrices ligula nunc, sed blandit urna aliquam eget. Proin consequat viverra ex non rhoncus. Phasellus id nibh at tortor sodales blandit. Donec dignissim ipsum vel ligula malesuada rhoncus. + +Nullam mauris sem, dapibus ac nisl at, hendrerit faucibus nisi. Mauris dictum fermentum pellentesque. Praesent in gravida odio. Vestibulum pharetra iaculis est quis tincidunt. Sed venenatis rhoncus lacus, non porta ante vulputate at. Duis fringilla ipsum at urna euismod molestie. Duis a porttitor ante. Mauris pharetra elit et metus auctor, a eleifend sapien porta. Vivamus ut tincidunt purus, lobortis euismod tellus. Nullam non nulla gravida, laoreet tellus ac, placerat urna. + +Sed justo sapien, scelerisque nec nisl vel, efficitur aliquet purus. Phasellus eget posuere nisl. Integer porta vel purus nec ornare. Pellentesque rutrum in nulla at hendrerit. Nullam elementum sodales volutpat. Duis tempus, purus sagittis auctor ullamcorper, dui erat hendrerit nulla, id finibus eros dui quis risus. Quisque posuere nunc quis augue placerat lacinia. Nunc quis lorem vulputate, tincidunt enim nec, rhoncus odio. Ut porttitor porta velit ut vulputate. Duis convallis sagittis magna nec gravida. Ut eu luctus sem, non placerat erat. Vivamus viverra ut ligula ac finibus. Cras ligula urna, tincidunt id risus eu, dapibus viverra lorem. Aliquam erat volutpat. Sed dolor nisi, faucibus non ligula faucibus, laoreet bibendum diam. Vivamus sagittis lacus ut condimentum tincidunt. + +Proin tristique mi ullamcorper dolor accumsan mollis. Nulla facilisi. Pellentesque iaculis mattis augue ac rutrum. Mauris in nulla at ligula fringilla pulvinar. Nam commodo ornare nibh ac viverra. Maecenas eget augue a risus mattis ullamcorper non at nibh. Aenean nec erat diam. Pellentesque augue nisl, venenatis mollis dolor non, bibendum malesuada leo. Pellentesque elementum purus ornare mauris iaculis porttitor. Sed mollis tempor dui eu elementum. Donec porttitor tellus non mattis gravida. Mauris venenatis suscipit convallis. Sed dolor sapien, pellentesque et tellus a, tempus cursus magna. Nunc lobortis leo magna, at maximus orci ultrices eget. Nullam vulputate dictum nisi, vel egestas orci. + +Nam nibh mi, ornare sit amet nibh vel, lobortis lacinia leo. Ut egestas mi vel nunc luctus vestibulum. Nullam id tempus felis, eget convallis erat. Aliquam venenatis ut tellus id fringilla. Aliquam erat volutpat. Sed vehicula, odio nec mollis dapibus, ligula sapien consequat risus, ut volutpat diam neque ut neque. Mauris venenatis libero vitae sem elementum, sit amet maximus massa varius. Maecenas malesuada mi id leo euismod, eu maximus sapien vehicula. Nulla facilisi. Duis nec venenatis ante, non pulvinar lacus. In sollicitudin sit amet velit suscipit egestas. Maecenas a lectus euismod, dictum nulla at, malesuada lectus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec lorem massa, laoreet non elit nec, sollicitudin blandit felis. + +Cras dignissim fermentum tellus ut fringilla. Maecenas maximus eros pretium sagittis venenatis. Maecenas id enim ac est semper efficitur ac hendrerit leo. Cras eu arcu tincidunt risus pellentesque aliquam. Pellentesque vel sodales libero, non rhoncus enim. Integer vulputate vulputate libero, eu consectetur eros euismod vitae. Fusce magna nibh, vehicula sed turpis eget, malesuada ultricies sem. Sed convallis sem nisl, rhoncus mollis mauris bibendum ut. Pellentesque vitae massa a justo dapibus laoreet. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Sed mollis egestas nisl vel ultricies. Phasellus sit amet varius leo, sed vehicula sapien. Etiam in urna eget neque aliquet ultricies in nec quam. Aliquam at feugiat elit. + +Maecenas placerat nisi ultrices neque pulvinar, et ultrices ante tempus. Vestibulum laoreet quam lacus, eget commodo magna dictum consectetur. Aliquam erat volutpat. Aenean tincidunt ipsum sit amet justo aliquet tempus. Quisque blandit sit amet est facilisis dictum. Sed non consequat dui. Integer purus diam, gravida quis tortor lobortis, iaculis vehicula sem. Morbi pellentesque est elit, vitae interdum elit laoreet et. Vestibulum in blandit ante, eu blandit velit. Morbi sagittis eu est eu vulputate. Nullam ac mi feugiat nibh feugiat suscipit. Sed interdum tincidunt efficitur. Integer dictum sem erat, ut pharetra libero lacinia in. Mauris at hendrerit odio, a facilisis lacus. Donec blandit massa ac nisi ultrices faucibus. + +Aenean tempus id ligula at mollis. Cras id dui magna. Integer id neque tincidunt, rhoncus nunc et, laoreet nibh. Pellentesque consectetur odio ligula, et consectetur tortor scelerisque non. Cras quis ex pharetra mi ornare lobortis. Phasellus fringilla interdum felis, vel aliquam ligula. Mauris cursus varius turpis in iaculis. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Praesent eget mi id nulla semper dapibus. + +Vivamus lacus augue, eleifend dignissim ipsum eu, interdum accumsan massa. Nam id quam vel ligula consectetur volutpat id eget eros. Aliquam sed felis velit. Duis egestas velit ac dolor blandit, ut elementum mi tincidunt. Integer varius nisl a sapien pellentesque, et pellentesque ante elementum. Fusce ac orci interdum, faucibus nisl ut, sagittis nisi. Vestibulum sed diam eu magna congue ornare. Integer dignissim ligula sit amet mauris pretium vulputate. Duis ac tincidunt magna. Sed vehicula, ante nec vulputate venenatis, mauris odio blandit dolor, vitae lacinia nibh lorem et metus. + +Sed non sapien vel lorem tempor semper ac ac tellus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas auctor ante sit amet suscipit vulputate. In ac cursus turpis. Donec eu sem bibendum, blandit est nec, blandit tortor. Pellentesque scelerisque justo vitae magna cursus, vitae egestas libero interdum. Pellentesque vitae ligula vel mauris vehicula convallis. Nunc ornare lectus sit amet lorem aliquet dignissim. Cras nec ligula pretium, semper nulla a, pulvinar lacus. Sed ac augue bibendum, posuere ipsum eu, venenatis quam. Sed elementum nunc quis odio pellentesque, a vestibulum ex congue. Cras id malesuada arcu. Mauris ut egestas nulla, sed tempus ligula. Donec ac elit ut nisi pulvinar elementum. Suspendisse id auctor lectus, vitae ultricies lacus. + +Vestibulum pulvinar ornare cursus. Curabitur accumsan sollicitudin mi in vestibulum. Vestibulum vulputate tincidunt luctus. Suspendisse potenti. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Phasellus a mattis neque, id malesuada odio. Aliquam id auctor magna. Vestibulum quis nibh lacinia, tincidunt felis sed, vestibulum ex. + +Donec placerat ipsum diam, vel imperdiet velit eleifend ac. Quisque dapibus erat non dui convallis eleifend. Praesent pellentesque felis id suscipit rutrum. Donec quis lacinia turpis. Nulla justo eros, fringilla vel fringilla in, euismod sollicitudin arcu. In ut lobortis arcu, at rhoncus elit. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. + +Pellentesque iaculis quam nec interdum eleifend. Fusce mauris mauris, imperdiet sollicitudin volutpat eu, sollicitudin blandit leo. Vestibulum hendrerit diam gravida erat imperdiet, blandit dignissim dui tempor. Phasellus viverra ante quis aliquam finibus. Duis sed condimentum augue, nec sollicitudin dui. Ut ullamcorper metus ac ligula accumsan, ac fringilla metus gravida. Morbi rhoncus est nunc, at lacinia ex fringilla sit amet. Integer lobortis ultricies nisi vitae accumsan. Nulla nec sagittis ante. Mauris bibendum nisi ut magna vulputate maximus. Praesent in elit eu metus dignissim cursus. Ut a tellus felis. Mauris eget ornare diam. + +Suspendisse potenti. Nam nec tortor ex. Duis eu elementum ligula. Pellentesque efficitur sodales orci, vitae sollicitudin odio tempor ac. Nam lacus ante, lobortis vitae lobortis eu, eleifend et velit. Nulla et lectus eu nibh tristique malesuada a sit amet felis. Proin molestie, lectus vitae sagittis malesuada, massa velit finibus est, id vestibulum dolor felis eget lectus. Phasellus varius pellentesque neque, a malesuada ex mattis eget. Nulla facilisi. Phasellus interdum placerat lobortis. Sed et odio tristique, viverra libero et, sagittis diam. Proin tristique lectus id bibendum maximus. Integer ornare euismod ligula nec malesuada. Nulla facilisi. Nullam bibendum hendrerit pharetra. + +Duis pharetra auctor felis, eget aliquet justo commodo at. Donec quis nibh non arcu sodales efficitur. Aenean lorem sapien, tincidunt eu sapien nec, convallis tempor diam. Curabitur volutpat, felis ut congue euismod, lacus nisl euismod ipsum, vel consectetur orci nibh sed ligula. Curabitur consectetur vitae mauris sed tempor. Fusce vel pretium nibh, et tristique dolor. Aliquam semper a ante non finibus. Praesent euismod urna augue, at feugiat orci commodo ut. Duis imperdiet purus non augue cursus gravida. Maecenas sodales purus et sollicitudin venenatis. Nam ultrices lorem lectus, ut pretium turpis hendrerit eget. Vestibulum vel lacinia lectus, at dignissim diam. Vivamus ut tortor ac tortor blandit finibus. Etiam porttitor tortor sit amet elit lacinia gravida. Pellentesque pretium, orci vitae tempor vehicula, nisi tellus tincidunt sapien, id egestas felis quam a nunc. Pellentesque sed vestibulum nisl. + +Mauris congue, justo vel dapibus pretium, ipsum augue consectetur leo, at aliquam ipsum neque non mauris. Nam sollicitudin in urna eu blandit. Aliquam erat volutpat. Morbi eget interdum ante. Pellentesque congue gravida arcu, eget ornare ante elementum nec. Integer a auctor dolor, in varius sem. Etiam dictum nibh magna, at volutpat nunc blandit eu. Curabitur at sem ipsum. + +Nulla porttitor erat eget volutpat iaculis. Pellentesque egestas, nisl vel ultrices efficitur, ex magna condimentum urna, in tincidunt massa turpis eget metus. Etiam vitae magna elementum, fermentum justo ut, pretium velit. Aliquam aliquet venenatis malesuada. Quisque consequat enim metus, pellentesque blandit leo rhoncus id. Vivamus vestibulum, est in condimentum mollis, ligula eros blandit lorem, vitae dignissim lectus mi eget nulla. Cras posuere leo at neque convallis, sed pretium massa ultrices. Morbi tempus porttitor turpis. Nam vitae mauris et massa venenatis tincidunt. Quisque vestibulum lacus ligula, in sodales felis elementum vel. Sed a tellus condimentum, sodales nisi id, aliquet elit. + +Vestibulum ac ex eu turpis lacinia condimentum nec eget ipsum. Nulla non imperdiet erat, at malesuada lorem. Praesent efficitur imperdiet augue vel laoreet. Sed dignissim, ante non porta feugiat, enim nunc rhoncus lorem, eu luctus turpis risus sit amet orci. Vivamus bibendum pharetra venenatis. In at gravida nisi. Vestibulum pulvinar sapien ac augue scelerisque, vitae blandit nibh malesuada. Nunc vitae est faucibus, accumsan risus a, laoreet urna. Cras imperdiet lacus in augue facilisis dignissim. Duis sed nisi quis diam mollis rutrum. + +Vestibulum eget egestas neque. Praesent viverra, velit quis porttitor euismod, sem libero venenatis mauris, id congue urna nulla sed lorem. Nulla mollis metus nec diam malesuada aliquam. Duis ac risus nunc. Cras sollicitudin urna nunc, id sodales quam gravida sit amet. Fusce in vulputate orci, in venenatis lorem. Donec a sagittis ipsum. Quisque consequat sapien tellus, sed efficitur lacus aliquam eu. Interdum et malesuada fames ac ante ipsum primis in faucibus. Aenean in neque at augue elementum commodo pellentesque eget ligula. Nullam condimentum efficitur tincidunt. Phasellus posuere tincidunt odio sed facilisis. Aenean eu risus at est euismod facilisis. Curabitur elit purus, malesuada quis blandit id, rutrum vitae dui. Praesent porta rutrum erat, ullamcorper viverra nunc. Cras ut velit dui. + +Etiam posuere pulvinar mi at ullamcorper. Pellentesque finibus, tellus non convallis commodo, orci nibh dapibus nisl, at aliquam purus nulla eget dui. Praesent fringilla urna nec nulla pellentesque, nec rhoncus turpis ultricies. Sed laoreet velit pellentesque libero varius, ac interdum urna viverra. Phasellus sed consectetur massa. Morbi quis velit nec ipsum varius tempor. Proin id sodales felis. Aliquam lacinia risus quis ligula condimentum sodales. Nulla vel arcu aliquet neque iaculis aliquet. Cras sed lorem eu turpis tincidunt sodales. Sed pulvinar elementum ligula, nec faucibus nisl. Fusce nec tellus eget dui tempor sagittis. In vitae enim in ex viverra commodo. Duis est erat, fringilla ac semper a, dapibus in tortor. + +Maecenas commodo vulputate iaculis. Aliquam non facilisis est. Donec pellentesque vitae nibh nec volutpat. In commodo metus placerat lorem commodo, non lacinia nibh bibendum. In viverra rhoncus erat. Mauris nec nisl blandit, elementum justo nec, accumsan libero. Aliquam elementum, velit et ullamcorper convallis, turpis lorem elementum lorem, quis consequat tortor eros ut erat. Curabitur et enim quis felis vulputate congue et vel purus. Sed elementum interdum ipsum, sed tincidunt arcu scelerisque et. + +Aenean interdum elementum mauris ut porta. Mauris vel purus ac odio vulputate pulvinar at quis odio. In turpis turpis, convallis in augue id, elementum vulputate lorem. Sed tincidunt fermentum vulputate. Nunc ipsum ipsum, molestie vel convallis id, pharetra vel arcu. Maecenas vel dui elit. Sed blandit dolor sit amet risus commodo faucibus. Duis rhoncus felis arcu, vel aliquam nisi faucibus sed. Suspendisse cursus eget nunc ut bibendum. Fusce non ligula risus. Curabitur vitae cursus metus, quis fringilla diam. Phasellus turpis ante, pulvinar ac turpis tristique, sollicitudin congue lectus. Proin quis ipsum at ipsum euismod euismod. + +Nunc ut fermentum nunc. Donec id commodo lacus, at hendrerit justo. Donec cursus purus sodales nunc commodo, quis bibendum elit hendrerit. Quisque quis pellentesque nibh, ac vulputate neque. Aenean non placerat felis, eget feugiat ligula. Vivamus a eros accumsan, cursus neque at, ultricies magna. Fusce tincidunt tellus vitae mi rutrum laoreet sed quis ligula. Nullam ullamcorper ligula ligula, vel fringilla metus aliquet a. Morbi aliquet, mauris vel interdum venenatis, ex arcu venenatis tortor, at tincidunt dui ipsum et arcu. Nulla blandit gravida nulla ac iaculis. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Sed ullamcorper lectus in sapien lobortis, eget posuere massa varius. + +Cras lacinia nunc faucibus mauris placerat pretium eget non sapien. Morbi viverra bibendum posuere. Aliquam accumsan sagittis dolor non iaculis. Sed nunc odio, lobortis in dolor ac, rutrum fermentum velit. In venenatis, velit in molestie semper, est magna condimentum dui, aliquam auctor lorem nulla vel ligula. Morbi vehicula turpis turpis, quis mollis justo tempus vitae. Proin luctus lacus in mauris porttitor aliquet. Duis vitae nunc ex. Nullam eget erat vitae nulla iaculis rhoncus. Sed lacus dui, suscipit eu leo vitae, tincidunt dignissim risus. Praesent ut massa ut arcu sagittis consequat. Sed sit amet tincidunt turpis. Nulla bibendum, felis eget posuere dictum, libero mi tristique elit, at venenatis neque elit ac quam. Curabitur nisi sem, scelerisque nec nunc ac, pulvinar pharetra odio. Curabitur egestas pellentesque arcu sed suscipit. In mattis dolor vel dui mollis feugiat. + +Sed commodo, dui ac vestibulum dictum, tellus libero tincidunt lacus, viverra commodo est felis vitae urna. Proin tincidunt neque vel turpis eleifend laoreet. Vestibulum sagittis, tortor sed iaculis consequat, urna ante sagittis est, ac ullamcorper lorem nibh dignissim odio. Nam arcu mi, cursus et blandit nec, aliquam ut nulla. Aenean quis iaculis tellus, eu egestas augue. Etiam pretium eget nisi quis iaculis. Aliquam sed convallis eros. Ut bibendum rhoncus lacus, in vestibulum dui ultricies id. Fusce vestibulum, mauris ut tempor consequat, dolor nisl pellentesque elit, in porta arcu elit vel ante. Vivamus at nisl est. Etiam nec blandit tortor, at pulvinar orci. Proin semper dapibus tincidunt. Phasellus lobortis enim ullamcorper dolor tempor cursus. + +Mauris a libero in enim gravida aliquet ac sit amet nibh. Vestibulum ac neque posuere, blandit libero ac, vehicula enim. Aliquam auctor iaculis eros sit amet molestie. Quisque faucibus turpis et massa tristique, nec dapibus mauris aliquet. Proin blandit aliquet mauris, non tincidunt odio blandit vel. Curabitur a nibh in eros commodo tincidunt eu et libero. Curabitur sit amet dapibus ex, in condimentum magna. Sed eu sem sem. Nunc tellus dolor, rutrum eu mauris nec, congue feugiat purus. Fusce tempor, neque vitae bibendum imperdiet, dolor ipsum condimentum urna, et egestas quam tortor in ex. Aliquam velit magna, commodo hendrerit sagittis sed, feugiat eget erat. Nunc quis ullamcorper velit, eu consequat augue. Nam arcu mauris, condimentum sit amet magna in, finibus scelerisque nunc. Mauris erat est, hendrerit ac accumsan eget, facilisis ut nisl. Quisque dignissim arcu quis diam tincidunt tristique. Sed rhoncus nisl non enim fermentum, a lobortis dolor consectetur. + +Sed eget condimentum ligula. Vestibulum vitae cursus eros. Donec elementum sapien magna, posuere iaculis sem ultrices lobortis. Morbi eu bibendum lectus. Suspendisse ante eros, ullamcorper ac viverra eget, pellentesque sed sapien. Duis sit amet tincidunt dui, vitae lobortis purus. Sed venenatis tincidunt volutpat. Vivamus a nisl ac elit consectetur semper ut eu libero. Proin id cursus ex. In hac habitasse platea dictumst. Aenean sed nisi vitae odio venenatis pulvinar vitae ac risus. Sed varius magna ut erat luctus vehicula. + +Nunc non ex eget purus blandit faucibus at quis velit. Donec quis mi vestibulum, facilisis nisi quis, tincidunt turpis. Sed bibendum metus sed consectetur mollis. Maecenas fermentum, erat finibus pulvinar lacinia, ex risus dictum sem, ut vestibulum augue diam vel diam. Donec ac massa non nibh pretium laoreet eget in orci. Nulla placerat eleifend mi, pretium vestibulum diam condimentum vitae. Nunc odio turpis, feugiat vitae turpis eget, pellentesque commodo turpis. Etiam sapien purus, consequat nec mi eget, consectetur efficitur neque. Phasellus porttitor sapien sit amet nunc semper, vel bibendum nibh finibus. Ut ac imperdiet ex, eu congue felis. In posuere nisi felis. Mauris tempus pretium mauris, ac viverra nunc hendrerit id. Sed fermentum nec sem ac pulvinar. Integer dictum velit eget congue venenatis. + +Cras eros dolor, venenatis ac dictum sed, dignissim nec sem. Curabitur tempor erat quis pretium interdum. Nunc vestibulum justo nisi, sit amet sagittis tellus consequat vel. Donec pharetra nunc vitae consequat eleifend. Quisque ut mauris quis nunc volutpat consectetur. Nam a suscipit ligula, at gravida libero. Vestibulum blandit, tellus sed bibendum volutpat, libero tortor convallis nisl, sit amet placerat lorem dolor nec libero. Integer blandit libero elit, ut congue ex euismod congue. Nulla sodales justo id eros condimentum faucibus. + +Proin quam ante, hendrerit at bibendum eu, pharetra at lectus. Proin finibus arcu id nisl aliquam dapibus. Fusce a suscipit nisl. Ut placerat ultrices nibh nec efficitur. Vestibulum vitae interdum magna. Donec lobortis finibus risus, et luctus ipsum efficitur euismod. Quisque dictum diam et venenatis euismod. Cras dictum molestie aliquet. Vestibulum imperdiet quam eget diam malesuada, quis pulvinar odio dignissim. Curabitur sapien elit, iaculis vitae justo eget, pretium malesuada elit. Donec gravida molestie tincidunt. Nullam at commodo ipsum. Sed nisi erat, tincidunt ultricies interdum consequat, pretium mollis ipsum. Vivamus in erat consectetur, sodales nibh at, porta nunc. Mauris semper, dui vitae condimentum tempus, mauris justo volutpat urna, ac ullamcorper tortor dolor in sem. + +Aenean leo ligula, egestas at vestibulum ac, porta vel mauris. Curabitur at lorem non mauris fringilla venenatis vel eu arcu. Donec posuere eleifend diam. Duis aliquet justo lacus, eu egestas erat eleifend sed. Sed non cursus urna. Sed pretium purus id blandit varius. Mauris turpis mi, lobortis eu tellus sit amet, maximus venenatis felis. Proin non varius enim, aliquet finibus velit. Praesent posuere pharetra ipsum eget varius. Phasellus non fermentum magna, ut iaculis augue. Praesent ut nisl nunc. + +Sed ligula tellus, interdum at sapien ut, dictum pellentesque nisl. Duis fringilla, sem nec elementum dapibus, nisl tellus maximus velit, eu varius sem nisl feugiat eros. Maecenas quis viverra lacus. Nulla nec commodo ex, nec placerat enim. Curabitur ultrices sagittis fringilla. Vestibulum sit amet enim sagittis, condimentum nisl sit amet, pulvinar orci. Ut at erat finibus erat bibendum convallis. Quisque euismod magna eget leo facilisis hendrerit. Cras venenatis, nisi quis sollicitudin volutpat, metus enim vestibulum nunc, at mollis leo leo nec est. Fusce fermentum tristique feugiat. Suspendisse sem est, condimentum ut ante at, egestas convallis ante. Vestibulum dictum, ex nec imperdiet laoreet, magna est ullamcorper augue, vel molestie quam odio vel ante. Donec dui dui, posuere a neque ac, condimentum vehicula magna. Curabitur suscipit, risus vitae bibendum posuere, mauris lorem viverra est, at tristique arcu quam quis leo. Donec sed posuere neque. Suspendisse iaculis aliquet condimentum. + +Morbi tempus condimentum diam eget bibendum. Aliquam varius magna quis lectus interdum, in elementum ligula tempor. Morbi vitae lorem sapien. Morbi luctus consectetur eros non aliquet. Pellentesque vestibulum sem sed ante accumsan faucibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Etiam ac suscipit justo. Nunc efficitur lectus eu arcu venenatis, vel accumsan ex suscipit. Vestibulum egestas ultricies tellus, et interdum enim pretium eu. Aenean rutrum est tincidunt rhoncus molestie. Phasellus hendrerit tellus et laoreet varius. Integer efficitur felis magna, nec sollicitudin arcu sollicitudin in. Curabitur non feugiat odio. Mauris nisi odio, luctus quis dolor sed, tristique luctus ex. Nullam libero enim, facilisis ut venenatis et, vulputate sed purus. + +Mauris a odio ut leo porttitor ultrices a et ex. Pellentesque vestibulum lacinia faucibus. In pellentesque eget augue at feugiat. Integer finibus augue dolor, in luctus lorem rutrum vitae. Donec pharetra lectus ac purus sollicitudin, vel tristique mauris pretium. Cras porttitor mi eu lectus consequat, a ultrices felis venenatis. In condimentum turpis in velit mattis laoreet. Aliquam sed mauris id nulla ultrices convallis vel vel velit. Suspendisse ut arcu finibus justo fermentum fringilla. Etiam in malesuada nisl. In hac habitasse platea dictumst. Duis a est lacinia, pretium dui eget, condimentum turpis. Integer ac placerat augue. Donec et eros felis. + +Integer cursus magna id quam sagittis consectetur. Aliquam erat volutpat. Quisque ullamcorper nisl nec massa dapibus facilisis vitae at nunc. Donec laoreet, libero in elementum tempus, enim odio porttitor felis, venenatis fermentum augue velit eu urna. Ut at ullamcorper enim. In molestie, velit et blandit maximus, erat nunc laoreet quam, vel finibus est mauris non sapien. Donec et dictum lorem. Nunc vestibulum, dolor eget tempus maximus, elit eros aliquam velit, vitae mattis mauris lectus ac tortor. Donec rutrum justo orci, id dictum metus vestibulum a. Etiam bibendum ipsum convallis, lacinia turpis vitae, dictum tortor. In velit dolor, scelerisque sed molestie nec, volutpat a turpis. Cras posuere commodo erat ut gravida. Quisque ante ipsum, volutpat a tellus non, dignissim ornare elit. Pellentesque sed porttitor dui, luctus rhoncus purus. In vitae ante pulvinar, consectetur orci quis, tempus velit. Curabitur tempus ligula id sapien ornare rhoncus. + +Maecenas eu nibh et elit accumsan blandit a at erat. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Etiam commodo feugiat condimentum. In non ante ut mauris eleifend lobortis. Phasellus eleifend vitae metus et semper. Nam sit amet rhoncus diam. Nunc molestie libero sed erat volutpat consequat. + +Aenean eget tristique odio. Vivamus quam tellus, dignissim sed faucibus sed, sagittis ut elit. Maecenas in ullamcorper sem. In at ipsum accumsan lectus vestibulum commodo nec non leo. Aliquam at suscipit felis. Nunc non egestas tortor. Donec sit amet eleifend eros. + +Sed eleifend nisi velit, in egestas erat condimentum eu. Pellentesque a tincidunt urna. Interdum et malesuada fames ac ante ipsum primis in faucibus. Vestibulum dapibus mauris vitae elit auctor, id venenatis sem consectetur. Vivamus non leo pulvinar, blandit libero ut, vehicula arcu. Nullam elementum ex enim, at mattis massa pharetra eu. Nunc nulla magna, lobortis a magna sit amet, laoreet fermentum justo. Curabitur aliquam sollicitudin posuere. Aenean semper porta dictum. Mauris accumsan non nisi nec faucibus augue. diff --git a/powertools-idempotency/pom.xml b/powertools-idempotency/pom.xml index 088f595c1..6b9e46a4a 100644 --- a/powertools-idempotency/pom.xml +++ b/powertools-idempotency/pom.xml @@ -1,4 +1,18 @@ + + @@ -91,7 +105,6 @@ org.junit-pioneer junit-pioneer - 1.9.1 test @@ -180,6 +193,10 @@ + + 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 { *

              * Idempotency.config().withConfig(config).configure();
              * 
    + * * @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-parent software.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 Issues https://github.com/aws-powertools/powertools-lambda-java/issues @@ -45,34 +58,48 @@ software.amazon.lambda powertools-core + + org.aspectj + aspectjrt + com.amazonaws - aws-lambda-java-core + aws-lambda-java-events software.amazon.payloadoffloading payloadoffloading-common - com.amazonaws - aws-lambda-java-events + com.fasterxml.jackson.core + jackson-core software.amazon.awssdk - sqs + sdk-core software.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-pioneer test - org.apache.commons - commons-lang3 + org.mockito + mockito-core test org.mockito - mockito-core + mockito-inline test - org.aspectj - aspectjweaver + org.apache.commons + commons-lang3 test @@ -111,6 +138,29 @@ assertj-core test + + 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 MyLambdaHandler() {
    + *     LargeMessageConfig.init().withS3Client(S3Client.create());
    + * }
    + * 
    + */ +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. - * + *

    * {@see Logging} */ public final class LoggingUtils { @@ -35,7 +35,7 @@ private LoggingUtils() { * Appends an additional key and value to each log entry made. Duplicate values * for the same key will be replaced with the latest. * - * @param key The name of the key to be logged + * @param key The name of the key to be logged * @param value The value to be logged */ public static void appendKey(String key, String value) { @@ -43,7 +43,6 @@ public static void appendKey(String key, String value) { } - /** * Appends additional key and value to each log entry made. Duplicate values * for the same key will be replaced with the latest. @@ -93,8 +92,8 @@ public static void defaultObjectMapper(ObjectMapper objectMapper) { } public static ObjectMapper objectMapper() { - if(null == objectMapper) { - objectMapper = new ObjectMapper(); + if (null == objectMapper) { + objectMapper = new ObjectMapper(); } return objectMapper; diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/AbstractJacksonLayoutCopy.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/AbstractJacksonLayoutCopy.java index 3ceda4b79..17d09729f 100644 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/AbstractJacksonLayoutCopy.java +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/AbstractJacksonLayoutCopy.java @@ -1,11 +1,31 @@ +/* + * 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.internal; +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonRootName; +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import com.fasterxml.jackson.core.JsonGenerationException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectWriter; import java.io.IOException; import java.io.Writer; import java.nio.charset.Charset; import java.util.LinkedHashMap; import java.util.Map; - import org.apache.logging.log4j.Level; import org.apache.logging.log4j.Marker; import org.apache.logging.log4j.ThreadContext; @@ -25,19 +45,146 @@ import org.apache.logging.log4j.util.ReadOnlyStringMap; import org.apache.logging.log4j.util.Strings; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonRootName; -import com.fasterxml.jackson.annotation.JsonUnwrapped; -import com.fasterxml.jackson.core.JsonGenerationException; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.ObjectWriter; - @Deprecated abstract class AbstractJacksonLayoutCopy extends AbstractStringLayout { protected static final String DEFAULT_EOL = "\r\n"; protected static final String COMPACT_EOL = Strings.EMPTY; + protected final String eol; + protected final ObjectWriter objectWriter; + protected final boolean compact; + protected final boolean complete; + protected final boolean includeNullDelimiter; + protected final ResolvableKeyValuePair[] additionalFields; + + @Deprecated + protected AbstractJacksonLayoutCopy(final Configuration config, final ObjectWriter objectWriter, + final Charset charset, + final boolean compact, final boolean complete, final boolean eventEol, + final Serializer headerSerializer, + final Serializer footerSerializer) { + this(config, objectWriter, charset, compact, complete, eventEol, headerSerializer, footerSerializer, false); + } + + @Deprecated + protected AbstractJacksonLayoutCopy(final Configuration config, final ObjectWriter objectWriter, + final Charset charset, + final boolean compact, final boolean complete, final boolean eventEol, + final Serializer headerSerializer, + final Serializer footerSerializer, final boolean includeNullDelimiter) { + this(config, objectWriter, charset, compact, complete, eventEol, null, headerSerializer, footerSerializer, + includeNullDelimiter, null); + } + + protected AbstractJacksonLayoutCopy(final Configuration config, final ObjectWriter objectWriter, + final Charset charset, + final boolean compact, final boolean complete, final boolean eventEol, + final String endOfLine, final Serializer headerSerializer, + final Serializer footerSerializer, final boolean includeNullDelimiter, + final KeyValuePair[] additionalFields) { + super(config, charset, headerSerializer, footerSerializer); + this.objectWriter = objectWriter; + this.compact = compact; + this.complete = complete; + this.eol = endOfLine != null ? endOfLine : compact && !eventEol ? COMPACT_EOL : DEFAULT_EOL; + this.includeNullDelimiter = includeNullDelimiter; + this.additionalFields = prepareAdditionalFields(config, additionalFields); + } + + protected static boolean valueNeedsLookup(final String value) { + return value != null && value.contains("${"); + } + + private static ResolvableKeyValuePair[] prepareAdditionalFields(final Configuration config, + final KeyValuePair[] additionalFields) { + if (additionalFields == null || additionalFields.length == 0) { + // No fields set + return ResolvableKeyValuePair.EMPTY_ARRAY; + } + + // Convert to specific class which already determines whether values needs lookup during serialization + final ResolvableKeyValuePair[] resolvableFields = new ResolvableKeyValuePair[additionalFields.length]; + + for (int i = 0; i < additionalFields.length; i++) { + final ResolvableKeyValuePair resolvable = + resolvableFields[i] = new ResolvableKeyValuePair(additionalFields[i]); + + // Validate + if (config == null && resolvable.valueNeedsLookup) { + throw new IllegalArgumentException( + "configuration needs to be set when there are additional fields with variables"); + } + } + + return resolvableFields; + } + + private static LogEvent convertMutableToLog4jEvent(final LogEvent event) { + return event instanceof Log4jLogEvent ? event : Log4jLogEvent.createMemento(event); + } + + /** + * Formats a {@link org.apache.logging.log4j.core.LogEvent}. + * + * @param event The LogEvent. + * @return The XML representation of the LogEvent. + */ + @Override + public String toSerializable(final LogEvent event) { + final StringBuilderWriter writer = new StringBuilderWriter(); + try { + toSerializable(event, writer); + return writer.toString(); + } catch (final IOException e) { + // Should this be an ISE or IAE? + LOGGER.error(e); + return Strings.EMPTY; + } + } + + protected Object wrapLogEvent(final LogEvent event) { + if (additionalFields.length > 0) { + // Construct map for serialization - note that we are intentionally using original LogEvent + final Map additionalFieldsMap = resolveAdditionalFields(event); + // This class combines LogEvent with AdditionalFields during serialization + return new LogEventWithAdditionalFields(event, additionalFieldsMap); + } else if (event instanceof Message) { + // If the LogEvent implements the Messagee interface Jackson will not treat is as a LogEvent. + return new ReadOnlyLogEventWrapper(event); + } else { + // No additional fields, return original object + return event; + } + } + + private Map resolveAdditionalFields(final LogEvent logEvent) { + // Note: LinkedHashMap retains order + final Map additionalFieldsMap = new LinkedHashMap<>(additionalFields.length); + final StrSubstitutor strSubstitutor = configuration.getStrSubstitutor(); + + // Go over each field + for (final ResolvableKeyValuePair pair : additionalFields) { + if (pair.valueNeedsLookup) { + // Resolve value + additionalFieldsMap.put(pair.key, strSubstitutor.replace(logEvent, pair.value)); + } else { + // Plain text value + additionalFieldsMap.put(pair.key, pair.value); + } + } + + return additionalFieldsMap; + } + + public void toSerializable(final LogEvent event, final Writer writer) + throws JsonGenerationException, JsonMappingException, IOException { + objectWriter.writeValue(writer, wrapLogEvent(convertMutableToLog4jEvent(event))); + writer.write(eol); + if (includeNullDelimiter) { + writer.write('\0'); + } + markEvent(); + } public static abstract class Builder> extends AbstractStringLayout.Builder { @@ -82,80 +229,68 @@ public boolean getEventEol() { return eventEol; } - public String getEndOfLine() { - return endOfLine; - } - - public boolean isCompact() { - return compact; - } - - public boolean isComplete() { - return complete; - } - - public boolean isLocationInfo() { - return locationInfo; - } - - public boolean isProperties() { - return properties; - } - - /** - * If "true", includes the stacktrace of any Throwable in the generated data, defaults to "true". - * @return If "true", includes the stacktrace of any Throwable in the generated data, defaults to "true". - */ - public boolean isIncludeStacktrace() { - return includeStacktrace; - } - - public boolean isStacktraceAsString() { - return stacktraceAsString; - } - - public boolean isIncludeNullDelimiter() { return includeNullDelimiter; } - - public boolean isIncludeTimeMillis() { - return includeTimeMillis; - } - - public KeyValuePair[] getAdditionalFields() { - return additionalFields; - } - public B setEventEol(final boolean eventEol) { this.eventEol = eventEol; return asBuilder(); } + public String getEndOfLine() { + return endOfLine; + } + public B setEndOfLine(final String endOfLine) { this.endOfLine = endOfLine; return asBuilder(); } + public boolean isCompact() { + return compact; + } + public B setCompact(final boolean compact) { this.compact = compact; return asBuilder(); } + public boolean isComplete() { + return complete; + } + public B setComplete(final boolean complete) { this.complete = complete; return asBuilder(); } + public boolean isLocationInfo() { + return locationInfo; + } + public B setLocationInfo(final boolean locationInfo) { this.locationInfo = locationInfo; return asBuilder(); } + public boolean isProperties() { + return properties; + } + public B setProperties(final boolean properties) { this.properties = properties; return asBuilder(); } + /** + * If "true", includes the stacktrace of any Throwable in the generated data, defaults to "true". + * + * @return If "true", includes the stacktrace of any Throwable in the generated data, defaults to "true". + */ + public boolean isIncludeStacktrace() { + return includeStacktrace; + } + /** * If "true", includes the stacktrace of any Throwable in the generated JSON, defaults to "true". + * * @param includeStacktrace If "true", includes the stacktrace of any Throwable in the generated JSON, defaults to "true". * @return this builder */ @@ -164,6 +299,10 @@ public B setIncludeStacktrace(final boolean includeStacktrace) { return asBuilder(); } + public boolean isStacktraceAsString() { + return stacktraceAsString; + } + /** * Whether to format the stacktrace as a string, and not a nested object (optional, defaults to false). * @@ -174,6 +313,10 @@ public B setStacktraceAsString(final boolean stacktraceAsString) { return asBuilder(); } + public boolean isIncludeNullDelimiter() { + return includeNullDelimiter; + } + /** * Whether to include NULL byte as delimiter after each event (optional, default to false). * @@ -184,6 +327,10 @@ public B setIncludeNullDelimiter(final boolean includeNullDelimiter) { return asBuilder(); } + public boolean isIncludeTimeMillis() { + return includeTimeMillis; + } + /** * Whether to include the timestamp (in addition to the Instant) (optional, default to false). * @@ -194,6 +341,10 @@ public B setIncludeTimeMillis(final boolean includeTimeMillis) { return asBuilder(); } + public KeyValuePair[] getAdditionalFields() { + return additionalFields; + } + /** * Additional fields to set on each log event. * @@ -205,132 +356,6 @@ public B setAdditionalFields(final KeyValuePair[] additionalFields) { } } - protected final String eol; - protected final ObjectWriter objectWriter; - protected final boolean compact; - protected final boolean complete; - protected final boolean includeNullDelimiter; - protected final ResolvableKeyValuePair[] additionalFields; - - @Deprecated - protected AbstractJacksonLayoutCopy(final Configuration config, final ObjectWriter objectWriter, final Charset charset, - final boolean compact, final boolean complete, final boolean eventEol, final Serializer headerSerializer, - final Serializer footerSerializer) { - this(config, objectWriter, charset, compact, complete, eventEol, headerSerializer, footerSerializer, false); - } - - @Deprecated - protected AbstractJacksonLayoutCopy(final Configuration config, final ObjectWriter objectWriter, final Charset charset, - final boolean compact, final boolean complete, final boolean eventEol, final Serializer headerSerializer, - final Serializer footerSerializer, final boolean includeNullDelimiter) { - this(config, objectWriter, charset, compact, complete, eventEol, null, headerSerializer, footerSerializer, includeNullDelimiter, null); - } - - protected AbstractJacksonLayoutCopy(final Configuration config, final ObjectWriter objectWriter, final Charset charset, - final boolean compact, final boolean complete, final boolean eventEol, final String endOfLine, final Serializer headerSerializer, - final Serializer footerSerializer, final boolean includeNullDelimiter, - final KeyValuePair[] additionalFields) { - super(config, charset, headerSerializer, footerSerializer); - this.objectWriter = objectWriter; - this.compact = compact; - this.complete = complete; - this.eol = endOfLine != null ? endOfLine : compact && !eventEol ? COMPACT_EOL : DEFAULT_EOL; - this.includeNullDelimiter = includeNullDelimiter; - this.additionalFields = prepareAdditionalFields(config, additionalFields); - } - - protected static boolean valueNeedsLookup(final String value) { - return value != null && value.contains("${"); - } - - private static ResolvableKeyValuePair[] prepareAdditionalFields(final Configuration config, final KeyValuePair[] additionalFields) { - if (additionalFields == null || additionalFields.length == 0) { - // No fields set - return ResolvableKeyValuePair.EMPTY_ARRAY; - } - - // Convert to specific class which already determines whether values needs lookup during serialization - final ResolvableKeyValuePair[] resolvableFields = new ResolvableKeyValuePair[additionalFields.length]; - - for (int i = 0; i < additionalFields.length; i++) { - final ResolvableKeyValuePair resolvable = resolvableFields[i] = new ResolvableKeyValuePair(additionalFields[i]); - - // Validate - if (config == null && resolvable.valueNeedsLookup) { - throw new IllegalArgumentException("configuration needs to be set when there are additional fields with variables"); - } - } - - return resolvableFields; - } - - /** - * Formats a {@link org.apache.logging.log4j.core.LogEvent}. - * - * @param event The LogEvent. - * @return The XML representation of the LogEvent. - */ - @Override - public String toSerializable(final LogEvent event) { - final StringBuilderWriter writer = new StringBuilderWriter(); - try { - toSerializable(event, writer); - return writer.toString(); - } catch (final IOException e) { - // Should this be an ISE or IAE? - LOGGER.error(e); - return Strings.EMPTY; - } - } - - private static LogEvent convertMutableToLog4jEvent(final LogEvent event) { - return event instanceof Log4jLogEvent ? event : Log4jLogEvent.createMemento(event); - } - - protected Object wrapLogEvent(final LogEvent event) { - if (additionalFields.length > 0) { - // Construct map for serialization - note that we are intentionally using original LogEvent - final Map additionalFieldsMap = resolveAdditionalFields(event); - // This class combines LogEvent with AdditionalFields during serialization - return new LogEventWithAdditionalFields(event, additionalFieldsMap); - } else if (event instanceof Message) { - // If the LogEvent implements the Messagee interface Jackson will not treat is as a LogEvent. - return new ReadOnlyLogEventWrapper(event); - } else { - // No additional fields, return original object - return event; - } - } - - private Map resolveAdditionalFields(final LogEvent logEvent) { - // Note: LinkedHashMap retains order - final Map additionalFieldsMap = new LinkedHashMap<>(additionalFields.length); - final StrSubstitutor strSubstitutor = configuration.getStrSubstitutor(); - - // Go over each field - for (final ResolvableKeyValuePair pair : additionalFields) { - if (pair.valueNeedsLookup) { - // Resolve value - additionalFieldsMap.put(pair.key, strSubstitutor.replace(logEvent, pair.value)); - } else { - // Plain text value - additionalFieldsMap.put(pair.key, pair.value); - } - } - - return additionalFieldsMap; - } - - public void toSerializable(final LogEvent event, final Writer writer) - throws JsonGenerationException, JsonMappingException, IOException { - objectWriter.writeValue(writer, wrapLogEvent(convertMutableToLog4jEvent(event))); - writer.write(eol); - if (includeNullDelimiter) { - writer.write('\0'); - } - markEvent(); - } - @JsonRootName(XmlConstants.ELT_EVENT) public static class LogEventWithAdditionalFields { @@ -472,13 +497,13 @@ public boolean isEndOfBatch() { } @Override - public boolean isIncludeLocation() { - return event.isIncludeLocation(); + public void setEndOfBatch(boolean endOfBatch) { + } @Override - public void setEndOfBatch(boolean endOfBatch) { - + public boolean isIncludeLocation() { + return event.isIncludeLocation(); } @Override diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/DefaultLambdaFields.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/DefaultLambdaFields.java index a50b292b2..2461ae771 100644 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/DefaultLambdaFields.java +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/DefaultLambdaFields.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,10 +11,10 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.logging.internal; import com.amazonaws.services.lambda.runtime.Context; - import java.util.HashMap; import java.util.Map; @@ -31,10 +31,6 @@ enum DefaultLambdaFields { this.name = name; } - public String getName() { - return name; - } - static Map values(Context context) { Map hashMap = new HashMap<>(); @@ -46,4 +42,8 @@ static Map values(Context context) { return hashMap; } + + public String getName() { + return name; + } } diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/JacksonFactoryCopy.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/JacksonFactoryCopy.java index 41247cfdb..6b568be30 100644 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/JacksonFactoryCopy.java +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/JacksonFactoryCopy.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.internal; import com.fasterxml.jackson.core.PrettyPrinter; @@ -7,16 +21,57 @@ import com.fasterxml.jackson.databind.ObjectWriter; import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter; import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider; +import java.util.HashSet; +import java.util.Set; import org.apache.logging.log4j.core.impl.Log4jLogEvent; import org.apache.logging.log4j.core.jackson.JsonConstants; import org.apache.logging.log4j.core.jackson.Log4jJsonObjectMapper; -import java.util.HashSet; -import java.util.Set; - @Deprecated abstract class JacksonFactoryCopy { + abstract protected String getPropertyNameForTimeMillis(); + + abstract protected String getPropertyNameForInstant(); + + abstract protected String getPropertNameForContextMap(); + + abstract protected String getPropertNameForSource(); + + abstract protected String getPropertNameForNanoTime(); + + abstract protected PrettyPrinter newCompactPrinter(); + + abstract protected ObjectMapper newObjectMapper(); + + abstract protected PrettyPrinter newPrettyPrinter(); + + ObjectWriter newWriter(final boolean locationInfo, final boolean properties, final boolean compact) { + return newWriter(locationInfo, properties, compact, false); + } + + ObjectWriter newWriter(final boolean locationInfo, final boolean properties, final boolean compact, + final boolean includeMillis) { + final SimpleFilterProvider filters = new SimpleFilterProvider(); + final Set except = new HashSet<>(3); + if (!locationInfo) { + except.add(this.getPropertNameForSource()); + } + if (!properties) { + except.add(this.getPropertNameForContextMap()); + } + if (includeMillis) { + except.add(getPropertyNameForInstant()); + } else { + except.add(getPropertyNameForTimeMillis()); + } + except.add(this.getPropertNameForNanoTime()); + filters.addFilter(Log4jLogEvent.class.getName(), SimpleBeanPropertyFilter.serializeAllExcept(except)); + final ObjectWriter writer = + this.newObjectMapper().writer(compact ? this.newCompactPrinter() : this.newPrettyPrinter()); + return writer.with(filters); + } + static class JSON extends JacksonFactoryCopy { private final boolean encodeThreadContextAsList; @@ -24,7 +79,8 @@ static class JSON extends JacksonFactoryCopy { private final boolean stacktraceAsString; private final boolean objectMessageAsJsonObject; - public JSON(final boolean encodeThreadContextAsList, final boolean includeStacktrace, final boolean stacktraceAsString, final boolean objectMessageAsJsonObject) { + public JSON(final boolean encodeThreadContextAsList, final boolean includeStacktrace, + final boolean stacktraceAsString, final boolean objectMessageAsJsonObject) { this.encodeThreadContextAsList = encodeThreadContextAsList; this.includeStacktrace = includeStacktrace; this.stacktraceAsString = stacktraceAsString; @@ -63,7 +119,8 @@ protected PrettyPrinter newCompactPrinter() { @Override protected ObjectMapper newObjectMapper() { - return new Log4jJsonObjectMapper(encodeThreadContextAsList, includeStacktrace, stacktraceAsString, objectMessageAsJsonObject); + return new Log4jJsonObjectMapper(encodeThreadContextAsList, includeStacktrace, stacktraceAsString, + objectMessageAsJsonObject); } @Override @@ -73,45 +130,4 @@ protected PrettyPrinter newPrettyPrinter() { } - abstract protected String getPropertyNameForTimeMillis(); - - abstract protected String getPropertyNameForInstant(); - - abstract protected String getPropertNameForContextMap(); - - abstract protected String getPropertNameForSource(); - - abstract protected String getPropertNameForNanoTime(); - - abstract protected PrettyPrinter newCompactPrinter(); - - abstract protected ObjectMapper newObjectMapper(); - - abstract protected PrettyPrinter newPrettyPrinter(); - - ObjectWriter newWriter(final boolean locationInfo, final boolean properties, final boolean compact) { - return newWriter(locationInfo, properties, compact, false); - } - - ObjectWriter newWriter(final boolean locationInfo, final boolean properties, final boolean compact, - final boolean includeMillis) { - final SimpleFilterProvider filters = new SimpleFilterProvider(); - final Set except = new HashSet<>(3); - if (!locationInfo) { - except.add(this.getPropertNameForSource()); - } - if (!properties) { - except.add(this.getPropertNameForContextMap()); - } - if (includeMillis) { - except.add(getPropertyNameForInstant()); - } else { - except.add(getPropertyNameForTimeMillis()); - } - except.add(this.getPropertNameForNanoTime()); - filters.addFilter(Log4jLogEvent.class.getName(), SimpleBeanPropertyFilter.serializeAllExcept(except)); - final ObjectWriter writer = this.newObjectMapper().writer(compact ? this.newCompactPrinter() : this.newPrettyPrinter()); - return writer.with(filters); - } - } \ No newline at end of file diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaJsonLayout.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaJsonLayout.java index 578937231..c2c13c86f 100644 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaJsonLayout.java +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaJsonLayout.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,12 +11,25 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.logging.internal; +import static java.time.Instant.ofEpochMilli; +import static java.time.format.DateTimeFormatter.ISO_ZONED_DATE_TIME; + import com.fasterxml.jackson.annotation.JsonAnyGetter; import com.fasterxml.jackson.annotation.JsonGetter; import com.fasterxml.jackson.annotation.JsonRootName; import com.fasterxml.jackson.annotation.JsonUnwrapped; +import java.io.IOException; +import java.io.Writer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; import org.apache.logging.log4j.core.Layout; import org.apache.logging.log4j.core.LogEvent; import org.apache.logging.log4j.core.config.Configuration; @@ -30,75 +43,16 @@ import org.apache.logging.log4j.core.util.KeyValuePair; import org.apache.logging.log4j.util.Strings; -import java.io.IOException; -import java.io.Writer; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; - -import static java.time.Instant.ofEpochMilli; -import static java.time.format.DateTimeFormatter.ISO_ZONED_DATE_TIME; - /*** * Note: The LambdaJsonLayout should be considered to be deprecated. Please use JsonTemplateLayout instead. */ @Deprecated @Plugin(name = "LambdaJsonLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true) public final class LambdaJsonLayout extends AbstractJacksonLayoutCopy { + static final String CONTENT_TYPE = "application/json"; private static final String DEFAULT_FOOTER = "]"; - private static final String DEFAULT_HEADER = "["; - static final String CONTENT_TYPE = "application/json"; - - public static class Builder> extends AbstractJacksonLayoutCopy.Builder - implements org.apache.logging.log4j.core.util.Builder { - - @PluginBuilderAttribute - private boolean propertiesAsList; - - @PluginBuilderAttribute - private boolean objectMessageAsJsonObject; - - public Builder() { - super(); - setCharset(StandardCharsets.UTF_8); - } - - @Override - public LambdaJsonLayout build() { - final boolean encodeThreadContextAsList = isProperties() && propertiesAsList; - final String headerPattern = toStringOrNull(getHeader()); - final String footerPattern = toStringOrNull(getFooter()); - return new LambdaJsonLayout(getConfiguration(), isLocationInfo(), isProperties(), encodeThreadContextAsList, - isComplete(), isCompact(), getEventEol(), headerPattern, footerPattern, getCharset(), - isIncludeStacktrace(), isStacktraceAsString(), isIncludeNullDelimiter(), - getAdditionalFields(), getObjectMessageAsJsonObject()); - } - - public boolean isPropertiesAsList() { - return propertiesAsList; - } - - public B setPropertiesAsList(final boolean propertiesAsList) { - this.propertiesAsList = propertiesAsList; - return asBuilder(); - } - - public boolean getObjectMessageAsJsonObject() { - return objectMessageAsJsonObject; - } - - public B setObjectMessageAsJsonObject(final boolean objectMessageAsJsonObject) { - this.objectMessageAsJsonObject = objectMessageAsJsonObject; - return asBuilder(); - } - } - private LambdaJsonLayout(final Configuration config, final boolean locationInfo, final boolean properties, final boolean encodeThreadContextAsList, final boolean complete, final boolean compact, final boolean eventEol, @@ -106,16 +60,34 @@ private LambdaJsonLayout(final Configuration config, final boolean locationInfo, final boolean includeStacktrace, final boolean stacktraceAsString, final boolean includeNullDelimiter, final KeyValuePair[] additionalFields, final boolean objectMessageAsJsonObject) { - super(config, new JacksonFactoryCopy.JSON(encodeThreadContextAsList, includeStacktrace, stacktraceAsString, objectMessageAsJsonObject).newWriter( - locationInfo, properties, compact), + super(config, new JacksonFactoryCopy.JSON(encodeThreadContextAsList, includeStacktrace, stacktraceAsString, + objectMessageAsJsonObject).newWriter( + locationInfo, properties, compact), charset, compact, complete, eventEol, null, - PatternLayout.newSerializerBuilder().setConfiguration(config).setPattern(headerPattern).setDefaultPattern(DEFAULT_HEADER).build(), - PatternLayout.newSerializerBuilder().setConfiguration(config).setPattern(footerPattern).setDefaultPattern(DEFAULT_FOOTER).build(), + PatternLayout.newSerializerBuilder().setConfiguration(config).setPattern(headerPattern) + .setDefaultPattern(DEFAULT_HEADER).build(), + PatternLayout.newSerializerBuilder().setConfiguration(config).setPattern(footerPattern) + .setDefaultPattern(DEFAULT_FOOTER).build(), includeNullDelimiter, additionalFields); } + @PluginBuilderFactory + public static > B newBuilder() { + return new Builder().asBuilder(); + } + + /** + * Creates a JSON Layout using the default settings. Useful for testing. + * + * @return A JSON Layout. + */ + public static LambdaJsonLayout createDefaultLayout() { + return new LambdaJsonLayout(new DefaultConfiguration(), false, false, false, false, false, false, + DEFAULT_HEADER, DEFAULT_FOOTER, StandardCharsets.UTF_8, true, false, false, null, false); + } + /** * Returns appropriate JSON header. * @@ -170,21 +142,6 @@ public String getContentType() { return CONTENT_TYPE + "; charset=" + this.getCharset(); } - @PluginBuilderFactory - public static > B newBuilder() { - return new Builder().asBuilder(); - } - - /** - * Creates a JSON Layout using the default settings. Useful for testing. - * - * @return A JSON Layout. - */ - public static LambdaJsonLayout createDefaultLayout() { - return new LambdaJsonLayout(new DefaultConfiguration(), false, false, false, false, false, false, - DEFAULT_HEADER, DEFAULT_FOOTER, StandardCharsets.UTF_8, true, false, false, null, false); - } - @Override public Object wrapLogEvent(final LogEvent event) { Map additionalFieldsMap = resolveAdditionalFields(event); @@ -205,15 +162,60 @@ private Map resolveAdditionalFields(LogEvent logEvent) { final Map additionalFieldsMap = new LinkedHashMap<>(additionalFields.length); // Go over MDC - logEvent.getContextData().forEach((key, value) -> { - if (Strings.isNotBlank(key) && value != null) { - additionalFieldsMap.put(key, value); - } - }); + logEvent.getContextData().forEach((key, value) -> + { + if (Strings.isNotBlank(key) && value != null) { + additionalFieldsMap.put(key, value); + } + }); return additionalFieldsMap; } + public static class Builder> extends AbstractJacksonLayoutCopy.Builder + implements org.apache.logging.log4j.core.util.Builder { + + @PluginBuilderAttribute + private boolean propertiesAsList; + + @PluginBuilderAttribute + private boolean objectMessageAsJsonObject; + + public Builder() { + super(); + setCharset(StandardCharsets.UTF_8); + } + + @Override + public LambdaJsonLayout build() { + final boolean encodeThreadContextAsList = isProperties() && propertiesAsList; + final String headerPattern = toStringOrNull(getHeader()); + final String footerPattern = toStringOrNull(getFooter()); + return new LambdaJsonLayout(getConfiguration(), isLocationInfo(), isProperties(), encodeThreadContextAsList, + isComplete(), isCompact(), getEventEol(), headerPattern, footerPattern, getCharset(), + isIncludeStacktrace(), isStacktraceAsString(), isIncludeNullDelimiter(), + getAdditionalFields(), getObjectMessageAsJsonObject()); + } + + public boolean isPropertiesAsList() { + return propertiesAsList; + } + + public B setPropertiesAsList(final boolean propertiesAsList) { + this.propertiesAsList = propertiesAsList; + return asBuilder(); + } + + public boolean getObjectMessageAsJsonObject() { + return objectMessageAsJsonObject; + } + + public B setObjectMessageAsJsonObject(final boolean objectMessageAsJsonObject) { + this.objectMessageAsJsonObject = objectMessageAsJsonObject; + return asBuilder(); + } + } + @JsonRootName(XmlConstants.ELT_EVENT) public static class LogEventWithAdditionalFields { @@ -237,7 +239,8 @@ public Map getAdditionalFields() { @JsonGetter("timestamp") public String getTimestamp() { - return ISO_ZONED_DATE_TIME.format(ZonedDateTime.from(ofEpochMilli(logEvent.getTimeMillis()).atZone(ZoneId.systemDefault()))); + return ISO_ZONED_DATE_TIME.format( + ZonedDateTime.from(ofEpochMilli(logEvent.getTimeMillis()).atZone(ZoneId.systemDefault()))); } } } diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaLoggingAspect.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaLoggingAspect.java index f522a4711..4a98735af 100644 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaLoggingAspect.java +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaLoggingAspect.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,8 +11,28 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.logging.internal; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Optional.empty; +import static java.util.Optional.ofNullable; +import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.coldStartDone; +import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.extractContext; +import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.getXrayTraceId; +import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.isColdStart; +import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.isHandlerMethod; +import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.placedOnRequestHandler; +import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.placedOnStreamHandler; +import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.serviceName; +import static software.amazon.lambda.powertools.logging.LoggingUtils.appendKey; +import static software.amazon.lambda.powertools.logging.LoggingUtils.appendKeys; +import static software.amazon.lambda.powertools.logging.LoggingUtils.objectMapper; + +import com.amazonaws.services.lambda.runtime.Context; +import com.fasterxml.jackson.core.JsonPointer; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -22,11 +42,6 @@ import java.util.Map; import java.util.Optional; import java.util.Random; - -import com.amazonaws.services.lambda.runtime.Context; -import com.fasterxml.jackson.core.JsonPointer; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -42,21 +57,6 @@ import software.amazon.lambda.powertools.logging.Logging; import software.amazon.lambda.powertools.logging.LoggingUtils; -import static java.nio.charset.StandardCharsets.UTF_8; -import static java.util.Optional.empty; -import static java.util.Optional.ofNullable; -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.coldStartDone; -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.extractContext; -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.getXrayTraceId; -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.isColdStart; -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.isHandlerMethod; -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.placedOnRequestHandler; -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.placedOnStreamHandler; -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.serviceName; -import static software.amazon.lambda.powertools.logging.LoggingUtils.appendKey; -import static software.amazon.lambda.powertools.logging.LoggingUtils.appendKeys; -import static software.amazon.lambda.powertools.logging.LoggingUtils.objectMapper; - @Aspect @DeclarePrecedence("*, software.amazon.lambda.powertools.logging.internal.LambdaLoggingAspect") public final class LambdaLoggingAspect { @@ -76,6 +76,12 @@ public final class LambdaLoggingAspect { LEVEL_AT_INITIALISATION = LOG.getLevel(); } + private static void resetLogLevels(Level logLevel) { + LoggerContext ctx = (LoggerContext) LogManager.getContext(false); + Configurator.setAllLevels(LogManager.getRootLogger().getName(), logLevel); + ctx.updateLoggers(); + } + @SuppressWarnings({"EmptyMethod"}) @Pointcut("@annotation(logging)") public void callAt(Logging logging) { @@ -90,7 +96,7 @@ public Object around(ProceedingJoinPoint pjp, Context extractedContext = extractContext(pjp); - if(null != extractedContext) { + if (null != extractedContext) { appendKeys(DefaultLambdaFields.values(extractedContext)); appendKey("coldStart", isColdStart() ? "true" : "false"); appendKey("service", serviceName()); @@ -108,7 +114,7 @@ public Object around(ProceedingJoinPoint pjp, Object proceed = pjp.proceed(proceedArgs); - if(logging.clearState()) { + if (logging.clearState()) { ThreadContext.clearMap(); } @@ -116,12 +122,6 @@ public Object around(ProceedingJoinPoint pjp, return proceed; } - private static void resetLogLevels(Level logLevel) { - LoggerContext ctx = (LoggerContext) LogManager.getContext(false); - Configurator.setAllLevels(LogManager.getRootLogger().getName(), logLevel); - ctx.updateLoggers(); - } - private void setLogLevelBasedOnSamplingRate(final ProceedingJoinPoint pjp, final Logging logging) { double samplingRate = samplingRate(logging); @@ -129,7 +129,8 @@ private void setLogLevelBasedOnSamplingRate(final ProceedingJoinPoint pjp, if (isHandlerMethod(pjp)) { if (samplingRate < 0 || samplingRate > 1) { - LOG.debug("Skipping sampling rate configuration because of invalid value. Sampling rate: {}", samplingRate); + LOG.debug("Skipping sampling rate configuration because of invalid value. Sampling rate: {}", + samplingRate); return; } diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/PowertoolsResolver.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/PowertoolsResolver.java index c392e2ed9..c7b7c5d53 100644 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/PowertoolsResolver.java +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/PowertoolsResolver.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.internal; import org.apache.logging.log4j.core.LogEvent; @@ -25,12 +39,13 @@ public void resolve(LogEvent logEvent, JsonWriter jsonWriter) { // Inject all the context information. ReadOnlyStringMap contextData = logEvent.getContextData(); - contextData.forEach((key, value) -> { + contextData.forEach((key, value) -> + { jsonWriter.writeSeparator(); jsonWriter.writeString(key); stringBuilder.append(':'); jsonWriter.writeValue(value); - }); + }); } }; } diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/PowertoolsResolverFactory.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/PowertoolsResolverFactory.java index 5683c9688..7d688f469 100644 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/PowertoolsResolverFactory.java +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/PowertoolsResolverFactory.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.internal; import org.apache.logging.log4j.core.LogEvent; @@ -14,7 +28,8 @@ public final class PowertoolsResolverFactory implements EventResolverFactory { private static final PowertoolsResolverFactory INSTANCE = new PowertoolsResolverFactory(); - private PowertoolsResolverFactory() {} + private PowertoolsResolverFactory() { + } @PluginFactory public static PowertoolsResolverFactory getInstance() { diff --git a/powertools-logging/src/test/java/org/apache/logging/log4j/core/layout/LambdaJsonLayoutTest.java b/powertools-logging/src/test/java/org/apache/logging/log4j/core/layout/LambdaJsonLayoutTest.java index 5fc0398d1..47b495da3 100644 --- a/powertools-logging/src/test/java/org/apache/logging/log4j/core/layout/LambdaJsonLayoutTest.java +++ b/powertools-logging/src/test/java/org/apache/logging/log4j/core/layout/LambdaJsonLayoutTest.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,8 +11,20 @@ * limitations under the License. * */ + package org.apache.logging.log4j.core.layout; +import static java.util.Collections.emptyMap; +import static org.apache.commons.lang3.reflect.FieldUtils.writeStaticField; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.openMocks; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -21,11 +33,6 @@ import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.util.Map; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.logging.log4j.Level; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -34,13 +41,6 @@ import software.amazon.lambda.powertools.logging.handlers.PowerLogToolSamplingEnabled; import software.amazon.lambda.powertools.logging.internal.LambdaLoggingAspect; -import static java.util.Collections.emptyMap; -import static org.apache.commons.lang3.reflect.FieldUtils.writeStaticField; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; -import static org.mockito.Mockito.when; -import static org.mockito.MockitoAnnotations.openMocks; - class LambdaJsonLayoutTest { private RequestHandler handler = new PowerLogToolEnabled(); @@ -74,22 +74,24 @@ void shouldLogInStructuredFormat() throws IOException { } @Test - void shouldModifyLogLevelBasedOnEnvVariable() throws IllegalAccessException, IOException, NoSuchMethodException, InvocationTargetException { + void shouldModifyLogLevelBasedOnEnvVariable() + throws IllegalAccessException, IOException, NoSuchMethodException, InvocationTargetException { resetLogLevel(Level.DEBUG); handler.handleRequest("test", context); assertThat(Files.lines(Paths.get("target/logfile.json"))) .hasSize(2) - .satisfies(line -> { - assertThat(parseToMap(line.get(0))) - .containsEntry("level", "INFO") - .containsEntry("message", "Test event"); - - assertThat(parseToMap(line.get(1))) - .containsEntry("level", "DEBUG") - .containsEntry("message", "Test debug event"); - }); + .satisfies(line -> + { + assertThat(parseToMap(line.get(0))) + .containsEntry("level", "INFO") + .containsEntry("message", "Test event"); + + assertThat(parseToMap(line.get(1))) + .containsEntry("level", "DEBUG") + .containsEntry("message", "Test debug event"); + }); } @Test @@ -100,22 +102,24 @@ void shouldModifyLogLevelBasedOnSamplingRule() throws IOException { assertThat(Files.lines(Paths.get("target/logfile.json"))) .hasSize(3) - .satisfies(line -> { - assertThat(parseToMap(line.get(0))) - .containsEntry("level", "DEBUG") - .containsEntry("loggerName", LambdaLoggingAspect.class.getCanonicalName()); - - assertThat(parseToMap(line.get(1))) - .containsEntry("level", "INFO") - .containsEntry("message", "Test event"); - - assertThat(parseToMap(line.get(2))) - .containsEntry("level", "DEBUG") - .containsEntry("message", "Test debug event"); - }); + .satisfies(line -> + { + assertThat(parseToMap(line.get(0))) + .containsEntry("level", "DEBUG") + .containsEntry("loggerName", LambdaLoggingAspect.class.getCanonicalName()); + + assertThat(parseToMap(line.get(1))) + .containsEntry("level", "INFO") + .containsEntry("message", "Test event"); + + assertThat(parseToMap(line.get(2))) + .containsEntry("level", "DEBUG") + .containsEntry("message", "Test debug event"); + }); } - private void resetLogLevel(Level level) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { + private void resetLogLevel(Level level) + throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { Method resetLogLevels = LambdaLoggingAspect.class.getDeclaredMethod("resetLogLevels", Level.class); resetLogLevels.setAccessible(true); resetLogLevels.invoke(null, level); diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/LoggingUtilsTest.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/LoggingUtilsTest.java index 91fea4c7a..8889fb93c 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/LoggingUtilsTest.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/LoggingUtilsTest.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,17 +11,17 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.logging; +import static org.assertj.core.api.Assertions.assertThat; + import java.util.HashMap; import java.util.Map; - import org.apache.logging.log4j.ThreadContext; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import static org.assertj.core.api.Assertions.assertThat; - class LoggingUtilsTest { diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolApiGatewayHttpApiCorrelationId.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolApiGatewayHttpApiCorrelationId.java index 4e40e0f97..54d87d5cb 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolApiGatewayHttpApiCorrelationId.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolApiGatewayHttpApiCorrelationId.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,8 +11,11 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.logging.handlers; +import static software.amazon.lambda.powertools.logging.CorrelationIdPathConstants.API_GATEWAY_HTTP; + import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; @@ -20,8 +23,6 @@ import org.apache.logging.log4j.Logger; import software.amazon.lambda.powertools.logging.Logging; -import static software.amazon.lambda.powertools.logging.CorrelationIdPathConstants.API_GATEWAY_HTTP; - public class PowerLogToolApiGatewayHttpApiCorrelationId implements RequestHandler { private final Logger LOG = LogManager.getLogger(PowerLogToolApiGatewayHttpApiCorrelationId.class); diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolApiGatewayRestApiCorrelationId.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolApiGatewayRestApiCorrelationId.java index e3cadaf84..2b6e5a8d4 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolApiGatewayRestApiCorrelationId.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolApiGatewayRestApiCorrelationId.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,8 +11,11 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.logging.handlers; +import static software.amazon.lambda.powertools.logging.CorrelationIdPathConstants.API_GATEWAY_REST; + import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; @@ -20,8 +23,6 @@ import org.apache.logging.log4j.Logger; import software.amazon.lambda.powertools.logging.Logging; -import static software.amazon.lambda.powertools.logging.CorrelationIdPathConstants.API_GATEWAY_REST; - public class PowerLogToolApiGatewayRestApiCorrelationId implements RequestHandler { private final Logger LOG = LogManager.getLogger(PowerLogToolApiGatewayRestApiCorrelationId.class); diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolEnabled.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolEnabled.java index e154bbcf3..df68ea14f 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolEnabled.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolEnabled.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.handlers; import com.amazonaws.services.lambda.runtime.Context; diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolEnabledForStream.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolEnabledForStream.java index e2c2d66d0..83a370437 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolEnabledForStream.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolEnabledForStream.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,14 +11,14 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.logging.handlers; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestStreamHandler; -import software.amazon.lambda.powertools.logging.Logging; - import java.io.InputStream; import java.io.OutputStream; +import software.amazon.lambda.powertools.logging.Logging; public class PowerLogToolEnabledForStream implements RequestStreamHandler { diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolSamplingEnabled.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolSamplingEnabled.java index 9d3d68e2e..357520395 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolSamplingEnabled.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolSamplingEnabled.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.handlers; import com.amazonaws.services.lambda.runtime.Context; diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolDisabled.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolDisabled.java index 0391a5177..48a2e3b81 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolDisabled.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolDisabled.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.handlers; import com.amazonaws.services.lambda.runtime.Context; diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolDisabledForStream.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolDisabledForStream.java index f0f7f676e..7f93145c7 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolDisabledForStream.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolDisabledForStream.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,13 +11,13 @@ * limitations under the License. * */ -package software.amazon.lambda.powertools.logging.handlers; -import java.io.InputStream; -import java.io.OutputStream; +package software.amazon.lambda.powertools.logging.handlers; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestStreamHandler; +import java.io.InputStream; +import java.io.OutputStream; public class PowerToolDisabledForStream implements RequestStreamHandler { diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolLogEventEnabled.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolLogEventEnabled.java index 152eb284d..8a960fa87 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolLogEventEnabled.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolLogEventEnabled.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.handlers; import com.amazonaws.services.lambda.runtime.Context; diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolLogEventEnabledForStream.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolLogEventEnabledForStream.java index 473042e6c..9de76586f 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolLogEventEnabledForStream.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolLogEventEnabledForStream.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,17 +11,17 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.logging.handlers; 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 java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Map; +import software.amazon.lambda.powertools.logging.Logging; public class PowerToolLogEventEnabledForStream implements RequestStreamHandler { diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolLogEventEnabledWithCustomMapper.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolLogEventEnabledWithCustomMapper.java index d761c9ac0..838de1216 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolLogEventEnabledWithCustomMapper.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolLogEventEnabledWithCustomMapper.java @@ -1,6 +1,18 @@ -package software.amazon.lambda.powertools.logging.handlers; +/* + * 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.io.IOException; +package software.amazon.lambda.powertools.logging.handlers; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; @@ -10,6 +22,7 @@ import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import java.io.IOException; import software.amazon.lambda.powertools.logging.Logging; import software.amazon.lambda.powertools.logging.LoggingUtils; @@ -40,7 +53,8 @@ public S3EventNotificationSerializer(Class t) { } @Override - public void serialize(S3EventNotification o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { + public void serialize(S3EventNotification o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) + throws IOException { jsonGenerator.writeStartObject(); jsonGenerator.writeStringField("eventSource", o.getRecords().get(0).getEventSource()); jsonGenerator.writeEndObject(); diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolAlbCorrelationId.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogAlbCorrelationId.java similarity index 75% rename from powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolAlbCorrelationId.java rename to powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogAlbCorrelationId.java index 125c13e26..a32e3e06e 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolAlbCorrelationId.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogAlbCorrelationId.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,21 +11,20 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.logging.handlers; +import static software.amazon.lambda.powertools.logging.CorrelationIdPathConstants.APPLICATION_LOAD_BALANCER; + import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerRequestEvent; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import software.amazon.lambda.powertools.logging.CorrelationIdPathConstants; import software.amazon.lambda.powertools.logging.Logging; -import static software.amazon.lambda.powertools.logging.CorrelationIdPathConstants.API_GATEWAY_REST; -import static software.amazon.lambda.powertools.logging.CorrelationIdPathConstants.APPLICATION_LOAD_BALANCER; - -public class PowerLogToolAlbCorrelationId implements RequestHandler { - private final Logger LOG = LogManager.getLogger(PowerLogToolAlbCorrelationId.class); +public class PowertoolsLogAlbCorrelationId implements RequestHandler { + private final Logger LOG = LogManager.getLogger(PowertoolsLogAlbCorrelationId.class); @Override @Logging(correlationIdPath = APPLICATION_LOAD_BALANCER) diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolEnabledWithClearState.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEnabledWithClearState.java similarity index 81% rename from powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolEnabledWithClearState.java rename to powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEnabledWithClearState.java index 8fef32c94..f21d9f118 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolEnabledWithClearState.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEnabledWithClearState.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.handlers; import com.amazonaws.services.lambda.runtime.Context; @@ -20,13 +21,14 @@ import software.amazon.lambda.powertools.logging.Logging; import software.amazon.lambda.powertools.logging.LoggingUtils; -public class PowerLogToolEnabledWithClearState implements RequestHandler { - private final Logger LOG = LogManager.getLogger(PowerLogToolEnabledWithClearState.class); +public class PowertoolsLogEnabledWithClearState implements RequestHandler { + private static final Logger LOG = LogManager.getLogger(PowertoolsLogEnabledWithClearState.class); public static int COUNT = 1; + @Override @Logging(clearState = true) public Object handleRequest(Object input, Context context) { - if(COUNT == 1) { + if (COUNT == 1) { LoggingUtils.appendKey("TestKey", "TestValue"); } LOG.info("Test event"); diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEventBridgeCorrelationId.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEventBridgeCorrelationId.java new file mode 100644 index 000000000..53e06cb2e --- /dev/null +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEventBridgeCorrelationId.java @@ -0,0 +1,37 @@ +/* + * 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.handlers; + +import static software.amazon.lambda.powertools.logging.CorrelationIdPathConstants.EVENT_BRIDGE; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestStreamHandler; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import software.amazon.lambda.powertools.logging.Logging; + +public class PowertoolsLogEventBridgeCorrelationId implements RequestStreamHandler { + + private final Logger LOG = LogManager.getLogger(PowertoolsLogEventBridgeCorrelationId.class); + + @Override + @Logging(correlationIdPath = EVENT_BRIDGE) + public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) throws IOException { + LOG.info("Test event"); + } +} \ No newline at end of file diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaLoggingAspectTest.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaLoggingAspectTest.java index e2cb58453..b78710586 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaLoggingAspectTest.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaLoggingAspectTest.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,8 +11,31 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.logging.internal; +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonList; +import static java.util.stream.Collectors.joining; +import static org.apache.commons.lang3.reflect.FieldUtils.writeStaticField; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.openMocks; +import static org.skyscreamer.jsonassert.JSONAssert.assertEquals; +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 com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; +import com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerRequestEvent; +import com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification; +import com.amazonaws.services.lambda.runtime.tests.annotations.Event; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -28,17 +51,6 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import com.amazonaws.services.lambda.runtime.RequestStreamHandler; -import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; -import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; -import com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerRequestEvent; -import com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification; -import com.amazonaws.services.lambda.runtime.tests.annotations.Event; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.ThreadContext; import org.json.JSONException; @@ -49,36 +61,18 @@ import org.mockito.MockedStatic; import software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor; import software.amazon.lambda.powertools.core.internal.SystemWrapper; -import software.amazon.lambda.powertools.logging.handlers.PowerLogToolAlbCorrelationId; import software.amazon.lambda.powertools.logging.handlers.PowerLogToolApiGatewayHttpApiCorrelationId; import software.amazon.lambda.powertools.logging.handlers.PowerLogToolApiGatewayRestApiCorrelationId; import software.amazon.lambda.powertools.logging.handlers.PowerLogToolEnabled; import software.amazon.lambda.powertools.logging.handlers.PowerLogToolEnabledForStream; -import software.amazon.lambda.powertools.logging.handlers.PowerLogToolEnabledWithClearState; import software.amazon.lambda.powertools.logging.handlers.PowerToolDisabled; import software.amazon.lambda.powertools.logging.handlers.PowerToolDisabledForStream; import software.amazon.lambda.powertools.logging.handlers.PowerToolLogEventEnabled; import software.amazon.lambda.powertools.logging.handlers.PowerToolLogEventEnabledForStream; import software.amazon.lambda.powertools.logging.handlers.PowerToolLogEventEnabledWithCustomMapper; - -import static com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification.RequestParametersEntity; -import static com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification.ResponseElementsEntity; -import static com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification.S3BucketEntity; -import static com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification.S3Entity; -import static com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification.S3EventNotificationRecord; -import static com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification.S3ObjectEntity; -import static com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification.UserIdentityEntity; -import static java.util.Collections.emptyMap; -import static java.util.Collections.singletonList; -import static java.util.stream.Collectors.joining; -import static org.apache.commons.lang3.reflect.FieldUtils.writeStaticField; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; -import static org.mockito.Mockito.mockStatic; -import static org.mockito.Mockito.when; -import static org.mockito.MockitoAnnotations.openMocks; -import static org.skyscreamer.jsonassert.JSONAssert.assertEquals; -import static software.amazon.lambda.powertools.core.internal.SystemWrapper.getenv; +import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogAlbCorrelationId; +import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEnabledWithClearState; +import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEventBridgeCorrelationId; class LambdaLoggingAspectTest { @@ -121,7 +115,8 @@ void shouldSetLambdaContextWhenEnabled() { void shouldSetLambdaContextForStreamHandlerWhenEnabled() throws IOException { requestStreamHandler = new PowerLogToolEnabledForStream(); - requestStreamHandler.handleRequest(new ByteArrayInputStream(new byte[]{}), new ByteArrayOutputStream(), context); + requestStreamHandler.handleRequest(new ByteArrayInputStream(new byte[] {}), new ByteArrayOutputStream(), + context); assertThat(ThreadContext.getImmutableContext()) .hasSize(EXPECTED_CONTEXT_SIZE) @@ -136,13 +131,15 @@ void shouldSetLambdaContextForStreamHandlerWhenEnabled() throws IOException { @Test void shouldSetColdStartFlag() throws IOException { - requestStreamHandler.handleRequest(new ByteArrayInputStream(new byte[]{}), new ByteArrayOutputStream(), context); + requestStreamHandler.handleRequest(new ByteArrayInputStream(new byte[] {}), new ByteArrayOutputStream(), + context); assertThat(ThreadContext.getImmutableContext()) .hasSize(EXPECTED_CONTEXT_SIZE) .containsEntry("coldStart", "true"); - requestStreamHandler.handleRequest(new ByteArrayInputStream(new byte[]{}), new ByteArrayOutputStream(), context); + requestStreamHandler.handleRequest(new ByteArrayInputStream(new byte[] {}), new ByteArrayOutputStream(), + context); assertThat(ThreadContext.getImmutableContext()) .hasSize(EXPECTED_CONTEXT_SIZE) @@ -190,7 +187,8 @@ void shouldLogEventForHandler() throws IOException, JSONException { String event = (String) log.get("message"); - String expectEvent = new BufferedReader(new InputStreamReader(this.getClass().getResourceAsStream("/s3EventNotification.json"))) + String expectEvent = new BufferedReader( + new InputStreamReader(this.getClass().getResourceAsStream("/s3EventNotification.json"))) .lines().collect(joining("\n")); assertEquals(expectEvent, event, false); @@ -207,7 +205,8 @@ void shouldLogEventForHandlerWithOverriddenObjectMapper() throws IOException, JS String event = (String) log.get("message"); - String expectEvent = new BufferedReader(new InputStreamReader(this.getClass().getResourceAsStream("/customizedLogEvent.json"))) + String expectEvent = new BufferedReader( + new InputStreamReader(this.getClass().getResourceAsStream("/customizedLogEvent.json"))) .lines().collect(joining("\n")); assertEquals(expectEvent, event, false); @@ -219,7 +218,8 @@ void shouldLogEventForStreamAndLambdaStreamIsValid() throws IOException, JSONExc ByteArrayOutputStream output = new ByteArrayOutputStream(); S3EventNotification s3EventNotification = s3EventNotification(); - requestStreamHandler.handleRequest(new ByteArrayInputStream(new ObjectMapper().writeValueAsBytes(s3EventNotification)), output, context); + requestStreamHandler.handleRequest( + new ByteArrayInputStream(new ObjectMapper().writeValueAsBytes(s3EventNotification)), output, context); assertThat(new String(output.toByteArray(), StandardCharsets.UTF_8)) .isNotEmpty(); @@ -228,7 +228,8 @@ void shouldLogEventForStreamAndLambdaStreamIsValid() throws IOException, JSONExc String event = (String) log.get("message"); - String expectEvent = new BufferedReader(new InputStreamReader(this.getClass().getResourceAsStream("/s3EventNotification.json"))) + String expectEvent = new BufferedReader( + new InputStreamReader(this.getClass().getResourceAsStream("/s3EventNotification.json"))) .lines().collect(joining("\n")); assertEquals(expectEvent, event, false); @@ -249,7 +250,8 @@ void shouldLogxRayTraceIdEnvVarSet() { String xRayTraceId = "1-5759e988-bd862e3fe1be46a994272793"; try (MockedStatic mocked = mockStatic(SystemWrapper.class)) { - mocked.when(() -> getenv("_X_AMZN_TRACE_ID")).thenReturn("Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1\""); + mocked.when(() -> getenv("_X_AMZN_TRACE_ID")) + .thenReturn("Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1\""); requestHandler.handleRequest(new Object(), context); @@ -284,7 +286,7 @@ void shouldLogCorrelationIdOnAPIGatewayV2HTTPEvent(APIGatewayV2HTTPEvent event) @ParameterizedTest @Event(value = "albEvent.json", type = ApplicationLoadBalancerRequestEvent.class) void shouldLogCorrelationIdOnALBEvent(ApplicationLoadBalancerRequestEvent event) { - RequestHandler handler = new PowerLogToolAlbCorrelationId(); + RequestHandler handler = new PowertoolsLogAlbCorrelationId(); handler.handleRequest(event, context); assertThat(ThreadContext.getImmutableContext()) @@ -292,9 +294,23 @@ void shouldLogCorrelationIdOnALBEvent(ApplicationLoadBalancerRequestEvent event) .containsEntry("correlation_id", event.getHeaders().get("x-amzn-trace-id")); } + @Test + void shouldLogCorrelationIdOnStreamHandler() throws IOException { + RequestStreamHandler handler = new PowertoolsLogEventBridgeCorrelationId(); + String eventId = "3"; + String event = "{\"id\":" + eventId + "}"; // CorrelationIdPathConstants.EVENT_BRIDGE + ByteArrayInputStream inputStream = new ByteArrayInputStream(event.getBytes()); + handler.handleRequest(inputStream, new ByteArrayOutputStream(), context); + + + assertThat(ThreadContext.getImmutableContext()) + .hasSize(EXPECTED_CONTEXT_SIZE + 1) + .containsEntry("correlation_id", eventId); + } + @Test void shouldLogAndClearLogContextOnEachRequest() throws IOException { - requestHandler = new PowerLogToolEnabledWithClearState(); + requestHandler = new PowertoolsLogEnabledWithClearState(); S3EventNotification s3EventNotification = s3EventNotification(); requestHandler.handleRequest(s3EventNotification, context); @@ -320,7 +336,8 @@ private void setupContext() { when(context.getAwsRequestId()).thenReturn("RequestId"); } - private void resetLogLevel(Level level) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { + private void resetLogLevel(Level level) + throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { Method resetLogLevels = LambdaLoggingAspect.class.getDeclaredMethod("resetLogLevels", Level.class); resetLogLevels.setAccessible(true); resetLogLevels.invoke(null, level); @@ -328,25 +345,27 @@ private void resetLogLevel(Level level) throws NoSuchMethodException, IllegalAcc } private S3EventNotification s3EventNotification() { - S3EventNotificationRecord record = new S3EventNotificationRecord("us-west-2", - "ObjectCreated:Put", - "aws:s3", - null, - "2.1", - new RequestParametersEntity("127.0.0.1"), - new ResponseElementsEntity("C3D13FE58DE4C810", "FMyUVURIY8/IgAtTv8xRjskZQpcIZ9KG4V5Wp6S7S/JRWeUWerMUE5JgHvANOjpD"), - new S3Entity("testConfigRule", - new S3BucketEntity("mybucket", - new UserIdentityEntity("A3NL1KOZZKExample"), - "arn:aws:s3:::mybucket"), - new S3ObjectEntity("HappyFace.jpg", - 1024L, - "d41d8cd98f00b204e9800998ecf8427e", - "096fKKXTRTtl3on89fVO.nfljtsv6qko", - "0055AED6DCD90281E5"), - "1.0"), - new UserIdentityEntity("AIDAJDPLRKLG7UEXAMPLE") - ); + S3EventNotification.S3EventNotificationRecord record = + new S3EventNotification.S3EventNotificationRecord("us-west-2", + "ObjectCreated:Put", + "aws:s3", + null, + "2.1", + new S3EventNotification.RequestParametersEntity("127.0.0.1"), + new S3EventNotification.ResponseElementsEntity("C3D13FE58DE4C810", + "FMyUVURIY8/IgAtTv8xRjskZQpcIZ9KG4V5Wp6S7S/JRWeUWerMUE5JgHvANOjpD"), + new S3EventNotification.S3Entity("testConfigRule", + new S3EventNotification.S3BucketEntity("mybucket", + new S3EventNotification.UserIdentityEntity("A3NL1KOZZKExample"), + "arn:aws:s3:::mybucket"), + new S3EventNotification.S3ObjectEntity("HappyFace.jpg", + 1024L, + "d41d8cd98f00b204e9800998ecf8427e", + "096fKKXTRTtl3on89fVO.nfljtsv6qko", + "0055AED6DCD90281E5"), + "1.0"), + new S3EventNotification.UserIdentityEntity("AIDAJDPLRKLG7UEXAMPLE") + ); return new S3EventNotification(singletonList(record)); } diff --git a/powertools-metrics/pom.xml b/powertools-metrics/pom.xml index 8c6bc821e..0681ed000 100644 --- a/powertools-metrics/pom.xml +++ b/powertools-metrics/pom.xml @@ -1,4 +1,18 @@ + + @@ -112,4 +126,12 @@ + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + \ No newline at end of file diff --git a/powertools-metrics/src/main/java/software/amazon/cloudwatchlogs/emf/model/MetricsLoggerHelper.java b/powertools-metrics/src/main/java/software/amazon/cloudwatchlogs/emf/model/MetricsLoggerHelper.java index f59658e9c..e2d886fe5 100644 --- a/powertools-metrics/src/main/java/software/amazon/cloudwatchlogs/emf/model/MetricsLoggerHelper.java +++ b/powertools-metrics/src/main/java/software/amazon/cloudwatchlogs/emf/model/MetricsLoggerHelper.java @@ -1,9 +1,23 @@ -package software.amazon.cloudwatchlogs.emf.model; +/* + * 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.lang.reflect.Field; +package software.amazon.cloudwatchlogs.emf.model; import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; +import java.lang.reflect.Field; + public final class MetricsLoggerHelper { private MetricsLoggerHelper() { } diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/Metrics.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/Metrics.java index 2a4f4d472..fb92c900d 100644 --- a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/Metrics.java +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/Metrics.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.metrics; import java.lang.annotation.ElementType; @@ -44,7 +58,10 @@ @Target(ElementType.METHOD) public @interface Metrics { String namespace() default ""; + String service() default ""; + boolean captureColdStart() default false; + boolean raiseOnEmptyMetrics() default false; } diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsUtils.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsUtils.java index 8705c2da4..1da100f26 100644 --- a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsUtils.java +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsUtils.java @@ -1,9 +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.metrics; +import static java.util.Objects.requireNonNull; +import static java.util.Optional.ofNullable; +import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.getXrayTraceId; +import static software.amazon.lambda.powertools.metrics.internal.LambdaMetricsAspect.REQUEST_ID_PROPERTY; +import static software.amazon.lambda.powertools.metrics.internal.LambdaMetricsAspect.TRACE_ID_PROPERTY; + import java.util.Arrays; import java.util.Optional; import java.util.function.Consumer; - import software.amazon.cloudwatchlogs.emf.config.SystemWrapper; import software.amazon.cloudwatchlogs.emf.environment.EnvironmentProvider; import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; @@ -12,16 +31,10 @@ import software.amazon.cloudwatchlogs.emf.model.MetricsLoggerHelper; import software.amazon.cloudwatchlogs.emf.model.Unit; -import static java.util.Objects.requireNonNull; -import static java.util.Optional.ofNullable; -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.getXrayTraceId; -import static software.amazon.lambda.powertools.metrics.internal.LambdaMetricsAspect.REQUEST_ID_PROPERTY; -import static software.amazon.lambda.powertools.metrics.internal.LambdaMetricsAspect.TRACE_ID_PROPERTY; - /** * A class used to retrieve the instance of the {@code MetricsLogger} used by * {@code Metrics}. - * + *

    * {@see Metrics} */ public final class MetricsUtils { @@ -43,6 +56,7 @@ public static MetricsLogger metricsLogger() { /** * Configure default dimension to be used by logger. * By default, @{@link Metrics} annotation captures configured service as a dimension Service + * * @param dimensionSets Default value of dimensions set for logger */ public static void defaultDimensions(final DimensionSet... dimensionSets) { @@ -52,15 +66,15 @@ public static void defaultDimensions(final DimensionSet... dimensionSets) { /** * Configure default dimension to be used by logger. * By default, @{@link Metrics} annotation captures configured service as a dimension Service + * * @param dimensionSet Default value of dimension set for logger * @deprecated use {@link #defaultDimensions(DimensionSet...)} instead - * */ @Deprecated public static void defaultDimensionSet(final DimensionSet dimensionSet) { requireNonNull(dimensionSet, "Null dimension set not allowed"); - if(dimensionSet.getDimensionKeys().size() > 0) { + if (dimensionSet.getDimensionKeys().size() > 0) { defaultDimensions(dimensionSet); } } @@ -81,10 +95,11 @@ public static void withSingleMetric(final String name, final double value, final Unit unit, final Consumer logger) { - withMetricsLogger(metricsLogger -> { - metricsLogger.putMetric(name, value, unit); - logger.accept(metricsLogger); - }); + withMetricsLogger(metricsLogger -> + { + metricsLogger.putMetric(name, value, unit); + logger.accept(metricsLogger); + }); } /** @@ -92,22 +107,23 @@ public static void withSingleMetric(final String name, * It by default captures function_request_id as property if used together with {@link Metrics} annotation. It will also * capture xray_trace_id as property if tracing is enabled. * - * @param name the name of the metric - * @param value the value of the metric - * @param unit the unit type of the metric + * @param name the name of the metric + * @param value the value of the metric + * @param unit the unit type of the metric * @param namespace the namespace associated with the metric - * @param logger the MetricsLogger + * @param logger the MetricsLogger */ public static void withSingleMetric(final String name, final double value, final Unit unit, final String namespace, final Consumer logger) { - withMetricsLogger(metricsLogger -> { - metricsLogger.setNamespace(namespace); - metricsLogger.putMetric(name, value, unit); - logger.accept(metricsLogger); - }); + withMetricsLogger(metricsLogger -> + { + metricsLogger.setNamespace(namespace); + metricsLogger.putMetric(name, value, unit); + logger.accept(metricsLogger); + }); } /** @@ -137,7 +153,6 @@ public static void withMetricsLogger(final Consumer logger) { * capture xray_trace_id as property if tracing is enabled. * * @param logger the MetricsLogger - * * @deprecated use {@link MetricsUtils#withMetricsLogger} instead */ @Deprecated @@ -164,7 +179,7 @@ private static void captureRequestAndTraceId(MetricsLogger metricsLogger) { private static String defaultNameSpace() { MetricsContext context = MetricsLoggerHelper.metricsContext(); return "aws-embedded-metrics".equals(context.getNamespace()) ? - SystemWrapper.getenv("POWERTOOLS_METRICS_NAMESPACE"): context.getNamespace(); + SystemWrapper.getenv("POWERTOOLS_METRICS_NAMESPACE") : context.getNamespace(); } private static Optional awsRequestId() { diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/ValidationException.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/ValidationException.java index 2da9a539c..a553abbbd 100644 --- a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/ValidationException.java +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/ValidationException.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.metrics; public class ValidationException extends RuntimeException { diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspect.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspect.java index 927359fc5..8ca069b01 100644 --- a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspect.java +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspect.java @@ -1,8 +1,31 @@ +/* + * 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.metrics.internal; -import java.lang.reflect.Field; +import static software.amazon.cloudwatchlogs.emf.model.MetricsLoggerHelper.dimensionsCount; +import static software.amazon.cloudwatchlogs.emf.model.MetricsLoggerHelper.hasNoMetrics; +import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.coldStartDone; +import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.extractContext; +import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.isColdStart; +import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.isHandlerMethod; +import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.serviceName; +import static software.amazon.lambda.powertools.metrics.MetricsUtils.hasDefaultDimension; +import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; import com.amazonaws.services.lambda.runtime.Context; +import java.lang.reflect.Field; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; @@ -16,21 +39,33 @@ import software.amazon.lambda.powertools.metrics.MetricsUtils; import software.amazon.lambda.powertools.metrics.ValidationException; -import static software.amazon.cloudwatchlogs.emf.model.MetricsLoggerHelper.dimensionsCount; -import static software.amazon.cloudwatchlogs.emf.model.MetricsLoggerHelper.hasNoMetrics; -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.coldStartDone; -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.extractContext; -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.isColdStart; -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.isHandlerMethod; -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.serviceName; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.hasDefaultDimension; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; - @Aspect public class LambdaMetricsAspect { - private static final String NAMESPACE = System.getenv("POWERTOOLS_METRICS_NAMESPACE"); public static final String TRACE_ID_PROPERTY = "xray_trace_id"; public static final String REQUEST_ID_PROPERTY = "function_request_id"; + private static final String NAMESPACE = System.getenv("POWERTOOLS_METRICS_NAMESPACE"); + + private static String service(Metrics metrics) { + return !"".equals(metrics.service()) ? metrics.service() : serviceName(); + } + + // This can be simplified after this issues https://github.com/awslabs/aws-embedded-metrics-java/issues/35 is fixed + public static void refreshMetricsContext(Metrics metrics) { + try { + Field f = metricsLogger().getClass().getDeclaredField("context"); + f.setAccessible(true); + MetricsContext context = new MetricsContext(); + + DimensionSet[] defaultDimensions = hasDefaultDimension() ? MetricsUtils.getDefaultDimensions() + : new DimensionSet[] {DimensionSet.of("Service", service(metrics))}; + + context.setDimensions(defaultDimensions); + + f.set(metricsLogger(), context); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } @SuppressWarnings({"EmptyMethod"}) @Pointcut("@annotation(metrics)") @@ -52,8 +87,9 @@ public Object around(ProceedingJoinPoint pjp, Context extractedContext = extractContext(pjp); - if( null != extractedContext) { - coldStartSingleMetricIfApplicable(extractedContext.getAwsRequestId(), extractedContext.getFunctionName(), metrics); + if (null != extractedContext) { + coldStartSingleMetricIfApplicable(extractedContext.getAwsRequestId(), + extractedContext.getFunctionName(), metrics); logger.putProperty(REQUEST_ID_PROPERTY, extractedContext.getAwsRequestId()); } @@ -79,12 +115,12 @@ private void coldStartSingleMetricIfApplicable(final String awsRequestId, final Metrics metrics) { if (metrics.captureColdStart() && isColdStart()) { - MetricsLogger metricsLogger = new MetricsLogger(); - metricsLogger.setNamespace(namespace(metrics)); - metricsLogger.putMetric("ColdStart", 1, Unit.COUNT); - metricsLogger.setDimensions(DimensionSet.of("Service", service(metrics), "FunctionName", functionName)); - metricsLogger.putProperty(REQUEST_ID_PROPERTY, awsRequestId); - metricsLogger.flush(); + MetricsLogger metricsLogger = new MetricsLogger(); + metricsLogger.setNamespace(namespace(metrics)); + metricsLogger.putMetric("ColdStart", 1, Unit.COUNT); + metricsLogger.setDimensions(DimensionSet.of("Service", service(metrics), "FunctionName", functionName)); + metricsLogger.putProperty(REQUEST_ID_PROPERTY, awsRequestId); + metricsLogger.flush(); } } @@ -104,34 +140,12 @@ private String namespace(Metrics metrics) { return !"".equals(metrics.namespace()) ? metrics.namespace() : NAMESPACE; } - private static String service(Metrics metrics) { - return !"".equals(metrics.service()) ? metrics.service() : serviceName(); - } - private void validateMetricsAndRefreshOnFailure(Metrics metrics) { try { validateBeforeFlushingMetrics(metrics); - } catch (ValidationException e){ + } catch (ValidationException e) { refreshMetricsContext(metrics); throw e; } } - - // This can be simplified after this issues https://github.com/awslabs/aws-embedded-metrics-java/issues/35 is fixed - public static void refreshMetricsContext(Metrics metrics) { - try { - Field f = metricsLogger().getClass().getDeclaredField("context"); - f.setAccessible(true); - MetricsContext context = new MetricsContext(); - - DimensionSet[] defaultDimensions = hasDefaultDimension() ? MetricsUtils.getDefaultDimensions() - : new DimensionSet[]{DimensionSet.of("Service", service(metrics))}; - - context.setDimensions(defaultDimensions); - - f.set(metricsLogger(), context); - } catch (NoSuchFieldException | IllegalAccessException e) { - throw new RuntimeException(e); - } - } } diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerTest.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerTest.java index 6ebf30e04..da4162ea0 100644 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerTest.java +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerTest.java @@ -1,12 +1,31 @@ +/* + * 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.metrics; +import static java.util.Collections.emptyMap; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; +import static org.mockito.Mockito.mockStatic; +import static software.amazon.lambda.powertools.core.internal.SystemWrapper.getenv; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import java.io.ByteArrayOutputStream; import java.io.PrintStream; import java.util.Map; import java.util.function.Consumer; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -17,18 +36,19 @@ import software.amazon.cloudwatchlogs.emf.model.DimensionSet; import software.amazon.cloudwatchlogs.emf.model.Unit; -import static java.util.Collections.emptyMap; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatNullPointerException; -import static org.mockito.Mockito.mockStatic; -import static software.amazon.lambda.powertools.core.internal.SystemWrapper.getenv; - class MetricsLoggerTest { private final ByteArrayOutputStream out = new ByteArrayOutputStream(); private final PrintStream originalOut = System.out; private final ObjectMapper mapper = new ObjectMapper(); + @BeforeAll + static void beforeAll() { + try (MockedStatic mocked = mockStatic(SystemWrapper.class)) { + mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda"); + } + } + @BeforeEach void setUp() { System.setOut(new PrintStream(out)); @@ -39,88 +59,92 @@ void tearDown() { System.setOut(originalOut); } - @BeforeAll - static void beforeAll() { - try (MockedStatic mocked = mockStatic(SystemWrapper.class)) { - mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda"); - } - } - @Test void singleMetricsCaptureUtilityWithDefaultDimension() { try (MockedStatic mocked = mockStatic(SystemWrapper.class); - MockedStatic internalWrapper = mockStatic(software.amazon.lambda.powertools.core.internal.SystemWrapper.class)) { + MockedStatic internalWrapper = mockStatic( + software.amazon.lambda.powertools.core.internal.SystemWrapper.class)) { mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda"); - internalWrapper.when(() -> getenv("_X_AMZN_TRACE_ID")).thenReturn("Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1\""); + internalWrapper.when(() -> getenv("_X_AMZN_TRACE_ID")) + .thenReturn("Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1\""); MetricsUtils.defaultDimensions(DimensionSet.of("Service", "Booking")); MetricsUtils.withSingleMetric("Metric1", 1, Unit.COUNT, "test", - metricsLogger -> {}); + metricsLogger -> + { + }); assertThat(out.toString()) - .satisfies(s -> { - Map logAsJson = readAsJson(s); - - assertThat(logAsJson) - .containsEntry("Metric1", 1.0) - .containsEntry("Service", "Booking") - .containsKey("_aws") - .containsEntry("xray_trace_id", "1-5759e988-bd862e3fe1be46a994272793"); - }); + .satisfies(s -> + { + Map logAsJson = readAsJson(s); + + assertThat(logAsJson) + .containsEntry("Metric1", 1.0) + .containsEntry("Service", "Booking") + .containsKey("_aws") + .containsEntry("xray_trace_id", "1-5759e988-bd862e3fe1be46a994272793"); + }); } } @Test void singleMetricsCaptureUtility() { try (MockedStatic mocked = mockStatic(SystemWrapper.class); - MockedStatic internalWrapper = mockStatic(software.amazon.lambda.powertools.core.internal.SystemWrapper.class)) { + MockedStatic internalWrapper = mockStatic( + software.amazon.lambda.powertools.core.internal.SystemWrapper.class)) { mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda"); - internalWrapper.when(() -> getenv("_X_AMZN_TRACE_ID")).thenReturn("Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1\""); + internalWrapper.when(() -> getenv("_X_AMZN_TRACE_ID")) + .thenReturn("Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1\""); MetricsUtils.withSingleMetric("Metric1", 1, Unit.COUNT, "test", metricsLogger -> metricsLogger.setDimensions(DimensionSet.of("Dimension1", "Value1"))); assertThat(out.toString()) - .satisfies(s -> { - Map logAsJson = readAsJson(s); - - assertThat(logAsJson) - .containsEntry("Metric1", 1.0) - .containsEntry("Dimension1", "Value1") - .containsKey("_aws") - .containsEntry("xray_trace_id", "1-5759e988-bd862e3fe1be46a994272793"); - }); + .satisfies(s -> + { + Map logAsJson = readAsJson(s); + + assertThat(logAsJson) + .containsEntry("Metric1", 1.0) + .containsEntry("Dimension1", "Value1") + .containsKey("_aws") + .containsEntry("xray_trace_id", "1-5759e988-bd862e3fe1be46a994272793"); + }); } } @Test void singleMetricsCaptureUtilityWithDefaultNameSpace() { try (MockedStatic mocked = mockStatic(SystemWrapper.class); - MockedStatic internalWrapper = mockStatic(software.amazon.lambda.powertools.core.internal.SystemWrapper.class)) { + MockedStatic internalWrapper = mockStatic( + software.amazon.lambda.powertools.core.internal.SystemWrapper.class)) { mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda"); mocked.when(() -> SystemWrapper.getenv("POWERTOOLS_METRICS_NAMESPACE")).thenReturn("GlobalName"); - internalWrapper.when(() -> getenv("_X_AMZN_TRACE_ID")).thenReturn("Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1\""); + internalWrapper.when(() -> getenv("_X_AMZN_TRACE_ID")) + .thenReturn("Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1\""); MetricsUtils.withSingleMetric("Metric1", 1, Unit.COUNT, metricsLogger -> metricsLogger.setDimensions(DimensionSet.of("Dimension1", "Value1"))); assertThat(out.toString()) - .satisfies(s -> { - Map logAsJson = readAsJson(s); - - assertThat(logAsJson) - .containsEntry("Metric1", 1.0) - .containsEntry("Dimension1", "Value1") - .containsKey("_aws") - .containsEntry("xray_trace_id", "1-5759e988-bd862e3fe1be46a994272793"); - - Map aws = (Map) logAsJson.get("_aws"); - - assertThat(aws.get("CloudWatchMetrics")) - .asString() - .contains("Namespace=GlobalName"); - }); + .satisfies(s -> + { + Map logAsJson = readAsJson(s); + + assertThat(logAsJson) + .containsEntry("Metric1", 1.0) + .containsEntry("Dimension1", "Value1") + .containsKey("_aws") + .containsEntry("xray_trace_id", "1-5759e988-bd862e3fe1be46a994272793"); + + Map aws = (Map) logAsJson.get("_aws"); + + assertThat(aws.get("CloudWatchMetrics")) + .asString() + .contains("Namespace=GlobalName"); + }); } } @@ -143,32 +167,36 @@ void shouldThrowExceptionWhenDefaultDimensionIsNull() { private void testLogger(Consumer> methodToTest) { try (MockedStatic mocked = mockStatic(SystemWrapper.class); - MockedStatic internalWrapper = mockStatic(software.amazon.lambda.powertools.core.internal.SystemWrapper.class)) { + MockedStatic internalWrapper = mockStatic( + software.amazon.lambda.powertools.core.internal.SystemWrapper.class)) { mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda"); mocked.when(() -> SystemWrapper.getenv("POWERTOOLS_METRICS_NAMESPACE")).thenReturn("GlobalName"); - internalWrapper.when(() -> getenv("_X_AMZN_TRACE_ID")).thenReturn("Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1\""); + internalWrapper.when(() -> getenv("_X_AMZN_TRACE_ID")) + .thenReturn("Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1\""); - methodToTest.accept(metricsLogger -> { - metricsLogger.setDimensions(DimensionSet.of("Dimension1", "Value1")); - metricsLogger.putMetric("Metric1", 1, Unit.COUNT); - }); + methodToTest.accept(metricsLogger -> + { + metricsLogger.setDimensions(DimensionSet.of("Dimension1", "Value1")); + metricsLogger.putMetric("Metric1", 1, Unit.COUNT); + }); assertThat(out.toString()) - .satisfies(s -> { - Map logAsJson = readAsJson(s); - - assertThat(logAsJson) - .containsEntry("Metric1", 1.0) - .containsEntry("Dimension1", "Value1") - .containsKey("_aws") - .containsEntry("xray_trace_id", "1-5759e988-bd862e3fe1be46a994272793"); - - Map aws = (Map) logAsJson.get("_aws"); - - assertThat(aws.get("CloudWatchMetrics")) - .asString() - .contains("Namespace=GlobalName"); - }); + .satisfies(s -> + { + Map logAsJson = readAsJson(s); + + assertThat(logAsJson) + .containsEntry("Metric1", 1.0) + .containsEntry("Dimension1", "Value1") + .containsKey("_aws") + .containsEntry("xray_trace_id", "1-5759e988-bd862e3fe1be46a994272793"); + + Map aws = (Map) logAsJson.get("_aws"); + + assertThat(aws.get("CloudWatchMetrics")) + .asString() + .contains("Namespace=GlobalName"); + }); } } diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsColdStartEnabledHandler.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsColdStartEnabledHandler.java index a722bd689..e3a0fa22e 100644 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsColdStartEnabledHandler.java +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsColdStartEnabledHandler.java @@ -1,13 +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 software.amazon.lambda.powertools.metrics.handlers; +import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; + import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; import software.amazon.cloudwatchlogs.emf.model.Unit; import software.amazon.lambda.powertools.metrics.Metrics; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; - public class PowertoolsMetricsColdStartEnabledHandler implements RequestHandler { @Override diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledDefaultDimensionHandler.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledDefaultDimensionHandler.java index f66269546..5d7fb7120 100644 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledDefaultDimensionHandler.java +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledDefaultDimensionHandler.java @@ -1,5 +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 software.amazon.lambda.powertools.metrics.handlers; +import static software.amazon.lambda.powertools.metrics.MetricsUtils.defaultDimensions; +import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; +import static software.amazon.lambda.powertools.metrics.MetricsUtils.withSingleMetric; + import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; @@ -7,10 +25,6 @@ import software.amazon.cloudwatchlogs.emf.model.Unit; import software.amazon.lambda.powertools.metrics.Metrics; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.defaultDimensions; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.withSingleMetric; - public class PowertoolsMetricsEnabledDefaultDimensionHandler implements RequestHandler { static { @@ -23,7 +37,9 @@ public Object handleRequest(Object input, Context context) { MetricsLogger metricsLogger = metricsLogger(); metricsLogger.putMetric("Metric1", 1, Unit.BYTES); - withSingleMetric("Metric2", 1, Unit.COUNT, log -> {}); + withSingleMetric("Metric2", 1, Unit.COUNT, log -> + { + }); return null; } diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledDefaultNoDimensionHandler.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledDefaultNoDimensionHandler.java index 761362f43..0a0079b80 100644 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledDefaultNoDimensionHandler.java +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledDefaultNoDimensionHandler.java @@ -1,5 +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.metrics.handlers; +import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; +import static software.amazon.lambda.powertools.metrics.MetricsUtils.withSingleMetric; + import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; @@ -7,9 +24,6 @@ import software.amazon.lambda.powertools.metrics.Metrics; import software.amazon.lambda.powertools.metrics.MetricsUtils; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.withSingleMetric; - public class PowertoolsMetricsEnabledDefaultNoDimensionHandler implements RequestHandler { static { @@ -22,7 +36,9 @@ public Object handleRequest(Object input, Context context) { MetricsLogger metricsLogger = metricsLogger(); metricsLogger.putMetric("Metric1", 1, Unit.BYTES); - withSingleMetric("Metric2", 1, Unit.COUNT, log -> {}); + withSingleMetric("Metric2", 1, Unit.COUNT, log -> + { + }); return null; } diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledHandler.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledHandler.java index 160109787..7cfee533d 100644 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledHandler.java +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledHandler.java @@ -1,5 +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.metrics.handlers; +import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; +import static software.amazon.lambda.powertools.metrics.MetricsUtils.withSingleMetric; + import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; @@ -7,9 +24,6 @@ import software.amazon.cloudwatchlogs.emf.model.Unit; import software.amazon.lambda.powertools.metrics.Metrics; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.withSingleMetric; - public class PowertoolsMetricsEnabledHandler implements RequestHandler { @Override diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledStreamHandler.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledStreamHandler.java index 2eb877dc3..1600f4a64 100644 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledStreamHandler.java +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledStreamHandler.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.metrics.handlers; -import java.io.InputStream; -import java.io.OutputStream; +import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestStreamHandler; +import java.io.InputStream; +import java.io.OutputStream; import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; import software.amazon.cloudwatchlogs.emf.model.Unit; import software.amazon.lambda.powertools.metrics.Metrics; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; - public class PowertoolsMetricsEnabledStreamHandler implements RequestStreamHandler { @Override diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsExceptionWhenNoMetricsHandler.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsExceptionWhenNoMetricsHandler.java index 8ada044ee..42e0b3ad4 100644 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsExceptionWhenNoMetricsHandler.java +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsExceptionWhenNoMetricsHandler.java @@ -1,12 +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.metrics.handlers; +import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; + import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; import software.amazon.lambda.powertools.metrics.Metrics; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; - public class PowertoolsMetricsExceptionWhenNoMetricsHandler implements RequestHandler { @Override diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsNoDimensionsHandler.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsNoDimensionsHandler.java index 1c4cc3f77..04b02e166 100644 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsNoDimensionsHandler.java +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsNoDimensionsHandler.java @@ -1,13 +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 software.amazon.lambda.powertools.metrics.handlers; +import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; + import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; import software.amazon.cloudwatchlogs.emf.model.DimensionSet; import software.amazon.lambda.powertools.metrics.Metrics; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; - public class PowertoolsMetricsNoDimensionsHandler implements RequestHandler { @Override diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsNoExceptionWhenNoMetricsHandler.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsNoExceptionWhenNoMetricsHandler.java index 3639542f8..c08ce2f86 100644 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsNoExceptionWhenNoMetricsHandler.java +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsNoExceptionWhenNoMetricsHandler.java @@ -1,12 +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.metrics.handlers; +import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; + import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; import software.amazon.lambda.powertools.metrics.Metrics; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; - public class PowertoolsMetricsNoExceptionWhenNoMetricsHandler implements RequestHandler { @Override diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsTooManyDimensionsHandler.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsTooManyDimensionsHandler.java index ccec863f9..bc8a6e949 100644 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsTooManyDimensionsHandler.java +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsTooManyDimensionsHandler.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 software.amazon.lambda.powertools.metrics.handlers; -import java.util.stream.IntStream; +import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; +import java.util.stream.IntStream; import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; import software.amazon.cloudwatchlogs.emf.model.DimensionSet; import software.amazon.lambda.powertools.metrics.Metrics; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; - public class PowertoolsMetricsTooManyDimensionsHandler implements RequestHandler { @Override diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsWithExceptionInHandler.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsWithExceptionInHandler.java index db75d9f95..da9028a70 100644 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsWithExceptionInHandler.java +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsWithExceptionInHandler.java @@ -1,12 +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.metrics.handlers; +import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; + import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; import software.amazon.lambda.powertools.metrics.Metrics; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; - public class PowertoolsMetricsWithExceptionInHandler implements RequestHandler { @Override diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspectTest.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspectTest.java index 6c18d5d7a..44202b8b8 100644 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspectTest.java +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspectTest.java @@ -1,16 +1,38 @@ +/* + * 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.metrics.internal; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.PrintStream; -import java.util.Map; +import static java.util.Collections.emptyMap; +import static org.apache.commons.lang3.reflect.FieldUtils.writeStaticField; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.openMocks; +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 com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.util.Map; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -18,7 +40,6 @@ import org.mockito.Mock; import org.mockito.MockedStatic; import software.amazon.cloudwatchlogs.emf.config.SystemWrapper; -import software.amazon.cloudwatchlogs.emf.model.DimensionSet; import software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor; import software.amazon.lambda.powertools.metrics.MetricsUtils; import software.amazon.lambda.powertools.metrics.ValidationException; @@ -33,22 +54,12 @@ import software.amazon.lambda.powertools.metrics.handlers.PowertoolsMetricsTooManyDimensionsHandler; import software.amazon.lambda.powertools.metrics.handlers.PowertoolsMetricsWithExceptionInHandler; -import static java.util.Collections.emptyMap; -import static org.apache.commons.lang3.reflect.FieldUtils.writeStaticField; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.mockito.Mockito.mockStatic; -import static org.mockito.Mockito.when; -import static org.mockito.MockitoAnnotations.openMocks; -import static software.amazon.lambda.powertools.core.internal.SystemWrapper.getenv; - public class LambdaMetricsAspectTest { - @Mock - private Context context; - private final ByteArrayOutputStream out = new ByteArrayOutputStream(); private final PrintStream originalOut = System.out; private final ObjectMapper mapper = new ObjectMapper(); + @Mock + private Context context; private RequestHandler requestHandler; @@ -75,10 +86,12 @@ void tearDown() { @Test public void metricsWithoutColdStart() { try (MockedStatic mocked = mockStatic(SystemWrapper.class); - MockedStatic internalWrapper = mockStatic(software.amazon.lambda.powertools.core.internal.SystemWrapper.class)) { + MockedStatic internalWrapper = mockStatic( + software.amazon.lambda.powertools.core.internal.SystemWrapper.class)) { mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda"); - internalWrapper.when(() -> getenv("_X_AMZN_TRACE_ID")).thenReturn("Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1\""); + internalWrapper.when(() -> getenv("_X_AMZN_TRACE_ID")) + .thenReturn("Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1\""); MetricsUtils.defaultDimensions(null); requestHandler = new PowertoolsMetricsEnabledHandler(); @@ -86,40 +99,43 @@ public void metricsWithoutColdStart() { assertThat(out.toString().split("\n")) .hasSize(2) - .satisfies(s -> { - Map logAsJson = readAsJson(s[0]); - - assertThat(logAsJson) - .containsEntry("Metric2", 1.0) - .containsEntry("Dimension1", "Value1") - .containsKey("_aws") - .containsEntry("xray_trace_id", "1-5759e988-bd862e3fe1be46a994272793") - .containsEntry("function_request_id", "123ABC"); - - Map aws = (Map) logAsJson.get("_aws"); - - assertThat(aws.get("CloudWatchMetrics")) - .asString() - .contains("Namespace=ExampleApplication"); - - logAsJson = readAsJson(s[1]); - - assertThat(logAsJson) - .containsEntry("Metric1", 1.0) - .containsEntry("Service", "booking") - .containsEntry("function_request_id", "123ABC") - .containsKey("_aws"); - }); + .satisfies(s -> + { + Map logAsJson = readAsJson(s[0]); + + assertThat(logAsJson) + .containsEntry("Metric2", 1.0) + .containsEntry("Dimension1", "Value1") + .containsKey("_aws") + .containsEntry("xray_trace_id", "1-5759e988-bd862e3fe1be46a994272793") + .containsEntry("function_request_id", "123ABC"); + + Map aws = (Map) logAsJson.get("_aws"); + + assertThat(aws.get("CloudWatchMetrics")) + .asString() + .contains("Namespace=ExampleApplication"); + + logAsJson = readAsJson(s[1]); + + assertThat(logAsJson) + .containsEntry("Metric1", 1.0) + .containsEntry("Service", "booking") + .containsEntry("function_request_id", "123ABC") + .containsKey("_aws"); + }); } } @Test public void metricsWithDefaultDimensionSpecified() { try (MockedStatic mocked = mockStatic(SystemWrapper.class); - MockedStatic internalWrapper = mockStatic(software.amazon.lambda.powertools.core.internal.SystemWrapper.class)) { + MockedStatic internalWrapper = mockStatic( + software.amazon.lambda.powertools.core.internal.SystemWrapper.class)) { mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda"); - internalWrapper.when(() -> getenv("_X_AMZN_TRACE_ID")).thenReturn("Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1\""); + internalWrapper.when(() -> getenv("_X_AMZN_TRACE_ID")) + .thenReturn("Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1\""); requestHandler = new PowertoolsMetricsEnabledDefaultDimensionHandler(); @@ -127,40 +143,43 @@ public void metricsWithDefaultDimensionSpecified() { assertThat(out.toString().split("\n")) .hasSize(2) - .satisfies(s -> { - Map logAsJson = readAsJson(s[0]); - - assertThat(logAsJson) - .containsEntry("Metric2", 1.0) - .containsEntry("CustomDimension", "booking") - .containsKey("_aws") - .containsEntry("xray_trace_id", "1-5759e988-bd862e3fe1be46a994272793") - .containsEntry("function_request_id", "123ABC"); - - Map aws = (Map) logAsJson.get("_aws"); - - assertThat(aws.get("CloudWatchMetrics")) - .asString() - .contains("Namespace=ExampleApplication"); - - logAsJson = readAsJson(s[1]); - - assertThat(logAsJson) - .containsEntry("Metric1", 1.0) - .containsEntry("CustomDimension", "booking") - .containsEntry("function_request_id", "123ABC") - .containsKey("_aws"); - }); + .satisfies(s -> + { + Map logAsJson = readAsJson(s[0]); + + assertThat(logAsJson) + .containsEntry("Metric2", 1.0) + .containsEntry("CustomDimension", "booking") + .containsKey("_aws") + .containsEntry("xray_trace_id", "1-5759e988-bd862e3fe1be46a994272793") + .containsEntry("function_request_id", "123ABC"); + + Map aws = (Map) logAsJson.get("_aws"); + + assertThat(aws.get("CloudWatchMetrics")) + .asString() + .contains("Namespace=ExampleApplication"); + + logAsJson = readAsJson(s[1]); + + assertThat(logAsJson) + .containsEntry("Metric1", 1.0) + .containsEntry("CustomDimension", "booking") + .containsEntry("function_request_id", "123ABC") + .containsKey("_aws"); + }); } } @Test public void metricsWithDefaultNoDimensionSpecified() { try (MockedStatic mocked = mockStatic(SystemWrapper.class); - MockedStatic internalWrapper = mockStatic(software.amazon.lambda.powertools.core.internal.SystemWrapper.class)) { + MockedStatic internalWrapper = mockStatic( + software.amazon.lambda.powertools.core.internal.SystemWrapper.class)) { mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda"); - internalWrapper.when(() -> getenv("_X_AMZN_TRACE_ID")).thenReturn("Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1\""); + internalWrapper.when(() -> getenv("_X_AMZN_TRACE_ID")) + .thenReturn("Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1\""); requestHandler = new PowertoolsMetricsEnabledDefaultNoDimensionHandler(); @@ -168,28 +187,29 @@ public void metricsWithDefaultNoDimensionSpecified() { assertThat(out.toString().split("\n")) .hasSize(2) - .satisfies(s -> { - Map logAsJson = readAsJson(s[0]); + .satisfies(s -> + { + Map logAsJson = readAsJson(s[0]); - assertThat(logAsJson) - .containsEntry("Metric2", 1.0) - .containsKey("_aws") - .containsEntry("xray_trace_id", "1-5759e988-bd862e3fe1be46a994272793") - .containsEntry("function_request_id", "123ABC"); + assertThat(logAsJson) + .containsEntry("Metric2", 1.0) + .containsKey("_aws") + .containsEntry("xray_trace_id", "1-5759e988-bd862e3fe1be46a994272793") + .containsEntry("function_request_id", "123ABC"); - Map aws = (Map) logAsJson.get("_aws"); + Map aws = (Map) logAsJson.get("_aws"); - assertThat(aws.get("CloudWatchMetrics")) - .asString() - .contains("Namespace=ExampleApplication"); + assertThat(aws.get("CloudWatchMetrics")) + .asString() + .contains("Namespace=ExampleApplication"); - logAsJson = readAsJson(s[1]); + logAsJson = readAsJson(s[1]); - assertThat(logAsJson) - .containsEntry("Metric1", 1.0) - .containsEntry("function_request_id", "123ABC") - .containsKey("_aws"); - }); + assertThat(logAsJson) + .containsEntry("Metric1", 1.0) + .containsEntry("function_request_id", "123ABC") + .containsKey("_aws"); + }); } } @@ -207,25 +227,26 @@ public void metricsWithColdStart() { assertThat(out.toString().split("\n")) .hasSize(2) - .satisfies(s -> { - Map logAsJson = readAsJson(s[0]); - - assertThat(logAsJson) - .doesNotContainKey("Metric1") - .containsEntry("ColdStart", 1.0) - .containsEntry("Service", "booking") - .containsEntry("function_request_id", "123ABC") - .containsKey("_aws"); - - logAsJson = readAsJson(s[1]); - - assertThat(logAsJson) - .doesNotContainKey("ColdStart") - .containsEntry("Metric1", 1.0) - .containsEntry("Service", "booking") - .containsEntry("function_request_id", "123ABC") - .containsKey("_aws"); - }); + .satisfies(s -> + { + Map logAsJson = readAsJson(s[0]); + + assertThat(logAsJson) + .doesNotContainKey("Metric1") + .containsEntry("ColdStart", 1.0) + .containsEntry("Service", "booking") + .containsEntry("function_request_id", "123ABC") + .containsKey("_aws"); + + logAsJson = readAsJson(s[1]); + + assertThat(logAsJson) + .doesNotContainKey("ColdStart") + .containsEntry("Metric1", 1.0) + .containsEntry("Service", "booking") + .containsEntry("function_request_id", "123ABC") + .containsKey("_aws"); + }); } } @@ -243,34 +264,35 @@ public void noColdStartMetricsWhenColdStartDone() { assertThat(out.toString().split("\n")) .hasSize(3) - .satisfies(s -> { - Map logAsJson = readAsJson(s[0]); - - assertThat(logAsJson) - .doesNotContainKey("Metric1") - .containsEntry("ColdStart", 1.0) - .containsEntry("Service", "booking") - .containsEntry("function_request_id", "123ABC") - .containsKey("_aws"); - - logAsJson = readAsJson(s[1]); - - assertThat(logAsJson) - .doesNotContainKey("ColdStart") - .containsEntry("Metric1", 1.0) - .containsEntry("Service", "booking") - .containsEntry("function_request_id", "123ABC") - .containsKey("_aws"); - - logAsJson = readAsJson(s[2]); - - assertThat(logAsJson) - .doesNotContainKey("ColdStart") - .containsEntry("Metric1", 1.0) - .containsEntry("Service", "booking") - .containsEntry("function_request_id", "123ABC") - .containsKey("_aws"); - }); + .satisfies(s -> + { + Map logAsJson = readAsJson(s[0]); + + assertThat(logAsJson) + .doesNotContainKey("Metric1") + .containsEntry("ColdStart", 1.0) + .containsEntry("Service", "booking") + .containsEntry("function_request_id", "123ABC") + .containsKey("_aws"); + + logAsJson = readAsJson(s[1]); + + assertThat(logAsJson) + .doesNotContainKey("ColdStart") + .containsEntry("Metric1", 1.0) + .containsEntry("Service", "booking") + .containsEntry("function_request_id", "123ABC") + .containsKey("_aws"); + + logAsJson = readAsJson(s[2]); + + assertThat(logAsJson) + .doesNotContainKey("ColdStart") + .containsEntry("Metric1", 1.0) + .containsEntry("Service", "booking") + .containsEntry("function_request_id", "123ABC") + .containsKey("_aws"); + }); } } @@ -283,18 +305,19 @@ public void metricsWithStreamHandler() throws IOException { MetricsUtils.defaultDimensions(null); RequestStreamHandler streamHandler = new PowertoolsMetricsEnabledStreamHandler(); - streamHandler.handleRequest(new ByteArrayInputStream(new byte[]{}), new ByteArrayOutputStream(), context); + streamHandler.handleRequest(new ByteArrayInputStream(new byte[] {}), new ByteArrayOutputStream(), context); assertThat(out.toString()) - .satisfies(s -> { - Map logAsJson = readAsJson(s); - - assertThat(logAsJson) - .containsEntry("Metric1", 1.0) - .containsEntry("Service", "booking") - .containsEntry("function_request_id", "123ABC") - .containsKey("_aws"); - }); + .satisfies(s -> + { + Map logAsJson = readAsJson(s); + + assertThat(logAsJson) + .containsEntry("Metric1", 1.0) + .containsEntry("Service", "booking") + .containsEntry("function_request_id", "123ABC") + .containsKey("_aws"); + }); } } @@ -324,13 +347,14 @@ public void noExceptionWhenNoMetricsEmitted() { requestHandler.handleRequest("input", context); assertThat(out.toString()) - .satisfies(s -> { - Map logAsJson = readAsJson(s); - - assertThat(logAsJson) - .containsEntry("Service", "booking") - .doesNotContainKey("_aws"); - }); + .satisfies(s -> + { + Map logAsJson = readAsJson(s); + + assertThat(logAsJson) + .containsEntry("Service", "booking") + .doesNotContainKey("_aws"); + }); } } @@ -344,13 +368,14 @@ public void allowWhenNoDimensionsSet() { requestHandler.handleRequest("input", context); assertThat(out.toString()) - .satisfies(s -> { - Map logAsJson = readAsJson(s); - assertThat(logAsJson) - .containsEntry("CoolMetric", 1.0) - .containsEntry("function_request_id", "123ABC") - .containsKey("_aws"); - }); + .satisfies(s -> + { + Map logAsJson = readAsJson(s); + assertThat(logAsJson) + .containsEntry("CoolMetric", 1.0) + .containsEntry("function_request_id", "123ABC") + .containsKey("_aws"); + }); } } @@ -383,14 +408,15 @@ public void metricsPublishedEvenHandlerThrowsException() { .withMessage("Whoops, unexpected exception"); assertThat(out.toString()) - .satisfies(s -> { - Map logAsJson = readAsJson(s); - assertThat(logAsJson) - .containsEntry("CoolMetric", 1.0) - .containsEntry("Service", "booking") - .containsEntry("function_request_id", "123ABC") - .containsKey("_aws"); - }); + .satisfies(s -> + { + Map logAsJson = readAsJson(s); + assertThat(logAsJson) + .containsEntry("CoolMetric", 1.0) + .containsEntry("Service", "booking") + .containsEntry("function_request_id", "123ABC") + .containsKey("_aws"); + }); } } diff --git a/powertools-parameters/pom.xml b/powertools-parameters/pom.xml index e339487be..367996e9c 100644 --- a/powertools-parameters/pom.xml +++ b/powertools-parameters/pom.xml @@ -1,4 +1,18 @@ + + @@ -133,4 +147,21 @@ + + + + maven-surefire-plugin + 3.1.2 + + + eu-central-1 + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + diff --git a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/AppConfigProvider.java b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/AppConfigProvider.java index 6e664a82e..0df05f875 100644 --- a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/AppConfigProvider.java +++ b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/AppConfigProvider.java @@ -1,31 +1,40 @@ +/* + * 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.parameters; -import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; +import java.util.HashMap; +import java.util.Map; import software.amazon.awssdk.core.SdkSystemSetting; +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.appconfigdata.AppConfigDataClient; -import software.amazon.awssdk.services.appconfigdata.AppConfigDataClientBuilder; import software.amazon.awssdk.services.appconfigdata.model.GetLatestConfigurationRequest; import software.amazon.awssdk.services.appconfigdata.model.GetLatestConfigurationResponse; import software.amazon.awssdk.services.appconfigdata.model.StartConfigurationSessionRequest; -import software.amazon.awssdk.services.ssm.SsmClient; -import software.amazon.awssdk.services.ssm.SsmClientBuilder; -import software.amazon.lambda.powertools.core.internal.LambdaConstants; +import software.amazon.lambda.powertools.core.internal.UserAgentConfigurator; import software.amazon.lambda.powertools.parameters.cache.CacheManager; import software.amazon.lambda.powertools.parameters.transform.TransformationManager; -import java.util.HashMap; -import java.util.Map; - -import static software.amazon.lambda.powertools.core.internal.LambdaConstants.AWS_LAMBDA_INITIALIZATION_TYPE; - /** * Implements a {@link ParamProvider} on top of the AppConfig service. AppConfig provides * a mechanism to retrieve and update configuration of applications over time. * AppConfig requires the user to create an application, environment, and configuration profile. * The configuration profile's value can then be retrieved, by key name, through this provider. - * + *

    * Because AppConfig is designed to handle rollouts of configuration over time, we must first * establish a session for each key we wish to retrieve, and then poll the session for the latest * value when the user re-requests it. This means we must hold a keyed set of session tokens @@ -34,24 +43,11 @@ * @see Parameters provider documentation * @see AppConfig documentation */ -public class AppConfigProvider extends BaseProvider{ - - private static class EstablishedSession { - private final String nextSessionToken; - private final String lastConfigurationValue; - - private EstablishedSession(String nextSessionToken, String value) { - this.nextSessionToken = nextSessionToken; - this.lastConfigurationValue = value; - } - } +public class AppConfigProvider extends BaseProvider { private final AppConfigDataClient client; - private final String application; - private final String environment; - private final HashMap establishedSessions = new HashMap<>(); AppConfigProvider(CacheManager cacheManager, AppConfigDataClient client, String environment, String application) { @@ -61,6 +57,14 @@ private EstablishedSession(String nextSessionToken, String value) { this.environment = environment; } + /** + * Create a builder that can be used to configure and create a {@link AppConfigProvider}. + * + * @return a new instance of {@link AppConfigProvider.Builder} + */ + public static AppConfigProvider.Builder builder() { + return new AppConfigProvider.Builder(); + } /** * Retrieve the parameter value from the AppConfig parameter store.
    @@ -74,18 +78,18 @@ protected String getValue(String key) { // so that we can the initial token. If we already have a session, we can take // the next request token from there. EstablishedSession establishedSession = establishedSessions.getOrDefault(key, null); - String sessionToken = establishedSession != null? + String sessionToken = establishedSession != null ? establishedSession.nextSessionToken : client.startConfigurationSession(StartConfigurationSessionRequest.builder() - .applicationIdentifier(this.application) - .environmentIdentifier(this.environment) - .configurationProfileIdentifier(key) - .build()) - .initialConfigurationToken(); + .applicationIdentifier(this.application) + .environmentIdentifier(this.environment) + .configurationProfileIdentifier(key) + .build()) + .initialConfigurationToken(); // Get the configuration using the token GetLatestConfigurationResponse response = client.getLatestConfiguration(GetLatestConfigurationRequest.builder() - .configurationToken(sessionToken) + .configurationToken(sessionToken) .build()); // Get the next session token we'll use next time we are asked for this key @@ -94,11 +98,12 @@ protected String getValue(String key) { // Get the value of the key. Note that AppConfig will return null if the value // has not changed since we last asked for it in this session - in this case // we return the value we stashed at last request. - String value = response.configuration() != null? + String value = response.configuration() != null ? response.configuration().asUtf8String() : // if we have a new value, use it - establishedSession != null? - establishedSession.lastConfigurationValue : // if we don't but we have a previous value, use that - null; // otherwise we've got no value + establishedSession != null ? + establishedSession.lastConfigurationValue : + // if we don't but we have a previous value, use that + null; // otherwise we've got no value // Update the cache so we can get the next value later establishedSessions.put(key, new EstablishedSession(nextSessionToken, value)); @@ -109,16 +114,18 @@ protected String getValue(String key) { @Override protected Map getMultipleValues(String path) { // Retrieving multiple values is not supported with the AppConfig provider. - throw new RuntimeException("Retrieving multiple parameter values is not supported with the AWS App Config Provider"); + throw new RuntimeException( + "Retrieving multiple parameter values is not supported with the AWS App Config Provider"); } - /** - * Create a builder that can be used to configure and create a {@link AppConfigProvider}. - * - * @return a new instance of {@link AppConfigProvider.Builder} - */ - public static AppConfigProvider.Builder builder() { - return new AppConfigProvider.Builder(); + private static class EstablishedSession { + private final String nextSessionToken; + private final String lastConfigurationValue; + + private EstablishedSession(String nextSessionToken, String value) { + this.nextSessionToken = nextSessionToken; + this.lastConfigurationValue = value; + } } static class Builder { @@ -146,19 +153,12 @@ public AppConfigProvider build() { // Create a AppConfigDataClient if we haven't been given one if (client == null) { - AppConfigDataClientBuilder appConfigDataClientBuilder = AppConfigDataClient.builder() + client = AppConfigDataClient.builder() .httpClientBuilder(UrlConnectionHttpClient.builder()) - .region(Region.of(System.getenv(SdkSystemSetting.AWS_REGION.environmentVariable()))); - - // 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(LambdaConstants.ON_DEMAND)) { - appConfigDataClientBuilder.credentialsProvider(EnvironmentVariableCredentialsProvider.create()); - } - - client = appConfigDataClientBuilder.build(); + .region(Region.of(System.getenv(SdkSystemSetting.AWS_REGION.environmentVariable()))) + .overrideConfiguration(ClientOverrideConfiguration.builder() + .putAdvancedOption(SdkAdvancedClientOption.USER_AGENT_SUFFIX, UserAgentConfigurator.getUserAgent(PARAMETERS)).build()) + .build(); } AppConfigProvider provider = new AppConfigProvider(cacheManager, client, environment, application); diff --git a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/BaseProvider.java b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/BaseProvider.java index fb539f850..e6481c5da 100644 --- a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/BaseProvider.java +++ b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/BaseProvider.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,8 +11,14 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.parameters; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Map; import software.amazon.awssdk.annotations.NotThreadSafe; import software.amazon.lambda.powertools.parameters.cache.CacheManager; import software.amazon.lambda.powertools.parameters.exception.TransformationException; @@ -20,17 +26,12 @@ import software.amazon.lambda.powertools.parameters.transform.TransformationManager; import software.amazon.lambda.powertools.parameters.transform.Transformer; -import java.time.Clock; -import java.time.Duration; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.Map; - /** * Base class for all parameter providers. */ @NotThreadSafe public abstract class BaseProvider implements ParamProvider { + public static final String PARAMETERS = "parameters"; protected final CacheManager cacheManager; private TransformationManager transformationManager; @@ -106,7 +107,8 @@ public BaseProvider withMaxAge(int maxAge, ChronoUnit unit) { */ public BaseProvider withTransformation(Class transformerClass) { if (transformationManager == null) { - throw new IllegalStateException("Trying to add transformation while no TransformationManager has been provided."); + throw new IllegalStateException( + "Trying to add transformation while no TransformationManager has been provided."); } transformationManager.setTransformer(transformerClass); return this; @@ -126,15 +128,16 @@ public Map getMultiple(String path) { // remove trailing whitespace String pathWithoutTrailingSlash = path.replaceAll("\\/+$", ""); try { - return (Map) cacheManager.getIfNotExpired(pathWithoutTrailingSlash, now()).orElseGet(() -> { - Map params = getMultipleValues(pathWithoutTrailingSlash); + return (Map) cacheManager.getIfNotExpired(pathWithoutTrailingSlash, now()).orElseGet(() -> + { + Map params = getMultipleValues(pathWithoutTrailingSlash); - cacheManager.putInCache(pathWithoutTrailingSlash, params); + cacheManager.putInCache(pathWithoutTrailingSlash, params); - params.forEach((k, v) -> cacheManager.putInCache(pathWithoutTrailingSlash + "/" + k, v)); + params.forEach((k, v) -> cacheManager.putInCache(pathWithoutTrailingSlash + "/" + k, v)); - return params; - }); + return params; + }); } finally { resetToDefaults(); } @@ -148,24 +151,25 @@ public Map getMultiple(String path) { * * @param key key of the parameter * @return the String value of the parameter - * @throws IllegalStateException if a wrong transformer class is provided through {@link #withTransformation(Class)}. Needs to be a {@link BasicTransformer}. - * @throws TransformationException if the transformation could not be done, because of a wrong format or an error during transformation. + * @throws IllegalStateException if a wrong transformer class is provided through {@link #withTransformation(Class)}. Needs to be a {@link BasicTransformer}. + * @throws TransformationException if the transformation could not be done, because of a wrong format or an error during transformation. */ @Override public String get(final String key) { try { - return (String) cacheManager.getIfNotExpired(key, now()).orElseGet(() -> { - String value = getValue(key); + return (String) cacheManager.getIfNotExpired(key, now()).orElseGet(() -> + { + String value = getValue(key); - String transformedValue = value; - if (transformationManager != null && transformationManager.shouldTransform()) { - transformedValue = transformationManager.performBasicTransformation(value); - } + String transformedValue = value; + if (transformationManager != null && transformationManager.shouldTransform()) { + transformedValue = transformationManager.performBasicTransformation(value); + } - cacheManager.putInCache(key, transformedValue); + cacheManager.putInCache(key, transformedValue); - return transformedValue; - }); + return transformedValue; + }); } finally { // in all case, we reset options to default, for next call resetToDefaults(); @@ -181,24 +185,26 @@ public String get(final String key) { * @param key key of the parameter * @param targetClass class of the target Object (after transformation) * @return the Object (T) value of the parameter - * @throws IllegalStateException if no transformation class was provided through {@link #withTransformation(Class)} - * @throws TransformationException if the transformation could not be done, because of a wrong format or an error during transformation. + * @throws IllegalStateException if no transformation class was provided through {@link #withTransformation(Class)} + * @throws TransformationException if the transformation could not be done, because of a wrong format or an error during transformation. */ @Override public T get(final String key, final Class targetClass) { try { - return (T) cacheManager.getIfNotExpired(key, now()).orElseGet(() -> { - String value = getValue(key); + return (T) cacheManager.getIfNotExpired(key, now()).orElseGet(() -> + { + String value = getValue(key); - if (transformationManager == null) { - throw new IllegalStateException("Trying to transform value while no TransformationManager has been provided."); - } - T transformedValue = transformationManager.performComplexTransformation(value, targetClass); + if (transformationManager == null) { + throw new IllegalStateException( + "Trying to transform value while no TransformationManager has been provided."); + } + T transformedValue = transformationManager.performComplexTransformation(value, targetClass); - cacheManager.putInCache(key, transformedValue); + cacheManager.putInCache(key, transformedValue); - return transformedValue; - }); + return transformedValue; + }); } finally { // in all case, we reset options to default, for next call resetToDefaults(); @@ -225,6 +231,7 @@ protected void setTransformationManager(TransformationManager transformationMana /** * For test purpose + * * @param clock */ void setClock(Clock clock) { diff --git a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/DynamoDbProvider.java b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/DynamoDbProvider.java index bf8d763b9..499241927 100644 --- a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/DynamoDbProvider.java +++ b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/DynamoDbProvider.java @@ -1,48 +1,68 @@ +/* + * 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.parameters; -import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; +import java.util.Collections; +import java.util.Map; +import java.util.stream.Collectors; import software.amazon.awssdk.core.SdkSystemSetting; +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.model.*; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; +import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; +import software.amazon.awssdk.services.dynamodb.model.QueryRequest; +import software.amazon.awssdk.services.dynamodb.model.QueryResponse; +import software.amazon.lambda.powertools.core.internal.UserAgentConfigurator; import software.amazon.lambda.powertools.parameters.cache.CacheManager; import software.amazon.lambda.powertools.parameters.exception.DynamoDbProviderSchemaException; import software.amazon.lambda.powertools.parameters.transform.TransformationManager; -import java.util.Collections; -import java.util.Map; -import java.util.stream.Collectors; - /** * Implements a {@link ParamProvider} on top of DynamoDB. The schema of the table * is described in the Powertools for AWS Lambda (Java) documentation. * * @see Parameters provider documentation - * */ public class DynamoDbProvider extends BaseProvider { private final DynamoDbClient client; private final String tableName; - public DynamoDbProvider(CacheManager cacheManager, String tableName) { - this(cacheManager, DynamoDbClient.builder() - .httpClientBuilder(UrlConnectionHttpClient.builder()) - .credentialsProvider(EnvironmentVariableCredentialsProvider.create()) - .region(Region.of(System.getenv(SdkSystemSetting.AWS_REGION.environmentVariable()))) - .build(), - tableName - ); - - } - DynamoDbProvider(CacheManager cacheManager, DynamoDbClient client, String tableName) { super(cacheManager); this.client = client; this.tableName = tableName; } + DynamoDbProvider(CacheManager cacheManager, String tableName) { + this(cacheManager, Builder.createClient(), tableName); + } + + /** + * Create a builder that can be used to configure and create a {@link DynamoDbProvider}. + * + * @return a new instance of {@link DynamoDbProvider.Builder} + */ + public static DynamoDbProvider.Builder builder() { + return new DynamoDbProvider.Builder(); + } + /** * Return a single value from the DynamoDB parameter provider. * @@ -86,9 +106,10 @@ protected Map getMultipleValues(String path) { .build()); return resp - .items() - .stream() - .peek((i) -> { + .items() + .stream() + .peek((i) -> + { if (!i.containsKey("sk")) { throw new DynamoDbProviderSchemaException("Missing 'sk': " + i.toString()); } @@ -96,21 +117,12 @@ protected Map getMultipleValues(String path) { throw new DynamoDbProviderSchemaException("Missing 'value': " + i.toString()); } }) - .collect( - Collectors.toMap( - (i) -> i.get("sk").s(), - (i) -> i.get("value").s())); - + .collect( + Collectors.toMap( + (i) -> i.get("sk").s(), + (i) -> i.get("value").s())); - } - /** - * Create a builder that can be used to configure and create a {@link DynamoDbProvider}. - * - * @return a new instance of {@link DynamoDbProvider.Builder} - */ - public static DynamoDbProvider.Builder builder() { - return new DynamoDbProvider.Builder(); } static class Builder { @@ -119,6 +131,14 @@ static class Builder { private CacheManager cacheManager; private TransformationManager transformationManager; + private static DynamoDbClient createClient() { + return DynamoDbClient.builder() + .httpClientBuilder(UrlConnectionHttpClient.builder()) + .region(Region.of(System.getenv(SdkSystemSetting.AWS_REGION.environmentVariable()))) + .overrideConfiguration(ClientOverrideConfiguration.builder().putAdvancedOption(SdkAdvancedClientOption.USER_AGENT_SUFFIX, UserAgentConfigurator.getUserAgent(PARAMETERS)).build()) + .build(); + } + /** * Create a {@link DynamoDbProvider} instance. * @@ -132,11 +152,11 @@ public DynamoDbProvider build() { throw new IllegalStateException("No DynamoDB table name provided; please provide one"); } DynamoDbProvider provider; - if (client != null) { - provider = new DynamoDbProvider(cacheManager, client, table); - } else { - provider = new DynamoDbProvider(cacheManager, table); + if (client == null) { + client = createClient(); } + provider = new DynamoDbProvider(cacheManager, client, table); + if (transformationManager != null) { provider.setTransformationManager(transformationManager); } diff --git a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/Param.java b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/Param.java index ef3d08b72..7ffb0310c 100644 --- a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/Param.java +++ b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/Param.java @@ -1,11 +1,24 @@ -package software.amazon.lambda.powertools.parameters; +/* + * 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 software.amazon.lambda.powertools.parameters.transform.Transformer; +package software.amazon.lambda.powertools.parameters; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import software.amazon.lambda.powertools.parameters.transform.Transformer; /** * {@code Param} is used to signal that the annotated field should be @@ -25,6 +38,8 @@ @Target(ElementType.FIELD) public @interface Param { String key(); + Class provider() default SSMProvider.class; + Class transformer() default Transformer.class; } diff --git a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/ParamManager.java b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/ParamManager.java index 96cbabd0e..6fee0f114 100644 --- a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/ParamManager.java +++ b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/ParamManager.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,19 +11,18 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.parameters; +import java.lang.reflect.Constructor; +import java.util.concurrent.ConcurrentHashMap; import software.amazon.awssdk.services.appconfigdata.AppConfigDataClient; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; import software.amazon.awssdk.services.ssm.SsmClient; -import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import software.amazon.lambda.powertools.parameters.cache.CacheManager; import software.amazon.lambda.powertools.parameters.transform.TransformationManager; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.util.concurrent.ConcurrentHashMap; - /** * Utility class to retrieve instances of parameter providers. * Each instance is unique (singleton). @@ -38,20 +37,28 @@ public final class ParamManager { /** * Get a concrete implementation of {@link BaseProvider}.
    - * You can specify {@link SecretsProvider}, {@link SSMProvider}, {@link DynamoDbProvider}, or create your + * You can specify {@link SecretsProvider}, {@link SSMProvider} or create your * custom provider by extending {@link BaseProvider} if you need to integrate with a different parameter store. + * * @return a {@link SecretsProvider} + * @deprecated You should not use this method directly but a typed one (getSecretsProvider, getSsmProvider, getDynamoDbProvider, getAppConfigProvider), will be removed in v2 */ + // TODO in v2: remove public access to this and review how we get providers (it was not designed for DDB and AppConfig in mind initially) public static T getProvider(Class providerClass) { if (providerClass == null) { throw new IllegalStateException("providerClass cannot be null."); } + if (providerClass == DynamoDbProvider.class || providerClass == AppConfigProvider.class) { + throw new IllegalArgumentException( + providerClass + " cannot be instantiated like this, additional parameters are required"); + } return (T) providers.computeIfAbsent(providerClass, ParamManager::createProvider); } /** * Get a {@link SecretsProvider} with default {@link SecretsManagerClient}.
    * If you need to customize the region, or other part of the client, use {@link ParamManager#getSecretsProvider(SecretsManagerClient)} instead. + * * @return a {@link SecretsProvider} */ public static SecretsProvider getSecretsProvider() { @@ -61,6 +68,7 @@ public static SecretsProvider getSecretsProvider() { /** * Get a {@link SSMProvider} with default {@link SsmClient}.
    * If you need to customize the region, or other part of the client, use {@link ParamManager#getSsmProvider(SsmClient)} instead. + * * @return a {@link SSMProvider} */ public static SSMProvider getSsmProvider() { @@ -85,6 +93,7 @@ public static DynamoDbProvider getDynamoDbProvider(String tableName) { /** * Get a {@link AppConfigProvider} with default {@link AppConfigDataClient}.
    * If you need to customize the region, or other part of the client, use {@link ParamManager#getAppConfigProvider(AppConfigDataClient, String, String)} instead. + * * @return a {@link AppConfigProvider} */ public static AppConfigProvider getAppConfigProvider(String environment, String application) { @@ -103,6 +112,7 @@ public static AppConfigProvider getAppConfigProvider(String environment, String /** * Get a {@link SecretsProvider} with your custom {@link SecretsManagerClient}.
    * Use this to configure region or other part of the client. Use {@link ParamManager#getSsmProvider()} if you don't need this customization. + * * @return a {@link SecretsProvider} */ public static SecretsProvider getSecretsProvider(SecretsManagerClient client) { @@ -116,6 +126,7 @@ public static SecretsProvider getSecretsProvider(SecretsManagerClient client) { /** * Get a {@link SSMProvider} with your custom {@link SsmClient}.
    * Use this to configure region or other part of the client. Use {@link ParamManager#getSsmProvider()} if you don't need this customization. + * * @return a {@link SSMProvider} */ public static SSMProvider getSsmProvider(SsmClient client) { @@ -129,6 +140,7 @@ public static SSMProvider getSsmProvider(SsmClient client) { /** * Get a {@link DynamoDbProvider} with your custom {@link DynamoDbClient}.
    * Use this to configure region or other part of the client. Use {@link ParamManager#getDynamoDbProvider(String)} )} if you don't need this customization. + * * @return a {@link DynamoDbProvider} */ public static DynamoDbProvider getDynamoDbProvider(DynamoDbClient client, String table) { @@ -139,13 +151,15 @@ public static DynamoDbProvider getDynamoDbProvider(DynamoDbClient client, String .withTransformationManager(transformationManager) .build()); } - + /** * Get a {@link AppConfigProvider} with your custom {@link AppConfigDataClient}.
    * Use this to configure region or other part of the client. Use {@link ParamManager#getAppConfigProvider(String, String)} if you don't need this customization. + * * @return a {@link AppConfigProvider} */ - public static AppConfigProvider getAppConfigProvider(AppConfigDataClient client, String environment, String application) { + public static AppConfigProvider getAppConfigProvider(AppConfigDataClient client, String environment, + String application) { return (AppConfigProvider) providers.computeIfAbsent(AppConfigProvider.class, (k) -> AppConfigProvider.builder() .withClient(client) .withCacheManager(cacheManager) @@ -164,13 +178,14 @@ public static TransformationManager getTransformationManager() { return transformationManager; } - private static T createProvider(Class providerClass) { + static T createProvider(Class providerClass) { try { Constructor constructor = providerClass.getDeclaredConstructor(CacheManager.class); - T provider = constructor.newInstance(cacheManager); + T provider = + constructor.newInstance(cacheManager); // FIXME: avoid reflection here as we may have issues (#1280) provider.setTransformationManager(transformationManager); return provider; - } catch (NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException e) { + } catch (ReflectiveOperationException e) { throw new RuntimeException("Unexpected error occurred. Please raise issue at " + "https://github.com/aws-powertools/powertools-lambda-java/issues", e); } diff --git a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/ParamProvider.java b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/ParamProvider.java index b496ed4f3..ba4232261 100644 --- a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/ParamProvider.java +++ b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/ParamProvider.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.parameters; import java.util.Map; diff --git a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/SSMProvider.java b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/SSMProvider.java index bf36aa717..4cfd8f899 100644 --- a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/SSMProvider.java +++ b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/SSMProvider.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,29 +11,27 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.parameters; import java.time.temporal.ChronoUnit; import java.util.HashMap; import java.util.Map; - -import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; import software.amazon.awssdk.core.SdkSystemSetting; +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.ssm.SsmClient; -import software.amazon.awssdk.services.ssm.SsmClientBuilder; import software.amazon.awssdk.services.ssm.model.GetParameterRequest; import software.amazon.awssdk.services.ssm.model.GetParametersByPathRequest; import software.amazon.awssdk.services.ssm.model.GetParametersByPathResponse; import software.amazon.awssdk.utils.StringUtils; -import software.amazon.lambda.powertools.core.internal.LambdaConstants; +import software.amazon.lambda.powertools.core.internal.UserAgentConfigurator; import software.amazon.lambda.powertools.parameters.cache.CacheManager; import software.amazon.lambda.powertools.parameters.transform.TransformationManager; import software.amazon.lambda.powertools.parameters.transform.Transformer; -import static software.amazon.lambda.powertools.core.internal.LambdaConstants.AWS_LAMBDA_INITIALIZATION_TYPE; - /** * AWS System Manager Parameter Store Provider

    * @@ -75,7 +73,6 @@ public class SSMProvider extends BaseProvider { private final SsmClient client; - private boolean decrypt = false; private boolean recursive = false; @@ -92,6 +89,26 @@ public class SSMProvider extends BaseProvider { this.client = client; } + /** + * Constructor with only a CacheManager
    + *

    + * Used in {@link ParamManager#createProvider(Class)} + * + * @param cacheManager handles the parameter caching + */ + SSMProvider(CacheManager cacheManager) { + this(cacheManager, Builder.createClient()); + } + + /** + * Create a builder that can be used to configure and create a {@link SSMProvider}. + * + * @return a new instance of {@link SSMProvider.Builder} + */ + public static SSMProvider.Builder builder() { + return new SSMProvider.Builder(); + } + /** * Retrieve the parameter value from the AWS System Manager Parameter Store. * @@ -190,19 +207,20 @@ private Map getMultipleBis(String path, String nextToken) { // not using the client.getParametersByPathPaginator() as hardly testable GetParametersByPathResponse res = client.getParametersByPath(request); if (res.hasParameters()) { - res.parameters().forEach(parameter -> { + res.parameters().forEach(parameter -> + { /* Standardize the parameter name The parameter name returned by SSM will contained the full path. However, for readability, we should return only the part after the path. */ - String name = parameter.name(); - if (name.startsWith(path)) { - name = name.replaceFirst(path, ""); - } - name = name.replaceFirst("/", ""); - params.put(name, parameter.value()); - }); + String name = parameter.name(); + if (name.startsWith(path)) { + name = name.replaceFirst(path, ""); + } + name = name.replaceFirst("/", ""); + params.put(name, parameter.value()); + }); } if (!StringUtils.isEmpty(res.nextToken())) { @@ -219,13 +237,9 @@ protected void resetToDefaults() { decrypt = false; } - /** - * Create a builder that can be used to configure and create a {@link SSMProvider}. - * - * @return a new instance of {@link SSMProvider.Builder} - */ - public static SSMProvider.Builder builder() { - return new SSMProvider.Builder(); + // For tests purpose only + SsmClient getClient() { + return client; } static class Builder { @@ -233,6 +247,14 @@ static class Builder { private CacheManager cacheManager; private TransformationManager transformationManager; + private static SsmClient createClient() { + return SsmClient.builder() + .httpClientBuilder(UrlConnectionHttpClient.builder()) + .region(Region.of(System.getenv(SdkSystemSetting.AWS_REGION.environmentVariable()))) + .overrideConfiguration(ClientOverrideConfiguration.builder().putAdvancedOption(SdkAdvancedClientOption.USER_AGENT_SUFFIX, UserAgentConfigurator.getUserAgent(PARAMETERS)).build()) + .build(); + } + /** * Create a {@link SSMProvider} instance. * @@ -244,19 +266,7 @@ public SSMProvider build() { } SSMProvider provider; if (client == null) { - SsmClientBuilder ssmClientBuilder = SsmClient.builder() - .httpClientBuilder(UrlConnectionHttpClient.builder()) - .region(Region.of(System.getenv(SdkSystemSetting.AWS_REGION.environmentVariable()))); - - // 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(LambdaConstants.ON_DEMAND)) { - ssmClientBuilder.credentialsProvider(EnvironmentVariableCredentialsProvider.create()); - } - - client = ssmClientBuilder.build(); + client = createClient(); } provider = new SSMProvider(cacheManager, client); diff --git a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/SecretsProvider.java b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/SecretsProvider.java index 9764564a9..788367ea8 100644 --- a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/SecretsProvider.java +++ b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/SecretsProvider.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,27 +11,26 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.parameters; +import static java.nio.charset.StandardCharsets.UTF_8; + import java.time.temporal.ChronoUnit; import java.util.Base64; import java.util.Map; - -import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; import software.amazon.awssdk.core.SdkSystemSetting; +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.secretsmanager.SecretsManagerClient; -import software.amazon.awssdk.services.secretsmanager.SecretsManagerClientBuilder; import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest; -import software.amazon.lambda.powertools.core.internal.LambdaConstants; +import software.amazon.lambda.powertools.core.internal.UserAgentConfigurator; import software.amazon.lambda.powertools.parameters.cache.CacheManager; import software.amazon.lambda.powertools.parameters.transform.TransformationManager; import software.amazon.lambda.powertools.parameters.transform.Transformer; -import static java.nio.charset.StandardCharsets.UTF_8; -import static software.amazon.lambda.powertools.core.internal.LambdaConstants.AWS_LAMBDA_INITIALIZATION_TYPE; - /** * AWS Secrets Manager Parameter Provider

    * @@ -63,7 +62,7 @@ public class SecretsProvider extends BaseProvider { /** * Constructor with custom {@link SecretsManagerClient}.
    * Use when you need to customize region or any other attribute of the client.

    - * + *

    * Use the {@link Builder} to create an instance of it. * * @param client custom client you would like to use. @@ -73,6 +72,26 @@ public class SecretsProvider extends BaseProvider { this.client = client; } + /** + * Constructor with only a CacheManager
    + *

    + * Used in {@link ParamManager#createProvider(Class)} + * + * @param cacheManager handles the parameter caching + */ + SecretsProvider(CacheManager cacheManager) { + this(cacheManager, Builder.createClient()); + } + + /** + * Create a builder that can be used to configure and create a {@link SecretsProvider}. + * + * @return a new instance of {@link SecretsProvider.Builder} + */ + public static Builder builder() { + return new Builder(); + } + /** * Retrieve the parameter value from the AWS Secrets Manager. * @@ -85,13 +104,14 @@ protected String getValue(String key) { String secretValue = client.getSecretValue(request).secretString(); if (secretValue == null) { - secretValue = new String(Base64.getDecoder().decode(client.getSecretValue(request).secretBinary().asByteArray()), UTF_8); + secretValue = + new String(Base64.getDecoder().decode(client.getSecretValue(request).secretBinary().asByteArray()), + UTF_8); } return secretValue; } /** - * * @throws UnsupportedOperationException as it is not possible to get multiple values simultaneously from Secrets Manager */ @Override @@ -126,13 +146,9 @@ public SecretsProvider withTransformation(Class transform return this; } - /** - * Create a builder that can be used to configure and create a {@link SecretsProvider}. - * - * @return a new instance of {@link SecretsProvider.Builder} - */ - public static Builder builder() { - return new Builder(); + // For test purpose only + SecretsManagerClient getClient() { + return client; } static class Builder { @@ -141,6 +157,14 @@ static class Builder { private CacheManager cacheManager; private TransformationManager transformationManager; + private static SecretsManagerClient createClient() { + return SecretsManagerClient.builder() + .httpClientBuilder(UrlConnectionHttpClient.builder()) + .region(Region.of(System.getenv(SdkSystemSetting.AWS_REGION.environmentVariable()))) + .overrideConfiguration(ClientOverrideConfiguration.builder().putAdvancedOption(SdkAdvancedClientOption.USER_AGENT_SUFFIX, UserAgentConfigurator.getUserAgent(PARAMETERS)).build()) + .build(); + } + /** * Create a {@link SecretsProvider} instance. * @@ -152,19 +176,7 @@ public SecretsProvider build() { } SecretsProvider provider; if (client == null) { - SecretsManagerClientBuilder secretsManagerClientBuilder = SecretsManagerClient.builder() - .httpClientBuilder(UrlConnectionHttpClient.builder()) - .region(Region.of(System.getenv(SdkSystemSetting.AWS_REGION.environmentVariable()))); - - // 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(LambdaConstants.ON_DEMAND)) { - secretsManagerClientBuilder.credentialsProvider(EnvironmentVariableCredentialsProvider.create()); - } - - client = secretsManagerClientBuilder.build(); + client = createClient(); } provider = new SecretsProvider(cacheManager, client); diff --git a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/cache/CacheManager.java b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/cache/CacheManager.java index 687337a96..b868cb642 100644 --- a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/cache/CacheManager.java +++ b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/cache/CacheManager.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,15 +11,16 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.parameters.cache; +import static java.time.temporal.ChronoUnit.SECONDS; + import java.time.Clock; import java.time.Duration; import java.time.Instant; import java.util.Optional; -import static java.time.temporal.ChronoUnit.SECONDS; - public class CacheManager { static final Duration DEFAULT_MAX_AGE_SECS = Duration.of(5, SECONDS); diff --git a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/cache/DataStore.java b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/cache/DataStore.java index 9ad8df12c..737faa353 100644 --- a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/cache/DataStore.java +++ b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/cache/DataStore.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.parameters.cache; import java.time.Instant; @@ -27,21 +28,11 @@ public DataStore() { this.store = new ConcurrentHashMap<>(); } - static class ValueNode { - public final Object value; - public final Instant time; - - public ValueNode(Object value, Instant time){ - this.value = value; - this.time = time; - } - } - - public void put(String key, Object value, Instant time){ + public void put(String key, Object value, Instant time) { store.put(key, new ValueNode(value, time)); } - public void remove(String Key){ + public void remove(String Key) { store.remove(Key); } @@ -51,11 +42,21 @@ public Object get(String key) { } public boolean hasExpired(String key, Instant now) { - boolean hasExpired = !store.containsKey(key) || now.isAfter(store.get(key).time); + boolean hasExpired = !store.containsKey(key) || now.isAfter(store.get(key).time); // Auto-clean if the parameter has expired if (hasExpired) { remove(key); } return hasExpired; } + + static class ValueNode { + public final Object value; + public final Instant time; + + public ValueNode(Object value, Instant time) { + this.value = value; + this.time = time; + } + } } diff --git a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/exception/DynamoDbProviderSchemaException.java b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/exception/DynamoDbProviderSchemaException.java index b7574e81d..77df6e3d3 100644 --- a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/exception/DynamoDbProviderSchemaException.java +++ b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/exception/DynamoDbProviderSchemaException.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.parameters.exception; /** diff --git a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/exception/TransformationException.java b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/exception/TransformationException.java index 7d28d12d1..f071c8a6b 100644 --- a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/exception/TransformationException.java +++ b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/exception/TransformationException.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.parameters.exception; public class TransformationException extends RuntimeException { @@ -19,6 +20,7 @@ public TransformationException(Exception e) { super(e); } - public TransformationException(String message) { super(message); + public TransformationException(String message) { + super(message); } } diff --git a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/internal/LambdaParametersAspect.java b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/internal/LambdaParametersAspect.java index ea4d465cd..081af108d 100644 --- a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/internal/LambdaParametersAspect.java +++ b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/internal/LambdaParametersAspect.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.parameters.internal; import org.aspectj.lang.ProceedingJoinPoint; @@ -5,7 +19,9 @@ import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.FieldSignature; -import software.amazon.lambda.powertools.parameters.*; +import software.amazon.lambda.powertools.parameters.BaseProvider; +import software.amazon.lambda.powertools.parameters.Param; +import software.amazon.lambda.powertools.parameters.ParamManager; @Aspect public class LambdaParametersAspect { @@ -16,17 +32,14 @@ public void getParam(Param paramAnnotation) { @Around("getParam(paramAnnotation)") public Object injectParam(final ProceedingJoinPoint joinPoint, final Param paramAnnotation) { - if(null == paramAnnotation.provider()) { - throw new IllegalArgumentException("provider for Param annotation cannot be null!"); - } BaseProvider provider = ParamManager.getProvider(paramAnnotation.provider()); - if(paramAnnotation.transformer().isInterface()) { + if (paramAnnotation.transformer().isInterface()) { // No transformation return provider.get(paramAnnotation.key()); } else { FieldSignature s = (FieldSignature) joinPoint.getSignature(); - if(String.class.isAssignableFrom(s.getFieldType())) { + if (String.class.isAssignableFrom(s.getFieldType())) { // Basic transformation return provider .withTransformation(paramAnnotation.transformer()) diff --git a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/transform/Base64Transformer.java b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/transform/Base64Transformer.java index 944f4f03c..e8557ebfd 100644 --- a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/transform/Base64Transformer.java +++ b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/transform/Base64Transformer.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,14 +11,14 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.parameters.transform; -import java.util.Base64; +import static java.nio.charset.StandardCharsets.UTF_8; +import java.util.Base64; import software.amazon.lambda.powertools.parameters.exception.TransformationException; -import static java.nio.charset.StandardCharsets.UTF_8; - /** * Transformer that take a base64 encoded string and return a decoded string. */ diff --git a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/transform/BasicTransformer.java b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/transform/BasicTransformer.java index 5251d9f16..92e73d9b0 100644 --- a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/transform/BasicTransformer.java +++ b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/transform/BasicTransformer.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.parameters.transform; import software.amazon.lambda.powertools.parameters.exception.TransformationException; diff --git a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/transform/JsonTransformer.java b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/transform/JsonTransformer.java index d84a1ab3a..0eff58ea8 100644 --- a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/transform/JsonTransformer.java +++ b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/transform/JsonTransformer.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.parameters.transform; import com.fasterxml.jackson.core.JsonProcessingException; diff --git a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/transform/TransformationManager.java b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/transform/TransformationManager.java index 00e6f84a9..d3fbce14f 100644 --- a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/transform/TransformationManager.java +++ b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/transform/TransformationManager.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,11 +11,11 @@ * limitations under the License. * */ -package software.amazon.lambda.powertools.parameters.transform; -import software.amazon.lambda.powertools.parameters.exception.TransformationException; +package software.amazon.lambda.powertools.parameters.transform; import java.lang.reflect.InvocationTargetException; +import software.amazon.lambda.powertools.parameters.exception.TransformationException; /** * Manager in charge of transforming parameter values in another format.
    @@ -50,15 +50,18 @@ public boolean shouldTransform() { */ public String performBasicTransformation(String value) { if (transformer == null) { - throw new IllegalStateException("You cannot perform a transformation without Transformer, use the provider.withTransformation() method to specify it."); + throw new IllegalStateException( + "You cannot perform a transformation without Transformer, use the provider.withTransformation() method to specify it."); } if (!BasicTransformer.class.isAssignableFrom(transformer)) { throw new IllegalStateException("Wrong Transformer for a String, choose a BasicTransformer."); } try { - BasicTransformer basicTransformer = (BasicTransformer) transformer.getDeclaredConstructor().newInstance(null); + BasicTransformer basicTransformer = + (BasicTransformer) transformer.getDeclaredConstructor().newInstance(null); return basicTransformer.applyTransformation(value); - } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { + } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | + InvocationTargetException e) { throw new TransformationException(e); } } @@ -66,19 +69,21 @@ public String performBasicTransformation(String value) { /** * Transform a String in a Java Object. * - * @param value the value to transform + * @param value the value to transform * @param targetClass the type of the target object. * @return the value transformed in an object ot type T. */ public T performComplexTransformation(String value, Class targetClass) { if (transformer == null) { - throw new IllegalStateException("You cannot perform a transformation without Transformer, use the provider.withTransformation() method to specify it."); + throw new IllegalStateException( + "You cannot perform a transformation without Transformer, use the provider.withTransformation() method to specify it."); } try { Transformer complexTransformer = transformer.getDeclaredConstructor().newInstance(null); return complexTransformer.applyTransformation(value, targetClass); - } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { + } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | + InvocationTargetException e) { throw new TransformationException(e); } } diff --git a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/transform/Transformer.java b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/transform/Transformer.java index 3c57b2aa9..d9aea2644 100644 --- a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/transform/Transformer.java +++ b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/transform/Transformer.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.parameters.transform; import software.amazon.lambda.powertools.parameters.exception.TransformationException; @@ -34,7 +35,8 @@ public interface Transformer { /** * Apply a transformation on the input value (String) - * @param value the parameter value to transform + * + * @param value the parameter value to transform * @param targetClass class of the target object * @return a transformed parameter * @throws TransformationException when a transformation error occurs diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/AppConfigProviderTest.java b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/AppConfigProviderTest.java index d72a1f042..f467dca72 100644 --- a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/AppConfigProviderTest.java +++ b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/AppConfigProviderTest.java @@ -1,5 +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.parameters; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatRuntimeException; +import static org.mockito.MockitoAnnotations.openMocks; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -7,7 +26,6 @@ import org.mockito.Mock; import org.mockito.Mockito; import software.amazon.awssdk.core.SdkBytes; -import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; import software.amazon.awssdk.services.appconfigdata.AppConfigDataClient; import software.amazon.awssdk.services.appconfigdata.model.GetLatestConfigurationRequest; import software.amazon.awssdk.services.appconfigdata.model.GetLatestConfigurationResponse; @@ -16,35 +34,26 @@ import software.amazon.lambda.powertools.parameters.cache.CacheManager; import software.amazon.lambda.powertools.parameters.transform.TransformationManager; -import static org.assertj.core.api.Assertions.assertThat; - -import java.time.Duration; -import java.util.Optional; - -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.MockitoAnnotations.openMocks; - public class AppConfigProviderTest { + private final String environmentName = "test"; + private final String applicationName = "fakeApp"; + private final String defaultTestKey = "key1"; + @Mock AppConfigDataClient client; - private AppConfigProvider provider; - @Captor ArgumentCaptor startSessionRequestCaptor; @Captor ArgumentCaptor getLatestConfigurationRequestCaptor; - private final String environmentName = "test"; - - private final String applicationName = "fakeApp"; - - private final String defaultTestKey = "key1"; + private AppConfigProvider provider; @BeforeEach public void init() { openMocks(this); + provider = AppConfigProvider.builder() .withClient(client) .withApplication(applicationName) @@ -69,18 +78,18 @@ public void getValueRetrievesValue() { .build(); // first response returns 'value1' GetLatestConfigurationResponse firstResponse = GetLatestConfigurationResponse.builder() - .nextPollConfigurationToken("token2") - .configuration(SdkBytes.fromUtf8String("value1")) - .build(); + .nextPollConfigurationToken("token2") + .configuration(SdkBytes.fromUtf8String("value1")) + .build(); // Second response returns 'value2' GetLatestConfigurationResponse secondResponse = GetLatestConfigurationResponse.builder() - .nextPollConfigurationToken("token3") - .configuration(SdkBytes.fromUtf8String("value2")) - .build(); + .nextPollConfigurationToken("token3") + .configuration(SdkBytes.fromUtf8String("value2")) + .build(); // Third response returns nothing, which means the provider should yield the previous value again GetLatestConfigurationResponse thirdResponse = GetLatestConfigurationResponse.builder() - .nextPollConfigurationToken("token4") - .build(); + .nextPollConfigurationToken("token4") + .build(); Mockito.when(client.startConfigurationSession(startSessionRequestCaptor.capture())) .thenReturn(firstSession); Mockito.when(client.getLatestConfiguration(getLatestConfigurationRequestCaptor.capture())) @@ -94,13 +103,40 @@ public void getValueRetrievesValue() { // Assert assertThat(returnedValue1).isEqualTo(firstResponse.configuration().asUtf8String()); assertThat(returnedValue2).isEqualTo(secondResponse.configuration().asUtf8String()); - assertThat(returnedValue3).isEqualTo(secondResponse.configuration().asUtf8String()); // Third response is mocked to return null and should re-use previous value + assertThat(returnedValue3).isEqualTo(secondResponse.configuration() + .asUtf8String()); // Third response is mocked to return null and should re-use previous value assertThat(startSessionRequestCaptor.getValue().applicationIdentifier()).isEqualTo(applicationName); assertThat(startSessionRequestCaptor.getValue().environmentIdentifier()).isEqualTo(environmentName); assertThat(startSessionRequestCaptor.getValue().configurationProfileIdentifier()).isEqualTo(defaultTestKey); - assertThat(getLatestConfigurationRequestCaptor.getAllValues().get(0).configurationToken()).isEqualTo(firstSession.initialConfigurationToken()); - assertThat(getLatestConfigurationRequestCaptor.getAllValues().get(1).configurationToken()).isEqualTo(firstResponse.nextPollConfigurationToken()); - assertThat(getLatestConfigurationRequestCaptor.getAllValues().get(2).configurationToken()).isEqualTo(secondResponse.nextPollConfigurationToken()); + assertThat(getLatestConfigurationRequestCaptor.getAllValues().get(0).configurationToken()).isEqualTo( + firstSession.initialConfigurationToken()); + assertThat(getLatestConfigurationRequestCaptor.getAllValues().get(1).configurationToken()).isEqualTo( + firstResponse.nextPollConfigurationToken()); + assertThat(getLatestConfigurationRequestCaptor.getAllValues().get(2).configurationToken()).isEqualTo( + secondResponse.nextPollConfigurationToken()); + } + + @Test + public void getValueNoValueExists() { + + // Arrange + StartConfigurationSessionResponse session = StartConfigurationSessionResponse.builder() + .initialConfigurationToken("token1") + .build(); + GetLatestConfigurationResponse response = GetLatestConfigurationResponse.builder() + .nextPollConfigurationToken("token2") + .build(); + Mockito.when(client.startConfigurationSession(startSessionRequestCaptor.capture())) + .thenReturn(session); + Mockito.when(client.getLatestConfiguration(getLatestConfigurationRequestCaptor.capture())) + .thenReturn(response); + + // Act + String returnedValue = provider.getValue(defaultTestKey); + + + // Assert + assertThat(returnedValue).isEqualTo(null); } /** @@ -138,11 +174,58 @@ public void multipleKeysRetrievalWorks() { // Assert assertThat(firstKeyValue).isEqualTo(param1Response.configuration().asUtf8String()); assertThat(secondKeyValue).isEqualTo(param2Response.configuration().asUtf8String()); - assertThat(startSessionRequestCaptor.getAllValues().get(0).configurationProfileIdentifier()).isEqualTo(param1Key); - assertThat(startSessionRequestCaptor.getAllValues().get(1).configurationProfileIdentifier()).isEqualTo(param2Key); - assertThat(getLatestConfigurationRequestCaptor.getAllValues().get(0).configurationToken()).isEqualTo(param1Session.initialConfigurationToken()); - assertThat(getLatestConfigurationRequestCaptor.getAllValues().get(1).configurationToken()).isEqualTo(param2Session.initialConfigurationToken()); + assertThat(startSessionRequestCaptor.getAllValues().get(0).configurationProfileIdentifier()).isEqualTo( + param1Key); + assertThat(startSessionRequestCaptor.getAllValues().get(1).configurationProfileIdentifier()).isEqualTo( + param2Key); + assertThat(getLatestConfigurationRequestCaptor.getAllValues().get(0).configurationToken()).isEqualTo( + param1Session.initialConfigurationToken()); + assertThat(getLatestConfigurationRequestCaptor.getAllValues().get(1).configurationToken()).isEqualTo( + param2Session.initialConfigurationToken()); } + @Test + public void getMultipleValuesThrowsException() { + + // Act & Assert + assertThatRuntimeException().isThrownBy(() -> provider.getMultipleValues("path")) + .withMessage("Retrieving multiple parameter values is not supported with the AWS App Config Provider"); + } + + @Test + public void testAppConfigProviderBuilderMissingCacheManager_throwsException() { + + // Act & Assert + assertThatIllegalStateException().isThrownBy(() -> AppConfigProvider.builder() + .withEnvironment(environmentName) + .withApplication(applicationName) + .withClient(client) + .build()) + .withMessage("No CacheManager provided; please provide one"); + } + + @Test + public void testAppConfigProviderBuilderMissingEnvironment_throwsException() { + + // Act & Assert + assertThatIllegalStateException().isThrownBy(() -> AppConfigProvider.builder() + .withCacheManager(new CacheManager()) + .withApplication(applicationName) + .withClient(client) + .build()) + .withMessage("No environment provided; please provide one"); + } + + @Test + public void testAppConfigProviderBuilderMissingApplication_throwsException() { + + // Act & Assert + assertThatIllegalStateException().isThrownBy(() -> AppConfigProvider.builder() + .withCacheManager(new CacheManager()) + .withEnvironment(environmentName) + .withClient(client) + .build()) + .withMessage("No application provided; please provide one"); + } } diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/BaseProviderTest.java b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/BaseProviderTest.java index 8dd2d7658..edc671e2c 100644 --- a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/BaseProviderTest.java +++ b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/BaseProviderTest.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,20 +11,8 @@ * limitations under the License. * */ -package software.amazon.lambda.powertools.parameters; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import software.amazon.lambda.powertools.parameters.cache.CacheManager; -import software.amazon.lambda.powertools.parameters.transform.ObjectToDeserialize; -import software.amazon.lambda.powertools.parameters.transform.TransformationManager; -import software.amazon.lambda.powertools.parameters.transform.Transformer; -import java.time.Clock; -import java.time.temporal.ChronoUnit; -import java.util.Base64; -import java.util.HashMap; -import java.util.Map; +package software.amazon.lambda.powertools.parameters; import static java.time.Clock.offset; import static java.time.Duration.of; @@ -36,6 +24,18 @@ import static software.amazon.lambda.powertools.parameters.transform.Transformer.base64; import static software.amazon.lambda.powertools.parameters.transform.Transformer.json; +import java.time.Clock; +import java.time.temporal.ChronoUnit; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import software.amazon.lambda.powertools.parameters.cache.CacheManager; +import software.amazon.lambda.powertools.parameters.transform.ObjectToDeserialize; +import software.amazon.lambda.powertools.parameters.transform.TransformationManager; +import software.amazon.lambda.powertools.parameters.transform.Transformer; + public class BaseProviderTest { Clock clock; @@ -45,33 +45,6 @@ public class BaseProviderTest { boolean getFromStore = false; - class BasicProvider extends BaseProvider { - - public BasicProvider(CacheManager cacheManager) { - super(cacheManager); - } - - private String value = "valueFromStore"; - - public void setValue(String value) { - this.value = value; - } - - @Override - protected String getValue(String key) { - getFromStore = true; - return value; - } - - @Override - protected Map getMultipleValues(String path) { - getFromStore = true; - Map map = new HashMap<>(); - map.put(path, value); - return map; - } - } - @BeforeEach public void setup() { openMocks(this); @@ -224,10 +197,11 @@ public void get_basicTransformation_shouldTransformInString() { public void get_complexTransformation_shouldTransformInObject() { provider.setValue("{\"foo\":\"Foo\", \"bar\":42, \"baz\":123456789}"); - ObjectToDeserialize objectToDeserialize = provider.withTransformation(json).get("foo", ObjectToDeserialize.class); + ObjectToDeserialize objectToDeserialize = + provider.withTransformation(json).get("foo", ObjectToDeserialize.class); assertThat(objectToDeserialize).matches( - o -> o.getFoo().equals("Foo") + o -> o.getFoo().equals("Foo") && o.getBar() == 42 && o.getBaz() == 123456789); } @@ -400,4 +374,31 @@ public void getTwoParams_shouldResetTransformationOptionsInBetween() { assertThat(foob64).isEqualTo("base64encoded"); assertThat(foostr).isEqualTo("string"); } + + class BasicProvider extends BaseProvider { + + private String value = "valueFromStore"; + + public BasicProvider(CacheManager cacheManager) { + super(cacheManager); + } + + public void setValue(String value) { + this.value = value; + } + + @Override + protected String getValue(String key) { + getFromStore = true; + return value; + } + + @Override + protected Map getMultipleValues(String path) { + getFromStore = true; + Map map = new HashMap<>(); + map.put(path, value); + return map; + } + } } diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/DynamoDbProviderE2ETest.java b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/DynamoDbProviderE2ETest.java index c9397676b..18212b45c 100644 --- a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/DynamoDbProviderE2ETest.java +++ b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/DynamoDbProviderE2ETest.java @@ -1,5 +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 software.amazon.lambda.powertools.parameters; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.HashMap; +import java.util.Map; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; @@ -8,16 +26,11 @@ import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; import software.amazon.lambda.powertools.parameters.cache.CacheManager; -import java.util.HashMap; -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; - /** * This class provides simple end-to-end style testing of the DynamoDBProvider class. * It is ignored, for now, as it requires AWS access and that's not yet run as part * of our unit test suite in the cloud. - * + *

    * The test is kept here for 1/ local development and 2/ in preparation for future * E2E tests running in the cloud CI. Once the E2E test structure is merged we * will move this across. @@ -46,8 +59,8 @@ public void TestGetValue() { testItem.put("id", AttributeValue.fromS("test_param")); testItem.put("value", AttributeValue.fromS("the_value_is_hello!")); ddbClient.putItem(PutItemRequest.builder() - .tableName(ParamsTestTable) - .item(testItem) + .tableName(ParamsTestTable) + .item(testItem) .build()); // Act diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/DynamoDbProviderTest.java b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/DynamoDbProviderTest.java index 0e5b734d6..abfc9ab8a 100644 --- a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/DynamoDbProviderTest.java +++ b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/DynamoDbProviderTest.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.parameters; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.MockitoAnnotations.openMocks; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -8,31 +29,27 @@ import org.mockito.Mock; import org.mockito.Mockito; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; -import software.amazon.awssdk.services.dynamodb.model.*; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; +import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; +import software.amazon.awssdk.services.dynamodb.model.QueryRequest; +import software.amazon.awssdk.services.dynamodb.model.QueryResponse; import software.amazon.lambda.powertools.parameters.cache.CacheManager; import software.amazon.lambda.powertools.parameters.exception.DynamoDbProviderSchemaException; - -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.MockitoAnnotations.openMocks; +import software.amazon.lambda.powertools.parameters.transform.TransformationManager; public class DynamoDbProviderTest { + private final String tableName = "ddb-test-table"; @Mock DynamoDbClient client; - + @Mock + TransformationManager transformationManager; @Captor ArgumentCaptor getItemValueCaptor; - @Captor ArgumentCaptor queryRequestCaptor; - - private DynamoDbProvider provider; - private final String tableName = "ddb-test-table"; @BeforeEach public void init() { @@ -67,7 +84,7 @@ public void getValue() { @Test - public void getValueWithoutResultsReturnsNull() { + public void getValueWithNullResultsReturnsNull() { // Arrange Mockito.when(client.getItem(getItemValueCaptor.capture())).thenReturn(GetItemResponse.builder() .item(null) @@ -80,6 +97,20 @@ public void getValueWithoutResultsReturnsNull() { assertThat(value).isEqualTo(null); } + @Test + public void getValueWithoutResultsReturnsNull() { + // Arrange + Mockito.when(client.getItem(getItemValueCaptor.capture())).thenReturn(GetItemResponse.builder() + .item(new HashMap<>()) + .build()); + + // Act + String value = provider.getValue("key"); + + // Assert + assertThat(value).isEqualTo(null); + } + @Test public void getValueWithMalformedRowThrows() { // Arrange @@ -91,9 +122,10 @@ public void getValueWithMalformedRowThrows() { .item(responseData) .build()); // Act - Assertions.assertThrows(DynamoDbProviderSchemaException.class, () -> { - String value = provider.getValue(key); - }); + Assertions.assertThrows(DynamoDbProviderSchemaException.class, () -> + { + provider.getValue(key); + }); } @@ -144,6 +176,26 @@ public void getValuesWithoutResultsReturnsNull() { assertThat(values.size()).isEqualTo(0); } + @Test + public void getMultipleValuesMissingSortKey_throwsException() { + // Arrange + String key = "Key1"; + HashMap item = new HashMap(); + item.put("id", AttributeValue.fromS(key)); + item.put("value", AttributeValue.fromS("somevalue")); + QueryResponse response = QueryResponse.builder() + .items(item) + .build(); + Mockito.when(client.query(queryRequestCaptor.capture())).thenReturn(response); + + // Assert + Assertions.assertThrows(DynamoDbProviderSchemaException.class, () -> + { + // Act + provider.getMultipleValues(key); + }); + } + @Test public void getValuesWithMalformedRowThrows() { // Arrange @@ -158,11 +210,29 @@ public void getValuesWithMalformedRowThrows() { Mockito.when(client.query(queryRequestCaptor.capture())).thenReturn(response); // Assert - Assertions.assertThrows(DynamoDbProviderSchemaException.class, () -> { - // Act - Map values = provider.getMultipleValues(key); - }); + Assertions.assertThrows(DynamoDbProviderSchemaException.class, () -> + { + // Act + provider.getMultipleValues(key); + }); } + @Test + public void testDynamoDBBuilderMissingCacheManager_throwsException() { + + // Act & Assert + assertThatIllegalStateException().isThrownBy(() -> DynamoDbProvider.builder() + .withTable("table") + .build()); + } + + @Test + public void testDynamoDBBuilderMissingTable_throwsException() { + + // Act & Assert + assertThatIllegalStateException().isThrownBy(() -> DynamoDbProvider.builder() + .withCacheManager(new CacheManager()) + .build()); + } } diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/ParamManagerIntegrationTest.java b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/ParamManagerIntegrationTest.java index fca0a9362..d6fbe66f0 100644 --- a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/ParamManagerIntegrationTest.java +++ b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/ParamManagerIntegrationTest.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,8 +11,21 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.parameters; +import static org.apache.commons.lang3.reflect.FieldUtils.writeStaticField; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.openMocks; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import org.assertj.core.data.MapEntry; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -25,18 +38,11 @@ import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest; import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueResponse; import software.amazon.awssdk.services.ssm.SsmClient; -import software.amazon.awssdk.services.ssm.model.*; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import static org.apache.commons.lang3.reflect.FieldUtils.writeStaticField; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; -import static org.mockito.MockitoAnnotations.openMocks; +import software.amazon.awssdk.services.ssm.model.GetParameterRequest; +import software.amazon.awssdk.services.ssm.model.GetParameterResponse; +import software.amazon.awssdk.services.ssm.model.GetParametersByPathRequest; +import software.amazon.awssdk.services.ssm.model.GetParametersByPathResponse; +import software.amazon.awssdk.services.ssm.model.Parameter; public class ParamManagerIntegrationTest { @@ -45,21 +51,16 @@ public class ParamManagerIntegrationTest { @Mock DynamoDbClient ddbClient; - - @Mock - private AppConfigDataClient appConfigDataClient; - @Captor ArgumentCaptor ssmParamCaptor; - @Captor ArgumentCaptor ssmParamByPathCaptor; - @Mock SecretsManagerClient secretsManagerClient; - @Captor ArgumentCaptor secretsCaptor; + @Mock + private AppConfigDataClient appConfigDataClient; @BeforeEach public void setup() throws IllegalAccessException { diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/ParamManagerTest.java b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/ParamManagerTest.java new file mode 100644 index 000000000..b84fcf743 --- /dev/null +++ b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/ParamManagerTest.java @@ -0,0 +1,118 @@ +/* + * 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.parameters; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatRuntimeException; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.junit.jupiter.api.Test; +import software.amazon.lambda.powertools.parameters.cache.CacheManager; +import software.amazon.lambda.powertools.parameters.internal.CustomProvider; +import software.amazon.lambda.powertools.parameters.transform.TransformationManager; + +public class ParamManagerTest { + + @Test + public void testGetCacheManager() { + + // Act + CacheManager cacheManager = ParamManager.getCacheManager(); + + // Assert + assertNotNull(cacheManager); + } + + @Test + public void testGetTransformationManager() { + + // Act + TransformationManager transformationManager = ParamManager.getTransformationManager(); + + // Assert + assertNotNull(transformationManager); + } + + @Test + public void testCreateProvider() { + + // Act + CustomProvider customProvider = ParamManager.createProvider(CustomProvider.class); + + // Assert + assertNotNull(customProvider); + } + + @Test + public void testCreateUninstanciableProvider_throwsException() { + + // Act & Assert + assertThatRuntimeException().isThrownBy(() -> ParamManager.createProvider(BaseProvider.class)); + } + + @Test + public void testGetProviderWithProviderClass() { + + // Act + SecretsProvider secretsProvider = ParamManager.getProvider(SecretsProvider.class); + + // Assert + assertNotNull(secretsProvider); + } + + @Test + public void testGetProviderWithProviderClass_throwsException() { + + // Act & Assert + assertThatIllegalStateException().isThrownBy(() -> ParamManager.getProvider(null)); + } + + @Test + public void testGetSecretsProvider_withoutParameter_shouldCreateDefaultClient() { + + // Act + SecretsProvider secretsProvider = ParamManager.getSecretsProvider(); + + // Assert + assertNotNull(secretsProvider); + assertNotNull(secretsProvider.getClient()); + } + + @Test + public void testGetSSMProvider_withoutParameter_shouldCreateDefaultClient() { + + // Act + SSMProvider ssmProvider = ParamManager.getSsmProvider(); + + // Assert + assertNotNull(ssmProvider); + assertNotNull(ssmProvider.getClient()); + } + + @Test + public void testGetDynamoDBProvider_requireOtherParameters_throwException() { + + // Act & Assert + assertThatIllegalArgumentException().isThrownBy(() -> ParamManager.getProvider(DynamoDbProvider.class)); + } + + @Test + public void testGetAppConfigProvider_requireOtherParameters_throwException() { + + // Act & Assert + assertThatIllegalArgumentException().isThrownBy(() -> ParamManager.getProvider(AppConfigProvider.class)); + } +} diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/SSMProviderTest.java b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/SSMProviderTest.java index da299c38d..6a5aa3e68 100644 --- a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/SSMProviderTest.java +++ b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/SSMProviderTest.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,8 +11,22 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.parameters; +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.openMocks; + +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; import org.assertj.core.data.MapEntry; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -20,23 +34,22 @@ import org.mockito.Captor; import org.mockito.Mock; import software.amazon.awssdk.services.ssm.SsmClient; -import software.amazon.awssdk.services.ssm.model.*; +import software.amazon.awssdk.services.ssm.model.GetParameterRequest; +import software.amazon.awssdk.services.ssm.model.GetParameterResponse; +import software.amazon.awssdk.services.ssm.model.GetParametersByPathRequest; +import software.amazon.awssdk.services.ssm.model.GetParametersByPathResponse; +import software.amazon.awssdk.services.ssm.model.Parameter; import software.amazon.lambda.powertools.parameters.cache.CacheManager; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import static java.util.Arrays.asList; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.*; -import static org.mockito.MockitoAnnotations.openMocks; +import software.amazon.lambda.powertools.parameters.transform.TransformationManager; public class SSMProviderTest { @Mock SsmClient client; + @Mock + TransformationManager transformationManager; + @Captor ArgumentCaptor paramCaptor; @@ -152,7 +165,8 @@ public void getMultipleWithNextToken() { List parameters1 = new ArrayList<>(); parameters1.add(Parameter.builder().name("/prod/app1/key1").value("foo1").build()); parameters1.add(Parameter.builder().name("/prod/app1/key2").value("foo2").build()); - GetParametersByPathResponse response1 = GetParametersByPathResponse.builder().parameters(parameters1).nextToken("123abc").build(); + GetParametersByPathResponse response1 = + GetParametersByPathResponse.builder().parameters(parameters1).nextToken("123abc").build(); List parameters2 = new ArrayList<>(); parameters2.add(Parameter.builder().name("/prod/app1/key3").value("foo3").build()); @@ -171,20 +185,35 @@ public void getMultipleWithNextToken() { GetParametersByPathRequest request1 = requestParams.get(0); GetParametersByPathRequest request2 = requestParams.get(1); - assertThat(asList(request1, request2)).allSatisfy(req -> { - assertThat(req.path()).isEqualTo("/prod/app1"); - assertThat(req.withDecryption()).isFalse(); - assertThat(req.recursive()).isFalse(); - }); + assertThat(asList(request1, request2)).allSatisfy(req -> + { + assertThat(req.path()).isEqualTo("/prod/app1"); + assertThat(req.withDecryption()).isFalse(); + assertThat(req.recursive()).isFalse(); + }); assertThat(request1.nextToken()).isNull(); assertThat(request2.nextToken()).isEqualTo("123abc"); } + @Test + public void testSecretsProviderBuilderMissingCacheManager_throwsException() { + + // Act & Assert + assertThatIllegalStateException().isThrownBy(() -> SSMProvider.builder() + .withClient(client) + .withTransformationManager(transformationManager) + .build()) + .withMessage("No CacheManager provided, please provide one"); + } + private void initMock(String expectedValue) { Parameter parameter = Parameter.builder().value(expectedValue).build(); GetParameterResponse result = GetParameterResponse.builder().parameter(parameter).build(); when(client.getParameter(paramCaptor.capture())).thenReturn(result); + provider.defaultMaxAge(1, ChronoUnit.DAYS); + provider.withMaxAge(2, ChronoUnit.DAYS); + provider.recursive(); } } diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/SecretsProviderTest.java b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/SecretsProviderTest.java index 611f05fa9..f4f2d9bee 100644 --- a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/SecretsProviderTest.java +++ b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/SecretsProviderTest.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,8 +11,16 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.parameters; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatRuntimeException; +import static org.mockito.MockitoAnnotations.openMocks; + +import java.time.temporal.ChronoUnit; +import java.util.Base64; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -24,17 +32,16 @@ import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest; import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueResponse; import software.amazon.lambda.powertools.parameters.cache.CacheManager; - -import java.util.Base64; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.MockitoAnnotations.openMocks; +import software.amazon.lambda.powertools.parameters.transform.TransformationManager; public class SecretsProviderTest { @Mock SecretsManagerClient client; + @Mock + TransformationManager transformationManager; + @Captor ArgumentCaptor paramCaptor; @@ -55,6 +62,8 @@ public void getValue() { String expectedValue = "Value1"; GetSecretValueResponse response = GetSecretValueResponse.builder().secretString(expectedValue).build(); Mockito.when(client.getSecretValue(paramCaptor.capture())).thenReturn(response); + provider.defaultMaxAge(1, ChronoUnit.DAYS); + provider.withMaxAge(2, ChronoUnit.DAYS); String value = provider.getValue(key); @@ -67,7 +76,8 @@ public void getValueBase64() { String key = "Key2"; String expectedValue = "Value2"; byte[] valueb64 = Base64.getEncoder().encode(expectedValue.getBytes()); - GetSecretValueResponse response = GetSecretValueResponse.builder().secretBinary(SdkBytes.fromByteArray(valueb64)).build(); + GetSecretValueResponse response = + GetSecretValueResponse.builder().secretBinary(SdkBytes.fromByteArray(valueb64)).build(); Mockito.when(client.getSecretValue(paramCaptor.capture())).thenReturn(response); String value = provider.getValue(key); @@ -75,4 +85,24 @@ public void getValueBase64() { assertThat(value).isEqualTo(expectedValue); assertThat(paramCaptor.getValue().secretId()).isEqualTo(key); } + + @Test + public void getMultipleValuesThrowsException() { + + // Act & Assert + assertThatRuntimeException().isThrownBy(() -> provider.getMultipleValues("path")) + .withMessage("Impossible to get multiple values from AWS Secrets Manager"); + + } + + @Test + public void testSecretsProviderBuilderMissingCacheManager_throwsException() { + + // Act & Assert + assertThatIllegalStateException().isThrownBy(() -> SecretsProvider.builder() + .withClient(client) + .withTransformationManager(transformationManager) + .build()) + .withMessage("No CacheManager provided, please provide one"); + } } diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/cache/CacheManagerTest.java b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/cache/CacheManagerTest.java index 2464b4278..2bcfcc566 100644 --- a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/cache/CacheManagerTest.java +++ b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/cache/CacheManagerTest.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,19 +11,19 @@ * limitations under the License. * */ -package software.amazon.lambda.powertools.parameters.cache; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import java.time.Clock; -import java.util.Optional; +package software.amazon.lambda.powertools.parameters.cache; import static java.time.Clock.offset; import static java.time.Duration.of; import static java.time.temporal.ChronoUnit.SECONDS; import static org.assertj.core.api.Assertions.assertThat; +import java.time.Clock; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + public class CacheManagerTest { CacheManager manager; diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/cache/DataStoreTest.java b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/cache/DataStoreTest.java index c68992bf1..e86ded9be 100644 --- a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/cache/DataStoreTest.java +++ b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/cache/DataStoreTest.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,19 +11,19 @@ * limitations under the License. * */ -package software.amazon.lambda.powertools.parameters.cache; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import java.time.Clock; -import java.time.Instant; +package software.amazon.lambda.powertools.parameters.cache; import static java.time.Clock.offset; import static java.time.Duration.of; import static java.time.temporal.ChronoUnit.SECONDS; import static org.assertj.core.api.Assertions.assertThat; +import java.time.Clock; +import java.time.Instant; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + public class DataStoreTest { Clock clock; diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/internal/AnotherObject.java b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/internal/AnotherObject.java index b58ad7b3d..5ce2b355f 100644 --- a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/internal/AnotherObject.java +++ b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/internal/AnotherObject.java @@ -1,12 +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 software.amazon.lambda.powertools.parameters.internal; public class AnotherObject { - public AnotherObject() {} - private String another; private int object; + public AnotherObject() { + } + public String getAnother() { return another; } diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/internal/CustomProvider.java b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/internal/CustomProvider.java index e58ef746c..2c9db3712 100644 --- a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/internal/CustomProvider.java +++ b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/internal/CustomProvider.java @@ -1,11 +1,24 @@ -package software.amazon.lambda.powertools.parameters.internal; +/* + * 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 software.amazon.lambda.powertools.parameters.BaseProvider; -import software.amazon.lambda.powertools.parameters.cache.CacheManager; +package software.amazon.lambda.powertools.parameters.internal; import java.util.Base64; import java.util.HashMap; import java.util.Map; +import software.amazon.lambda.powertools.parameters.BaseProvider; +import software.amazon.lambda.powertools.parameters.cache.CacheManager; public class CustomProvider extends BaseProvider { diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/internal/LambdaParametersAspectTest.java b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/internal/LambdaParametersAspectTest.java index 2edbc8b24..d346a1aa4 100644 --- a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/internal/LambdaParametersAspectTest.java +++ b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/internal/LambdaParametersAspectTest.java @@ -1,5 +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 software.amazon.lambda.powertools.parameters.internal; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.openMocks; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; @@ -12,11 +34,6 @@ import software.amazon.lambda.powertools.parameters.transform.JsonTransformer; import software.amazon.lambda.powertools.parameters.transform.ObjectToDeserialize; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.mockito.Mockito.*; -import static org.mockito.MockitoAnnotations.openMocks; - public class LambdaParametersAspectTest { @Mock @@ -72,14 +89,17 @@ public void testWithComplexTransform() { .isInstanceOf(ObjectToDeserialize.class) .matches( o -> o.getFoo().equals("Foo") && - o.getBar() == 42 && - o.getBaz() == 123456789); + o.getBar() == 42 && + o.getBaz() == 123456789); } @Test public void testWithComplexTransformWrongTargetClass_ShouldThrowException() { assertThatExceptionOfType(TransformationException.class) - .isThrownBy(() -> {AnotherObject obj = wrongTransform; }); + .isThrownBy(() -> + { + AnotherObject obj = wrongTransform; + }); } } diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/transform/Base64TransformerTest.java b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/transform/Base64TransformerTest.java index 428b7e0ab..ea713b552 100644 --- a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/transform/Base64TransformerTest.java +++ b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/transform/Base64TransformerTest.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,16 +11,16 @@ * limitations under the License. * */ -package software.amazon.lambda.powertools.parameters.transform; -import org.junit.jupiter.api.Test; -import software.amazon.lambda.powertools.parameters.exception.TransformationException; - -import java.util.Base64; +package software.amazon.lambda.powertools.parameters.transform; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import java.util.Base64; +import org.junit.jupiter.api.Test; +import software.amazon.lambda.powertools.parameters.exception.TransformationException; + public class Base64TransformerTest { @Test diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/transform/JsonTransformerTest.java b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/transform/JsonTransformerTest.java index fe4fae0bb..5cb980cc7 100644 --- a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/transform/JsonTransformerTest.java +++ b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/transform/JsonTransformerTest.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,35 +11,38 @@ * limitations under the License. * */ -package software.amazon.lambda.powertools.parameters.transform; -import org.junit.jupiter.api.Test; -import software.amazon.lambda.powertools.parameters.exception.TransformationException; - -import java.util.Map; +package software.amazon.lambda.powertools.parameters.transform; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.data.MapEntry.entry; +import java.util.Map; +import org.junit.jupiter.api.Test; +import software.amazon.lambda.powertools.parameters.exception.TransformationException; + public class JsonTransformerTest { @Test public void transform_json_shouldTransformInObject() throws TransformationException { JsonTransformer transformation = new JsonTransformer<>(); - ObjectToDeserialize objectToDeserialize = transformation.applyTransformation("{\"foo\":\"Foo\", \"bar\":42, \"baz\":123456789}", ObjectToDeserialize.class); + ObjectToDeserialize objectToDeserialize = + transformation.applyTransformation("{\"foo\":\"Foo\", \"bar\":42, \"baz\":123456789}", + ObjectToDeserialize.class); assertThat(objectToDeserialize).matches( o -> o.getFoo().equals("Foo") - && o.getBar() == 42 - && o.getBaz() == 123456789); + && o.getBar() == 42 + && o.getBaz() == 123456789); } @Test public void transform_json_shouldTransformInHashMap() throws TransformationException { JsonTransformer transformation = new JsonTransformer<>(); - Map map = transformation.applyTransformation("{\"foo\":\"Foo\", \"bar\":42, \"baz\":123456789}", Map.class); + Map map = + transformation.applyTransformation("{\"foo\":\"Foo\", \"bar\":42, \"baz\":123456789}", Map.class); assertThat(map).contains( entry("foo", "Foo"), entry("bar", 42), @@ -51,6 +54,7 @@ public void transform_badJson_shouldThrowException() { JsonTransformer transformation = new JsonTransformer<>(); assertThatExceptionOfType(TransformationException.class) - .isThrownBy(() -> transformation.applyTransformation("{\"fo\":\"Foo\", \"bat\":42, \"bau\":123456789}", ObjectToDeserialize.class)); + .isThrownBy(() -> transformation.applyTransformation("{\"fo\":\"Foo\", \"bat\":42, \"bau\":123456789}", + ObjectToDeserialize.class)); } } diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/transform/ObjectToDeserialize.java b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/transform/ObjectToDeserialize.java index 0e1fd0f5c..8b30858fd 100644 --- a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/transform/ObjectToDeserialize.java +++ b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/transform/ObjectToDeserialize.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,17 +11,18 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.parameters.transform; public class ObjectToDeserialize { - public ObjectToDeserialize() { - } - private String foo; private int bar; private long baz; + public ObjectToDeserialize() { + } + public String getFoo() { return foo; } diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/transform/TransformationManagerTest.java b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/transform/TransformationManagerTest.java index 0fcfa8c51..39e69f9e0 100644 --- a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/transform/TransformationManagerTest.java +++ b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/transform/TransformationManagerTest.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,22 +11,26 @@ * limitations under the License. * */ -package software.amazon.lambda.powertools.parameters.transform; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import java.util.Base64; +package software.amazon.lambda.powertools.parameters.transform; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static software.amazon.lambda.powertools.parameters.transform.Transformer.base64; import static software.amazon.lambda.powertools.parameters.transform.Transformer.json; +import java.util.Base64; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import software.amazon.lambda.powertools.parameters.exception.TransformationException; + public class TransformationManagerTest { TransformationManager manager; + Class basicTransformer = BasicTransformer.class; + @BeforeEach public void setup() { manager = new TransformationManager(); @@ -58,6 +62,14 @@ public void performBasicTransformation_notBasicTransformer_shouldThrowException( .isThrownBy(() -> manager.performBasicTransformation("value")); } + @Test + public void performBasicTransformation_abstractTransformer_throwsTransformationException() { + manager.setTransformer(basicTransformer); + + assertThatExceptionOfType(TransformationException.class) + .isThrownBy(() -> manager.performBasicTransformation("value")); + } + @Test public void performBasicTransformation_shouldPerformTransformation() { manager.setTransformer(base64); @@ -78,8 +90,18 @@ public void performComplexTransformation_noTransformer_shouldThrowException() { public void performComplexTransformation_shouldPerformTransformation() { manager.setTransformer(json); - ObjectToDeserialize object = manager.performComplexTransformation("{\"foo\":\"Foo\", \"bar\":42, \"baz\":123456789}", ObjectToDeserialize.class); + ObjectToDeserialize object = + manager.performComplexTransformation("{\"foo\":\"Foo\", \"bar\":42, \"baz\":123456789}", + ObjectToDeserialize.class); assertThat(object).isNotNull(); } + + @Test + public void performComplexTransformation_throwsTransformationException() { + manager.setTransformer(basicTransformer); + + assertThatExceptionOfType(TransformationException.class) + .isThrownBy(() -> manager.performComplexTransformation("value", ObjectToDeserialize.class)); + } } diff --git a/powertools-serialization/pom.xml b/powertools-serialization/pom.xml index e1fac2b02..c34cddef1 100644 --- a/powertools-serialization/pom.xml +++ b/powertools-serialization/pom.xml @@ -1,4 +1,18 @@ + + @@ -82,6 +96,10 @@ true + + org.apache.maven.plugins + maven-checkstyle-plugin + diff --git a/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/EventDeserializationException.java b/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/EventDeserializationException.java index 2b06c9256..ae97232b0 100644 --- a/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/EventDeserializationException.java +++ b/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/EventDeserializationException.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.utilities; public class EventDeserializationException extends RuntimeException { diff --git a/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/EventDeserializer.java b/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/EventDeserializer.java index 9742299ee..22712e8ce 100644 --- a/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/EventDeserializer.java +++ b/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/EventDeserializer.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,22 +11,36 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.utilities; -import com.amazonaws.services.lambda.runtime.events.*; +import static java.nio.charset.StandardCharsets.UTF_8; +import static software.amazon.lambda.powertools.utilities.jmespath.Base64Function.decode; +import static software.amazon.lambda.powertools.utilities.jmespath.Base64GZipFunction.decompress; + +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; +import com.amazonaws.services.lambda.runtime.events.ActiveMQEvent; +import com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerRequestEvent; +import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent; +import com.amazonaws.services.lambda.runtime.events.CloudWatchLogsEvent; +import com.amazonaws.services.lambda.runtime.events.KafkaEvent; +import com.amazonaws.services.lambda.runtime.events.KinesisAnalyticsFirehoseInputPreprocessingEvent; +import com.amazonaws.services.lambda.runtime.events.KinesisAnalyticsStreamsInputPreprocessingEvent; +import com.amazonaws.services.lambda.runtime.events.KinesisEvent; +import com.amazonaws.services.lambda.runtime.events.KinesisFirehoseEvent; +import com.amazonaws.services.lambda.runtime.events.RabbitMQEvent; +import com.amazonaws.services.lambda.runtime.events.SNSEvent; +import com.amazonaws.services.lambda.runtime.events.SQSEvent; +import com.amazonaws.services.lambda.runtime.events.ScheduledEvent; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectReader; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.IOException; import java.util.List; import java.util.Map; import java.util.stream.Collectors; - -import static java.nio.charset.StandardCharsets.UTF_8; -import static software.amazon.lambda.powertools.utilities.jmespath.Base64Function.decode; -import static software.amazon.lambda.powertools.utilities.jmespath.Base64GZipFunction.decompress; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Class that can be used to extract the meaningful part of an event and deserialize it into a Java object.
    @@ -122,12 +136,14 @@ public static EventPart extractDataFrom(Object object) { .map(r -> decode(r.getData())) .collect(Collectors.toList())); } else if (object instanceof KinesisAnalyticsFirehoseInputPreprocessingEvent) { - KinesisAnalyticsFirehoseInputPreprocessingEvent event = (KinesisAnalyticsFirehoseInputPreprocessingEvent) object; + KinesisAnalyticsFirehoseInputPreprocessingEvent event = + (KinesisAnalyticsFirehoseInputPreprocessingEvent) object; return new EventPart(event.getRecords().stream() .map(r -> decode(r.getData())) .collect(Collectors.toList())); } else if (object instanceof KinesisAnalyticsStreamsInputPreprocessingEvent) { - KinesisAnalyticsStreamsInputPreprocessingEvent event = (KinesisAnalyticsStreamsInputPreprocessingEvent) object; + KinesisAnalyticsStreamsInputPreprocessingEvent event = + (KinesisAnalyticsStreamsInputPreprocessingEvent) object; return new EventPart(event.getRecords().stream() .map(r -> decode(r.getData())) .collect(Collectors.toList())); @@ -167,8 +183,9 @@ private EventPart(Object content) { /** * Deserialize this part of event from JSON to an object of type T + * * @param clazz the target type for deserialization - * @param type of object to return + * @param type of object to return * @return an Object of type T (deserialized from the content) */ public T as(Class clazz) { @@ -187,7 +204,8 @@ public T as(Class clazz) { return (T) contentObject; } if (contentList != null) { - throw new EventDeserializationException("The content of this event is a list, consider using 'asListOf' instead"); + throw new EventDeserializationException( + "The content of this event is a list, consider using 'asListOf' instead"); } // should not occur, except if the event is malformed (missing fields) throw new IllegalStateException("Event content is null: the event may be malformed (missing fields)"); @@ -198,14 +216,16 @@ public T as(Class clazz) { /** * Deserialize this part of event from JSON to a list of objects of type T + * * @param clazz the target type for deserialization - * @param type of object to return + * @param type of object to return * @return a list of objects of type T (deserialized from the content) */ public List asListOf(Class clazz) { if (contentList == null && content == null) { if (contentMap != null || contentObject != null) { - throw new EventDeserializationException("The content of this event is not a list, consider using 'as' instead"); + throw new EventDeserializationException( + "The content of this event is not a list, consider using 'as' instead"); } // should not occur, except if the event is really malformed throw new IllegalStateException("Event content is null: the event may be malformed (missing fields)"); @@ -215,16 +235,20 @@ public List asListOf(Class clazz) { try { return reader.readValue(content); } catch (JsonProcessingException e) { - throw new EventDeserializationException("Cannot load the event as a list of " + clazz.getSimpleName() + ", consider using 'as' instead", e); + throw new EventDeserializationException( + "Cannot load the event as a list of " + clazz.getSimpleName() + + ", consider using 'as' instead", e); } } else { - return contentList.stream().map(s -> { - try { - return s == null ? null : JsonConfig.get().getObjectMapper().reader().readValue(s, clazz); - } catch (IOException e) { - throw new EventDeserializationException("Cannot load the event as a list of " + clazz.getSimpleName(), e); - } - }).collect(Collectors.toList()); + return contentList.stream().map(s -> + { + try { + return s == null ? null : JsonConfig.get().getObjectMapper().reader().readValue(s, clazz); + } catch (IOException e) { + throw new EventDeserializationException( + "Cannot load the event as a list of " + clazz.getSimpleName(), e); + } + }).collect(Collectors.toList()); } } } diff --git a/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/JsonConfig.java b/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/JsonConfig.java index d8af1c0cb..baa6a0367 100644 --- a/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/JsonConfig.java +++ b/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/JsonConfig.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.utilities; import com.fasterxml.jackson.databind.JsonNode; @@ -25,19 +26,7 @@ import software.amazon.lambda.powertools.utilities.jmespath.JsonFunction; public class JsonConfig { - private JsonConfig() { - } - - private static class ConfigHolder { - private final static JsonConfig instance = new JsonConfig(); - } - - public static JsonConfig get() { - return ConfigHolder.instance; - } - private static final ThreadLocal om = ThreadLocal.withInitial(ObjectMapper::new); - private final FunctionRegistry defaultFunctions = FunctionRegistry.defaultRegistry(); private final FunctionRegistry customFunctions = defaultFunctions.extend( new Base64Function(), @@ -45,10 +34,18 @@ public static JsonConfig get() { new JsonFunction() ); private final RuntimeConfiguration configuration = new RuntimeConfiguration.Builder() + .withSilentTypeErrors(true) .withFunctionRegistry(customFunctions) .build(); private JmesPath jmesPath = new JacksonRuntime(configuration, getObjectMapper()); + private JsonConfig() { + } + + public static JsonConfig get() { + return ConfigHolder.instance; + } + /** * Return an Object Mapper. Use this to customize (de)serialization config. * @@ -72,7 +69,7 @@ public JmesPath getJmesPath() { * {@link Base64Function} and {@link Base64GZipFunction} are already built-in. * * @param function the function to add - * @param Must extends {@link BaseFunction} + * @param Must extends {@link BaseFunction} */ public void addFunction(T function) { FunctionRegistry functionRegistryWithExtendedFunctions = configuration.functionRegistry().extend(function); @@ -83,4 +80,8 @@ public void addFunction(T function) { jmesPath = new JacksonRuntime(updatedConfig, getObjectMapper()); } + + private static class ConfigHolder { + private final static JsonConfig instance = new JsonConfig(); + } } diff --git a/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/jmespath/Base64Function.java b/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/jmespath/Base64Function.java index 737d96835..26b655fbd 100644 --- a/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/jmespath/Base64Function.java +++ b/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/jmespath/Base64Function.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,20 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.utilities.jmespath; +import static java.nio.charset.StandardCharsets.UTF_8; + import io.burt.jmespath.Adapter; import io.burt.jmespath.JmesPathType; import io.burt.jmespath.function.ArgumentConstraints; import io.burt.jmespath.function.BaseFunction; import io.burt.jmespath.function.FunctionArgument; - import java.nio.ByteBuffer; import java.util.Base64; import java.util.List; -import static java.nio.charset.StandardCharsets.UTF_8; - /** * Function used by JMESPath to decode a Base64 encoded String into a decoded String */ @@ -34,16 +34,6 @@ public Base64Function() { super("powertools_base64", ArgumentConstraints.typeOf(JmesPathType.STRING)); } - @Override - protected T callFunction(Adapter runtime, List> arguments) { - T value = arguments.get(0).value(); - String encodedString = runtime.toString(value); - - String decodedString = decode(encodedString); - - return runtime.createString(decodedString); - } - public static String decode(String encodedString) { return new String(decode(encodedString.getBytes(UTF_8)), UTF_8); } @@ -55,4 +45,14 @@ public static String decode(ByteBuffer byteBuffer) { public static byte[] decode(byte[] encoded) { return Base64.getDecoder().decode(encoded); } + + @Override + protected T callFunction(Adapter runtime, List> arguments) { + T value = arguments.get(0).value(); + String encodedString = runtime.toString(value); + + String decodedString = decode(encodedString); + + return runtime.createString(decodedString); + } } diff --git a/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/jmespath/Base64GZipFunction.java b/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/jmespath/Base64GZipFunction.java index 6b097af62..f5d5beeb1 100644 --- a/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/jmespath/Base64GZipFunction.java +++ b/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/jmespath/Base64GZipFunction.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,25 +11,24 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.utilities.jmespath; +import static java.nio.charset.StandardCharsets.UTF_8; +import static software.amazon.lambda.powertools.utilities.jmespath.Base64Function.decode; + import io.burt.jmespath.Adapter; import io.burt.jmespath.JmesPathType; import io.burt.jmespath.function.ArgumentConstraints; import io.burt.jmespath.function.BaseFunction; import io.burt.jmespath.function.FunctionArgument; - import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStreamReader; -import java.util.Arrays; import java.util.List; import java.util.zip.GZIPInputStream; -import static java.nio.charset.StandardCharsets.UTF_8; -import static software.amazon.lambda.powertools.utilities.jmespath.Base64Function.decode; - /** * Function used by JMESPath to decode a Base64 encoded GZipped String into a decoded String */ @@ -39,32 +38,22 @@ public Base64GZipFunction() { super("powertools_base64_gzip", ArgumentConstraints.typeOf(JmesPathType.STRING)); } - @Override - protected T callFunction(Adapter runtime, List> arguments) { - T value = arguments.get(0).value(); - String encodedString = runtime.toString(value); - - String decompressString = decompress(decode(encodedString.getBytes(UTF_8))); - - return runtime.createString(decompressString); - } - public static String decompress(byte[] compressed) { - if ((compressed == null) || (compressed.length == 0)) { - return ""; + if (compressed == null || compressed.length == 0) { + return null; + } + if (!isCompressed(compressed)) { + return new String(compressed, UTF_8); } try { StringBuilder out = new StringBuilder(); - if (isCompressed(compressed)) { - GZIPInputStream gzipStream = new GZIPInputStream(new ByteArrayInputStream(compressed)); - BufferedReader bf = new BufferedReader(new InputStreamReader(gzipStream, UTF_8)); - String line; - while ((line = bf.readLine()) != null) { - out.append(line); - } - } else { - out.append(Arrays.toString(compressed)); + GZIPInputStream gzipStream = new GZIPInputStream(new ByteArrayInputStream(compressed)); + BufferedReader bf = new BufferedReader(new InputStreamReader(gzipStream, UTF_8)); + + String line; + while ((line = bf.readLine()) != null) { + out.append(line); } return out.toString(); } catch (IOException e) { @@ -73,6 +62,21 @@ public static String decompress(byte[] compressed) { } public static boolean isCompressed(final byte[] compressed) { - return (compressed[0] == (byte) (GZIPInputStream.GZIP_MAGIC)) && (compressed[1] == (byte) (GZIPInputStream.GZIP_MAGIC >> 8)); + return (compressed[0] == (byte) (GZIPInputStream.GZIP_MAGIC)) && + (compressed[1] == (byte) (GZIPInputStream.GZIP_MAGIC >> 8)); + } + + @Override + protected T callFunction(Adapter runtime, List> arguments) { + T value = arguments.get(0).value(); + String encodedString = runtime.toString(value); + + String decompressString = decompress(decode(encodedString.getBytes(UTF_8))); + + if (decompressString == null) { + return runtime.createNull(); + } + + return runtime.createString(decompressString); } } diff --git a/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/jmespath/JsonFunction.java b/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/jmespath/JsonFunction.java index 584b544bf..b7661b5af 100644 --- a/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/jmespath/JsonFunction.java +++ b/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/jmespath/JsonFunction.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.utilities.jmespath; import io.burt.jmespath.Adapter; @@ -18,7 +19,6 @@ import io.burt.jmespath.function.ArgumentConstraints; import io.burt.jmespath.function.BaseFunction; import io.burt.jmespath.function.FunctionArgument; - import java.util.List; public class JsonFunction extends BaseFunction { diff --git a/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/EventDeserializerTest.java b/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/EventDeserializerTest.java index 90143b2a0..fcfdb47e3 100644 --- a/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/EventDeserializerTest.java +++ b/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/EventDeserializerTest.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,38 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.utilities; -import com.amazonaws.services.lambda.runtime.events.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static software.amazon.lambda.powertools.utilities.EventDeserializer.extractDataFrom; + +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; +import com.amazonaws.services.lambda.runtime.events.ActiveMQEvent; +import com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerRequestEvent; +import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent; +import com.amazonaws.services.lambda.runtime.events.CloudWatchLogsEvent; +import com.amazonaws.services.lambda.runtime.events.KafkaEvent; +import com.amazonaws.services.lambda.runtime.events.KinesisAnalyticsFirehoseInputPreprocessingEvent; +import com.amazonaws.services.lambda.runtime.events.KinesisAnalyticsStreamsInputPreprocessingEvent; +import com.amazonaws.services.lambda.runtime.events.KinesisEvent; +import com.amazonaws.services.lambda.runtime.events.KinesisFirehoseEvent; +import com.amazonaws.services.lambda.runtime.events.RabbitMQEvent; +import com.amazonaws.services.lambda.runtime.events.SNSEvent; +import com.amazonaws.services.lambda.runtime.events.SQSEvent; +import com.amazonaws.services.lambda.runtime.events.ScheduledEvent; import com.amazonaws.services.lambda.runtime.tests.annotations.Event; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import software.amazon.lambda.powertools.utilities.model.Basket; +import software.amazon.lambda.powertools.utilities.model.Order; import software.amazon.lambda.powertools.utilities.model.Product; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static software.amazon.lambda.powertools.utilities.EventDeserializer.extractDataFrom; - public class EventDeserializerTest { @Test @@ -46,7 +61,8 @@ public void testDeserializeStringAsObject_shouldReturnObject() { @Test public void testDeserializeStringArrayAsList_shouldReturnList() { - String productStr = "[{\"id\":1234, \"name\":\"product\", \"price\":42}, {\"id\":2345, \"name\":\"product2\", \"price\":43}]"; + String productStr = + "[{\"id\":1234, \"name\":\"product\", \"price\":42}, {\"id\":2345, \"name\":\"product2\", \"price\":43}]"; List products = extractDataFrom(productStr).asListOf(Product.class); assertThat(products).hasSize(2); assertProduct(products.get(0)); @@ -129,7 +145,23 @@ public void testDeserializeSQSEventMessageAsObject_shouldThrowException(SQSEvent public void testDeserializeAPIGatewayEventAsList_shouldThrowException(APIGatewayProxyRequestEvent event) { assertThatThrownBy(() -> extractDataFrom(event).asListOf(Product.class)) .isInstanceOf(EventDeserializationException.class) - .hasMessageContaining("consider using 'as' instead"); + .hasMessageContaining("consider using 'as' instead") + .hasMessageContaining("Cannot load the event as a list of"); + } + + @ParameterizedTest + @Event(value = "custom_event_map.json", type = HashMap.class) + public void testDeserializeAPIGatewayMapEventAsList_shouldThrowException(Map event) { + assertThatThrownBy(() -> extractDataFrom(event).asListOf(Order.class)) + .isInstanceOf(EventDeserializationException.class) + .hasMessage("The content of this event is not a list, consider using 'as' instead"); + } + + @Test + public void testDeserializeEmptyEventAsList_shouldThrowException() { + assertThatThrownBy(() -> extractDataFrom(null).asListOf(Product.class)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Event content is null: the event may be malformed (missing fields)"); } @ParameterizedTest @@ -145,7 +177,14 @@ public void testDeserializeSQSEventBodyAsWrongObjectType_shouldThrowException(SQ public void testDeserializeAPIGatewayNoBody_shouldThrowException(APIGatewayProxyRequestEvent event) { assertThatThrownBy(() -> extractDataFrom(event).as(Product.class)) .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("Event content is null"); + .hasMessage("Event content is null: the event may be malformed (missing fields)"); + } + + @Test + public void testDeserializeAPIGatewayNoBodyAsList_shouldThrowException() { + assertThatThrownBy(() -> extractDataFrom(new Object()).asListOf(Product.class)) + .isInstanceOf(EventDeserializationException.class) + .hasMessage("The content of this event is not a list, consider using 'as' instead"); } @ParameterizedTest @@ -164,9 +203,86 @@ public void testDeserializeProductAsProduct_shouldReturnProduct() { private void assertProduct(Product product) { -assertThat(product) + assertThat(product) .isEqualTo(new Product(1234, "product", 42)) .usingRecursiveComparison(); } + @ParameterizedTest + @Event(value = "scheduled_event.json", type = ScheduledEvent.class) + public void testDeserializeScheduledEventMessageAsObject_shouldReturnObject(ScheduledEvent event) { + Product product = extractDataFrom(event).as(Product.class); + assertProduct(product); + } + + @ParameterizedTest + @Event(value = "alb_event.json", type = ApplicationLoadBalancerRequestEvent.class) + public void testDeserializeALBEventMessageAsObjectShouldReturnObject(ApplicationLoadBalancerRequestEvent event) { + Product product = extractDataFrom(event).as(Product.class); + assertProduct(product); + } + + @ParameterizedTest + @Event(value = "cwl_event.json", type = CloudWatchLogsEvent.class) + public void testDeserializeCWLEventMessageAsObjectShouldReturnObject(CloudWatchLogsEvent event) { + Product product = extractDataFrom(event).as(Product.class); + assertProduct(product); + } + + @ParameterizedTest + @Event(value = "kf_event.json", type = KinesisFirehoseEvent.class) + public void testDeserializeKFEventMessageAsListShouldReturnList(KinesisFirehoseEvent event) { + List products = extractDataFrom(event).asListOf(Product.class); + assertThat(products).hasSize(1); + assertProduct(products.get(0)); + } + + @ParameterizedTest + @Event(value = "amq_event.json", type = ActiveMQEvent.class) + public void testDeserializeAMQEventMessageAsListShouldReturnList(ActiveMQEvent event) { + List products = extractDataFrom(event).asListOf(Product.class); + assertThat(products).hasSize(1); + assertProduct(products.get(0)); + } + + @ParameterizedTest + @Event(value = "rabbitmq_event.json", type = RabbitMQEvent.class) + public void testDeserializeRabbitMQEventMessageAsListShouldReturnList(RabbitMQEvent event) { + List products = extractDataFrom(event).asListOf(Product.class); + assertThat(products).hasSize(1); + assertProduct(products.get(0)); + } + + @ParameterizedTest + @Event(value = "kasip_event.json", type = KinesisAnalyticsStreamsInputPreprocessingEvent.class) + public void testDeserializeKasipEventMessageAsListShouldReturnList( + KinesisAnalyticsStreamsInputPreprocessingEvent event) { + List products = extractDataFrom(event).asListOf(Product.class); + assertThat(products).hasSize(1); + assertProduct(products.get(0)); + } + + @ParameterizedTest + @Event(value = "kafip_event.json", type = KinesisAnalyticsFirehoseInputPreprocessingEvent.class) + public void testDeserializeKafipEventMessageAsListShouldReturnList( + KinesisAnalyticsFirehoseInputPreprocessingEvent event) { + List products = extractDataFrom(event).asListOf(Product.class); + assertThat(products).hasSize(1); + assertProduct(products.get(0)); + } + + @ParameterizedTest + @Event(value = "apigwv2_event.json", type = APIGatewayV2HTTPEvent.class) + public void testDeserializeApiGWV2EventMessageAsObjectShouldReturnObject(APIGatewayV2HTTPEvent event) { + Product product = extractDataFrom(event).as(Product.class); + assertProduct(product); + } + + @ParameterizedTest + @Event(value = "cfcr_event.json", type = CloudFormationCustomResourceEvent.class) + public void testDeserializeCfcrEventMessageAsObjectShouldReturnObject(CloudFormationCustomResourceEvent event) { + Product product = extractDataFrom(event).as(Product.class); + assertProduct(product); + } + } diff --git a/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/jmespath/Base64FunctionTest.java b/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/jmespath/Base64FunctionTest.java index 5f243537c..d86af6671 100644 --- a/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/jmespath/Base64FunctionTest.java +++ b/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/jmespath/Base64FunctionTest.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,24 +11,26 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.utilities.jmespath; +import static org.assertj.core.api.Assertions.assertThat; + import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.JsonNodeType; import io.burt.jmespath.Expression; +import java.io.IOException; import org.junit.jupiter.api.Test; import software.amazon.lambda.powertools.utilities.JsonConfig; -import java.io.IOException; - -import static org.assertj.core.api.Assertions.assertThat; - public class Base64FunctionTest { @Test public void testPowertoolsBase64() throws IOException { - JsonNode event = JsonConfig.get().getObjectMapper().readTree(this.getClass().getResourceAsStream("/custom_event.json")); - Expression expression = JsonConfig.get().getJmesPath().compile("basket.powertools_base64(hiddenProduct)"); + JsonNode event = + JsonConfig.get().getObjectMapper().readTree(this.getClass().getResourceAsStream("/custom_event.json")); + Expression expression = + JsonConfig.get().getJmesPath().compile("basket.powertools_base64(hiddenProduct)"); JsonNode result = expression.search(event); assertThat(result.getNodeType()).isEqualTo(JsonNodeType.STRING); assertThat(result.asText()).isEqualTo("{\n" + diff --git a/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/jmespath/Base64GZipFunctionTest.java b/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/jmespath/Base64GZipFunctionTest.java index 8c617a634..eeb605076 100644 --- a/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/jmespath/Base64GZipFunctionTest.java +++ b/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/jmespath/Base64GZipFunctionTest.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,26 +11,78 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.utilities.jmespath; +import static org.assertj.core.api.Assertions.assertThat; + import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.JsonNodeType; import io.burt.jmespath.Expression; +import io.burt.jmespath.JmesPathType; +import java.io.IOException; import org.junit.jupiter.api.Test; import software.amazon.lambda.powertools.utilities.JsonConfig; -import java.io.IOException; +public class Base64GZipFunctionTest { -import static org.assertj.core.api.Assertions.assertThat; + @Test + public void testConstructor() { + Base64GZipFunction base64GZipFunction = new Base64GZipFunction(); + assertThat(base64GZipFunction.name()).isEqualTo("powertools_base64_gzip"); + assertThat(base64GZipFunction.argumentConstraints().expectedType().toLowerCase()).isEqualTo( + JmesPathType.STRING.name().toLowerCase()); + assertThat(base64GZipFunction.argumentConstraints().minArity()).isEqualTo(1); + assertThat(base64GZipFunction.argumentConstraints().maxArity()).isEqualTo(1); -public class Base64GZipFunctionTest { + } @Test public void testPowertoolsGzip() throws IOException { - JsonNode event = JsonConfig.get().getObjectMapper().readTree(this.getClass().getResourceAsStream("/custom_event_gzip.json")); - Expression expression = JsonConfig.get().getJmesPath().compile("basket.powertools_base64_gzip(hiddenProduct)"); + JsonNode event = JsonConfig.get().getObjectMapper() + .readTree(this.getClass().getResourceAsStream("/custom_event_gzip.json")); + Expression expression = + JsonConfig.get().getJmesPath().compile("basket.powertools_base64_gzip(hiddenProduct)"); JsonNode result = expression.search(event); assertThat(result.getNodeType()).isEqualTo(JsonNodeType.STRING); assertThat(result.asText()).isEqualTo("{ \"id\": 43242, \"name\": \"FooBar XY\", \"price\": 258}"); } + + @Test + public void testPowertoolsGzipEmptyJsonAttribute() throws IOException { + JsonNode event = JsonConfig.get().getObjectMapper() + .readTree(this.getClass().getResourceAsStream("/custom_event_gzip.json")); + Expression expression = JsonConfig.get().getJmesPath().compile("basket.powertools_base64_gzip('')"); + JsonNode result = expression.search(event); + assertThat(result.getNodeType()).isEqualTo(JsonNodeType.NULL); + } + + @Test + public void testPowertoolsGzipWrongArgumentType() throws IOException { + JsonNode event = JsonConfig.get().getObjectMapper() + .readTree(this.getClass().getResourceAsStream("/custom_event_gzip.json")); + Expression expression = JsonConfig.get().getJmesPath().compile("basket.powertools_base64_gzip(null)"); + JsonNode result = expression.search(event); + + assertThat(result.getNodeType()).isEqualTo(JsonNodeType.NULL); + } + + @Test + public void testBase64GzipDecompressNull() { + String result = Base64GZipFunction.decompress(null); + assertThat(result).isNull(); + } + + @Test + public void testPowertoolsGzipNotCompressedJsonAttribute() throws IOException { + JsonNode event = JsonConfig.get().getObjectMapper() + .readTree(this.getClass().getResourceAsStream("/custom_event_gzip.json")); + Expression expression = + JsonConfig.get().getJmesPath().compile("basket.powertools_base64_gzip(encodedString)"); + JsonNode result = expression.search(event); + assertThat(result.getNodeType()).isEqualTo(JsonNodeType.STRING); + assertThat(result.asText()).isEqualTo("test"); + } + + } diff --git a/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/jmespath/JsonFunctionTest.java b/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/jmespath/JsonFunctionTest.java index 4ea4eed35..0bfb635fa 100644 --- a/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/jmespath/JsonFunctionTest.java +++ b/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/jmespath/JsonFunctionTest.java @@ -1,20 +1,34 @@ +/* + * 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.utilities.jmespath; +import static org.assertj.core.api.Assertions.assertThat; + import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.JsonNodeType; import io.burt.jmespath.Expression; +import java.io.IOException; import org.junit.jupiter.api.Test; import software.amazon.lambda.powertools.utilities.JsonConfig; -import java.io.IOException; - -import static org.assertj.core.api.Assertions.assertThat; - public class JsonFunctionTest { @Test public void testJsonFunction() throws IOException { - JsonNode event = JsonConfig.get().getObjectMapper().readTree(this.getClass().getResourceAsStream("/custom_event_json.json")); + JsonNode event = JsonConfig.get().getObjectMapper() + .readTree(this.getClass().getResourceAsStream("/custom_event_json.json")); Expression expression = JsonConfig.get().getJmesPath().compile("powertools_json(body)"); JsonNode result = expression.search(event); assertThat(result.getNodeType()).isEqualTo(JsonNodeType.OBJECT); @@ -25,7 +39,8 @@ public void testJsonFunction() throws IOException { @Test public void testJsonFunctionChild() throws IOException { - JsonNode event = JsonConfig.get().getObjectMapper().readTree(this.getClass().getResourceAsStream("/custom_event_json.json")); + JsonNode event = JsonConfig.get().getObjectMapper() + .readTree(this.getClass().getResourceAsStream("/custom_event_json.json")); Expression expression = JsonConfig.get().getJmesPath().compile("powertools_json(body).list[0].item"); JsonNode result = expression.search(event); assertThat(result.getNodeType()).isEqualTo(JsonNodeType.STRING); diff --git a/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/model/Basket.java b/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/model/Basket.java index 228089c52..4bf427a21 100644 --- a/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/model/Basket.java +++ b/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/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.utilities.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-serialization/src/test/java/software/amazon/lambda/powertools/utilities/model/Order.java b/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/model/Order.java new file mode 100644 index 000000000..6b48ccd1d --- /dev/null +++ b/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/model/Order.java @@ -0,0 +1,34 @@ +/* + * 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.utilities.model; + +import java.util.HashMap; +import java.util.Map; + +public class Order { + private Map orders = new HashMap<>(); + + public Order() { + } + + public Map getProducts() { + return orders; + } + + public void setProducts(Map products) { + this.orders = products; + } + +} diff --git a/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/model/Product.java b/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/model/Product.java index f03f6d426..c90a4632e 100644 --- a/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/model/Product.java +++ b/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/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.utilities.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-serialization/src/test/resources/alb_event.json b/powertools-serialization/src/test/resources/alb_event.json new file mode 100644 index 000000000..d2b0d3cda --- /dev/null +++ b/powertools-serialization/src/test/resources/alb_event.json @@ -0,0 +1,28 @@ +{ + "requestContext": { + "elb": { + "targetGroupArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/lambda-279XGJDqGZ5rsrHC2Fjr/49e9d65c45c6791a" + } + }, + "httpMethod": "GET", + "path": "/lambda", + "queryStringParameters": { + "query": "1234ABCD" + }, + "headers": { + "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", + "accept-encoding": "gzip", + "accept-language": "en-US,en;q=0.9", + "connection": "keep-alive", + "host": "lambda-alb-123578498.us-east-1.elb.amazonaws.com", + "upgrade-insecure-requests": "1", + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36", + "x-amzn-trace-id": "Root=1-5c536348-3d683b8b04734faae651f476", + "x-forwarded-for": "72.12.164.125", + "x-forwarded-port": "80", + "x-forwarded-proto": "http", + "x-imforwards": "20" + }, + "body": "{\"id\":1234, \"name\":\"product\", \"price\":42}", + "isBase64Encoded": false +} \ No newline at end of file diff --git a/powertools-serialization/src/test/resources/amq_event.json b/powertools-serialization/src/test/resources/amq_event.json new file mode 100644 index 000000000..00923d5e5 --- /dev/null +++ b/powertools-serialization/src/test/resources/amq_event.json @@ -0,0 +1,29 @@ +{ + "eventSource": "aws:mq", + "eventSourceArn": "arn:aws:mq:us-west-2:111122223333:broker:test:b-9bcfa592-423a-4942-879d-eb284b418fc8", + "messages": [ + { + "messageID": "ID:b-9bcfa592-423a-4942-879d-eb284b418fc8-1.mq.us-west-2.amazonaws.com-37557-1234520418293-4:1:1:1:1", + "messageType": "jms/text-message", + "deliveryMode": 1, + "replyTo": null, + "type": null, + "expiration": "60000", + "priority": 1, + "correlationId": "myJMSCoID", + "redelivered": false, + "destination": { + "physicalName": "testQueue" + }, + "data": "ewogICJpZCI6IDEyMzQsCiAgIm5hbWUiOiAicHJvZHVjdCIsCiAgInByaWNlIjogNDIKfQ==", + "timestamp": 1598827811958, + "brokerInTime": 1598827811958, + "brokerOutTime": 1598827811959, + "properties": { + "index": "1", + "doAlarm": "false", + "myCustomProperty": "value" + } + } + ] +} \ No newline at end of file diff --git a/powertools-serialization/src/test/resources/apigwv2_event.json b/powertools-serialization/src/test/resources/apigwv2_event.json new file mode 100644 index 000000000..db4fc0f95 --- /dev/null +++ b/powertools-serialization/src/test/resources/apigwv2_event.json @@ -0,0 +1,57 @@ +{ + "version": "V2", + "routeKey": "routeKey", + "rawPath": "rawPath", + "rawQueryString": "rawQueryString", + "cookies": + ["foo", "bar"] + , + "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" + }, + "queryStringParameters": { + "foo": "bar" + }, + "pathParameters": { + "proxy": "/path/to/resource" + }, + "stageVariables": { + "baz": "qux" + }, + "body": "{\"id\":1234, \"name\":\"product\", \"price\":42}", + "isBase64Encoded": false, + "requestContext": { + "routeKey": "routeKey", + "accountId": "123456789012", + "stage": "prod", + "apiId": "1234567890", + "domainName": "domainName", + "domainPrefix": "domainPrefix", + "time": "09/Apr/2015:12:34:56 +0000", + "timeEpoch": 1428582896000, + "http": { + "method": "POST", + "path": "/path/to/resource", + "protocol": "HTTP/1.1", + "sourceIp": "1.1.1.1", + "userAgent": "Chrome" + } + } +} diff --git a/powertools-serialization/src/test/resources/cfcr_event.json b/powertools-serialization/src/test/resources/cfcr_event.json new file mode 100644 index 000000000..58d054c06 --- /dev/null +++ b/powertools-serialization/src/test/resources/cfcr_event.json @@ -0,0 +1,20 @@ +{ + "RequestType": "requestType", + "ServiceToken": "serviceToken", + "ResponseUrl": "responseUrl", + "StackId": "stackId", + "RequestId": "requestId", + "LogicalResourceId": "logicalResourceId", + "ResourceType": "resourceType", + "ResourceProperties": { + "id": 1234, + "name": "product", + "price": 42 + }, + "OldResourceProperties": { + "id": 1234, + "name": "product", + "price": 40 + } +} + diff --git a/powertools-serialization/src/test/resources/custom_event.json b/powertools-serialization/src/test/resources/custom_event.json index 13103c434..918cad81f 100644 --- a/powertools-serialization/src/test/resources/custom_event.json +++ b/powertools-serialization/src/test/resources/custom_event.json @@ -1,6 +1,6 @@ { "basket": { - "products" : [ + "products": [ { "id": 43242, "name": "FooBar XY", diff --git a/powertools-serialization/src/test/resources/custom_event_gzip.json b/powertools-serialization/src/test/resources/custom_event_gzip.json index d212052d0..2cf088092 100644 --- a/powertools-serialization/src/test/resources/custom_event_gzip.json +++ b/powertools-serialization/src/test/resources/custom_event_gzip.json @@ -1,12 +1,13 @@ { "basket": { - "products" : [ + "products": [ { "id": 43242, "name": "FooBar XY", "price": 258 } ], - "hiddenProduct": "H4sIAAAAAAAA/6vmUlBQykxRslIwMTYyMdIBcfMSc1OBAkpu+flOiUUKEZFKYOGCosxkkLiRqQVXLQDnWo6bOAAAAA==" + "hiddenProduct": "H4sIAAAAAAAA/6vmUlBQykxRslIwMTYyMdIBcfMSc1OBAkpu+flOiUUKEZFKYOGCosxkkLiRqQVXLQDnWo6bOAAAAA==", + "encodedString": "dGVzdA==" } } \ No newline at end of file diff --git a/powertools-serialization/src/test/resources/custom_event_map.json b/powertools-serialization/src/test/resources/custom_event_map.json new file mode 100644 index 000000000..7d3f076d7 --- /dev/null +++ b/powertools-serialization/src/test/resources/custom_event_map.json @@ -0,0 +1,9 @@ +{ + "products": { + "12345": { + "id": 43242, + "name": "FooBar XY", + "price": 258 + } + } +} \ No newline at end of file diff --git a/powertools-serialization/src/test/resources/cwl_event.json b/powertools-serialization/src/test/resources/cwl_event.json new file mode 100644 index 000000000..911ab1b3a --- /dev/null +++ b/powertools-serialization/src/test/resources/cwl_event.json @@ -0,0 +1,5 @@ +{ + "awslogs": { + "data": "ewogICJpZCI6IDEyMzQsCiAgIm5hbWUiOiAicHJvZHVjdCIsCiAgInByaWNlIjogNDIKfQ==" + } +} \ No newline at end of file diff --git a/powertools-serialization/src/test/resources/kafip_event.json b/powertools-serialization/src/test/resources/kafip_event.json new file mode 100644 index 000000000..01196256c --- /dev/null +++ b/powertools-serialization/src/test/resources/kafip_event.json @@ -0,0 +1,14 @@ +{ + "invocationId": "arn:aws:iam::EXAMPLE", + "applicationArn": "arn:aws:kinesis:EXAMPLE", + "streamArn": "arn:aws:kinesis:EXAMPLE", + "records": [ + { + "kinesisFirehoseRecordMetadata": { + "approximateArrivalTimestamp": 1428537600 + }, + "recordId": "record-id", + "data": "eyJpZCI6MTIzNCwgIm5hbWUiOiJwcm9kdWN0IiwgInByaWNlIjo0Mn0=" + } + ] +} \ No newline at end of file diff --git a/powertools-serialization/src/test/resources/kasip_event.json b/powertools-serialization/src/test/resources/kasip_event.json new file mode 100644 index 000000000..78bc9a3fb --- /dev/null +++ b/powertools-serialization/src/test/resources/kasip_event.json @@ -0,0 +1,17 @@ +{ + "invocationId": "arn:aws:iam::EXAMPLE", + "applicationArn": "arn:aws:kinesis:EXAMPLE", + "streamArn": "arn:aws:kinesis:EXAMPLE", + "records": [ + { + "kinesisStreamRecordMetadata": { + "sequenceNumber": "49545115243490985018280067714973144582180062593244200961", + "partitionKey": "partitionKey-03", + "shardId": "12", + "approximateArrivalTimestamp": 1428537600 + }, + "recordId": "record-id", + "data": "eyJpZCI6MTIzNCwgIm5hbWUiOiJwcm9kdWN0IiwgInByaWNlIjo0Mn0=" + } + ] +} \ No newline at end of file diff --git a/powertools-serialization/src/test/resources/kf_event.json b/powertools-serialization/src/test/resources/kf_event.json new file mode 100644 index 000000000..e36bc4c3f --- /dev/null +++ b/powertools-serialization/src/test/resources/kf_event.json @@ -0,0 +1,12 @@ +{ + "invocationId": "invocationIdExample", + "deliveryStreamArn": "arn:aws:kinesis:EXAMPLE", + "region": "us-east-1", + "records": [ + { + "recordId": "49546986683135544286507457936321625675700192471156785154", + "approximateArrivalTimestamp": 1495072949453, + "data": "ewogICJpZCI6IDEyMzQsCiAgIm5hbWUiOiAicHJvZHVjdCIsCiAgInByaWNlIjogNDIKfQ==" + } + ] +} \ No newline at end of file diff --git a/powertools-serialization/src/test/resources/rabbitmq_event.json b/powertools-serialization/src/test/resources/rabbitmq_event.json new file mode 100644 index 000000000..698e37143 --- /dev/null +++ b/powertools-serialization/src/test/resources/rabbitmq_event.json @@ -0,0 +1,51 @@ +{ + "eventSource": "aws:rmq", + "eventSourceArn": "arn:aws:mq:us-west-2:111122223333:broker:pizzaBroker:b-9bcfa592-423a-4942-879d-eb284b418fc8", + "rmqMessagesByQueue": { + "pizzaQueue::/": [ + { + "basicProperties": { + "contentType": "text/plain", + "contentEncoding": null, + "headers": { + "header1": { + "bytes": [ + 118, + 97, + 108, + 117, + 101, + 49 + ] + }, + "header2": { + "bytes": [ + 118, + 97, + 108, + 117, + 101, + 50 + ] + }, + "numberInHeader": 10 + }, + "deliveryMode": 1, + "priority": 34, + "correlationId": null, + "replyTo": null, + "expiration": "60000", + "messageId": null, + "timestamp": "Jan 1, 1970, 12:33:41 AM", + "type": null, + "userId": "AIDACKCEVSQ6C2EXAMPLE", + "appId": null, + "clusterId": null, + "bodySize": 80 + }, + "redelivered": false, + "data": "ewogICJpZCI6IDEyMzQsCiAgIm5hbWUiOiAicHJvZHVjdCIsCiAgInByaWNlIjogNDIKfQ==" + } + ] + } +} diff --git a/powertools-serialization/src/test/resources/scheduled_event.json b/powertools-serialization/src/test/resources/scheduled_event.json new file mode 100644 index 000000000..9a65f4bd4 --- /dev/null +++ b/powertools-serialization/src/test/resources/scheduled_event.json @@ -0,0 +1,12 @@ +{ + "id": "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c", + "detail-type": "Scheduled Event", + "source": "aws.events", + "account": "123456789012", + "time": "1970-01-01T00:00:00Z", + "region": "eu-central-1", + "resources": [ + "arn:aws:events:eu-central-1:123456789012:rule/my-schedule" + ], + "detail": {"id":1234, "name":"product", "price":42} +} \ No newline at end of file diff --git a/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/SQSBatchProcessingException.java b/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/SQSBatchProcessingException.java deleted file mode 100644 index 85231a003..000000000 --- a/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/SQSBatchProcessingException.java +++ /dev/null @@ -1,78 +0,0 @@ -package software.amazon.lambda.powertools.sqs; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import com.amazonaws.services.lambda.runtime.events.SQSEvent; - -import static com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; -import static java.util.Collections.*; -import static java.util.stream.Collectors.joining; - -/** - *

    - * When one or more {@link SQSMessage} fails and if any exception is thrown from {@link SqsMessageHandler#process(SQSMessage)} - * during processing of a messages, this exception is with all the details of successful and failed messages. - *

    - * - *

    - * This exception can be thrown form: - *

      - *
    • {@link SqsBatch}
    • - *
    • {@link SqsUtils#batchProcessor(SQSEvent, Class)}
    • - *
    • {@link SqsUtils#batchProcessor(SQSEvent, boolean, Class)}
    • - *
    • {@link SqsUtils#batchProcessor(SQSEvent, SqsMessageHandler)}
    • - *
    • {@link SqsUtils#batchProcessor(SQSEvent, boolean, SqsMessageHandler)}
    • - *
    - *

    - */ -public class SQSBatchProcessingException extends RuntimeException { - - private final List exceptions; - private final List failures; - private final List returnValues; - - public SQSBatchProcessingException(final List exceptions, - final List failures, - final List successReturns) { - super(exceptions.stream() - .map(Throwable::toString) - .collect(joining("\n"))); - - this.exceptions = new ArrayList<>(exceptions); - this.failures = new ArrayList<>(failures); - this.returnValues = new ArrayList<>(successReturns); - } - - /** - * Details for exceptions that occurred while processing messages in {@link SqsMessageHandler#process(SQSMessage)} - * @return List of exceptions that occurred while processing messages - */ - public List getExceptions() { - return unmodifiableList(exceptions); - } - - /** - * List of returns from {@link SqsMessageHandler#process(SQSMessage)} that were successfully processed. - * @return List of returns from successfully processed messages - */ - public List successMessageReturnValues() { - return unmodifiableList(returnValues); - } - - /** - * Details of {@link SQSMessage} that failed in {@link SqsMessageHandler#process(SQSMessage)} - * @return List of failed messages - */ - public List getFailures() { - return unmodifiableList(failures); - } - - @Override - public void printStackTrace() { - for (Exception exception : exceptions) { - exception.printStackTrace(); - } - } -} diff --git a/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/SqsBatch.java b/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/SqsBatch.java deleted file mode 100644 index cd529ff22..000000000 --- a/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/SqsBatch.java +++ /dev/null @@ -1,85 +0,0 @@ -package software.amazon.lambda.powertools.sqs; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -import com.amazonaws.services.lambda.runtime.events.SQSEvent; - -import static com.amazonaws.services.lambda.runtime.events.SQSEvent.*; - -/** - * {@link SqsBatch} is used to process batch messages in {@link SQSEvent} - * - *

    - * When using the annotation, implementation of {@link SqsMessageHandler} is required. Annotation will take care of - * calling {@link SqsMessageHandler#process(SQSMessage)} method for each {@link SQSMessage} in the received {@link SQSEvent} - *

    - * - *

    - * If any exception is thrown from {@link SqsMessageHandler#process(SQSMessage)} during processing of a messages, Utility - * will take care of deleting all the successful messages from SQS. When one or more single message fails processing due - * to exception thrown from {@link SqsMessageHandler#process(SQSMessage)}, Lambda execution will fail - * with {@link SQSBatchProcessingException}. - * - * If all the messages are successfully processes, No SQS messages are deleted explicitly but is rather delegated to - * Lambda execution context for deletion. - *

    - * - *

    - * If you want to suppress the exception even if any message in batch fails, set - * {@link SqsBatch#suppressException()} to true. By default its value is false - *

    - * - *

    - * If you want certain exceptions to be treated as permanent failures, i.e. exceptions where the result of retrying will - * always be a failure and want these can be immediately moved to the dead letter queue associated to the source SQS queue, - * - * you can use {@link SqsBatch#nonRetryableExceptions()} to configure such exceptions. - * Make sure function execution role has sqs:GetQueueAttributes permission on source SQS queue and sqs:SendMessage, - * sqs:SendMessageBatch permission for configured DLQ. - * - * If you want such messages to be deleted instead, set {@link SqsBatch#deleteNonRetryableMessageFromQueue()} to true. - * By default its value is false. - * - * If there is no DLQ configured on source SQS queue and {@link SqsBatch#nonRetryableExceptions()} attribute is set, if - * nonRetryableExceptions occurs from {@link SqsMessageHandler}, such exceptions will still be treated as temporary - * exceptions and the message will be moved back to source SQS queue for reprocessing. The same behaviour will occur if - * for some reason the utility is unable to move the message to the DLQ. An example of this could be because the function - * is missing the correct permissions. - *

    - * - *
    - * public class SqsMessageHandler implements RequestHandler {
    - *
    - *    {@literal @}Override
    - *    {@literal @}{@link SqsBatch (SqsMessageHandler)}
    - *     public String handleRequest(SQSEvent sqsEvent, Context context) {
    - *
    - *         return "ok";
    - *     }
    - *
    - *     public class DummySqsMessageHandler implements SqsMessageHandler{
    - *     @Override
    - *     public Object process(SQSEvent.SQSMessage message) {
    - *         throw new UnsupportedOperationException();
    - *     }
    - * }
    - *
    - *     ...
    - * 
    - * @see Amazon SQS dead-letter queues
    - */
    -@Retention(RetentionPolicy.RUNTIME)
    -@Target(ElementType.METHOD)
    -public @interface SqsBatch {
    -
    -    Class> value();
    -
    -    boolean suppressException() default false;
    -
    -    Class[] nonRetryableExceptions() default {};
    -
    -    boolean deleteNonRetryableMessageFromQueue() default false;
    -}
    diff --git a/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/SqsLargeMessage.java b/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/SqsLargeMessage.java
    deleted file mode 100644
    index d96245006..000000000
    --- a/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/SqsLargeMessage.java
    +++ /dev/null
    @@ -1,68 +0,0 @@
    -package software.amazon.lambda.powertools.sqs;
    -
    -import java.lang.annotation.ElementType;
    -import java.lang.annotation.Retention;
    -import java.lang.annotation.RetentionPolicy;
    -import java.lang.annotation.Target;
    -
    -/**
    - * {@code SqsLargeMessage} is used to signal that the annotated method
    - * should be extended to handle large SQS messages which have been offloaded
    - * to S3
    - *
    - * 

    {@code SqsLargeMessage} automatically retrieves and deletes messages - * which have been offloaded to S3 using the {@code amazon-sqs-java-extended-client-lib} - * client library.

    - * - *

    This version of the {@code SqsLargeMessage} is compatible with version - * 1.1.0+ of {@code amazon-sqs-java-extended-client-lib}.

    - * - *
    - * <dependency>
    - *   <groupId>com.amazonaws</groupId>
    - *   <artifactId>amazon-sqs-java-extended-client-lib</artifactId>
    - *   <version>1.1.0</version>
    - * </dependency>
    - * 
    - * - *

    {@code SqsLargeMessage} should be used with the handleRequest method of a class - * which implements {@code com.amazonaws.services.lambda.runtime.RequestHandler} with - * {@code com.amazonaws.services.lambda.runtime.events.SQSEvent} as the first parameter.

    - * - *
    - * public class SqsMessageHandler implements RequestHandler {
    - *
    - *    {@literal @}Override
    - *    {@literal @}SqsLargeMessage
    - *     public String handleRequest(SQSEvent sqsEvent, Context context) {
    - *
    - *         // process messages
    - *
    - *         return "ok";
    - *     }
    - *
    - *     ...
    - * 
    - * - *

    Using the default S3 Client {@code AmazonS3 amazonS3 = AmazonS3ClientBuilder.defaultClient();} - * each record received in the SQSEvent {@code SqsLargeMessage} will checked - * to see if it's body contains a payload which has been offloaded to S3. If it - * does then {@code getObject(bucket, key)} will be called and the payload - * retrieved.

    - * - *

    Note: Retreiving payloads from S3 will increase the duration of the - * Lambda function.

    - * - *

    If the request handler method returns then each payload will be deleted - * from S3 using {@code deleteObject(bucket, key)}

    - * - *

    To disable deletion of payloads setting the following annotation parameter - * {@code @SqsLargeMessage(deletePayloads=false)}

    - * - */ -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.METHOD) -public @interface SqsLargeMessage { - - boolean deletePayloads() default true; -} diff --git a/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/SqsMessageHandler.java b/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/SqsMessageHandler.java deleted file mode 100644 index 17e37797c..000000000 --- a/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/SqsMessageHandler.java +++ /dev/null @@ -1,29 +0,0 @@ -package software.amazon.lambda.powertools.sqs; - -import com.amazonaws.services.lambda.runtime.events.SQSEvent; - -import static com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; - -/** - *

    - * This interface should be implemented for processing {@link SQSMessage} inside {@link SQSEvent} received by lambda - * function. - *

    - * - *

    - * It is required by utilities: - *

      - *
    • {@link SqsBatch}
    • - *
    • {@link SqsUtils#batchProcessor(SQSEvent, Class)}
    • - *
    • {@link SqsUtils#batchProcessor(SQSEvent, boolean, Class)}
    • - *
    • {@link SqsUtils#batchProcessor(SQSEvent, SqsMessageHandler)}
    • - *
    • {@link SqsUtils#batchProcessor(SQSEvent, boolean, SqsMessageHandler)}
    • - *
    - *

    - * @param Return value type from {@link SqsMessageHandler#process(SQSMessage)} - */ -@FunctionalInterface -public interface SqsMessageHandler { - - R process(SQSMessage message); -} diff --git a/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/SqsUtils.java b/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/SqsUtils.java deleted file mode 100644 index 3461c6755..000000000 --- a/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/SqsUtils.java +++ /dev/null @@ -1,546 +0,0 @@ -/* - * Copyright 2020 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.sqs; - -import java.lang.reflect.Constructor; -import java.util.ArrayList; -import java.util.List; -import java.util.function.Function; -import java.util.stream.Collectors; - -import com.amazonaws.services.lambda.runtime.events.SQSEvent; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import software.amazon.awssdk.services.s3.S3Client; -import software.amazon.awssdk.services.sqs.SqsClient; -import software.amazon.lambda.powertools.sqs.internal.BatchContext; -import software.amazon.payloadoffloading.PayloadS3Pointer; -import software.amazon.lambda.powertools.sqs.internal.SqsLargeMessageAspect; - -import static com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; -import static software.amazon.lambda.powertools.sqs.internal.SqsLargeMessageAspect.processMessages; - -/** - * A class of helper functions to add additional functionality to {@link SQSEvent} processing. - */ -public final class SqsUtils { - private static final Logger LOG = LoggerFactory.getLogger(SqsUtils.class); - - private static final ObjectMapper objectMapper = new ObjectMapper(); - private static SqsClient client; - private static S3Client s3Client; - - private SqsUtils() { - } - - /** - * This is a utility method when you want to avoid using {@code SqsLargeMessage} annotation. - * Gives you access to enriched messages from S3 in the SQS event produced via extended client lib. - * If all the large S3 payload are successfully retrieved, it will delete them from S3 post success. - * - * @param sqsEvent Event received from SQS Extended client library - * @param messageFunction Function to execute you business logic which provides access to enriched messages from S3 when needed. - * @return Return value from the function. - */ - public static R enrichedMessageFromS3(final SQSEvent sqsEvent, - final Function, R> messageFunction) { - return enrichedMessageFromS3(sqsEvent, true, messageFunction); - } - - /** - * This is a utility method when you want to avoid using {@code SqsLargeMessage} annotation. - * Gives you access to enriched messages from S3 in the SQS event produced via extended client lib. - * if all the large S3 payload are successfully retrieved, Control if it will delete payload from S3 post success. - * - * @param sqsEvent Event received from SQS Extended client library - * @param messageFunction Function to execute you business logic which provides access to enriched messages from S3 when needed. - * @return Return value from the function. - */ - public static R enrichedMessageFromS3(final SQSEvent sqsEvent, - final boolean deleteS3Payload, - final Function, R> messageFunction) { - - List sqsMessages = sqsEvent.getRecords().stream() - .map(SqsUtils::clonedMessage) - .collect(Collectors.toList()); - - List s3Pointers = processMessages(sqsMessages); - - R returnValue = messageFunction.apply(sqsMessages); - - if (deleteS3Payload) { - s3Pointers.forEach(SqsLargeMessageAspect::deleteMessage); - } - - return returnValue; - } - - /** - * Provides ability to set default {@link SqsClient} to be used by utility. - * If no default configuration is provided, client is instantiated via {@link SqsClient#create()} - * - * @param client {@link SqsClient} to be used by utility - */ - public static void overrideSqsClient(SqsClient client) { - SqsUtils.client = client; - } - - /** - * By default, the S3Client is instantiated via {@link S3Client#create()}. - * This method provides the ability to override the S3Client with your own custom version. - * - * @param s3Client {@link S3Client} to be used by utility - */ - public static void overrideS3Client(S3Client s3Client) { - SqsUtils.s3Client = s3Client; - } - - /** - * This utility method is used to process each {@link SQSMessage} inside the received {@link SQSEvent} - * - *

    - * The Utility will call {@link SqsMessageHandler#process(SQSMessage)} method for each {@link SQSMessage} - * in the received {@link SQSEvent} - *

    - * - *

    - * If any exception is thrown from {@link SqsMessageHandler#process(SQSMessage)} during processing of a message, - * the utility will take care of deleting all the successful messages from SQS. When one or more single message fails - * processing due to exception thrown from {@link SqsMessageHandler#process(SQSMessage)} - * {@link SQSBatchProcessingException} is thrown with all the details of successful and failed messages. - *

    - * If all the messages are successfully processes, No SQS messages are deleted explicitly but is rather delegated to - * Lambda execution context for deletion. - *

    - * - *

    - * If you dont want the utility to throw {@link SQSBatchProcessingException} in case of failures but rather suppress - * it, Refer {@link SqsUtils#batchProcessor(SQSEvent, boolean, Class)} - *

    - * - * @param event {@link SQSEvent} received by lambda function. - * @param handler Class implementing {@link SqsMessageHandler} which will be called for each message in event. - * @return List of values returned by {@link SqsMessageHandler#process(SQSMessage)} while processing each message. - * @throws SQSBatchProcessingException if some messages fail during processing. - */ - public static List batchProcessor(final SQSEvent event, - final Class> handler) { - return batchProcessor(event, false, handler); - } - - /** - * This utility method is used to process each {@link SQSMessage} inside the received {@link SQSEvent} - * - *

    - * The Utility will call {@link SqsMessageHandler#process(SQSMessage)} method for each {@link SQSMessage} - * in the received {@link SQSEvent} - *

    - *

    - * If any exception is thrown from {@link SqsMessageHandler#process(SQSMessage)} during processing of a message, - * the utility will take care of deleting all the successful messages from SQS. When one or more single message fails - * processing due to exception thrown from {@link SqsMessageHandler#process(SQSMessage)} - * {@link SQSBatchProcessingException} is thrown with all the details of successful and failed messages. - * - *

    - * - *

    - * If all the messages are successfully processes, No SQS messages are deleted explicitly but is rather delegated to - * Lambda execution context for deletion. - *

    - * - *

    - * If you dont want the utility to throw {@link SQSBatchProcessingException} in case of failures but rather suppress - * it, Refer {@link SqsUtils#batchProcessor(SQSEvent, boolean, Class)} - *

    - * - *

    - * If you want certain exceptions to be treated as permanent failures, i.e. exceptions where the result of retrying will - * always be a failure and want these can be immediately moved to the dead letter queue associated to the source SQS queue, - * you can use nonRetryableExceptions parameter to configure such exceptions. - * - * Make sure function execution role has sqs:GetQueueAttributes permission on source SQS queue and sqs:SendMessage, - * sqs:SendMessageBatch permission for configured DLQ. - * - * If there is no DLQ configured on source SQS queue and {@link SqsBatch#nonRetryableExceptions()} attribute is set, if - * nonRetryableExceptions occurs from {@link SqsMessageHandler}, such exceptions will still be treated as temporary - * exceptions and the message will be moved back to source SQS queue for reprocessing. The same behaviour will occur if - * for some reason the utility is unable to move the message to the DLQ. An example of this could be because the function - * is missing the correct permissions. - *

    - * @see Amazon SQS dead-letter queues - * @param event {@link SQSEvent} received by lambda function. - * @param handler Class implementing {@link SqsMessageHandler} which will be called for each message in event. - * @param nonRetryableExceptions exception classes that are to be treated as permanent exceptions and to be moved - * to DLQ. - * @return List of values returned by {@link SqsMessageHandler#process(SQSMessage)} while processing each message. - * @throws SQSBatchProcessingException if some messages fail during processing. - */ - @SafeVarargs - public static List batchProcessor(final SQSEvent event, - final Class> handler, - final Class... nonRetryableExceptions) { - return batchProcessor(event, false, handler, nonRetryableExceptions); - } - - /** - * This utility method is used to process each {@link SQSMessage} inside the received {@link SQSEvent} - * - *

    - * The Utility will call {@link SqsMessageHandler#process(SQSMessage)} method for each {@link SQSMessage} - * in the received {@link SQSEvent} - *

    - *

    - * If any exception is thrown from {@link SqsMessageHandler#process(SQSMessage)} during processing of a message, - * the utility will take care of deleting all the successful messages from SQS. When one or more single message fails - * processing due to exception thrown from {@link SqsMessageHandler#process(SQSMessage)} - * {@link SQSBatchProcessingException} is thrown with all the details of successful and failed messages. - *

    - * Exception can also be suppressed if desired. - *

    - * If all the messages are successfully processes, No SQS messages are deleted explicitly but is rather delegated to - * Lambda execution context for deletion. - *

    - * - * @param event {@link SQSEvent} received by lambda function. - * @param suppressException if this is set to true, No {@link SQSBatchProcessingException} is thrown even on failed - * messages. - * @param handler Class implementing {@link SqsMessageHandler} which will be called for each message in event. - * @return List of values returned by {@link SqsMessageHandler#process(SQSMessage)} while processing each message. - * @throws SQSBatchProcessingException if some messages fail during processing and no suppression enabled. - */ - public static List batchProcessor(final SQSEvent event, - final boolean suppressException, - final Class> handler) { - - SqsMessageHandler handlerInstance = instantiatedHandler(handler); - return batchProcessor(event, suppressException, handlerInstance); - } - - /** - * This utility method is used to process each {@link SQSMessage} inside the received {@link SQSEvent} - * - *

    - * The Utility will call {@link SqsMessageHandler#process(SQSMessage)} method for each {@link SQSMessage} - * in the received {@link SQSEvent} - *

    - *

    - * If any exception is thrown from {@link SqsMessageHandler#process(SQSMessage)} during processing of a message, - * the utility will take care of deleting all the successful messages from SQS. When one or more single message fails - * processing due to exception thrown from {@link SqsMessageHandler#process(SQSMessage)} - * {@link SQSBatchProcessingException} is thrown with all the details of successful and failed messages. - * - *

    - * - *

    - * If all the messages are successfully processes, No SQS messages are deleted explicitly but is rather delegated to - * Lambda execution context for deletion. - *

    - * - *

    - * If you dont want the utility to throw {@link SQSBatchProcessingException} in case of failures but rather suppress - * it, Refer {@link SqsUtils#batchProcessor(SQSEvent, boolean, Class)} - *

    - * - *

    - * If you want certain exceptions to be treated as permanent failures, i.e. exceptions where the result of retrying will - * always be a failure and want these can be immediately moved to the dead letter queue associated to the source SQS queue, - * you can use nonRetryableExceptions parameter to configure such exceptions. - * - * Make sure function execution role has sqs:GetQueueAttributes permission on source SQS queue and sqs:SendMessage, - * sqs:SendMessageBatch permission for configured DLQ. - * - * If there is no DLQ configured on source SQS queue and {@link SqsBatch#nonRetryableExceptions()} attribute is set, if - * nonRetryableExceptions occurs from {@link SqsMessageHandler}, such exceptions will still be treated as temporary - * exceptions and the message will be moved back to source SQS queue for reprocessing. The same behaviour will occur if - * for some reason the utility is unable to move the message to the DLQ. An example of this could be because the function - * is missing the correct permissions. - *

    - * @see Amazon SQS dead-letter queues - * - * @param event {@link SQSEvent} received by lambda function. - * @param suppressException if this is set to true, No {@link SQSBatchProcessingException} is thrown even on failed - * messages. - * @param handler Class implementing {@link SqsMessageHandler} which will be called for each message in event. - * @param nonRetryableExceptions exception classes that are to be treated as permanent exceptions and to be moved - * to DLQ. - * @return List of values returned by {@link SqsMessageHandler#process(SQSMessage)} while processing each message. - * @throws SQSBatchProcessingException if some messages fail during processing. - */ - @SafeVarargs - public static List batchProcessor(final SQSEvent event, - final boolean suppressException, - final Class> handler, - final Class... nonRetryableExceptions) { - - SqsMessageHandler handlerInstance = instantiatedHandler(handler); - return batchProcessor(event, suppressException, handlerInstance, false, nonRetryableExceptions); - } - - /** - * This utility method is used to process each {@link SQSMessage} inside the received {@link SQSEvent} - * - *

    - * The Utility will call {@link SqsMessageHandler#process(SQSMessage)} method for each {@link SQSMessage} - * in the received {@link SQSEvent} - *

    - * - *

    - * If any exception is thrown from {@link SqsMessageHandler#process(SQSMessage)} during processing of a message, - * the utility will take care of deleting all the successful messages from SQS. When one or more single message fails - * processing due to exception thrown from {@link SqsMessageHandler#process(SQSMessage)} - * {@link SQSBatchProcessingException} is thrown with all the details of successful and failed messages. - * - *

    - * - *

    - * If all the messages are successfully processes, No SQS messages are deleted explicitly but is rather delegated to - * Lambda execution context for deletion. - *

    - * - *

    - * If you dont want the utility to throw {@link SQSBatchProcessingException} in case of failures but rather suppress - * it, Refer {@link SqsUtils#batchProcessor(SQSEvent, boolean, Class)} - *

    - * - *

    - * If you want certain exceptions to be treated as permanent failures, i.e. exceptions where the result of retrying will - * always be a failure and want these can be immediately moved to the dead letter queue associated to the source SQS queue, - * you can use nonRetryableExceptions parameter to configure such exceptions. - * - * Make sure function execution role has sqs:GetQueueAttributes permission on source SQS queue and sqs:SendMessage, - * sqs:SendMessageBatch permission for configured DLQ. - * - * If you want such messages to be deleted instead, set deleteNonRetryableMessageFromQueue to true. - * - * If there is no DLQ configured on source SQS queue and {@link SqsBatch#nonRetryableExceptions()} attribute is set, if - * nonRetryableExceptions occurs from {@link SqsMessageHandler}, such exceptions will still be treated as temporary - * exceptions and the message will be moved back to source SQS queue for reprocessing. The same behaviour will occur if - * for some reason the utility is unable to move the message to the DLQ. An example of this could be because the function - * is missing the correct permissions. - *

    - * @see Amazon SQS dead-letter queues - * @param event {@link SQSEvent} received by lambda function. - * @param suppressException if this is set to true, No {@link SQSBatchProcessingException} is thrown even on failed - * messages. - * @param handler Class implementing {@link SqsMessageHandler} which will be called for each message in event. - * @param deleteNonRetryableMessageFromQueue If messages with nonRetryableExceptions are to be deleted from SQS queue. - * @param nonRetryableExceptions exception classes that are to be treated as permanent exceptions and to be moved - * to DLQ. - * @return List of values returned by {@link SqsMessageHandler#process(SQSMessage)} while processing each message. - * @throws SQSBatchProcessingException if some messages fail during processing. - */ - @SafeVarargs - public static List batchProcessor(final SQSEvent event, - final boolean suppressException, - final Class> handler, - final boolean deleteNonRetryableMessageFromQueue, - final Class... nonRetryableExceptions) { - - SqsMessageHandler handlerInstance = instantiatedHandler(handler); - return batchProcessor(event, suppressException, handlerInstance, deleteNonRetryableMessageFromQueue, nonRetryableExceptions); - } - - /** - * This utility method is used to process each {@link SQSMessage} inside the received {@link SQSEvent} - * - *

    - * The Utility will call {@link SqsMessageHandler#process(SQSMessage)} method for each {@link SQSMessage} - * in the received {@link SQSEvent} - *

    - * - *

    - * If any exception is thrown from {@link SqsMessageHandler#process(SQSMessage)} during processing of a messages, - * Utility will take care of deleting all the successful messages from SQS. When one or more single message fails - * processing due to exception thrown from {@link SqsMessageHandler#process(SQSMessage)} - * {@link SQSBatchProcessingException} is thrown with all the details of successful and failed messages. - *

    - * If all the messages are successfully processes, No SQS messages are deleted explicitly but is rather delegated to - * Lambda execution context for deletion. - *

    - * - *

    - * If you dont want the utility to throw {@link SQSBatchProcessingException} in case of failures but rather suppress - * it, Refer {@link SqsUtils#batchProcessor(SQSEvent, boolean, SqsMessageHandler)} - *

    - * - * @param event {@link SQSEvent} received by lambda function. - * @param handler Instance of class implementing {@link SqsMessageHandler} which will be called for each message in event. - * @return List of values returned by {@link SqsMessageHandler#process(SQSMessage)} while processing each message- - * @throws SQSBatchProcessingException if some messages fail during processing. - */ - public static List batchProcessor(final SQSEvent event, - final SqsMessageHandler handler) { - return batchProcessor(event, false, handler); - } - - - /** - * This utility method is used to process each {@link SQSMessage} inside the received {@link SQSEvent} - * - *

    - * The Utility will call {@link SqsMessageHandler#process(SQSMessage)} method for each {@link SQSMessage} - * in the received {@link SQSEvent} - *

    - * - *

    - * If any exception is thrown from {@link SqsMessageHandler#process(SQSMessage)} during processing of a message, - * the utility will take care of deleting all the successful messages from SQS. When one or more single message fails - * processing due to exception thrown from {@link SqsMessageHandler#process(SQSMessage)} - * {@link SQSBatchProcessingException} is thrown with all the details of successful and failed messages. - * - *

    - * - *

    - * If all the messages are successfully processes, No SQS messages are deleted explicitly but is rather delegated to - * Lambda execution context for deletion. - *

    - * - *

    - * If you dont want the utility to throw {@link SQSBatchProcessingException} in case of failures but rather suppress - * it, Refer {@link SqsUtils#batchProcessor(SQSEvent, boolean, Class)} - *

    - * - *

    - * If you want certain exceptions to be treated as permanent failures, i.e. exceptions where the result of retrying will - * always be a failure and want these can be immediately moved to the dead letter queue associated to the source SQS queue, - * you can use nonRetryableExceptions parameter to configure such exceptions. - * - * Make sure function execution role has sqs:GetQueueAttributes permission on source SQS queue and sqs:SendMessage, - * sqs:SendMessageBatch permission for configured DLQ. - * - * If there is no DLQ configured on source SQS queue and {@link SqsBatch#nonRetryableExceptions()} attribute is set, if - * nonRetryableExceptions occurs from {@link SqsMessageHandler}, such exceptions will still be treated as temporary - * exceptions and the message will be moved back to source SQS queue for reprocessing.The same behaviour will occur if - * for some reason the utility is unable to moved the message to the DLQ. An example of this could be because the function - * is missing the correct permissions. - *

    - * @see Amazon SQS dead-letter queues - * @param event {@link SQSEvent} received by lambda function. - * @param handler Instance of class implementing {@link SqsMessageHandler} which will be called for each message in event. - * @param nonRetryableExceptions exception classes that are to be treated as permanent exceptions and to be moved - * to DLQ. - * @return List of values returned by {@link SqsMessageHandler#process(SQSMessage)} while processing each message. - * @throws SQSBatchProcessingException if some messages fail during processing. - */ - @SafeVarargs - public static List batchProcessor(final SQSEvent event, - final SqsMessageHandler handler, - final Class... nonRetryableExceptions) { - return batchProcessor(event, false, handler, false, nonRetryableExceptions); - } - - - /** - * This utility method is used to process each {@link SQSMessage} inside the received {@link SQSEvent} - * - *

    - * The Utility will call {@link SqsMessageHandler#process(SQSMessage)} method for each {@link SQSMessage} - * in the received {@link SQSEvent} - *

    - * - *

    - * If any exception is thrown from {@link SqsMessageHandler#process(SQSMessage)} during processing of a messages, - * the utility will take care of deleting all the successful messages from SQS. When one or more single message fails - * processing due to exception thrown from {@link SqsMessageHandler#process(SQSMessage)} - * {@link SQSBatchProcessingException} is thrown with all the details of successful and failed messages. - *

    - * Exception can also be suppressed if desired. - *

    - * If all the messages are successfully processes, No SQS messages are deleted explicitly but is rather delegated to - * Lambda execution context for deletion. - *

    - * - * @param event {@link SQSEvent} received by lambda function. - * @param suppressException if this is set to true, No {@link SQSBatchProcessingException} is thrown even on failed - * messages. - * @param handler Instance of class implementing {@link SqsMessageHandler} which will be called for each message in event. - * @return List of values returned by {@link SqsMessageHandler#process(SQSMessage)} while processing each message. - * @throws SQSBatchProcessingException if some messages fail during processing and no suppression enabled. - */ - public static List batchProcessor(final SQSEvent event, - final boolean suppressException, - final SqsMessageHandler handler) { - return batchProcessor(event, suppressException, handler, false); - - } - - @SafeVarargs - public static List batchProcessor(final SQSEvent event, - final boolean suppressException, - final SqsMessageHandler handler, - final boolean deleteNonRetryableMessageFromQueue, - final Class... nonRetryableExceptions) { - final List handlerReturn = new ArrayList<>(); - - if(client == null) { - client = SqsClient.create(); - } - - BatchContext batchContext = new BatchContext(client); - - for (SQSMessage message : event.getRecords()) { - try { - handlerReturn.add(handler.process(message)); - batchContext.addSuccess(message); - } catch (Exception e) { - batchContext.addFailure(message, e); - LOG.error("Encountered issue processing message: {}", message.getMessageId(), e); - } - } - - batchContext.processSuccessAndHandleFailed(handlerReturn, suppressException, deleteNonRetryableMessageFromQueue, nonRetryableExceptions); - - return handlerReturn; - } - - private static SqsMessageHandler instantiatedHandler(final Class> handler) { - - try { - if (null == handler.getDeclaringClass()) { - return handler.getDeclaredConstructor().newInstance(); - } - - final Constructor> constructor = handler.getDeclaredConstructor(handler.getDeclaringClass()); - constructor.setAccessible(true); - return constructor.newInstance(handler.getDeclaringClass().getDeclaredConstructor().newInstance()); - } catch (Exception e) { - LOG.error("Failed creating handler instance", e); - throw new RuntimeException("Unexpected error occurred. Please raise issue at " + - "https://github.com/aws-powertools/powertools-lambda-java/issues", e); - } - } - - private static SQSMessage clonedMessage(final SQSMessage sqsMessage) { - try { - return objectMapper - .readValue(objectMapper.writeValueAsString(sqsMessage), SQSMessage.class); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - } - - public static ObjectMapper objectMapper() { - return objectMapper; - } - - public static S3Client s3Client() { - if(null == s3Client) { - SqsUtils.s3Client = S3Client.create(); - } - - return s3Client; - } -} diff --git a/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/internal/BatchContext.java b/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/internal/BatchContext.java deleted file mode 100644 index 1e4eff3bf..000000000 --- a/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/internal/BatchContext.java +++ /dev/null @@ -1,251 +0,0 @@ -package software.amazon.lambda.powertools.sqs.internal; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.IntStream; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import software.amazon.awssdk.core.SdkBytes; -import software.amazon.awssdk.services.sqs.SqsClient; -import software.amazon.awssdk.services.sqs.model.DeleteMessageBatchRequest; -import software.amazon.awssdk.services.sqs.model.DeleteMessageBatchRequestEntry; -import software.amazon.awssdk.services.sqs.model.DeleteMessageBatchResponse; -import software.amazon.awssdk.services.sqs.model.GetQueueAttributesRequest; -import software.amazon.awssdk.services.sqs.model.GetQueueAttributesResponse; -import software.amazon.awssdk.services.sqs.model.MessageAttributeValue; -import software.amazon.awssdk.services.sqs.model.QueueAttributeName; -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.sqs.SQSBatchProcessingException; -import software.amazon.lambda.powertools.sqs.SqsUtils; - -import static com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; -import static java.lang.String.format; -import static java.util.Optional.ofNullable; -import static java.util.stream.Collectors.toList; - -public final class BatchContext { - private static final Logger LOG = LoggerFactory.getLogger(BatchContext.class); - private static final Map QUEUE_ARN_TO_DLQ_URL_MAPPING = new HashMap<>(); - - private final Map messageToException = new HashMap<>(); - private final List success = new ArrayList<>(); - - private final SqsClient client; - - public BatchContext(SqsClient client) { - this.client = client; - } - - public void addSuccess(SQSMessage event) { - success.add(event); - } - - public void addFailure(SQSMessage event, Exception e) { - messageToException.put(event, e); - } - - @SafeVarargs - public final void processSuccessAndHandleFailed(final List successReturns, - final boolean suppressException, - final boolean deleteNonRetryableMessageFromQueue, - final Class... nonRetryableExceptions) { - if (hasFailures()) { - - List exceptions = new ArrayList<>(); - List failedMessages = new ArrayList<>(); - Map nonRetryableMessageToException = new HashMap<>(); - - if (nonRetryableExceptions.length == 0) { - exceptions.addAll(messageToException.values()); - failedMessages.addAll(messageToException.keySet()); - } else { - messageToException.forEach((sqsMessage, exception) -> { - boolean nonRetryableException = isNonRetryableException(exception, nonRetryableExceptions); - - if (nonRetryableException) { - nonRetryableMessageToException.put(sqsMessage, exception); - } else { - exceptions.add(exception); - failedMessages.add(sqsMessage); - } - }); - } - - List messagesToBeDeleted = new ArrayList<>(success); - - if (!nonRetryableMessageToException.isEmpty() && deleteNonRetryableMessageFromQueue) { - messagesToBeDeleted.addAll(nonRetryableMessageToException.keySet()); - } else if (!nonRetryableMessageToException.isEmpty()) { - - boolean isMovedToDlq = moveNonRetryableMessagesToDlqIfConfigured(nonRetryableMessageToException); - - if (!isMovedToDlq) { - exceptions.addAll(nonRetryableMessageToException.values()); - failedMessages.addAll(nonRetryableMessageToException.keySet()); - } - } - - deleteMessagesFromQueue(messagesToBeDeleted); - - processFailedMessages(successReturns, suppressException, exceptions, failedMessages); - } - } - - private void processFailedMessages(List successReturns, - boolean suppressException, - List exceptions, - List failedMessages) { - if (failedMessages.isEmpty()) { - return; - } - - if (suppressException) { - List messageIds = failedMessages.stream(). - map(SQSMessage::getMessageId) - .collect(toList()); - - LOG.debug(format("[%d] records failed processing, but exceptions are suppressed. " + - "Failed messages %s", failedMessages.size(), messageIds)); - } else { - throw new SQSBatchProcessingException(exceptions, failedMessages, successReturns); - } - } - - private boolean isNonRetryableException(Exception exception, Class[] nonRetryableExceptions) { - return Arrays.stream(nonRetryableExceptions) - .anyMatch(aClass -> aClass.isInstance(exception)); - } - - private boolean moveNonRetryableMessagesToDlqIfConfigured(Map nonRetryableMessageToException) { - Optional dlqUrl = fetchDlqUrl(nonRetryableMessageToException); - - if (!dlqUrl.isPresent()) { - return false; - } - - List dlqMessages = nonRetryableMessageToException.keySet().stream() - .map(sqsMessage -> { - Map messageAttributesMap = new HashMap<>(); - - sqsMessage.getMessageAttributes().forEach((s, messageAttribute) -> { - MessageAttributeValue.Builder builder = MessageAttributeValue.builder(); - - builder - .dataType(messageAttribute.getDataType()) - .stringValue(messageAttribute.getStringValue()); - - if (null != messageAttribute.getBinaryValue()) { - builder.binaryValue(SdkBytes.fromByteBuffer(messageAttribute.getBinaryValue())); - } - - messageAttributesMap.put(s, builder.build()); - }); - - return SendMessageBatchRequestEntry.builder() - .messageBody(sqsMessage.getBody()) - .id(sqsMessage.getMessageId()) - .messageAttributes(messageAttributesMap) - .build(); - }) - .collect(toList()); - - List sendMessageBatchResponses = batchRequest(dlqMessages, 10, entriesToSend -> { - - SendMessageBatchResponse sendMessageBatchResponse = client.sendMessageBatch(SendMessageBatchRequest.builder() - .entries(entriesToSend) - .queueUrl(dlqUrl.get()) - .build()); - - - LOG.debug("Response from send batch message to DLQ request {}", sendMessageBatchResponse); - - return sendMessageBatchResponse; - }); - - return sendMessageBatchResponses.stream() - .filter(response -> null != response && response.hasFailed()) - .peek(sendMessageBatchResponse -> LOG.error("Failed sending message to the DLQ. Entire batch will be re processed. Check if needed permissions are configured for the function. Response: {}", sendMessageBatchResponse)) - .count() == 0; - } - - - private Optional fetchDlqUrl(Map nonRetryableMessageToException) { - return nonRetryableMessageToException.keySet().stream() - .findFirst() - .map(sqsMessage -> QUEUE_ARN_TO_DLQ_URL_MAPPING.computeIfAbsent(sqsMessage.getEventSourceArn(), sourceArn -> { - String queueUrl = url(sourceArn); - - GetQueueAttributesResponse queueAttributes = client.getQueueAttributes(GetQueueAttributesRequest.builder() - .attributeNames(QueueAttributeName.REDRIVE_POLICY) - .queueUrl(queueUrl) - .build()); - - return ofNullable(queueAttributes.attributes().get(QueueAttributeName.REDRIVE_POLICY)) - .map(policy -> { - try { - return SqsUtils.objectMapper().readTree(policy); - } catch (JsonProcessingException e) { - LOG.debug("Unable to parse Re drive policy for queue {}. Even if DLQ exists, failed messages will be send back to main queue.", queueUrl, e); - return null; - } - }) - .map(node -> node.get("deadLetterTargetArn")) - .map(JsonNode::asText) - .map(this::url) - .orElse(null); - })); - } - - private boolean hasFailures() { - return !messageToException.isEmpty(); - } - - private void deleteMessagesFromQueue(final List messages) { - if (!messages.isEmpty()) { - - List entries = messages.stream().map(m -> DeleteMessageBatchRequestEntry.builder() - .id(m.getMessageId()) - .receiptHandle(m.getReceiptHandle()) - .build()).collect(toList()); - - batchRequest(entries, 10, entriesToDelete -> { - DeleteMessageBatchRequest request = DeleteMessageBatchRequest.builder() - .queueUrl(url(messages.get(0).getEventSourceArn())) - .entries(entriesToDelete) - .build(); - - DeleteMessageBatchResponse deleteMessageBatchResponse = client.deleteMessageBatch(request); - - LOG.debug("Response from delete request {}", deleteMessageBatchResponse); - - return deleteMessageBatchResponse; - }); - } - } - - private List batchRequest(final List listOFEntries, - final int size, - final Function, R> batchLogic) { - - return IntStream.range(0, listOFEntries.size()) - .filter(index -> index % size == 0) - .mapToObj(index -> listOFEntries.subList(index, Math.min(index + size, listOFEntries.size()))) - .map(batchLogic) - .collect(Collectors.toList()); - } - - private String url(String queueArn) { - String[] arnArray = queueArn.split(":"); - return String.format("https://sqs.%s.amazonaws.com/%s/%s", arnArray[3], arnArray[4], arnArray[5]); - } -} diff --git a/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/internal/SqsLargeMessageAspect.java b/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/internal/SqsLargeMessageAspect.java deleted file mode 100644 index 588d434d7..000000000 --- a/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/internal/SqsLargeMessageAspect.java +++ /dev/null @@ -1,146 +0,0 @@ -package software.amazon.lambda.powertools.sqs.internal; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.function.Function; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.events.SQSEvent; -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.awssdk.core.ResponseInputStream; -import software.amazon.awssdk.core.exception.SdkClientException; -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.awssdk.utils.IoUtils; -import software.amazon.lambda.powertools.sqs.SqsLargeMessage; -import software.amazon.payloadoffloading.PayloadS3Pointer; - -import static com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; -import static java.lang.String.format; -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.isHandlerMethod; -import static software.amazon.lambda.powertools.sqs.SqsUtils.s3Client; - -@Aspect -public class SqsLargeMessageAspect { - - private static final Logger LOG = LoggerFactory.getLogger(SqsLargeMessageAspect.class); - - @SuppressWarnings({"EmptyMethod"}) - @Pointcut("@annotation(sqsLargeMessage)") - public void callAt(SqsLargeMessage sqsLargeMessage) { - } - - @Around(value = "callAt(sqsLargeMessage) && execution(@SqsLargeMessage * *.*(..))", argNames = "pjp,sqsLargeMessage") - public Object around(ProceedingJoinPoint pjp, - SqsLargeMessage sqsLargeMessage) throws Throwable { - Object[] proceedArgs = pjp.getArgs(); - - if (isHandlerMethod(pjp) - && placedOnSqsEventRequestHandler(pjp)) { - List pointersToDelete = rewriteMessages((SQSEvent) proceedArgs[0]); - - Object proceed = pjp.proceed(proceedArgs); - - if (sqsLargeMessage.deletePayloads()) { - pointersToDelete.forEach(SqsLargeMessageAspect::deleteMessage); - } - return proceed; - } - - return pjp.proceed(proceedArgs); - } - - private List rewriteMessages(SQSEvent sqsEvent) { - List records = sqsEvent.getRecords(); - return processMessages(records); - } - - public static List processMessages(final List records) { - List s3Pointers = new ArrayList<>(); - for (SQSMessage sqsMessage : records) { - if (isBodyLargeMessagePointer(sqsMessage.getBody())) { - - PayloadS3Pointer s3Pointer = Optional.ofNullable(PayloadS3Pointer.fromJson(sqsMessage.getBody())) - .orElseThrow(() -> new FailedProcessingLargePayloadException(format("Failed processing SQS body to extract S3 details. [ %s ].", sqsMessage.getBody()))); - - ResponseInputStream s3Object = callS3Gracefully(s3Pointer, pointer -> { - ResponseInputStream response = s3Client().getObject(GetObjectRequest.builder() - .bucket(pointer.getS3BucketName()) - .key(pointer.getS3Key()) - .build()); - - LOG.debug("Object downloaded with key: " + s3Pointer.getS3Key()); - return response; - }); - - sqsMessage.setBody(readStringFromS3Object(s3Object, s3Pointer)); - s3Pointers.add(s3Pointer); - } - } - - return s3Pointers; - } - - private static boolean isBodyLargeMessagePointer(String record) { - return record.startsWith("[\"software.amazon.payloadoffloading.PayloadS3Pointer\""); - } - - private static String readStringFromS3Object(ResponseInputStream response, - PayloadS3Pointer s3Pointer) { - try (ResponseInputStream content = response) { - return IoUtils.toUtf8String(content); - } catch (IOException e) { - LOG.error("Error converting S3 object to String", e); - throw new FailedProcessingLargePayloadException(format("Failed processing S3 record with [Bucket Name: %s Bucket Key: %s]", s3Pointer.getS3BucketName(), s3Pointer.getS3Key()), e); - } - } - - public static void deleteMessage(PayloadS3Pointer s3Pointer) { - callS3Gracefully(s3Pointer, pointer -> { - s3Client().deleteObject(DeleteObjectRequest.builder() - .bucket(pointer.getS3BucketName()) - .key(pointer.getS3Key()) - .build()); - LOG.info("Message deleted from S3: " + s3Pointer.toJson()); - return null; - }); - } - - private static R callS3Gracefully(final PayloadS3Pointer pointer, - final Function function) { - try { - return function.apply(pointer); - } catch (S3Exception e) { - LOG.error("A service exception", e); - throw new FailedProcessingLargePayloadException(format("Failed processing S3 record with [Bucket Name: %s Bucket Key: %s]", pointer.getS3BucketName(), pointer.getS3Key()), e); - } catch (SdkClientException e) { - LOG.error("Some sort of client exception", e); - throw new FailedProcessingLargePayloadException(format("Failed processing S3 record with [Bucket Name: %s Bucket Key: %s]", pointer.getS3BucketName(), pointer.getS3Key()), e); - } - } - - public static boolean placedOnSqsEventRequestHandler(ProceedingJoinPoint pjp) { - return pjp.getArgs().length == 2 - && pjp.getArgs()[0] instanceof SQSEvent - && pjp.getArgs()[1] instanceof Context; - } - - public static class FailedProcessingLargePayloadException extends RuntimeException { - public FailedProcessingLargePayloadException(String message, Throwable cause) { - super(message, cause); - } - - public FailedProcessingLargePayloadException(String message) { - super(message); - } - } -} diff --git a/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/internal/SqsMessageBatchProcessorAspect.java b/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/internal/SqsMessageBatchProcessorAspect.java deleted file mode 100644 index 73e91c3a7..000000000 --- a/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/internal/SqsMessageBatchProcessorAspect.java +++ /dev/null @@ -1,41 +0,0 @@ -package software.amazon.lambda.powertools.sqs.internal; - -import com.amazonaws.services.lambda.runtime.events.SQSEvent; -import org.aspectj.lang.ProceedingJoinPoint; -import org.aspectj.lang.annotation.Around; -import org.aspectj.lang.annotation.Aspect; -import org.aspectj.lang.annotation.Pointcut; -import software.amazon.lambda.powertools.sqs.SqsBatch; - -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.isHandlerMethod; -import static software.amazon.lambda.powertools.sqs.SqsUtils.batchProcessor; -import static software.amazon.lambda.powertools.sqs.internal.SqsLargeMessageAspect.placedOnSqsEventRequestHandler; - -@Aspect -public class SqsMessageBatchProcessorAspect { - - @SuppressWarnings({"EmptyMethod"}) - @Pointcut("@annotation(sqsBatch)") - public void callAt(SqsBatch sqsBatch) { - } - - @Around(value = "callAt(sqsBatch) && execution(@SqsBatch * *.*(..))", argNames = "pjp,sqsBatch") - public Object around(ProceedingJoinPoint pjp, - SqsBatch sqsBatch) throws Throwable { - Object[] proceedArgs = pjp.getArgs(); - - if (isHandlerMethod(pjp) - && placedOnSqsEventRequestHandler(pjp)) { - - SQSEvent sqsEvent = (SQSEvent) proceedArgs[0]; - - batchProcessor(sqsEvent, - sqsBatch.suppressException(), - sqsBatch.value(), - sqsBatch.deleteNonRetryableMessageFromQueue(), - sqsBatch.nonRetryableExceptions()); - } - - return pjp.proceed(proceedArgs); - } -} diff --git a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/SampleSqsHandler.java b/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/SampleSqsHandler.java deleted file mode 100644 index d48cded5f..000000000 --- a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/SampleSqsHandler.java +++ /dev/null @@ -1,12 +0,0 @@ -package software.amazon.lambda.powertools.sqs; - -import com.amazonaws.services.lambda.runtime.events.SQSEvent; - -public class SampleSqsHandler implements SqsMessageHandler { - private int counter; - - @Override - public String process(SQSEvent.SQSMessage message) { - return String.valueOf(counter++); - } -} diff --git a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/SqsUtilsBatchProcessorTest.java b/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/SqsUtilsBatchProcessorTest.java deleted file mode 100644 index 0d1c9c35a..000000000 --- a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/SqsUtilsBatchProcessorTest.java +++ /dev/null @@ -1,361 +0,0 @@ -package software.amazon.lambda.powertools.sqs; - -import java.io.IOException; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.function.Consumer; -import java.util.function.Function; -import com.amazonaws.services.lambda.runtime.events.SQSEvent; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; -import org.mockito.ArgumentCaptor; -import software.amazon.awssdk.services.sqs.SqsClient; -import software.amazon.awssdk.services.sqs.model.DeleteMessageBatchRequest; -import software.amazon.awssdk.services.sqs.model.GetQueueAttributesRequest; -import software.amazon.awssdk.services.sqs.model.GetQueueAttributesResponse; -import software.amazon.awssdk.services.sqs.model.QueueAttributeName; -import software.amazon.awssdk.services.sqs.model.SendMessageBatchRequest; -import software.amazon.awssdk.services.sqs.model.SendMessageBatchRequestEntry; - -import static com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; -import static org.assertj.core.api.Assertions.*; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.when; -import static software.amazon.lambda.powertools.sqs.SqsUtils.batchProcessor; -import static software.amazon.lambda.powertools.sqs.SqsUtils.overrideSqsClient; - -class SqsUtilsBatchProcessorTest { - - private static final SqsClient sqsClient = mock(SqsClient.class); - private static final SqsClient interactionClient = mock(SqsClient.class); - private static final ObjectMapper MAPPER = new ObjectMapper(); - private SQSEvent event; - - @BeforeEach - void setUp() throws IOException { - reset(sqsClient, interactionClient); - event = MAPPER.readValue(this.getClass().getResource("/sampleSqsBatchEvent.json"), SQSEvent.class); - overrideSqsClient(sqsClient); - } - - @Test - void shouldBatchProcessAndNotDeleteMessagesWhenAllSuccess() { - List returnValues = batchProcessor(event, false, (message) -> { - interactionClient.listQueues(); - return "Success"; - }); - - assertThat(returnValues) - .hasSize(2) - .containsExactly("Success", "Success"); - - verify(interactionClient, times(2)).listQueues(); - verifyNoInteractions(sqsClient); - } - - @ParameterizedTest - @ValueSource(classes = {SampleInnerSqsHandler.class, SampleSqsHandler.class}) - void shouldBatchProcessViaClassAndNotDeleteMessagesWhenAllSuccess(Class> handler) { - List returnValues = batchProcessor(event, handler); - - assertThat(returnValues) - .hasSize(2) - .containsExactly("0", "1"); - - verifyNoInteractions(sqsClient); - } - - @Test - void shouldBatchProcessAndDeleteSuccessMessageOnPartialFailures() { - String failedId = "2e1424d4-f796-459a-8184-9c92662be6da"; - - SqsMessageHandler failedHandler = (message) -> { - if (failedId.equals(message.getMessageId())) { - throw new RuntimeException("Failed processing"); - } - - interactionClient.listQueues(); - return "Success"; - }; - - assertThatExceptionOfType(SQSBatchProcessingException.class) - .isThrownBy(() -> batchProcessor(event, failedHandler)) - .satisfies(e -> { - - assertThat(e.successMessageReturnValues()) - .hasSize(1) - .contains("Success"); - - assertThat(e.getFailures()) - .hasSize(1) - .extracting("messageId") - .contains(failedId); - - assertThat(e.getExceptions()) - .hasSize(1) - .extracting("detailMessage") - .contains("Failed processing"); - }); - - verify(interactionClient).listQueues(); - - ArgumentCaptor captor = ArgumentCaptor.forClass(DeleteMessageBatchRequest.class); - verify(sqsClient).deleteMessageBatch(captor.capture()); - - assertThat(captor.getValue()) - .hasFieldOrPropertyWithValue("queueUrl", "https://sqs.us-east-2.amazonaws.com/123456789012/my-queue"); - } - - @Test - void shouldBatchProcessAndFullFailuresInBatch() { - SqsMessageHandler failedHandler = (message) -> { - throw new RuntimeException(message.getMessageId()); - }; - - assertThatExceptionOfType(SQSBatchProcessingException.class) - .isThrownBy(() -> batchProcessor(event, failedHandler)) - .satisfies(e -> { - - assertThat(e.successMessageReturnValues()) - .isEmpty(); - - assertThat(e.getFailures()) - .hasSize(2) - .extracting("messageId") - .containsExactly("059f36b4-87a3-44ab-83d2-661975830a7d", - "2e1424d4-f796-459a-8184-9c92662be6da"); - - assertThat(e.getExceptions()) - .hasSize(2) - .extracting("detailMessage") - .containsExactly("059f36b4-87a3-44ab-83d2-661975830a7d", - "2e1424d4-f796-459a-8184-9c92662be6da"); - }); - - verifyNoInteractions(sqsClient); - } - - @Test - void shouldBatchProcessViaClassAndDeleteSuccessMessageOnPartialFailures() { - assertThatExceptionOfType(SQSBatchProcessingException.class) - .isThrownBy(() -> batchProcessor(event, FailureSampleInnerSqsHandler.class)) - .satisfies(e -> { - - assertThat(e.successMessageReturnValues()) - .hasSize(1) - .contains("Success"); - - assertThat(e.getFailures()) - .hasSize(1) - .extracting("messageId") - .contains("2e1424d4-f796-459a-8184-9c92662be6da"); - - assertThat(e.getExceptions()) - .hasSize(1) - .extracting("detailMessage") - .contains("Failed processing"); - }); - - verify(sqsClient).deleteMessageBatch(any(DeleteMessageBatchRequest.class)); - } - - - @Test - void shouldBatchProcessAndSuppressExceptions() { - String failedId = "2e1424d4-f796-459a-8184-9c92662be6da"; - - SqsMessageHandler failedHandler = (message) -> { - if (failedId.equals(message.getMessageId())) { - throw new RuntimeException("Failed processing"); - } - - interactionClient.listQueues(); - return "Success"; - }; - - List returnValues = batchProcessor(event, true, failedHandler); - - assertThat(returnValues) - .hasSize(1) - .contains("Success"); - - verify(interactionClient).listQueues(); - verify(sqsClient).deleteMessageBatch(any(DeleteMessageBatchRequest.class)); - } - - @Test - void shouldBatchProcessViaClassAndSuppressExceptions() { - List returnValues = batchProcessor(event, true, FailureSampleInnerSqsHandler.class); - - assertThat(returnValues) - .hasSize(1) - .contains("Success"); - - verify(interactionClient).listQueues(); - verify(sqsClient).deleteMessageBatch(any(DeleteMessageBatchRequest.class)); - } - - public class SampleInnerSqsHandler implements SqsMessageHandler { - private int counter; - - @Override - public String process(SQSMessage message) { - interactionClient.listQueues(); - return String.valueOf(counter++); - } - } - - @Test - void shouldBatchProcessAndMoveNonRetryableExceptionToDlq() { - String failedId = "2e1424d4-f796-459a-8184-9c92662be6da"; - HashMap attributes = new HashMap<>(); - - attributes.put(QueueAttributeName.REDRIVE_POLICY, "{\n" + - " \"deadLetterTargetArn\": \"arn:aws:sqs:us-east-2:123456789012:retry-queue\",\n" + - " \"maxReceiveCount\": 2\n" + - "}"); - - when(sqsClient.getQueueAttributes(any(GetQueueAttributesRequest.class))).thenReturn(GetQueueAttributesResponse.builder() - .attributes(attributes) - .build()); - - List batchProcessor = batchProcessor(event, (message) -> { - if (failedId.equals(message.getMessageId())) { - throw new IllegalStateException("Failed processing"); - } - - interactionClient.listQueues(); - return "Success"; - }, IllegalStateException.class, IllegalArgumentException.class); - - assertThat(batchProcessor) - .hasSize(1); - - verify(sqsClient).sendMessageBatch(any(SendMessageBatchRequest.class)); - } - - @Test - void shouldBatchProcessAndDeleteNonRetryableException() { - String failedId = "2e1424d4-f796-459a-8184-9c92662be6da"; - HashMap attributes = new HashMap<>(); - - attributes.put(QueueAttributeName.REDRIVE_POLICY, "{\n" + - " \"deadLetterTargetArn\": \"arn:aws:sqs:us-east-2:123456789012:retry-queue\",\n" + - " \"maxReceiveCount\": 2\n" + - "}"); - - when(sqsClient.getQueueAttributes(any(GetQueueAttributesRequest.class))).thenReturn(GetQueueAttributesResponse.builder() - .attributes(attributes) - .build()); - - List batchProcessor = batchProcessor(event, false, (message) -> { - if (failedId.equals(message.getMessageId())) { - throw new IllegalStateException("Failed processing"); - } - - interactionClient.listQueues(); - return "Success"; - }, true, IllegalStateException.class, IllegalArgumentException.class); - - assertThat(batchProcessor) - .hasSize(1); - - verify(sqsClient, times(0)).sendMessageBatch(any(SendMessageBatchRequest.class)); - verify(sqsClient).deleteMessageBatch(any(DeleteMessageBatchRequest.class)); - } - - @Test - void shouldDeleteSuccessfulMessageInBatchesOfT10orLess() throws IOException { - SQSEvent batch25Message = MAPPER.readValue(this.getClass().getResource("/sampleSqsBatchEventBatchSize25.json"), SQSEvent.class); - - assertThatExceptionOfType(SQSBatchProcessingException.class) - .isThrownBy(() -> batchProcessor(batch25Message, FailureSampleInnerSqsHandler.class)) - .satisfies(e -> { - - assertThat(e.successMessageReturnValues()) - .hasSize(24) - .contains("Success"); - - assertThat(e.getFailures()) - .hasSize(1) - .extracting("messageId") - .contains("2e1424d4-f796-459a-8184-9c92662be6da"); - - assertThat(e.getExceptions()) - .hasSize(1) - .extracting("detailMessage") - .contains("Failed processing"); - }); - - ArgumentCaptor captor = ArgumentCaptor.forClass(DeleteMessageBatchRequest.class); - - verify(sqsClient, times(3)).deleteMessageBatch(captor.capture()); - - assertThat(captor.getAllValues()) - .hasSize(3) - .flatMap(DeleteMessageBatchRequest::entries) - .hasSize(24); - } - - @Test - void shouldBatchProcessAndMoveNonRetryableExceptionToDlqInBatchesOfT10orLess() throws IOException { - SQSEvent batch25Message = MAPPER.readValue(this.getClass().getResource("/sampleSqsBatchEventBatchSize25.json"), SQSEvent.class); - - HashMap attributes = new HashMap<>(); - - attributes.put(QueueAttributeName.REDRIVE_POLICY, "{\n" + - " \"deadLetterTargetArn\": \"arn:aws:sqs:us-east-2:123456789012:retry-queue\",\n" + - " \"maxReceiveCount\": 2\n" + - "}"); - - when(sqsClient.getQueueAttributes(any(GetQueueAttributesRequest.class))).thenReturn(GetQueueAttributesResponse.builder() - .attributes(attributes) - .build()); - - List batchProcessor = batchProcessor(batch25Message, (message) -> { - if ("2e1424d4-f796-459a-8184-9c92662be6da".equals(message.getMessageId())) { - interactionClient.listQueues(); - return "Success"; - } - - throw new IllegalStateException("Failed processing"); - }, IllegalStateException.class, IllegalArgumentException.class); - - assertThat(batchProcessor) - .hasSize(1); - - ArgumentCaptor captor = ArgumentCaptor.forClass(SendMessageBatchRequest.class); - - - verify(sqsClient, times(3)).sendMessageBatch(captor.capture()); - - assertThat(captor.getAllValues()) - .hasSize(3) - .flatMap(SendMessageBatchRequest::entries) - .hasSize(24); - } - - public class FailureSampleInnerSqsHandler implements SqsMessageHandler { - @Override - public String process(SQSEvent.SQSMessage message) { - if ("2e1424d4-f796-459a-8184-9c92662be6da".equals(message.getMessageId())) { - throw new RuntimeException("Failed processing"); - } - - interactionClient.listQueues(); - return "Success"; - } - } -} \ No newline at end of file diff --git a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/SqsUtilsLargeMessageTest.java b/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/SqsUtilsLargeMessageTest.java deleted file mode 100644 index 48de3e6a9..000000000 --- a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/SqsUtilsLargeMessageTest.java +++ /dev/null @@ -1,194 +0,0 @@ -package software.amazon.lambda.powertools.sqs; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; -import java.util.function.Consumer; -import java.util.stream.Stream; - -import com.amazonaws.services.lambda.runtime.events.SQSEvent; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.params.provider.ValueSource; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import software.amazon.awssdk.core.ResponseInputStream; -import software.amazon.awssdk.core.exception.SdkClientException; -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.awssdk.utils.StringInputStream; -import software.amazon.lambda.powertools.sqs.internal.SqsLargeMessageAspect; - -import static com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; -import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -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; - -class SqsUtilsLargeMessageTest { - - @Mock - private S3Client s3Client; - private static final String BUCKET_NAME = "ms-extended-sqs-client"; - private static final String BUCKET_KEY = "c71eb2ae-37e0-4265-8909-32f4153faddf"; - - @BeforeEach - void setUp() { - openMocks(this); - SqsUtils.overrideS3Client(s3Client); - } - - @Test - public void testLargeMessage() { - ResponseInputStream s3Response = new ResponseInputStream<>(GetObjectResponse.builder().build(), AbortableInputStream.create(new ByteArrayInputStream("A big message".getBytes()))); - when(s3Client.getObject(any(GetObjectRequest.class))).thenReturn(s3Response); - - SQSEvent sqsEvent = messageWithBody("[\"software.amazon.payloadoffloading.PayloadS3Pointer\",{\"s3BucketName\":\"" + BUCKET_NAME + "\",\"s3Key\":\"" + BUCKET_KEY + "\"}]"); - - Map sqsMessage = SqsUtils.enrichedMessageFromS3(sqsEvent, sqsMessages -> { - Map someBusinessLogic = new HashMap<>(); - someBusinessLogic.put("Message", sqsMessages.get(0).getBody()); - return someBusinessLogic; - }); - - assertThat(sqsMessage) - .hasSize(1) - .containsEntry("Message", "A big message"); - - 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); - }); - } - - @ParameterizedTest - @ValueSource(booleans = {true, false}) - public void testLargeMessageDeleteFromS3Toggle(boolean deleteS3Payload) { - ResponseInputStream s3Response = new ResponseInputStream<>(GetObjectResponse.builder().build(), AbortableInputStream.create(new ByteArrayInputStream("A big message".getBytes()))); - - when(s3Client.getObject(any(GetObjectRequest.class))).thenReturn(s3Response); - - SQSEvent sqsEvent = messageWithBody("[\"software.amazon.payloadoffloading.PayloadS3Pointer\",{\"s3BucketName\":\"" + BUCKET_NAME + "\",\"s3Key\":\"" + BUCKET_KEY + "\"}]"); - - Map sqsMessage = SqsUtils.enrichedMessageFromS3(sqsEvent, deleteS3Payload, sqsMessages -> { - Map someBusinessLogic = new HashMap<>(); - someBusinessLogic.put("Message", sqsMessages.get(0).getBody()); - return someBusinessLogic; - }); - - assertThat(sqsMessage) - .hasSize(1) - .containsEntry("Message", "A big message"); - if (deleteS3Payload) { - 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); - }); - } else { - verify(s3Client, never()).deleteObject(any(DeleteObjectRequest.class)); - } - } - - @Test - public void shouldNotProcessSmallMessageBody() { - ResponseInputStream s3Response = new ResponseInputStream<>(GetObjectResponse.builder().build(), AbortableInputStream.create(new ByteArrayInputStream("A big message".getBytes()))); - - when(s3Client.getObject(any(GetObjectRequest.class))).thenReturn(s3Response); - - SQSEvent sqsEvent = messageWithBody("This is small message"); - - Map sqsMessage = SqsUtils.enrichedMessageFromS3(sqsEvent, sqsMessages -> { - Map someBusinessLogic = new HashMap<>(); - someBusinessLogic.put("Message", sqsMessages.get(0).getBody()); - return someBusinessLogic; - }); - - assertThat(sqsMessage) - .containsEntry("Message", "This is small message"); - - verifyNoInteractions(s3Client); - } - - @ParameterizedTest - @MethodSource("exception") - public void shouldFailEntireBatchIfFailedDownloadingFromS3(RuntimeException exception) { - when(s3Client.getObject(any(GetObjectRequest.class))).thenThrow(exception); - - String messageBody = "[\"software.amazon.payloadoffloading.PayloadS3Pointer\",{\"s3BucketName\":\"" + BUCKET_NAME + "\",\"s3Key\":\"" + BUCKET_KEY + "\"}]"; - SQSEvent sqsEvent = messageWithBody(messageBody); - - assertThatExceptionOfType(SqsLargeMessageAspect.FailedProcessingLargePayloadException.class) - .isThrownBy(() -> SqsUtils.enrichedMessageFromS3(sqsEvent, sqsMessages -> sqsMessages.get(0).getBody())) - .withCause(exception); - - verify(s3Client, never()).deleteObject(any(DeleteObjectRequest.class)); - } - - @Test - public void shouldFailEntireBatchIfFailedProcessingDownloadMessageFromS3() { - ResponseInputStream s3Response = new ResponseInputStream<>(GetObjectResponse.builder().build(), AbortableInputStream.create(new StringInputStream("test") { - @Override - public void close() throws IOException { - throw new IOException("Failed"); - } - })); - - when(s3Client.getObject(any(GetObjectRequest.class))).thenReturn(s3Response); - - String messageBody = "[\"software.amazon.payloadoffloading.PayloadS3Pointer\",{\"s3BucketName\":\"" + BUCKET_NAME + "\",\"s3Key\":\"" + BUCKET_KEY + "\"}]"; - SQSEvent sqsEvent = messageWithBody(messageBody); - - assertThatExceptionOfType(SqsLargeMessageAspect.FailedProcessingLargePayloadException.class) - .isThrownBy(() -> SqsUtils.enrichedMessageFromS3(sqsEvent, sqsMessages -> sqsMessages.get(0).getBody())) - .withCauseInstanceOf(IOException.class); - - verify(s3Client, never()).deleteObject(any(DeleteObjectRequest.class)); - } - - private static Stream exception() { - return Stream.of(Arguments.of(S3Exception.builder() - .message("Service Exception") - .build()), - Arguments.of(SdkClientException.builder() - .message("Client Exception") - .build())); - } - - private SQSEvent messageWithBody(String messageBody) { - SQSMessage sqsMessage = new SQSMessage(); - sqsMessage.setBody(messageBody); - SQSEvent sqsEvent = new SQSEvent(); - sqsEvent.setRecords(singletonList(sqsMessage)); - return sqsEvent; - } -} \ No newline at end of file diff --git a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/LambdaHandlerApiGateway.java b/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/LambdaHandlerApiGateway.java deleted file mode 100644 index b0d8177ac..000000000 --- a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/LambdaHandlerApiGateway.java +++ /dev/null @@ -1,18 +0,0 @@ -package software.amazon.lambda.powertools.sqs.handlers; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; -import software.amazon.lambda.powertools.sqs.SqsLargeMessage; -import software.amazon.lambda.powertools.sqs.SampleSqsHandler; -import software.amazon.lambda.powertools.sqs.SqsBatch; - -public class LambdaHandlerApiGateway implements RequestHandler { - - @Override - @SqsLargeMessage - @SqsBatch(value = SampleSqsHandler.class) - public String handleRequest(APIGatewayProxyRequestEvent sqsEvent, Context context) { - return sqsEvent.getBody(); - } -} diff --git a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/PartialBatchFailureSuppressedHandler.java b/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/PartialBatchFailureSuppressedHandler.java deleted file mode 100644 index 63f1573bf..000000000 --- a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/PartialBatchFailureSuppressedHandler.java +++ /dev/null @@ -1,32 +0,0 @@ -package software.amazon.lambda.powertools.sqs.handlers; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import com.amazonaws.services.lambda.runtime.events.SQSEvent; -import software.amazon.lambda.powertools.sqs.SqsBatch; -import software.amazon.lambda.powertools.sqs.SqsMessageHandler; - -import static com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; -import static software.amazon.lambda.powertools.sqs.internal.SqsMessageBatchProcessorAspectTest.interactionClient; - -public class PartialBatchFailureSuppressedHandler implements RequestHandler { - @Override - @SqsBatch(value = InnerMessageHandler.class, suppressException = true) - public String handleRequest(final SQSEvent sqsEvent, - final Context context) { - return "Success"; - } - - private class InnerMessageHandler implements SqsMessageHandler { - - @Override - public String process(SQSMessage message) { - if ("2e1424d4-f796-459a-8184-9c92662be6da".equals(message.getMessageId())) { - throw new RuntimeException("2e1424d4-f796-459a-8184-9c92662be6da"); - } - - interactionClient.listQueues(); - return "Success"; - } - } -} diff --git a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/PartialBatchPartialFailureHandler.java b/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/PartialBatchPartialFailureHandler.java deleted file mode 100644 index 653459d82..000000000 --- a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/PartialBatchPartialFailureHandler.java +++ /dev/null @@ -1,32 +0,0 @@ -package software.amazon.lambda.powertools.sqs.handlers; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import com.amazonaws.services.lambda.runtime.events.SQSEvent; -import software.amazon.lambda.powertools.sqs.SqsBatch; -import software.amazon.lambda.powertools.sqs.SqsMessageHandler; - -import static com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; -import static software.amazon.lambda.powertools.sqs.internal.SqsMessageBatchProcessorAspectTest.interactionClient; - -public class PartialBatchPartialFailureHandler implements RequestHandler { - @Override - @SqsBatch(InnerMessageHandler.class) - public String handleRequest(final SQSEvent sqsEvent, - final Context context) { - return "Success"; - } - - private class InnerMessageHandler implements SqsMessageHandler { - - @Override - public String process(SQSMessage message) { - if ("2e1424d4-f796-459a-8184-9c92662be6da".equals(message.getMessageId())) { - throw new RuntimeException("2e1424d4-f796-459a-8184-9c92662be6da"); - } - - interactionClient.listQueues(); - return "Success"; - } - } -} diff --git a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/PartialBatchSuccessHandler.java b/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/PartialBatchSuccessHandler.java deleted file mode 100644 index 926cdb4f5..000000000 --- a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/PartialBatchSuccessHandler.java +++ /dev/null @@ -1,28 +0,0 @@ -package software.amazon.lambda.powertools.sqs.handlers; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import com.amazonaws.services.lambda.runtime.events.SQSEvent; -import software.amazon.lambda.powertools.sqs.SqsBatch; -import software.amazon.lambda.powertools.sqs.SqsMessageHandler; - -import static com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; -import static software.amazon.lambda.powertools.sqs.internal.SqsMessageBatchProcessorAspectTest.interactionClient; - -public class PartialBatchSuccessHandler implements RequestHandler { - @Override - @SqsBatch(InnerMessageHandler.class) - public String handleRequest(final SQSEvent sqsEvent, - final Context context) { - return "Success"; - } - - private class InnerMessageHandler implements SqsMessageHandler { - - @Override - public String process(SQSMessage message) { - interactionClient.listQueues(); - return "Success"; - } - } -} diff --git a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/SqsMessageHandler.java b/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/SqsMessageHandler.java deleted file mode 100644 index ee8c100e6..000000000 --- a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/SqsMessageHandler.java +++ /dev/null @@ -1,15 +0,0 @@ -package software.amazon.lambda.powertools.sqs.handlers; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import com.amazonaws.services.lambda.runtime.events.SQSEvent; -import software.amazon.lambda.powertools.sqs.SqsLargeMessage; - -public class SqsMessageHandler implements RequestHandler { - - @Override - @SqsLargeMessage - public String handleRequest(SQSEvent sqsEvent, Context context) { - return sqsEvent.getRecords().get(0).getBody(); - } -} diff --git a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/SqsMessageHandlerWithNonRetryableHandler.java b/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/SqsMessageHandlerWithNonRetryableHandler.java deleted file mode 100644 index 6eec87301..000000000 --- a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/SqsMessageHandlerWithNonRetryableHandler.java +++ /dev/null @@ -1,37 +0,0 @@ -package software.amazon.lambda.powertools.sqs.handlers; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import com.amazonaws.services.lambda.runtime.events.SQSEvent; -import software.amazon.lambda.powertools.sqs.SqsBatch; -import software.amazon.lambda.powertools.sqs.SqsMessageHandler; - -import static com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; -import static software.amazon.lambda.powertools.sqs.internal.SqsMessageBatchProcessorAspectTest.interactionClient; - -public class SqsMessageHandlerWithNonRetryableHandler implements RequestHandler { - - @Override - @SqsBatch(value = InnerMessageHandler.class, nonRetryableExceptions = {IllegalStateException.class, IllegalArgumentException.class}) - public String handleRequest(final SQSEvent sqsEvent, - final Context context) { - return "Success"; - } - - private class InnerMessageHandler implements SqsMessageHandler { - - @Override - public String process(SQSMessage message) { - if(message.getMessageId().isEmpty()) { - throw new IllegalArgumentException("Invalid message and was moved to DLQ"); - } - - if("2e1424d4-f796-459a-9696-9c92662ba5da".equals(message.getMessageId())) { - throw new RuntimeException("Invalid message and should be reprocessed"); - } - - interactionClient.listQueues(); - return "Success"; - } - } -} diff --git a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/SqsMessageHandlerWithNonRetryableHandlerWithDelete.java b/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/SqsMessageHandlerWithNonRetryableHandlerWithDelete.java deleted file mode 100644 index 789a7b86d..000000000 --- a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/SqsMessageHandlerWithNonRetryableHandlerWithDelete.java +++ /dev/null @@ -1,39 +0,0 @@ -package software.amazon.lambda.powertools.sqs.handlers; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import com.amazonaws.services.lambda.runtime.events.SQSEvent; -import software.amazon.lambda.powertools.sqs.SqsBatch; -import software.amazon.lambda.powertools.sqs.SqsMessageHandler; - -import static com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; -import static software.amazon.lambda.powertools.sqs.internal.SqsMessageBatchProcessorAspectTest.interactionClient; - -public class SqsMessageHandlerWithNonRetryableHandlerWithDelete implements RequestHandler { - - @Override - @SqsBatch(value = InnerMessageHandler.class, - nonRetryableExceptions = {IllegalStateException.class, IllegalArgumentException.class}, - deleteNonRetryableMessageFromQueue = true) - public String handleRequest(final SQSEvent sqsEvent, - final Context context) { - return "Success"; - } - - private class InnerMessageHandler implements SqsMessageHandler { - - @Override - public String process(SQSMessage message) { - if(message.getMessageId().isEmpty()) { - throw new IllegalArgumentException("Invalid message and was moved to DLQ"); - } - - if("2e1424d4-f796-459a-9696-9c92662ba5da".equals(message.getMessageId())) { - throw new RuntimeException("Invalid message and should be reprocessed"); - } - - interactionClient.listQueues(); - return "Success"; - } - } -} diff --git a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/SqsNoDeleteMessageHandler.java b/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/SqsNoDeleteMessageHandler.java deleted file mode 100644 index 337592004..000000000 --- a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/SqsNoDeleteMessageHandler.java +++ /dev/null @@ -1,15 +0,0 @@ -package software.amazon.lambda.powertools.sqs.handlers; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import com.amazonaws.services.lambda.runtime.events.SQSEvent; -import software.amazon.lambda.powertools.sqs.SqsLargeMessage; - -public class SqsNoDeleteMessageHandler implements RequestHandler { - - @Override - @SqsLargeMessage(deletePayloads = false) - public String handleRequest(SQSEvent sqsEvent, Context context) { - return sqsEvent.getRecords().get(0).getBody(); - } -} \ No newline at end of file diff --git a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/internal/SqsLargeMessageAspectTest.java b/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/internal/SqsLargeMessageAspectTest.java deleted file mode 100644 index 22844ab4c..000000000 --- a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/internal/SqsLargeMessageAspectTest.java +++ /dev/null @@ -1,199 +0,0 @@ -package software.amazon.lambda.powertools.sqs.internal; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.util.function.Consumer; -import java.util.stream.Stream; - -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.SQSEvent; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import software.amazon.awssdk.core.ResponseInputStream; -import software.amazon.awssdk.core.exception.SdkClientException; -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.awssdk.utils.StringInputStream; -import software.amazon.lambda.powertools.sqs.SqsUtils; -import software.amazon.lambda.powertools.sqs.handlers.LambdaHandlerApiGateway; -import software.amazon.lambda.powertools.sqs.handlers.SqsMessageHandler; -import software.amazon.lambda.powertools.sqs.handlers.SqsNoDeleteMessageHandler; - -import static com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; -import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -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.sqs.internal.SqsLargeMessageAspect.FailedProcessingLargePayloadException; - -public class SqsLargeMessageAspectTest { - - private RequestHandler requestHandler; - - @Mock - private Context context; - - @Mock - private S3Client s3Client; - - private static final String BUCKET_NAME = "bucketname"; - private static final String BUCKET_KEY = "c71eb2ae-37e0-4265-8909-32f4153faddf"; - - @BeforeEach - void setUp() { - openMocks(this); - setupContext(); - SqsUtils.overrideS3Client(s3Client); - requestHandler = new SqsMessageHandler(); - } - - @Test - public void testLargeMessage() { - when(s3Client.getObject(any(GetObjectRequest.class))).thenReturn(s3ObjectWithLargeMessage()); - SQSEvent sqsEvent = messageWithBody("[\"software.amazon.payloadoffloading.PayloadS3Pointer\",{\"s3BucketName\":\"" + BUCKET_NAME + "\",\"s3Key\":\"" + BUCKET_KEY + "\"}]"); - - String response = requestHandler.handleRequest(sqsEvent, context); - - assertThat(response) - .isEqualTo("A big message"); - - 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 shouldNotProcessSmallMessageBody() { - when(s3Client.getObject(any(GetObjectRequest.class))).thenReturn(s3ObjectWithLargeMessage()); - - SQSEvent sqsEvent = messageWithBody("This is small message"); - - String response = requestHandler.handleRequest(sqsEvent, context); - - assertThat(response) - .isEqualTo("This is small message"); - - verifyNoInteractions(s3Client); - } - - @ParameterizedTest - @MethodSource("exception") - public void shouldFailEntireBatchIfFailedDownloadingFromS3(RuntimeException exception) { - when(s3Client.getObject(any(GetObjectRequest.class))).thenThrow(exception); - - String messageBody = "[\"software.amazon.payloadoffloading.PayloadS3Pointer\",{\"s3BucketName\":\"" + BUCKET_NAME + "\",\"s3Key\":\"" + BUCKET_KEY + "\"}]"; - SQSEvent sqsEvent = messageWithBody(messageBody); - - assertThatExceptionOfType(FailedProcessingLargePayloadException.class) - .isThrownBy(() -> requestHandler.handleRequest(sqsEvent, context)) - .withCause(exception); - - verify(s3Client, never()).deleteObject(any(DeleteObjectRequest.class)); - } - - @Test - public void testLargeMessageWithDeletionOff() { - requestHandler = new SqsNoDeleteMessageHandler(); - - when(s3Client.getObject(any(GetObjectRequest.class))).thenReturn(s3ObjectWithLargeMessage()); - SQSEvent sqsEvent = messageWithBody("[\"software.amazon.payloadoffloading.PayloadS3Pointer\",{\"s3BucketName\":\"" + BUCKET_NAME + "\",\"s3Key\":\"" + BUCKET_KEY + "\"}]"); - - String response = requestHandler.handleRequest(sqsEvent, context); - - assertThat(response).isEqualTo("A big message"); - - verify(s3Client, never()).deleteObject(any(DeleteObjectRequest.class)); - } - - - @Test - public void shouldFailEntireBatchIfFailedProcessingDownloadMessageFromS3() { - ResponseInputStream s3Response = new ResponseInputStream<>(GetObjectResponse.builder().build(), AbortableInputStream.create(new StringInputStream("test") { - @Override - public void close() throws IOException { - throw new IOException("Failed"); - } - })); - - when(s3Client.getObject(any(GetObjectRequest.class))).thenReturn(s3Response); - - String messageBody = "[\"software.amazon.payloadoffloading.PayloadS3Pointer\",{\"s3BucketName\":\"" + BUCKET_NAME + "\",\"s3Key\":\"" + BUCKET_KEY + "\"}]"; - SQSEvent sqsEvent = messageWithBody(messageBody); - - assertThatExceptionOfType(FailedProcessingLargePayloadException.class) - .isThrownBy(() -> requestHandler.handleRequest(sqsEvent, context)) - .withCauseInstanceOf(IOException.class); - - verify(s3Client, never()).deleteObject(any(DeleteObjectRequest.class)); - } - - @Test - public void shouldNotDoAnyProcessingWhenNotSqsEvent() { - LambdaHandlerApiGateway handler = new LambdaHandlerApiGateway(); - - String messageBody = "[\"software.amazon.payloadoffloading.PayloadS3Pointer\",{\"s3BucketName\":\"" + BUCKET_NAME + "\",\"s3Key\":\"" + BUCKET_KEY + "\"}]"; - - APIGatewayProxyRequestEvent event = new APIGatewayProxyRequestEvent(); - event.setBody(messageBody); - String response = handler.handleRequest(event, context); - - assertThat(response) - .isEqualTo(messageBody); - - verifyNoInteractions(s3Client); - } - - private ResponseInputStream s3ObjectWithLargeMessage() { - return new ResponseInputStream<>(GetObjectResponse.builder().build(), AbortableInputStream.create(new ByteArrayInputStream("A big message".getBytes()))); - } - - private static Stream exception() { - return Stream.of(Arguments.of(S3Exception.builder() - .message("Service Exception") - .build()), - Arguments.of(SdkClientException.builder() - .message("Client Exception") - .build())); - } - - private SQSEvent messageWithBody(String messageBody) { - SQSMessage sqsMessage = new SQSMessage(); - sqsMessage.setBody(messageBody); - SQSEvent sqsEvent = new SQSEvent(); - sqsEvent.setRecords(singletonList(sqsMessage)); - return sqsEvent; - } - - private void setupContext() { - when(context.getFunctionName()).thenReturn("testFunction"); - when(context.getInvokedFunctionArn()).thenReturn("testArn"); - when(context.getFunctionVersion()).thenReturn("1"); - when(context.getMemoryLimitInMB()).thenReturn(10); - } -} \ No newline at end of file diff --git a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/internal/SqsMessageBatchProcessorAspectTest.java b/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/internal/SqsMessageBatchProcessorAspectTest.java deleted file mode 100644 index a65aa486b..000000000 --- a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/internal/SqsMessageBatchProcessorAspectTest.java +++ /dev/null @@ -1,307 +0,0 @@ -package software.amazon.lambda.powertools.sqs.internal; - -import java.io.IOException; -import java.util.HashMap; -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.SQSEvent; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import software.amazon.awssdk.services.sqs.SqsClient; -import software.amazon.awssdk.services.sqs.model.BatchResultErrorEntry; -import software.amazon.awssdk.services.sqs.model.DeleteMessageBatchRequest; -import software.amazon.awssdk.services.sqs.model.GetQueueAttributesRequest; -import software.amazon.awssdk.services.sqs.model.GetQueueAttributesResponse; -import software.amazon.awssdk.services.sqs.model.QueueAttributeName; -import software.amazon.awssdk.services.sqs.model.SendMessageBatchRequest; -import software.amazon.awssdk.services.sqs.model.SendMessageBatchResponse; -import software.amazon.lambda.powertools.sqs.SQSBatchProcessingException; -import software.amazon.lambda.powertools.sqs.handlers.LambdaHandlerApiGateway; -import software.amazon.lambda.powertools.sqs.handlers.PartialBatchFailureSuppressedHandler; -import software.amazon.lambda.powertools.sqs.handlers.PartialBatchPartialFailureHandler; -import software.amazon.lambda.powertools.sqs.handlers.PartialBatchSuccessHandler; -import software.amazon.lambda.powertools.sqs.handlers.SqsMessageHandlerWithNonRetryableHandler; -import software.amazon.lambda.powertools.sqs.handlers.SqsMessageHandlerWithNonRetryableHandlerWithDelete; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.when; -import static software.amazon.lambda.powertools.sqs.SqsUtils.overrideSqsClient; - -public class SqsMessageBatchProcessorAspectTest { - public static final SqsClient interactionClient = mock(SqsClient.class); - private static final SqsClient sqsClient = mock(SqsClient.class); - private static final ObjectMapper MAPPER = new ObjectMapper(); - - private SQSEvent event; - private RequestHandler requestHandler; - - private final Context context = mock(Context.class); - - @BeforeEach - void setUp() throws IOException { - overrideSqsClient(sqsClient); - reset(interactionClient); - reset(sqsClient); - setupContext(); - event = MAPPER.readValue(this.getClass().getResource("/sampleSqsBatchEvent.json"), SQSEvent.class); - requestHandler = new PartialBatchSuccessHandler(); - } - - @Test - void shouldBatchProcessAllMessageSuccessfullyAndNotDeleteFromSQS() { - requestHandler.handleRequest(event, context); - - verify(interactionClient, times(2)).listQueues(); - verify(sqsClient, times(0)).deleteMessageBatch(any(DeleteMessageBatchRequest.class)); - } - - @Test - void shouldBatchProcessMessageWithSuccessDeletedOnFailureInBatchFromSQS() { - requestHandler = new PartialBatchPartialFailureHandler(); - - assertThatExceptionOfType(SQSBatchProcessingException.class) - .isThrownBy(() -> requestHandler.handleRequest(event, context)) - .satisfies(e -> { - assertThat(e.getExceptions()) - .hasSize(1) - .extracting("message") - .containsExactly("2e1424d4-f796-459a-8184-9c92662be6da"); - - assertThat(e.getFailures()) - .hasSize(1) - .extracting("messageId") - .containsExactly("2e1424d4-f796-459a-8184-9c92662be6da"); - - assertThat(e.successMessageReturnValues()) - .hasSize(1) - .contains("Success"); - }); - - verify(interactionClient).listQueues(); - verify(sqsClient).deleteMessageBatch(any(DeleteMessageBatchRequest.class)); - } - - @Test - void shouldBatchProcessMessageWithSuccessDeletedOnFailureWithSuppressionInBatchFromSQS() { - requestHandler = new PartialBatchFailureSuppressedHandler(); - - requestHandler.handleRequest(event, context); - - verify(interactionClient).listQueues(); - verify(sqsClient).deleteMessageBatch(any(DeleteMessageBatchRequest.class)); - } - - @Test - void shouldNotTakeEffectOnNonSqsEventHandler() { - LambdaHandlerApiGateway handlerApiGateway = new LambdaHandlerApiGateway(); - - handlerApiGateway.handleRequest(mock(APIGatewayProxyRequestEvent.class), context); - - verifyNoInteractions(sqsClient); - } - - @Test - void shouldBatchProcessAndMoveNonRetryableExceptionToDlq() { - requestHandler = new SqsMessageHandlerWithNonRetryableHandler(); - event.getRecords().get(0).setMessageId(""); - - HashMap attributes = new HashMap<>(); - - attributes.put(QueueAttributeName.REDRIVE_POLICY, "{\n" + - " \"deadLetterTargetArn\": \"arn:aws:sqs:us-east-2:123456789012:retry-queue\",\n" + - " \"maxReceiveCount\": 2\n" + - "}"); - - when(sqsClient.getQueueAttributes(any(GetQueueAttributesRequest.class))).thenReturn(GetQueueAttributesResponse.builder() - .attributes(attributes) - .build()); - - requestHandler.handleRequest(event, context); - - verify(interactionClient).listQueues(); - verify(sqsClient).deleteMessageBatch(any(DeleteMessageBatchRequest.class)); - verify(sqsClient).sendMessageBatch(any(SendMessageBatchRequest.class)); - } - - @Test - void shouldBatchProcessAndThrowExceptionForNonRetryableExceptionWhenMoveToDlqReturnFailedResponse() { - requestHandler = new SqsMessageHandlerWithNonRetryableHandler(); - event.getRecords().get(0).setMessageId(""); - - when(sqsClient.sendMessageBatch(any(SendMessageBatchRequest.class))).thenReturn(SendMessageBatchResponse.builder() - .failed(BatchResultErrorEntry.builder() - .message("Permission Error") - .code("KMS.AccessDeniedException") - .senderFault(true) - .build()) - .build()); - - HashMap attributes = new HashMap<>(); - - attributes.put(QueueAttributeName.REDRIVE_POLICY, "{\n" + - " \"deadLetterTargetArn\": \"arn:aws:sqs:us-east-2:123456789012:retry-queue\",\n" + - " \"maxReceiveCount\": 2\n" + - "}"); - - when(sqsClient.getQueueAttributes(any(GetQueueAttributesRequest.class))).thenReturn(GetQueueAttributesResponse.builder() - .attributes(attributes) - .build()); - - Assertions.assertThatExceptionOfType(SQSBatchProcessingException.class). - isThrownBy(() -> requestHandler.handleRequest(event, context)); - - verify(interactionClient).listQueues(); - verify(sqsClient).sendMessageBatch(any(SendMessageBatchRequest.class)); - verify(sqsClient).deleteMessageBatch(any(DeleteMessageBatchRequest.class)); - } - - @Test - void shouldBatchProcessAndDeleteNonRetryableExceptionMessage() { - requestHandler = new SqsMessageHandlerWithNonRetryableHandlerWithDelete(); - event.getRecords().get(0).setMessageId(""); - - requestHandler.handleRequest(event, context); - - verify(interactionClient).listQueues(); - ArgumentCaptor captor = ArgumentCaptor.forClass(DeleteMessageBatchRequest.class); - verify(sqsClient).deleteMessageBatch(captor.capture()); - verify(sqsClient, never()).sendMessageBatch(any(SendMessageBatchRequest.class)); - verify(sqsClient, never()).getQueueAttributes(any(GetQueueAttributesRequest.class)); - - assertThat(captor.getValue()) - .satisfies(deleteMessageBatchRequest -> assertThat(deleteMessageBatchRequest.entries()) - .hasSize(2) - .extracting("id") - .contains("", "2e1424d4-f796-459a-8184-9c92662be6da")); - } - - @Test - void shouldBatchProcessAndFailWithExceptionForNonRetryableExceptionAndNoDlq() { - requestHandler = new SqsMessageHandlerWithNonRetryableHandler(); - - event.getRecords().get(0).setMessageId(""); - event.getRecords().forEach(sqsMessage -> sqsMessage.setEventSourceArn(sqsMessage.getEventSourceArn() + "-temp")); - - when(sqsClient.getQueueAttributes(any(GetQueueAttributesRequest.class))).thenReturn(GetQueueAttributesResponse.builder() - .build()); - - assertThatExceptionOfType(SQSBatchProcessingException.class) - .isThrownBy(() -> requestHandler.handleRequest(event, context)) - .satisfies(e -> { - assertThat(e.getExceptions()) - .hasSize(1) - .extracting("message") - .containsExactly("Invalid message and was moved to DLQ"); - - assertThat(e.getFailures()) - .hasSize(1) - .extracting("messageId") - .containsExactly(""); - - assertThat(e.successMessageReturnValues()) - .hasSize(1) - .contains("Success"); - }); - - verify(interactionClient).listQueues(); - verify(sqsClient).deleteMessageBatch(any(DeleteMessageBatchRequest.class)); - verify(sqsClient, never()).sendMessageBatch(any(SendMessageBatchRequest.class)); - } - - @Test - void shouldBatchProcessAndFailWithExceptionForNonRetryableExceptionWhenFailedParsingPolicy() { - requestHandler = new SqsMessageHandlerWithNonRetryableHandler(); - event.getRecords().get(0).setMessageId(""); - event.getRecords().forEach(sqsMessage -> sqsMessage.setEventSourceArn(sqsMessage.getEventSourceArn() + "-temp-queue")); - - HashMap attributes = new HashMap<>(); - - attributes.put(QueueAttributeName.REDRIVE_POLICY, "MalFormedRedrivePolicy"); - - when(sqsClient.getQueueAttributes(any(GetQueueAttributesRequest.class))).thenReturn(GetQueueAttributesResponse.builder() - .attributes(attributes) - .build()); - - assertThatExceptionOfType(SQSBatchProcessingException.class) - .isThrownBy(() -> requestHandler.handleRequest(event, context)) - .satisfies(e -> { - assertThat(e.getExceptions()) - .hasSize(1) - .extracting("message") - .containsExactly("Invalid message and was moved to DLQ"); - - assertThat(e.getFailures()) - .hasSize(1) - .extracting("messageId") - .containsExactly(""); - - assertThat(e.successMessageReturnValues()) - .hasSize(1) - .contains("Success"); - }); - - verify(interactionClient).listQueues(); - verify(sqsClient).deleteMessageBatch(any(DeleteMessageBatchRequest.class)); - verify(sqsClient, never()).sendMessageBatch(any(SendMessageBatchRequest.class)); - } - - @Test - void shouldBatchProcessAndMoveNonRetryableExceptionToDlqAndThrowException() throws IOException { - requestHandler = new SqsMessageHandlerWithNonRetryableHandler(); - event = MAPPER.readValue(this.getClass().getResource("/threeMessageSqsBatchEvent.json"), SQSEvent.class); - - event.getRecords().get(1).setMessageId(""); - - HashMap attributes = new HashMap<>(); - - attributes.put(QueueAttributeName.REDRIVE_POLICY, "{\n" + - " \"deadLetterTargetArn\": \"arn:aws:sqs:us-east-2:123456789012:retry-queue\",\n" + - " \"maxReceiveCount\": 2\n" + - "}"); - - when(sqsClient.getQueueAttributes(any(GetQueueAttributesRequest.class))).thenReturn(GetQueueAttributesResponse.builder() - .attributes(attributes) - .build()); - - assertThatExceptionOfType(SQSBatchProcessingException.class) - .isThrownBy(() -> requestHandler.handleRequest(event, context)) - .satisfies(e -> { - assertThat(e.getExceptions()) - .hasSize(1) - .extracting("message") - .containsExactly("Invalid message and should be reprocessed"); - - assertThat(e.getFailures()) - .hasSize(1) - .extracting("messageId") - .containsExactly("2e1424d4-f796-459a-9696-9c92662ba5da"); - - assertThat(e.successMessageReturnValues()) - .hasSize(1) - .contains("Success"); - }); - - verify(interactionClient).listQueues(); - verify(sqsClient).deleteMessageBatch(any(DeleteMessageBatchRequest.class)); - verify(sqsClient).sendMessageBatch(any(SendMessageBatchRequest.class)); - } - - private void setupContext() { - when(context.getFunctionName()).thenReturn("testFunction"); - when(context.getInvokedFunctionArn()).thenReturn("testArn"); - when(context.getFunctionVersion()).thenReturn("1"); - when(context.getMemoryLimitInMB()).thenReturn(10); - } -} \ No newline at end of file diff --git a/powertools-sqs/src/test/resources/sampleSqsBatchEvent.json b/powertools-sqs/src/test/resources/sampleSqsBatchEvent.json deleted file mode 100644 index 8a5fbf309..000000000 --- a/powertools-sqs/src/test/resources/sampleSqsBatchEvent.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "records": [ - { - "messageId": "059f36b4-87a3-44ab-83d2-661975830a7d", - "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082649183", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082649185" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6da", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - } - ] -} \ No newline at end of file diff --git a/powertools-sqs/src/test/resources/sampleSqsBatchEventBatchSize25.json b/powertools-sqs/src/test/resources/sampleSqsBatchEventBatchSize25.json deleted file mode 100644 index 31b8862ad..000000000 --- a/powertools-sqs/src/test/resources/sampleSqsBatchEventBatchSize25.json +++ /dev/null @@ -1,404 +0,0 @@ -{ - "records": [ - { - "messageId": "059f36b4-87a3-44ab-83d2-661975830a7d", - "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082649183", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082649185" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6da", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d3", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d4", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b5", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d6", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b7", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d8", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d9", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d10", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d11", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d12", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d13", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d14", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d15", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d16", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d17", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d18", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d19", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d20", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d21", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d22", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d23", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d24", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d25", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d26", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d27", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - } - ] -} \ No newline at end of file diff --git a/powertools-sqs/src/test/resources/threeMessageSqsBatchEvent.json b/powertools-sqs/src/test/resources/threeMessageSqsBatchEvent.json deleted file mode 100644 index b3b61da3b..000000000 --- a/powertools-sqs/src/test/resources/threeMessageSqsBatchEvent.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "records": [ - { - "messageId": "059f36b4-87a3-44ab-83d2-661975830a7d", - "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082649183", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082649185" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-1:906126917321:sqs-lambda-MySqsQueue-4DYWWJIE97N5", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6da", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": { - "Attribute3" : { - "binaryValue" : "MTEwMA==", - "dataType" : "Binary" - }, - "Attribute2" : { - "stringValue" : "123", - "dataType" : "Number" - }, - "Attribute1" : { - "stringValue" : "AttributeValue1", - "dataType" : "String" - } - }, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-1:906126917321:sqs-lambda-MySqsQueue-4DYWWJIE97N5", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-9696-9c92662ba5da", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": { - "Attribute2" : { - "stringValue" : "123", - "dataType" : "Number" - } - }, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-1:906126917321:sqs-lambda-MySqsQueue-4DYWWJIE97N5", - "awsRegion": "us-east-2" - } - ] -} \ No newline at end of file diff --git a/powertools-test-suite/pom.xml b/powertools-test-suite/pom.xml deleted file mode 100644 index 5cd811051..000000000 --- a/powertools-test-suite/pom.xml +++ /dev/null @@ -1,150 +0,0 @@ - - - 4.0.0 - - powertools-test-suite - jar - - - powertools-parent - software.amazon.lambda - 2.0.0-SNAPSHOT - - - Powertools for AWS Lambda (Java) library Test Suite - - A suite of tests for interactions between the various Powertools for AWS Lambda (Java) modules. - - https://aws.amazon.com/lambda/ - - GitHub Issues - https://github.com/aws-powertools/powertools-lambda-java/issues - - - https://github.com/aws-powertools/powertools-lambda-java.git - - - - Powertools for AWS Lambda team - Amazon Web Services - https://aws.amazon.com/ - - - - - true - - - - - ossrh - https://aws.oss.sonatype.org/content/repositories/snapshots - - - - - - software.amazon.lambda - powertools-core - - - org.apache.logging.log4j - log4j-jcl - - - com.amazonaws - aws-lambda-java-core - - - com.amazonaws - aws-lambda-java-events - - - software.amazon.lambda - powertools-logging - - - software.amazon.lambda - powertools-tracing - - - software.amazon.lambda - powertools-sqs - - - - - org.junit.jupiter - junit-jupiter-api - test - - - org.junit.jupiter - junit-jupiter-engine - test - - - org.apache.commons - commons-lang3 - test - - - org.mockito - mockito-core - test - - - org.aspectj - aspectjweaver - test - - - org.assertj - assertj-core - test - - - org.skyscreamer - jsonassert - test - - - - - - - dev.aspectj - aspectj-maven-plugin - 1.13.1 - - ${maven.compiler.source} - ${maven.compiler.target} - ${maven.compiler.target} - - - software.amazon.lambda - powertools-logging - - - software.amazon.lambda - powertools-tracing - - - software.amazon.lambda - powertools-sqs - - - - - - - compile - - - - - - - \ No newline at end of file diff --git a/powertools-test-suite/src/test/java/software/amazon/lambda/powertools/testsuite/LoggingOrderTest.java b/powertools-test-suite/src/test/java/software/amazon/lambda/powertools/testsuite/LoggingOrderTest.java deleted file mode 100644 index 7c3e79112..000000000 --- a/powertools-test-suite/src/test/java/software/amazon/lambda/powertools/testsuite/LoggingOrderTest.java +++ /dev/null @@ -1,171 +0,0 @@ -package software.amazon.lambda.powertools.testsuite; - - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.nio.channels.FileChannel; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.nio.file.StandardOpenOption; -import java.util.Map; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.events.SQSEvent; -import com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification; -import com.amazonaws.xray.AWSXRay; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.apache.logging.log4j.Level; -import org.apache.logging.log4j.ThreadContext; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -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.GetObjectRequest; -import software.amazon.awssdk.services.s3.model.GetObjectResponse; -import software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor; -import software.amazon.lambda.powertools.logging.internal.LambdaLoggingAspect; -import software.amazon.lambda.powertools.sqs.SqsUtils; -import software.amazon.lambda.powertools.testsuite.handler.LoggingOrderMessageHandler; -import software.amazon.lambda.powertools.testsuite.handler.TracingLoggingStreamMessageHandler; - -import static java.util.Collections.emptyMap; -import static java.util.Collections.singletonList; -import static org.apache.commons.lang3.reflect.FieldUtils.writeStaticField; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; -import static org.mockito.MockitoAnnotations.openMocks; - -public class LoggingOrderTest { - - private static final String BUCKET_NAME = "ms-extended-sqs-client"; - private static final String BUCKET_KEY = "c71eb2ae-37e0-4265-8909-32f4153faddf"; - - @Mock - private Context context; - - @Mock - private S3Client s3Client; - - @BeforeEach - void setUp() throws IllegalAccessException, IOException, NoSuchMethodException, InvocationTargetException { - openMocks(this); - SqsUtils.overrideS3Client(s3Client); - ThreadContext.clearAll(); - writeStaticField(LambdaHandlerProcessor.class, "IS_COLD_START", null, true); - setupContext(); - //Make sure file is cleaned up before running full stack logging regression - FileChannel.open(Paths.get("target/logfile.json"), StandardOpenOption.WRITE).truncate(0).close(); - resetLogLevel(Level.INFO); - AWSXRay.beginSegment(LoggingOrderTest.class.getName()); - } - - @AfterEach - void tearDown() { - AWSXRay.endSegment(); - } - - /** - * The SQSEvent payload will be altered by the @SqsLargeMessage annotation. Logging of the event should happen - * after the event has been altered - */ - @Test - public void testThatLoggingAnnotationActsLast() throws IOException { - ResponseInputStream s3Response = new ResponseInputStream<>(GetObjectResponse.builder().build(), AbortableInputStream.create(new ByteArrayInputStream("A big message".getBytes()))); - - when(s3Client.getObject(any(GetObjectRequest.class))).thenReturn(s3Response); - SQSEvent sqsEvent = messageWithBody("[\"software.amazon.payloadoffloading.PayloadS3Pointer\",{\"s3BucketName\":\"" + BUCKET_NAME + "\",\"s3Key\":\"" + BUCKET_KEY + "\"}]"); - - LoggingOrderMessageHandler requestHandler = new LoggingOrderMessageHandler(); - requestHandler.handleRequest(sqsEvent, context); - - assertThat(Files.lines(Paths.get("target/logfile.json"))) - .hasSize(2) - .satisfies(line -> { - Map actual = parseToMap(line.get(0)); - - String message = actual.get("message").toString(); - - assertThat(message) - .contains("A big message"); - }); - } - - @Test - public void testLoggingAnnotationActsAfterTracingForStreamingHandler() throws IOException { - - ByteArrayOutputStream output = new ByteArrayOutputStream(); - S3EventNotification s3EventNotification = s3EventNotification(); - - TracingLoggingStreamMessageHandler handler = new TracingLoggingStreamMessageHandler(); - handler.handleRequest(new ByteArrayInputStream(new ObjectMapper().writeValueAsBytes(s3EventNotification)), output, context); - - assertThat(new String(output.toByteArray(), StandardCharsets.UTF_8)) - .isNotEmpty(); - } - - private void setupContext() { - when(context.getFunctionName()).thenReturn("testFunction"); - when(context.getInvokedFunctionArn()).thenReturn("testArn"); - when(context.getFunctionVersion()).thenReturn("1"); - when(context.getMemoryLimitInMB()).thenReturn(10); - when(context.getAwsRequestId()).thenReturn("RequestId"); - } - - private void resetLogLevel(Level level) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { - Method resetLogLevels = LambdaLoggingAspect.class.getDeclaredMethod("resetLogLevels", Level.class); - resetLogLevels.setAccessible(true); - resetLogLevels.invoke(null, level); - writeStaticField(LambdaLoggingAspect.class, "LEVEL_AT_INITIALISATION", level, true); - } - - private Map parseToMap(String stringAsJson) { - try { - return new ObjectMapper().readValue(stringAsJson, Map.class); - } catch (JsonProcessingException e) { - fail("Failed parsing logger line " + stringAsJson); - return emptyMap(); - } - } - - private S3EventNotification s3EventNotification() { - S3EventNotification.S3EventNotificationRecord record = new S3EventNotification.S3EventNotificationRecord("us-west-2", - "ObjectCreated:Put", - "aws:s3", - null, - "2.1", - new S3EventNotification.RequestParametersEntity("127.0.0.1"), - new S3EventNotification.ResponseElementsEntity("C3D13FE58DE4C810", "FMyUVURIY8/IgAtTv8xRjskZQpcIZ9KG4V5Wp6S7S/JRWeUWerMUE5JgHvANOjpD"), - new S3EventNotification.S3Entity("testConfigRule", - new S3EventNotification.S3BucketEntity("mybucket", - new S3EventNotification.UserIdentityEntity("A3NL1KOZZKExample"), - "arn:aws:s3:::mybucket"), - new S3EventNotification.S3ObjectEntity("HappyFace.jpg", - 1024L, - "d41d8cd98f00b204e9800998ecf8427e", - "096fKKXTRTtl3on89fVO.nfljtsv6qko", - "0055AED6DCD90281E5"), - "1.0"), - new S3EventNotification.UserIdentityEntity("AIDAJDPLRKLG7UEXAMPLE") - ); - - return new S3EventNotification(singletonList(record)); - } - - private SQSEvent messageWithBody(String messageBody) { - SQSEvent.SQSMessage sqsMessage = new SQSEvent.SQSMessage(); - sqsMessage.setBody(messageBody); - SQSEvent sqsEvent = new SQSEvent(); - sqsEvent.setRecords(singletonList(sqsMessage)); - return sqsEvent; - } -} \ No newline at end of file diff --git a/powertools-test-suite/src/test/java/software/amazon/lambda/powertools/testsuite/handler/LoggingOrderMessageHandler.java b/powertools-test-suite/src/test/java/software/amazon/lambda/powertools/testsuite/handler/LoggingOrderMessageHandler.java deleted file mode 100644 index a85c81b1d..000000000 --- a/powertools-test-suite/src/test/java/software/amazon/lambda/powertools/testsuite/handler/LoggingOrderMessageHandler.java +++ /dev/null @@ -1,17 +0,0 @@ -package software.amazon.lambda.powertools.testsuite.handler; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import com.amazonaws.services.lambda.runtime.events.SQSEvent; -import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.sqs.SqsLargeMessage; - -public class LoggingOrderMessageHandler implements RequestHandler { - - @Override - @SqsLargeMessage - @Logging(logEvent = true) - public String handleRequest(SQSEvent sqsEvent, Context context) { - return sqsEvent.getRecords().get(0).getBody(); - } -} diff --git a/powertools-test-suite/src/test/java/software/amazon/lambda/powertools/testsuite/handler/TracingLoggingStreamMessageHandler.java b/powertools-test-suite/src/test/java/software/amazon/lambda/powertools/testsuite/handler/TracingLoggingStreamMessageHandler.java deleted file mode 100644 index d0f2b3ac5..000000000 --- a/powertools-test-suite/src/test/java/software/amazon/lambda/powertools/testsuite/handler/TracingLoggingStreamMessageHandler.java +++ /dev/null @@ -1,23 +0,0 @@ -package software.amazon.lambda.powertools.testsuite.handler; - -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.tracing.Tracing; - -public class TracingLoggingStreamMessageHandler implements RequestStreamHandler { - - @Logging(logEvent = true) - @Tracing - @Override - public void handleRequest(InputStream input, OutputStream output, Context context) throws IOException { - ObjectMapper mapper = new ObjectMapper(); - mapper.writeValue(output, mapper.readValue(input, Map.class)); - } -} diff --git a/powertools-test-suite/src/test/resources/log4j2.xml b/powertools-test-suite/src/test/resources/log4j2.xml deleted file mode 100644 index 8ac9b34ce..000000000 --- a/powertools-test-suite/src/test/resources/log4j2.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/powertools-tracing/pom.xml b/powertools-tracing/pom.xml index aad2b5d85..5f397aa9f 100644 --- a/powertools-tracing/pom.xml +++ b/powertools-tracing/pom.xml @@ -1,4 +1,18 @@ + + @@ -113,4 +127,13 @@ + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + + \ No newline at end of file diff --git a/powertools-tracing/src/main/java/software/amazon/lambda/powertools/tracing/CaptureMode.java b/powertools-tracing/src/main/java/software/amazon/lambda/powertools/tracing/CaptureMode.java index 9fd09e8ee..29d10d188 100644 --- a/powertools-tracing/src/main/java/software/amazon/lambda/powertools/tracing/CaptureMode.java +++ b/powertools-tracing/src/main/java/software/amazon/lambda/powertools/tracing/CaptureMode.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.tracing; public enum CaptureMode { diff --git a/powertools-tracing/src/main/java/software/amazon/lambda/powertools/tracing/Tracing.java b/powertools-tracing/src/main/java/software/amazon/lambda/powertools/tracing/Tracing.java index cb90f3315..6f17a2e33 100644 --- a/powertools-tracing/src/main/java/software/amazon/lambda/powertools/tracing/Tracing.java +++ b/powertools-tracing/src/main/java/software/amazon/lambda/powertools/tracing/Tracing.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.tracing; import java.lang.annotation.ElementType; @@ -39,7 +40,7 @@ * to a sub segment named after the method.

    * *

    To disable this functionality you can specify {@code @Tracing( captureError = false)}

    - *e + * e *

    All traces have a namespace set. If {@code @Tracing( namespace = "ExampleService")} is set * this takes precedent over any value set in the environment variable {@code POWER_TOOLS_SERVICE_NAME}. * If both are undefined then the value will default to {@code service_undefined}

    @@ -48,6 +49,7 @@ @Target(ElementType.METHOD) public @interface Tracing { String namespace() default ""; + /** * @deprecated As of release 1.2.0, replaced by captureMode() * in order to support different modes and support via @@ -55,6 +57,7 @@ */ @Deprecated boolean captureResponse() default true; + /** * @deprecated As of release 1.2.0, replaced by captureMode() * in order to support different modes and support via @@ -62,6 +65,8 @@ */ @Deprecated boolean captureError() default true; + String segmentName() default ""; + CaptureMode captureMode() default CaptureMode.ENVIRONMENT_VAR; } diff --git a/powertools-tracing/src/main/java/software/amazon/lambda/powertools/tracing/TracingUtils.java b/powertools-tracing/src/main/java/software/amazon/lambda/powertools/tracing/TracingUtils.java index 0e956e539..9fb021548 100644 --- a/powertools-tracing/src/main/java/software/amazon/lambda/powertools/tracing/TracingUtils.java +++ b/powertools-tracing/src/main/java/software/amazon/lambda/powertools/tracing/TracingUtils.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,20 +11,20 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.tracing; -import java.util.function.Consumer; +import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.serviceName; + import com.amazonaws.xray.AWSXRay; import com.amazonaws.xray.entities.Entity; import com.amazonaws.xray.entities.Subsegment; import com.fasterxml.jackson.databind.ObjectMapper; - -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.serviceName; +import java.util.function.Consumer; /** * A class of helper functions to add additional functionality and ease * of use. - * */ public final class TracingUtils { private static ObjectMapper objectMapper; @@ -32,7 +32,7 @@ public final class TracingUtils { /** * Put an annotation to the current subsegment with a String value. * - * @param key the key of the annotation + * @param key the key of the annotation * @param value the value of the annotation */ public static void putAnnotation(String key, String value) { @@ -43,33 +43,33 @@ public static void putAnnotation(String key, String value) { /** * Put an annotation to the current subsegment with a Number value. * - * @param key the key of the annotation + * @param key the key of the annotation * @param value the value of the annotation */ public static void putAnnotation(String key, Number value) { AWSXRay.getCurrentSubsegmentOptional() - .ifPresent(segment -> segment.putAnnotation(key, value)); + .ifPresent(segment -> segment.putAnnotation(key, value)); } /** * Put an annotation to the current subsegment with a Boolean value. * - * @param key the key of the annotation + * @param key the key of the annotation * @param value the value of the annotation */ public static void putAnnotation(String key, Boolean value) { AWSXRay.getCurrentSubsegmentOptional() - .ifPresent(segment -> segment.putAnnotation(key, value)); + .ifPresent(segment -> segment.putAnnotation(key, value)); } /** * Put additional metadata for the current subsegment. - * + *

    * The namespace used will be the namespace of the current subsegment if it * is set else it will follow the namespace process as described in * {@link Tracing} * - * @param key the key of the metadata + * @param key the key of the metadata * @param value the value of the metadata */ public static void putMetadata(String key, Object value) { @@ -83,8 +83,8 @@ public static void putMetadata(String key, Object value) { * Put additional metadata for the current subsegment. * * @param namespace the namespace of the metadata - * @param key the key of the metadata - * @param value the value of the metadata + * @param key the key of the metadata + * @param value the value of the metadata */ public static void putMetadata(String namespace, String key, Object value) { AWSXRay.getCurrentSubsegmentOptional() @@ -94,14 +94,14 @@ public static void putMetadata(String namespace, String key, Object value) { /** * Adds a new subsegment around the passed consumer. This also provides access to * the newly created subsegment. - * + *

    * The namespace used follows the flow as described in {@link Tracing} - * + *

    * This method is intended for use with multi-threaded programming where the * context is lost between threads. * - * @param name the name of the subsegment - * @param entity the current x-ray context + * @param name the name of the subsegment + * @param entity the current x-ray context * @param subsegment the x-ray subsegment for the wrapped consumer */ public static void withEntitySubsegment(String name, Entity entity, Consumer subsegment) { @@ -112,16 +112,17 @@ public static void withEntitySubsegment(String name, Entity entity, Consumer * This method is intended for use with multi-threaded programming where the * context is lost between threads. * - * @param namespace the namespace of the subsegment - * @param name the name of the subsegment - * @param entity the current x-ray context + * @param namespace the namespace of the subsegment + * @param name the name of the subsegment + * @param entity the current x-ray context * @param subsegment the x-ray subsegment for the wrapped consumer */ - public static void withEntitySubsegment(String namespace, String name, Entity entity, Consumer subsegment) { + public static void withEntitySubsegment(String namespace, String name, Entity entity, + Consumer subsegment) { AWSXRay.setTraceEntity(entity); withSubsegment(namespace, name, subsegment); } @@ -129,10 +130,10 @@ public static void withEntitySubsegment(String namespace, String name, Entity en /** * Adds a new subsegment around the passed consumer. This also provides access to * the newly created subsegment. - * + *

    * The namespace used follows the flow as described in {@link Tracing} * - * @param name the name of the subsegment + * @param name the name of the subsegment * @param subsegment the x-ray subsegment for the wrapped consumer */ public static void withSubsegment(String name, Consumer subsegment) { @@ -143,8 +144,8 @@ public static void withSubsegment(String name, Consumer subsegment) * Adds a new subsegment around the passed consumer. This also provides access to * the newly created subsegment. * - * @param namespace the namespace for the subsegment - * @param name the name of the subsegment + * @param namespace the namespace for the subsegment + * @param name the name of the subsegment * @param subsegment the x-ray subsegment for the wrapped consumer */ public static void withSubsegment(String namespace, String name, Consumer subsegment) { diff --git a/powertools-tracing/src/main/java/software/amazon/lambda/powertools/tracing/internal/LambdaTracingAspect.java b/powertools-tracing/src/main/java/software/amazon/lambda/powertools/tracing/internal/LambdaTracingAspect.java index 26feec66b..62416fce6 100644 --- a/powertools-tracing/src/main/java/software/amazon/lambda/powertools/tracing/internal/LambdaTracingAspect.java +++ b/powertools-tracing/src/main/java/software/amazon/lambda/powertools/tracing/internal/LambdaTracingAspect.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,24 +11,25 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.tracing.internal; -import java.util.function.Supplier; +import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.coldStartDone; +import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.isColdStart; +import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.isHandlerMethod; +import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.isSamLocal; +import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.serviceName; +import static software.amazon.lambda.powertools.tracing.TracingUtils.objectMapper; + import com.amazonaws.xray.AWSXRay; import com.amazonaws.xray.entities.Subsegment; +import java.util.function.Supplier; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import software.amazon.lambda.powertools.tracing.Tracing; -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.coldStartDone; -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.isColdStart; -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.isHandlerMethod; -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.isSamLocal; -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.serviceName; -import static software.amazon.lambda.powertools.tracing.TracingUtils.objectMapper; - @Aspect public final class LambdaTracingAspect { @SuppressWarnings({"EmptyMethod"}) @@ -42,8 +43,8 @@ public Object around(ProceedingJoinPoint pjp, Object[] proceedArgs = pjp.getArgs(); Subsegment segment = AWSXRay.beginSubsegment( - customSegmentNameOrDefault(tracing, - () -> "## " + pjp.getSignature().getName())); + customSegmentNameOrDefault(tracing, + () -> "## " + pjp.getSignature().getName())); segment.setNamespace(namespace(tracing)); if (isHandlerMethod(pjp)) { @@ -57,7 +58,8 @@ public Object around(ProceedingJoinPoint pjp, try { Object methodReturn = pjp.proceed(proceedArgs); if (captureResponse) { - segment.putMetadata(namespace(tracing), pjp.getSignature().getName() + " response", null != objectMapper() ? objectMapper().writeValueAsString(methodReturn): methodReturn); + segment.putMetadata(namespace(tracing), pjp.getSignature().getName() + " response", + null != objectMapper() ? objectMapper().writeValueAsString(methodReturn) : methodReturn); } if (isHandlerMethod(pjp)) { @@ -67,7 +69,8 @@ public Object around(ProceedingJoinPoint pjp, return methodReturn; } catch (Exception e) { if (captureError) { - segment.putMetadata(namespace(tracing), pjp.getSignature().getName() + " error", null != objectMapper() ? objectMapper().writeValueAsString(e) : e); + segment.putMetadata(namespace(tracing), pjp.getSignature().getName() + " error", + null != objectMapper() ? objectMapper().writeValueAsString(e) : e); } throw e; } finally { @@ -81,7 +84,8 @@ private boolean captureResponse(Tracing powerToolsTracing) { switch (powerToolsTracing.captureMode()) { case ENVIRONMENT_VAR: boolean captureResponse = environmentVariable("POWERTOOLS_TRACER_CAPTURE_RESPONSE"); - return isEnvironmentVariableSet("POWERTOOLS_TRACER_CAPTURE_RESPONSE") ? captureResponse : powerToolsTracing.captureResponse(); + return isEnvironmentVariableSet("POWERTOOLS_TRACER_CAPTURE_RESPONSE") ? captureResponse : + powerToolsTracing.captureResponse(); case RESPONSE: case RESPONSE_AND_ERROR: return true; @@ -95,7 +99,8 @@ private boolean captureError(Tracing powerToolsTracing) { switch (powerToolsTracing.captureMode()) { case ENVIRONMENT_VAR: boolean captureError = environmentVariable("POWERTOOLS_TRACER_CAPTURE_ERROR"); - return isEnvironmentVariableSet("POWERTOOLS_TRACER_CAPTURE_ERROR") ? captureError : powerToolsTracing.captureError(); + return isEnvironmentVariableSet("POWERTOOLS_TRACER_CAPTURE_ERROR") ? captureError : + powerToolsTracing.captureError(); case ERROR: case RESPONSE_AND_ERROR: return true; diff --git a/powertools-tracing/src/main/java/software/amazon/lambda/powertools/tracing/internal/SystemWrapper.java b/powertools-tracing/src/main/java/software/amazon/lambda/powertools/tracing/internal/SystemWrapper.java index da1a92ced..c66b8b7ee 100644 --- a/powertools-tracing/src/main/java/software/amazon/lambda/powertools/tracing/internal/SystemWrapper.java +++ b/powertools-tracing/src/main/java/software/amazon/lambda/powertools/tracing/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.tracing.internal; public class SystemWrapper { diff --git a/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/TracingUtilsTest.java b/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/TracingUtilsTest.java index d2a96ec65..69054d0c6 100644 --- a/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/TracingUtilsTest.java +++ b/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/TracingUtilsTest.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,8 +11,15 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.tracing; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static software.amazon.lambda.powertools.tracing.TracingUtils.withEntitySubsegment; + import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.xray.AWSXRay; import com.amazonaws.xray.entities.Entity; @@ -20,12 +27,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.entry; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static software.amazon.lambda.powertools.tracing.TracingUtils.withEntitySubsegment; - class TracingUtilsTest { @BeforeEach @@ -51,12 +52,12 @@ void shouldSetAnnotationOnCurrentSubSegment() { TracingUtils.putAnnotation("booleanKey", false); assertThat(AWSXRay.getTraceEntity().getAnnotations()) - .hasSize(3) - .contains( - entry("stringKey", "val"), - entry("numberKey", 10), - entry("booleanKey", false) - ); + .hasSize(3) + .contains( + entry("stringKey", "val"), + entry("numberKey", 10), + entry("booleanKey", false) + ); } @Test @@ -94,60 +95,64 @@ void shouldNotSetMetaDataIfNoCurrentSubSegment() { void shouldInvokeCodeBlockWrappedWithinSubsegment() { Context test = mock(Context.class); - TracingUtils.withSubsegment("testSubSegment", subsegment -> { - subsegment.putAnnotation("key", "val"); - subsegment.putMetadata("key", "val"); - test.getFunctionName(); - }); + TracingUtils.withSubsegment("testSubSegment", subsegment -> + { + subsegment.putAnnotation("key", "val"); + subsegment.putMetadata("key", "val"); + test.getFunctionName(); + }); verify(test).getFunctionName(); assertThat(AWSXRay.getTraceEntity().getSubsegments()) .hasSize(1) - .allSatisfy(subsegment -> { - assertThat(subsegment.getName()) - .isEqualTo("## testSubSegment"); + .allSatisfy(subsegment -> + { + assertThat(subsegment.getName()) + .isEqualTo("## testSubSegment"); - assertThat(subsegment.getNamespace()) - .isEqualTo("service_undefined"); + assertThat(subsegment.getNamespace()) + .isEqualTo("service_undefined"); - assertThat(subsegment.getAnnotations()) - .hasSize(1) - .containsEntry("key", "val"); + assertThat(subsegment.getAnnotations()) + .hasSize(1) + .containsEntry("key", "val"); - assertThat(subsegment.getMetadata()) - .hasSize(1); - }); + assertThat(subsegment.getMetadata()) + .hasSize(1); + }); } @Test void shouldInvokeCodeBlockWrappedWithinNamespacedSubsegment() { Context test = mock(Context.class); - TracingUtils.withSubsegment("testNamespace", "testSubSegment", subsegment -> { - subsegment.putAnnotation("key", "val"); - subsegment.putMetadata("key", "val"); - test.getFunctionName(); - }); + TracingUtils.withSubsegment("testNamespace", "testSubSegment", subsegment -> + { + subsegment.putAnnotation("key", "val"); + subsegment.putMetadata("key", "val"); + test.getFunctionName(); + }); verify(test).getFunctionName(); assertThat(AWSXRay.getTraceEntity().getSubsegments()) .hasSize(1) - .allSatisfy(subsegment -> { - assertThat(subsegment.getName()) - .isEqualTo("## testSubSegment"); + .allSatisfy(subsegment -> + { + assertThat(subsegment.getName()) + .isEqualTo("## testSubSegment"); - assertThat(subsegment.getNamespace()) - .isEqualTo("testNamespace"); + assertThat(subsegment.getNamespace()) + .isEqualTo("testNamespace"); - assertThat(subsegment.getAnnotations()) - .hasSize(1) - .containsEntry("key", "val"); + assertThat(subsegment.getAnnotations()) + .hasSize(1) + .containsEntry("key", "val"); - assertThat(subsegment.getMetadata()) - .hasSize(1); - }); + assertThat(subsegment.getMetadata()) + .hasSize(1); + }); } @Test @@ -156,10 +161,11 @@ void shouldInvokeCodeBlockWrappedWithinEntitySubsegment() throws InterruptedExce Entity traceEntity = AWSXRay.getTraceEntity(); - Thread thread = new Thread(() -> withEntitySubsegment("testSubSegment", traceEntity, subsegment -> { - subsegment.putAnnotation("key", "val"); - test.getFunctionName(); - })); + Thread thread = new Thread(() -> withEntitySubsegment("testSubSegment", traceEntity, subsegment -> + { + subsegment.putAnnotation("key", "val"); + test.getFunctionName(); + })); thread.start(); thread.join(); @@ -168,17 +174,18 @@ void shouldInvokeCodeBlockWrappedWithinEntitySubsegment() throws InterruptedExce assertThat(AWSXRay.getTraceEntity().getSubsegments()) .hasSize(1) - .allSatisfy(subsegment -> { - assertThat(subsegment.getName()) - .isEqualTo("## testSubSegment"); - - assertThat(subsegment.getNamespace()) - .isEqualTo("service_undefined"); - - assertThat(subsegment.getAnnotations()) - .hasSize(1) - .containsEntry("key", "val"); - }); + .allSatisfy(subsegment -> + { + assertThat(subsegment.getName()) + .isEqualTo("## testSubSegment"); + + assertThat(subsegment.getNamespace()) + .isEqualTo("service_undefined"); + + assertThat(subsegment.getAnnotations()) + .hasSize(1) + .containsEntry("key", "val"); + }); } @Test @@ -187,10 +194,12 @@ void shouldInvokeCodeBlockWrappedWithinNamespacedEntitySubsegment() throws Inter Entity traceEntity = AWSXRay.getTraceEntity(); - Thread thread = new Thread(() -> withEntitySubsegment("testNamespace", "testSubSegment", traceEntity, subsegment -> { - subsegment.putAnnotation("key", "val"); - test.getFunctionName(); - })); + Thread thread = + new Thread(() -> withEntitySubsegment("testNamespace", "testSubSegment", traceEntity, subsegment -> + { + subsegment.putAnnotation("key", "val"); + test.getFunctionName(); + })); thread.start(); thread.join(); @@ -199,16 +208,17 @@ void shouldInvokeCodeBlockWrappedWithinNamespacedEntitySubsegment() throws Inter assertThat(AWSXRay.getTraceEntity().getSubsegments()) .hasSize(1) - .allSatisfy(subsegment -> { - assertThat(subsegment.getName()) - .isEqualTo("## testSubSegment"); - - assertThat(subsegment.getNamespace()) - .isEqualTo("testNamespace"); - - assertThat(subsegment.getAnnotations()) - .hasSize(1) - .containsEntry("key", "val"); - }); + .allSatisfy(subsegment -> + { + assertThat(subsegment.getName()) + .isEqualTo("## testSubSegment"); + + assertThat(subsegment.getNamespace()) + .isEqualTo("testNamespace"); + + assertThat(subsegment.getAnnotations()) + .hasSize(1) + .containsEntry("key", "val"); + }); } } \ No newline at end of file diff --git a/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerToolDisabled.java b/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerToolDisabled.java index 78878ffe5..1a9dc851a 100644 --- a/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerToolDisabled.java +++ b/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerToolDisabled.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.tracing.handlers; import com.amazonaws.services.lambda.runtime.Context; diff --git a/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerToolDisabledForStream.java b/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerToolDisabledForStream.java index 80f37b8b6..49bc4e095 100644 --- a/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerToolDisabledForStream.java +++ b/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerToolDisabledForStream.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,13 +11,13 @@ * limitations under the License. * */ -package software.amazon.lambda.powertools.tracing.handlers; -import java.io.InputStream; -import java.io.OutputStream; +package software.amazon.lambda.powertools.tracing.handlers; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestStreamHandler; +import java.io.InputStream; +import java.io.OutputStream; public class PowerToolDisabledForStream implements RequestStreamHandler { diff --git a/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabled.java b/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabled.java index 3be79fb76..5af3c65af 100644 --- a/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabled.java +++ b/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabled.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.tracing.handlers; import com.amazonaws.services.lambda.runtime.Context; diff --git a/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledExplicitlyForResponseAndError.java b/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledExplicitlyForResponseAndError.java index cd026f427..88b42c690 100644 --- a/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledExplicitlyForResponseAndError.java +++ b/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledExplicitlyForResponseAndError.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,14 +11,15 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.tracing.handlers; +import static software.amazon.lambda.powertools.tracing.CaptureMode.RESPONSE_AND_ERROR; + import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import software.amazon.lambda.powertools.tracing.Tracing; -import static software.amazon.lambda.powertools.tracing.CaptureMode.RESPONSE_AND_ERROR; - public class PowerTracerToolEnabledExplicitlyForResponseAndError implements RequestHandler { @Override diff --git a/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledForError.java b/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledForError.java index c84d25763..47e23ed18 100644 --- a/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledForError.java +++ b/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledForError.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,14 +11,15 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.tracing.handlers; +import static software.amazon.lambda.powertools.tracing.CaptureMode.ERROR; + import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import software.amazon.lambda.powertools.tracing.Tracing; -import static software.amazon.lambda.powertools.tracing.CaptureMode.ERROR; - public class PowerTracerToolEnabledForError implements RequestHandler { @Override diff --git a/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledForResponse.java b/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledForResponse.java index 1e82f2148..be0fe9b24 100644 --- a/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledForResponse.java +++ b/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledForResponse.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,14 +11,15 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.tracing.handlers; +import static software.amazon.lambda.powertools.tracing.CaptureMode.RESPONSE; + import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import software.amazon.lambda.powertools.tracing.Tracing; -import static software.amazon.lambda.powertools.tracing.CaptureMode.RESPONSE; - public class PowerTracerToolEnabledForResponse implements RequestHandler { @Override diff --git a/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledForResponseWithCustomMapper.java b/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledForResponseWithCustomMapper.java index b7c908473..a18b1580d 100644 --- a/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledForResponseWithCustomMapper.java +++ b/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledForResponseWithCustomMapper.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,9 +11,11 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.tracing.handlers; -import java.io.IOException; +import static software.amazon.lambda.powertools.tracing.CaptureMode.RESPONSE; + import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import com.fasterxml.jackson.core.JsonGenerator; @@ -21,11 +23,10 @@ import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import java.io.IOException; import software.amazon.lambda.powertools.tracing.Tracing; import software.amazon.lambda.powertools.tracing.TracingUtils; -import static software.amazon.lambda.powertools.tracing.CaptureMode.RESPONSE; - public class PowerTracerToolEnabledForResponseWithCustomMapper implements RequestHandler { static { ObjectMapper objectMapper = new ObjectMapper(); @@ -35,6 +36,7 @@ public class PowerTracerToolEnabledForResponseWithCustomMapper implements Reques TracingUtils.defaultObjectMapper(objectMapper); } + @Override @Tracing(namespace = "lambdaHandler", captureMode = RESPONSE) public Object handleRequest(Object input, Context context) { @@ -44,6 +46,25 @@ public Object handleRequest(Object input, Context context) { return parentClass; } + public static class ChildSerializer extends StdSerializer { + + public ChildSerializer() { + this(null); + } + + public ChildSerializer(Class t) { + super(t); + } + + @Override + public void serialize(ChildClass value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + jgen.writeStartObject(); + jgen.writeStringField("name", value.name); + jgen.writeStringField("p", value.p.name); + jgen.writeEndObject(); + } + } + public class ParentClass { public String name; public ChildClass c; @@ -66,23 +87,4 @@ public ChildClass(String name, ParentClass p) { this.p = p; } } - - public static class ChildSerializer extends StdSerializer { - - public ChildSerializer() { - this(null); - } - - public ChildSerializer(Class t) { - super(t); - } - - @Override - public void serialize(ChildClass value, JsonGenerator jgen, SerializerProvider provider) throws IOException { - jgen.writeStartObject(); - jgen.writeStringField("name", value.name); - jgen.writeStringField("p", value.p.name); - jgen.writeEndObject(); - } - } } diff --git a/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledForStream.java b/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledForStream.java index e316ffe7d..5c693445a 100644 --- a/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledForStream.java +++ b/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledForStream.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,14 +11,14 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.tracing.handlers; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestStreamHandler; -import software.amazon.lambda.powertools.tracing.Tracing; - import java.io.InputStream; import java.io.OutputStream; +import software.amazon.lambda.powertools.tracing.Tracing; public class PowerTracerToolEnabledForStream implements RequestStreamHandler { diff --git a/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledForStreamWithNoMetaData.java b/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledForStreamWithNoMetaData.java index 4cd381807..4cd2bb0b7 100644 --- a/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledForStreamWithNoMetaData.java +++ b/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledForStreamWithNoMetaData.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,17 +11,17 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.tracing.handlers; -import java.io.InputStream; -import java.io.OutputStream; +import static software.amazon.lambda.powertools.tracing.CaptureMode.DISABLED; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestStreamHandler; +import java.io.InputStream; +import java.io.OutputStream; import software.amazon.lambda.powertools.tracing.Tracing; -import static software.amazon.lambda.powertools.tracing.CaptureMode.DISABLED; - public class PowerTracerToolEnabledForStreamWithNoMetaData implements RequestStreamHandler { @Override diff --git a/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledWithException.java b/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledWithException.java index cc184d020..88c506502 100644 --- a/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledWithException.java +++ b/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledWithException.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.tracing.handlers; import com.amazonaws.services.lambda.runtime.Context; diff --git a/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledWithNoMetaData.java b/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledWithNoMetaData.java index 2109d6647..b4d77cde3 100644 --- a/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledWithNoMetaData.java +++ b/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledWithNoMetaData.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,14 +11,15 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.tracing.handlers; +import static software.amazon.lambda.powertools.tracing.CaptureMode.DISABLED; + import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import software.amazon.lambda.powertools.tracing.Tracing; -import static software.amazon.lambda.powertools.tracing.CaptureMode.DISABLED; - public class PowerTracerToolEnabledWithNoMetaData implements RequestHandler { @Override diff --git a/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledWithNoMetaDataDeprecated.java b/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledWithNoMetaDataDeprecated.java index 2ded5e69f..c13f28df0 100644 --- a/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledWithNoMetaDataDeprecated.java +++ b/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledWithNoMetaDataDeprecated.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,14 +11,13 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.tracing.handlers; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import software.amazon.lambda.powertools.tracing.Tracing; -import static software.amazon.lambda.powertools.tracing.CaptureMode.DISABLED; - public class PowerTracerToolEnabledWithNoMetaDataDeprecated implements RequestHandler { @Override diff --git a/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/internal/LambdaTracingAspectTest.java b/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/internal/LambdaTracingAspectTest.java index 8cd9b2f71..5e3ec6545 100644 --- a/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/internal/LambdaTracingAspectTest.java +++ b/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/internal/LambdaTracingAspectTest.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,15 +11,24 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.tracing.internal; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; +import static org.apache.commons.lang3.reflect.FieldUtils.writeStaticField; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.Assertions.catchThrowable; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.openMocks; + import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import com.amazonaws.services.lambda.runtime.RequestStreamHandler; import com.amazonaws.xray.AWSXRay; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -41,14 +50,6 @@ import software.amazon.lambda.powertools.tracing.handlers.PowerTracerToolEnabledWithNoMetaDataDeprecated; import software.amazon.lambda.powertools.tracing.nonhandler.PowerToolNonHandler; -import static org.apache.commons.lang3.reflect.FieldUtils.writeStaticField; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatNoException; -import static org.assertj.core.api.Assertions.catchThrowable; -import static org.mockito.Mockito.mockStatic; -import static org.mockito.Mockito.when; -import static org.mockito.MockitoAnnotations.openMocks; - class LambdaTracingAspectTest { private RequestHandler requestHandler; private RequestStreamHandler streamHandler; @@ -118,16 +119,17 @@ void shouldCaptureTraces() { assertThat(AWSXRay.getTraceEntity().getSubsegmentsCopy()) .hasSize(1) - .allSatisfy(subsegment -> { - assertThat(subsegment.getAnnotations()) - .hasSize(2) - .containsEntry("ColdStart", true) - .containsEntry("Service", "lambdaHandler"); - - assertThat(subsegment.getMetadata()) - .hasSize(1) - .containsKey("lambdaHandler"); - }); + .allSatisfy(subsegment -> + { + assertThat(subsegment.getAnnotations()) + .hasSize(2) + .containsEntry("ColdStart", true) + .containsEntry("Service", "lambdaHandler"); + + assertThat(subsegment.getMetadata()) + .hasSize(1) + .containsKey("lambdaHandler"); + }); } @Test @@ -141,20 +143,21 @@ void shouldCaptureTracesWithExceptionMetaData() { assertThat(AWSXRay.getTraceEntity().getSubsegmentsCopy()) .hasSize(1) - .allSatisfy(subsegment -> { - assertThat(subsegment.getAnnotations()) - .hasSize(2) - .containsEntry("ColdStart", true) - .containsEntry("Service", "lambdaHandler"); - - assertThat(subsegment.getMetadata()) - .hasSize(1) - .containsKey("lambdaHandler"); - - assertThat(subsegment.getMetadata().get("lambdaHandler")) - .satisfies(stringObjectMap -> assertThat(stringObjectMap) - .containsEntry("handleRequest error", exception)); - }); + .allSatisfy(subsegment -> + { + assertThat(subsegment.getAnnotations()) + .hasSize(2) + .containsEntry("ColdStart", true) + .containsEntry("Service", "lambdaHandler"); + + assertThat(subsegment.getMetadata()) + .hasSize(1) + .containsKey("lambdaHandler"); + + assertThat(subsegment.getMetadata().get("lambdaHandler")) + .satisfies(stringObjectMap -> assertThat(stringObjectMap) + .containsEntry("handleRequest error", exception)); + }); } @Test @@ -166,16 +169,17 @@ void shouldCaptureTracesForStream() throws IOException { assertThat(AWSXRay.getTraceEntity().getSubsegmentsCopy()) .hasSize(1) - .allSatisfy(subsegment -> { - assertThat(subsegment.getAnnotations()) - .hasSize(2) - .containsEntry("ColdStart", true) - .containsEntry("Service", "streamHandler"); - - assertThat(subsegment.getMetadata()) - .hasSize(1) - .containsKey("streamHandler"); - }); + .allSatisfy(subsegment -> + { + assertThat(subsegment.getAnnotations()) + .hasSize(2) + .containsEntry("ColdStart", true) + .containsEntry("Service", "streamHandler"); + + assertThat(subsegment.getMetadata()) + .hasSize(1) + .containsKey("streamHandler"); + }); } @Test @@ -207,15 +211,16 @@ void shouldCaptureTracesWithNoMetadata() { assertThat(AWSXRay.getTraceEntity().getSubsegmentsCopy()) .hasSize(1) - .allSatisfy(subsegment -> { - assertThat(subsegment.getAnnotations()) - .hasSize(2) - .containsEntry("ColdStart", true) - .containsEntry("Service", "service_undefined"); - - assertThat(subsegment.getMetadata()) - .isEmpty(); - }); + .allSatisfy(subsegment -> + { + assertThat(subsegment.getAnnotations()) + .hasSize(2) + .containsEntry("ColdStart", true) + .containsEntry("Service", "service_undefined"); + + assertThat(subsegment.getMetadata()) + .isEmpty(); + }); } @Test @@ -229,15 +234,16 @@ void shouldCaptureTracesForStreamWithNoMetadata() throws IOException { assertThat(AWSXRay.getTraceEntity().getSubsegmentsCopy()) .hasSize(1) - .allSatisfy(subsegment -> { - assertThat(subsegment.getAnnotations()) - .hasSize(2) - .containsEntry("ColdStart", true) - .containsEntry("Service", "service_undefined"); - - assertThat(subsegment.getMetadata()) - .isEmpty(); - }); + .allSatisfy(subsegment -> + { + assertThat(subsegment.getAnnotations()) + .hasSize(2) + .containsEntry("ColdStart", true) + .containsEntry("Service", "service_undefined"); + + assertThat(subsegment.getMetadata()) + .isEmpty(); + }); } @Test @@ -251,15 +257,16 @@ void shouldCaptureTracesWithNoMetadataDeprecated() { assertThat(AWSXRay.getTraceEntity().getSubsegmentsCopy()) .hasSize(1) - .allSatisfy(subsegment -> { - assertThat(subsegment.getAnnotations()) - .hasSize(2) - .containsEntry("ColdStart", true) - .containsEntry("Service", "service_undefined"); - - assertThat(subsegment.getMetadata()) - .isEmpty(); - }); + .allSatisfy(subsegment -> + { + assertThat(subsegment.getAnnotations()) + .hasSize(2) + .containsEntry("ColdStart", true) + .containsEntry("Service", "service_undefined"); + + assertThat(subsegment.getMetadata()) + .isEmpty(); + }); } @Test @@ -275,15 +282,16 @@ void shouldNotCaptureTracesIfDisabledViaEnvironmentVariable() { assertThat(AWSXRay.getTraceEntity().getSubsegmentsCopy()) .hasSize(1) - .allSatisfy(subsegment -> { - assertThat(subsegment.getAnnotations()) - .hasSize(2) - .containsEntry("ColdStart", true) - .containsEntry("Service", "lambdaHandler"); - - assertThat(subsegment.getMetadata()) - .isEmpty(); - }); + .allSatisfy(subsegment -> + { + assertThat(subsegment.getAnnotations()) + .hasSize(2) + .containsEntry("ColdStart", true) + .containsEntry("Service", "lambdaHandler"); + + assertThat(subsegment.getMetadata()) + .isEmpty(); + }); } } @@ -300,16 +308,17 @@ void shouldCaptureTracesIfExplicitlyEnabledAndEnvironmentVariableIsDisabled() { assertThat(AWSXRay.getTraceEntity().getSubsegmentsCopy()) .hasSize(1) - .allSatisfy(subsegment -> { - assertThat(subsegment.getAnnotations()) - .hasSize(2) - .containsEntry("ColdStart", true) - .containsEntry("Service", "lambdaHandler"); - - assertThat(subsegment.getMetadata()) - .hasSize(1) - .containsKey("lambdaHandler"); - }); + .allSatisfy(subsegment -> + { + assertThat(subsegment.getAnnotations()) + .hasSize(2) + .containsEntry("ColdStart", true) + .containsEntry("Service", "lambdaHandler"); + + assertThat(subsegment.getMetadata()) + .hasSize(1) + .containsKey("lambdaHandler"); + }); } } @@ -324,14 +333,16 @@ void shouldCaptureTracesForSelfReferencingReturnTypesViaCustomMapper() { assertThat(AWSXRay.getTraceEntity().getSubsegmentsCopy()) .hasSize(1) - .allSatisfy(subsegment -> { - assertThat(subsegment.getMetadata()) - .hasSize(1) - .containsKey("lambdaHandler"); + .allSatisfy(subsegment -> + { + assertThat(subsegment.getMetadata()) + .hasSize(1) + .containsKey("lambdaHandler"); - assertThat(subsegment.getMetadata().get("lambdaHandler")) - .hasFieldOrPropertyWithValue("handleRequest response", "{\"name\":\"parent\",\"c\":{\"name\":\"child\",\"p\":\"parent\"}}"); - }); + assertThat(subsegment.getMetadata().get("lambdaHandler")) + .hasFieldOrPropertyWithValue("handleRequest response", + "{\"name\":\"parent\",\"c\":{\"name\":\"child\",\"p\":\"parent\"}}"); + }); assertThatNoException().isThrownBy(AWSXRay::endSegment); @@ -354,16 +365,17 @@ void shouldCaptureTracesIfExplicitlyEnabledBothAndEnvironmentVariableIsDisabled( assertThat(AWSXRay.getTraceEntity().getSubsegmentsCopy()) .hasSize(1) - .allSatisfy(subsegment -> { - assertThat(subsegment.getAnnotations()) - .hasSize(2) - .containsEntry("ColdStart", true) - .containsEntry("Service", "lambdaHandler"); - - assertThat(subsegment.getMetadata()) - .hasSize(1) - .containsKey("lambdaHandler"); - }); + .allSatisfy(subsegment -> + { + assertThat(subsegment.getAnnotations()) + .hasSize(2) + .containsEntry("ColdStart", true) + .containsEntry("Service", "lambdaHandler"); + + assertThat(subsegment.getMetadata()) + .hasSize(1) + .containsKey("lambdaHandler"); + }); } } @@ -384,15 +396,16 @@ void shouldNotCaptureTracesWithExceptionMetaDataIfDisabledViaEnvironmentVariable assertThat(AWSXRay.getTraceEntity().getSubsegmentsCopy()) .hasSize(1) - .allSatisfy(subsegment -> { - assertThat(subsegment.getAnnotations()) - .hasSize(2) - .containsEntry("ColdStart", true) - .containsEntry("Service", "lambdaHandler"); - - assertThat(subsegment.getMetadata()) - .isEmpty(); - }); + .allSatisfy(subsegment -> + { + assertThat(subsegment.getAnnotations()) + .hasSize(2) + .containsEntry("ColdStart", true) + .containsEntry("Service", "lambdaHandler"); + + assertThat(subsegment.getMetadata()) + .isEmpty(); + }); } } @@ -410,20 +423,21 @@ void shouldCaptureTracesWithExceptionMetaDataEnabledExplicitlyAndEnvironmentVari assertThat(AWSXRay.getTraceEntity().getSubsegmentsCopy()) .hasSize(1) - .allSatisfy(subsegment -> { - assertThat(subsegment.getAnnotations()) - .hasSize(2) - .containsEntry("ColdStart", true) - .containsEntry("Service", "lambdaHandler"); - - assertThat(subsegment.getMetadata()) - .hasSize(1) - .containsKey("lambdaHandler"); - - assertThat(subsegment.getMetadata().get("lambdaHandler")) - .satisfies(stringObjectMap -> assertThat(stringObjectMap) - .containsEntry("handleRequest error", exception)); - }); + .allSatisfy(subsegment -> + { + assertThat(subsegment.getAnnotations()) + .hasSize(2) + .containsEntry("ColdStart", true) + .containsEntry("Service", "lambdaHandler"); + + assertThat(subsegment.getMetadata()) + .hasSize(1) + .containsKey("lambdaHandler"); + + assertThat(subsegment.getMetadata().get("lambdaHandler")) + .satisfies(stringObjectMap -> assertThat(stringObjectMap) + .containsEntry("handleRequest error", exception)); + }); } } diff --git a/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/nonhandler/PowerToolNonHandler.java b/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/nonhandler/PowerToolNonHandler.java index 309eb5598..48ffc93ae 100644 --- a/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/nonhandler/PowerToolNonHandler.java +++ b/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/nonhandler/PowerToolNonHandler.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.tracing.nonhandler; import software.amazon.lambda.powertools.tracing.Tracing; diff --git a/powertools-validation/pom.xml b/powertools-validation/pom.xml index c8a1f60c4..1b939f46f 100644 --- a/powertools-validation/pom.xml +++ b/powertools-validation/pom.xml @@ -1,6 +1,20 @@ - + + 4.0.0 @@ -73,7 +87,7 @@ com.networknt json-schema-validator - 1.0.85 + 1.0.86 com.amazonaws @@ -112,6 +126,20 @@ assertj-core test + + org.junit.jupiter + junit-jupiter-params + test + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + + \ No newline at end of file diff --git a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/Validation.java b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/Validation.java index 68260cb47..f41364c1a 100644 --- a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/Validation.java +++ b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/Validation.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,11 +11,13 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.validation; +import static com.networknt.schema.SpecVersion.VersionFlag.V7; + import com.amazonaws.services.lambda.runtime.Context; import com.networknt.schema.SpecVersion.VersionFlag; - import java.io.InputStream; import java.io.OutputStream; import java.lang.annotation.ElementType; @@ -23,8 +25,6 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import static com.networknt.schema.SpecVersion.VersionFlag.V7; - /** * {@link Validation} is used to specify that the annotated method input and/or output needs to be valid.
    * diff --git a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/ValidationConfig.java b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/ValidationConfig.java index 3fd964226..baf5e2465 100644 --- a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/ValidationConfig.java +++ b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/ValidationConfig.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.validation; import com.fasterxml.jackson.databind.JsonNode; @@ -26,24 +27,24 @@ /** * Use this if you need to customize some part of the JSON Schema validation * (eg. specification version, Jackson ObjectMapper, or adding functions to JMESPath). - * + *

    * For everything but the validation features (factory, schemaVersion), {@link ValidationConfig} * is just a wrapper of {@link JsonConfig}. */ public class ValidationConfig { - private ValidationConfig() { - } + private SpecVersion.VersionFlag jsonSchemaVersion = SpecVersion.VersionFlag.V7; + private JsonSchemaFactory factory = JsonSchemaFactory.getInstance(jsonSchemaVersion); - private static class ConfigHolder { - private final static ValidationConfig instance = new ValidationConfig(); + private ValidationConfig() { } public static ValidationConfig get() { return ConfigHolder.instance; } - private SpecVersion.VersionFlag jsonSchemaVersion = SpecVersion.VersionFlag.V7; - private JsonSchemaFactory factory = JsonSchemaFactory.getInstance(jsonSchemaVersion); + public SpecVersion.VersionFlag getSchemaVersion() { + return jsonSchemaVersion; + } /** * Set the version of the json schema specifications (default is V7) @@ -57,16 +58,12 @@ public void setSchemaVersion(SpecVersion.VersionFlag version) { } } - public SpecVersion.VersionFlag getSchemaVersion() { - return jsonSchemaVersion; - } - /** * Add a custom {@link io.burt.jmespath.function.Function} to JMESPath * {@link Base64Function} and {@link Base64GZipFunction} are already built-in. * * @param function the function to add - * @param Must extends {@link BaseFunction} + * @param Must extend {@link BaseFunction} */ public void addFunction(T function) { JsonConfig.get().addFunction(function); @@ -98,4 +95,8 @@ public JmesPath getJmesPath() { public ObjectMapper getObjectMapper() { return JsonConfig.get().getObjectMapper(); } + + private static class ConfigHolder { + private final static ValidationConfig instance = new ValidationConfig(); + } } diff --git a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/ValidationException.java b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/ValidationException.java index 2d3e1b350..fd4cb66a6 100644 --- a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/ValidationException.java +++ b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/ValidationException.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.validation; public class ValidationException extends RuntimeException { diff --git a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/ValidationUtils.java b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/ValidationUtils.java index 9b73806a5..4eecb3ab5 100644 --- a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/ValidationUtils.java +++ b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/ValidationUtils.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,16 +11,8 @@ * limitations under the License. * */ -package software.amazon.lambda.powertools.validation; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.Collections; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; +package software.amazon.lambda.powertools.validation; import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer; import com.amazonaws.services.lambda.runtime.serialization.events.LambdaEventSerializers; @@ -31,6 +23,13 @@ import com.networknt.schema.JsonSchema; import com.networknt.schema.ValidationMessage; import io.burt.jmespath.Expression; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; import software.amazon.lambda.powertools.validation.internal.ValidationAspect; /** @@ -69,7 +68,8 @@ public static void validate(Object obj, JsonSchema jsonSchema, String envelope) } JsonNode subNode; try { - PojoSerializer pojoSerializer = LambdaEventSerializers.serializerFor(obj.getClass(), ClassLoader.getSystemClassLoader()); + PojoSerializer pojoSerializer = + LambdaEventSerializers.serializerFor(obj.getClass(), ClassLoader.getSystemClassLoader()); ByteArrayOutputStream out = new ByteArrayOutputStream(); pojoSerializer.toJson(obj, out); JsonNode jsonNode = ValidationConfig.get().getObjectMapper().readTree(out.toString("UTF-8")); @@ -79,7 +79,7 @@ public static void validate(Object obj, JsonSchema jsonSchema, String envelope) throw new ValidationException("Envelope not found in the object"); } } catch (Exception e) { - throw new ValidationException("Cannot find envelope <"+envelope+"> in the object <"+obj+">", e); + throw new ValidationException("Cannot find envelope <" + envelope + "> in the object <" + obj + ">", e); } if (subNode.getNodeType() == JsonNodeType.ARRAY) { subNode.forEach(jsonNode -> validate(jsonNode, jsonSchema)); @@ -90,7 +90,8 @@ public static void validate(Object obj, JsonSchema jsonSchema, String envelope) try { validate(subNode.asText(), jsonSchema); } catch (ValidationException e) { - throw new ValidationException("Invalid format for '" + envelope + "': 'STRING' and no JSON found in it."); + throw new ValidationException( + "Invalid format for '" + envelope + "': 'STRING' and no JSON found in it."); } } else { throw new ValidationException("Invalid format for '" + envelope + "': '" + subNode.getNodeType() + "'"); @@ -120,7 +121,7 @@ public static void validate(Object obj, JsonSchema jsonSchema) throws Validation try { jsonNode = ValidationConfig.get().getObjectMapper().valueToTree(obj); } catch (Exception e) { - throw new ValidationException("Object <"+obj+"> is not valid against the schema provided", e); + throw new ValidationException("Object <" + obj + "> is not valid against the schema provided", e); } validate(jsonNode, jsonSchema); @@ -149,7 +150,7 @@ public static void validate(String json, JsonSchema jsonSchema) throws Validatio try { jsonNode = ValidationConfig.get().getObjectMapper().readTree(json); } catch (Exception e) { - throw new ValidationException("Json <"+json+"> is not valid against the schema provided", e); + throw new ValidationException("Json <" + json + "> is not valid against the schema provided", e); } validate(jsonNode, jsonSchema); @@ -178,7 +179,7 @@ public static void validate(Map map, JsonSchema jsonSchema) thro try { jsonNode = ValidationConfig.get().getObjectMapper().valueToTree(map); } catch (Exception e) { - throw new ValidationException("Map <"+map+"> cannot be converted to json for validation", e); + throw new ValidationException("Map <" + map + "> cannot be converted to json for validation", e); } validate(jsonNode, jsonSchema); @@ -209,9 +210,11 @@ public static void validate(JsonNode jsonNode, JsonSchema jsonSchema) throws Val if (!validationMessages.isEmpty()) { String message; try { - message = ValidationConfig.get().getObjectMapper().writeValueAsString(new ValidationErrors(validationMessages)); + message = ValidationConfig.get().getObjectMapper() + .writeValueAsString(new ValidationErrors(validationMessages)); } catch (JsonProcessingException e) { - message = validationMessages.stream().map(ValidationMessage::getMessage).collect(Collectors.joining(", ")); + message = validationMessages.stream().map(ValidationMessage::getMessage) + .collect(Collectors.joining(", ")); } throw new ValidationException(message); } @@ -253,13 +256,11 @@ private static JsonSchema createJsonSchema(String schema) { if (schema.startsWith(CLASSPATH)) { String filePath = schema.substring(CLASSPATH.length()); try (InputStream schemaStream = ValidationAspect.class.getResourceAsStream(filePath)) { - if (schemaStream == null) { - throw new IllegalArgumentException("'" + schema + "' is invalid, verify '" + filePath + "' is in your classpath"); - } jsonSchema = ValidationConfig.get().getFactory().getSchema(schemaStream); - } catch (IOException e) { - throw new IllegalArgumentException("'" + schema + "' is invalid, verify '" + filePath + "' is in your classpath"); + } catch (Exception e) { + throw new IllegalArgumentException( + "'" + schema + "' is invalid, verify '" + filePath + "' is in your classpath"); } } else { jsonSchema = ValidationConfig.get().getFactory().getSchema(schema); @@ -274,7 +275,8 @@ private static void validateSchema(String schema, JsonSchema jsonSchema) { validate(jsonSchema.getSchemaNode(), getJsonSchema("classpath:/schemas/meta_schema_" + version)); } catch (ValidationException ve) { - throw new IllegalArgumentException("The schema " + schema + " is not valid, it does not respect the specification " + version, ve); + throw new IllegalArgumentException( + "The schema " + schema + " is not valid, it does not respect the specification " + version, ve); } } diff --git a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java index be19a50a0..6055f8d58 100644 --- a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java +++ b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.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,9 +11,37 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.validation.internal; -import com.amazonaws.services.lambda.runtime.events.*; +import static com.networknt.schema.SpecVersion.VersionFlag.V201909; +import static java.nio.charset.StandardCharsets.UTF_8; +import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.placedOnRequestHandler; +import static software.amazon.lambda.powertools.utilities.jmespath.Base64Function.decode; +import static software.amazon.lambda.powertools.utilities.jmespath.Base64GZipFunction.decompress; +import static software.amazon.lambda.powertools.validation.ValidationUtils.getJsonSchema; +import static software.amazon.lambda.powertools.validation.ValidationUtils.validate; + +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPResponse; +import com.amazonaws.services.lambda.runtime.events.APIGatewayV2WebSocketResponse; +import com.amazonaws.services.lambda.runtime.events.ActiveMQEvent; +import com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerRequestEvent; +import com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerResponseEvent; +import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent; +import com.amazonaws.services.lambda.runtime.events.CloudWatchLogsEvent; +import com.amazonaws.services.lambda.runtime.events.KafkaEvent; +import com.amazonaws.services.lambda.runtime.events.KinesisAnalyticsFirehoseInputPreprocessingEvent; +import com.amazonaws.services.lambda.runtime.events.KinesisAnalyticsInputPreprocessingResponse; +import com.amazonaws.services.lambda.runtime.events.KinesisAnalyticsStreamsInputPreprocessingEvent; +import com.amazonaws.services.lambda.runtime.events.KinesisEvent; +import com.amazonaws.services.lambda.runtime.events.KinesisFirehoseEvent; +import com.amazonaws.services.lambda.runtime.events.RabbitMQEvent; +import com.amazonaws.services.lambda.runtime.events.SNSEvent; +import com.amazonaws.services.lambda.runtime.events.SQSEvent; +import com.amazonaws.services.lambda.runtime.events.ScheduledEvent; import com.networknt.schema.JsonSchema; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; @@ -24,14 +52,6 @@ import software.amazon.lambda.powertools.validation.Validation; import software.amazon.lambda.powertools.validation.ValidationConfig; -import static com.networknt.schema.SpecVersion.VersionFlag.V201909; -import static java.nio.charset.StandardCharsets.UTF_8; -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.placedOnRequestHandler; -import static software.amazon.lambda.powertools.utilities.jmespath.Base64Function.decode; -import static software.amazon.lambda.powertools.utilities.jmespath.Base64GZipFunction.decompress; -import static software.amazon.lambda.powertools.validation.ValidationUtils.getJsonSchema; -import static software.amazon.lambda.powertools.validation.ValidationUtils.validate; - /** * Aspect for {@link Validation} annotation */ @@ -89,27 +109,33 @@ public Object around(ProceedingJoinPoint pjp, validate(event.getResourceProperties(), inboundJsonSchema); } else if (obj instanceof KinesisEvent) { KinesisEvent event = (KinesisEvent) obj; - event.getRecords().forEach(record -> validate(decode(record.getKinesis().getData()), inboundJsonSchema)); + event.getRecords() + .forEach(record -> validate(decode(record.getKinesis().getData()), inboundJsonSchema)); } else if (obj instanceof KinesisFirehoseEvent) { KinesisFirehoseEvent event = (KinesisFirehoseEvent) obj; event.getRecords().forEach(record -> validate(decode(record.getData()), inboundJsonSchema)); } else if (obj instanceof KafkaEvent) { KafkaEvent event = (KafkaEvent) obj; - event.getRecords().forEach((s, records) -> records.forEach(record -> validate(decode(record.getValue()), inboundJsonSchema))); + event.getRecords().forEach((s, records) -> records.forEach( + record -> validate(decode(record.getValue()), inboundJsonSchema))); } else if (obj instanceof ActiveMQEvent) { ActiveMQEvent event = (ActiveMQEvent) obj; event.getMessages().forEach(record -> validate(decode(record.getData()), inboundJsonSchema)); } else if (obj instanceof RabbitMQEvent) { RabbitMQEvent event = (RabbitMQEvent) obj; - event.getRmqMessagesByQueue().forEach((s, records) -> records.forEach(record -> validate(decode(record.getData()), inboundJsonSchema))); + event.getRmqMessagesByQueue().forEach((s, records) -> records.forEach( + record -> validate(decode(record.getData()), inboundJsonSchema))); } else if (obj instanceof KinesisAnalyticsFirehoseInputPreprocessingEvent) { - KinesisAnalyticsFirehoseInputPreprocessingEvent event = (KinesisAnalyticsFirehoseInputPreprocessingEvent) obj; + KinesisAnalyticsFirehoseInputPreprocessingEvent event = + (KinesisAnalyticsFirehoseInputPreprocessingEvent) obj; event.getRecords().forEach(record -> validate(decode(record.getData()), inboundJsonSchema)); } else if (obj instanceof KinesisAnalyticsStreamsInputPreprocessingEvent) { - KinesisAnalyticsStreamsInputPreprocessingEvent event = (KinesisAnalyticsStreamsInputPreprocessingEvent) obj; + KinesisAnalyticsStreamsInputPreprocessingEvent event = + (KinesisAnalyticsStreamsInputPreprocessingEvent) obj; event.getRecords().forEach(record -> validate(decode(record.getData()), inboundJsonSchema)); } else { - LOG.warn("Unhandled event type {}, please use the 'envelope' parameter to specify what to validate", obj.getClass().getName()); + LOG.warn("Unhandled event type {}, please use the 'envelope' parameter to specify what to validate", + obj.getClass().getName()); } } } @@ -132,10 +158,12 @@ public Object around(ProceedingJoinPoint pjp, ApplicationLoadBalancerResponseEvent response = (ApplicationLoadBalancerResponseEvent) result; validate(response.getBody(), outboundJsonSchema); } else if (result instanceof KinesisAnalyticsInputPreprocessingResponse) { - KinesisAnalyticsInputPreprocessingResponse response = (KinesisAnalyticsInputPreprocessingResponse) result; + KinesisAnalyticsInputPreprocessingResponse response = + (KinesisAnalyticsInputPreprocessingResponse) result; response.getRecords().forEach(record -> validate(decode(record.getData()), outboundJsonSchema)); } else { - LOG.warn("Unhandled response type {}, please use the 'envelope' parameter to specify what to validate", result.getClass().getName()); + LOG.warn("Unhandled response type {}, please use the 'envelope' parameter to specify what to validate", + result.getClass().getName()); } } diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/ValidationUtilsTest.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/ValidationUtilsTest.java index d5e9332ed..86dddd3e2 100644 --- a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/ValidationUtilsTest.java +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/ValidationUtilsTest.java @@ -1,25 +1,42 @@ +/* + * 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.validation; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static software.amazon.lambda.powertools.validation.ValidationUtils.getJsonSchema; +import static software.amazon.lambda.powertools.validation.ValidationUtils.validate; + import com.fasterxml.jackson.databind.JsonNode; import com.networknt.schema.JsonSchema; import com.networknt.schema.SpecVersion; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import software.amazon.lambda.powertools.validation.model.Basket; import software.amazon.lambda.powertools.validation.model.MyCustomEvent; import software.amazon.lambda.powertools.validation.model.Product; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; - -import static org.assertj.core.api.Assertions.*; -import static software.amazon.lambda.powertools.validation.ValidationUtils.getJsonSchema; -import static software.amazon.lambda.powertools.validation.ValidationUtils.validate; - public class ValidationUtilsTest { - private JsonSchema schema = getJsonSchema("classpath:/schema_v7.json"); + private String schemaString = "classpath:/schema_v7.json"; + private JsonSchema schema = getJsonSchema(schemaString); @BeforeEach public void setup() { @@ -39,13 +56,18 @@ public void testLoadSchemaV7KO() { ValidationConfig.get().setSchemaVersion(SpecVersion.VersionFlag.V7); assertThatThrownBy(() -> getJsonSchema("classpath:/schema_v7_ko.json", true)) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("The schema classpath:/schema_v7_ko.json is not valid, it does not respect the specification V7"); + .hasMessage( + "The schema classpath:/schema_v7_ko.json is not valid, it does not respect the specification V7"); } @Test public void testLoadMetaSchema_NoValidation() { - ValidationConfig.get().setSchemaVersion(SpecVersion.VersionFlag.V201909); - getJsonSchema("classpath:/schemas/meta_schema_V201909", false); + ValidationConfig.get().setSchemaVersion(SpecVersion.VersionFlag.V7); + + assertThatNoException().isThrownBy(() -> + { + getJsonSchema("classpath:/schema_v7_ko.json", false); + }); } @Test @@ -92,26 +114,35 @@ public void testLoadSchemaNotFound() { @Test public void testValidateJsonNodeOK() throws IOException { - JsonNode node = ValidationConfig.get().getObjectMapper().readTree(this.getClass().getResourceAsStream("/json_ok.json")); + JsonNode node = + ValidationConfig.get().getObjectMapper().readTree(this.getClass().getResourceAsStream("/json_ok.json")); - validate(node, schema); + assertThatNoException().isThrownBy(() -> + { + validate(node, schemaString); + }); } @Test public void testValidateJsonNodeKO() throws IOException { - JsonNode node = ValidationConfig.get().getObjectMapper().readTree(this.getClass().getResourceAsStream("/json_ko.json")); + JsonNode node = + ValidationConfig.get().getObjectMapper().readTree(this.getClass().getResourceAsStream("/json_ko.json")); assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> validate(node, schema)); } @Test public void testValidateMapOK() { + Map map = new HashMap<>(); map.put("id", 43242); map.put("name", "FooBar XY"); map.put("price", 258); - validate(map, schema); + assertThatNoException().isThrownBy(() -> + { + validate(map, schemaString); + }); } @Test @@ -123,11 +154,22 @@ public void testValidateMapKO() { assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> validate(map, schema)); } + @Test + public void testValidateMapNotValidJsonObject() { + Map map = new HashMap<>(); + map.put("1234", new Object()); + + assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> validate(map, schema)); + } + @Test public void testValidateStringOK() { String json = "{\n \"id\": 43242,\n \"name\": \"FooBar XY\",\n \"price\": 258\n}"; - validate(json, schema); + assertThatNoException().isThrownBy(() -> + { + validate(json, schemaString); + }); } @Test @@ -140,14 +182,23 @@ public void testValidateStringKO() { @Test public void testValidateObjectOK() { Product product = new Product(42, "FooBar", 42); - validate(product, schema); + + assertThatNoException().isThrownBy(() -> + { + validate(product, schemaString); + }); } @Test public void testValidateObjectKO() { - Product product = new Product(42, "FooBar", -12); - assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> validate(product, schema)); + assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> validate(new Object(), schema)); + } + + @Test + public void testValidateObjectNotValidJson() { + + assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> validate(new Object(), schema)); } @Test @@ -158,7 +209,11 @@ public void testValidateSubObjectOK() { basket.add(product); basket.add(product2); MyCustomEvent event = new MyCustomEvent(basket); - validate(event, schema, "basket.products[0]"); + + assertThatNoException().isThrownBy(() -> + { + validate(event, schemaString, "basket.products[0]"); + }); } @Test @@ -170,7 +225,8 @@ public void testValidateSubObjectKO() { basket.add(product2); MyCustomEvent event = new MyCustomEvent(basket); - assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> validate(event, schema, "basket.products[0]")); + assertThatExceptionOfType(ValidationException.class).isThrownBy( + () -> validate(event, schema, "basket.products[0]")); } @Test @@ -182,7 +238,7 @@ public void testValidateSubObjectListOK() { basket.add(product2); MyCustomEvent event = new MyCustomEvent(basket); - validate(event, schema, "basket.products[*]"); + assertThatNoException().isThrownBy(() -> validate(event, schema, "basket.products[*]")); } @Test @@ -194,7 +250,8 @@ public void testValidateSubObjectListKO() { basket.add(product2); MyCustomEvent event = new MyCustomEvent(basket); - assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> validate(event, schema, "basket.products[*]")); + assertThatExceptionOfType(ValidationException.class).isThrownBy( + () -> validate(event, schema, "basket.products[*]")); } @Test @@ -203,7 +260,7 @@ public void testValidateSubObjectNotFound() { Basket basket = new Basket(); basket.add(product); MyCustomEvent event = new MyCustomEvent(basket); - assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> validate(event, schema, "basket.product")); + assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> validate(event, schema, "basket.")); } @Test @@ -226,7 +283,7 @@ public void testValidateSubObjectJsonString() { basket.setHiddenProduct("ewogICJpZCI6IDQzMjQyLAogICJuYW1lIjogIkZvb0JhciBYWSIsCiAgInByaWNlIjogMjU4Cn0="); MyCustomEvent event = new MyCustomEvent(basket); - validate(event, schema, "basket.powertools_base64(hiddenProduct)"); + assertThatNoException().isThrownBy(() -> validate(event, schema, "basket.powertools_base64(hiddenProduct)")); } @Test @@ -243,7 +300,13 @@ public void testValidateSubObjectSimpleString() { @Test public void testValidateSubObjectWithoutEnvelope() { Product product = new Product(42, "BarBazFoo", 42); - validate(product, schema, null); + assertThatNoException().isThrownBy(() -> validate(product, schema, null)); + } + + @Test + public void testValidateSubObjectWithEmptyEnvelope() { + Product product = new Product(42, "BarBazFoo", 42); + assertThatNoException().isThrownBy(() -> validate(product, schema, "")); } } diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/SQSHandler.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/GenericSchemaV7Handler.java similarity index 78% rename from powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/SQSHandler.java rename to powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/GenericSchemaV7Handler.java index cd6719b0f..5b8343d1b 100644 --- a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/SQSHandler.java +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/GenericSchemaV7Handler.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.validation.handlers; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; -import com.amazonaws.services.lambda.runtime.events.SQSEvent; import software.amazon.lambda.powertools.validation.Validation; -public class SQSHandler implements RequestHandler { +public class GenericSchemaV7Handler implements RequestHandler { - @Override @Validation(inboundSchema = "classpath:/schema_v7.json") - public String handleRequest(SQSEvent input, Context context) { + @Override + public String handleRequest(T input, Context context) { return "OK"; } } diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/KinesisHandler.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/KinesisHandler.java deleted file mode 100644 index 7132fcb9b..000000000 --- a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/KinesisHandler.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2020 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.validation.handlers; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import com.amazonaws.services.lambda.runtime.events.KinesisEvent; -import software.amazon.lambda.powertools.validation.Validation; - -public class KinesisHandler implements RequestHandler { - - @Validation(inboundSchema = "classpath:/schema_v7.json") - @Override - public String handleRequest(KinesisEvent input, Context context) { - return "OK"; - } -} diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/MyCustomEventHandler.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/MyCustomEventHandler.java index 07954ddff..6989cdbb6 100644 --- a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/MyCustomEventHandler.java +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/MyCustomEventHandler.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.validation.handlers; import com.amazonaws.services.lambda.runtime.Context; diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/SQSWithCustomEnvelopeHandler.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/SQSWithCustomEnvelopeHandler.java index 9eb96c0e8..c49ebff69 100644 --- a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/SQSWithCustomEnvelopeHandler.java +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/SQSWithCustomEnvelopeHandler.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.validation.handlers; import com.amazonaws.services.lambda.runtime.Context; diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/SQSWithWrongEnvelopeHandler.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/SQSWithWrongEnvelopeHandler.java index c1859f29a..d3f46d4ad 100644 --- a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/SQSWithWrongEnvelopeHandler.java +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/SQSWithWrongEnvelopeHandler.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.validation.handlers; import com.amazonaws.services.lambda.runtime.Context; diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/ValidationInboundClasspathHandler.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/ValidationInboundClasspathHandler.java deleted file mode 100644 index 59b6cc7b5..000000000 --- a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/ValidationInboundClasspathHandler.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2020 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.validation.handlers; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; -import software.amazon.lambda.powertools.validation.Validation; - - -public class ValidationInboundClasspathHandler implements RequestHandler { - - @Override - @Validation(inboundSchema = "classpath:/schema_v7.json") - public String handleRequest(APIGatewayProxyRequestEvent input, Context context) { - return "OK"; - } -} diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/ValidationInboundStringHandler.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/ValidationInboundStringHandler.java index e27f31129..fd5692884 100644 --- a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/ValidationInboundStringHandler.java +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/ValidationInboundStringHandler.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.validation.handlers; import com.amazonaws.services.lambda.runtime.Context; @@ -76,7 +77,7 @@ public class ValidationInboundStringHandler implements RequestHandler provideArguments(ExtensionContext context) { + + String body = "{id"; + + final APIGatewayProxyResponseEvent apiGWProxyResponseEvent = new APIGatewayProxyResponseEvent().withBody(body); + + APIGatewayV2HTTPResponse apiGWV2HTTPResponse = new APIGatewayV2HTTPResponse(); + apiGWV2HTTPResponse.setBody(body); + + APIGatewayV2WebSocketResponse apiGWV2WebSocketResponse = new APIGatewayV2WebSocketResponse(); + apiGWV2WebSocketResponse.setBody(body); + + ApplicationLoadBalancerResponseEvent albResponseEvent = new ApplicationLoadBalancerResponseEvent(); + albResponseEvent.setBody(body); + + KinesisAnalyticsInputPreprocessingResponse kaipResponse = new KinesisAnalyticsInputPreprocessingResponse(); + List records = new ArrayList(); + ByteBuffer buffer = ByteBuffer.wrap(body.getBytes(StandardCharsets.UTF_8)); + records.add(new KinesisAnalyticsInputPreprocessingResponse.Record("1", + KinesisAnalyticsInputPreprocessingResponse.Result.Ok, buffer)); + kaipResponse.setRecords(records); + + return Stream.of(apiGWProxyResponseEvent, apiGWV2HTTPResponse, apiGWV2WebSocketResponse, albResponseEvent, + kaipResponse).map(Arguments::of); + } +} diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java index 2c66885d6..9ea596ff3 100644 --- a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.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,42 +11,133 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.validation.internal; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.mockito.Mockito.when; + 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.APIGatewayV2HTTPEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPResponse; +import com.amazonaws.services.lambda.runtime.events.ActiveMQEvent; +import com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerRequestEvent; +import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent; +import com.amazonaws.services.lambda.runtime.events.CloudWatchLogsEvent; +import com.amazonaws.services.lambda.runtime.events.KafkaEvent; +import com.amazonaws.services.lambda.runtime.events.KinesisAnalyticsFirehoseInputPreprocessingEvent; +import com.amazonaws.services.lambda.runtime.events.KinesisAnalyticsStreamsInputPreprocessingEvent; import com.amazonaws.services.lambda.runtime.events.KinesisEvent; +import com.amazonaws.services.lambda.runtime.events.KinesisFirehoseEvent; +import com.amazonaws.services.lambda.runtime.events.RabbitMQEvent; +import com.amazonaws.services.lambda.runtime.events.SNSEvent; import com.amazonaws.services.lambda.runtime.events.SQSEvent; +import com.amazonaws.services.lambda.runtime.events.ScheduledEvent; import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer; import com.amazonaws.services.lambda.runtime.serialization.events.LambdaEventSerializers; +import com.networknt.schema.SpecVersion; +import java.io.IOException; +import java.util.stream.Stream; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.Signature; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import software.amazon.lambda.powertools.validation.Validation; import software.amazon.lambda.powertools.validation.ValidationConfig; import software.amazon.lambda.powertools.validation.ValidationException; -import software.amazon.lambda.powertools.validation.handlers.*; +import software.amazon.lambda.powertools.validation.handlers.GenericSchemaV7Handler; +import software.amazon.lambda.powertools.validation.handlers.SQSWithCustomEnvelopeHandler; +import software.amazon.lambda.powertools.validation.handlers.SQSWithWrongEnvelopeHandler; +import software.amazon.lambda.powertools.validation.handlers.ValidationInboundStringHandler; import software.amazon.lambda.powertools.validation.model.MyCustomEvent; -import java.io.IOException; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; public class ValidationAspectTest { + @Mock + Validation validation; + @Mock + Signature signature; @Mock private Context context; + @Mock + private ProceedingJoinPoint pjp; + private ValidationAspect validationAspect = new ValidationAspect(); + + private static Stream provideEventAndEventType() { + return Stream.of( + Arguments.of("/sns_event.json", SNSEvent.class), + Arguments.of("/scheduled_event.json", ScheduledEvent.class), + Arguments.of("/alb_event.json", ApplicationLoadBalancerRequestEvent.class), + Arguments.of("/cwl_event.json", CloudWatchLogsEvent.class), + Arguments.of("/cfcr_event.json", CloudFormationCustomResourceEvent.class), + Arguments.of("/kf_event.json", KinesisFirehoseEvent.class), + Arguments.of("/kafka_event.json", KafkaEvent.class), + Arguments.of("/amq_event.json", ActiveMQEvent.class), + Arguments.of("/rabbitmq_event.json", RabbitMQEvent.class), + Arguments.of("/kafip_event.json", KinesisAnalyticsFirehoseInputPreprocessingEvent.class), + Arguments.of("/kasip_event.json", KinesisAnalyticsStreamsInputPreprocessingEvent.class), + Arguments.of("/custom_event.json", MyCustomEvent.class) + + ); + } @BeforeEach void setUp() { MockitoAnnotations.openMocks(this); } + @ParameterizedTest + @ArgumentsSource(ResponseEventsArgumentsProvider.class) + public void testValidateOutboundJsonSchema(Object object) throws Throwable { + when(validation.schemaVersion()).thenReturn(SpecVersion.VersionFlag.V7); + when(pjp.getSignature()).thenReturn(signature); + when(pjp.getSignature().getDeclaringType()).thenReturn(RequestHandler.class); + Object[] args = {new Object(), context}; + when(pjp.getArgs()).thenReturn(args); + when(pjp.proceed(args)).thenReturn(object); + when(validation.inboundSchema()).thenReturn(""); + when(validation.outboundSchema()).thenReturn("classpath:/schema_v7.json"); + + assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> + { + validationAspect.around(pjp, validation); + }); + } + + @Test + public void testValidateOutboundJsonSchema_APIGWV2() throws Throwable { + when(validation.schemaVersion()).thenReturn(SpecVersion.VersionFlag.V7); + when(pjp.getSignature()).thenReturn(signature); + when(pjp.getSignature().getDeclaringType()).thenReturn(RequestHandler.class); + Object[] args = {new Object(), context}; + when(pjp.getArgs()).thenReturn(args); + APIGatewayV2HTTPResponse apiGatewayV2HTTPResponse = new APIGatewayV2HTTPResponse(); + apiGatewayV2HTTPResponse.setBody("{" + + " \"id\": 1," + + " \"name\": \"Lampshade\"," + + " \"price\": 42" + + "}"); + when(pjp.proceed(args)).thenReturn(apiGatewayV2HTTPResponse); + when(validation.inboundSchema()).thenReturn(""); + when(validation.outboundSchema()).thenReturn("classpath:/schema_v7.json"); + + assertThatNoException().isThrownBy(() -> validationAspect.around(pjp, validation)); + } + @Test public void validate_inputOK_schemaInClasspath_shouldValidate() { - ValidationInboundClasspathHandler handler = new ValidationInboundClasspathHandler(); + GenericSchemaV7Handler handler = new GenericSchemaV7Handler(); APIGatewayProxyRequestEvent event = new APIGatewayProxyRequestEvent(); event.setBody("{" + " \"id\": 1," + @@ -58,7 +149,7 @@ public void validate_inputOK_schemaInClasspath_shouldValidate() { @Test public void validate_inputKO_schemaInClasspath_shouldThrowValidationException() { - ValidationInboundClasspathHandler handler = new ValidationInboundClasspathHandler(); + GenericSchemaV7Handler handler = new GenericSchemaV7Handler(); APIGatewayProxyRequestEvent event = new APIGatewayProxyRequestEvent(); event.setBody("{" + " \"id\": 1," + @@ -94,16 +185,18 @@ public void validate_inputKO_schemaInString_shouldThrowValidationException() { @Test public void validate_SQS() { - PojoSerializer pojoSerializer = LambdaEventSerializers.serializerFor(SQSEvent.class, ClassLoader.getSystemClassLoader()); + PojoSerializer pojoSerializer = + LambdaEventSerializers.serializerFor(SQSEvent.class, ClassLoader.getSystemClassLoader()); SQSEvent event = pojoSerializer.fromJson(this.getClass().getResourceAsStream("/sqs.json")); - SQSHandler handler = new SQSHandler(); + GenericSchemaV7Handler handler = new GenericSchemaV7Handler(); assertThat(handler.handleRequest(event, context)).isEqualTo("OK"); } @Test public void validate_SQS_CustomEnvelopeTakePrecedence() { - PojoSerializer pojoSerializer = LambdaEventSerializers.serializerFor(SQSEvent.class, ClassLoader.getSystemClassLoader()); + PojoSerializer pojoSerializer = + LambdaEventSerializers.serializerFor(SQSEvent.class, ClassLoader.getSystemClassLoader()); SQSEvent event = pojoSerializer.fromJson(this.getClass().getResourceAsStream("/sqs_message.json")); SQSWithCustomEnvelopeHandler handler = new SQSWithCustomEnvelopeHandler(); @@ -112,7 +205,8 @@ public void validate_SQS_CustomEnvelopeTakePrecedence() { @Test public void validate_SQS_WrongEnvelope_shouldThrowValidationException() { - PojoSerializer pojoSerializer = LambdaEventSerializers.serializerFor(SQSEvent.class, ClassLoader.getSystemClassLoader()); + PojoSerializer pojoSerializer = + LambdaEventSerializers.serializerFor(SQSEvent.class, ClassLoader.getSystemClassLoader()); SQSEvent event = pojoSerializer.fromJson(this.getClass().getResourceAsStream("/sqs_message.json")); SQSWithWrongEnvelopeHandler handler = new SQSWithWrongEnvelopeHandler(); @@ -121,18 +215,21 @@ public void validate_SQS_WrongEnvelope_shouldThrowValidationException() { @Test public void validate_Kinesis() { - PojoSerializer pojoSerializer = LambdaEventSerializers.serializerFor(KinesisEvent.class, ClassLoader.getSystemClassLoader()); + PojoSerializer pojoSerializer = + LambdaEventSerializers.serializerFor(KinesisEvent.class, ClassLoader.getSystemClassLoader()); KinesisEvent event = pojoSerializer.fromJson(this.getClass().getResourceAsStream("/kinesis.json")); - KinesisHandler handler = new KinesisHandler(); + GenericSchemaV7Handler handler = new GenericSchemaV7Handler(); assertThat(handler.handleRequest(event, context)).isEqualTo("OK"); } - @Test - public void validate_CustomObject() throws IOException { - MyCustomEvent event = ValidationConfig.get().getObjectMapper().readValue(this.getClass().getResourceAsStream("/custom_event.json"), MyCustomEvent.class); + @ParameterizedTest + @MethodSource("provideEventAndEventType") + public void validateEEvent(String jsonResource, Class eventClass) throws IOException { + Object event = ValidationConfig.get().getObjectMapper() + .readValue(this.getClass().getResourceAsStream(jsonResource), eventClass); - MyCustomEventHandler handler = new MyCustomEventHandler(); + GenericSchemaV7Handler handler = new GenericSchemaV7Handler(); assertThat(handler.handleRequest(event, context)).isEqualTo("OK"); } } diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/model/Basket.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/model/Basket.java index 548ef4660..881090bdc 100644 --- a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/model/Basket.java +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/model/Basket.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.validation.model; import java.util.ArrayList; @@ -21,6 +22,9 @@ public class Basket { private String hiddenProduct; + public Basket() { + } + public List getProducts() { return products; } @@ -29,9 +33,6 @@ public void setProducts(List products) { this.products = products; } - public Basket() { - } - public void add(Product product) { products.add(product); } diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/model/MyCustomEvent.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/model/MyCustomEvent.java index 12f3f99ca..04c7c3a4a 100644 --- a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/model/MyCustomEvent.java +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/model/MyCustomEvent.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.validation.model; public class MyCustomEvent { diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/model/Product.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/model/Product.java index fde888b76..93f5ab39f 100644 --- a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/model/Product.java +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/model/Product.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.validation.model; public class Product { diff --git a/powertools-validation/src/test/resources/alb_event.json b/powertools-validation/src/test/resources/alb_event.json new file mode 100644 index 000000000..d2b0d3cda --- /dev/null +++ b/powertools-validation/src/test/resources/alb_event.json @@ -0,0 +1,28 @@ +{ + "requestContext": { + "elb": { + "targetGroupArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/lambda-279XGJDqGZ5rsrHC2Fjr/49e9d65c45c6791a" + } + }, + "httpMethod": "GET", + "path": "/lambda", + "queryStringParameters": { + "query": "1234ABCD" + }, + "headers": { + "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", + "accept-encoding": "gzip", + "accept-language": "en-US,en;q=0.9", + "connection": "keep-alive", + "host": "lambda-alb-123578498.us-east-1.elb.amazonaws.com", + "upgrade-insecure-requests": "1", + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36", + "x-amzn-trace-id": "Root=1-5c536348-3d683b8b04734faae651f476", + "x-forwarded-for": "72.12.164.125", + "x-forwarded-port": "80", + "x-forwarded-proto": "http", + "x-imforwards": "20" + }, + "body": "{\"id\":1234, \"name\":\"product\", \"price\":42}", + "isBase64Encoded": false +} \ No newline at end of file diff --git a/powertools-validation/src/test/resources/amq_event.json b/powertools-validation/src/test/resources/amq_event.json new file mode 100644 index 000000000..2f29bdc9f --- /dev/null +++ b/powertools-validation/src/test/resources/amq_event.json @@ -0,0 +1,28 @@ +{ + "eventSource": "aws:mq", + "eventSourceArn": "arn:aws:mq:us-west-2:111122223333:broker:test:b-9bcfa592-423a-4942-879d-eb284b418fc8", + "messages": [ + { + "messageID": "ID:b-9bcfa592-423a-4942-879d-eb284b418fc8-1.mq.us-west-2.amazonaws.com-37557-1234520418293-4:1:1:1:1", + "messageType": "jms/text-message", + "deliveryMode": 1, + "replyTo": null, + "type": null, + "expiration": "60000", + "priority": 1, + "redelivered": false, + "destination": { + "physicalName": "testQueue" + }, + "data": "ewogICJpZCI6IDEyMzQsCiAgIm5hbWUiOiAicHJvZHVjdCIsCiAgInByaWNlIjogNDIKfQ==", + "timestamp": 1598827811958, + "brokerInTime": 1598827811958, + "brokerOutTime": 1598827811959, + "properties": { + "index": "1", + "doAlarm": "false", + "myCustomProperty": "value" + } + } + ] +} \ No newline at end of file diff --git a/powertools-validation/src/test/resources/cfcr_event.json b/powertools-validation/src/test/resources/cfcr_event.json new file mode 100644 index 000000000..98b4a9eba --- /dev/null +++ b/powertools-validation/src/test/resources/cfcr_event.json @@ -0,0 +1,20 @@ +{ + "requestType": "requestType", + "serviceToken": "serviceToken", + "responseUrl": "responseUrl", + "stackId": "stackId", + "requestId": "requestId", + "logicalResourceId": "logicalResourceId", + "resourceType": "resourceType", + "resourceProperties": { + "id": 1234, + "name": "product", + "price": 42 + }, + "oldResourceProperties": { + "id": 1234, + "name": "product", + "price": 40 + } +} + diff --git a/powertools-validation/src/test/resources/custom_event.json b/powertools-validation/src/test/resources/custom_event.json index 13103c434..918cad81f 100644 --- a/powertools-validation/src/test/resources/custom_event.json +++ b/powertools-validation/src/test/resources/custom_event.json @@ -1,6 +1,6 @@ { "basket": { - "products" : [ + "products": [ { "id": 43242, "name": "FooBar XY", diff --git a/powertools-validation/src/test/resources/custom_event_gzip.json b/powertools-validation/src/test/resources/custom_event_gzip.json index d212052d0..165db7071 100644 --- a/powertools-validation/src/test/resources/custom_event_gzip.json +++ b/powertools-validation/src/test/resources/custom_event_gzip.json @@ -1,6 +1,6 @@ { "basket": { - "products" : [ + "products": [ { "id": 43242, "name": "FooBar XY", diff --git a/powertools-validation/src/test/resources/cwl_event.json b/powertools-validation/src/test/resources/cwl_event.json new file mode 100644 index 000000000..5dd0f6657 --- /dev/null +++ b/powertools-validation/src/test/resources/cwl_event.json @@ -0,0 +1,5 @@ +{ + "awsLogs": { + "data": "ewogICJpZCI6IDEyMzQsCiAgIm5hbWUiOiAicHJvZHVjdCIsCiAgInByaWNlIjogNDIKfQ==" + } +} \ No newline at end of file diff --git a/powertools-validation/src/test/resources/kafip_event.json b/powertools-validation/src/test/resources/kafip_event.json new file mode 100644 index 000000000..01196256c --- /dev/null +++ b/powertools-validation/src/test/resources/kafip_event.json @@ -0,0 +1,14 @@ +{ + "invocationId": "arn:aws:iam::EXAMPLE", + "applicationArn": "arn:aws:kinesis:EXAMPLE", + "streamArn": "arn:aws:kinesis:EXAMPLE", + "records": [ + { + "kinesisFirehoseRecordMetadata": { + "approximateArrivalTimestamp": 1428537600 + }, + "recordId": "record-id", + "data": "eyJpZCI6MTIzNCwgIm5hbWUiOiJwcm9kdWN0IiwgInByaWNlIjo0Mn0=" + } + ] +} \ No newline at end of file diff --git a/powertools-validation/src/test/resources/kafka_event.json b/powertools-validation/src/test/resources/kafka_event.json new file mode 100644 index 000000000..cf1bad615 --- /dev/null +++ b/powertools-validation/src/test/resources/kafka_event.json @@ -0,0 +1,27 @@ +{ + "eventSource": "aws:kafka", + "eventSourceArn": "arn:aws:kafka:us-east-1:123456789012:cluster/vpc-3432434/4834-3547-3455-9872-7929", + "bootstrapServers": "b-2.demo-cluster-1.a1bcde.c1.kafka.us-east-1.amazonaws.com:9092,b-1.demo-cluster-1.a1bcde.c1.kafka.us-east-1.amazonaws.com:9092", + "records": { + "mytopic-01": [ + { + "topic": "mytopic1", + "partition": 0, + "offset": 15, + "timestamp": 1596480920837, + "timestampType": "CREATE_TIME", + "value": "eyJpZCI6MTIzNCwgIm5hbWUiOiJwcm9kdWN0IiwgInByaWNlIjo0Mn0=" + } + ], + "mytopic-02": [ + { + "topic": "mytopic2", + "partition": 0, + "offset": 15, + "timestamp": 1596480920838, + "timestampType": "CREATE_TIME", + "value": "eyJpZCI6MTIzNDUsICJuYW1lIjoicHJvZHVjdDUiLCAicHJpY2UiOjQ1fQ==" + } + ] + } +} diff --git a/powertools-validation/src/test/resources/kasip_event.json b/powertools-validation/src/test/resources/kasip_event.json new file mode 100644 index 000000000..78bc9a3fb --- /dev/null +++ b/powertools-validation/src/test/resources/kasip_event.json @@ -0,0 +1,17 @@ +{ + "invocationId": "arn:aws:iam::EXAMPLE", + "applicationArn": "arn:aws:kinesis:EXAMPLE", + "streamArn": "arn:aws:kinesis:EXAMPLE", + "records": [ + { + "kinesisStreamRecordMetadata": { + "sequenceNumber": "49545115243490985018280067714973144582180062593244200961", + "partitionKey": "partitionKey-03", + "shardId": "12", + "approximateArrivalTimestamp": 1428537600 + }, + "recordId": "record-id", + "data": "eyJpZCI6MTIzNCwgIm5hbWUiOiJwcm9kdWN0IiwgInByaWNlIjo0Mn0=" + } + ] +} \ No newline at end of file diff --git a/powertools-validation/src/test/resources/kf_event.json b/powertools-validation/src/test/resources/kf_event.json new file mode 100644 index 000000000..e36bc4c3f --- /dev/null +++ b/powertools-validation/src/test/resources/kf_event.json @@ -0,0 +1,12 @@ +{ + "invocationId": "invocationIdExample", + "deliveryStreamArn": "arn:aws:kinesis:EXAMPLE", + "region": "us-east-1", + "records": [ + { + "recordId": "49546986683135544286507457936321625675700192471156785154", + "approximateArrivalTimestamp": 1495072949453, + "data": "ewogICJpZCI6IDEyMzQsCiAgIm5hbWUiOiAicHJvZHVjdCIsCiAgInByaWNlIjogNDIKfQ==" + } + ] +} \ No newline at end of file diff --git a/powertools-validation/src/test/resources/rabbitmq_event.json b/powertools-validation/src/test/resources/rabbitmq_event.json new file mode 100644 index 000000000..698e37143 --- /dev/null +++ b/powertools-validation/src/test/resources/rabbitmq_event.json @@ -0,0 +1,51 @@ +{ + "eventSource": "aws:rmq", + "eventSourceArn": "arn:aws:mq:us-west-2:111122223333:broker:pizzaBroker:b-9bcfa592-423a-4942-879d-eb284b418fc8", + "rmqMessagesByQueue": { + "pizzaQueue::/": [ + { + "basicProperties": { + "contentType": "text/plain", + "contentEncoding": null, + "headers": { + "header1": { + "bytes": [ + 118, + 97, + 108, + 117, + 101, + 49 + ] + }, + "header2": { + "bytes": [ + 118, + 97, + 108, + 117, + 101, + 50 + ] + }, + "numberInHeader": 10 + }, + "deliveryMode": 1, + "priority": 34, + "correlationId": null, + "replyTo": null, + "expiration": "60000", + "messageId": null, + "timestamp": "Jan 1, 1970, 12:33:41 AM", + "type": null, + "userId": "AIDACKCEVSQ6C2EXAMPLE", + "appId": null, + "clusterId": null, + "bodySize": 80 + }, + "redelivered": false, + "data": "ewogICJpZCI6IDEyMzQsCiAgIm5hbWUiOiAicHJvZHVjdCIsCiAgInByaWNlIjogNDIKfQ==" + } + ] + } +} diff --git a/powertools-validation/src/test/resources/scheduled_event.json b/powertools-validation/src/test/resources/scheduled_event.json new file mode 100644 index 000000000..12d18ba5d --- /dev/null +++ b/powertools-validation/src/test/resources/scheduled_event.json @@ -0,0 +1,14 @@ +{ + "id": "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c", + "source": "aws.events", + "account": "123456789012", + "region": "eu-central-1", + "resources": [ + "arn:aws:events:eu-central-1:123456789012:rule/my-schedule" + ], + "detail": { + "id": 1234, + "name": "product", + "price": 42 + } +} \ No newline at end of file diff --git a/powertools-validation/src/test/resources/schema_v4.json b/powertools-validation/src/test/resources/schema_v4.json index ae277d476..3cd310a53 100644 --- a/powertools-validation/src/test/resources/schema_v4.json +++ b/powertools-validation/src/test/resources/schema_v4.json @@ -18,5 +18,9 @@ "exclusiveMinimum": true } }, - "required": ["id", "name", "price"] + "required": [ + "id", + "name", + "price" + ] } \ No newline at end of file diff --git a/powertools-validation/src/test/resources/sns_event.json b/powertools-validation/src/test/resources/sns_event.json new file mode 100644 index 000000000..81f6a4795 --- /dev/null +++ b/powertools-validation/src/test/resources/sns_event.json @@ -0,0 +1,26 @@ +{ + "records": [ + { + "eventSource": "aws:sns", + "eventVersion": "1.0", + "eventSubscriptionArn": "arn:aws:sns:eu-central-1:123456789012:TopicSendToMe:e3ddc7d5-2f86-40b8-a13d-3362f94fd8dd", + "sns": { + "type": "Notification", + "messageId": "dc918f50-80c6-56a2-ba33-d8a9bbf013ab", + "topicArn": "arn:aws:sns:eu-central-1:123456789012:TopicSendToMe", + "subject": "Test sns message", + "message": "{\"id\":1234, \"name\":\"product\", \"price\":42}", + "signatureVersion": "1", + "signature": "UWnPpkqPAphyr+6PXzUF9++4zJcw==", + "signingCertUrl": "https://sns.eu-central-1.amazonaws.com/SimpleNotificationService-a86cb10b4e1f29c941702d737128f7b6.pem", + "unsubscribeUrl": "https://sns.eu-central-1.amazonaws.com/?Action=Unsubscribe", + "messageAttributes": { + "name": { + "type": "String", + "value": "Bob" + } + } + } + } + ] +} \ No newline at end of file diff --git a/powertools-validation/src/test/resources/sqs.json b/powertools-validation/src/test/resources/sqs.json index 129e79bb2..10a2bbbbf 100644 --- a/powertools-validation/src/test/resources/sqs.json +++ b/powertools-validation/src/test/resources/sqs.json @@ -11,7 +11,6 @@ "ApproximateFirstReceiveTimestamp": "1601975709499" }, "messageAttributes": { - }, "md5OfBody": "0f96e88a291edb4429f2f7b9fdc3df96", "eventSource": "aws:sqs", diff --git a/powertools-validation/src/test/resources/sqs_message.json b/powertools-validation/src/test/resources/sqs_message.json index 068279b74..878b402df 100644 --- a/powertools-validation/src/test/resources/sqs_message.json +++ b/powertools-validation/src/test/resources/sqs_message.json @@ -11,7 +11,6 @@ "ApproximateFirstReceiveTimestamp": "1601975709499" }, "messageAttributes": { - }, "md5OfBody": "0f96e88a291edb4429f2f7b9fdc3df96", "eventSource": "aws:sqs", diff --git a/spotbugs-exclude.xml b/spotbugs-exclude.xml index 5a5e3bed8..eca7e266f 100644 --- a/spotbugs-exclude.xml +++ b/spotbugs-exclude.xml @@ -36,6 +36,10 @@ + + + + @@ -93,6 +97,10 @@ + + + +