From 7fc50ae6f185b00672dc46adc772738e9962219d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 30 Jun 2023 16:09:26 +0200 Subject: [PATCH 01/55] build(deps-dev): bump constructs from 10.2.62 to 10.2.64 (#1257) Bumps [constructs](https://github.com/aws/constructs) from 10.2.62 to 10.2.64. --- powertools-e2e-tests/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/powertools-e2e-tests/pom.xml b/powertools-e2e-tests/pom.xml index b5b875cec..27d889981 100644 --- a/powertools-e2e-tests/pom.xml +++ b/powertools-e2e-tests/pom.xml @@ -16,7 +16,7 @@ 1.8 1.8 - 10.2.62 + 10.2.64 2.85.0 From 716a798a5207694ca934857b82279e7e94cc9b2c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 30 Jun 2023 16:15:42 +0200 Subject: [PATCH 02/55] build(deps-dev): bump aws-cdk-lib from 2.85.0 to 2.86.0 (#1256) Bumps [aws-cdk-lib](https://github.com/aws/aws-cdk) from 2.85.0 to 2.86.0. - [Release notes](https://github.com/aws/aws-cdk/releases) - [Changelog](https://github.com/aws/aws-cdk/blob/main/CHANGELOG.v2.md) - [Commits](https://github.com/aws/aws-cdk/compare/v2.85.0...v2.86.0) --- updated-dependencies: - dependency-name: software.amazon.awscdk:aws-cdk-lib dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- powertools-e2e-tests/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/powertools-e2e-tests/pom.xml b/powertools-e2e-tests/pom.xml index 27d889981..1e3e6a167 100644 --- a/powertools-e2e-tests/pom.xml +++ b/powertools-e2e-tests/pom.xml @@ -17,7 +17,7 @@ 1.8 1.8 10.2.64 - 2.85.0 + 2.86.0 true From d8d13c006ac89740088078e98b14538d318471a6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Jul 2023 13:30:59 +0200 Subject: [PATCH 03/55] build(deps): bump aws.sdk.version from 2.20.96 to 2.20.97 (#1260) Bumps `aws.sdk.version` from 2.20.96 to 2.20.97. Updates `software.amazon.awssdk:bom` from 2.20.96 to 2.20.97 Updates `http-client-spi` from 2.20.96 to 2.20.97 Updates `url-connection-client` from 2.20.96 to 2.20.97 Updates `s3` from 2.20.96 to 2.20.97 Updates `lambda` from 2.20.96 to 2.20.97 Updates `cloudwatch` from 2.20.96 to 2.20.97 Updates `xray` from 2.20.96 to 2.20.97 Updates `cloudformation` from 2.20.96 to 2.20.97 Updates `sts` from 2.20.96 to 2.20.97 --- updated-dependencies: - dependency-name: software.amazon.awssdk:bom dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:http-client-spi dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:url-connection-client dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:s3 dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:lambda dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:cloudwatch dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:xray dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:cloudformation dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:sts dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/powertools-examples-sqs/pom.xml | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/powertools-examples-sqs/pom.xml b/examples/powertools-examples-sqs/pom.xml index d989fba75..1ee276d18 100644 --- a/examples/powertools-examples-sqs/pom.xml +++ b/examples/powertools-examples-sqs/pom.xml @@ -28,7 +28,7 @@ software.amazon.awssdk url-connection-client - 2.20.96 + 2.20.97 com.amazonaws diff --git a/pom.xml b/pom.xml index af11f402b..28118a33f 100644 --- a/pom.xml +++ b/pom.xml @@ -60,7 +60,7 @@ 2.20.0 2.15.2 1.9.7 - 2.20.96 + 2.20.97 2.14.0 2.1.3 UTF-8 From 3e97bc803195a518746731fe8c80d2b0a2ea507a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Jul 2023 13:35:59 +0200 Subject: [PATCH 04/55] build(deps-dev): bump constructs from 10.2.64 to 10.2.67 (#1261) Bumps [constructs](https://github.com/aws/constructs) from 10.2.64 to 10.2.67. - [Release notes](https://github.com/aws/constructs/releases) - [Commits](https://github.com/aws/constructs/compare/v10.2.64...v10.2.67) --- updated-dependencies: - dependency-name: software.constructs:constructs dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- powertools-e2e-tests/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/powertools-e2e-tests/pom.xml b/powertools-e2e-tests/pom.xml index 1e3e6a167..7ebeec5e0 100644 --- a/powertools-e2e-tests/pom.xml +++ b/powertools-e2e-tests/pom.xml @@ -16,7 +16,7 @@ 1.8 1.8 - 10.2.64 + 10.2.67 2.86.0 From f0dc2123e56b9af37544fbb6e8f26433d1b68d35 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Jul 2023 13:48:26 +0200 Subject: [PATCH 05/55] build(deps): bump aws.sdk.version from 2.20.97 to 2.20.98 (#1262) Bumps `aws.sdk.version` from 2.20.97 to 2.20.98. Updates `software.amazon.awssdk:bom` from 2.20.97 to 2.20.98 Updates `http-client-spi` from 2.20.97 to 2.20.98 Updates `url-connection-client` from 2.20.97 to 2.20.98 Updates `s3` from 2.20.97 to 2.20.98 Updates `lambda` from 2.20.97 to 2.20.98 Updates `cloudwatch` from 2.20.97 to 2.20.98 Updates `xray` from 2.20.97 to 2.20.98 Updates `cloudformation` from 2.20.97 to 2.20.98 Updates `sts` from 2.20.97 to 2.20.98 --- updated-dependencies: - dependency-name: software.amazon.awssdk:bom dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:http-client-spi dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:url-connection-client dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:s3 dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:lambda dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:cloudwatch dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:xray dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:cloudformation dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:sts dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/powertools-examples-sqs/pom.xml | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/powertools-examples-sqs/pom.xml b/examples/powertools-examples-sqs/pom.xml index 1ee276d18..3b84b42b8 100644 --- a/examples/powertools-examples-sqs/pom.xml +++ b/examples/powertools-examples-sqs/pom.xml @@ -28,7 +28,7 @@ software.amazon.awssdk url-connection-client - 2.20.97 + 2.20.98 com.amazonaws diff --git a/pom.xml b/pom.xml index 28118a33f..7c165e35c 100644 --- a/pom.xml +++ b/pom.xml @@ -60,7 +60,7 @@ 2.20.0 2.15.2 1.9.7 - 2.20.97 + 2.20.98 2.14.0 2.1.3 UTF-8 From 49cb12f5e551f736b50a0efa34e222af072179c9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Jul 2023 13:53:34 +0200 Subject: [PATCH 06/55] build(deps-dev): bump constructs from 10.2.67 to 10.2.68 (#1263) Bumps [constructs](https://github.com/aws/constructs) from 10.2.67 to 10.2.68. - [Release notes](https://github.com/aws/constructs/releases) - [Commits](https://github.com/aws/constructs/compare/v10.2.67...v10.2.68) --- updated-dependencies: - dependency-name: software.constructs:constructs dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- powertools-e2e-tests/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/powertools-e2e-tests/pom.xml b/powertools-e2e-tests/pom.xml index 7ebeec5e0..0d7a7ccda 100644 --- a/powertools-e2e-tests/pom.xml +++ b/powertools-e2e-tests/pom.xml @@ -16,7 +16,7 @@ 1.8 1.8 - 10.2.67 + 10.2.68 2.86.0 From d257989870692f6d6342b8d7edf0bd2b50aa95a6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Jul 2023 13:50:54 +0200 Subject: [PATCH 07/55] build(deps-dev): bump constructs from 10.2.68 to 10.2.69 (#1265) Bumps [constructs](https://github.com/aws/constructs) from 10.2.68 to 10.2.69. - [Release notes](https://github.com/aws/constructs/releases) - [Commits](https://github.com/aws/constructs/compare/v10.2.68...v10.2.69) --- updated-dependencies: - dependency-name: software.constructs:constructs dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- powertools-e2e-tests/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/powertools-e2e-tests/pom.xml b/powertools-e2e-tests/pom.xml index 0d7a7ccda..bf98b0691 100644 --- a/powertools-e2e-tests/pom.xml +++ b/powertools-e2e-tests/pom.xml @@ -16,7 +16,7 @@ 1.8 1.8 - 10.2.68 + 10.2.69 2.86.0 From a6ec45870d7af17f1a8b81963623b776c222d3a8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Jul 2023 14:00:09 +0200 Subject: [PATCH 08/55] build(deps): bump aws.sdk.version from 2.20.98 to 2.20.99 (#1266) Bumps `aws.sdk.version` from 2.20.98 to 2.20.99. Updates `software.amazon.awssdk:bom` from 2.20.98 to 2.20.99 Updates `http-client-spi` from 2.20.98 to 2.20.99 Updates `url-connection-client` from 2.20.98 to 2.20.99 Updates `s3` from 2.20.98 to 2.20.99 Updates `lambda` from 2.20.98 to 2.20.99 Updates `cloudwatch` from 2.20.98 to 2.20.99 Updates `xray` from 2.20.98 to 2.20.99 Updates `cloudformation` from 2.20.98 to 2.20.99 Updates `sts` from 2.20.98 to 2.20.99 --- updated-dependencies: - dependency-name: software.amazon.awssdk:bom dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:http-client-spi dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:url-connection-client dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:s3 dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:lambda dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:cloudwatch dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:xray dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:cloudformation dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:sts dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/powertools-examples-sqs/pom.xml | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/powertools-examples-sqs/pom.xml b/examples/powertools-examples-sqs/pom.xml index 3b84b42b8..5ecc17d47 100644 --- a/examples/powertools-examples-sqs/pom.xml +++ b/examples/powertools-examples-sqs/pom.xml @@ -28,7 +28,7 @@ software.amazon.awssdk url-connection-client - 2.20.98 + 2.20.99 com.amazonaws diff --git a/pom.xml b/pom.xml index 7c165e35c..980d10aa7 100644 --- a/pom.xml +++ b/pom.xml @@ -60,7 +60,7 @@ 2.20.0 2.15.2 1.9.7 - 2.20.98 + 2.20.99 2.14.0 2.1.3 UTF-8 From f13dbc5666c6c1c228e4a11fd02723c081df6b73 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Jul 2023 14:05:10 +0200 Subject: [PATCH 09/55] build(deps): bump json-schema-validator from 1.0.85 to 1.0.86 (#1267) Bumps [json-schema-validator](https://github.com/networknt/json-schema-validator) from 1.0.85 to 1.0.86. - [Release notes](https://github.com/networknt/json-schema-validator/releases) - [Changelog](https://github.com/networknt/json-schema-validator/blob/master/CHANGELOG.md) - [Commits](https://github.com/networknt/json-schema-validator/compare/1.0.85...1.0.86) --- updated-dependencies: - dependency-name: com.networknt:json-schema-validator dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- powertools-validation/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/powertools-validation/pom.xml b/powertools-validation/pom.xml index 7ab43831e..76c434614 100644 --- a/powertools-validation/pom.xml +++ b/powertools-validation/pom.xml @@ -73,7 +73,7 @@ com.networknt json-schema-validator - 1.0.85 + 1.0.86 com.amazonaws From 66cac4dbd6db6e968a2deb3de30e8d90ed05e024 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Jul 2023 14:06:06 +0200 Subject: [PATCH 10/55] build(deps-dev): bump aws-cdk-lib from 2.86.0 to 2.87.0 (#1270) Bumps [aws-cdk-lib](https://github.com/aws/aws-cdk) from 2.86.0 to 2.87.0. - [Release notes](https://github.com/aws/aws-cdk/releases) - [Changelog](https://github.com/aws/aws-cdk/blob/main/CHANGELOG.v2.md) - [Commits](https://github.com/aws/aws-cdk/compare/v2.86.0...v2.87.0) --- updated-dependencies: - dependency-name: software.amazon.awscdk:aws-cdk-lib dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- powertools-e2e-tests/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/powertools-e2e-tests/pom.xml b/powertools-e2e-tests/pom.xml index bf98b0691..8754117db 100644 --- a/powertools-e2e-tests/pom.xml +++ b/powertools-e2e-tests/pom.xml @@ -17,7 +17,7 @@ 1.8 1.8 10.2.69 - 2.86.0 + 2.87.0 true From 2c282a9353a199510fec903fd8d7667a0f4820d1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Jul 2023 13:33:01 +0200 Subject: [PATCH 11/55] build(deps): bump aws.sdk.version from 2.20.99 to 2.20.101 (#1275) Bumps `aws.sdk.version` from 2.20.99 to 2.20.101. Updates `software.amazon.awssdk:bom` from 2.20.99 to 2.20.101 Updates `http-client-spi` from 2.20.99 to 2.20.101 Updates `url-connection-client` from 2.20.99 to 2.20.101 Updates `s3` from 2.20.99 to 2.20.101 Updates `lambda` from 2.20.99 to 2.20.101 Updates `cloudwatch` from 2.20.99 to 2.20.101 Updates `xray` from 2.20.99 to 2.20.101 Updates `cloudformation` from 2.20.99 to 2.20.101 Updates `sts` from 2.20.99 to 2.20.101 --- updated-dependencies: - dependency-name: software.amazon.awssdk:bom dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:http-client-spi dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:url-connection-client dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:s3 dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:lambda dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:cloudwatch dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:xray dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:cloudformation dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:sts dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/powertools-examples-sqs/pom.xml | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/powertools-examples-sqs/pom.xml b/examples/powertools-examples-sqs/pom.xml index 5ecc17d47..314a8c32b 100644 --- a/examples/powertools-examples-sqs/pom.xml +++ b/examples/powertools-examples-sqs/pom.xml @@ -28,7 +28,7 @@ software.amazon.awssdk url-connection-client - 2.20.99 + 2.20.101 com.amazonaws diff --git a/pom.xml b/pom.xml index 980d10aa7..2e414d94f 100644 --- a/pom.xml +++ b/pom.xml @@ -60,7 +60,7 @@ 2.20.0 2.15.2 1.9.7 - 2.20.99 + 2.20.101 2.14.0 2.1.3 UTF-8 From c7aedc4d2a61013450bf472bb7a8285f8cf80e41 Mon Sep 17 00:00:00 2001 From: Eleni Dimitropoulou <12170229+eldimi@users.noreply.github.com> Date: Mon, 10 Jul 2023 14:57:06 +0300 Subject: [PATCH 12/55] chore(unit-test): Add missing unit tests in modules with low coverage (#1264) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(unit-tests): add unit tests to powertools-core * chore(unit-tests): add unit tests to powertools-parameters * chore(unit-tests): add unit tests to powertools-serialization * chore(unit-tests): add unit tests to powertools-validation * chore(unit-tests): add unit tests to powertools-logging * chore(unit-tests): minor formatting fixes * chore(unit-tests): add missing return statement * chore(unit-test): rename, reformat, fix typos as suggested from code review Co-authored-by: Jérôme Van Der Linden <117538+jeromevdl@users.noreply.github.com> * chore(unit-tests): address review comments * chore(unit-tests): revert refactoring of EventDeserializer * chore(unit-tests): expand and optimize imports * chore(unit-tests): change response of base64gzip when decompressing empty string * chore(unit-tests): add unit test and fix config for wrong arg type in base64gzip function invocation * chore(unit-tests): fix base64gzip function invocation for argument value argument --------- Co-authored-by: Jérôme Van Der Linden <117538+jeromevdl@users.noreply.github.com> --- pom.xml | 1 + powertools-core/pom.xml | 5 + .../core/internal/LambdaConstants.java | 5 + .../core/internal/LambdaHandlerProcessor.java | 39 ++-- .../internal/LambdaHandlerProcessorTest.java | 203 ++++++++++++++++-- .../powertools/logging/LoggingUtils.java | 4 +- .../internal/AbstractJacksonLayoutCopy.java | 25 ++- .../logging/internal/LambdaLoggingAspect.java | 20 +- .../core/layout/LambdaJsonLayoutTest.java | 18 +- .../powertools/logging/LoggingUtilsTest.java | 6 +- .../handlers/PowerToolDisabledForStream.java | 6 +- ...erToolLogEventEnabledWithCustomMapper.java | 4 +- ...ava => PowertoolsLogAlbCorrelationId.java} | 6 +- ...> PowertoolsLogEnabledWithClearState.java} | 7 +- ...PowertoolsLogEventBridgeCorrelationId.java | 37 ++++ .../internal/LambdaLoggingAspectTest.java | 78 ++++--- .../parameters/AppConfigProvider.java | 2 - .../parameters/DynamoDbProvider.java | 6 +- .../powertools/parameters/ParamManager.java | 7 +- .../powertools/parameters/SSMProvider.java | 19 +- .../parameters/SecretsProvider.java | 19 +- .../internal/LambdaParametersAspect.java | 7 +- .../transform/Base64Transformer.java | 4 +- .../parameters/AppConfigProviderTest.java | 107 +++++++-- .../parameters/DynamoDbProviderTest.java | 66 +++++- .../ParamManagerIntegrationTest.java | 10 +- .../parameters/ParamManagerTest.java | 101 +++++++++ .../parameters/SSMProviderTest.java | 31 ++- .../parameters/SecretsProviderTest.java | 29 +++ .../internal/LambdaParametersAspectTest.java | 5 +- .../transform/TransformationManagerTest.java | 20 ++ .../utilities/EventDeserializer.java | 16 +- .../powertools/utilities/JsonConfig.java | 1 + .../jmespath/Base64GZipFunction.java | 26 ++- .../utilities/EventDeserializerTest.java | 121 ++++++++++- .../jmespath/Base64GZipFunctionTest.java | 45 ++++ .../powertools/utilities/model/Order.java | 33 +++ .../src/test/resources/alb_event.json | 28 +++ .../src/test/resources/amq_event.json | 29 +++ .../src/test/resources/apigwv2_event.json | 57 +++++ .../src/test/resources/cfcr_event.json | 20 ++ .../src/test/resources/custom_event.json | 2 +- .../src/test/resources/custom_event_gzip.json | 5 +- .../src/test/resources/custom_event_map.json | 9 + .../src/test/resources/cwl_event.json | 5 + .../src/test/resources/kafip_event.json | 14 ++ .../src/test/resources/kasip_event.json | 17 ++ .../src/test/resources/kf_event.json | 12 ++ .../src/test/resources/rabbitmq_event.json | 51 +++++ .../src/test/resources/scheduled_event.json | 12 ++ .../sqs/SqsUtilsBatchProcessorTest.java | 1 - powertools-validation/pom.xml | 9 +- .../validation/ValidationConfig.java | 22 +- .../validation/ValidationUtils.java | 30 ++- .../validation/internal/ValidationAspect.java | 21 +- .../validation/ValidationUtilsTest.java | 69 ++++-- ...ndler.java => GenericSchemaV7Handler.java} | 7 +- .../validation/handlers/KinesisHandler.java | 28 --- .../ValidationInboundClasspathHandler.java | 29 --- .../ValidationInboundStringHandler.java | 2 +- .../ResponseEventsArgumentsProvider.java | 57 +++++ .../internal/ValidationAspectTest.java | 109 +++++++++- .../powertools/validation/model/Basket.java | 6 +- .../src/test/resources/alb_event.json | 28 +++ .../src/test/resources/amq_event.json | 28 +++ .../src/test/resources/cfcr_event.json | 20 ++ .../src/test/resources/custom_event.json | 2 +- .../src/test/resources/custom_event_gzip.json | 2 +- .../src/test/resources/cwl_event.json | 5 + .../src/test/resources/kafip_event.json | 14 ++ .../src/test/resources/kafka_event.json | 27 +++ .../src/test/resources/kasip_event.json | 17 ++ .../src/test/resources/kf_event.json | 12 ++ .../src/test/resources/rabbitmq_event.json | 51 +++++ .../src/test/resources/scheduled_event.json | 14 ++ .../src/test/resources/schema_v4.json | 6 +- .../src/test/resources/sns_event.json | 26 +++ .../src/test/resources/sqs.json | 1 - .../src/test/resources/sqs_message.json | 1 - 79 files changed, 1728 insertions(+), 316 deletions(-) rename powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/{PowerLogToolAlbCorrelationId.java => PowertoolsLogAlbCorrelationId.java} (78%) rename powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/{PowerLogToolEnabledWithClearState.java => PowertoolsLogEnabledWithClearState.java} (85%) create mode 100644 powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEventBridgeCorrelationId.java create mode 100644 powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/ParamManagerTest.java create mode 100644 powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/model/Order.java create mode 100644 powertools-serialization/src/test/resources/alb_event.json create mode 100644 powertools-serialization/src/test/resources/amq_event.json create mode 100644 powertools-serialization/src/test/resources/apigwv2_event.json create mode 100644 powertools-serialization/src/test/resources/cfcr_event.json create mode 100644 powertools-serialization/src/test/resources/custom_event_map.json create mode 100644 powertools-serialization/src/test/resources/cwl_event.json create mode 100644 powertools-serialization/src/test/resources/kafip_event.json create mode 100644 powertools-serialization/src/test/resources/kasip_event.json create mode 100644 powertools-serialization/src/test/resources/kf_event.json create mode 100644 powertools-serialization/src/test/resources/rabbitmq_event.json create mode 100644 powertools-serialization/src/test/resources/scheduled_event.json rename powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/{SQSHandler.java => GenericSchemaV7Handler.java} (82%) delete mode 100644 powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/KinesisHandler.java delete mode 100644 powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/ValidationInboundClasspathHandler.java create mode 100644 powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ResponseEventsArgumentsProvider.java create mode 100644 powertools-validation/src/test/resources/alb_event.json create mode 100644 powertools-validation/src/test/resources/amq_event.json create mode 100644 powertools-validation/src/test/resources/cfcr_event.json create mode 100644 powertools-validation/src/test/resources/cwl_event.json create mode 100644 powertools-validation/src/test/resources/kafip_event.json create mode 100644 powertools-validation/src/test/resources/kafka_event.json create mode 100644 powertools-validation/src/test/resources/kasip_event.json create mode 100644 powertools-validation/src/test/resources/kf_event.json create mode 100644 powertools-validation/src/test/resources/rabbitmq_event.json create mode 100644 powertools-validation/src/test/resources/scheduled_event.json create mode 100644 powertools-validation/src/test/resources/sns_event.json diff --git a/pom.xml b/pom.xml index 2e414d94f..b7aaa1e90 100644 --- a/pom.xml +++ b/pom.xml @@ -516,6 +516,7 @@ 3.1.2 + @{argLine} --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED diff --git a/powertools-core/pom.xml b/powertools-core/pom.xml index f259b1c65..cf9ad45d1 100644 --- a/powertools-core/pom.xml +++ b/powertools-core/pom.xml @@ -82,6 +82,11 @@ assertj-core test + + org.mockito + mockito-inline + test + \ 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..bb5fc4666 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 @@ -18,4 +18,9 @@ public class LambdaConstants { public static final String AWS_REGION_ENV = "AWS_REGION"; public static final String AWS_LAMBDA_INITIALIZATION_TYPE = "AWS_LAMBDA_INITIALIZATION_TYPE"; 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..c7f8b119f 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 @@ -27,15 +27,21 @@ import static software.amazon.lambda.powertools.core.internal.SystemWrapper.getenv; 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/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..94ad97506 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 @@ -6,60 +6,231 @@ import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; import java.io.InputStream; import java.io.OutputStream; +import java.util.Optional; 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; 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-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..9a1567d57 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 @@ -13,11 +13,11 @@ */ package software.amazon.lambda.powertools.logging; -import java.util.Map; - import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.logging.log4j.ThreadContext; +import java.util.Map; + import static java.util.Arrays.asList; /** 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..c96d1383e 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,12 @@ package software.amazon.lambda.powertools.logging.internal; -import java.io.IOException; -import java.io.Writer; -import java.nio.charset.Charset; -import java.util.LinkedHashMap; -import java.util.Map; - +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 org.apache.logging.log4j.Level; import org.apache.logging.log4j.Marker; import org.apache.logging.log4j.ThreadContext; @@ -25,13 +26,11 @@ 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; +import java.io.IOException; +import java.io.Writer; +import java.nio.charset.Charset; +import java.util.LinkedHashMap; +import java.util.Map; @Deprecated abstract class AbstractJacksonLayoutCopy extends AbstractStringLayout { 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..d489e093b 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 @@ -13,16 +13,6 @@ */ package software.amazon.lambda.powertools.logging.internal; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; -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; @@ -42,6 +32,16 @@ import software.amazon.lambda.powertools.logging.Logging; import software.amazon.lambda.powertools.logging.LoggingUtils; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.util.Map; +import java.util.Optional; +import java.util.Random; + import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Optional.empty; import static java.util.Optional.ofNullable; 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..60f0806a9 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 @@ -13,15 +13,6 @@ */ package org.apache.logging.log4j.core.layout; -import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.nio.channels.FileChannel; -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.RequestHandler; import com.fasterxml.jackson.core.JsonProcessingException; @@ -34,6 +25,15 @@ import software.amazon.lambda.powertools.logging.handlers.PowerLogToolSamplingEnabled; import software.amazon.lambda.powertools.logging.internal.LambdaLoggingAspect; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +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; 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..eee8ace05 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 @@ -13,13 +13,13 @@ */ package software.amazon.lambda.powertools.logging; -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 java.util.HashMap; +import java.util.Map; + import static org.assertj.core.api.Assertions.assertThat; 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..15b39c6c5 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 @@ -13,12 +13,12 @@ */ package software.amazon.lambda.powertools.logging.handlers; -import java.io.InputStream; -import java.io.OutputStream; - 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 { @Override 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..f1c2f62c8 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,7 +1,5 @@ package software.amazon.lambda.powertools.logging.handlers; -import java.io.IOException; - import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification; @@ -13,6 +11,8 @@ import software.amazon.lambda.powertools.logging.Logging; import software.amazon.lambda.powertools.logging.LoggingUtils; +import java.io.IOException; + public class PowerToolLogEventEnabledWithCustomMapper implements RequestHandler { static { 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 78% 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..c06f8326e 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 @@ -18,14 +18,12 @@ 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 85% 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..8b7d4fcaa 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 @@ -20,13 +20,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 { public static int COUNT = 1; + private static final Logger LOG = LogManager.getLogger(PowertoolsLogEnabledWithClearState.class); + @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..f03983aff --- /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 com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestStreamHandler; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import software.amazon.lambda.powertools.logging.Logging; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import static software.amazon.lambda.powertools.logging.CorrelationIdPathConstants.EVENT_BRIDGE; + +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..8e7d2d3e6 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 @@ -13,22 +13,6 @@ */ package software.amazon.lambda.powertools.logging.internal; -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStreamReader; -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.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; @@ -49,25 +33,35 @@ 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 software.amazon.lambda.powertools.logging.handlers.PowertoolsLogAlbCorrelationId; +import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEnabledWithClearState; +import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEventBridgeCorrelationId; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +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.List; +import java.util.Map; +import java.util.stream.Collectors; -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; @@ -284,7 +278,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 +286,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); @@ -328,24 +336,24 @@ private void resetLogLevel(Level level) throws NoSuchMethodException, IllegalAcc } private S3EventNotification s3EventNotification() { - S3EventNotificationRecord record = new S3EventNotificationRecord("us-west-2", + S3EventNotification.S3EventNotificationRecord record = new S3EventNotification.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"), + 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 S3ObjectEntity("HappyFace.jpg", + new S3EventNotification.S3ObjectEntity("HappyFace.jpg", 1024L, "d41d8cd98f00b204e9800998ecf8427e", "096fKKXTRTtl3on89fVO.nfljtsv6qko", "0055AED6DCD90281E5"), "1.0"), - new UserIdentityEntity("AIDAJDPLRKLG7UEXAMPLE") + new S3EventNotification.UserIdentityEntity("AIDAJDPLRKLG7UEXAMPLE") ); return new S3EventNotification(singletonList(record)); 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..e0255125d 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 @@ -9,8 +9,6 @@ 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.parameters.cache.CacheManager; import software.amazon.lambda.powertools.parameters.transform.TransformationManager; 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..5144af0c2 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 @@ -5,7 +5,11 @@ 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.parameters.cache.CacheManager; import software.amazon.lambda.powertools.parameters.exception.DynamoDbProviderSchemaException; import software.amazon.lambda.powertools.parameters.transform.TransformationManager; 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..b2e541c43 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 @@ -14,14 +14,13 @@ package software.amazon.lambda.powertools.parameters; 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; /** @@ -164,13 +163,13 @@ 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); 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/SSMProvider.java b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/SSMProvider.java index bf36aa717..d6d87747b 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 @@ -13,10 +13,6 @@ */ 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.http.urlconnection.UrlConnectionHttpClient; @@ -32,6 +28,10 @@ import software.amazon.lambda.powertools.parameters.transform.TransformationManager; import software.amazon.lambda.powertools.parameters.transform.Transformer; +import java.time.temporal.ChronoUnit; +import java.util.HashMap; +import java.util.Map; + import static software.amazon.lambda.powertools.core.internal.LambdaConstants.AWS_LAMBDA_INITIALIZATION_TYPE; /** @@ -74,7 +74,7 @@ */ public class SSMProvider extends BaseProvider { - private final SsmClient client; + private SsmClient client; private boolean decrypt = false; private boolean recursive = false; @@ -92,6 +92,15 @@ public class SSMProvider extends BaseProvider { this.client = client; } + /** + * Constructor + * + * @param cacheManager handles the parameter caching + */ + SSMProvider(CacheManager cacheManager) { + super(cacheManager); + } + /** * Retrieve the parameter value from the AWS System Manager Parameter Store. * 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..54d0daee3 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 @@ -13,10 +13,6 @@ */ package software.amazon.lambda.powertools.parameters; -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.http.urlconnection.UrlConnectionHttpClient; @@ -29,6 +25,10 @@ import software.amazon.lambda.powertools.parameters.transform.TransformationManager; import software.amazon.lambda.powertools.parameters.transform.Transformer; +import java.time.temporal.ChronoUnit; +import java.util.Base64; +import java.util.Map; + import static java.nio.charset.StandardCharsets.UTF_8; import static software.amazon.lambda.powertools.core.internal.LambdaConstants.AWS_LAMBDA_INITIALIZATION_TYPE; @@ -58,7 +58,7 @@ */ public class SecretsProvider extends BaseProvider { - private final SecretsManagerClient client; + private SecretsManagerClient client; /** * Constructor with custom {@link SecretsManagerClient}.
@@ -73,6 +73,15 @@ public class SecretsProvider extends BaseProvider { this.client = client; } + /** + * Constructor + * + * @param cacheManager handles the parameter caching + */ + SecretsProvider(CacheManager cacheManager) { + super(cacheManager); + } + /** * Retrieve the parameter value from the AWS Secrets Manager. * 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..8de2f3f57 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 @@ -5,7 +5,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,9 +18,6 @@ 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()) { 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..c666edce7 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 @@ -13,10 +13,10 @@ */ package software.amazon.lambda.powertools.parameters.transform; -import java.util.Base64; - import software.amazon.lambda.powertools.parameters.exception.TransformationException; +import java.util.Base64; + import static java.nio.charset.StandardCharsets.UTF_8; /** 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..23f6271da 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 @@ -7,7 +7,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; @@ -17,34 +16,30 @@ 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.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatRuntimeException; 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 +64,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())) @@ -103,6 +98,29 @@ public void getValueRetrievesValue() { 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); + } + /** * If we mix requests for different keys together through the same provider, retrieval should * work as expected. This means two separate configuration sessions should be established with AppConfig. @@ -145,4 +163,47 @@ public void multipleKeysRetrievalWorks() { } + @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/DynamoDbProviderTest.java b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/DynamoDbProviderTest.java index 0e5b734d6..d6818a64f 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 @@ -8,15 +8,21 @@ 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 software.amazon.lambda.powertools.parameters.transform.TransformationManager; import java.util.HashMap; import java.util.Map; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.mockito.MockitoAnnotations.openMocks; public class DynamoDbProviderTest { @@ -24,6 +30,9 @@ public class DynamoDbProviderTest { @Mock DynamoDbClient client; + @Mock + TransformationManager transformationManager; + @Captor ArgumentCaptor getItemValueCaptor; @@ -67,7 +76,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 +89,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 @@ -92,7 +115,7 @@ public void getValueWithMalformedRowThrows() { .build()); // Act Assertions.assertThrows(DynamoDbProviderSchemaException.class, () -> { - String value = provider.getValue(key); + provider.getValue(key); }); } @@ -144,6 +167,25 @@ 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 @@ -160,9 +202,25 @@ public void getValuesWithMalformedRowThrows() { // Assert Assertions.assertThrows(DynamoDbProviderSchemaException.class, () -> { // Act - Map values = provider.getMultipleValues(key); + 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..e1cb72be9 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 @@ -25,7 +25,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 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 java.util.ArrayList; import java.util.List; @@ -35,7 +39,9 @@ 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.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.openMocks; public class ParamManagerIntegrationTest { 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..ee61691c1 --- /dev/null +++ b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/ParamManagerTest.java @@ -0,0 +1,101 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.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 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; + +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatRuntimeException; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +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() { + + // Act + SecretsProvider secretsProvider = ParamManager.getSecretsProvider(); + + // Assert + assertNotNull(secretsProvider); + } + + @Test + public void testGetSSMProvider() { + + // Act + SSMProvider ssmProvider = ParamManager.getSsmProvider(); + + // Assert + assertNotNull(ssmProvider); + } + +} 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..e55f3d7e6 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 @@ -20,16 +20,26 @@ 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 software.amazon.lambda.powertools.parameters.transform.TransformationManager; +import java.time.temporal.ChronoUnit; 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.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; public class SSMProviderTest { @@ -37,6 +47,9 @@ public class SSMProviderTest { @Mock SsmClient client; + @Mock + TransformationManager transformationManager; + @Captor ArgumentCaptor paramCaptor; @@ -181,10 +194,24 @@ public void getMultipleWithNextToken() { 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..2ab72ffdd 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 @@ -24,10 +24,14 @@ 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 software.amazon.lambda.powertools.parameters.transform.TransformationManager; +import java.time.temporal.ChronoUnit; import java.util.Base64; 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; public class SecretsProviderTest { @@ -35,6 +39,9 @@ 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); @@ -75,4 +84,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/internal/LambdaParametersAspectTest.java b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/internal/LambdaParametersAspectTest.java index 2edbc8b24..c390a051e 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 @@ -14,7 +14,10 @@ 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.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; public class LambdaParametersAspectTest { 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..6b6548071 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 @@ -15,10 +15,12 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import software.amazon.lambda.powertools.parameters.exception.TransformationException; import java.util.Base64; 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; @@ -27,6 +29,8 @@ 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); @@ -82,4 +94,12 @@ public void performComplexTransformation_shouldPerformTransformation() { 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/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..f1b248fae 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 @@ -13,7 +13,21 @@ */ package software.amazon.lambda.powertools.utilities; -import com.amazonaws.services.lambda.runtime.events.*; +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; 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..549418263 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 @@ -45,6 +45,7 @@ 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()); 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..8628fd1d2 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 @@ -46,25 +46,29 @@ protected T callFunction(Adapter runtime, List> argum String decompressString = decompress(decode(encodedString.getBytes(UTF_8))); + if (decompressString == null) { + return runtime.createNull(); + } + 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) { 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..5055d7086 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 @@ -13,11 +13,26 @@ */ package software.amazon.lambda.powertools.utilities; -import com.amazonaws.services.lambda.runtime.events.*; +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 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; @@ -129,7 +144,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 +176,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 +202,84 @@ 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/Base64GZipFunctionTest.java b/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/jmespath/Base64GZipFunctionTest.java index 8c617a634..8e574eba6 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 @@ -16,6 +16,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.JsonNodeType; import io.burt.jmespath.Expression; +import io.burt.jmespath.JmesPathType; import org.junit.jupiter.api.Test; import software.amazon.lambda.powertools.utilities.JsonConfig; @@ -25,6 +26,16 @@ public class Base64GZipFunctionTest { + @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); + + } + @Test public void testPowertoolsGzip() throws IOException { JsonNode event = JsonConfig.get().getObjectMapper().readTree(this.getClass().getResourceAsStream("/custom_event_gzip.json")); @@ -33,4 +44,38 @@ public void testPowertoolsGzip() throws IOException { 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/model/Order.java b/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/model/Order.java new file mode 100644 index 000000000..eca36c222 --- /dev/null +++ b/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/model/Order.java @@ -0,0 +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.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/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/test/java/software/amazon/lambda/powertools/sqs/SqsUtilsBatchProcessorTest.java b/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/SqsUtilsBatchProcessorTest.java index 0d1c9c35a..43a089d2c 100644 --- 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 @@ -23,7 +23,6 @@ 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; diff --git a/powertools-validation/pom.xml b/powertools-validation/pom.xml index 76c434614..1469183ef 100644 --- a/powertools-validation/pom.xml +++ b/powertools-validation/pom.xml @@ -1,6 +1,6 @@ - 4.0.0 @@ -112,6 +112,11 @@ assertj-core test
+ + org.junit.jupiter + junit-jupiter-params + test + \ No newline at end of file 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..b29adf8d7 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 @@ -31,19 +31,19 @@ * 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 +57,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 +94,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/ValidationUtils.java b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/ValidationUtils.java index 9b73806a5..3c2322edc 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 @@ -13,15 +13,6 @@ */ 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; - import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer; import com.amazonaws.services.lambda.runtime.serialization.events.LambdaEventSerializers; import com.fasterxml.jackson.core.JsonProcessingException; @@ -33,6 +24,14 @@ import io.burt.jmespath.Expression; import software.amazon.lambda.powertools.validation.internal.ValidationAspect; +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; + /** * Validation utility, used to manually validate Json against Json Schema */ @@ -79,7 +78,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)); @@ -120,7 +119,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 +148,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 +177,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); @@ -253,12 +252,9 @@ 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) { + } catch (Exception e) { throw new IllegalArgumentException("'" + schema + "' is invalid, verify '" + filePath + "' is in your classpath"); } } else { 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..e659abbeb 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 @@ -13,7 +13,26 @@ */ package software.amazon.lambda.powertools.validation.internal; -import com.amazonaws.services.lambda.runtime.events.*; +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; 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..6669e46b1 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 @@ -13,13 +13,17 @@ import java.util.HashMap; import java.util.Map; -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.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; 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() { @@ -44,8 +48,11 @@ public void testLoadSchemaV7KO() { @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 @@ -94,7 +101,9 @@ public void testLoadSchemaNotFound() { public void testValidateJsonNodeOK() throws IOException { JsonNode node = ValidationConfig.get().getObjectMapper().readTree(this.getClass().getResourceAsStream("/json_ok.json")); - validate(node, schema); + assertThatNoException().isThrownBy(() -> { + validate(node, schemaString); + }); } @Test @@ -106,12 +115,15 @@ public void testValidateJsonNodeKO() throws IOException { @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 +135,21 @@ 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 +162,22 @@ 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 +188,10 @@ 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 @@ -182,7 +215,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 @@ -203,7 +236,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 +259,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 +276,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 82% 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..1c64e7da1 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 @@ -15,14 +15,13 @@ 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/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..2e62ba88d 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 @@ -76,7 +76,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..63e93c3ac 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 @@ -14,39 +14,129 @@ package software.amazon.lambda.powertools.validation.internal; 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 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 java.util.stream.Stream; 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; + 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 +148,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," + @@ -97,7 +187,7 @@ public void validate_SQS() { 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"); } @@ -124,15 +214,16 @@ public void validate_Kinesis() { 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..398f67265 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 @@ -21,6 +21,9 @@ public class Basket { private String hiddenProduct; + public Basket() { + } + public List getProducts() { return products; } @@ -29,9 +32,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/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", From ba8075f291a623bd42b422b9edb96572ac0a6f0d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Jul 2023 13:29:24 +0200 Subject: [PATCH 13/55] build(deps): bump aws.sdk.version from 2.20.101 to 2.20.102 (#1279) Bumps `aws.sdk.version` from 2.20.101 to 2.20.102. Updates `software.amazon.awssdk:bom` from 2.20.101 to 2.20.102 Updates `http-client-spi` from 2.20.101 to 2.20.102 Updates `url-connection-client` from 2.20.101 to 2.20.102 Updates `s3` from 2.20.101 to 2.20.102 Updates `lambda` from 2.20.101 to 2.20.102 Updates `cloudwatch` from 2.20.101 to 2.20.102 Updates `xray` from 2.20.101 to 2.20.102 Updates `cloudformation` from 2.20.101 to 2.20.102 Updates `sts` from 2.20.101 to 2.20.102 --- updated-dependencies: - dependency-name: software.amazon.awssdk:bom dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:http-client-spi dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:url-connection-client dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:s3 dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:lambda dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:cloudwatch dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:xray dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:cloudformation dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:sts dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/powertools-examples-sqs/pom.xml | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/powertools-examples-sqs/pom.xml b/examples/powertools-examples-sqs/pom.xml index 314a8c32b..7b22ab33e 100644 --- a/examples/powertools-examples-sqs/pom.xml +++ b/examples/powertools-examples-sqs/pom.xml @@ -28,7 +28,7 @@ software.amazon.awssdk url-connection-client - 2.20.101 + 2.20.102 com.amazonaws diff --git a/pom.xml b/pom.xml index b7aaa1e90..916bc7fe2 100644 --- a/pom.xml +++ b/pom.xml @@ -60,7 +60,7 @@ 2.20.0 2.15.2 1.9.7 - 2.20.101 + 2.20.102 2.14.0 2.1.3 UTF-8 From 5ba9cf43695450b4f168a0cb6520e887fc8b720d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Van=20Der=20Linden?= <117538+jeromevdl@users.noreply.github.com> Date: Tue, 11 Jul 2023 17:29:00 +0200 Subject: [PATCH 14/55] docs: update documentation for aspectJ (#1273) * Update documentation for aspectJ * update README --- README.md | 96 +++++++++- docs/core/logging.md | 135 +++++++++++++ docs/core/metrics.md | 133 +++++++++++++ docs/core/tracing.md | 137 +++++++++++++- docs/index.md | 189 ++++++++++++++++--- docs/utilities/batch.md | 103 ++++++++-- docs/utilities/idempotency.md | 97 +++++++++- docs/utilities/parameters.md | 141 ++++++++++++-- docs/utilities/serialization.md | 16 +- docs/utilities/sqs_large_message_handling.md | 103 ++++++++-- docs/utilities/validation.md | 117 +++++++++--- mkdocs.yml | 2 +- 12 files changed, 1142 insertions(+), 127 deletions(-) diff --git a/README.md b/README.md index fe6e9afba..d1f635a9c 100644 --- a/README.md +++ b/README.md @@ -13,24 +13,24 @@ Powertools for AWS Lambda (Java) is a developer toolkit to implement Serverless Powertools for AWS Lambda (Java) is available in Maven Central. You can use your favourite dependency management tool to install it -* [maven](https://maven.apache.org/): +#### Maven: ```xml ... software.amazon.lambda powertools-tracing - 1.17.0-SNAPSHOT + 1.16.0 software.amazon.lambda powertools-logging - 1.17.0-SNAPSHOT + 1.16.0 software.amazon.lambda powertools-metrics - 1.17.0-SNAPSHOT + 1.16.0 ... @@ -38,6 +38,7 @@ Powertools for AWS Lambda (Java) is available in Maven Central. You can use your And configure the aspectj-maven-plugin to compile-time weave (CTW) the aws-lambda-powertools-java aspects into your project: +For Java 11+, use the following: ```xml @@ -78,6 +79,93 @@ And configure the aspectj-maven-plugin to compile-time weave (CTW) the aws-lambd ``` +For Java 8, use the following: +```xml + + + ... + + org.codehaus.mojo + aspectj-maven-plugin + 1.14.0 + + 1.8 + 1.8 + 1.8 + + + software.amazon.lambda + powertools-logging + + + software.amazon.lambda + powertools-tracing + + + software.amazon.lambda + powertools-metrics + + + + + + + compile + + + + + ... + + +``` +#### gradle + +For Java 11+: + + ```groovy + 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 + ``` + +For Java8: + + ```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 + ``` + + ## Example See the **[examples](examples)** directory for example projects showcasing usage of different utilities. diff --git a/docs/core/logging.md b/docs/core/logging.md index 1af206314..bf5fb6767 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. 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..f00e0314d 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?" + Lambda Powertools for 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/utilities/batch.md b/docs/utilities/batch.md index 0a2add081..c2072105d 100644 --- a/docs/utilities/batch.md +++ b/docs/utilities/batch.md @@ -23,10 +23,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 +37,7 @@ To install this utility, add the following dependency to your project. ... + ... @@ -44,6 +46,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 +115,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 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/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 index 5f7ede095..82e1afef4 100644 --- a/docs/utilities/sqs_large_message_handling.md +++ b/docs/utilities/sqs_large_message_handling.md @@ -30,11 +30,10 @@ This utility is compatible with versions *[1.1.0+](https://github.com/awslabs/am ``` ## 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" ... @@ -44,6 +43,7 @@ To install this utility, add the following dependency to your project. ... + ... @@ -52,6 +52,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 @@ -76,24 +121,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 ``` ## Lambda handler 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/mkdocs.yml b/mkdocs.yml index b758239de..5fd3206b0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -83,7 +83,7 @@ extra_javascript: extra: powertools: - version: 1.17.0-SNAPSHOT + version: 1.16.0 # to update after each release (we do not want snapshot version here) repo_url: https://github.com/aws-powertools/powertools-lambda-java edit_uri: edit/main/docs From a19def3e4ea9e510c0ce4ad602abe58db0610d1f Mon Sep 17 00:00:00 2001 From: Scott Gerring Date: Tue, 11 Jul 2023 17:39:45 +0200 Subject: [PATCH 15/55] fix: Handle batch failures in FIFO queues correctly (#1183) --- docs/utilities/batch.md | 10 +- powertools-sqs/pom.xml | 4 + .../lambda/powertools/sqs/SqsUtils.java | 45 ++++- ...ippedMessageDueToFailedBatchException.java | 12 ++ .../sqs/SqsUtilsFifoBatchProcessorTest.java | 166 ++++++++++++++++++ .../src/test/resources/SqsFifoBatchEvent.json | 73 ++++++++ 6 files changed, 305 insertions(+), 5 deletions(-) create mode 100644 powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/exception/SkippedMessageDueToFailedBatchException.java create mode 100644 powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/SqsUtilsFifoBatchProcessorTest.java create mode 100644 powertools-sqs/src/test/resources/SqsFifoBatchEvent.json diff --git a/docs/utilities/batch.md b/docs/utilities/batch.md index c2072105d..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** @@ -177,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/powertools-sqs/pom.xml b/powertools-sqs/pom.xml index 5eafcc8d3..618aa948c 100644 --- a/powertools-sqs/pom.xml +++ b/powertools-sqs/pom.xml @@ -45,6 +45,10 @@ software.amazon.lambda powertools-core + + com.amazonaws + aws-lambda-java-tests + com.amazonaws aws-lambda-java-core 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 index 3461c6755..9fff4dc6f 100644 --- 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 @@ -15,7 +15,9 @@ import java.lang.reflect.Constructor; import java.util.ArrayList; +import java.util.LinkedList; import java.util.List; +import java.util.Queue; import java.util.function.Function; import java.util.stream.Collectors; @@ -26,6 +28,7 @@ import org.slf4j.LoggerFactory; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.sqs.SqsClient; +import software.amazon.lambda.powertools.sqs.exception.SkippedMessageDueToFailedBatchException; import software.amazon.lambda.powertools.sqs.internal.BatchContext; import software.amazon.payloadoffloading.PayloadS3Pointer; import software.amazon.lambda.powertools.sqs.internal.SqsLargeMessageAspect; @@ -43,6 +46,9 @@ public final class SqsUtils { private static SqsClient client; private static S3Client s3Client; + // The attribute on an SQS-FIFO message used to record the message group ID + private static final String MESSAGE_GROUP_ID = "MessageGroupId"; + private SqsUtils() { } @@ -490,19 +496,52 @@ public static List batchProcessor(final SQSEvent event, } BatchContext batchContext = new BatchContext(client); - - for (SQSMessage message : event.getRecords()) { + int offset = 0; + boolean failedBatch = false; + while (offset < event.getRecords().size() && !failedBatch) { + // Get the current message and advance to the next. Doing this here + // makes it easier for us to know where we are up to if we have to + // break out of here early. + SQSMessage message = event.getRecords().get(offset); + offset++; + + // If the batch hasn't failed, try process the message try { handlerReturn.add(handler.process(message)); batchContext.addSuccess(message); } catch (Exception e) { + + // Record the failure batchContext.addFailure(message, e); + + // If we are trying to process a message that has a messageGroupId, we are on a FIFO queue. A failure + // now stops us from processing the rest of the batch; we break out of the loop leaving unprocessed + // messages in the queue + // https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html#services-sqs-batchfailurereporting + String messageGroupId = message.getAttributes() != null ? + message.getAttributes().get(MESSAGE_GROUP_ID) : null; + if (messageGroupId != null) { + LOG.info("A message in a message batch with messageGroupId {} and messageId {} failed; failing the rest of the batch too" + , messageGroupId, message.getMessageId()); + failedBatch = true; + } LOG.error("Encountered issue processing message: {}", message.getMessageId(), e); } } - batchContext.processSuccessAndHandleFailed(handlerReturn, suppressException, deleteNonRetryableMessageFromQueue, nonRetryableExceptions); + // If we have a FIFO batch failure, unprocessed messages will remain on the queue + // past the failed message. We have to add these to the errors + if (offset < event.getRecords().size()) { + event.getRecords() + .subList(offset, event.getRecords().size()) + .forEach(message -> { + LOG.info("Skipping message {} as another message with a message group failed in this batch", + message.getMessageId()); + batchContext.addFailure(message, new SkippedMessageDueToFailedBatchException()); + }); + } + batchContext.processSuccessAndHandleFailed(handlerReturn, suppressException, deleteNonRetryableMessageFromQueue, nonRetryableExceptions); return handlerReturn; } diff --git a/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/exception/SkippedMessageDueToFailedBatchException.java b/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/exception/SkippedMessageDueToFailedBatchException.java new file mode 100644 index 000000000..9dbb66509 --- /dev/null +++ b/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/exception/SkippedMessageDueToFailedBatchException.java @@ -0,0 +1,12 @@ +package software.amazon.lambda.powertools.sqs.exception; + +/** + * Indicates that a message has been skipped due to the batch it is + * within failing. + */ +public class SkippedMessageDueToFailedBatchException extends Exception { + + public SkippedMessageDueToFailedBatchException() { + } + +} diff --git a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/SqsUtilsFifoBatchProcessorTest.java b/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/SqsUtilsFifoBatchProcessorTest.java new file mode 100644 index 000000000..cfa79dc36 --- /dev/null +++ b/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/SqsUtilsFifoBatchProcessorTest.java @@ -0,0 +1,166 @@ +package software.amazon.lambda.powertools.sqs; + +import com.amazonaws.services.lambda.runtime.events.SQSEvent; +import com.amazonaws.services.lambda.runtime.tests.EventLoader; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.*; +import org.mockito.quality.Strictness; +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 java.io.IOException; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.MockitoAnnotations.openMocks; +import static software.amazon.lambda.powertools.sqs.SqsUtils.batchProcessor; +import static software.amazon.lambda.powertools.sqs.SqsUtils.overrideSqsClient; + +public class SqsUtilsFifoBatchProcessorTest { + + private static SQSEvent sqsBatchEvent; + private MockitoSession session; + + @Mock + private SqsClient sqsClient; + + @Captor + private ArgumentCaptor deleteMessageBatchCaptor; + + public SqsUtilsFifoBatchProcessorTest() throws IOException { + sqsBatchEvent = EventLoader.loadSQSEvent("SqsFifoBatchEvent.json"); + } + + @BeforeEach + public void setup() { + // Establish a strict mocking session. This means that any + // calls to a mock that have not been stubbed will throw + this.session = Mockito.mockitoSession() + .strictness(Strictness.STRICT_STUBS) + .initMocks(this) + .startMocking(); + + overrideSqsClient(sqsClient); + } + + @AfterEach + public void tearDown() { + session.finishMocking(); + } + + @Test + public void processWholeBatch() { + // Act + AtomicInteger processedCount = new AtomicInteger(); + List results = batchProcessor(sqsBatchEvent, false, (message) -> { + processedCount.getAndIncrement(); + return true; + }); + + // Assert + assertThat(processedCount.get()).isEqualTo(3); + assertThat(results.size()).isEqualTo(3); + } + + /** + * Check that a failure in the middle of the batch: + * - deletes the successful message explicitly from SQS + * - marks the failed and subsequent message as failed + * - does not delete the failed or subsequent message + */ + @Test + public void singleFailureInMiddleOfBatch() { + // Arrange + Mockito.when(sqsClient.deleteMessageBatch(deleteMessageBatchCaptor.capture())).thenReturn(DeleteMessageBatchResponse + .builder().build()); + + // Act + AtomicInteger processedCount = new AtomicInteger(); + assertThatExceptionOfType(SQSBatchProcessingException.class) + .isThrownBy(() -> batchProcessor(sqsBatchEvent, false, (message) -> { + int value = processedCount.getAndIncrement(); + if (value == 1) { + throw new RuntimeException("Whoops"); + } + return true; + })) + + // Assert + .isInstanceOf(SQSBatchProcessingException.class) + .satisfies(e -> { + List failures = ((SQSBatchProcessingException)e).getFailures(); + assertThat(failures.size()).isEqualTo(2); + List failureIds = failures.stream() + .map(SQSEvent.SQSMessage::getMessageId) + .collect(Collectors.toList()); + assertThat(failureIds).contains(sqsBatchEvent.getRecords().get(1).getMessageId()); + assertThat(failureIds).contains(sqsBatchEvent.getRecords().get(2).getMessageId()); + }); + + DeleteMessageBatchRequest deleteRequest = deleteMessageBatchCaptor.getValue(); + List messageIds = deleteRequest.entries().stream() + .map(DeleteMessageBatchRequestEntry::id) + .collect(Collectors.toList()); + assertThat(deleteRequest.entries().size()).isEqualTo(1); + assertThat(messageIds.contains(sqsBatchEvent.getRecords().get(0).getMessageId())).isTrue(); + + } + + @Test + public void singleFailureAtEndOfBatch() { + + // Arrange + Mockito.when(sqsClient.deleteMessageBatch(deleteMessageBatchCaptor.capture())).thenReturn(DeleteMessageBatchResponse + .builder().build()); + + + // Act + AtomicInteger processedCount = new AtomicInteger(); + assertThatExceptionOfType(SQSBatchProcessingException.class) + .isThrownBy(() -> batchProcessor(sqsBatchEvent, false, (message) -> { + int value = processedCount.getAndIncrement(); + if (value == 2) { + throw new RuntimeException("Whoops"); + } + return true; + })); + + // Assert + DeleteMessageBatchRequest deleteRequest = deleteMessageBatchCaptor.getValue(); + List messageIds = deleteRequest.entries().stream() + .map(DeleteMessageBatchRequestEntry::id) + .collect(Collectors.toList()); + assertThat(deleteRequest.entries().size()).isEqualTo(2); + assertThat(messageIds.contains(sqsBatchEvent.getRecords().get(0).getMessageId())).isTrue(); + assertThat(messageIds.contains(sqsBatchEvent.getRecords().get(1).getMessageId())).isTrue(); + assertThat(messageIds.contains(sqsBatchEvent.getRecords().get(2).getMessageId())).isFalse(); + } + + @Test + public void messageFailureStopsGroupProcessing() { + String groupToFail = sqsBatchEvent.getRecords().get(0).getAttributes().get("MessageGroupId"); + + assertThatExceptionOfType(SQSBatchProcessingException.class) + .isThrownBy(() -> batchProcessor(sqsBatchEvent, (message) -> { + String groupId = message.getAttributes().get("MessageGroupId"); + if (groupId.equals(groupToFail)) { + throw new RuntimeException("Failed processing"); + } + return groupId; + })) + .satisfies(e -> { + assertThat(e.successMessageReturnValues().size()).isEqualTo(0); + assertThat(e.successMessageReturnValues().contains(groupToFail)).isFalse(); + }); + } + +} diff --git a/powertools-sqs/src/test/resources/SqsFifoBatchEvent.json b/powertools-sqs/src/test/resources/SqsFifoBatchEvent.json new file mode 100644 index 000000000..726e45ea1 --- /dev/null +++ b/powertools-sqs/src/test/resources/SqsFifoBatchEvent.json @@ -0,0 +1,73 @@ +{ + "Records": [ + { + "messageId": "059f36b4-87a3-44ab-83d2-661975830a7d", + "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", + "body": "Test message.", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1545082649183", + "SenderId": "AIDAIENQZJOLO23YVJ4VO", + "ApproximateFirstReceiveTimestamp": "1545082649185", + "MessageGroupId": "Group1" + }, + "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", + "MessageGroupId": "Group2" + }, + "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", + "MessageGroupId": "Group1" + }, + "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 From b6b118f23025a5dd66ffe76641cda170b80105cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Van=20Der=20Linden?= <117538+jeromevdl@users.noreply.github.com> Date: Wed, 12 Jul 2023 17:18:13 +0200 Subject: [PATCH 16/55] fix: ParamManager cannot provide default SSM & Secrets providers (#1282) * fix issue 1280 * Update powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/ParamManager.java missing issue number --- powertools-parameters/pom.xml | 14 ++++++ .../parameters/DynamoDbProvider.java | 43 +++++++++++------- .../powertools/parameters/ParamManager.java | 9 +++- .../powertools/parameters/SSMProvider.java | 44 ++++++++++++------- .../parameters/SecretsProvider.java | 42 +++++++++++------- .../parameters/ParamManagerTest.java | 20 ++++++++- 6 files changed, 120 insertions(+), 52 deletions(-) diff --git a/powertools-parameters/pom.xml b/powertools-parameters/pom.xml index af980a4c3..b50844d99 100644 --- a/powertools-parameters/pom.xml +++ b/powertools-parameters/pom.xml @@ -133,4 +133,18 @@ + + + + maven-surefire-plugin + 3.1.2 + + + eu-central-1 + + + + + + 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 5144af0c2..1b77aed88 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 @@ -5,11 +5,13 @@ 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.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.LambdaConstants; import software.amazon.lambda.powertools.parameters.cache.CacheManager; import software.amazon.lambda.powertools.parameters.exception.DynamoDbProviderSchemaException; import software.amazon.lambda.powertools.parameters.transform.TransformationManager; @@ -18,6 +20,8 @@ import java.util.Map; import java.util.stream.Collectors; +import static software.amazon.lambda.powertools.core.internal.LambdaConstants.AWS_LAMBDA_INITIALIZATION_TYPE; + /** * Implements a {@link ParamProvider} on top of DynamoDB. The schema of the table * is described in the Powertools for AWS Lambda (Java) documentation. @@ -30,23 +34,16 @@ 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); + } + /** * Return a single value from the DynamoDB parameter provider. * @@ -136,11 +133,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); } @@ -191,5 +188,21 @@ public DynamoDbProvider.Builder withTransformationManager(TransformationManager this.transformationManager = transformationManager; return this; } + + private static DynamoDbClient createClient() { + DynamoDbClientBuilder dynamoDbClientBuilder = DynamoDbClient.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)) { + dynamoDbClientBuilder.credentialsProvider(EnvironmentVariableCredentialsProvider.create()); + } + + return dynamoDbClientBuilder.build(); + } } } 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 b2e541c43..c8abedf06 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 @@ -37,14 +37,19 @@ 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. + * @deprecated You should not use this method directly but a typed one (getSecretsProvider, getSsmProvider, getDynamoDbProvider, getAppConfigProvider), will be removed in v2 * @return a {@link SecretsProvider} */ + // 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); } @@ -166,7 +171,7 @@ public static TransformationManager getTransformationManager() { 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 (ReflectiveOperationException e) { 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 d6d87747b..2eb2d4199 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 @@ -74,8 +74,7 @@ */ public class SSMProvider extends BaseProvider { - private SsmClient client; - + private final SsmClient client; private boolean decrypt = false; private boolean recursive = false; @@ -93,12 +92,13 @@ public class SSMProvider extends BaseProvider { } /** - * Constructor + * Constructor with only a CacheManager
* + * Used in {@link ParamManager#createProvider(Class)} * @param cacheManager handles the parameter caching */ SSMProvider(CacheManager cacheManager) { - super(cacheManager); + this(cacheManager, Builder.createClient()); } /** @@ -228,6 +228,11 @@ protected void resetToDefaults() { decrypt = false; } + // For tests purpose only + SsmClient getClient() { + return client; + } + /** * Create a builder that can be used to configure and create a {@link SSMProvider}. * @@ -237,6 +242,7 @@ public static SSMProvider.Builder builder() { return new SSMProvider.Builder(); } + static class Builder { private SsmClient client; private CacheManager cacheManager; @@ -253,19 +259,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); @@ -288,6 +282,22 @@ public SSMProvider.Builder withClient(SsmClient client) { return this; } + private static SsmClient createClient() { + 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()); + } + + return ssmClientBuilder.build(); + } + /** * Mandatory. Provide a CacheManager to the {@link SSMProvider} * 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 54d0daee3..ea8b5a9d0 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 @@ -58,7 +58,7 @@ */ public class SecretsProvider extends BaseProvider { - private SecretsManagerClient client; + private final SecretsManagerClient client; /** * Constructor with custom {@link SecretsManagerClient}.
@@ -74,12 +74,13 @@ public class SecretsProvider extends BaseProvider { } /** - * Constructor + * Constructor with only a CacheManager
* + * Used in {@link ParamManager#createProvider(Class)} * @param cacheManager handles the parameter caching */ SecretsProvider(CacheManager cacheManager) { - super(cacheManager); + this(cacheManager, Builder.createClient()); } /** @@ -135,6 +136,11 @@ public SecretsProvider withTransformation(Class transform return this; } + // For test purpose only + SecretsManagerClient getClient() { + return client; + } + /** * Create a builder that can be used to configure and create a {@link SecretsProvider}. * @@ -161,19 +167,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); @@ -196,6 +190,22 @@ public Builder withClient(SecretsManagerClient client) { return this; } + private static SecretsManagerClient createClient() { + 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()); + } + + return secretsManagerClientBuilder.build(); + } + /** * Mandatory. Provide a CacheManager to the {@link SecretsProvider} * 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 index ee61691c1..a21a6082c 100644 --- 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 @@ -18,6 +18,7 @@ import software.amazon.lambda.powertools.parameters.internal.CustomProvider; import software.amazon.lambda.powertools.parameters.transform.TransformationManager; +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; @@ -79,23 +80,38 @@ public void testGetProviderWithProviderClass_throwsException() { } @Test - public void testGetSecretsProvider() { + public void testGetSecretsProvider_withoutParameter_shouldCreateDefaultClient() { // Act SecretsProvider secretsProvider = ParamManager.getSecretsProvider(); // Assert assertNotNull(secretsProvider); + assertNotNull(secretsProvider.getClient()); } @Test - public void testGetSSMProvider() { + 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)); + } } From f19e202d21239be4a3a5b3ba72424d345dc133ad Mon Sep 17 00:00:00 2001 From: Mark Sailes <45629314+msailes@users.noreply.github.com> Date: Thu, 13 Jul 2023 21:56:38 +0100 Subject: [PATCH 17/55] docs: adding our customer references (#1287) --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index d1f635a9c..d6312d348 100644 --- a/README.md +++ b/README.md @@ -172,6 +172,18 @@ See the **[examples](examples)** directory for example projects showcasing usag Have a demo project to contribute which showcase usage of different utilities from powertools? We are happy to accept it [here](CONTRIBUTING.md#security-issue-notifications). +## How to support Powertools for AWS Lambda (Java)? + +### Becoming a reference customer + +Knowing which companies are using this library is important to help prioritize the project internally. If your company is using Powertools for AWS Lambda (Java), you can request to have your name and logo added to the README file by raising a [Support Powertools for AWS Lambda (Java) (become a reference)](https://github.com/aws-powertools/powertools-lambda-java/issues/new?assignees=&labels=customer-reference&template=support_powertools.yml&title=%5BSupport+Lambda+Powertools%5D%3A+%3Cyour+organization+name%3E) issue. + +The following companies, among others, use Powertools: + +* [Capital One](https://www.capitalone.com/) +* [CPQi (Exadel Financial Services)](https://cpqi.com/) +* [Europace AG](https://europace.de/) + ## Credits * [MkDocs](https://www.mkdocs.org/) From a7e3782d31715134153ea88b0da827efc1c83cdc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Jul 2023 13:31:31 +0200 Subject: [PATCH 18/55] build(deps): bump aws.sdk.version from 2.20.102 to 2.20.103 (#1288) Bumps `aws.sdk.version` from 2.20.102 to 2.20.103. Updates `software.amazon.awssdk:bom` from 2.20.102 to 2.20.103 Updates `http-client-spi` from 2.20.102 to 2.20.103 Updates `url-connection-client` from 2.20.102 to 2.20.103 Updates `s3` from 2.20.102 to 2.20.103 Updates `lambda` from 2.20.102 to 2.20.103 Updates `cloudwatch` from 2.20.102 to 2.20.103 Updates `xray` from 2.20.102 to 2.20.103 Updates `cloudformation` from 2.20.102 to 2.20.103 Updates `sts` from 2.20.102 to 2.20.103 --- updated-dependencies: - dependency-name: software.amazon.awssdk:bom dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:http-client-spi dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:url-connection-client dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:s3 dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:lambda dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:cloudwatch dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:xray dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:cloudformation dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:sts dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/powertools-examples-sqs/pom.xml | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/powertools-examples-sqs/pom.xml b/examples/powertools-examples-sqs/pom.xml index 7b22ab33e..c6de62914 100644 --- a/examples/powertools-examples-sqs/pom.xml +++ b/examples/powertools-examples-sqs/pom.xml @@ -28,7 +28,7 @@ software.amazon.awssdk url-connection-client - 2.20.102 + 2.20.103 com.amazonaws diff --git a/pom.xml b/pom.xml index 916bc7fe2..2e68a99ec 100644 --- a/pom.xml +++ b/pom.xml @@ -60,7 +60,7 @@ 2.20.0 2.15.2 1.9.7 - 2.20.102 + 2.20.103 2.14.0 2.1.3 UTF-8 From ba190dcbf7814fb0719d3541f466257529cb2e8e Mon Sep 17 00:00:00 2001 From: Scott Gerring Date: Mon, 17 Jul 2023 10:31:44 +0200 Subject: [PATCH 19/55] fix: idempotency timeout bug (#1285) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Idemp should be fixed, test isnt * Fix some of the tests. Need to go through the rest still * Add some docs * Update powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/DataRecord.java Co-authored-by: Jérôme Van Der Linden <117538+jeromevdl@users.noreply.github.com> * Address review comment --------- Co-authored-by: Jérôme Van Der Linden <117538+jeromevdl@users.noreply.github.com> --- .../idempotency/persistence/DataRecord.java | 21 +++++++++++++++++++ .../persistence/DynamoDBPersistenceStore.java | 16 +++++++++++++- .../DynamoDBPersistenceStoreTest.java | 12 +++++------ 3 files changed, 42 insertions(+), 7 deletions(-) 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..54001c449 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 @@ -26,9 +26,30 @@ 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) { 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..6b5d0fcb2 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 @@ -126,6 +126,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())); @@ -152,6 +165,7 @@ public void putRecord(DataRecord record, Instant now) throws IdempotencyItemAlre Map expressionAttributeValues = Stream.of( 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,7 +174,7 @@ 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() 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..768da2eaa 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 @@ -95,7 +95,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()); @@ -152,14 +152,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,10 +172,10 @@ 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(); From 903d08b09e7b65316a073b5f5d8585e4adb36fb1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Jul 2023 14:01:59 +0200 Subject: [PATCH 20/55] build(deps): bump aws.sdk.version from 2.20.103 to 2.20.104 (#1292) Bumps `aws.sdk.version` from 2.20.103 to 2.20.104. Updates `software.amazon.awssdk:bom` from 2.20.103 to 2.20.104 Updates `http-client-spi` from 2.20.103 to 2.20.104 Updates `url-connection-client` from 2.20.103 to 2.20.104 Updates `s3` from 2.20.103 to 2.20.104 Updates `lambda` from 2.20.103 to 2.20.104 Updates `cloudwatch` from 2.20.103 to 2.20.104 Updates `xray` from 2.20.103 to 2.20.104 Updates `cloudformation` from 2.20.103 to 2.20.104 Updates `sts` from 2.20.103 to 2.20.104 --- updated-dependencies: - dependency-name: software.amazon.awssdk:bom dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:http-client-spi dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:url-connection-client dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:s3 dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:lambda dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:cloudwatch dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:xray dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:cloudformation dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:sts dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/powertools-examples-sqs/pom.xml | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/powertools-examples-sqs/pom.xml b/examples/powertools-examples-sqs/pom.xml index c6de62914..c5b811678 100644 --- a/examples/powertools-examples-sqs/pom.xml +++ b/examples/powertools-examples-sqs/pom.xml @@ -28,7 +28,7 @@ software.amazon.awssdk url-connection-client - 2.20.103 + 2.20.104 com.amazonaws diff --git a/pom.xml b/pom.xml index 2e68a99ec..f0c671883 100644 --- a/pom.xml +++ b/pom.xml @@ -60,7 +60,7 @@ 2.20.0 2.15.2 1.9.7 - 2.20.103 + 2.20.104 2.14.0 2.1.3 UTF-8 From 5b1c2c43d60b637b26d469ad678de1e3c907b03e Mon Sep 17 00:00:00 2001 From: Scott Gerring Date: Wed, 19 Jul 2023 09:49:34 +0200 Subject: [PATCH 21/55] docs: update README.md (#1294) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update README.md * Update README.md Co-authored-by: Jérôme Van Der Linden <117538+jeromevdl@users.noreply.github.com> --------- Co-authored-by: Jérôme Van Der Linden <117538+jeromevdl@users.noreply.github.com> --- README.md | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index d6312d348..0a7cd0112 100644 --- a/README.md +++ b/README.md @@ -36,9 +36,11 @@ Powertools for AWS Lambda (Java) is available in Maven Central. You can use your ``` -And configure the aspectj-maven-plugin to compile-time weave (CTW) the aws-lambda-powertools-java aspects into your project: +Next, configure the aspectj-maven-plugin to compile-time weave (CTW) the aws-lambda-powertools-java aspects into your project. A different configuration is needed for projects on Java 8. -For Java 11+, use the following: +
+ Maven - Java 11 and newer + ```xml @@ -78,8 +80,11 @@ For Java 11+, use the following: ``` +
-For Java 8, use the following: +
+Maven - Java 8 + ```xml @@ -119,11 +124,12 @@ For Java 8, use the following: ``` -#### gradle +
-For Java 11+: +
+Gradle - Java 11+ - ```groovy +```groovy plugins { id 'java' id 'io.freefair.aspectj.post-compile-weaving' version '8.1.0' @@ -141,11 +147,13 @@ For Java 11+: sourceCompatibility = 11 targetCompatibility = 11 - ``` +``` +
-For Java8: +
+Gradle - Java 8 - ```groovy +```groovy plugins { id 'java' id 'io.freefair.aspectj.post-compile-weaving' version '6.6.3' @@ -163,10 +171,10 @@ For Java8: sourceCompatibility = 1.8 targetCompatibility = 1.8 - ``` - +``` +
-## Example +## Examples See the **[examples](examples)** directory for example projects showcasing usage of different utilities. From 8da24cb175815418b0a4906c1ef9785b01394210 Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Wed, 19 Jul 2023 09:50:15 +0200 Subject: [PATCH 22/55] build(examples): Add example for the powertools-cloudformation module (#1089) --- examples/pom.xml | 1 + .../README.md | 40 ++++ .../infra/cdk/.gitignore | 13 ++ .../infra/cdk/cdk.json | 37 +++ .../infra/cdk/pom.xml | 53 +++++ ...owertoolsExamplesCloudformationCdkApp.java | 16 ++ ...ertoolsExamplesCloudformationCdkStack.java | 89 +++++++ .../infra/sam/events/create_event.json | 12 + .../infra/sam/events/delete_event.json | 14 ++ .../infra/sam/events/update_event.json | 14 ++ .../infra/sam/template.yaml | 50 ++++ .../pom.xml | 219 ++++++++++++++++++ .../src/main/java/helloworld/App.java | 164 +++++++++++++ .../src/main/resources/log4j2.xml | 17 ++ 14 files changed, 739 insertions(+) create mode 100644 examples/powertools-examples-cloudformation/README.md create mode 100644 examples/powertools-examples-cloudformation/infra/cdk/.gitignore create mode 100644 examples/powertools-examples-cloudformation/infra/cdk/cdk.json create mode 100644 examples/powertools-examples-cloudformation/infra/cdk/pom.xml create mode 100644 examples/powertools-examples-cloudformation/infra/cdk/src/main/java/com/myorg/PowertoolsExamplesCloudformationCdkApp.java create mode 100644 examples/powertools-examples-cloudformation/infra/cdk/src/main/java/com/myorg/PowertoolsExamplesCloudformationCdkStack.java create mode 100644 examples/powertools-examples-cloudformation/infra/sam/events/create_event.json create mode 100644 examples/powertools-examples-cloudformation/infra/sam/events/delete_event.json create mode 100644 examples/powertools-examples-cloudformation/infra/sam/events/update_event.json create mode 100644 examples/powertools-examples-cloudformation/infra/sam/template.yaml create mode 100644 examples/powertools-examples-cloudformation/pom.xml create mode 100644 examples/powertools-examples-cloudformation/src/main/java/helloworld/App.java create mode 100644 examples/powertools-examples-cloudformation/src/main/resources/log4j2.xml diff --git a/examples/pom.xml b/examples/pom.xml index cca621163..c9b8ea8ae 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -21,6 +21,7 @@ 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..6dbffcf37 --- /dev/null +++ b/examples/powertools-examples-cloudformation/README.md @@ -0,0 +1,40 @@ +# 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 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) +* 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/sam +sam build +sam deploy --guided --parameter-overrides BucketNameParam=my-unique-bucket-20230717 +``` + +### 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..84060171b --- /dev/null +++ b/examples/powertools-examples-cloudformation/infra/cdk/src/main/java/com/myorg/PowertoolsExamplesCloudformationCdkApp.java @@ -0,0 +1,16 @@ +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-cloudformation/pom.xml b/examples/powertools-examples-cloudformation/pom.xml new file mode 100644 index 000000000..198a85894 --- /dev/null +++ b/examples/powertools-examples-cloudformation/pom.xml @@ -0,0 +1,219 @@ + + 4.0.0 + + software.amazon.lambda.examples + 1.17.0-SNAPSHOT + powertools-examples-cloudformation + jar + + AWS Lambda Powertools for Java library Examples - CloudFormation + + + 2.20.0 + 1.8 + 1.8 + true + 1.2.2 + 3.11.2 + 2.20.102 + + + + + software.amazon.awssdk + bom + ${aws.sdk.version} + pom + import + + + + + + + com.amazonaws + aws-lambda-java-core + ${lambda.core.version} + + + com.amazonaws + aws-lambda-java-events + ${lambda.events.version} + + + software.amazon.lambda + powertools-cloudformation + ${project.version} + + + software.amazon.lambda + powertools-logging + ${project.version} + + + org.apache.logging.log4j + log4j-core + ${log4j.version} + + + org.apache.logging.log4j + log4j-api + ${log4j.version} + + + software.amazon.awssdk + s3 + + + software.amazon.awssdk + netty-nio-client + + + software.amazon.awssdk + apache-client + + + + + software.amazon.awssdk + apache-client + + + commons-logging + commons-logging + + + + + org.apache.logging.log4j + log4j-jcl + ${log4j.version} + + + + + + + + + + dev.aspectj + aspectj-maven-plugin + 1.13.1 + + ${maven.compiler.source} + ${maven.compiler.target} + ${maven.compiler.target} + + + software.amazon.lambda + powertools-logging + + + + + + + compile + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.0 + + + package + + shade + + + + + + + + + + + + com.github.edwgiz + maven-shade-plugin.log4j2-cachefile-transformer + 2.15 + + + + + + + + + jdk8 + + (,11) + + + 1.9.7 + + + + + org.aspectj + aspectjtools + ${aspectj.version} + + + + + + + + dev.aspectj + aspectj-maven-plugin + ${aspectj.plugin.version} + + ${maven.compiler.source} + ${maven.compiler.target} + ${maven.compiler.target} + + + software.amazon.lambda + powertools-logging + + + + + + + compile + test-compile + + + + + + + org.aspectj + aspectjtools + ${aspectj.version} + + + + + + + + + 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..c7744cd5a --- /dev/null +++ b/examples/powertools-examples-cloudformation/src/main/java/helloworld/App.java @@ -0,0 +1,164 @@ +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 From 39ba43b9fd65bcf12eae771ebbcdd5669a55a20c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Van=20Der=20Linden?= <117538+jeromevdl@users.noreply.github.com> Date: Wed, 19 Jul 2023 13:07:25 +0200 Subject: [PATCH 23/55] chore:Prep release 1.16.1 (#1296) --- CHANGELOG.md | 8 ++++++++ README.md | 6 +++--- examples/pom.xml | 2 +- examples/powertools-examples-cloudformation/pom.xml | 2 +- examples/powertools-examples-core/pom.xml | 2 +- examples/powertools-examples-idempotency/pom.xml | 2 +- examples/powertools-examples-parameters/pom.xml | 2 +- examples/powertools-examples-serialization/pom.xml | 2 +- examples/powertools-examples-sqs/pom.xml | 2 +- examples/powertools-examples-validation/pom.xml | 2 +- mkdocs.yml | 2 +- pom.xml | 2 +- powertools-cloudformation/pom.xml | 2 +- powertools-core/pom.xml | 2 +- powertools-e2e-tests/handlers/pom.xml | 2 +- powertools-e2e-tests/pom.xml | 2 +- powertools-idempotency/pom.xml | 2 +- powertools-logging/pom.xml | 2 +- powertools-metrics/pom.xml | 2 +- powertools-parameters/pom.xml | 2 +- powertools-serialization/pom.xml | 2 +- powertools-sqs/pom.xml | 2 +- powertools-test-suite/pom.xml | 2 +- powertools-tracing/pom.xml | 2 +- powertools-validation/pom.xml | 2 +- 25 files changed, 34 insertions(+), 26 deletions(-) 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/README.md b/README.md index 0a7cd0112..a841e7df7 100644 --- a/README.md +++ b/README.md @@ -20,17 +20,17 @@ Powertools for AWS Lambda (Java) is available in Maven Central. You can use your software.amazon.lambda powertools-tracing - 1.16.0 + 1.16.1 software.amazon.lambda powertools-logging - 1.16.0 + 1.16.1 software.amazon.lambda powertools-metrics - 1.16.0 + 1.16.1 ... diff --git a/examples/pom.xml b/examples/pom.xml index c9b8ea8ae..9ea82d99a 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -6,7 +6,7 @@ software.amazon.lambda powertools-examples - 1.17.0-SNAPSHOT + 1.16.1 pom Powertools for AWS Lambda (Java) library Examples diff --git a/examples/powertools-examples-cloudformation/pom.xml b/examples/powertools-examples-cloudformation/pom.xml index 198a85894..3fd582e32 100644 --- a/examples/powertools-examples-cloudformation/pom.xml +++ b/examples/powertools-examples-cloudformation/pom.xml @@ -3,7 +3,7 @@ 4.0.0 software.amazon.lambda.examples - 1.17.0-SNAPSHOT + 1.16.1 powertools-examples-cloudformation jar diff --git a/examples/powertools-examples-core/pom.xml b/examples/powertools-examples-core/pom.xml index a5d854d6b..e956c74bc 100644 --- a/examples/powertools-examples-core/pom.xml +++ b/examples/powertools-examples-core/pom.xml @@ -3,7 +3,7 @@ 4.0.0 software.amazon.lambda.examples - 1.17.0-SNAPSHOT + 1.16.1 powertools-examples-core jar diff --git a/examples/powertools-examples-idempotency/pom.xml b/examples/powertools-examples-idempotency/pom.xml index 0607fdd14..a5e779c2d 100644 --- a/examples/powertools-examples-idempotency/pom.xml +++ b/examples/powertools-examples-idempotency/pom.xml @@ -3,7 +3,7 @@ 4.0.0 software.amazon.lambda.examples - 1.17.0-SNAPSHOT + 1.16.1 powertools-examples-idempotency jar Powertools for AWS Lambda (Java) library Examples - Idempotency diff --git a/examples/powertools-examples-parameters/pom.xml b/examples/powertools-examples-parameters/pom.xml index c4631fd05..2104b151f 100644 --- a/examples/powertools-examples-parameters/pom.xml +++ b/examples/powertools-examples-parameters/pom.xml @@ -2,7 +2,7 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 software.amazon.lambda.examples - 1.17.0-SNAPSHOT + 1.16.1 powertools-examples-parameters jar Powertools for AWS Lambda (Java) library Examples - Parameters diff --git a/examples/powertools-examples-serialization/pom.xml b/examples/powertools-examples-serialization/pom.xml index 4974842dd..82a8af5ba 100644 --- a/examples/powertools-examples-serialization/pom.xml +++ b/examples/powertools-examples-serialization/pom.xml @@ -2,7 +2,7 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 software.amazon.lambda.examples - 1.17.0-SNAPSHOT + 1.16.1 powertools-examples-serialization jar Powertools for AWS Lambda (Java) library Examples - Serialization diff --git a/examples/powertools-examples-sqs/pom.xml b/examples/powertools-examples-sqs/pom.xml index c5b811678..ea3842de0 100644 --- a/examples/powertools-examples-sqs/pom.xml +++ b/examples/powertools-examples-sqs/pom.xml @@ -2,7 +2,7 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 software.amazon.lambda.examples - 1.17.0-SNAPSHOT + 1.16.1 powertools-examples-sqs jar Powertools for AWS Lambda (Java) library Examples - SQS diff --git a/examples/powertools-examples-validation/pom.xml b/examples/powertools-examples-validation/pom.xml index 455fd66b8..89f823a55 100644 --- a/examples/powertools-examples-validation/pom.xml +++ b/examples/powertools-examples-validation/pom.xml @@ -2,7 +2,7 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 software.amazon.lambda.examples - 1.17.0-SNAPSHOT + 1.16.1 powertools-examples-validation jar Powertools for AWS Lambda (Java) library Examples - Validation diff --git a/mkdocs.yml b/mkdocs.yml index 5fd3206b0..da00b24d1 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -83,7 +83,7 @@ extra_javascript: extra: powertools: - version: 1.16.0 # to update after each release (we do not want snapshot version here) + version: 1.16.1 # to update after each release (we do not want snapshot version here) repo_url: https://github.com/aws-powertools/powertools-lambda-java edit_uri: edit/main/docs diff --git a/pom.xml b/pom.xml index f0c671883..152035061 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ software.amazon.lambda powertools-parent - 1.17.0-SNAPSHOT + 1.16.1 pom Powertools for AWS Lambda (Java) library Parent diff --git a/powertools-cloudformation/pom.xml b/powertools-cloudformation/pom.xml index 3a846c378..0d1a3e45b 100644 --- a/powertools-cloudformation/pom.xml +++ b/powertools-cloudformation/pom.xml @@ -10,7 +10,7 @@ powertools-parent software.amazon.lambda - 1.17.0-SNAPSHOT + 1.16.1 Powertools for AWS Lambda (Java)library Cloudformation diff --git a/powertools-core/pom.xml b/powertools-core/pom.xml index cf9ad45d1..63867e8d6 100644 --- a/powertools-core/pom.xml +++ b/powertools-core/pom.xml @@ -10,7 +10,7 @@ powertools-parent software.amazon.lambda - 1.17.0-SNAPSHOT + 1.16.1 Powertools for AWS Lambda (Java) library Core diff --git a/powertools-e2e-tests/handlers/pom.xml b/powertools-e2e-tests/handlers/pom.xml index ae26364dd..d08ede7e9 100644 --- a/powertools-e2e-tests/handlers/pom.xml +++ b/powertools-e2e-tests/handlers/pom.xml @@ -10,7 +10,7 @@ Fake handlers that use Powertools for AWS Lambda (Java). - 1.17.0-SNAPSHOT + 1.16.1 UTF-8 1.8 1.8 diff --git a/powertools-e2e-tests/pom.xml b/powertools-e2e-tests/pom.xml index 8754117db..27782d4c5 100644 --- a/powertools-e2e-tests/pom.xml +++ b/powertools-e2e-tests/pom.xml @@ -6,7 +6,7 @@ powertools-parent software.amazon.lambda - 1.17.0-SNAPSHOT + 1.16.1 powertools-e2e-tests diff --git a/powertools-idempotency/pom.xml b/powertools-idempotency/pom.xml index cb4d9b802..f8856b00d 100644 --- a/powertools-idempotency/pom.xml +++ b/powertools-idempotency/pom.xml @@ -7,7 +7,7 @@ software.amazon.lambda powertools-parent - 1.17.0-SNAPSHOT + 1.16.1 powertools-idempotency diff --git a/powertools-logging/pom.xml b/powertools-logging/pom.xml index 767fbd3ee..a84de6b2f 100644 --- a/powertools-logging/pom.xml +++ b/powertools-logging/pom.xml @@ -10,7 +10,7 @@ powertools-parent software.amazon.lambda - 1.17.0-SNAPSHOT + 1.16.1 Powertools for AWS Lambda (Java) library Logging diff --git a/powertools-metrics/pom.xml b/powertools-metrics/pom.xml index ed2d2f815..413ac3ba1 100644 --- a/powertools-metrics/pom.xml +++ b/powertools-metrics/pom.xml @@ -10,7 +10,7 @@ powertools-parent software.amazon.lambda - 1.17.0-SNAPSHOT + 1.16.1 Powertools for AWS Lambda (Java) library Metrics diff --git a/powertools-parameters/pom.xml b/powertools-parameters/pom.xml index b50844d99..5013ba4c0 100644 --- a/powertools-parameters/pom.xml +++ b/powertools-parameters/pom.xml @@ -7,7 +7,7 @@ powertools-parent software.amazon.lambda - 1.17.0-SNAPSHOT + 1.16.1 powertools-parameters diff --git a/powertools-serialization/pom.xml b/powertools-serialization/pom.xml index f21ecb412..9481f38fa 100644 --- a/powertools-serialization/pom.xml +++ b/powertools-serialization/pom.xml @@ -7,7 +7,7 @@ powertools-parent software.amazon.lambda - 1.17.0-SNAPSHOT + 1.16.1 powertools-serialization diff --git a/powertools-sqs/pom.xml b/powertools-sqs/pom.xml index 618aa948c..b6ae9da90 100644 --- a/powertools-sqs/pom.xml +++ b/powertools-sqs/pom.xml @@ -10,7 +10,7 @@ powertools-parent software.amazon.lambda - 1.17.0-SNAPSHOT + 1.16.1 Powertools for AWS Lambda (Java) library SQS diff --git a/powertools-test-suite/pom.xml b/powertools-test-suite/pom.xml index 5737296da..bde7b7176 100644 --- a/powertools-test-suite/pom.xml +++ b/powertools-test-suite/pom.xml @@ -10,7 +10,7 @@ powertools-parent software.amazon.lambda - 1.17.0-SNAPSHOT + 1.16.1 Powertools for AWS Lambda (Java) library Test Suite diff --git a/powertools-tracing/pom.xml b/powertools-tracing/pom.xml index 42a229f42..51315d6a0 100644 --- a/powertools-tracing/pom.xml +++ b/powertools-tracing/pom.xml @@ -10,7 +10,7 @@ powertools-parent software.amazon.lambda - 1.17.0-SNAPSHOT + 1.16.1 Powertools for AWS Lambda (Java) library Tracing diff --git a/powertools-validation/pom.xml b/powertools-validation/pom.xml index 1469183ef..293c599f6 100644 --- a/powertools-validation/pom.xml +++ b/powertools-validation/pom.xml @@ -10,7 +10,7 @@ powertools-parent software.amazon.lambda - 1.17.0-SNAPSHOT + 1.16.1 Powertools for AWS Lambda (Java) validation library From b191d72f3572b67e05d0d7a077e47a3c8e969bb4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Jul 2023 13:45:18 +0200 Subject: [PATCH 24/55] build(deps): bump aws.sdk.version from 2.20.102 to 2.20.105 (#1297) Bumps `aws.sdk.version` from 2.20.102 to 2.20.105. Updates `software.amazon.awssdk:bom` from 2.20.102 to 2.20.105 Updates `http-client-spi` from 2.20.104 to 2.20.105 Updates `url-connection-client` from 2.20.104 to 2.20.105 Updates `s3` from 2.20.104 to 2.20.105 Updates `lambda` from 2.20.104 to 2.20.105 Updates `cloudwatch` from 2.20.104 to 2.20.105 Updates `xray` from 2.20.104 to 2.20.105 Updates `cloudformation` from 2.20.104 to 2.20.105 Updates `sts` from 2.20.104 to 2.20.105 --- updated-dependencies: - dependency-name: software.amazon.awssdk:bom dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:http-client-spi dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:url-connection-client dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:s3 dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:lambda dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:cloudwatch dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:xray dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:cloudformation dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:sts dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/powertools-examples-cloudformation/pom.xml | 2 +- examples/powertools-examples-sqs/pom.xml | 2 +- pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/powertools-examples-cloudformation/pom.xml b/examples/powertools-examples-cloudformation/pom.xml index 3fd582e32..0022485ca 100644 --- a/examples/powertools-examples-cloudformation/pom.xml +++ b/examples/powertools-examples-cloudformation/pom.xml @@ -16,7 +16,7 @@ true 1.2.2 3.11.2 - 2.20.102 + 2.20.105 diff --git a/examples/powertools-examples-sqs/pom.xml b/examples/powertools-examples-sqs/pom.xml index ea3842de0..7d42be4df 100644 --- a/examples/powertools-examples-sqs/pom.xml +++ b/examples/powertools-examples-sqs/pom.xml @@ -28,7 +28,7 @@ software.amazon.awssdk url-connection-client - 2.20.104 + 2.20.105 com.amazonaws diff --git a/pom.xml b/pom.xml index 152035061..69db43a9c 100644 --- a/pom.xml +++ b/pom.xml @@ -60,7 +60,7 @@ 2.20.0 2.15.2 1.9.7 - 2.20.104 + 2.20.105 2.14.0 2.1.3 UTF-8 From c185cb063a4612f704d092fb21b2493706baa193 Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Wed, 19 Jul 2023 15:26:55 +0200 Subject: [PATCH 25/55] chore: update poms to SNAPSHOT version for dev (#1299) --- examples/pom.xml | 2 +- examples/powertools-examples-cloudformation/pom.xml | 2 +- examples/powertools-examples-core/pom.xml | 2 +- examples/powertools-examples-idempotency/pom.xml | 2 +- examples/powertools-examples-parameters/pom.xml | 2 +- examples/powertools-examples-serialization/pom.xml | 2 +- examples/powertools-examples-sqs/pom.xml | 2 +- examples/powertools-examples-validation/pom.xml | 2 +- pom.xml | 2 +- powertools-cloudformation/pom.xml | 2 +- powertools-core/pom.xml | 2 +- powertools-e2e-tests/handlers/pom.xml | 2 +- powertools-e2e-tests/pom.xml | 2 +- powertools-idempotency/pom.xml | 2 +- powertools-logging/pom.xml | 2 +- powertools-metrics/pom.xml | 2 +- powertools-parameters/pom.xml | 2 +- powertools-serialization/pom.xml | 2 +- powertools-sqs/pom.xml | 2 +- powertools-test-suite/pom.xml | 2 +- powertools-tracing/pom.xml | 2 +- powertools-validation/pom.xml | 2 +- 22 files changed, 22 insertions(+), 22 deletions(-) diff --git a/examples/pom.xml b/examples/pom.xml index 9ea82d99a..c9b8ea8ae 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -6,7 +6,7 @@ software.amazon.lambda powertools-examples - 1.16.1 + 1.17.0-SNAPSHOT pom Powertools for AWS Lambda (Java) library Examples diff --git a/examples/powertools-examples-cloudformation/pom.xml b/examples/powertools-examples-cloudformation/pom.xml index 0022485ca..c46bed1de 100644 --- a/examples/powertools-examples-cloudformation/pom.xml +++ b/examples/powertools-examples-cloudformation/pom.xml @@ -3,7 +3,7 @@ 4.0.0 software.amazon.lambda.examples - 1.16.1 + 1.17.0-SNAPSHOT powertools-examples-cloudformation jar diff --git a/examples/powertools-examples-core/pom.xml b/examples/powertools-examples-core/pom.xml index e956c74bc..a5d854d6b 100644 --- a/examples/powertools-examples-core/pom.xml +++ b/examples/powertools-examples-core/pom.xml @@ -3,7 +3,7 @@ 4.0.0 software.amazon.lambda.examples - 1.16.1 + 1.17.0-SNAPSHOT powertools-examples-core jar diff --git a/examples/powertools-examples-idempotency/pom.xml b/examples/powertools-examples-idempotency/pom.xml index a5e779c2d..0607fdd14 100644 --- a/examples/powertools-examples-idempotency/pom.xml +++ b/examples/powertools-examples-idempotency/pom.xml @@ -3,7 +3,7 @@ 4.0.0 software.amazon.lambda.examples - 1.16.1 + 1.17.0-SNAPSHOT powertools-examples-idempotency jar Powertools for AWS Lambda (Java) library Examples - Idempotency diff --git a/examples/powertools-examples-parameters/pom.xml b/examples/powertools-examples-parameters/pom.xml index 2104b151f..c4631fd05 100644 --- a/examples/powertools-examples-parameters/pom.xml +++ b/examples/powertools-examples-parameters/pom.xml @@ -2,7 +2,7 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 software.amazon.lambda.examples - 1.16.1 + 1.17.0-SNAPSHOT powertools-examples-parameters jar Powertools for AWS Lambda (Java) library Examples - Parameters diff --git a/examples/powertools-examples-serialization/pom.xml b/examples/powertools-examples-serialization/pom.xml index 82a8af5ba..4974842dd 100644 --- a/examples/powertools-examples-serialization/pom.xml +++ b/examples/powertools-examples-serialization/pom.xml @@ -2,7 +2,7 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 software.amazon.lambda.examples - 1.16.1 + 1.17.0-SNAPSHOT powertools-examples-serialization jar Powertools for AWS Lambda (Java) library Examples - Serialization diff --git a/examples/powertools-examples-sqs/pom.xml b/examples/powertools-examples-sqs/pom.xml index 7d42be4df..d66f6dda5 100644 --- a/examples/powertools-examples-sqs/pom.xml +++ b/examples/powertools-examples-sqs/pom.xml @@ -2,7 +2,7 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 software.amazon.lambda.examples - 1.16.1 + 1.17.0-SNAPSHOT powertools-examples-sqs jar Powertools for AWS Lambda (Java) library Examples - SQS diff --git a/examples/powertools-examples-validation/pom.xml b/examples/powertools-examples-validation/pom.xml index 89f823a55..455fd66b8 100644 --- a/examples/powertools-examples-validation/pom.xml +++ b/examples/powertools-examples-validation/pom.xml @@ -2,7 +2,7 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 software.amazon.lambda.examples - 1.16.1 + 1.17.0-SNAPSHOT powertools-examples-validation jar Powertools for AWS Lambda (Java) library Examples - Validation diff --git a/pom.xml b/pom.xml index 69db43a9c..462ad1164 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ software.amazon.lambda powertools-parent - 1.16.1 + 1.17.0-SNAPSHOT pom Powertools for AWS Lambda (Java) library Parent diff --git a/powertools-cloudformation/pom.xml b/powertools-cloudformation/pom.xml index 0d1a3e45b..3a846c378 100644 --- a/powertools-cloudformation/pom.xml +++ b/powertools-cloudformation/pom.xml @@ -10,7 +10,7 @@ powertools-parent software.amazon.lambda - 1.16.1 + 1.17.0-SNAPSHOT Powertools for AWS Lambda (Java)library Cloudformation diff --git a/powertools-core/pom.xml b/powertools-core/pom.xml index 63867e8d6..cf9ad45d1 100644 --- a/powertools-core/pom.xml +++ b/powertools-core/pom.xml @@ -10,7 +10,7 @@ powertools-parent software.amazon.lambda - 1.16.1 + 1.17.0-SNAPSHOT Powertools for AWS Lambda (Java) library Core diff --git a/powertools-e2e-tests/handlers/pom.xml b/powertools-e2e-tests/handlers/pom.xml index d08ede7e9..ae26364dd 100644 --- a/powertools-e2e-tests/handlers/pom.xml +++ b/powertools-e2e-tests/handlers/pom.xml @@ -10,7 +10,7 @@ Fake handlers that use Powertools for AWS Lambda (Java). - 1.16.1 + 1.17.0-SNAPSHOT UTF-8 1.8 1.8 diff --git a/powertools-e2e-tests/pom.xml b/powertools-e2e-tests/pom.xml index 27782d4c5..8754117db 100644 --- a/powertools-e2e-tests/pom.xml +++ b/powertools-e2e-tests/pom.xml @@ -6,7 +6,7 @@ powertools-parent software.amazon.lambda - 1.16.1 + 1.17.0-SNAPSHOT powertools-e2e-tests diff --git a/powertools-idempotency/pom.xml b/powertools-idempotency/pom.xml index f8856b00d..cb4d9b802 100644 --- a/powertools-idempotency/pom.xml +++ b/powertools-idempotency/pom.xml @@ -7,7 +7,7 @@ software.amazon.lambda powertools-parent - 1.16.1 + 1.17.0-SNAPSHOT powertools-idempotency diff --git a/powertools-logging/pom.xml b/powertools-logging/pom.xml index a84de6b2f..767fbd3ee 100644 --- a/powertools-logging/pom.xml +++ b/powertools-logging/pom.xml @@ -10,7 +10,7 @@ powertools-parent software.amazon.lambda - 1.16.1 + 1.17.0-SNAPSHOT Powertools for AWS Lambda (Java) library Logging diff --git a/powertools-metrics/pom.xml b/powertools-metrics/pom.xml index 413ac3ba1..ed2d2f815 100644 --- a/powertools-metrics/pom.xml +++ b/powertools-metrics/pom.xml @@ -10,7 +10,7 @@ powertools-parent software.amazon.lambda - 1.16.1 + 1.17.0-SNAPSHOT Powertools for AWS Lambda (Java) library Metrics diff --git a/powertools-parameters/pom.xml b/powertools-parameters/pom.xml index 5013ba4c0..b50844d99 100644 --- a/powertools-parameters/pom.xml +++ b/powertools-parameters/pom.xml @@ -7,7 +7,7 @@ powertools-parent software.amazon.lambda - 1.16.1 + 1.17.0-SNAPSHOT powertools-parameters diff --git a/powertools-serialization/pom.xml b/powertools-serialization/pom.xml index 9481f38fa..f21ecb412 100644 --- a/powertools-serialization/pom.xml +++ b/powertools-serialization/pom.xml @@ -7,7 +7,7 @@ powertools-parent software.amazon.lambda - 1.16.1 + 1.17.0-SNAPSHOT powertools-serialization diff --git a/powertools-sqs/pom.xml b/powertools-sqs/pom.xml index b6ae9da90..618aa948c 100644 --- a/powertools-sqs/pom.xml +++ b/powertools-sqs/pom.xml @@ -10,7 +10,7 @@ powertools-parent software.amazon.lambda - 1.16.1 + 1.17.0-SNAPSHOT Powertools for AWS Lambda (Java) library SQS diff --git a/powertools-test-suite/pom.xml b/powertools-test-suite/pom.xml index bde7b7176..5737296da 100644 --- a/powertools-test-suite/pom.xml +++ b/powertools-test-suite/pom.xml @@ -10,7 +10,7 @@ powertools-parent software.amazon.lambda - 1.16.1 + 1.17.0-SNAPSHOT Powertools for AWS Lambda (Java) library Test Suite diff --git a/powertools-tracing/pom.xml b/powertools-tracing/pom.xml index 51315d6a0..42a229f42 100644 --- a/powertools-tracing/pom.xml +++ b/powertools-tracing/pom.xml @@ -10,7 +10,7 @@ powertools-parent software.amazon.lambda - 1.16.1 + 1.17.0-SNAPSHOT Powertools for AWS Lambda (Java) library Tracing diff --git a/powertools-validation/pom.xml b/powertools-validation/pom.xml index 293c599f6..1469183ef 100644 --- a/powertools-validation/pom.xml +++ b/powertools-validation/pom.xml @@ -10,7 +10,7 @@ powertools-parent software.amazon.lambda - 1.16.1 + 1.17.0-SNAPSHOT Powertools for AWS Lambda (Java) validation library From 0ac8127d098272360105063c3646277220dfae50 Mon Sep 17 00:00:00 2001 From: Scott Gerring Date: Thu, 20 Jul 2023 11:52:30 +0200 Subject: [PATCH 26/55] docs: Started cleaning up example doc (#1291) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Started cleaning up example doc * More work * More docs * More docs * More docs * Clean up SQS * Did validationg * Consistency * Add CF * More cleanp * Add magic back * Update examples/README.md Co-authored-by: Jérôme Van Der Linden <117538+jeromevdl@users.noreply.github.com> * Fix naming * fix syntax --------- Co-authored-by: Jérôme Van Der Linden <117538+jeromevdl@users.noreply.github.com> --- docs/index.md | 2 +- examples/README.md | 62 ++++++- .../README.md | 17 +- examples/powertools-examples-core/README.md | 151 ++++-------------- .../powertools-examples-idempotency/README.md | 25 +-- .../src/main/java/helloworld/App.java | 23 ++- .../powertools-examples-parameters/README.md | 37 +++-- .../template.yaml | 3 + .../README.md | 76 +++++++-- .../template.yaml | 2 +- examples/powertools-examples-sqs/README.md | 57 ++++++- .../powertools-examples-validation/README.md | 27 ++-- 12 files changed, 293 insertions(+), 189 deletions(-) diff --git a/docs/index.md b/docs/index.md index f00e0314d..d3e487174 100644 --- a/docs/index.md +++ b/docs/index.md @@ -253,7 +253,7 @@ Depending on your version of Java (either Java 1.8 or 11+), the configuration sl ``` ???+ tip "Why a different configuration?" - Lambda Powertools for Java is using [AspectJ](https://eclipse.dev/aspectj/doc/released/progguide/starting.html) internally + 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, diff --git a/examples/README.md b/examples/README.md index f7e6fc620..1869b4e8f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,4 +1,62 @@ -## 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. + +## The 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. + +### 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/powertools-examples-cloudformation/README.md b/examples/powertools-examples-cloudformation/README.md index 6dbffcf37..4b53ff0aa 100644 --- a/examples/powertools-examples-cloudformation/README.md +++ b/examples/powertools-examples-cloudformation/README.md @@ -1,4 +1,4 @@ -# Cloudformation Custom Resource Example +# 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/). @@ -7,20 +7,7 @@ This project contains an example of Lambda function using the CloudFormation mod This sample can be used either with the Serverless Application Model (SAM) or with CDK. ### Deploy with SAM CLI -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) -* 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/sam -sam build -sam deploy --guided --parameter-overrides BucketNameParam=my-unique-bucket-20230717 -``` +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. 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-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/src/main/java/helloworld/App.java b/examples/powertools-examples-idempotency/src/main/java/helloworld/App.java index f26877c34..0a20aa3a4 100644 --- a/examples/powertools-examples-idempotency/src/main/java/helloworld/App.java +++ b/examples/powertools-examples-idempotency/src/main/java/helloworld/App.java @@ -43,7 +43,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 +56,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 +69,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 +86,18 @@ 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-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/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/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 index 2b6da65b5..45f4a4a74 100644 --- a/examples/powertools-examples-sqs/README.md +++ b/examples/powertools-examples-sqs/README.md @@ -1,3 +1,56 @@ -## SqsBatchProcessingDemo +# Powertools for AWS Lambda (Java) - SQS Batch Processing Example -Demos setup of SQS Batch processing via Powertools +This project contains an example of Lambda function using the batch processing 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/batch/). + +The project contains two functions: + +* [SqsMessageSender](src/main/java/org/demo/sqs/SqsMessageSender.java) - Sends a set of messages to an SQS queue. +This function is triggered every 5 minutes by an EventBridge schedule rule. +* [SqsPoller](src/main/java/org/demo/sqs/SqsPoller.java) - Listens to the same queue, processing items off in batches + +The poller intentionally fails intermittently processing messages to demonstrate the replay behaviour of the batch +module: + +
    + +SqsPoller.java + +[SqsPoller.java:43](src/main/java/org/demo/sqs/SqsPoller.java) + +```java + 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"; + } +``` + +
    + +## Deploy the sample application + +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 + +As the test is pushing through a batch every 5 minutes, we can simply watch the logs to see the batches being processed: + +```bash + sam logs --tail --stack-name $MY_STACK +``` + +As the handler intentionally introduces intermittent failures, we should expect to see error messages too! 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 ``` From 577d03f38cc8895aa4c10ee61dd5924d92a03fe9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Jul 2023 13:24:05 +0200 Subject: [PATCH 27/55] build(deps): bump aws.sdk.version from 2.20.105 to 2.20.107 (#1300) Bumps `aws.sdk.version` from 2.20.105 to 2.20.107. Updates `software.amazon.awssdk:bom` from 2.20.105 to 2.20.107 Updates `http-client-spi` from 2.20.105 to 2.20.107 Updates `url-connection-client` from 2.20.105 to 2.20.107 Updates `s3` from 2.20.105 to 2.20.107 Updates `lambda` from 2.20.105 to 2.20.107 Updates `cloudwatch` from 2.20.105 to 2.20.107 Updates `xray` from 2.20.105 to 2.20.107 Updates `cloudformation` from 2.20.105 to 2.20.107 Updates `sts` from 2.20.105 to 2.20.107 --- updated-dependencies: - dependency-name: software.amazon.awssdk:bom dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:http-client-spi dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:url-connection-client dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:s3 dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:lambda dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:cloudwatch dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:xray dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:cloudformation dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:sts dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/powertools-examples-cloudformation/pom.xml | 2 +- examples/powertools-examples-sqs/pom.xml | 2 +- pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/powertools-examples-cloudformation/pom.xml b/examples/powertools-examples-cloudformation/pom.xml index c46bed1de..98703c60a 100644 --- a/examples/powertools-examples-cloudformation/pom.xml +++ b/examples/powertools-examples-cloudformation/pom.xml @@ -16,7 +16,7 @@ true 1.2.2 3.11.2 - 2.20.105 + 2.20.107
    diff --git a/examples/powertools-examples-sqs/pom.xml b/examples/powertools-examples-sqs/pom.xml index d66f6dda5..0fbfd284c 100644 --- a/examples/powertools-examples-sqs/pom.xml +++ b/examples/powertools-examples-sqs/pom.xml @@ -28,7 +28,7 @@ software.amazon.awssdk url-connection-client - 2.20.105 + 2.20.107 com.amazonaws diff --git a/pom.xml b/pom.xml index 462ad1164..859b20cf1 100644 --- a/pom.xml +++ b/pom.xml @@ -60,7 +60,7 @@ 2.20.0 2.15.2 1.9.7 - 2.20.105 + 2.20.107 2.14.0 2.1.3 UTF-8 From ac5ce9d89c1e788694c6a01c249787d06b844491 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Jul 2023 13:33:37 +0200 Subject: [PATCH 28/55] build(deps): bump aws.sdk.version from 2.20.107 to 2.20.108 (#1304) Bumps `aws.sdk.version` from 2.20.107 to 2.20.108. Updates `software.amazon.awssdk:bom` from 2.20.107 to 2.20.108 Updates `software.amazon.awssdk:http-client-spi` from 2.20.107 to 2.20.108 Updates `software.amazon.awssdk:url-connection-client` from 2.20.107 to 2.20.108 Updates `software.amazon.awssdk:s3` from 2.20.107 to 2.20.108 Updates `software.amazon.awssdk:lambda` from 2.20.107 to 2.20.108 Updates `software.amazon.awssdk:cloudwatch` from 2.20.107 to 2.20.108 Updates `software.amazon.awssdk:xray` from 2.20.107 to 2.20.108 Updates `software.amazon.awssdk:cloudformation` from 2.20.107 to 2.20.108 Updates `software.amazon.awssdk:sts` from 2.20.107 to 2.20.108 --- updated-dependencies: - dependency-name: software.amazon.awssdk:bom dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:http-client-spi dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:url-connection-client dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:s3 dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:lambda dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:cloudwatch dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:xray dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:cloudformation dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:sts dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/powertools-examples-cloudformation/pom.xml | 2 +- examples/powertools-examples-sqs/pom.xml | 2 +- pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/powertools-examples-cloudformation/pom.xml b/examples/powertools-examples-cloudformation/pom.xml index 98703c60a..c7e35c8d8 100644 --- a/examples/powertools-examples-cloudformation/pom.xml +++ b/examples/powertools-examples-cloudformation/pom.xml @@ -16,7 +16,7 @@ true 1.2.2 3.11.2 - 2.20.107 + 2.20.108 diff --git a/examples/powertools-examples-sqs/pom.xml b/examples/powertools-examples-sqs/pom.xml index 0fbfd284c..71c0eb131 100644 --- a/examples/powertools-examples-sqs/pom.xml +++ b/examples/powertools-examples-sqs/pom.xml @@ -28,7 +28,7 @@ software.amazon.awssdk url-connection-client - 2.20.107 + 2.20.108 com.amazonaws diff --git a/pom.xml b/pom.xml index 859b20cf1..197e84a91 100644 --- a/pom.xml +++ b/pom.xml @@ -60,7 +60,7 @@ 2.20.0 2.15.2 1.9.7 - 2.20.107 + 2.20.108 2.14.0 2.1.3 UTF-8 From 4c4235969ddb33222ab1f18af2c9120b361dca07 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Jul 2023 13:39:08 +0200 Subject: [PATCH 29/55] build(deps-dev): bump software.amazon.awscdk:aws-cdk-lib (#1305) Bumps [software.amazon.awscdk:aws-cdk-lib](https://github.com/aws/aws-cdk) from 2.87.0 to 2.88.0. - [Release notes](https://github.com/aws/aws-cdk/releases) - [Changelog](https://github.com/aws/aws-cdk/blob/main/CHANGELOG.v2.md) - [Commits](https://github.com/aws/aws-cdk/compare/v2.87.0...v2.88.0) --- updated-dependencies: - dependency-name: software.amazon.awscdk:aws-cdk-lib dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- powertools-e2e-tests/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/powertools-e2e-tests/pom.xml b/powertools-e2e-tests/pom.xml index 8754117db..5b6b50511 100644 --- a/powertools-e2e-tests/pom.xml +++ b/powertools-e2e-tests/pom.xml @@ -17,7 +17,7 @@ 1.8 1.8 10.2.69 - 2.87.0 + 2.88.0 true From 3594bbd1d4e0380fb1cd4fd143ac491b00f17181 Mon Sep 17 00:00:00 2001 From: Alexander Sparkowsky Date: Mon, 24 Jul 2023 11:31:37 +0200 Subject: [PATCH 30/55] fix: use default credentials provider for all provided SDK clients (#1303) --------- Co-authored-by: Scott Gerring --- .../core/internal/LambdaConstants.java | 4 ++++ .../persistence/DynamoDBPersistenceStore.java | 19 +++--------------- .../parameters/AppConfigProvider.java | 20 +++---------------- .../parameters/DynamoDbProvider.java | 20 +++---------------- .../powertools/parameters/SSMProvider.java | 20 +++---------------- .../parameters/SecretsProvider.java | 19 +++--------------- 6 files changed, 19 insertions(+), 83 deletions(-) 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 bb5fc4666..ea6a6ff44 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 @@ -16,7 +16,11 @@ 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"; 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 6b5d0fcb2..783b029bb 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 @@ -15,11 +15,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; 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.idempotency.Constants; @@ -34,10 +32,8 @@ 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; /** @@ -88,19 +84,10 @@ private DynamoDBPersistenceStore(String tableName, } else { String idempotencyDisabledEnv = System.getenv().get(Constants.IDEMPOTENCY_DISABLED_ENV); if (idempotencyDisabledEnv == null || idempotencyDisabledEnv.equalsIgnoreCase("false")) { - DynamoDbClientBuilder ddbBuilder = DynamoDbClient.builder() + 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(); + .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 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 e0255125d..c62d7a2e5 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,23 +1,18 @@ package software.amazon.lambda.powertools.parameters; -import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; import software.amazon.awssdk.core.SdkSystemSetting; 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.lambda.powertools.core.internal.LambdaConstants; 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. @@ -144,19 +139,10 @@ 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()))) + .build(); } AppConfigProvider provider = new AppConfigProvider(cacheManager, client, environment, application); 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 1b77aed88..e09f23348 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,17 +1,14 @@ package software.amazon.lambda.powertools.parameters; -import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; import software.amazon.awssdk.core.SdkSystemSetting; 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.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.LambdaConstants; import software.amazon.lambda.powertools.parameters.cache.CacheManager; import software.amazon.lambda.powertools.parameters.exception.DynamoDbProviderSchemaException; import software.amazon.lambda.powertools.parameters.transform.TransformationManager; @@ -20,8 +17,6 @@ import java.util.Map; import java.util.stream.Collectors; -import static software.amazon.lambda.powertools.core.internal.LambdaConstants.AWS_LAMBDA_INITIALIZATION_TYPE; - /** * Implements a {@link ParamProvider} on top of DynamoDB. The schema of the table * is described in the Powertools for AWS Lambda (Java) documentation. @@ -190,19 +185,10 @@ public DynamoDbProvider.Builder withTransformationManager(TransformationManager } private static DynamoDbClient createClient() { - DynamoDbClientBuilder dynamoDbClientBuilder = DynamoDbClient.builder() + return DynamoDbClient.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)) { - dynamoDbClientBuilder.credentialsProvider(EnvironmentVariableCredentialsProvider.create()); - } - - return dynamoDbClientBuilder.build(); + .region(Region.of(System.getenv(SdkSystemSetting.AWS_REGION.environmentVariable()))) + .build(); } } } 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 2eb2d4199..1fa4dbaab 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 @@ -13,17 +13,14 @@ */ package software.amazon.lambda.powertools.parameters; -import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; import software.amazon.awssdk.core.SdkSystemSetting; 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.parameters.cache.CacheManager; import software.amazon.lambda.powertools.parameters.transform.TransformationManager; import software.amazon.lambda.powertools.parameters.transform.Transformer; @@ -32,8 +29,6 @@ import java.util.HashMap; import java.util.Map; -import static software.amazon.lambda.powertools.core.internal.LambdaConstants.AWS_LAMBDA_INITIALIZATION_TYPE; - /** * AWS System Manager Parameter Store Provider

    * @@ -283,19 +278,10 @@ public SSMProvider.Builder withClient(SsmClient client) { } private static SsmClient createClient() { - SsmClientBuilder ssmClientBuilder = SsmClient.builder() + return 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()); - } - - return ssmClientBuilder.build(); + .region(Region.of(System.getenv(SdkSystemSetting.AWS_REGION.environmentVariable()))) + .build(); } /** 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 ea8b5a9d0..fd45da881 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 @@ -13,14 +13,11 @@ */ package software.amazon.lambda.powertools.parameters; -import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; import software.amazon.awssdk.core.SdkSystemSetting; 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.parameters.cache.CacheManager; import software.amazon.lambda.powertools.parameters.transform.TransformationManager; import software.amazon.lambda.powertools.parameters.transform.Transformer; @@ -30,7 +27,6 @@ import java.util.Map; 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

    @@ -191,19 +187,10 @@ public Builder withClient(SecretsManagerClient client) { } private static SecretsManagerClient createClient() { - SecretsManagerClientBuilder secretsManagerClientBuilder = SecretsManagerClient.builder() + return 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()); - } - - return secretsManagerClientBuilder.build(); + .region(Region.of(System.getenv(SdkSystemSetting.AWS_REGION.environmentVariable()))) + .build(); } /** From 4bc526c39b58ed2b591598c1854dbe8c5342f08f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jul 2023 13:45:45 +0200 Subject: [PATCH 31/55] build(deps): bump aws.sdk.version from 2.20.108 to 2.20.109 (#1308) Bumps `aws.sdk.version` from 2.20.108 to 2.20.109. Updates `software.amazon.awssdk:bom` from 2.20.108 to 2.20.109 Updates `software.amazon.awssdk:http-client-spi` from 2.20.108 to 2.20.109 Updates `software.amazon.awssdk:url-connection-client` from 2.20.108 to 2.20.109 Updates `software.amazon.awssdk:s3` from 2.20.108 to 2.20.109 Updates `software.amazon.awssdk:lambda` from 2.20.108 to 2.20.109 Updates `software.amazon.awssdk:cloudwatch` from 2.20.108 to 2.20.109 Updates `software.amazon.awssdk:xray` from 2.20.108 to 2.20.109 Updates `software.amazon.awssdk:cloudformation` from 2.20.108 to 2.20.109 Updates `software.amazon.awssdk:sts` from 2.20.108 to 2.20.109 --- updated-dependencies: - dependency-name: software.amazon.awssdk:bom dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:http-client-spi dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:url-connection-client dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:s3 dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:lambda dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:cloudwatch dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:xray dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:cloudformation dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:sts dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/powertools-examples-cloudformation/pom.xml | 2 +- examples/powertools-examples-sqs/pom.xml | 2 +- pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/powertools-examples-cloudformation/pom.xml b/examples/powertools-examples-cloudformation/pom.xml index c7e35c8d8..c93a0c845 100644 --- a/examples/powertools-examples-cloudformation/pom.xml +++ b/examples/powertools-examples-cloudformation/pom.xml @@ -16,7 +16,7 @@ true 1.2.2 3.11.2 - 2.20.108 + 2.20.109 diff --git a/examples/powertools-examples-sqs/pom.xml b/examples/powertools-examples-sqs/pom.xml index 71c0eb131..99a6d29f3 100644 --- a/examples/powertools-examples-sqs/pom.xml +++ b/examples/powertools-examples-sqs/pom.xml @@ -28,7 +28,7 @@ software.amazon.awssdk url-connection-client - 2.20.108 + 2.20.109 com.amazonaws diff --git a/pom.xml b/pom.xml index 197e84a91..b4134d1bc 100644 --- a/pom.xml +++ b/pom.xml @@ -60,7 +60,7 @@ 2.20.0 2.15.2 1.9.7 - 2.20.108 + 2.20.109 2.14.0 2.1.3 UTF-8 From 530968fc87d25d37e4b46e3313dc01a768eda6c9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Jul 2023 13:55:58 +0200 Subject: [PATCH 32/55] build(deps): bump aws.sdk.version from 2.20.109 to 2.20.110 (#1312) Bumps `aws.sdk.version` from 2.20.109 to 2.20.110. Updates `software.amazon.awssdk:bom` from 2.20.109 to 2.20.110 Updates `software.amazon.awssdk:http-client-spi` from 2.20.109 to 2.20.110 Updates `software.amazon.awssdk:url-connection-client` from 2.20.109 to 2.20.110 Updates `software.amazon.awssdk:s3` from 2.20.109 to 2.20.110 Updates `software.amazon.awssdk:lambda` from 2.20.109 to 2.20.110 Updates `software.amazon.awssdk:cloudwatch` from 2.20.109 to 2.20.110 Updates `software.amazon.awssdk:xray` from 2.20.109 to 2.20.110 Updates `software.amazon.awssdk:cloudformation` from 2.20.109 to 2.20.110 Updates `software.amazon.awssdk:sts` from 2.20.109 to 2.20.110 --- updated-dependencies: - dependency-name: software.amazon.awssdk:bom dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:http-client-spi dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:url-connection-client dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:s3 dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:lambda dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:cloudwatch dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:xray dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:cloudformation dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:sts dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/powertools-examples-cloudformation/pom.xml | 2 +- examples/powertools-examples-sqs/pom.xml | 2 +- pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/powertools-examples-cloudformation/pom.xml b/examples/powertools-examples-cloudformation/pom.xml index c93a0c845..45857eee2 100644 --- a/examples/powertools-examples-cloudformation/pom.xml +++ b/examples/powertools-examples-cloudformation/pom.xml @@ -16,7 +16,7 @@ true 1.2.2 3.11.2 - 2.20.109 + 2.20.110 diff --git a/examples/powertools-examples-sqs/pom.xml b/examples/powertools-examples-sqs/pom.xml index 99a6d29f3..d1b0c102b 100644 --- a/examples/powertools-examples-sqs/pom.xml +++ b/examples/powertools-examples-sqs/pom.xml @@ -28,7 +28,7 @@ software.amazon.awssdk url-connection-client - 2.20.109 + 2.20.110 com.amazonaws diff --git a/pom.xml b/pom.xml index b4134d1bc..cbd588c96 100644 --- a/pom.xml +++ b/pom.xml @@ -60,7 +60,7 @@ 2.20.0 2.15.2 1.9.7 - 2.20.109 + 2.20.110 2.14.0 2.1.3 UTF-8 From b1c688d56b5949d05b1118238914a0ad355fff15 Mon Sep 17 00:00:00 2001 From: Scott Gerring Date: Tue, 25 Jul 2023 14:04:22 +0200 Subject: [PATCH 33/55] Update README.md (#1311) --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index a841e7df7..70cfab314 100644 --- a/README.md +++ b/README.md @@ -197,6 +197,15 @@ The following companies, among others, use Powertools: * [MkDocs](https://www.mkdocs.org/) * [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/) +## Connect + +* **Powertools for AWS Lambda on Discord**: `#java` - **[Invite link](https://discord.gg/B8zZKbbyET)** +* **Email**: + +## Security disclosures + +If you think you’ve found a potential security issue, please do not post it in the Issues. Instead, please follow the instructions [here](https://aws.amazon.com/security/vulnerability-reporting/) or [email AWS security directly](mailto:aws-security@amazon.com). + ## License This library is licensed under the Apache License, Version 2.0. See the LICENSE file. From 48533f73ab939df2f259875a26c29ee82eba224b Mon Sep 17 00:00:00 2001 From: Jeroen Reijn Date: Wed, 26 Jul 2023 11:25:58 +0200 Subject: [PATCH 34/55] Make request for Logger explicit on current class (#1307) --- docs/core/logging.md | 18 +++++++++--------- .../demo/parameters/ParametersFunction.java | 2 +- .../handlers/IdempotencyFunction.java | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/core/logging.md b/docs/core/logging.md index bf5fb6767..09714a512 100644 --- a/docs/core/logging.md +++ b/docs/core/logging.md @@ -239,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) { @@ -256,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) { @@ -315,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) { @@ -364,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) { @@ -417,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) { @@ -449,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) { @@ -484,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) { @@ -545,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(); @@ -575,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/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..f96352e86 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 @@ -23,7 +23,7 @@ import static software.amazon.lambda.powertools.parameters.transform.Transformer.json; 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(); 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..0423bd90a 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 @@ -22,7 +22,7 @@ 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; From baa3592f12f81c13f8d076bef3fb947fc1f889d3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Jul 2023 14:19:05 +0200 Subject: [PATCH 35/55] build(deps): bump aws.sdk.version from 2.20.110 to 2.20.111 (#1315) Bumps `aws.sdk.version` from 2.20.110 to 2.20.111. Updates `software.amazon.awssdk:bom` from 2.20.110 to 2.20.111 Updates `software.amazon.awssdk:http-client-spi` from 2.20.110 to 2.20.111 Updates `software.amazon.awssdk:url-connection-client` from 2.20.110 to 2.20.111 Updates `software.amazon.awssdk:s3` from 2.20.110 to 2.20.111 Updates `software.amazon.awssdk:lambda` from 2.20.110 to 2.20.111 Updates `software.amazon.awssdk:cloudwatch` from 2.20.110 to 2.20.111 Updates `software.amazon.awssdk:xray` from 2.20.110 to 2.20.111 Updates `software.amazon.awssdk:cloudformation` from 2.20.110 to 2.20.111 Updates `software.amazon.awssdk:sts` from 2.20.110 to 2.20.111 --- updated-dependencies: - dependency-name: software.amazon.awssdk:bom dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:http-client-spi dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:url-connection-client dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:s3 dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:lambda dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:cloudwatch dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:xray dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:cloudformation dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:sts dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/powertools-examples-cloudformation/pom.xml | 2 +- examples/powertools-examples-sqs/pom.xml | 2 +- pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/powertools-examples-cloudformation/pom.xml b/examples/powertools-examples-cloudformation/pom.xml index 45857eee2..843d8b5e8 100644 --- a/examples/powertools-examples-cloudformation/pom.xml +++ b/examples/powertools-examples-cloudformation/pom.xml @@ -16,7 +16,7 @@ true 1.2.2 3.11.2 - 2.20.110 + 2.20.111 diff --git a/examples/powertools-examples-sqs/pom.xml b/examples/powertools-examples-sqs/pom.xml index d1b0c102b..78e4b0904 100644 --- a/examples/powertools-examples-sqs/pom.xml +++ b/examples/powertools-examples-sqs/pom.xml @@ -28,7 +28,7 @@ software.amazon.awssdk url-connection-client - 2.20.110 + 2.20.111 com.amazonaws diff --git a/pom.xml b/pom.xml index cbd588c96..ccc27b64c 100644 --- a/pom.xml +++ b/pom.xml @@ -60,7 +60,7 @@ 2.20.0 2.15.2 1.9.7 - 2.20.110 + 2.20.111 2.14.0 2.1.3 UTF-8 From 0e76f04f1946dc3cabb842fe4303ac122b67b563 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Van=20Der=20Linden?= <117538+jeromevdl@users.noreply.github.com> Date: Thu, 27 Jul 2023 10:31:19 +0200 Subject: [PATCH 36/55] chore: checkstyle formater & linter (#1316) * add checkstyle to the project * apply checkstyle and add copyright headers when missing * only check style on JDK 11 (fix jdk 8 build) * notice in the contribution guide --- CONTRIBUTING.md | 9 +- checkstyle.xml | 427 ++++++++++++++++++ examples/pom.xml | 14 + ...owertoolsExamplesCloudformationCdkApp.java | 5 +- .../src/main/java/helloworld/App.java | 15 +- .../src/main/java/helloworld/App.java | 70 +-- .../src/main/java/helloworld/AppStream.java | 21 +- .../src/test/java/helloworld/AppTest.java | 70 +-- .../powertools-examples-idempotency/pom.xml | 14 + .../src/main/java/helloworld/App.java | 37 +- .../src/test/java/helloworld/AppTest.java | 35 +- .../java/org/demo/parameters/MyObject.java | 14 + .../demo/parameters/ParametersFunction.java | 43 +- ...GatewayRequestDeserializationFunction.java | 32 +- .../java/org/demo/serialization/Product.java | 14 + .../SQSEventDeserializationFunction.java | 21 +- ...wayRequestDeserializationFunctionTest.java | 18 +- .../SQSEventDeserializationFunctionTest.java | 24 +- .../java/org/demo/sqs/SqsMessageSender.java | 63 ++- .../src/main/java/org/demo/sqs/SqsPoller.java | 37 +- .../powertools-examples-validation/pom.xml | 14 + .../demo/validation/InboundValidation.java | 20 +- .../validation/InboundValidationTest.java | 20 +- license-header | 13 + pom.xml | 53 +++ powertools-cloudformation/pom.xml | 22 + .../AbstractCustomResourceHandler.java | 19 +- .../CloudFormationResponse.java | 231 +++++----- .../CustomResourceResponseException.java | 14 + .../powertools/cloudformation/Response.java | 271 +++++------ .../AbstractCustomResourceHandlerTest.java | 209 +++++---- .../CloudFormationIntegrationTest.java | 99 ++-- .../CloudFormationResponseTest.java | 71 +-- .../cloudformation/ResponseTest.java | 47 +- .../NoPhysicalResourceIdSetHandler.java | 14 + .../PhysicalResourceIdSetHandler.java | 20 +- .../RuntimeExceptionThrownHandler.java | 14 + powertools-core/pom.xml | 23 + .../core/internal/LambdaConstants.java | 3 +- .../core/internal/LambdaHandlerProcessor.java | 14 +- .../core/internal/SystemWrapper.java | 14 + .../internal/LambdaHandlerProcessorTest.java | 40 +- .../lambda/powertools/e2e/Function.java | 27 +- .../amazon/lambda/powertools/e2e/Input.java | 14 + .../lambda/powertools/e2e/Function.java | 14 + .../amazon/lambda/powertools/e2e/Input.java | 14 + .../lambda/powertools/e2e/Function.java | 14 + .../amazon/lambda/powertools/e2e/Input.java | 21 +- .../lambda/powertools/e2e/Function.java | 14 + .../amazon/lambda/powertools/e2e/Input.java | 39 +- .../lambda/powertools/e2e/Function.java | 29 +- .../amazon/lambda/powertools/e2e/Input.java | 14 + powertools-e2e-tests/pom.xml | 18 + .../lambda/powertools/IdempotencyE2ET.java | 30 +- .../amazon/lambda/powertools/LoggingE2ET.java | 38 +- .../amazon/lambda/powertools/MetricsE2ET.java | 73 ++- .../lambda/powertools/ParametersE2ET.java | 43 +- .../amazon/lambda/powertools/TracingE2ET.java | 38 +- .../powertools/testutils/AppConfig.java | 17 +- .../powertools/testutils/Infrastructure.java | 304 +++++++------ .../powertools/testutils/JavaRuntime.java | 14 + .../testutils/lambda/InvocationResult.java | 18 +- .../testutils/lambda/LambdaInvoker.java | 27 +- .../testutils/logging/InvocationLogs.java | 18 +- .../testutils/metrics/MetricsFetcher.java | 105 +++-- .../testutils/tracing/SegmentDocument.java | 17 +- .../powertools/testutils/tracing/Trace.java | 17 +- .../testutils/tracing/TraceFetcher.java | 150 +++--- powertools-idempotency/pom.xml | 18 + .../powertools/idempotency/Constants.java | 3 +- .../powertools/idempotency/Idempotency.java | 52 ++- .../idempotency/IdempotencyConfig.java | 49 +- .../idempotency/IdempotencyKey.java | 3 +- .../powertools/idempotency/Idempotent.java | 4 +- ...IdempotencyAlreadyInProgressException.java | 3 +- .../IdempotencyConfigurationException.java | 3 +- ...IdempotencyInconsistentStateException.java | 3 +- ...IdempotencyItemAlreadyExistsException.java | 3 +- .../IdempotencyItemNotFoundException.java | 7 +- .../exceptions/IdempotencyKeyException.java | 3 +- .../IdempotencyPersistenceLayerException.java | 3 +- .../IdempotencyValidationException.java | 3 +- .../internal/IdempotencyHandler.java | 56 ++- .../internal/IdempotentAspect.java | 25 +- .../idempotency/internal/cache/LRUCache.java | 4 +- .../persistence/BasePersistenceStore.java | 63 +-- .../idempotency/persistence/DataRecord.java | 29 +- .../persistence/DynamoDBPersistenceStore.java | 84 ++-- .../persistence/PersistenceStore.java | 10 +- .../idempotency/DynamoDBConfig.java | 35 +- .../idempotency/IdempotencyTest.java | 24 +- .../handlers/IdempotencyEnabledFunction.java | 3 +- .../handlers/IdempotencyFunction.java | 29 +- .../handlers/IdempotencyInternalFunction.java | 7 +- ...dempotencyInternalFunctionInternalKey.java | 3 +- .../IdempotencyInternalFunctionInvalid.java | 5 +- .../IdempotencyInternalFunctionVoid.java | 5 +- .../handlers/IdempotencyStringFunction.java | 14 + .../IdempotencyWithErrorFunction.java | 3 +- .../internal/IdempotencyAspectTest.java | 71 ++- .../internal/cache/LRUCacheTest.java | 7 +- .../powertools/idempotency/model/Basket.java | 25 +- .../powertools/idempotency/model/Product.java | 11 +- .../persistence/BasePersistenceStoreTest.java | 71 +-- .../DynamoDBPersistenceStoreTest.java | 66 ++- powertools-logging/pom.xml | 22 + .../logging/CorrelationIdPathConstants.java | 18 +- .../lambda/powertools/logging/Logging.java | 6 +- .../powertools/logging/LoggingUtils.java | 19 +- .../internal/AbstractJacksonLayoutCopy.java | 381 ++++++++-------- .../logging/internal/DefaultLambdaFields.java | 12 +- .../logging/internal/JacksonFactoryCopy.java | 108 +++-- .../logging/internal/LambdaJsonLayout.java | 175 +++---- .../logging/internal/LambdaLoggingAspect.java | 71 +-- .../logging/internal/PowertoolsResolver.java | 19 +- .../internal/PowertoolsResolverFactory.java | 17 +- .../core/layout/LambdaJsonLayoutTest.java | 84 ++-- .../powertools/logging/LoggingUtilsTest.java | 12 +- ...LogToolApiGatewayHttpApiCorrelationId.java | 7 +- ...LogToolApiGatewayRestApiCorrelationId.java | 7 +- .../logging/handlers/PowerLogToolEnabled.java | 3 +- .../PowerLogToolEnabledForStream.java | 6 +- .../handlers/PowerLogToolSamplingEnabled.java | 3 +- .../logging/handlers/PowerToolDisabled.java | 3 +- .../handlers/PowerToolDisabledForStream.java | 4 +- .../handlers/PowerToolLogEventEnabled.java | 3 +- .../PowerToolLogEventEnabledForStream.java | 6 +- ...erToolLogEventEnabledWithCustomMapper.java | 20 +- .../PowertoolsLogAlbCorrelationId.java | 7 +- .../PowertoolsLogEnabledWithClearState.java | 5 +- ...PowertoolsLogEventBridgeCorrelationId.java | 12 +- .../internal/LambdaLoggingAspectTest.java | 125 ++--- powertools-metrics/pom.xml | 22 + .../emf/model/MetricsLoggerHelper.java | 18 +- .../lambda/powertools/metrics/Metrics.java | 17 + .../powertools/metrics/MetricsUtils.java | 65 ++- .../metrics/ValidationException.java | 14 + .../metrics/internal/LambdaMetricsAspect.java | 100 ++-- .../powertools/metrics/MetricsLoggerTest.java | 182 ++++---- ...ertoolsMetricsColdStartEnabledHandler.java | 18 +- ...MetricsEnabledDefaultDimensionHandler.java | 26 +- ...tricsEnabledDefaultNoDimensionHandler.java | 24 +- .../PowertoolsMetricsEnabledHandler.java | 20 +- ...PowertoolsMetricsEnabledStreamHandler.java | 21 +- ...sMetricsExceptionWhenNoMetricsHandler.java | 18 +- .../PowertoolsMetricsNoDimensionsHandler.java | 18 +- ...etricsNoExceptionWhenNoMetricsHandler.java | 18 +- ...rtoolsMetricsTooManyDimensionsHandler.java | 19 +- ...wertoolsMetricsWithExceptionInHandler.java | 18 +- .../internal/LambdaMetricsAspectTest.java | 362 ++++++++------- powertools-parameters/pom.xml | 19 +- .../parameters/AppConfigProvider.java | 85 ++-- .../powertools/parameters/BaseProvider.java | 78 ++-- .../parameters/DynamoDbProvider.java | 69 +-- .../lambda/powertools/parameters/Param.java | 19 +- .../powertools/parameters/ParamManager.java | 31 +- .../powertools/parameters/ParamProvider.java | 3 +- .../powertools/parameters/SSMProvider.java | 63 +-- .../parameters/SecretsProvider.java | 56 +-- .../parameters/cache/CacheManager.java | 7 +- .../parameters/cache/DataStore.java | 29 +- .../DynamoDbProviderSchemaException.java | 14 + .../exception/TransformationException.java | 6 +- .../internal/LambdaParametersAspect.java | 18 +- .../transform/Base64Transformer.java | 8 +- .../transform/BasicTransformer.java | 3 +- .../parameters/transform/JsonTransformer.java | 3 +- .../transform/TransformationManager.java | 23 +- .../parameters/transform/Transformer.java | 6 +- .../parameters/AppConfigProviderTest.java | 54 ++- .../parameters/BaseProviderTest.java | 87 ++-- .../parameters/DynamoDbProviderE2ETest.java | 29 +- .../parameters/DynamoDbProviderTest.java | 62 ++- .../ParamManagerIntegrationTest.java | 37 +- .../parameters/ParamManagerTest.java | 11 +- .../parameters/SSMProviderTest.java | 44 +- .../parameters/SecretsProviderTest.java | 27 +- .../parameters/cache/CacheManagerTest.java | 14 +- .../parameters/cache/DataStoreTest.java | 14 +- .../parameters/internal/AnotherObject.java | 18 +- .../parameters/internal/CustomProvider.java | 19 +- .../internal/LambdaParametersAspectTest.java | 39 +- .../transform/Base64TransformerTest.java | 12 +- .../transform/JsonTransformerTest.java | 26 +- .../transform/ObjectToDeserialize.java | 8 +- .../transform/TransformationManagerTest.java | 18 +- powertools-serialization/pom.xml | 18 + .../EventDeserializationException.java | 3 +- .../utilities/EventDeserializer.java | 54 ++- .../powertools/utilities/JsonConfig.java | 27 +- .../utilities/jmespath/Base64Function.java | 28 +- .../jmespath/Base64GZipFunction.java | 42 +- .../utilities/jmespath/JsonFunction.java | 4 +- .../utilities/EventDeserializerTest.java | 27 +- .../jmespath/Base64FunctionTest.java | 16 +- .../jmespath/Base64GZipFunctionTest.java | 31 +- .../utilities/jmespath/JsonFunctionTest.java | 27 +- .../powertools/utilities/model/Basket.java | 25 +- .../powertools/utilities/model/Order.java | 1 + .../powertools/utilities/model/Product.java | 11 +- powertools-sqs/pom.xml | 23 + .../sqs/SQSBatchProcessingException.java | 29 +- .../lambda/powertools/sqs/SqsBatch.java | 30 +- .../powertools/sqs/SqsLargeMessage.java | 15 +- .../powertools/sqs/SqsMessageHandler.java | 19 +- .../lambda/powertools/sqs/SqsUtils.java | 109 ++--- ...ippedMessageDueToFailedBatchException.java | 14 + .../powertools/sqs/internal/BatchContext.java | 194 ++++---- .../sqs/internal/SqsLargeMessageAspect.java | 146 +++--- .../SqsMessageBatchProcessorAspect.java | 22 +- .../powertools/sqs/SampleSqsHandler.java | 14 + .../sqs/SqsUtilsBatchProcessorTest.java | 298 ++++++------ .../sqs/SqsUtilsFifoBatchProcessorTest.java | 138 +++--- .../sqs/SqsUtilsLargeMessageTest.java | 164 ++++--- .../sqs/handlers/LambdaHandlerApiGateway.java | 16 +- .../PartialBatchFailureSuppressedHandler.java | 20 +- .../PartialBatchPartialFailureHandler.java | 20 +- .../handlers/PartialBatchSuccessHandler.java | 20 +- .../sqs/handlers/SqsMessageHandler.java | 14 + ...MessageHandlerWithNonRetryableHandler.java | 27 +- ...dlerWithNonRetryableHandlerWithDelete.java | 24 +- .../handlers/SqsNoDeleteMessageHandler.java | 14 + .../internal/SqsLargeMessageAspectTest.java | 121 +++-- .../SqsMessageBatchProcessorAspectTest.java | 214 +++++---- powertools-test-suite/pom.xml | 14 + .../testsuite/LoggingOrderTest.java | 114 +++-- .../handler/LoggingOrderMessageHandler.java | 14 + .../TracingLoggingStreamMessageHandler.java | 21 +- powertools-tracing/pom.xml | 23 + .../powertools/tracing/CaptureMode.java | 14 + .../lambda/powertools/tracing/Tracing.java | 9 +- .../powertools/tracing/TracingUtils.java | 55 +-- .../tracing/internal/LambdaTracingAspect.java | 35 +- .../tracing/internal/SystemWrapper.java | 14 + .../powertools/tracing/TracingUtilsTest.java | 160 ++++--- .../tracing/handlers/PowerToolDisabled.java | 3 +- .../handlers/PowerToolDisabledForStream.java | 8 +- .../handlers/PowerTracerToolEnabled.java | 3 +- ...lEnabledExplicitlyForResponseAndError.java | 7 +- .../PowerTracerToolEnabledForError.java | 7 +- .../PowerTracerToolEnabledForResponse.java | 7 +- ...oolEnabledForResponseWithCustomMapper.java | 48 +- .../PowerTracerToolEnabledForStream.java | 6 +- ...cerToolEnabledForStreamWithNoMetaData.java | 10 +- .../PowerTracerToolEnabledWithException.java | 3 +- .../PowerTracerToolEnabledWithNoMetaData.java | 7 +- ...erToolEnabledWithNoMetaDataDeprecated.java | 5 +- .../internal/LambdaTracingAspectTest.java | 278 ++++++------ .../nonhandler/PowerToolNonHandler.java | 14 + powertools-validation/pom.xml | 23 + .../powertools/validation/Validation.java | 8 +- .../validation/ValidationConfig.java | 7 +- .../validation/ValidationException.java | 3 +- .../validation/ValidationUtils.java | 24 +- .../validation/internal/ValidationAspect.java | 43 +- .../validation/ValidationUtilsTest.java | 92 ++-- .../handlers/GenericSchemaV7Handler.java | 3 +- .../handlers/MyCustomEventHandler.java | 3 +- .../SQSWithCustomEnvelopeHandler.java | 3 +- .../handlers/SQSWithWrongEnvelopeHandler.java | 3 +- .../ValidationInboundStringHandler.java | 3 +- .../ResponseEventsArgumentsProvider.java | 14 +- .../internal/ValidationAspectTest.java | 40 +- .../powertools/validation/model/Basket.java | 3 +- .../validation/model/MyCustomEvent.java | 3 +- .../powertools/validation/model/Product.java | 3 +- 266 files changed, 6810 insertions(+), 3722 deletions(-) create mode 100644 checkstyle.xml create mode 100644 license-header diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 08aeddcfe..46fab27cf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,10 +31,11 @@ To send us a pull request, please: 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. +3. Ensure local tests pass: `mvn clean test` +4. Ensure your code is formatted with the provided [checkstyle.xml](https://github.com/aws-powertools/powertools-lambda-java/blob/main/checkstyle.xml): `mvn clean verify` +5. Commit to your fork using clear commit messages. +6. Send us a pull request, answering any default questions in the pull request interface. +7. 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/). 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/examples/pom.xml b/examples/pom.xml index c9b8ea8ae..72f1dc03b 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -1,4 +1,18 @@ + + 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 index 84060171b..f4a4d06d7 100644 --- 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 @@ -7,8 +7,9 @@ public class PowertoolsExamplesCloudformationCdkApp { public static void main(final String[] args) { App app = new App(); - new PowertoolsExamplesCloudformationCdkStack(app, "PowertoolsExamplesCloudformationCdkStack", StackProps.builder() - .build()); + new PowertoolsExamplesCloudformationCdkStack(app, "PowertoolsExamplesCloudformationCdkStack", + StackProps.builder() + .build()); app.synth(); } diff --git a/examples/powertools-examples-cloudformation/src/main/java/helloworld/App.java b/examples/powertools-examples-cloudformation/src/main/java/helloworld/App.java index c7744cd5a..ca3cb0ab7 100644 --- a/examples/powertools-examples-cloudformation/src/main/java/helloworld/App.java +++ b/examples/powertools-examples-cloudformation/src/main/java/helloworld/App.java @@ -41,7 +41,8 @@ public App() { 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."); + Objects.requireNonNull(cloudFormationCustomResourceEvent.getResourceProperties().get("BucketName"), + "BucketName cannot be null."); log.info(cloudFormationCustomResourceEvent); String bucketName = (String) cloudFormationCustomResourceEvent.getResourceProperties().get("BucketName"); @@ -70,7 +71,8 @@ protected Response create(CloudFormationCustomResourceEvent cloudFormationCustom 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."); + 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) @@ -112,7 +114,8 @@ protected Response update(CloudFormationCustomResourceEvent cloudFormationCustom 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."); + 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. @@ -142,7 +145,8 @@ protected Response delete(CloudFormationCustomResourceEvent cloudFormationCustom private boolean bucketExists(String bucketName) { try { - HeadBucketResponse headBucketResponse = s3Client.headBucket(HeadBucketRequest.builder().bucket(bucketName).build()); + HeadBucketResponse headBucketResponse = + s3Client.headBucket(HeadBucketRequest.builder().bucket(bucketName).build()); if (headBucketResponse.sdkHttpResponse().isSuccessful()) { return true; } @@ -157,7 +161,8 @@ 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 waiterResponse = + waiter.waitUntilBucketExists(HeadBucketRequest.builder().bucket(bucketName).build()); waiterResponse.matched().response().ifPresent(log::info); log.info("Bucket Created {}", bucketName); } 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/pom.xml b/examples/powertools-examples-idempotency/pom.xml index 0607fdd14..f24b2ffc4 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 0a20aa3a4..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() @@ -45,7 +59,7 @@ public App(DynamoDbClient client) { /** * 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: * *

    @@ -89,13 +103,12 @@ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEv
     
         /**
          * 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 { 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/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 f96352e86..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,10 +29,11 @@ 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(ParametersFunction.class); @@ -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-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-sqs/src/main/java/org/demo/sqs/SqsMessageSender.java b/examples/powertools-examples-sqs/src/main/java/org/demo/sqs/SqsMessageSender.java index b90c50654..45856d198 100644 --- 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 @@ -1,11 +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 org.demo.sqs; +import static java.util.stream.Collectors.toList; + 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 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 org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; @@ -17,15 +38,6 @@ 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); @@ -50,22 +62,23 @@ public String handleRequest(final ScheduledEvent input, final Context context) { // 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()); + .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) 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 index bf2b7bdfe..9ad5c7868 100644 --- 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 @@ -1,10 +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 org.demo.sqs; -import java.util.Random; +import static com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import com.amazonaws.services.lambda.runtime.events.SQSEvent; +import java.security.SecureRandom; +import java.util.Random; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; @@ -13,18 +29,12 @@ 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() @@ -32,6 +42,9 @@ public class SqsPoller implements RequestHandler { .build()); } + Logger log = LogManager.getLogger(SqsPoller.class); + Random random = new SecureRandom(); + @SqsBatch(value = BatchProcessor.class, nonRetryableExceptions = {IllegalArgumentException.class}) @Logging(logEvent = true) public String handleRequest(final SQSEvent input, final Context context) { @@ -45,14 +58,16 @@ public String process(SQSMessage message) { int nextInt = random.nextInt(100); - if(nextInt <= 10) { + 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()); + throw new IllegalArgumentException( + "Failed business validation. No point of retrying. Move me to DLQ." + message.getMessageId()); } - if(nextInt > 90) { + 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()); + throw new RuntimeException( + "Failed due to intermittent issue. Will be sent back for retry." + message.getMessageId()); } return "Success"; diff --git a/examples/powertools-examples-validation/pom.xml b/examples/powertools-examples-validation/pom.xml index 455fd66b8..1c7e33de0 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/pom.xml b/pom.xml index ccc27b64c..7744913fd 100644 --- a/pom.xml +++ b/pom.xml @@ -1,4 +1,18 @@ + + @@ -525,6 +539,45 @@ + + jdk11 + + 11 + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.3.0 + + checkstyle.xml + UTF-8 + true + true + false + + + + + + com.puppycrawl.tools + checkstyle + 10.9.1 + + + + + + check + + + + + + + diff --git a/powertools-cloudformation/pom.xml b/powertools-cloudformation/pom.xml index 3a846c378..a122e7ac8 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 cf9ad45d1..1adb64af8 100644 --- a/powertools-core/pom.xml +++ b/powertools-core/pom.xml @@ -1,4 +1,18 @@ + + @@ -89,4 +103,13 @@ + + + + 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 ea6a6ff44..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,6 +11,7 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.core.internal; public class LambdaConstants { 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 c7f8b119f..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,20 +11,20 @@ * 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 { 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/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 94ad97506..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,24 +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.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 org.mockito.MockedStatic; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.Optional; - -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; - class LambdaHandlerProcessorTest { private Signature signature = mock(Signature.class); @@ -42,7 +55,7 @@ void isHandlerMethod_shouldRecognizeRequestStreamHandler() { @Test void isHandlerMethod_shouldReturnFalse() { - ProceedingJoinPoint pjpMock = mockRequestHandlerPjp(Object.class, new Object[]{}); + ProceedingJoinPoint pjpMock = mockRequestHandlerPjp(Object.class, new Object[] {}); boolean isHandlerMethod = LambdaHandlerProcessor.isHandlerMethod(pjpMock); @@ -210,7 +223,8 @@ void isSamLocal() { void serviceName() { try (MockedStatic mockedSystemWrapper = mockStatic(SystemWrapper.class)) { String expectedServiceName = "MyService"; - mockedSystemWrapper.when(() -> getenv(LambdaConstants.POWERTOOLS_SERVICE_NAME)).thenReturn(expectedServiceName); + mockedSystemWrapper.when(() -> getenv(LambdaConstants.POWERTOOLS_SERVICE_NAME)) + .thenReturn(expectedServiceName); String actualServiceName = LambdaHandlerProcessor.serviceName(); 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..8c2b5fc58 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; @@ -10,12 +29,6 @@ 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; - public class Function implements RequestHandler { @@ -42,7 +55,7 @@ public Function(DynamoDbClient client) { @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/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/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 5b6b50511..3e7f0f460 100644 --- a/powertools-e2e-tests/pom.xml +++ b/powertools-e2e-tests/pom.xml @@ -1,4 +1,18 @@ + + @@ -133,6 +147,10 @@ + + org.apache.maven.plugins + maven-checkstyle-plugin + org.apache.maven.plugins maven-compiler-plugin 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..fed823299 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,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; +import static software.amazon.lambda.powertools.testutils.lambda.LambdaInvoker.invokeFunction; + +import java.time.Year; +import java.util.Collections; +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 +28,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; @@ -34,8 +47,9 @@ public static void setup() { @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/LoggingE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LoggingE2ET.java index 15c5eb935..e4b8dccd2 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,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; +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; + import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +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 +32,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,7 +46,7 @@ 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()} }) @@ -44,15 +57,16 @@ public static void setup() { @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..32965b3be 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,12 +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; -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.lambda.LambdaInvoker.invokeFunction; import java.util.Collections; import java.util.List; @@ -14,13 +23,17 @@ 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,7 +44,7 @@ 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} }) @@ -42,32 +55,44 @@ public static void setup() { @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..678321a99 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,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; -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.lambda.LambdaInvoker.invokeFunction; import java.util.HashMap; import java.util.Map; @@ -11,23 +23,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,7 +54,7 @@ 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()} }) @@ -47,13 +65,14 @@ public void setup() { @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..cd4a21df4 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,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 org.assertj.core.api.Assertions.assertThat; +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 +31,9 @@ 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(); + // "TracingE2EService_e479fb27-422b-4107-9f8c-086c62e1cd12"; private static Infrastructure infrastructure; private static String functionName; @@ -38,8 +52,9 @@ public static void setup() { @AfterAll public static void tearDown() { - if (infrastructure != null) + if (infrastructure != null) { infrastructure.destroy(); + } } @Test @@ -77,7 +92,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..62bb018f4 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,23 +1,56 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.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.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.logs.LogGroup; import software.amazon.awscdk.services.logs.RetentionDays; @@ -27,7 +60,12 @@ 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 +74,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. *
    @@ -71,12 +101,10 @@ public class Infrastructure { private final String account; private final String idempotencyTable; private final AppConfig appConfig; - - + private final SdkHttpClient httpClient; private String functionName; private Object cfnTemplate; private String cfnAssetDirectory; - private final SdkHttpClient httpClient; private Infrastructure(Builder builder) { this.stackName = builder.stackName; @@ -110,8 +138,13 @@ 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() { @@ -124,9 +157,11 @@ 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"); + LOG.info("Stack " + waiterResponse.matched().response().get().stacks().get(0).stackName() + + " successfully deployed"); } else { throw new RuntimeException("Failed to create stack"); } @@ -141,93 +176,9 @@ 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() { @@ -259,7 +210,8 @@ private Stack createStackWithLambda() { functionName = stackName + "-function"; - 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() @@ -297,10 +249,10 @@ private Stack createStackWithLambda() { } 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") @@ -327,7 +279,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()) @@ -376,43 +328,133 @@ 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 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; + } + } + 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-idempotency/pom.xml b/powertools-idempotency/pom.xml index cb4d9b802..ba4b147ae 100644 --- a/powertools-idempotency/pom.xml +++ b/powertools-idempotency/pom.xml @@ -1,4 +1,18 @@ + + @@ -180,6 +194,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..d34dd72dd 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,20 @@ * 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.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,11 +32,6 @@ 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. @@ -48,19 +48,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 +78,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 54001c449..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. @@ -30,7 +29,7 @@ public class DataRecord { /** * 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. @@ -43,16 +42,17 @@ public class DataRecord { * 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 + * 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; @@ -61,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; @@ -110,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 783b029bb..7a023b4de 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,31 +11,37 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.idempotency.persistence; +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; +import java.util.HashMap; +import java.util.Map; +import java.util.OptionalLong; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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.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; import software.amazon.awssdk.utils.StringUtils; import software.amazon.lambda.powertools.idempotency.Constants; import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemAlreadyExistsException; import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemNotFoundException; -import java.time.Instant; -import java.util.AbstractMap; -import java.util.HashMap; -import java.util.Map; -import java.util.OptionalLong; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -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; - /** * DynamoDB version of the {@link PersistenceStore}. Will store idempotency data in DynamoDB.
    * Use the {@link Builder} to create a new instance. @@ -83,7 +89,7 @@ private DynamoDBPersistenceStore(String tableName, this.dynamoDbClient = client; } else { String idempotencyDisabledEnv = System.getenv().get(Constants.IDEMPOTENCY_DISABLED_ENV); - if (idempotencyDisabledEnv == null || idempotencyDisabledEnv.equalsIgnoreCase("false")) { + if (idempotencyDisabledEnv == null || "false".equalsIgnoreCase(idempotencyDisabledEnv)) { this.dynamoDbClient = DynamoDbClient.builder() .httpClient(UrlConnectionHttpClient.builder().build()) .region(Region.of(System.getenv(AWS_REGION_ENV))) @@ -96,6 +102,10 @@ private DynamoDBPersistenceStore(String tableName, } } + public static Builder builder() { + return new Builder(); + } + @Override public DataRecord getRecord(String idempotencyKey) throws IdempotencyItemNotFoundException { GetItemResponse response = dynamoDbClient.getItem( @@ -133,7 +143,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,9 +163,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<>(":now_milliseconds", AttributeValue.builder().n(String.valueOf(now.toEpochMilli())).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)); @@ -161,14 +176,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_milliseconds 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); } } @@ -184,15 +201,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() @@ -242,16 +263,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()); } /** @@ -288,7 +307,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 0423bd90a..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,14 +34,6 @@ 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(IdempotencyFunction.class); 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 768da2eaa..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)); @@ -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)); @@ -178,7 +193,8 @@ public void putRecord_shouldBlockUpdate_IfRecordAlreadyExistAndProgressNotExpire .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-logging/pom.xml b/powertools-logging/pom.xml index 767fbd3ee..83650fcde 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 9a1567d57..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 com.fasterxml.jackson.databind.ObjectMapper; -import org.apache.logging.log4j.ThreadContext; +import static java.util.Arrays.asList; +import com.fasterxml.jackson.databind.ObjectMapper; import java.util.Map; - -import static java.util.Arrays.asList; +import org.apache.logging.log4j.ThreadContext; /** * 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 c96d1383e..f98e2ee46 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,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.annotation.JsonAnyGetter; @@ -7,6 +21,11 @@ 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; @@ -26,17 +45,145 @@ import org.apache.logging.log4j.util.ReadOnlyStringMap; import org.apache.logging.log4j.util.Strings; -import java.io.IOException; -import java.io.Writer; -import java.nio.charset.Charset; -import java.util.LinkedHashMap; -import java.util.Map; - @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 { @@ -81,80 +228,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 */ @@ -163,6 +298,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). * @@ -173,6 +312,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). * @@ -183,6 +326,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). * @@ -193,6 +340,10 @@ public B setIncludeTimeMillis(final boolean includeTimeMillis) { return asBuilder(); } + public KeyValuePair[] getAdditionalFields() { + return additionalFields; + } + /** * Additional fields to set on each log event. * @@ -204,132 +355,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 { @@ -471,13 +496,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 d489e093b..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,12 +11,37 @@ * 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; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.util.Map; +import java.util.Optional; +import java.util.Random; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -32,31 +57,6 @@ import software.amazon.lambda.powertools.logging.Logging; import software.amazon.lambda.powertools.logging.LoggingUtils; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; -import java.util.Map; -import java.util.Optional; -import java.util.Random; - -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 60f0806a9..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,20 +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 org.apache.logging.log4j.Level; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mock; -import software.amazon.lambda.powertools.logging.handlers.PowerLogToolEnabled; -import software.amazon.lambda.powertools.logging.handlers.PowerLogToolSamplingEnabled; -import software.amazon.lambda.powertools.logging.internal.LambdaLoggingAspect; - import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -33,13 +33,13 @@ import java.nio.file.Paths; import java.nio.file.StandardOpenOption; 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.fail; -import static org.mockito.Mockito.when; -import static org.mockito.MockitoAnnotations.openMocks; +import org.apache.logging.log4j.Level; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import software.amazon.lambda.powertools.logging.handlers.PowerLogToolEnabled; +import software.amazon.lambda.powertools.logging.handlers.PowerLogToolSamplingEnabled; +import software.amazon.lambda.powertools.logging.internal.LambdaLoggingAspect; class LambdaJsonLayoutTest { @@ -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 eee8ace05..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,16 +11,16 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.logging; -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; import java.util.HashMap; import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; +import org.apache.logging.log4j.ThreadContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; 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 15b39c6c5..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,11 +11,11 @@ * 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 java.io.InputStream; import java.io.OutputStream; 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 f1c2f62c8..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,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.handlers; import com.amazonaws.services.lambda.runtime.Context; @@ -8,11 +22,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.logging.Logging; import software.amazon.lambda.powertools.logging.LoggingUtils; -import java.io.IOException; - public class PowerToolLogEventEnabledWithCustomMapper implements RequestHandler { static { @@ -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/PowertoolsLogAlbCorrelationId.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogAlbCorrelationId.java index c06f8326e..a32e3e06e 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogAlbCorrelationId.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,8 +11,11 @@ * 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; @@ -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.APPLICATION_LOAD_BALANCER; - public class PowertoolsLogAlbCorrelationId implements RequestHandler { private final Logger LOG = LogManager.getLogger(PowertoolsLogAlbCorrelationId.class); diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEnabledWithClearState.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEnabledWithClearState.java index 8b7d4fcaa..f21d9f118 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEnabledWithClearState.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; @@ -21,8 +22,8 @@ import software.amazon.lambda.powertools.logging.LoggingUtils; public class PowertoolsLogEnabledWithClearState implements RequestHandler { - public static int COUNT = 1; private static final Logger LOG = LogManager.getLogger(PowertoolsLogEnabledWithClearState.class); + public static int COUNT = 1; @Override @Logging(clearState = true) 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 index f03983aff..53e06cb2e 100644 --- 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 @@ -11,19 +11,19 @@ * 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 org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import software.amazon.lambda.powertools.logging.Logging; - import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; - -import static software.amazon.lambda.powertools.logging.CorrelationIdPathConstants.EVENT_BRIDGE; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import software.amazon.lambda.powertools.logging.Logging; public class PowertoolsLogEventBridgeCorrelationId implements RequestStreamHandler { 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 8e7d2d3e6..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,21 @@ * 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; @@ -23,6 +36,21 @@ 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; +import java.io.IOException; +import java.io.InputStreamReader; +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.List; +import java.util.Map; +import java.util.stream.Collectors; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.ThreadContext; import org.json.JSONException; @@ -46,34 +74,6 @@ import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEnabledWithClearState; import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEventBridgeCorrelationId; -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStreamReader; -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.List; -import java.util.Map; -import java.util.stream.Collectors; - -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; - class LambdaLoggingAspectTest { private static final int EXPECTED_CONTEXT_SIZE = 8; @@ -115,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) @@ -130,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) @@ -184,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); @@ -201,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); @@ -213,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(); @@ -222,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); @@ -243,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); @@ -328,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); @@ -336,25 +345,27 @@ private void resetLogLevel(Level level) throws NoSuchMethodException, IllegalAcc } 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") - ); + 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 ed2d2f815..7b2e99045 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 b50844d99..fab72a9b7 100644 --- a/powertools-parameters/pom.xml +++ b/powertools-parameters/pom.xml @@ -1,4 +1,18 @@ + + @@ -144,7 +158,10 @@ + + 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 c62d7a2e5..130be25a3 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,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.parameters; +import java.util.HashMap; +import java.util.Map; import software.amazon.awssdk.core.SdkSystemSetting; import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; import software.amazon.awssdk.regions.Region; @@ -10,15 +26,12 @@ import software.amazon.lambda.powertools.parameters.cache.CacheManager; import software.amazon.lambda.powertools.parameters.transform.TransformationManager; -import java.util.HashMap; -import java.util.Map; - /** * 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 @@ -27,24 +40,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) { @@ -54,6 +54,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.
    @@ -67,18 +75,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 @@ -87,11 +95,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)); @@ -102,16 +111,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 { 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..e7bfdf835 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,12 +26,6 @@ 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. */ @@ -106,7 +106,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 +127,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 +150,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 +184,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 +230,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 e09f23348..2b0694a5d 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,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.parameters; +import java.util.Collections; +import java.util.Map; +import java.util.stream.Collectors; import software.amazon.awssdk.core.SdkSystemSetting; import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; import software.amazon.awssdk.regions.Region; @@ -13,16 +30,11 @@ 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 { @@ -39,6 +51,15 @@ public class DynamoDbProvider extends BaseProvider { 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. * @@ -82,9 +103,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()); } @@ -92,29 +114,27 @@ 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 { private DynamoDbClient client; private String table; 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()))) + .build(); + } + /** * Create a {@link DynamoDbProvider} instance. * @@ -183,12 +203,5 @@ public DynamoDbProvider.Builder withTransformationManager(TransformationManager this.transformationManager = transformationManager; return this; } - - private static DynamoDbClient createClient() { - return DynamoDbClient.builder() - .httpClientBuilder(UrlConnectionHttpClient.builder()) - .region(Region.of(System.getenv(SdkSystemSetting.AWS_REGION.environmentVariable()))) - .build(); - } } } 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 c8abedf06..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,8 +11,11 @@ * 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; @@ -20,9 +23,6 @@ import software.amazon.lambda.powertools.parameters.cache.CacheManager; import software.amazon.lambda.powertools.parameters.transform.TransformationManager; -import java.lang.reflect.Constructor; -import java.util.concurrent.ConcurrentHashMap; - /** * Utility class to retrieve instances of parameter providers. * Each instance is unique (singleton). @@ -39,8 +39,9 @@ public final class ParamManager { * Get a concrete implementation of {@link BaseProvider}.
    * 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. - * @deprecated You should not use this method directly but a typed one (getSecretsProvider, getSsmProvider, getDynamoDbProvider, getAppConfigProvider), will be removed in v2 + * * @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) { @@ -48,7 +49,8 @@ public static T getProvider(Class providerClass) { 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"); + throw new IllegalArgumentException( + providerClass + " cannot be instantiated like this, additional parameters are required"); } return (T) providers.computeIfAbsent(providerClass, ParamManager::createProvider); } @@ -56,6 +58,7 @@ public static T getProvider(Class providerClass) { /** * 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() { @@ -65,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() { @@ -89,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) { @@ -107,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) { @@ -120,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) { @@ -133,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) { @@ -143,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) @@ -168,10 +178,11 @@ public static TransformationManager getTransformationManager() { return transformationManager; } - static T createProvider(Class providerClass) { + static T createProvider(Class providerClass) { try { Constructor constructor = providerClass.getDeclaredConstructor(CacheManager.class); - T provider = constructor.newInstance(cacheManager); // FIXME: avoid reflection here as we may have issues (#1280) + T provider = + constructor.newInstance(cacheManager); // FIXME: avoid reflection here as we may have issues (#1280) provider.setTransformationManager(transformationManager); return provider; } catch (ReflectiveOperationException 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 1fa4dbaab..b24b1e319 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,8 +11,12 @@ * 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.core.SdkSystemSetting; import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; import software.amazon.awssdk.regions.Region; @@ -25,10 +29,6 @@ import software.amazon.lambda.powertools.parameters.transform.TransformationManager; import software.amazon.lambda.powertools.parameters.transform.Transformer; -import java.time.temporal.ChronoUnit; -import java.util.HashMap; -import java.util.Map; - /** * AWS System Manager Parameter Store Provider

    * @@ -88,14 +88,24 @@ public class SSMProvider extends BaseProvider { /** * 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. * @@ -194,19 +204,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())) { @@ -228,21 +239,18 @@ SsmClient getClient() { return client; } - /** - * 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(); - } - - static class Builder { private SsmClient client; 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()))) + .build(); + } + /** * Create a {@link SSMProvider} instance. * @@ -277,13 +285,6 @@ public SSMProvider.Builder withClient(SsmClient client) { return this; } - private static SsmClient createClient() { - return SsmClient.builder() - .httpClientBuilder(UrlConnectionHttpClient.builder()) - .region(Region.of(System.getenv(SdkSystemSetting.AWS_REGION.environmentVariable()))) - .build(); - } - /** * Mandatory. Provide a CacheManager to the {@link SSMProvider} * 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 fd45da881..99b87f84b 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,8 +11,14 @@ * 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.core.SdkSystemSetting; import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; import software.amazon.awssdk.regions.Region; @@ -22,12 +28,6 @@ import software.amazon.lambda.powertools.parameters.transform.TransformationManager; import software.amazon.lambda.powertools.parameters.transform.Transformer; -import java.time.temporal.ChronoUnit; -import java.util.Base64; -import java.util.Map; - -import static java.nio.charset.StandardCharsets.UTF_8; - /** * AWS Secrets Manager Parameter Provider

    * @@ -59,7 +59,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. @@ -71,14 +71,24 @@ public class SecretsProvider extends BaseProvider { /** * 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. * @@ -91,13 +101,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 @@ -137,21 +148,19 @@ SecretsManagerClient getClient() { return client; } - /** - * 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(); - } - static class Builder { private SecretsManagerClient client; 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()))) + .build(); + } + /** * Create a {@link SecretsProvider} instance. * @@ -186,13 +195,6 @@ public Builder withClient(SecretsManagerClient client) { return this; } - private static SecretsManagerClient createClient() { - return SecretsManagerClient.builder() - .httpClientBuilder(UrlConnectionHttpClient.builder()) - .region(Region.of(System.getenv(SdkSystemSetting.AWS_REGION.environmentVariable()))) - .build(); - } - /** * Mandatory. Provide a CacheManager to the {@link SecretsProvider} * 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 8de2f3f57..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; @@ -20,12 +34,12 @@ public void getParam(Param paramAnnotation) { public Object injectParam(final ProceedingJoinPoint joinPoint, final Param paramAnnotation) { 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 c666edce7..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,13 +11,13 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.parameters.transform; -import software.amazon.lambda.powertools.parameters.exception.TransformationException; +import static java.nio.charset.StandardCharsets.UTF_8; import java.util.Base64; - -import static java.nio.charset.StandardCharsets.UTF_8; +import software.amazon.lambda.powertools.parameters.exception.TransformationException; /** * 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 23f6271da..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; @@ -15,23 +34,18 @@ 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 static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.assertj.core.api.Assertions.assertThatRuntimeException; -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; - + @Captor ArgumentCaptor startSessionRequestCaptor; - + @Captor ArgumentCaptor getLatestConfigurationRequestCaptor; private AppConfigProvider provider; @@ -89,13 +103,17 @@ 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 @@ -156,10 +174,14 @@ 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()); } 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 d6818a64f..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; @@ -17,31 +38,18 @@ import software.amazon.lambda.powertools.parameters.exception.DynamoDbProviderSchemaException; import software.amazon.lambda.powertools.parameters.transform.TransformationManager; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.mockito.MockitoAnnotations.openMocks; - 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() { @@ -114,9 +122,10 @@ public void getValueWithMalformedRowThrows() { .item(responseData) .build()); // Act - Assertions.assertThrows(DynamoDbProviderSchemaException.class, () -> { - provider.getValue(key); - }); + Assertions.assertThrows(DynamoDbProviderSchemaException.class, () -> + { + provider.getValue(key); + }); } @@ -180,10 +189,11 @@ public void getMultipleValuesMissingSortKey_throwsException() { Mockito.when(client.query(queryRequestCaptor.capture())).thenReturn(response); // Assert - Assertions.assertThrows(DynamoDbProviderSchemaException.class, () -> { - // Act - provider.getMultipleValues(key); - }); + Assertions.assertThrows(DynamoDbProviderSchemaException.class, () -> + { + // Act + provider.getMultipleValues(key); + }); } @Test @@ -200,10 +210,11 @@ public void getValuesWithMalformedRowThrows() { Mockito.when(client.query(queryRequestCaptor.capture())).thenReturn(response); // Assert - Assertions.assertThrows(DynamoDbProviderSchemaException.class, () -> { - // Act - provider.getMultipleValues(key); - }); + Assertions.assertThrows(DynamoDbProviderSchemaException.class, () -> + { + // Act + provider.getMultipleValues(key); + }); } @Test @@ -214,6 +225,7 @@ public void testDynamoDBBuilderMissingCacheManager_throwsException() { .withTable("table") .build()); } + @Test public void testDynamoDBBuilderMissingTable_throwsException() { 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 e1cb72be9..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; @@ -31,19 +44,6 @@ import software.amazon.awssdk.services.ssm.model.GetParametersByPathResponse; import software.amazon.awssdk.services.ssm.model.Parameter; -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.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.mockito.MockitoAnnotations.openMocks; - public class ParamManagerIntegrationTest { @Mock @@ -51,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 index a21a6082c..b84fcf743 100644 --- 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 @@ -11,18 +11,19 @@ * limitations under the License. * */ -package software.amazon.lambda.powertools.parameters; -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; +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 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 e55f3d7e6..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; @@ -28,20 +42,6 @@ import software.amazon.lambda.powertools.parameters.cache.CacheManager; import software.amazon.lambda.powertools.parameters.transform.TransformationManager; -import java.time.temporal.ChronoUnit; -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.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; - public class SSMProviderTest { @Mock @@ -165,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()); @@ -184,11 +185,12 @@ 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"); 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 2ab72ffdd..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; @@ -26,14 +34,6 @@ import software.amazon.lambda.powertools.parameters.cache.CacheManager; import software.amazon.lambda.powertools.parameters.transform.TransformationManager; -import java.time.temporal.ChronoUnit; -import java.util.Base64; - -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; - public class SecretsProviderTest { @Mock @@ -76,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); @@ -99,9 +100,9 @@ public void testSecretsProviderBuilderMissingCacheManager_throwsException() { // Act & Assert assertThatIllegalStateException().isThrownBy(() -> SecretsProvider.builder() - .withClient(client) - .withTransformationManager(transformationManager) - .build()) + .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..074a08844 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,11 +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.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 c390a051e..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,14 +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.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; - public class LambdaParametersAspectTest { @Mock @@ -75,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..1d09fbeda 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,16 +11,16 @@ * 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 6b6548071..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,13 +11,8 @@ * limitations under the License. * */ -package software.amazon.lambda.powertools.parameters.transform; - -import org.junit.jupiter.api.BeforeEach; -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; @@ -25,6 +20,11 @@ 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; @@ -90,7 +90,9 @@ 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(); } diff --git a/powertools-serialization/pom.xml b/powertools-serialization/pom.xml index f21ecb412..2a57f21e3 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 f1b248fae..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,8 +11,13 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.utilities; +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; @@ -30,17 +35,12 @@ 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.
    @@ -136,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())); @@ -181,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) { @@ -201,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)"); @@ -212,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)"); @@ -229,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 549418263..f5a6d8c11 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(), @@ -49,6 +38,12 @@ public static JsonConfig get() { .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. @@ -73,7 +68,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); @@ -84,4 +79,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 8628fd1d2..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,20 +38,6 @@ 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))); - - if (decompressString == null) { - return runtime.createNull(); - } - - return runtime.createString(decompressString); - } - public static String decompress(byte[] compressed) { if (compressed == null || compressed.length == 0) { return null; @@ -77,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 5055d7086..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,8 +11,13 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.utilities; +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; @@ -29,20 +34,15 @@ 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 @@ -61,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)); @@ -254,7 +255,8 @@ public void testDeserializeRabbitMQEventMessageAsListShouldReturnList(RabbitMQEv @ParameterizedTest @Event(value = "kasip_event.json", type = KinesisAnalyticsStreamsInputPreprocessingEvent.class) - public void testDeserializeKasipEventMessageAsListShouldReturnList(KinesisAnalyticsStreamsInputPreprocessingEvent event) { + public void testDeserializeKasipEventMessageAsListShouldReturnList( + KinesisAnalyticsStreamsInputPreprocessingEvent event) { List products = extractDataFrom(event).asListOf(Product.class); assertThat(products).hasSize(1); assertProduct(products.get(0)); @@ -262,7 +264,8 @@ public void testDeserializeKasipEventMessageAsListShouldReturnList(KinesisAnalyt @ParameterizedTest @Event(value = "kafip_event.json", type = KinesisAnalyticsFirehoseInputPreprocessingEvent.class) - public void testDeserializeKafipEventMessageAsListShouldReturnList(KinesisAnalyticsFirehoseInputPreprocessingEvent event) { + public void testDeserializeKafipEventMessageAsListShouldReturnList( + KinesisAnalyticsFirehoseInputPreprocessingEvent event) { List products = extractDataFrom(event).asListOf(Product.class); assertThat(products).hasSize(1); assertProduct(products.get(0)); 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 8e574eba6..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,27 @@ * 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; - -import static org.assertj.core.api.Assertions.assertThat; - public class Base64GZipFunctionTest { @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().expectedType().toLowerCase()).isEqualTo( + JmesPathType.STRING.name().toLowerCase()); assertThat(base64GZipFunction.argumentConstraints().minArity()).isEqualTo(1); assertThat(base64GZipFunction.argumentConstraints().maxArity()).isEqualTo(1); @@ -38,8 +39,10 @@ public void testConstructor() { @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}"); @@ -47,7 +50,8 @@ public void testPowertoolsGzip() throws IOException { @Test public void testPowertoolsGzipEmptyJsonAttribute() throws IOException { - JsonNode event = JsonConfig.get().getObjectMapper().readTree(this.getClass().getResourceAsStream("/custom_event_gzip.json")); + 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); @@ -55,7 +59,8 @@ public void testPowertoolsGzipEmptyJsonAttribute() throws IOException { @Test public void testPowertoolsGzipWrongArgumentType() throws IOException { - JsonNode event = JsonConfig.get().getObjectMapper().readTree(this.getClass().getResourceAsStream("/custom_event_gzip.json")); + 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); @@ -70,8 +75,10 @@ public void testBase64GzipDecompressNull() { @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 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 index eca36c222..6b48ccd1d 100644 --- 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 @@ -11,6 +11,7 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.utilities.model; import java.util.HashMap; 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-sqs/pom.xml b/powertools-sqs/pom.xml index 618aa948c..d8afac783 100644 --- a/powertools-sqs/pom.xml +++ b/powertools-sqs/pom.xml @@ -1,4 +1,18 @@ + + @@ -117,4 +131,13 @@ + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + + \ 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 index 85231a003..7adc2afe5 100644 --- 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 @@ -1,15 +1,27 @@ -package software.amazon.lambda.powertools.sqs; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.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.events.SQSEvent; +package software.amazon.lambda.powertools.sqs; import static com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; -import static java.util.Collections.*; +import static java.util.Collections.unmodifiableList; import static java.util.stream.Collectors.joining; +import com.amazonaws.services.lambda.runtime.events.SQSEvent; +import java.util.ArrayList; +import java.util.List; + /** *

    * When one or more {@link SQSMessage} fails and if any exception is thrown from {@link SqsMessageHandler#process(SQSMessage)} @@ -47,6 +59,7 @@ public SQSBatchProcessingException(final List exceptions, /** * 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() { @@ -55,6 +68,7 @@ public List getExceptions() { /** * List of returns from {@link SqsMessageHandler#process(SQSMessage)} that were successfully processed. + * * @return List of returns from successfully processed messages */ public List successMessageReturnValues() { @@ -63,6 +77,7 @@ public List successMessageReturnValues() { /** * Details of {@link SQSMessage} that failed in {@link SqsMessageHandler#process(SQSMessage)} + * * @return List of failed messages */ public List getFailures() { 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 index cd529ff22..d0ffe6a73 100644 --- 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 @@ -1,14 +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.sqs; +import static com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; + +import com.amazonaws.services.lambda.runtime.events.SQSEvent; 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} * @@ -22,7 +35,7 @@ * 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. *

    @@ -35,14 +48,14 @@ *

    * 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 @@ -69,6 +82,7 @@ * * ... * + * * @see Amazon SQS dead-letter queues */ @Retention(RetentionPolicy.RUNTIME) 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 index d96245006..847dd456c 100644 --- 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 @@ -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.sqs; import java.lang.annotation.ElementType; @@ -58,7 +72,6 @@ * *

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

    - * */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) 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 index 17e37797c..0c8f03ee9 100644 --- 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 @@ -1,9 +1,23 @@ -package software.amazon.lambda.powertools.sqs; +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.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.events.SQSEvent; +package software.amazon.lambda.powertools.sqs; import static com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; +import com.amazonaws.services.lambda.runtime.events.SQSEvent; + /** *

    * This interface should be implemented for processing {@link SQSMessage} inside {@link SQSEvent} received by lambda @@ -20,6 +34,7 @@ *

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

    + * * @param Return value type from {@link SqsMessageHandler#process(SQSMessage)} */ @FunctionalInterface 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 index 9fff4dc6f..8c06a6291 100644 --- 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 @@ -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,30 +11,28 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.sqs; +import static com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; +import static software.amazon.lambda.powertools.sqs.internal.SqsLargeMessageAspect.processMessages; + +import com.amazonaws.services.lambda.runtime.events.SQSEvent; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import java.lang.reflect.Constructor; import java.util.ArrayList; -import java.util.LinkedList; import java.util.List; -import java.util.Queue; 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.exception.SkippedMessageDueToFailedBatchException; 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; +import software.amazon.payloadoffloading.PayloadS3Pointer; /** * A class of helper functions to add additional functionality to {@link SQSEvent} processing. @@ -43,11 +41,10 @@ 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; - // The attribute on an SQS-FIFO message used to record the message group ID private static final String MESSAGE_GROUP_ID = "MessageGroupId"; + private static SqsClient client; + private static S3Client s3Client; private SqsUtils() { } @@ -176,23 +173,24 @@ public static List batchProcessor(final SQSEvent event, * 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 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. + * @see Amazon SQS dead-letter queues */ @SafeVarargs public static List batchProcessor(final SQSEvent event, @@ -264,26 +262,26 @@ public static List batchProcessor(final SQSEvent event, * 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 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. + * @see Amazon SQS dead-letter queues */ @SafeVarargs public static List batchProcessor(final SQSEvent event, @@ -325,28 +323,29 @@ public static List batchProcessor(final SQSEvent event, * 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 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. + * @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. + * @see Amazon SQS dead-letter queues */ @SafeVarargs public static List batchProcessor(final SQSEvent event, @@ -356,7 +355,8 @@ public static List batchProcessor(final SQSEvent event, final Class... nonRetryableExceptions) { SqsMessageHandler handlerInstance = instantiatedHandler(handler); - return batchProcessor(event, suppressException, handlerInstance, deleteNonRetryableMessageFromQueue, nonRetryableExceptions); + return batchProcessor(event, suppressException, handlerInstance, deleteNonRetryableMessageFromQueue, + nonRetryableExceptions); } /** @@ -423,23 +423,24 @@ public static List batchProcessor(final SQSEvent event, * 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 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. + * @see Amazon SQS dead-letter queues */ @SafeVarargs public static List batchProcessor(final SQSEvent event, @@ -491,7 +492,7 @@ public static List batchProcessor(final SQSEvent event, final Class... nonRetryableExceptions) { final List handlerReturn = new ArrayList<>(); - if(client == null) { + if (client == null) { client = SqsClient.create(); } @@ -521,7 +522,8 @@ public static List batchProcessor(final SQSEvent event, String messageGroupId = message.getAttributes() != null ? message.getAttributes().get(MESSAGE_GROUP_ID) : null; if (messageGroupId != null) { - LOG.info("A message in a message batch with messageGroupId {} and messageId {} failed; failing the rest of the batch too" + LOG.info( + "A message in a message batch with messageGroupId {} and messageId {} failed; failing the rest of the batch too" , messageGroupId, message.getMessageId()); failedBatch = true; } @@ -534,14 +536,16 @@ public static List batchProcessor(final SQSEvent event, if (offset < event.getRecords().size()) { event.getRecords() .subList(offset, event.getRecords().size()) - .forEach(message -> { - LOG.info("Skipping message {} as another message with a message group failed in this batch", - message.getMessageId()); - batchContext.addFailure(message, new SkippedMessageDueToFailedBatchException()); - }); + .forEach(message -> + { + LOG.info("Skipping message {} as another message with a message group failed in this batch", + message.getMessageId()); + batchContext.addFailure(message, new SkippedMessageDueToFailedBatchException()); + }); } - batchContext.processSuccessAndHandleFailed(handlerReturn, suppressException, deleteNonRetryableMessageFromQueue, nonRetryableExceptions); + batchContext.processSuccessAndHandleFailed(handlerReturn, suppressException, deleteNonRetryableMessageFromQueue, + nonRetryableExceptions); return handlerReturn; } @@ -552,7 +556,8 @@ private static SqsMessageHandler instantiatedHandler(final Class> constructor = handler.getDeclaredConstructor(handler.getDeclaringClass()); + final Constructor> constructor = + handler.getDeclaredConstructor(handler.getDeclaringClass()); constructor.setAccessible(true); return constructor.newInstance(handler.getDeclaringClass().getDeclaredConstructor().newInstance()); } catch (Exception e) { @@ -576,7 +581,7 @@ public static ObjectMapper objectMapper() { } public static S3Client s3Client() { - if(null == s3Client) { + if (null == s3Client) { SqsUtils.s3Client = S3Client.create(); } diff --git a/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/exception/SkippedMessageDueToFailedBatchException.java b/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/exception/SkippedMessageDueToFailedBatchException.java index 9dbb66509..fbb4289d8 100644 --- a/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/exception/SkippedMessageDueToFailedBatchException.java +++ b/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/exception/SkippedMessageDueToFailedBatchException.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.sqs.exception; /** 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 index 1e4eff3bf..57ddeb22f 100644 --- 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 @@ -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.sqs.internal; +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; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -9,8 +30,6 @@ 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; @@ -28,11 +47,6 @@ 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<>(); @@ -69,16 +83,17 @@ public final void processSuccessAndHandleFailed(final List successReturns 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); - } - }); + 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); @@ -126,7 +141,8 @@ private boolean isNonRetryableException(Exception exception, Class aClass.isInstance(exception)); } - private boolean moveNonRetryableMessagesToDlqIfConfigured(Map nonRetryableMessageToException) { + private boolean moveNonRetryableMessagesToDlqIfConfigured( + Map nonRetryableMessageToException) { Optional dlqUrl = fetchDlqUrl(nonRetryableMessageToException); if (!dlqUrl.isPresent()) { @@ -134,76 +150,88 @@ private boolean moveNonRetryableMessagesToDlqIfConfigured(Map dlqMessages = nonRetryableMessageToException.keySet().stream() - .map(sqsMessage -> { - Map messageAttributesMap = new HashMap<>(); + .map(sqsMessage -> + { + Map messageAttributesMap = new HashMap<>(); - sqsMessage.getMessageAttributes().forEach((s, messageAttribute) -> { - MessageAttributeValue.Builder builder = MessageAttributeValue.builder(); + sqsMessage.getMessageAttributes().forEach((s, messageAttribute) -> + { + MessageAttributeValue.Builder builder = MessageAttributeValue.builder(); - builder - .dataType(messageAttribute.getDataType()) - .stringValue(messageAttribute.getStringValue()); + builder + .dataType(messageAttribute.getDataType()) + .stringValue(messageAttribute.getStringValue()); - if (null != messageAttribute.getBinaryValue()) { - builder.binaryValue(SdkBytes.fromByteBuffer(messageAttribute.getBinaryValue())); - } + if (null != messageAttribute.getBinaryValue()) { + builder.binaryValue(SdkBytes.fromByteBuffer(messageAttribute.getBinaryValue())); + } - messageAttributesMap.put(s, builder.build()); - }); + messageAttributesMap.put(s, builder.build()); + }); - return SendMessageBatchRequestEntry.builder() - .messageBody(sqsMessage.getBody()) - .id(sqsMessage.getMessageId()) - .messageAttributes(messageAttributesMap) - .build(); - }) + return SendMessageBatchRequestEntry.builder() + .messageBody(sqsMessage.getBody()) + .id(sqsMessage.getMessageId()) + .messageAttributes(messageAttributesMap) + .build(); + }) .collect(toList()); - List sendMessageBatchResponses = batchRequest(dlqMessages, 10, entriesToSend -> { + List sendMessageBatchResponses = batchRequest(dlqMessages, 10, entriesToSend -> + { - SendMessageBatchResponse sendMessageBatchResponse = client.sendMessageBatch(SendMessageBatchRequest.builder() - .entries(entriesToSend) - .queueUrl(dlqUrl.get()) - .build()); + SendMessageBatchResponse sendMessageBatchResponse = + client.sendMessageBatch(SendMessageBatchRequest.builder() + .entries(entriesToSend) + .queueUrl(dlqUrl.get()) + .build()); - LOG.debug("Response from send batch message to DLQ request {}", sendMessageBatchResponse); + LOG.debug("Response from send batch message to DLQ request {}", sendMessageBatchResponse); - return 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; + .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); - })); + .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() { @@ -213,23 +241,25 @@ private boolean hasFailures() { 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(); + 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); + DeleteMessageBatchResponse deleteMessageBatchResponse = client.deleteMessageBatch(request); - LOG.debug("Response from delete request {}", deleteMessageBatchResponse); + LOG.debug("Response from delete request {}", deleteMessageBatchResponse); - return deleteMessageBatchResponse; - }); + return deleteMessageBatchResponse; + }); } } 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 index 588d434d7..7022e399a 100644 --- 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 @@ -1,13 +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.sqs.internal; +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; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.events.SQSEvent; 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; @@ -24,63 +42,32 @@ 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; - }); + .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); @@ -100,31 +87,38 @@ private static String readStringFromS3Object(ResponseInputStream { - s3Client().deleteObject(DeleteObjectRequest.builder() - .bucket(pointer.getS3BucketName()) - .key(pointer.getS3Key()) - .build()); - LOG.info("Message deleted from S3: " + s3Pointer.toJson()); - return null; - }); + 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) { + 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); + 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); + throw new FailedProcessingLargePayloadException( + format("Failed processing S3 record with [Bucket Name: %s Bucket Key: %s]", + pointer.getS3BucketName(), pointer.getS3Key()), e); } } @@ -134,6 +128,36 @@ public static boolean placedOnSqsEventRequestHandler(ProceedingJoinPoint pjp) { && pjp.getArgs()[1] instanceof Context; } + @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 class FailedProcessingLargePayloadException extends RuntimeException { public FailedProcessingLargePayloadException(String message, Throwable cause) { super(message, cause); 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 index 73e91c3a7..ff0b5b014 100644 --- 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 @@ -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.sqs.internal; +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; + import com.amazonaws.services.lambda.runtime.events.SQSEvent; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; @@ -7,10 +25,6 @@ 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 { 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 index d48cded5f..557aa214d 100644 --- 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 @@ -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.sqs; import com.amazonaws.services.lambda.runtime.events.SQSEvent; 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 index 43a089d2c..42e4b9d8f 100644 --- 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 @@ -1,14 +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.sqs; +import static com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; +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; + +import com.amazonaws.services.lambda.runtime.events.SQSEvent; +import com.fasterxml.jackson.databind.ObjectMapper; 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; @@ -20,20 +43,6 @@ 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.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 { @@ -51,10 +60,11 @@ void setUp() throws IOException { @Test void shouldBatchProcessAndNotDeleteMessagesWhenAllSuccess() { - List returnValues = batchProcessor(event, false, (message) -> { - interactionClient.listQueues(); - return "Success"; - }); + List returnValues = batchProcessor(event, false, (message) -> + { + interactionClient.listQueues(); + return "Success"; + }); assertThat(returnValues) .hasSize(2) @@ -66,7 +76,8 @@ void shouldBatchProcessAndNotDeleteMessagesWhenAllSuccess() { @ParameterizedTest @ValueSource(classes = {SampleInnerSqsHandler.class, SampleSqsHandler.class}) - void shouldBatchProcessViaClassAndNotDeleteMessagesWhenAllSuccess(Class> handler) { + void shouldBatchProcessViaClassAndNotDeleteMessagesWhenAllSuccess( + Class> handler) { List returnValues = batchProcessor(event, handler); assertThat(returnValues) @@ -80,33 +91,35 @@ void shouldBatchProcessViaClassAndNotDeleteMessagesWhenAllSuccess(Class failedHandler = (message) -> { - if (failedId.equals(message.getMessageId())) { - throw new RuntimeException("Failed processing"); - } + SqsMessageHandler failedHandler = (message) -> + { + if (failedId.equals(message.getMessageId())) { + throw new RuntimeException("Failed processing"); + } - interactionClient.listQueues(); - return "Success"; - }; + interactionClient.listQueues(); + return "Success"; + }; assertThatExceptionOfType(SQSBatchProcessingException.class) .isThrownBy(() -> batchProcessor(event, failedHandler)) - .satisfies(e -> { + .satisfies(e -> + { - assertThat(e.successMessageReturnValues()) - .hasSize(1) - .contains("Success"); + assertThat(e.successMessageReturnValues()) + .hasSize(1) + .contains("Success"); - assertThat(e.getFailures()) - .hasSize(1) - .extracting("messageId") - .contains(failedId); + assertThat(e.getFailures()) + .hasSize(1) + .extracting("messageId") + .contains(failedId); - assertThat(e.getExceptions()) - .hasSize(1) - .extracting("detailMessage") - .contains("Failed processing"); - }); + assertThat(e.getExceptions()) + .hasSize(1) + .extracting("detailMessage") + .contains("Failed processing"); + }); verify(interactionClient).listQueues(); @@ -119,29 +132,31 @@ void shouldBatchProcessAndDeleteSuccessMessageOnPartialFailures() { @Test void shouldBatchProcessAndFullFailuresInBatch() { - SqsMessageHandler failedHandler = (message) -> { - throw new RuntimeException(message.getMessageId()); - }; + SqsMessageHandler failedHandler = (message) -> + { + throw new RuntimeException(message.getMessageId()); + }; assertThatExceptionOfType(SQSBatchProcessingException.class) .isThrownBy(() -> batchProcessor(event, failedHandler)) - .satisfies(e -> { + .satisfies(e -> + { - assertThat(e.successMessageReturnValues()) - .isEmpty(); + assertThat(e.successMessageReturnValues()) + .isEmpty(); - assertThat(e.getFailures()) - .hasSize(2) - .extracting("messageId") - .containsExactly("059f36b4-87a3-44ab-83d2-661975830a7d", - "2e1424d4-f796-459a-8184-9c92662be6da"); + 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"); - }); + assertThat(e.getExceptions()) + .hasSize(2) + .extracting("detailMessage") + .containsExactly("059f36b4-87a3-44ab-83d2-661975830a7d", + "2e1424d4-f796-459a-8184-9c92662be6da"); + }); verifyNoInteractions(sqsClient); } @@ -150,22 +165,23 @@ void shouldBatchProcessAndFullFailuresInBatch() { void shouldBatchProcessViaClassAndDeleteSuccessMessageOnPartialFailures() { assertThatExceptionOfType(SQSBatchProcessingException.class) .isThrownBy(() -> batchProcessor(event, FailureSampleInnerSqsHandler.class)) - .satisfies(e -> { + .satisfies(e -> + { - assertThat(e.successMessageReturnValues()) - .hasSize(1) - .contains("Success"); + assertThat(e.successMessageReturnValues()) + .hasSize(1) + .contains("Success"); - assertThat(e.getFailures()) - .hasSize(1) - .extracting("messageId") - .contains("2e1424d4-f796-459a-8184-9c92662be6da"); + assertThat(e.getFailures()) + .hasSize(1) + .extracting("messageId") + .contains("2e1424d4-f796-459a-8184-9c92662be6da"); - assertThat(e.getExceptions()) - .hasSize(1) - .extracting("detailMessage") - .contains("Failed processing"); - }); + assertThat(e.getExceptions()) + .hasSize(1) + .extracting("detailMessage") + .contains("Failed processing"); + }); verify(sqsClient).deleteMessageBatch(any(DeleteMessageBatchRequest.class)); } @@ -175,14 +191,15 @@ void shouldBatchProcessViaClassAndDeleteSuccessMessageOnPartialFailures() { void shouldBatchProcessAndSuppressExceptions() { String failedId = "2e1424d4-f796-459a-8184-9c92662be6da"; - SqsMessageHandler failedHandler = (message) -> { - if (failedId.equals(message.getMessageId())) { - throw new RuntimeException("Failed processing"); - } + SqsMessageHandler failedHandler = (message) -> + { + if (failedId.equals(message.getMessageId())) { + throw new RuntimeException("Failed processing"); + } - interactionClient.listQueues(); - return "Success"; - }; + interactionClient.listQueues(); + return "Success"; + }; List returnValues = batchProcessor(event, true, failedHandler); @@ -206,16 +223,6 @@ void shouldBatchProcessViaClassAndSuppressExceptions() { 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"; @@ -226,18 +233,20 @@ void shouldBatchProcessAndMoveNonRetryableExceptionToDlq() { " \"maxReceiveCount\": 2\n" + "}"); - when(sqsClient.getQueueAttributes(any(GetQueueAttributesRequest.class))).thenReturn(GetQueueAttributesResponse.builder() + when(sqsClient.getQueueAttributes(any(GetQueueAttributesRequest.class))).thenReturn( + GetQueueAttributesResponse.builder() .attributes(attributes) - .build()); + .build()); - List batchProcessor = batchProcessor(event, (message) -> { - if (failedId.equals(message.getMessageId())) { - throw new IllegalStateException("Failed processing"); - } + List batchProcessor = batchProcessor(event, (message) -> + { + if (failedId.equals(message.getMessageId())) { + throw new IllegalStateException("Failed processing"); + } - interactionClient.listQueues(); - return "Success"; - }, IllegalStateException.class, IllegalArgumentException.class); + interactionClient.listQueues(); + return "Success"; + }, IllegalStateException.class, IllegalArgumentException.class); assertThat(batchProcessor) .hasSize(1); @@ -255,18 +264,20 @@ void shouldBatchProcessAndDeleteNonRetryableException() { " \"maxReceiveCount\": 2\n" + "}"); - when(sqsClient.getQueueAttributes(any(GetQueueAttributesRequest.class))).thenReturn(GetQueueAttributesResponse.builder() - .attributes(attributes) - .build()); + 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"); - } + 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); + interactionClient.listQueues(); + return "Success"; + }, true, IllegalStateException.class, IllegalArgumentException.class); assertThat(batchProcessor) .hasSize(1); @@ -277,26 +288,28 @@ void shouldBatchProcessAndDeleteNonRetryableException() { @Test void shouldDeleteSuccessfulMessageInBatchesOfT10orLess() throws IOException { - SQSEvent batch25Message = MAPPER.readValue(this.getClass().getResource("/sampleSqsBatchEventBatchSize25.json"), SQSEvent.class); + SQSEvent batch25Message = + MAPPER.readValue(this.getClass().getResource("/sampleSqsBatchEventBatchSize25.json"), SQSEvent.class); assertThatExceptionOfType(SQSBatchProcessingException.class) .isThrownBy(() -> batchProcessor(batch25Message, FailureSampleInnerSqsHandler.class)) - .satisfies(e -> { + .satisfies(e -> + { - assertThat(e.successMessageReturnValues()) - .hasSize(24) - .contains("Success"); + assertThat(e.successMessageReturnValues()) + .hasSize(24) + .contains("Success"); - assertThat(e.getFailures()) - .hasSize(1) - .extracting("messageId") - .contains("2e1424d4-f796-459a-8184-9c92662be6da"); + assertThat(e.getFailures()) + .hasSize(1) + .extracting("messageId") + .contains("2e1424d4-f796-459a-8184-9c92662be6da"); - assertThat(e.getExceptions()) - .hasSize(1) - .extracting("detailMessage") - .contains("Failed processing"); - }); + assertThat(e.getExceptions()) + .hasSize(1) + .extracting("detailMessage") + .contains("Failed processing"); + }); ArgumentCaptor captor = ArgumentCaptor.forClass(DeleteMessageBatchRequest.class); @@ -310,7 +323,8 @@ void shouldDeleteSuccessfulMessageInBatchesOfT10orLess() throws IOException { @Test void shouldBatchProcessAndMoveNonRetryableExceptionToDlqInBatchesOfT10orLess() throws IOException { - SQSEvent batch25Message = MAPPER.readValue(this.getClass().getResource("/sampleSqsBatchEventBatchSize25.json"), SQSEvent.class); + SQSEvent batch25Message = + MAPPER.readValue(this.getClass().getResource("/sampleSqsBatchEventBatchSize25.json"), SQSEvent.class); HashMap attributes = new HashMap<>(); @@ -319,18 +333,20 @@ void shouldBatchProcessAndMoveNonRetryableExceptionToDlqInBatchesOfT10orLess() t " \"maxReceiveCount\": 2\n" + "}"); - when(sqsClient.getQueueAttributes(any(GetQueueAttributesRequest.class))).thenReturn(GetQueueAttributesResponse.builder() - .attributes(attributes) - .build()); + 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"; - } + 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); + throw new IllegalStateException("Failed processing"); + }, IllegalStateException.class, IllegalArgumentException.class); assertThat(batchProcessor) .hasSize(1); @@ -346,6 +362,16 @@ void shouldBatchProcessAndMoveNonRetryableExceptionToDlqInBatchesOfT10orLess() t .hasSize(24); } + public class SampleInnerSqsHandler implements SqsMessageHandler { + private int counter; + + @Override + public String process(SQSMessage message) { + interactionClient.listQueues(); + return String.valueOf(counter++); + } + } + public class FailureSampleInnerSqsHandler implements SqsMessageHandler { @Override public String process(SQSEvent.SQSMessage message) { diff --git a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/SqsUtilsFifoBatchProcessorTest.java b/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/SqsUtilsFifoBatchProcessorTest.java index cfa79dc36..53beeefcb 100644 --- a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/SqsUtilsFifoBatchProcessorTest.java +++ b/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/SqsUtilsFifoBatchProcessorTest.java @@ -1,30 +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.sqs; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static software.amazon.lambda.powertools.sqs.SqsUtils.batchProcessor; +import static software.amazon.lambda.powertools.sqs.SqsUtils.overrideSqsClient; + import com.amazonaws.services.lambda.runtime.events.SQSEvent; import com.amazonaws.services.lambda.runtime.tests.EventLoader; -import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.*; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoSession; import org.mockito.quality.Strictness; 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 java.io.IOException; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Collectors; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.MockitoAnnotations.openMocks; -import static software.amazon.lambda.powertools.sqs.SqsUtils.batchProcessor; -import static software.amazon.lambda.powertools.sqs.SqsUtils.overrideSqsClient; - public class SqsUtilsFifoBatchProcessorTest { private static SQSEvent sqsBatchEvent; @@ -61,10 +75,11 @@ public void tearDown() { public void processWholeBatch() { // Act AtomicInteger processedCount = new AtomicInteger(); - List results = batchProcessor(sqsBatchEvent, false, (message) -> { - processedCount.getAndIncrement(); - return true; - }); + List results = batchProcessor(sqsBatchEvent, false, (message) -> + { + processedCount.getAndIncrement(); + return true; + }); // Assert assertThat(processedCount.get()).isEqualTo(3); @@ -80,31 +95,34 @@ public void processWholeBatch() { @Test public void singleFailureInMiddleOfBatch() { // Arrange - Mockito.when(sqsClient.deleteMessageBatch(deleteMessageBatchCaptor.capture())).thenReturn(DeleteMessageBatchResponse - .builder().build()); + Mockito.when(sqsClient.deleteMessageBatch(deleteMessageBatchCaptor.capture())) + .thenReturn(DeleteMessageBatchResponse + .builder().build()); // Act AtomicInteger processedCount = new AtomicInteger(); assertThatExceptionOfType(SQSBatchProcessingException.class) - .isThrownBy(() -> batchProcessor(sqsBatchEvent, false, (message) -> { - int value = processedCount.getAndIncrement(); - if (value == 1) { - throw new RuntimeException("Whoops"); - } - return true; - })) - - // Assert + .isThrownBy(() -> batchProcessor(sqsBatchEvent, false, (message) -> + { + int value = processedCount.getAndIncrement(); + if (value == 1) { + throw new RuntimeException("Whoops"); + } + return true; + })) + + // Assert .isInstanceOf(SQSBatchProcessingException.class) - .satisfies(e -> { - List failures = ((SQSBatchProcessingException)e).getFailures(); - assertThat(failures.size()).isEqualTo(2); - List failureIds = failures.stream() - .map(SQSEvent.SQSMessage::getMessageId) - .collect(Collectors.toList()); - assertThat(failureIds).contains(sqsBatchEvent.getRecords().get(1).getMessageId()); - assertThat(failureIds).contains(sqsBatchEvent.getRecords().get(2).getMessageId()); - }); + .satisfies(e -> + { + List failures = ((SQSBatchProcessingException) e).getFailures(); + assertThat(failures.size()).isEqualTo(2); + List failureIds = failures.stream() + .map(SQSEvent.SQSMessage::getMessageId) + .collect(Collectors.toList()); + assertThat(failureIds).contains(sqsBatchEvent.getRecords().get(1).getMessageId()); + assertThat(failureIds).contains(sqsBatchEvent.getRecords().get(2).getMessageId()); + }); DeleteMessageBatchRequest deleteRequest = deleteMessageBatchCaptor.getValue(); List messageIds = deleteRequest.entries().stream() @@ -119,20 +137,22 @@ public void singleFailureInMiddleOfBatch() { public void singleFailureAtEndOfBatch() { // Arrange - Mockito.when(sqsClient.deleteMessageBatch(deleteMessageBatchCaptor.capture())).thenReturn(DeleteMessageBatchResponse - .builder().build()); + Mockito.when(sqsClient.deleteMessageBatch(deleteMessageBatchCaptor.capture())) + .thenReturn(DeleteMessageBatchResponse + .builder().build()); // Act AtomicInteger processedCount = new AtomicInteger(); assertThatExceptionOfType(SQSBatchProcessingException.class) - .isThrownBy(() -> batchProcessor(sqsBatchEvent, false, (message) -> { - int value = processedCount.getAndIncrement(); - if (value == 2) { - throw new RuntimeException("Whoops"); - } - return true; - })); + .isThrownBy(() -> batchProcessor(sqsBatchEvent, false, (message) -> + { + int value = processedCount.getAndIncrement(); + if (value == 2) { + throw new RuntimeException("Whoops"); + } + return true; + })); // Assert DeleteMessageBatchRequest deleteRequest = deleteMessageBatchCaptor.getValue(); @@ -150,17 +170,19 @@ public void messageFailureStopsGroupProcessing() { String groupToFail = sqsBatchEvent.getRecords().get(0).getAttributes().get("MessageGroupId"); assertThatExceptionOfType(SQSBatchProcessingException.class) - .isThrownBy(() -> batchProcessor(sqsBatchEvent, (message) -> { - String groupId = message.getAttributes().get("MessageGroupId"); - if (groupId.equals(groupToFail)) { - throw new RuntimeException("Failed processing"); - } - return groupId; - })) - .satisfies(e -> { - assertThat(e.successMessageReturnValues().size()).isEqualTo(0); - assertThat(e.successMessageReturnValues().contains(groupToFail)).isFalse(); - }); + .isThrownBy(() -> batchProcessor(sqsBatchEvent, (message) -> + { + String groupId = message.getAttributes().get("MessageGroupId"); + if (groupId.equals(groupToFail)) { + throw new RuntimeException("Failed processing"); + } + return groupId; + })) + .satisfies(e -> + { + assertThat(e.successMessageReturnValues().size()).isEqualTo(0); + assertThat(e.successMessageReturnValues().contains(groupToFail)).isFalse(); + }); } } 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 index 48de3e6a9..d3b675371 100644 --- 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 @@ -1,13 +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.sqs; +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 com.amazonaws.services.lambda.runtime.events.SQSEvent; 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; @@ -28,23 +52,21 @@ 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"; + @Mock + private S3Client s3Client; + + private static Stream exception() { + return Stream.of(Arguments.of(S3Exception.builder() + .message("Service Exception") + .build()), + Arguments.of(SdkClientException.builder() + .message("Client Exception") + .build())); + } @BeforeEach void setUp() { @@ -54,16 +76,21 @@ void setUp() { @Test public void testLargeMessage() { - ResponseInputStream s3Response = new ResponseInputStream<>(GetObjectResponse.builder().build(), AbortableInputStream.create(new ByteArrayInputStream("A big message".getBytes()))); + 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 + "\"}]"); + 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; - }); + Map sqsMessage = SqsUtils.enrichedMessageFromS3(sqsEvent, sqsMessages -> + { + Map someBusinessLogic = new HashMap<>(); + someBusinessLogic.put("Message", sqsMessages.get(0).getBody()); + return someBusinessLogic; + }); assertThat(sqsMessage) .hasSize(1) @@ -74,29 +101,35 @@ public void testLargeMessage() { verify(s3Client).deleteObject(delete.capture()); Assertions.assertThat(delete.getValue()) - .satisfies((Consumer) deleteObjectRequest -> { - assertThat(deleteObjectRequest.bucket()) - .isEqualTo(BUCKET_NAME); + .satisfies((Consumer) deleteObjectRequest -> + { + assertThat(deleteObjectRequest.bucket()) + .isEqualTo(BUCKET_NAME); - assertThat(deleteObjectRequest.key()) - .isEqualTo(BUCKET_KEY); - }); + 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()))); + 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 + "\"}]"); + 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; - }); + Map sqsMessage = SqsUtils.enrichedMessageFromS3(sqsEvent, deleteS3Payload, sqsMessages -> + { + Map someBusinessLogic = new HashMap<>(); + someBusinessLogic.put("Message", sqsMessages.get(0).getBody()); + return someBusinessLogic; + }); assertThat(sqsMessage) .hasSize(1) @@ -107,13 +140,14 @@ public void testLargeMessageDeleteFromS3Toggle(boolean deleteS3Payload) { verify(s3Client).deleteObject(delete.capture()); Assertions.assertThat(delete.getValue()) - .satisfies((Consumer) deleteObjectRequest -> { - assertThat(deleteObjectRequest.bucket()) - .isEqualTo(BUCKET_NAME); - - assertThat(deleteObjectRequest.key()) - .isEqualTo(BUCKET_KEY); - }); + .satisfies((Consumer) deleteObjectRequest -> + { + assertThat(deleteObjectRequest.bucket()) + .isEqualTo(BUCKET_NAME); + + assertThat(deleteObjectRequest.key()) + .isEqualTo(BUCKET_KEY); + }); } else { verify(s3Client, never()).deleteObject(any(DeleteObjectRequest.class)); } @@ -121,17 +155,20 @@ public void testLargeMessageDeleteFromS3Toggle(boolean deleteS3Payload) { @Test public void shouldNotProcessSmallMessageBody() { - ResponseInputStream s3Response = new ResponseInputStream<>(GetObjectResponse.builder().build(), AbortableInputStream.create(new ByteArrayInputStream("A big message".getBytes()))); + 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; - }); + 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"); @@ -144,7 +181,9 @@ public void shouldNotProcessSmallMessageBody() { 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 + "\"}]"; + String messageBody = + "[\"software.amazon.payloadoffloading.PayloadS3Pointer\",{\"s3BucketName\":\"" + BUCKET_NAME + + "\",\"s3Key\":\"" + BUCKET_KEY + "\"}]"; SQSEvent sqsEvent = messageWithBody(messageBody); assertThatExceptionOfType(SqsLargeMessageAspect.FailedProcessingLargePayloadException.class) @@ -156,16 +195,20 @@ public void shouldFailEntireBatchIfFailedDownloadingFromS3(RuntimeException exce @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"); - } - })); + 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 + "\"}]"; + String messageBody = + "[\"software.amazon.payloadoffloading.PayloadS3Pointer\",{\"s3BucketName\":\"" + BUCKET_NAME + + "\",\"s3Key\":\"" + BUCKET_KEY + "\"}]"; SQSEvent sqsEvent = messageWithBody(messageBody); assertThatExceptionOfType(SqsLargeMessageAspect.FailedProcessingLargePayloadException.class) @@ -175,15 +218,6 @@ public void close() throws IOException { 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); 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 index b0d8177ac..3bad9644f 100644 --- 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 @@ -1,11 +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.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; +import software.amazon.lambda.powertools.sqs.SqsLargeMessage; public class LambdaHandlerApiGateway implements RequestHandler { 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 index 63f1573bf..172179057 100644 --- 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 @@ -1,14 +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.sqs.handlers; +import static com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; +import static software.amazon.lambda.powertools.sqs.internal.SqsMessageBatchProcessorAspectTest.interactionClient; + 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) 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 index 653459d82..6e3971269 100644 --- 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 @@ -1,14 +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.sqs.handlers; +import static com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; +import static software.amazon.lambda.powertools.sqs.internal.SqsMessageBatchProcessorAspectTest.interactionClient; + 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) 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 index 926cdb4f5..acfcd7109 100644 --- 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 @@ -1,14 +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.sqs.handlers; +import static com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; +import static software.amazon.lambda.powertools.sqs.internal.SqsMessageBatchProcessorAspectTest.interactionClient; + 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) 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 index ee8c100e6..de096679f 100644 --- 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 @@ -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.sqs.handlers; import com.amazonaws.services.lambda.runtime.Context; 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 index 6eec87301..74ff02e2c 100644 --- 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 @@ -1,18 +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.sqs.handlers; +import static com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; +import static software.amazon.lambda.powertools.sqs.internal.SqsMessageBatchProcessorAspectTest.interactionClient; + 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}) + @SqsBatch(value = InnerMessageHandler.class, nonRetryableExceptions = {IllegalStateException.class, + IllegalArgumentException.class}) public String handleRequest(final SQSEvent sqsEvent, final Context context) { return "Success"; @@ -22,11 +37,11 @@ private class InnerMessageHandler implements SqsMessageHandler { @Override public String process(SQSMessage message) { - if(message.getMessageId().isEmpty()) { + if (message.getMessageId().isEmpty()) { throw new IllegalArgumentException("Invalid message and was moved to DLQ"); } - if("2e1424d4-f796-459a-9696-9c92662ba5da".equals(message.getMessageId())) { + if ("2e1424d4-f796-459a-9696-9c92662ba5da".equals(message.getMessageId())) { throw new RuntimeException("Invalid message and should be reprocessed"); } 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 index 789a7b86d..5b341880e 100644 --- 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 @@ -1,14 +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.sqs.handlers; +import static com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; +import static software.amazon.lambda.powertools.sqs.internal.SqsMessageBatchProcessorAspectTest.interactionClient; + 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 @@ -24,11 +38,11 @@ private class InnerMessageHandler implements SqsMessageHandler { @Override public String process(SQSMessage message) { - if(message.getMessageId().isEmpty()) { + if (message.getMessageId().isEmpty()) { throw new IllegalArgumentException("Invalid message and was moved to DLQ"); } - if("2e1424d4-f796-459a-9696-9c92662ba5da".equals(message.getMessageId())) { + if ("2e1424d4-f796-459a-9696-9c92662ba5da".equals(message.getMessageId())) { throw new RuntimeException("Invalid message and should be reprocessed"); } 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 index 337592004..e96dc5581 100644 --- 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 @@ -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.sqs.handlers; import com.amazonaws.services.lambda.runtime.Context; 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 index 22844ab4c..ff04aba25 100644 --- 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 @@ -1,14 +1,39 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.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.internal; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.util.function.Consumer; -import java.util.stream.Stream; +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; 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 java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.function.Consumer; +import java.util.stream.Stream; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -31,30 +56,24 @@ 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 static final String BUCKET_NAME = "bucketname"; + private static final String BUCKET_KEY = "c71eb2ae-37e0-4265-8909-32f4153faddf"; 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"; + private static Stream exception() { + return Stream.of(Arguments.of(S3Exception.builder() + .message("Service Exception") + .build()), + Arguments.of(SdkClientException.builder() + .message("Client Exception") + .build())); + } @BeforeEach void setUp() { @@ -67,7 +86,9 @@ void setUp() { @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 + "\"}]"); + SQSEvent sqsEvent = messageWithBody( + "[\"software.amazon.payloadoffloading.PayloadS3Pointer\",{\"s3BucketName\":\"" + BUCKET_NAME + + "\",\"s3Key\":\"" + BUCKET_KEY + "\"}]"); String response = requestHandler.handleRequest(sqsEvent, context); @@ -79,13 +100,14 @@ public void testLargeMessage() { verify(s3Client).deleteObject(delete.capture()); Assertions.assertThat(delete.getValue()) - .satisfies((Consumer) deleteObjectRequest -> { - assertThat(deleteObjectRequest.bucket()) - .isEqualTo(BUCKET_NAME); - - assertThat(deleteObjectRequest.key()) - .isEqualTo(BUCKET_KEY); - }); + .satisfies((Consumer) deleteObjectRequest -> + { + assertThat(deleteObjectRequest.bucket()) + .isEqualTo(BUCKET_NAME); + + assertThat(deleteObjectRequest.key()) + .isEqualTo(BUCKET_KEY); + }); } @Test @@ -107,7 +129,9 @@ public void shouldNotProcessSmallMessageBody() { 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 + "\"}]"; + String messageBody = + "[\"software.amazon.payloadoffloading.PayloadS3Pointer\",{\"s3BucketName\":\"" + BUCKET_NAME + + "\",\"s3Key\":\"" + BUCKET_KEY + "\"}]"; SQSEvent sqsEvent = messageWithBody(messageBody); assertThatExceptionOfType(FailedProcessingLargePayloadException.class) @@ -122,7 +146,9 @@ 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 + "\"}]"); + SQSEvent sqsEvent = messageWithBody( + "[\"software.amazon.payloadoffloading.PayloadS3Pointer\",{\"s3BucketName\":\"" + BUCKET_NAME + + "\",\"s3Key\":\"" + BUCKET_KEY + "\"}]"); String response = requestHandler.handleRequest(sqsEvent, context); @@ -131,19 +157,22 @@ public void testLargeMessageWithDeletionOff() { 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"); - } - })); + 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 + "\"}]"; + String messageBody = + "[\"software.amazon.payloadoffloading.PayloadS3Pointer\",{\"s3BucketName\":\"" + BUCKET_NAME + + "\",\"s3Key\":\"" + BUCKET_KEY + "\"}]"; SQSEvent sqsEvent = messageWithBody(messageBody); assertThatExceptionOfType(FailedProcessingLargePayloadException.class) @@ -157,7 +186,9 @@ public void close() throws IOException { public void shouldNotDoAnyProcessingWhenNotSqsEvent() { LambdaHandlerApiGateway handler = new LambdaHandlerApiGateway(); - String messageBody = "[\"software.amazon.payloadoffloading.PayloadS3Pointer\",{\"s3BucketName\":\"" + BUCKET_NAME + "\",\"s3Key\":\"" + BUCKET_KEY + "\"}]"; + String messageBody = + "[\"software.amazon.payloadoffloading.PayloadS3Pointer\",{\"s3BucketName\":\"" + BUCKET_NAME + + "\",\"s3Key\":\"" + BUCKET_KEY + "\"}]"; APIGatewayProxyRequestEvent event = new APIGatewayProxyRequestEvent(); event.setBody(messageBody); @@ -170,16 +201,8 @@ public void shouldNotDoAnyProcessingWhenNotSqsEvent() { } 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())); + return new ResponseInputStream<>(GetObjectResponse.builder().build(), + AbortableInputStream.create(new ByteArrayInputStream("A big message".getBytes()))); } private SQSEvent messageWithBody(String messageBody) { 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 index a65aa486b..b257c1962 100644 --- 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 @@ -1,12 +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.sqs.internal; -import java.io.IOException; -import java.util.HashMap; +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; + 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 java.io.IOException; +import java.util.HashMap; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -27,28 +53,14 @@ 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 final Context context = mock(Context.class); private SQSEvent event; private RequestHandler requestHandler; - private final Context context = mock(Context.class); - @BeforeEach void setUp() throws IOException { overrideSqsClient(sqsClient); @@ -73,21 +85,22 @@ void shouldBatchProcessMessageWithSuccessDeletedOnFailureInBatchFromSQS() { 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"); - }); + .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)); @@ -124,9 +137,10 @@ void shouldBatchProcessAndMoveNonRetryableExceptionToDlq() { " \"maxReceiveCount\": 2\n" + "}"); - when(sqsClient.getQueueAttributes(any(GetQueueAttributesRequest.class))).thenReturn(GetQueueAttributesResponse.builder() - .attributes(attributes) - .build()); + when(sqsClient.getQueueAttributes(any(GetQueueAttributesRequest.class))).thenReturn( + GetQueueAttributesResponse.builder() + .attributes(attributes) + .build()); requestHandler.handleRequest(event, context); @@ -140,13 +154,14 @@ void shouldBatchProcessAndThrowExceptionForNonRetryableExceptionWhenMoveToDlqRet requestHandler = new SqsMessageHandlerWithNonRetryableHandler(); event.getRecords().get(0).setMessageId(""); - when(sqsClient.sendMessageBatch(any(SendMessageBatchRequest.class))).thenReturn(SendMessageBatchResponse.builder() + when(sqsClient.sendMessageBatch(any(SendMessageBatchRequest.class))).thenReturn( + SendMessageBatchResponse.builder() .failed(BatchResultErrorEntry.builder() .message("Permission Error") .code("KMS.AccessDeniedException") .senderFault(true) .build()) - .build()); + .build()); HashMap attributes = new HashMap<>(); @@ -155,9 +170,10 @@ void shouldBatchProcessAndThrowExceptionForNonRetryableExceptionWhenMoveToDlqRet " \"maxReceiveCount\": 2\n" + "}"); - when(sqsClient.getQueueAttributes(any(GetQueueAttributesRequest.class))).thenReturn(GetQueueAttributesResponse.builder() - .attributes(attributes) - .build()); + when(sqsClient.getQueueAttributes(any(GetQueueAttributesRequest.class))).thenReturn( + GetQueueAttributesResponse.builder() + .attributes(attributes) + .build()); Assertions.assertThatExceptionOfType(SQSBatchProcessingException.class). isThrownBy(() -> requestHandler.handleRequest(event, context)); @@ -192,28 +208,31 @@ void shouldBatchProcessAndFailWithExceptionForNonRetryableExceptionAndNoDlq() { requestHandler = new SqsMessageHandlerWithNonRetryableHandler(); event.getRecords().get(0).setMessageId(""); - event.getRecords().forEach(sqsMessage -> sqsMessage.setEventSourceArn(sqsMessage.getEventSourceArn() + "-temp")); + event.getRecords() + .forEach(sqsMessage -> sqsMessage.setEventSourceArn(sqsMessage.getEventSourceArn() + "-temp")); - when(sqsClient.getQueueAttributes(any(GetQueueAttributesRequest.class))).thenReturn(GetQueueAttributesResponse.builder() - .build()); + 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"); - }); + .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)); @@ -224,33 +243,36 @@ void shouldBatchProcessAndFailWithExceptionForNonRetryableExceptionAndNoDlq() { void shouldBatchProcessAndFailWithExceptionForNonRetryableExceptionWhenFailedParsingPolicy() { requestHandler = new SqsMessageHandlerWithNonRetryableHandler(); event.getRecords().get(0).setMessageId(""); - event.getRecords().forEach(sqsMessage -> sqsMessage.setEventSourceArn(sqsMessage.getEventSourceArn() + "-temp-queue")); + 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()); + 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"); - }); + .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)); @@ -271,27 +293,29 @@ void shouldBatchProcessAndMoveNonRetryableExceptionToDlqAndThrowException() thro " \"maxReceiveCount\": 2\n" + "}"); - when(sqsClient.getQueueAttributes(any(GetQueueAttributesRequest.class))).thenReturn(GetQueueAttributesResponse.builder() - .attributes(attributes) - .build()); + 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"); - }); + .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)); diff --git a/powertools-test-suite/pom.xml b/powertools-test-suite/pom.xml index 5737296da..4a79d2987 100644 --- a/powertools-test-suite/pom.xml +++ b/powertools-test-suite/pom.xml @@ -1,4 +1,18 @@ + + 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 index 7c3e79112..55349b267 100644 --- 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 @@ -1,6 +1,35 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.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.testsuite; +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; + +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 java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -12,13 +41,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.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; @@ -36,15 +58,6 @@ 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"; @@ -79,25 +92,30 @@ void tearDown() { * 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()))); + 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 + "\"}]"); + 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)); + .satisfies(line -> + { + Map actual = parseToMap(line.get(0)); - String message = actual.get("message").toString(); + String message = actual.get("message").toString(); - assertThat(message) - .contains("A big message"); - }); + assertThat(message) + .contains("A big message"); + }); } @Test @@ -107,7 +125,8 @@ public void testLoggingAnnotationActsAfterTracingForStreamingHandler() throws IO S3EventNotification s3EventNotification = s3EventNotification(); TracingLoggingStreamMessageHandler handler = new TracingLoggingStreamMessageHandler(); - handler.handleRequest(new ByteArrayInputStream(new ObjectMapper().writeValueAsBytes(s3EventNotification)), output, context); + handler.handleRequest(new ByteArrayInputStream(new ObjectMapper().writeValueAsBytes(s3EventNotification)), + output, context); assertThat(new String(output.toByteArray(), StandardCharsets.UTF_8)) .isNotEmpty(); @@ -121,7 +140,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); @@ -138,25 +158,27 @@ private Map parseToMap(String stringAsJson) { } 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") - ); + 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-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 index a85c81b1d..5592b1fd3 100644 --- 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 @@ -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.testsuite.handler; import com.amazonaws.services.lambda.runtime.Context; 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 index d0f2b3ac5..4a60d0949 100644 --- 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 @@ -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 software.amazon.lambda.powertools.testsuite.handler; +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.tracing.Tracing; diff --git a/powertools-tracing/pom.xml b/powertools-tracing/pom.xml index 42a229f42..b5de90f7b 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 1469183ef..daec3aa99 100644 --- a/powertools-validation/pom.xml +++ b/powertools-validation/pom.xml @@ -1,4 +1,18 @@ + + @@ -119,4 +133,13 @@ + + + + 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 b29adf8d7..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,7 +27,7 @@ /** * 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}. */ @@ -62,7 +63,7 @@ public void setSchemaVersion(SpecVersion.VersionFlag version) { * {@link Base64Function} and {@link Base64GZipFunction} are already built-in. * * @param function the function to add - * @param Must extend {@link BaseFunction} + * @param Must extend {@link BaseFunction} */ public void addFunction(T function) { JsonConfig.get().addFunction(function); 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 3c2322edc..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,6 +11,7 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.validation; import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer; @@ -22,8 +23,6 @@ import com.networknt.schema.JsonSchema; import com.networknt.schema.ValidationMessage; import io.burt.jmespath.Expression; -import software.amazon.lambda.powertools.validation.internal.ValidationAspect; - import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.util.Collections; @@ -31,6 +30,7 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; +import software.amazon.lambda.powertools.validation.internal.ValidationAspect; /** * Validation utility, used to manually validate Json against Json Schema @@ -68,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")); @@ -89,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() + "'"); @@ -208,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); } @@ -255,7 +259,8 @@ private static JsonSchema createJsonSchema(String schema) { jsonSchema = ValidationConfig.get().getFactory().getSchema(schemaStream); } catch (Exception e) { - throw new IllegalArgumentException("'" + schema + "' is invalid, verify '" + filePath + "' is in your classpath"); + throw new IllegalArgumentException( + "'" + schema + "' is invalid, verify '" + filePath + "' is in your classpath"); } } else { jsonSchema = ValidationConfig.get().getFactory().getSchema(schema); @@ -270,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 e659abbeb..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,8 +11,17 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.validation.internal; +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; @@ -43,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 */ @@ -108,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()); } } } @@ -151,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 6669e46b1..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,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.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.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; - public class ValidationUtilsTest { private String schemaString = "classpath:/schema_v7.json"; @@ -43,16 +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.V7); - assertThatNoException().isThrownBy(() -> { - getJsonSchema("classpath:/schema_v7_ko.json", false); - }); + assertThatNoException().isThrownBy(() -> + { + getJsonSchema("classpath:/schema_v7_ko.json", false); + }); } @Test @@ -99,16 +114,19 @@ 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")); - assertThatNoException().isThrownBy(() -> { - validate(node, schemaString); - }); + 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)); } @@ -121,9 +139,10 @@ public void testValidateMapOK() { map.put("name", "FooBar XY"); map.put("price", 258); - assertThatNoException().isThrownBy(() -> { - validate(map, schemaString); - }); + assertThatNoException().isThrownBy(() -> + { + validate(map, schemaString); + }); } @Test @@ -147,9 +166,10 @@ public void testValidateMapNotValidJsonObject() { public void testValidateStringOK() { String json = "{\n \"id\": 43242,\n \"name\": \"FooBar XY\",\n \"price\": 258\n}"; - assertThatNoException().isThrownBy(() -> { - validate(json, schemaString); - }); + assertThatNoException().isThrownBy(() -> + { + validate(json, schemaString); + }); } @Test @@ -163,9 +183,10 @@ public void testValidateStringKO() { public void testValidateObjectOK() { Product product = new Product(42, "FooBar", 42); - assertThatNoException().isThrownBy(() -> { - validate(product, schemaString); - }); + assertThatNoException().isThrownBy(() -> + { + validate(product, schemaString); + }); } @Test @@ -189,9 +210,10 @@ public void testValidateSubObjectOK() { basket.add(product2); MyCustomEvent event = new MyCustomEvent(basket); - assertThatNoException().isThrownBy(() -> { - validate(event, schemaString, "basket.products[0]"); - }); + assertThatNoException().isThrownBy(() -> + { + validate(event, schemaString, "basket.products[0]"); + }); } @Test @@ -203,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 @@ -227,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 diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/GenericSchemaV7Handler.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/GenericSchemaV7Handler.java index 1c64e7da1..5b8343d1b 100644 --- a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/GenericSchemaV7Handler.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,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/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/ValidationInboundStringHandler.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/ValidationInboundStringHandler.java index 2e62ba88d..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; diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ResponseEventsArgumentsProvider.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ResponseEventsArgumentsProvider.java index 9df0ff508..b634d6f8c 100644 --- a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ResponseEventsArgumentsProvider.java +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ResponseEventsArgumentsProvider.java @@ -11,6 +11,7 @@ * limitations under the License. * */ + package software.amazon.lambda.powertools.validation.internal; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; @@ -18,15 +19,14 @@ import com.amazonaws.services.lambda.runtime.events.APIGatewayV2WebSocketResponse; import com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerResponseEvent; import com.amazonaws.services.lambda.runtime.events.KinesisAnalyticsInputPreprocessingResponse; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; - import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.stream.Stream; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; public class ResponseEventsArgumentsProvider implements ArgumentsProvider { @@ -49,9 +49,11 @@ public Stream provideArguments(ExtensionContext context) { 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)); + records.add(new KinesisAnalyticsInputPreprocessingResponse.Record("1", + KinesisAnalyticsInputPreprocessingResponse.Result.Ok, buffer)); kaipResponse.setRecords(records); - return Stream.of(apiGWProxyResponseEvent, apiGWV2HTTPResponse, apiGWV2WebSocketResponse, albResponseEvent, kaipResponse).map(Arguments::of); + 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 63e93c3ac..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,8 +11,14 @@ * 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; @@ -34,6 +40,8 @@ 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; @@ -53,14 +61,6 @@ import software.amazon.lambda.powertools.validation.handlers.ValidationInboundStringHandler; import software.amazon.lambda.powertools.validation.model.MyCustomEvent; -import java.io.IOException; -import java.util.stream.Stream; - -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; - public class ValidationAspectTest { @@ -109,9 +109,10 @@ public void testValidateOutboundJsonSchema(Object object) throws Throwable { when(validation.inboundSchema()).thenReturn(""); when(validation.outboundSchema()).thenReturn("classpath:/schema_v7.json"); - assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> { - validationAspect.around(pjp, validation); - }); + assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> + { + validationAspect.around(pjp, validation); + }); } @Test @@ -184,7 +185,8 @@ 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")); GenericSchemaV7Handler handler = new GenericSchemaV7Handler(); @@ -193,7 +195,8 @@ public void validate_SQS() { @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(); @@ -202,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(); @@ -211,7 +215,8 @@ 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")); GenericSchemaV7Handler handler = new GenericSchemaV7Handler(); @@ -221,7 +226,8 @@ public void validate_Kinesis() { @ParameterizedTest @MethodSource("provideEventAndEventType") public void validateEEvent(String jsonResource, Class eventClass) throws IOException { - Object event = ValidationConfig.get().getObjectMapper().readValue(this.getClass().getResourceAsStream(jsonResource), eventClass); + Object event = ValidationConfig.get().getObjectMapper() + .readValue(this.getClass().getResourceAsStream(jsonResource), eventClass); 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 398f67265..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; 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 { From 8548397747be3dfc70eea10b7a3084f6ce15c637 Mon Sep 17 00:00:00 2001 From: Jerome Van Der Linden Date: Thu, 27 Jul 2023 11:39:50 +0200 Subject: [PATCH 37/55] few formatting things --- .../test/java/software/amazon/lambda/powertools/TracingE2ET.java | 1 - .../powertools/logging/internal/AbstractJacksonLayoutCopy.java | 1 + .../lambda/powertools/parameters/internal/AnotherObject.java | 1 + .../powertools/parameters/transform/ObjectToDeserialize.java | 1 + .../software/amazon/lambda/powertools/utilities/JsonConfig.java | 1 + 5 files changed, 4 insertions(+), 1 deletion(-) 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 cd4a21df4..043de5494 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 @@ -33,7 +33,6 @@ public class TracingE2ET { private static final String service = "TracingE2EService_" + UUID.randomUUID(); - // "TracingE2EService_e479fb27-422b-4107-9f8c-086c62e1cd12"; private static Infrastructure infrastructure; private static String functionName; 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 f98e2ee46..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 @@ -56,6 +56,7 @@ abstract class AbstractJacksonLayoutCopy extends AbstractStringLayout { protected final boolean complete; protected final boolean includeNullDelimiter; protected final ResolvableKeyValuePair[] additionalFields; + @Deprecated protected AbstractJacksonLayoutCopy(final Configuration config, final ObjectWriter objectWriter, final Charset charset, 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 074a08844..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 @@ -18,6 +18,7 @@ public class AnotherObject { private String another; private int object; + public AnotherObject() { } 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 1d09fbeda..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 @@ -19,6 +19,7 @@ public class ObjectToDeserialize { private String foo; private int bar; private long baz; + public ObjectToDeserialize() { } 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 f5a6d8c11..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 @@ -38,6 +38,7 @@ public class JsonConfig { .withFunctionRegistry(customFunctions) .build(); private JmesPath jmesPath = new JacksonRuntime(configuration, getObjectMapper()); + private JsonConfig() { } From 0671bbeaf2366502fb603ae6c79dbb1865bb2067 Mon Sep 17 00:00:00 2001 From: Alexey Soshin Date: Thu, 27 Jul 2023 12:42:02 +0100 Subject: [PATCH 38/55] Adding external examples from https://github.com/aws/aws-sam-cli-app-templates (#1318) --- examples/README.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/examples/README.md b/examples/README.md index 1869b4e8f..b44ff2433 100644 --- a/examples/README.md +++ b/examples/README.md @@ -3,7 +3,7 @@ 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. -## The Examples +## 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 @@ -51,6 +51,17 @@ The first command will build the source of your application. The second command 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. From 661100c14b712b940d8af85f7623525012ae9466 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 27 Jul 2023 14:07:41 +0200 Subject: [PATCH 39/55] build(deps): bump com.puppycrawl.tools:checkstyle from 10.9.1 to 10.12.1 (#1320) Bumps [com.puppycrawl.tools:checkstyle](https://github.com/checkstyle/checkstyle) from 10.9.1 to 10.12.1. - [Release notes](https://github.com/checkstyle/checkstyle/releases) - [Commits](https://github.com/checkstyle/checkstyle/compare/checkstyle-10.9.1...checkstyle-10.12.1) --- updated-dependencies: - dependency-name: com.puppycrawl.tools:checkstyle dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7744913fd..e8f85e3ff 100644 --- a/pom.xml +++ b/pom.xml @@ -564,7 +564,7 @@ com.puppycrawl.tools checkstyle - 10.9.1 + 10.12.1 From 3dc84051a1a32cf90b8e3d716fe608e707ee4c16 Mon Sep 17 00:00:00 2001 From: Grzegorz Kozub Date: Fri, 28 Jul 2023 08:56:43 +0200 Subject: [PATCH 40/55] docs: add support for docs versioning (#1239) (#1293) * docs: add support for docs versioning (#1239) * docs: change configuration to release docs only during the release or by hand. Rollback config for building doc for PR. (#1239) * docs: Uncomment logic responsible for pushing changes to S3.(#1239) --------- Co-authored-by: Scott Gerring --- .github/workflows/build-docs.yml | 2 +- .github/workflows/docs.yml | 41 ------- .github/workflows/rebuild-latest-docs.yml | 39 ++++++ .github/workflows/release-doc.yml | 19 +++ .github/workflows/reusable-publish-docs.yml | 124 ++++++++++++++++++++ mkdocs.yml | 3 + 6 files changed, 186 insertions(+), 42 deletions(-) delete mode 100644 .github/workflows/docs.yml create mode 100644 .github/workflows/rebuild-latest-docs.yml create mode 100644 .github/workflows/release-doc.yml create mode 100644 .github/workflows/reusable-publish-docs.yml diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index cbcdcdd9e..15d08587d 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -34,4 +34,4 @@ jobs: - name: Build docs website run: | echo "GIT_PYTHON_REFRESH=quiet" - make build-docs-website \ No newline at end of file + make build-docs-website diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml deleted file mode 100644 index 9c09e294d..000000000 --- a/.github/workflows/docs.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Docs - -on: - release: - types: - - published - workflow_dispatch: {} - -permissions: - id-token: write - contents: write - pages: write - -jobs: - docs: - runs-on: ubuntu-latest - environment: Docs - steps: - - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: "3.8" - - name: Capture branch and tag - id: branch_name - run: | - echo "SOURCE_BRANCH=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV - echo "SOURCE_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV - - name: Build docs website - run: | - make build-docs-website - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@e1e17a757e536f70e52b5a12b2e8d1d1c60e04ef - with: - aws-region: us-east-1 - role-to-assume: ${{ secrets.AWS_DOCS_ROLE_ARN }} - - name: Deploy Docs - run: | - aws s3 sync \ - dist \ - s3://${{ secrets.AWS_DOCS_BUCKET }}/lambda-java/ diff --git a/.github/workflows/rebuild-latest-docs.yml b/.github/workflows/rebuild-latest-docs.yml new file mode 100644 index 000000000..853aaa1d3 --- /dev/null +++ b/.github/workflows/rebuild-latest-docs.yml @@ -0,0 +1,39 @@ +name: Rebuild latest docs + +# PROCESS +# +# 1. Build User Guide and API docs +# 2. Publish to GitHub Pages +# 3. Publish to S3 (new home) + +# USAGE +# +# Only used for deploying a documentation hotfix to /latest and its associated version w/o a full release. +# +# Steps: +# +# 1. Trigger "Rebuild latest docs" workflow manually: https://docs.github.com/en/actions/managing-workflow-runs/manually-running-a-workflow +# 2. Use the latest version released under Releases e.g. 2.0.0 + +on: + workflow_dispatch: + inputs: + latest_published_version: + description: "Latest published version to rebuild latest docs for, e.g. 1.4.2" + default: "1.4.2" + required: true + +permissions: + contents: read + +jobs: + release-docs: + permissions: + contents: write # push to gh-pages + pages: write # deploy gh-pages website + id-token: write # trade JWT token for AWS credentials in AWS Docs account + secrets: inherit + uses: ./.github/workflows/reusable_publish_docs.yml + with: + version: ${{ inputs.latest_published_version }} + alias: latest diff --git a/.github/workflows/release-doc.yml b/.github/workflows/release-doc.yml new file mode 100644 index 000000000..5f87f970a --- /dev/null +++ b/.github/workflows/release-doc.yml @@ -0,0 +1,19 @@ +name: Docs + +on: + release: + types: + - published + workflow_dispatch: {} + +jobs: + release-docs: + permissions: + contents: write + pages: write + id-token: write + secrets: inherit + uses: ./.github/workflows/reusable-publish-docs.yml + with: + version: main + alias: stage \ No newline at end of file diff --git a/.github/workflows/reusable-publish-docs.yml b/.github/workflows/reusable-publish-docs.yml new file mode 100644 index 000000000..9ab53dac2 --- /dev/null +++ b/.github/workflows/reusable-publish-docs.yml @@ -0,0 +1,124 @@ +name: Reusable Publish docs + +env: + BRANCH: main + ORIGIN: aws-powertools/powertools-lambda-java + VERSION: "" + +on: + workflow_call: + inputs: + version: + description: "Version to build and publish docs (1.28.0, develop)" + required: true + type: string + alias: + description: "Alias to associate version (latest, stage)" + required: true + type: string + detached_mode: + description: "Whether it's running in git detached mode to ensure git is sync'd" + required: false + default: false + type: boolean + +permissions: + contents: write + id-token: write + pages: write + +jobs: + publish-docs: + runs-on: ubuntu-latest + environment: Docs + steps: + - name: Checkout code + uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + with: + # While `fetch-depth` is used to allow the workflow to later commit & push the changes. + fetch-depth: 0 + - name: Setup dependencies + uses: ./.github/actions/cached-node-modules + - name: Set up Python + uses: actions/setup-python@57ded4d7d5e986d7296eab16560982c6dd7c923b # v4.6.0 + with: + python-version: "3.8" + - name: Install doc generation dependencies + run: | + pip install --upgrade pip + pip install -r docs/requirements.txt + - name: Setup doc deploy + run: | + git config --global user.name Docs deploy + git config --global user.email aws-devax-open-source@amazon.com + - name: Git refresh tip (detached mode) + # Git Detached mode (release notes) doesn't have origin + if: ${{ inputs.detached_mode }} + run: | + git config pull.rebase true + git config remote.origin.url >&- || git remote add origin https://github.com/"$ORIGIN" + git pull origin "$BRANCH" + - name: Normalize Version Number + run: echo "VERSION=$(echo ${{ inputs.version }} | sed 's/v//')" >> $GITHUB_ENV + - name: Build docs website and API reference + env: + ALIAS: ${{ inputs.alias }} + run: | + rm -rf site + mkdocs build + mike deploy --update-aliases --no-redirect ${{ env.VERSION }} ${{ env.ALIAS }} --branch backup-gh-pages + # Set latest version as a default + mike set-default latest --branch backup-gh-pages + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@e1e17a757e536f70e52b5a12b2e8d1d1c60e04ef # v2.0.0 + with: + aws-region: us-east-1 + role-to-assume: ${{ secrets.AWS_DOCS_ROLE_ARN }} + - name: Copy API Docs + run: | + cp -r api site/ + + - name: Deploy Docs (Version) + env: + VERSION: ${{ inputs.version }} + ALIAS: ${{ inputs.alias }} + run: | + aws s3 sync \ + site/ \ + s3://${{ secrets.AWS_DOCS_BUCKET }}/lambda-java/${{ env.VERSION }}/ + - name: Deploy Docs (Alias) + env: + VERSION: ${{ inputs.version }} + ALIAS: ${{ inputs.alias }} + run: | + aws s3 sync \ + site/ \ + s3://${{ secrets.AWS_DOCS_BUCKET }}/lambda-java/${{ env.ALIAS }}/ + - name: Deploy Docs (Version JSON) + env: + VERSION: ${{ inputs.version }} + ALIAS: ${{ inputs.alias }} + + + # We originally used "mike" from PyPi to manage versions for us, but since we moved to S3, we can't use it to manage versions any more. + # Instead, we're using some shell script that manages the versions. + # + # Operations: + # 1. Download the versions.json file from S3 + # 2. Find any reference to the alias and delete it from the versions file + # 3. This is voodoo (don't use JQ): + # - we assign the input as $o and the new version/alias as $n, + # - we check if the version number exists in the file already (for republishing docs) + # - if it's an alias (stage/latest/*) or old version, we do nothing and output $o (original input) + # - if it's a new version number, we add it at position 0 in the array. + # 4. Once done, we'll upload it back to S3. + + run: | + aws s3 cp \ + s3://${{ secrets.AWS_DOCS_BUCKET }}/lambda-java/versions.json \ + versions_old.json + jq 'del(.[].aliases[] | select(. == "${{ env.ALIAS }}"))' < versions_old.json > versions_proc.json + jq '. as $o | [{"title": "${{ env.VERSION }}", "version": "${{ env.VERSION }}", "aliases": ["${{env.ALIAS}}"] }] as $n | $n | if .[0].title | test("[a-z]+") or any($o[].title == "${{ env.VERSION }}";.) then $o else $n + $o end' < versions_proc.json > versions.json + aws s3 cp \ + versions.json \ + s3://${{ secrets.AWS_DOCS_BUCKET }}/lambda-java/versions.json diff --git a/mkdocs.yml b/mkdocs.yml index da00b24d1..05f02c5ca 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -82,6 +82,9 @@ extra_javascript: - javascript/extra.js extra: + version: + provider: mike + default: latest powertools: version: 1.16.1 # to update after each release (we do not want snapshot version here) From a0c7d3860dd86c5f991c21e55177917d7119cd50 Mon Sep 17 00:00:00 2001 From: Grzegorz Kozub Date: Fri, 28 Jul 2023 10:02:09 +0200 Subject: [PATCH 41/55] docs: versioning - fix typo (#1322) * docs: add support for docs versioning (#1239) * docs: change configuration to release docs only during the release or by hand. Rollback config for building doc for PR. (#1239) * docs: HotFix - workflow path(#1239) * docs: HotFix - workflow path(#1239) * docs: typo. (#1239) --- .github/workflows/rebuild-latest-docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rebuild-latest-docs.yml b/.github/workflows/rebuild-latest-docs.yml index 853aaa1d3..f176ca901 100644 --- a/.github/workflows/rebuild-latest-docs.yml +++ b/.github/workflows/rebuild-latest-docs.yml @@ -33,7 +33,7 @@ jobs: pages: write # deploy gh-pages website id-token: write # trade JWT token for AWS credentials in AWS Docs account secrets: inherit - uses: ./.github/workflows/reusable_publish_docs.yml + uses: ./.github/workflows/reusable-publish-docs.yml with: version: ${{ inputs.latest_published_version }} alias: latest From bddd99a84be8cdacfc18e53c710647434ed058f8 Mon Sep 17 00:00:00 2001 From: Scott Gerring Date: Fri, 28 Jul 2023 09:59:55 +0100 Subject: [PATCH 42/55] fix: Rollback doc changes (#1323) * Revert "docs: versioning - fix typo (#1322)" This reverts commit a0c7d3860dd86c5f991c21e55177917d7119cd50. * Revert "docs: add support for docs versioning (#1239) (#1293)" This reverts commit 3dc84051a1a32cf90b8e3d716fe608e707ee4c16. --- .github/workflows/build-docs.yml | 2 +- .github/workflows/docs.yml | 41 +++++++ .github/workflows/rebuild-latest-docs.yml | 39 ------ .github/workflows/release-doc.yml | 19 --- .github/workflows/reusable-publish-docs.yml | 124 -------------------- mkdocs.yml | 3 - 6 files changed, 42 insertions(+), 186 deletions(-) create mode 100644 .github/workflows/docs.yml delete mode 100644 .github/workflows/rebuild-latest-docs.yml delete mode 100644 .github/workflows/release-doc.yml delete mode 100644 .github/workflows/reusable-publish-docs.yml diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 15d08587d..cbcdcdd9e 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -34,4 +34,4 @@ jobs: - name: Build docs website run: | echo "GIT_PYTHON_REFRESH=quiet" - make build-docs-website + make build-docs-website \ No newline at end of file diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 000000000..9c09e294d --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,41 @@ +name: Docs + +on: + release: + types: + - published + workflow_dispatch: {} + +permissions: + id-token: write + contents: write + pages: write + +jobs: + docs: + runs-on: ubuntu-latest + environment: Docs + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.8" + - name: Capture branch and tag + id: branch_name + run: | + echo "SOURCE_BRANCH=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV + echo "SOURCE_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV + - name: Build docs website + run: | + make build-docs-website + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@e1e17a757e536f70e52b5a12b2e8d1d1c60e04ef + with: + aws-region: us-east-1 + role-to-assume: ${{ secrets.AWS_DOCS_ROLE_ARN }} + - name: Deploy Docs + run: | + aws s3 sync \ + dist \ + s3://${{ secrets.AWS_DOCS_BUCKET }}/lambda-java/ diff --git a/.github/workflows/rebuild-latest-docs.yml b/.github/workflows/rebuild-latest-docs.yml deleted file mode 100644 index f176ca901..000000000 --- a/.github/workflows/rebuild-latest-docs.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Rebuild latest docs - -# PROCESS -# -# 1. Build User Guide and API docs -# 2. Publish to GitHub Pages -# 3. Publish to S3 (new home) - -# USAGE -# -# Only used for deploying a documentation hotfix to /latest and its associated version w/o a full release. -# -# Steps: -# -# 1. Trigger "Rebuild latest docs" workflow manually: https://docs.github.com/en/actions/managing-workflow-runs/manually-running-a-workflow -# 2. Use the latest version released under Releases e.g. 2.0.0 - -on: - workflow_dispatch: - inputs: - latest_published_version: - description: "Latest published version to rebuild latest docs for, e.g. 1.4.2" - default: "1.4.2" - required: true - -permissions: - contents: read - -jobs: - release-docs: - permissions: - contents: write # push to gh-pages - pages: write # deploy gh-pages website - id-token: write # trade JWT token for AWS credentials in AWS Docs account - secrets: inherit - uses: ./.github/workflows/reusable-publish-docs.yml - with: - version: ${{ inputs.latest_published_version }} - alias: latest diff --git a/.github/workflows/release-doc.yml b/.github/workflows/release-doc.yml deleted file mode 100644 index 5f87f970a..000000000 --- a/.github/workflows/release-doc.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: Docs - -on: - release: - types: - - published - workflow_dispatch: {} - -jobs: - release-docs: - permissions: - contents: write - pages: write - id-token: write - secrets: inherit - uses: ./.github/workflows/reusable-publish-docs.yml - with: - version: main - alias: stage \ No newline at end of file diff --git a/.github/workflows/reusable-publish-docs.yml b/.github/workflows/reusable-publish-docs.yml deleted file mode 100644 index 9ab53dac2..000000000 --- a/.github/workflows/reusable-publish-docs.yml +++ /dev/null @@ -1,124 +0,0 @@ -name: Reusable Publish docs - -env: - BRANCH: main - ORIGIN: aws-powertools/powertools-lambda-java - VERSION: "" - -on: - workflow_call: - inputs: - version: - description: "Version to build and publish docs (1.28.0, develop)" - required: true - type: string - alias: - description: "Alias to associate version (latest, stage)" - required: true - type: string - detached_mode: - description: "Whether it's running in git detached mode to ensure git is sync'd" - required: false - default: false - type: boolean - -permissions: - contents: write - id-token: write - pages: write - -jobs: - publish-docs: - runs-on: ubuntu-latest - environment: Docs - steps: - - name: Checkout code - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 - with: - # While `fetch-depth` is used to allow the workflow to later commit & push the changes. - fetch-depth: 0 - - name: Setup dependencies - uses: ./.github/actions/cached-node-modules - - name: Set up Python - uses: actions/setup-python@57ded4d7d5e986d7296eab16560982c6dd7c923b # v4.6.0 - with: - python-version: "3.8" - - name: Install doc generation dependencies - run: | - pip install --upgrade pip - pip install -r docs/requirements.txt - - name: Setup doc deploy - run: | - git config --global user.name Docs deploy - git config --global user.email aws-devax-open-source@amazon.com - - name: Git refresh tip (detached mode) - # Git Detached mode (release notes) doesn't have origin - if: ${{ inputs.detached_mode }} - run: | - git config pull.rebase true - git config remote.origin.url >&- || git remote add origin https://github.com/"$ORIGIN" - git pull origin "$BRANCH" - - name: Normalize Version Number - run: echo "VERSION=$(echo ${{ inputs.version }} | sed 's/v//')" >> $GITHUB_ENV - - name: Build docs website and API reference - env: - ALIAS: ${{ inputs.alias }} - run: | - rm -rf site - mkdocs build - mike deploy --update-aliases --no-redirect ${{ env.VERSION }} ${{ env.ALIAS }} --branch backup-gh-pages - # Set latest version as a default - mike set-default latest --branch backup-gh-pages - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@e1e17a757e536f70e52b5a12b2e8d1d1c60e04ef # v2.0.0 - with: - aws-region: us-east-1 - role-to-assume: ${{ secrets.AWS_DOCS_ROLE_ARN }} - - name: Copy API Docs - run: | - cp -r api site/ - - - name: Deploy Docs (Version) - env: - VERSION: ${{ inputs.version }} - ALIAS: ${{ inputs.alias }} - run: | - aws s3 sync \ - site/ \ - s3://${{ secrets.AWS_DOCS_BUCKET }}/lambda-java/${{ env.VERSION }}/ - - name: Deploy Docs (Alias) - env: - VERSION: ${{ inputs.version }} - ALIAS: ${{ inputs.alias }} - run: | - aws s3 sync \ - site/ \ - s3://${{ secrets.AWS_DOCS_BUCKET }}/lambda-java/${{ env.ALIAS }}/ - - name: Deploy Docs (Version JSON) - env: - VERSION: ${{ inputs.version }} - ALIAS: ${{ inputs.alias }} - - - # We originally used "mike" from PyPi to manage versions for us, but since we moved to S3, we can't use it to manage versions any more. - # Instead, we're using some shell script that manages the versions. - # - # Operations: - # 1. Download the versions.json file from S3 - # 2. Find any reference to the alias and delete it from the versions file - # 3. This is voodoo (don't use JQ): - # - we assign the input as $o and the new version/alias as $n, - # - we check if the version number exists in the file already (for republishing docs) - # - if it's an alias (stage/latest/*) or old version, we do nothing and output $o (original input) - # - if it's a new version number, we add it at position 0 in the array. - # 4. Once done, we'll upload it back to S3. - - run: | - aws s3 cp \ - s3://${{ secrets.AWS_DOCS_BUCKET }}/lambda-java/versions.json \ - versions_old.json - jq 'del(.[].aliases[] | select(. == "${{ env.ALIAS }}"))' < versions_old.json > versions_proc.json - jq '. as $o | [{"title": "${{ env.VERSION }}", "version": "${{ env.VERSION }}", "aliases": ["${{env.ALIAS}}"] }] as $n | $n | if .[0].title | test("[a-z]+") or any($o[].title == "${{ env.VERSION }}";.) then $o else $n + $o end' < versions_proc.json > versions.json - aws s3 cp \ - versions.json \ - s3://${{ secrets.AWS_DOCS_BUCKET }}/lambda-java/versions.json diff --git a/mkdocs.yml b/mkdocs.yml index 05f02c5ca..da00b24d1 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -82,9 +82,6 @@ extra_javascript: - javascript/extra.js extra: - version: - provider: mike - default: latest powertools: version: 1.16.1 # to update after each release (we do not want snapshot version here) From a2ad29251a7ade7433030b3efcb901573c2e5d5b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 28 Jul 2023 13:24:40 +0200 Subject: [PATCH 43/55] build(deps): bump aws.sdk.version from 2.20.111 to 2.20.114 (#1324) Bumps `aws.sdk.version` from 2.20.111 to 2.20.114. Updates `software.amazon.awssdk:bom` from 2.20.111 to 2.20.114 Updates `software.amazon.awssdk:http-client-spi` from 2.20.111 to 2.20.114 Updates `software.amazon.awssdk:url-connection-client` from 2.20.111 to 2.20.114 Updates `software.amazon.awssdk:s3` from 2.20.111 to 2.20.114 Updates `software.amazon.awssdk:lambda` from 2.20.111 to 2.20.114 Updates `software.amazon.awssdk:cloudwatch` from 2.20.111 to 2.20.114 Updates `software.amazon.awssdk:xray` from 2.20.111 to 2.20.114 Updates `software.amazon.awssdk:cloudformation` from 2.20.111 to 2.20.114 Updates `software.amazon.awssdk:sts` from 2.20.111 to 2.20.114 --- updated-dependencies: - dependency-name: software.amazon.awssdk:bom dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:http-client-spi dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:url-connection-client dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:s3 dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:lambda dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:cloudwatch dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:xray dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:cloudformation dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:sts dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/powertools-examples-cloudformation/pom.xml | 2 +- examples/powertools-examples-sqs/pom.xml | 2 +- pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/powertools-examples-cloudformation/pom.xml b/examples/powertools-examples-cloudformation/pom.xml index 843d8b5e8..2de8c75c4 100644 --- a/examples/powertools-examples-cloudformation/pom.xml +++ b/examples/powertools-examples-cloudformation/pom.xml @@ -16,7 +16,7 @@ true 1.2.2 3.11.2 - 2.20.111 + 2.20.114 diff --git a/examples/powertools-examples-sqs/pom.xml b/examples/powertools-examples-sqs/pom.xml index 78e4b0904..98f7fc9c2 100644 --- a/examples/powertools-examples-sqs/pom.xml +++ b/examples/powertools-examples-sqs/pom.xml @@ -28,7 +28,7 @@ software.amazon.awssdk url-connection-client - 2.20.111 + 2.20.114 com.amazonaws diff --git a/pom.xml b/pom.xml index e8f85e3ff..c1ac76f72 100644 --- a/pom.xml +++ b/pom.xml @@ -74,7 +74,7 @@ 2.20.0 2.15.2 1.9.7 - 2.20.111 + 2.20.114 2.14.0 2.1.3 UTF-8 From 1261e58f837b5f883dcb7214f6a0e0d812f57a40 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Jul 2023 11:17:18 +0200 Subject: [PATCH 44/55] build(deps): bump org.apache.commons:commons-lang3 from 3.12.0 to 3.13.0 (#1325) Bumps org.apache.commons:commons-lang3 from 3.12.0 to 3.13.0. --- updated-dependencies: - dependency-name: org.apache.commons:commons-lang3 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c1ac76f72..b0f7030ec 100644 --- a/pom.xml +++ b/pom.xml @@ -263,7 +263,7 @@ org.apache.commons commons-lang3 - 3.12.0 + 3.13.0 test From 70da5f042ca289ee2584e2da89a32026f1a8cfa4 Mon Sep 17 00:00:00 2001 From: Scott Gerring Date: Mon, 31 Jul 2023 10:23:20 +0100 Subject: [PATCH 45/55] docs: Add maintainers guide (#1326) --- docs/processes/maintainers.md | 247 ++++++++++++++++++++++++++++++++++ mkdocs.yml | 2 + 2 files changed, 249 insertions(+) create mode 100644 docs/processes/maintainers.md 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/mkdocs.yml b/mkdocs.yml index da00b24d1..841a95fae 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -18,6 +18,8 @@ nav: - utilities/validation.md - utilities/custom_resources.md - utilities/serialization.md + - Processes: + - processes/maintainers.md theme: name: material From bc40ea251eaba7320c2f5ef1e2c4883d4c35e02d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Jul 2023 13:58:07 +0200 Subject: [PATCH 46/55] build(deps): bump aws.sdk.version from 2.20.114 to 2.20.115 (#1328) Bumps `aws.sdk.version` from 2.20.114 to 2.20.115. Updates `software.amazon.awssdk:bom` from 2.20.114 to 2.20.115 Updates `software.amazon.awssdk:http-client-spi` from 2.20.114 to 2.20.115 Updates `software.amazon.awssdk:url-connection-client` from 2.20.114 to 2.20.115 Updates `software.amazon.awssdk:s3` from 2.20.114 to 2.20.115 Updates `software.amazon.awssdk:lambda` from 2.20.114 to 2.20.115 Updates `software.amazon.awssdk:cloudwatch` from 2.20.114 to 2.20.115 Updates `software.amazon.awssdk:xray` from 2.20.114 to 2.20.115 Updates `software.amazon.awssdk:cloudformation` from 2.20.114 to 2.20.115 Updates `software.amazon.awssdk:sts` from 2.20.114 to 2.20.115 --- updated-dependencies: - dependency-name: software.amazon.awssdk:bom dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:http-client-spi dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:url-connection-client dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:s3 dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:lambda dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:cloudwatch dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:xray dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:cloudformation dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:sts dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/powertools-examples-cloudformation/pom.xml | 2 +- examples/powertools-examples-sqs/pom.xml | 2 +- pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/powertools-examples-cloudformation/pom.xml b/examples/powertools-examples-cloudformation/pom.xml index 2de8c75c4..5d6d3a4ad 100644 --- a/examples/powertools-examples-cloudformation/pom.xml +++ b/examples/powertools-examples-cloudformation/pom.xml @@ -16,7 +16,7 @@ true 1.2.2 3.11.2 - 2.20.114 + 2.20.115 diff --git a/examples/powertools-examples-sqs/pom.xml b/examples/powertools-examples-sqs/pom.xml index 98f7fc9c2..13cb039bd 100644 --- a/examples/powertools-examples-sqs/pom.xml +++ b/examples/powertools-examples-sqs/pom.xml @@ -28,7 +28,7 @@ software.amazon.awssdk url-connection-client - 2.20.114 + 2.20.115 com.amazonaws diff --git a/pom.xml b/pom.xml index b0f7030ec..9ed8dbdd2 100644 --- a/pom.xml +++ b/pom.xml @@ -74,7 +74,7 @@ 2.20.0 2.15.2 1.9.7 - 2.20.114 + 2.20.115 2.14.0 2.1.3 UTF-8 From e378d4dd4e7e852656995a4dcafc23aedafbfc0b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Jul 2023 14:03:50 +0200 Subject: [PATCH 47/55] build(deps): bump com.puppycrawl.tools:checkstyle (#1329) Bumps [com.puppycrawl.tools:checkstyle](https://github.com/checkstyle/checkstyle) from 10.12.1 to 10.12.2. - [Release notes](https://github.com/checkstyle/checkstyle/releases) - [Commits](https://github.com/checkstyle/checkstyle/compare/checkstyle-10.12.1...checkstyle-10.12.2) --- updated-dependencies: - dependency-name: com.puppycrawl.tools:checkstyle dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 9ed8dbdd2..cab0a3574 100644 --- a/pom.xml +++ b/pom.xml @@ -564,7 +564,7 @@ com.puppycrawl.tools checkstyle - 10.12.1 + 10.12.2 From e2ef948a08eb64887d8dd09f60cda29985fa0b8a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Aug 2023 13:50:34 +0200 Subject: [PATCH 48/55] build(deps): bump aws.sdk.version from 2.20.115 to 2.20.116 (#1331) Bumps `aws.sdk.version` from 2.20.115 to 2.20.116. Updates `software.amazon.awssdk:bom` from 2.20.115 to 2.20.116 Updates `software.amazon.awssdk:http-client-spi` from 2.20.115 to 2.20.116 Updates `software.amazon.awssdk:url-connection-client` from 2.20.115 to 2.20.116 Updates `software.amazon.awssdk:s3` from 2.20.115 to 2.20.116 Updates `software.amazon.awssdk:lambda` from 2.20.115 to 2.20.116 Updates `software.amazon.awssdk:cloudwatch` from 2.20.115 to 2.20.116 Updates `software.amazon.awssdk:xray` from 2.20.115 to 2.20.116 Updates `software.amazon.awssdk:cloudformation` from 2.20.115 to 2.20.116 Updates `software.amazon.awssdk:sts` from 2.20.115 to 2.20.116 --- updated-dependencies: - dependency-name: software.amazon.awssdk:bom dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:http-client-spi dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:url-connection-client dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:s3 dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:lambda dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:cloudwatch dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:xray dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:cloudformation dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:sts dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/powertools-examples-cloudformation/pom.xml | 2 +- examples/powertools-examples-sqs/pom.xml | 2 +- pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/powertools-examples-cloudformation/pom.xml b/examples/powertools-examples-cloudformation/pom.xml index 5d6d3a4ad..20e1b4581 100644 --- a/examples/powertools-examples-cloudformation/pom.xml +++ b/examples/powertools-examples-cloudformation/pom.xml @@ -16,7 +16,7 @@ true 1.2.2 3.11.2 - 2.20.115 + 2.20.116 diff --git a/examples/powertools-examples-sqs/pom.xml b/examples/powertools-examples-sqs/pom.xml index 13cb039bd..e1a4b659e 100644 --- a/examples/powertools-examples-sqs/pom.xml +++ b/examples/powertools-examples-sqs/pom.xml @@ -28,7 +28,7 @@ software.amazon.awssdk url-connection-client - 2.20.115 + 2.20.116 com.amazonaws diff --git a/pom.xml b/pom.xml index cab0a3574..1d80b3081 100644 --- a/pom.xml +++ b/pom.xml @@ -74,7 +74,7 @@ 2.20.0 2.15.2 1.9.7 - 2.20.115 + 2.20.116 2.14.0 2.1.3 UTF-8 From 3a106ca7af82c1b548789d8584d7ea9b52b8959c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Aug 2023 20:06:49 +0200 Subject: [PATCH 49/55] build(deps-dev): bump software.amazon.awscdk:aws-cdk-lib (#1330) --- powertools-e2e-tests/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/powertools-e2e-tests/pom.xml b/powertools-e2e-tests/pom.xml index 3e7f0f460..7e354d15e 100644 --- a/powertools-e2e-tests/pom.xml +++ b/powertools-e2e-tests/pom.xml @@ -31,7 +31,7 @@ 1.8 1.8 10.2.69 - 2.88.0 + 2.89.0 true From a05823bc45407704bda69272b483842d98190a26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Van=20Der=20Linden?= <117538+jeromevdl@users.noreply.github.com> Date: Wed, 2 Aug 2023 11:53:24 +0200 Subject: [PATCH 50/55] feat: large message in SQS and SNS (#1310) --- .github/workflows/run-e2e-tests.yml | 7 + docs/utilities/large_messages.md | 419 ++++++++ docs/utilities/sqs_large_message_handling.md | 121 ++- mkdocs.yml | 2 +- pom.xml | 28 +- .../handlers/idempotency/pom.xml | 12 + .../lambda/powertools/e2e/Function.java | 2 + .../idempotency/src/main/resources/log4j2.xml | 16 + .../handlers/largemessage/pom.xml | 72 ++ .../lambda/powertools/e2e/Function.java | 77 ++ .../src/main/resources/log4j2.xml | 16 + .../handlers/largemessage_idempotent/pom.xml | 77 ++ .../lambda/powertools/e2e/Function.java | 105 ++ .../src/main/resources/log4j2.xml | 16 + powertools-e2e-tests/handlers/pom.xml | 20 + powertools-e2e-tests/pom.xml | 28 + .../lambda/powertools/IdempotencyE2ET.java | 7 +- .../lambda/powertools/LargeMessageE2ET.java | 167 +++ .../LargeMessageIdempotentE2ET.java | 191 ++++ .../amazon/lambda/powertools/LoggingE2ET.java | 5 +- .../amazon/lambda/powertools/MetricsE2ET.java | 5 +- .../lambda/powertools/ParametersE2ET.java | 4 +- .../amazon/lambda/powertools/TracingE2ET.java | 4 +- .../powertools/testutils/Infrastructure.java | 89 +- .../src/test/resources/large_sqs_message.txt | 977 ++++++++++++++++++ powertools-idempotency/pom.xml | 1 - .../internal/IdempotentAspect.java | 3 + powertools-large-messages/pom.xml | 166 +++ .../largemessages/LargeMessage.java | 70 ++ .../largemessages/LargeMessageConfig.java | 83 ++ .../LargeMessageProcessingException.java | 28 + .../internal/LargeMessageAspect.java | 61 ++ .../internal/LargeMessageProcessor.java | 142 +++ .../LargeMessageProcessorFactory.java | 36 + .../internal/LargeSNSMessageProcessor.java | 52 + .../internal/LargeSQSMessageProcessor.java | 173 ++++ .../largemessages/LargeMessageConfigTest.java | 57 + .../internal/LargeMessageAspectTest.java | 333 ++++++ .../LargeMessageProcessorFactoryTest.java | 46 + powertools-sqs/pom.xml | 1 + .../powertools/sqs/SqsLargeMessage.java | 8 +- .../lambda/powertools/sqs/SqsUtils.java | 4 + spotbugs-exclude.xml | 8 + 43 files changed, 3650 insertions(+), 89 deletions(-) create mode 100644 docs/utilities/large_messages.md create mode 100644 powertools-e2e-tests/handlers/idempotency/src/main/resources/log4j2.xml create mode 100644 powertools-e2e-tests/handlers/largemessage/pom.xml create mode 100644 powertools-e2e-tests/handlers/largemessage/src/main/java/software/amazon/lambda/powertools/e2e/Function.java create mode 100644 powertools-e2e-tests/handlers/largemessage/src/main/resources/log4j2.xml create mode 100644 powertools-e2e-tests/handlers/largemessage_idempotent/pom.xml create mode 100644 powertools-e2e-tests/handlers/largemessage_idempotent/src/main/java/software/amazon/lambda/powertools/e2e/Function.java create mode 100644 powertools-e2e-tests/handlers/largemessage_idempotent/src/main/resources/log4j2.xml create mode 100644 powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LargeMessageE2ET.java create mode 100644 powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LargeMessageIdempotentE2ET.java create mode 100644 powertools-e2e-tests/src/test/resources/large_sqs_message.txt create mode 100644 powertools-large-messages/pom.xml create mode 100644 powertools-large-messages/src/main/java/software/amazon/lambda/powertools/largemessages/LargeMessage.java create mode 100644 powertools-large-messages/src/main/java/software/amazon/lambda/powertools/largemessages/LargeMessageConfig.java create mode 100644 powertools-large-messages/src/main/java/software/amazon/lambda/powertools/largemessages/LargeMessageProcessingException.java create mode 100644 powertools-large-messages/src/main/java/software/amazon/lambda/powertools/largemessages/internal/LargeMessageAspect.java create mode 100644 powertools-large-messages/src/main/java/software/amazon/lambda/powertools/largemessages/internal/LargeMessageProcessor.java create mode 100644 powertools-large-messages/src/main/java/software/amazon/lambda/powertools/largemessages/internal/LargeMessageProcessorFactory.java create mode 100644 powertools-large-messages/src/main/java/software/amazon/lambda/powertools/largemessages/internal/LargeSNSMessageProcessor.java create mode 100644 powertools-large-messages/src/main/java/software/amazon/lambda/powertools/largemessages/internal/LargeSQSMessageProcessor.java create mode 100644 powertools-large-messages/src/test/java/software/amazon/lambda/powertools/largemessages/LargeMessageConfigTest.java create mode 100644 powertools-large-messages/src/test/java/software/amazon/lambda/powertools/largemessages/internal/LargeMessageAspectTest.java create mode 100644 powertools-large-messages/src/test/java/software/amazon/lambda/powertools/largemessages/internal/LargeMessageProcessorFactoryTest.java 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/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/sqs_large_message_handling.md b/docs/utilities/sqs_large_message_handling.md index 82e1afef4..6308f1c79 100644 --- a/docs/utilities/sqs_large_message_handling.md +++ b/docs/utilities/sqs_large_message_handling.md @@ -3,6 +3,10 @@ title: SQS Large Message Handling description: Utility --- +!!! warning +This module is now deprecated and will be removed in version 2. +See [Large Message Handling](large_messages.md) for the new module (`powertools-large-messages`) documentation. + 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. @@ -11,16 +15,19 @@ The utility automatically retrieves messages which have been offloaded to S3 usi 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. +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 - - ``` + +```xml + + + com.amazonaws + amazon-sqs-java-extended-client-lib + 1.1.0 + +``` === "Gradle" ```groovy @@ -33,48 +40,49 @@ This utility is compatible with versions *[1.1.0+](https://github.com/awslabs/am 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-sqs - {{ powertools.version }} - - ... - - ... - - - - ... - - dev.aspectj - aspectj-maven-plugin - 1.13.1 - - 11 - 11 - 11 - - - software.amazon.lambda - powertools-sqs - - - - - - - compile - - - - - ... - - - ``` + +```xml hl_lines="3-7 16 18 24-27" + +... + +software.amazon.lambda +powertools-sqs +{{ powertools.version }} + +... + +... + + + +... + +dev.aspectj +aspectj-maven-plugin +1.13.1 + +11 +11 +11 + + +software.amazon.lambda +powertools-sqs + + + + + + +compile + + + + +... + + +``` === "Maven Java 1.8" @@ -186,12 +194,13 @@ which implements `com.amazonaws.services.lambda.runtime.RequestHandler` with `@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. - +!!! 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)` diff --git a/mkdocs.yml b/mkdocs.yml index 841a95fae..62d8d75ce 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -13,7 +13,7 @@ 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 diff --git a/pom.xml b/pom.xml index 1d80b3081..3e751f186 100644 --- a/pom.xml +++ b/pom.xml @@ -52,6 +52,7 @@ powertools-test-suite powertools-cloudformation powertools-idempotency + powertools-large-messages powertools-e2e-tests examples @@ -89,7 +90,7 @@ 3.5.0 3.3.0 3.1.0 - 5.9.3 + 5.10.0 1.0.6 0.5.1 @@ -243,21 +244,16 @@ - 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 @@ -540,9 +536,9 @@ - jdk11 + newerThanJdk8 - 11 + [9,) 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 8c2b5fc58..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 @@ -28,6 +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 software.amazon.lambda.powertools.logging.Logging; public class Function implements RequestHandler { @@ -53,6 +54,7 @@ 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()); diff --git a/powertools-e2e-tests/handlers/idempotency/src/main/resources/log4j2.xml b/powertools-e2e-tests/handlers/idempotency/src/main/resources/log4j2.xml new file mode 100644 index 000000000..8925f70b9 --- /dev/null +++ b/powertools-e2e-tests/handlers/idempotency/src/main/resources/log4j2.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ 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/pom.xml b/powertools-e2e-tests/handlers/pom.xml index ae26364dd..4dd8cbb45 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/pom.xml b/powertools-e2e-tests/pom.xml index 7e354d15e..500b7f30a 100644 --- a/powertools-e2e-tests/pom.xml +++ b/powertools-e2e-tests/pom.xml @@ -51,6 +51,13 @@ test + + software.amazon.awssdk + dynamodb + ${aws.sdk.version} + test + + software.amazon.awssdk cloudwatch @@ -65,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 @@ -77,6 +98,12 @@ test + + commons-io + commons-io + 2.11.0 + + org.junit.jupiter junit-jupiter-engine @@ -182,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 fed823299..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 @@ -14,10 +14,11 @@ 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.Collections; +import java.util.Map; import java.util.UUID; import java.util.concurrent.TimeUnit; import org.assertj.core.api.Assertions; @@ -40,9 +41,9 @@ 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 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 e4b8dccd2..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 @@ -15,12 +15,14 @@ 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; @@ -52,7 +54,8 @@ public static void setup() { }) .collect(Collectors.toMap(data -> data[0], data -> data[1]))) .build(); - functionName = infrastructure.deploy(); + Map outputs = infrastructure.deploy(); + functionName = outputs.get(FUNCTION_NAME_OUTPUT); } @AfterAll 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 32965b3be..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 @@ -15,10 +15,12 @@ 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.List; +import java.util.Map; import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -50,7 +52,8 @@ public static void setup() { }) .collect(Collectors.toMap(data -> data[0], data -> data[1]))) .build(); - functionName = infrastructure.deploy(); + Map outputs = infrastructure.deploy(); + functionName = outputs.get(FUNCTION_NAME_OUTPUT); } @AfterAll 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 678321a99..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 @@ -15,6 +15,7 @@ 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.HashMap; @@ -60,7 +61,8 @@ public void setup() { }) .collect(Collectors.toMap(data -> data[0], data -> data[1]))) .build(); - functionName = infrastructure.deploy(); + Map outputs = infrastructure.deploy(); + functionName = outputs.get(FUNCTION_NAME_OUTPUT); } @AfterAll 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 043de5494..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 @@ -15,6 +15,7 @@ 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; @@ -46,7 +47,8 @@ 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 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 62bb018f4..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 @@ -33,6 +33,7 @@ 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; @@ -52,9 +53,14 @@ import software.amazon.awscdk.services.lambda.Code; import software.amazon.awscdk.services.lambda.Function; 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; @@ -85,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; @@ -102,6 +109,9 @@ public class Infrastructure { 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; @@ -115,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(); @@ -147,7 +159,7 @@ public static Builder builder() { * * @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() @@ -160,12 +172,14 @@ public String deploy() { 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; } /** @@ -182,6 +196,7 @@ public void destroy() { * @return the CDK stack */ private Stack createStackWithLambda() { + boolean createTableForAsyncTests = false; Stack stack = new Stack(app, stackName); List packagingInstruction = Arrays.asList( "/bin/sh", @@ -209,6 +224,9 @@ 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(" ", "[", "]"))); @@ -244,10 +262,43 @@ 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") @@ -260,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") @@ -306,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; } @@ -379,11 +444,13 @@ public static class Builder { 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(); @@ -453,6 +520,16 @@ 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 { 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 ba4b147ae..2e68ea563 100644 --- a/powertools-idempotency/pom.xml +++ b/powertools-idempotency/pom.xml @@ -105,7 +105,6 @@ org.junit-pioneer junit-pioneer - 1.9.1 test 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 d34dd72dd..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 @@ -23,6 +23,7 @@ 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 software.amazon.lambda.powertools.idempotency.Constants; @@ -37,6 +38,8 @@ * 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)") diff --git a/powertools-large-messages/pom.xml b/powertools-large-messages/pom.xml new file mode 100644 index 000000000..a9ded60c5 --- /dev/null +++ b/powertools-large-messages/pom.xml @@ -0,0 +1,166 @@ + + + + + 4.0.0 + +A suite of utilities for AWS Lambda Functions that makes handling large messages in SQS and SNS easier. + + + software.amazon.lambda + powertools-parent + 1.17.0-SNAPSHOT + + + powertools-large-messages + jar + + Powertools for AWS Lambda (Java) library Large messages + + + 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/ + + + + + + ossrh + https://aws.oss.sonatype.org/content/repositories/snapshots + + + + + + software.amazon.lambda + powertools-core + + + org.aspectj + aspectjrt + + + com.amazonaws + aws-lambda-java-events + + + software.amazon.payloadoffloading + payloadoffloading-common + + + com.fasterxml.jackson.core + jackson-core + + + software.amazon.awssdk + sdk-core + + + software.amazon.awssdk + utils + + + software.amazon.awssdk + s3 + + + software.amazon.awssdk + netty-nio-client + + + software.amazon.awssdk + apache-client + + + + + software.amazon.awssdk + url-connection-client + ${aws.sdk.version} + + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.junit-pioneer + junit-pioneer + test + + + org.mockito + mockito-core + test + + + org.mockito + mockito-inline + test + + + org.apache.commons + commons-lang3 + test + + + org.assertj + 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-sqs/pom.xml b/powertools-sqs/pom.xml index d8afac783..c21943fba 100644 --- a/powertools-sqs/pom.xml +++ b/powertools-sqs/pom.xml @@ -29,6 +29,7 @@ Powertools for AWS Lambda (Java) library SQS + Deprecated: Batch processing is now handled in powertools-batch and large messages in powertools-large-messages modules. 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/ 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 index 847dd456c..a3a92cea1 100644 --- 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 @@ -20,9 +20,12 @@ import java.lang.annotation.Target; /** - * {@code SqsLargeMessage} is used to signal that the annotated method + * @deprecated See software.amazon.lambda.powertools.largemessages.LargeMessage in powertools-large-messages module. + * Will be removed in version 2. + * + *

    {@code SqsLargeMessage} is used to signal that the annotated method * should be extended to handle large SQS messages which have been offloaded - * to S3 + * to S3

    * *

    {@code SqsLargeMessage} automatically retrieves and deletes messages * which have been offloaded to S3 using the {@code amazon-sqs-java-extended-client-lib} @@ -73,6 +76,7 @@ *

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

    */ +@Deprecated @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface SqsLargeMessage { 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 index 8c06a6291..ace2a42d6 100644 --- 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 @@ -36,7 +36,11 @@ /** * A class of helper functions to add additional functionality to {@link SQSEvent} processing. + * + * @deprecated Batch processing is now handled in powertools-batch and large messages in powertools-large-messages. + * This class will no longer be available in version 2. */ +@Deprecated public final class SqsUtils { private static final Logger LOG = LoggerFactory.getLogger(SqsUtils.class); 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 @@ + + + + From 6900b721191d1d89f0a896dcbc15822384bcc1f2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Aug 2023 14:05:29 +0200 Subject: [PATCH 51/55] build(deps): bump commons-io:commons-io from 2.11.0 to 2.13.0 (#1333) Bumps commons-io:commons-io from 2.11.0 to 2.13.0. --- updated-dependencies: - dependency-name: commons-io:commons-io dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- powertools-e2e-tests/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/powertools-e2e-tests/pom.xml b/powertools-e2e-tests/pom.xml index 500b7f30a..2c802edc3 100644 --- a/powertools-e2e-tests/pom.xml +++ b/powertools-e2e-tests/pom.xml @@ -101,7 +101,7 @@ commons-io commons-io - 2.11.0 + 2.13.0 From 7179713e211d7c3c6357423b16ab65cf9b8490f9 Mon Sep 17 00:00:00 2001 From: Eleni Dimitropoulou <12170229+eldimi@users.noreply.github.com> Date: Wed, 2 Aug 2023 16:25:02 +0300 Subject: [PATCH 52/55] chore: Add powertools specific user-agent-suffix to the AWS SDK v2 clients (#1306) --- powertools-core/pom.xml | 11 ++ .../core/internal/UserAgentConfigurator.java | 111 ++++++++++++++++ .../resources-filtered/version.properties | 9 ++ .../internal/UserAgentConfiguratorTest.java | 122 ++++++++++++++++++ .../src/test/resources/test.properties | 1 + .../src/test/resources/unreadable.properties | 2 + .../persistence/DynamoDBPersistenceStore.java | 24 ++-- .../parameters/AppConfigProvider.java | 5 + .../powertools/parameters/BaseProvider.java | 1 + .../parameters/DynamoDbProvider.java | 4 + .../powertools/parameters/SSMProvider.java | 4 + .../parameters/SecretsProvider.java | 4 + .../lambda/powertools/sqs/SqsUtils.java | 18 ++- 13 files changed, 304 insertions(+), 12 deletions(-) create mode 100644 powertools-core/src/main/java/software/amazon/lambda/powertools/core/internal/UserAgentConfigurator.java create mode 100644 powertools-core/src/main/resources-filtered/version.properties create mode 100644 powertools-core/src/test/java/software/amazon/lambda/powertools/core/internal/UserAgentConfiguratorTest.java create mode 100644 powertools-core/src/test/resources/test.properties create mode 100644 powertools-core/src/test/resources/unreadable.properties diff --git a/powertools-core/pom.xml b/powertools-core/pom.xml index 1adb64af8..78cd735b9 100644 --- a/powertools-core/pom.xml +++ b/powertools-core/pom.xml @@ -64,6 +64,11 @@ org.aspectj aspectjrt + + org.apache.logging.log4j + log4j-slf4j2-impl + ${log4j.version} + @@ -104,6 +109,12 @@
    + + + src/main/resources-filtered + true + + org.apache.maven.plugins 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/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-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 7a023b4de..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 @@ -14,6 +14,18 @@ package software.amazon.lambda.powertools.idempotency.persistence; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +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.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; @@ -25,11 +37,7 @@ import java.util.OptionalLong; import java.util.stream.Collectors; import java.util.stream.Stream; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -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.ConditionalCheckFailedException; import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest; @@ -37,10 +45,6 @@ import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest; -import software.amazon.awssdk.utils.StringUtils; -import software.amazon.lambda.powertools.idempotency.Constants; -import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemAlreadyExistsException; -import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemNotFoundException; /** * DynamoDB version of the {@link PersistenceStore}. Will store idempotency data in DynamoDB.
    @@ -49,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; @@ -92,6 +97,7 @@ private DynamoDBPersistenceStore(String tableName, if (idempotencyDisabledEnv == null || "false".equalsIgnoreCase(idempotencyDisabledEnv)) { this.dynamoDbClient = DynamoDbClient.builder() .httpClient(UrlConnectionHttpClient.builder().build()) + .overrideConfiguration(ClientOverrideConfiguration.builder().putAdvancedOption(SdkAdvancedClientOption.USER_AGENT_SUFFIX, UserAgentConfigurator.getUserAgent(IDEMPOTENCY)).build()) .region(Region.of(System.getenv(AWS_REGION_ENV))) .build(); } else { 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 130be25a3..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 @@ -17,12 +17,15 @@ 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.model.GetLatestConfigurationRequest; import software.amazon.awssdk.services.appconfigdata.model.GetLatestConfigurationResponse; import software.amazon.awssdk.services.appconfigdata.model.StartConfigurationSessionRequest; +import software.amazon.lambda.powertools.core.internal.UserAgentConfigurator; import software.amazon.lambda.powertools.parameters.cache.CacheManager; import software.amazon.lambda.powertools.parameters.transform.TransformationManager; @@ -153,6 +156,8 @@ public AppConfigProvider build() { client = AppConfigDataClient.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(); } 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 e7bfdf835..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 @@ -31,6 +31,7 @@ */ @NotThreadSafe public abstract class BaseProvider implements ParamProvider { + public static final String PARAMETERS = "parameters"; protected final CacheManager cacheManager; private TransformationManager transformationManager; 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 2b0694a5d..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 @@ -18,6 +18,8 @@ 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; @@ -26,6 +28,7 @@ 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; @@ -132,6 +135,7 @@ 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(); } 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 b24b1e319..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 @@ -18,6 +18,8 @@ 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.ssm.SsmClient; @@ -25,6 +27,7 @@ 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.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; @@ -248,6 +251,7 @@ 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(); } 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 99b87f84b..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 @@ -20,10 +20,13 @@ import java.util.Base64; 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.secretsmanager.SecretsManagerClient; import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest; +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; @@ -158,6 +161,7 @@ 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(); } 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 index ace2a42d6..c838180fd 100644 --- 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 @@ -27,8 +27,11 @@ import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.sqs.SqsClient; +import software.amazon.lambda.powertools.core.internal.UserAgentConfigurator; import software.amazon.lambda.powertools.sqs.exception.SkippedMessageDueToFailedBatchException; import software.amazon.lambda.powertools.sqs.internal.BatchContext; import software.amazon.lambda.powertools.sqs.internal.SqsLargeMessageAspect; @@ -42,10 +45,10 @@ */ @Deprecated public final class SqsUtils { - private static final Logger LOG = LoggerFactory.getLogger(SqsUtils.class); + public static final String SQS = "sqs"; + private static final Logger LOG = LoggerFactory.getLogger(SqsUtils.class); private static final ObjectMapper objectMapper = new ObjectMapper(); - // The attribute on an SQS-FIFO message used to record the message group ID private static final String MESSAGE_GROUP_ID = "MessageGroupId"; private static SqsClient client; private static S3Client s3Client; @@ -497,7 +500,11 @@ public static List batchProcessor(final SQSEvent event, final List handlerReturn = new ArrayList<>(); if (client == null) { - client = SqsClient.create(); + client = (SqsClient) SqsClient.builder() + .overrideConfiguration(ClientOverrideConfiguration.builder() + .putAdvancedOption(SdkAdvancedClientOption.USER_AGENT_SUFFIX, + UserAgentConfigurator.getUserAgent(SQS)) + .build()); } BatchContext batchContext = new BatchContext(client); @@ -586,6 +593,11 @@ public static ObjectMapper objectMapper() { public static S3Client s3Client() { if (null == s3Client) { + s3Client = (S3Client) S3Client.builder() + .overrideConfiguration(ClientOverrideConfiguration.builder() + .putAdvancedOption(SdkAdvancedClientOption.USER_AGENT_SUFFIX, + UserAgentConfigurator.getUserAgent(SQS)) + .build()); SqsUtils.s3Client = S3Client.create(); } From 7f6c8921d50dfe41cd62e06399c51a772c445f0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Van=20Der=20Linden?= <117538+jeromevdl@users.noreply.github.com> Date: Thu, 3 Aug 2023 08:56:54 +0200 Subject: [PATCH 53/55] docs: improve contributing guide (#1334) * contributing guide improved --- CONTRIBUTING.md | 121 +++++++++++++++++++++------ docs/media/intellij_checkstyle_1.png | Bin 0 -> 549697 bytes docs/media/intellij_checkstyle_2.png | Bin 0 -> 460656 bytes docs/media/intellij_checkstyle_3.png | Bin 0 -> 101689 bytes 4 files changed, 95 insertions(+), 26 deletions(-) create mode 100644 docs/media/intellij_checkstyle_1.png create mode 100644 docs/media/intellij_checkstyle_2.png create mode 100644 docs/media/intellij_checkstyle_3.png diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 46fab27cf..8db303737 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,59 +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: `mvn clean test` -4. Ensure your code is formatted with the provided [checkstyle.xml](https://github.com/aws-powertools/powertools-lambda-java/blob/main/checkstyle.xml): `mvn clean verify` -5. Commit to your fork using clear commit messages. -6. Send us a pull request, answering any default questions in the pull request interface. -7. 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/docs/media/intellij_checkstyle_1.png b/docs/media/intellij_checkstyle_1.png new file mode 100644 index 0000000000000000000000000000000000000000..322e247443111074748c9d65268641d3a929f85a GIT binary patch literal 549697 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

    u7xEmQFzzUtHc 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