diff --git a/docs/content/utilities/parameters.mdx b/docs/content/utilities/parameters.mdx index 78ecc3502..3c98901a1 100644 --- a/docs/content/utilities/parameters.mdx +++ b/docs/content/utilities/parameters.mdx @@ -264,4 +264,106 @@ And then use it like this : S3Provider provider = new S3Provider(ParamManager.getCacheManager()); provider.setTransformationManager(ParamManager.getTransformationManager()); // optional, needed for transformations String value = provider.withBucket("myBucket").get("myKey"); -``` \ No newline at end of file +``` + +## Annotation +You can make use of the annotation ```@Param``` to inject a parameter value in a variable. + +```java +@Param(key = "/my/parameter") +private String value; +``` +By default it will use ```SSMProvider``` to retrieve the value from AWS System Manager Parameter Store. +You could specify a different provider as long as it extends ```BaseProvider``` and/or a ```Transformer```. +For example: + +```java +@Param(key = "/my/parameter/json", provider = SecretsProvider.class, transformer = JsonTransformer.class) +private ObjectToDeserialize value; +``` + +In this case ```SecretsProvider``` will be used to retrieve a raw value that is then trasformed into the target Object by using ```JsonTransformer```. +To show the convenience of the annotation compare the following two code snippets. + +```java:title=AppWithoutAnnotation.java + +public class AppWithoutAnnotation implements RequestHandler { + + // Get an instance of the SSM Provider + SSMProvider ssmProvider = ParamManager.getSsmProvider(); + + // Retrieve a single parameter + ObjectToDeserialize value = ssmProvider + .withTransformation(Transformer.json) + .get("/my/parameter/json"); + +} +``` +And with the usage of ```@Param``` + +```java:title=AppWithAnnotation.java +public class AppWithAnnotation implements RequestHandler { + + @Param(key = "/my/parameter/json" transformer = JsonTransformer.class) + ObjectToDeserialize value; + +} +``` + +### Install + +If you want to use the ```@Param``` annotation in your project add configuration to compile-time weave (CTW) the powertools-parameters aspects into your project. + +* [maven](https://maven.apache.org/): +```xml + + + ... + + org.codehaus.mojo + aspectj-maven-plugin + 1.11 + + ... + + ... + + software.amazon.lambda + powertools-parameters + + + + + + + compile + + + + + ... + + +``` +**Note:** If you are working with lambda function on runtime post java8, please refer [issue](https://github.com/awslabs/aws-lambda-powertools-java/issues/50) for workaround + +* [gradle](https://gradle.org): +```groovy +plugins{ + id 'java' + id 'aspectj.AspectjGradlePlugin' version '0.0.6' +} +repositories { + jcenter() +} +dependencies { + ... + implementation 'software.amazon.lambda:powertools-parameters:1.0.1' + aspectpath 'software.amazon.lambda:powertools-parameters:1.0.1' +} +``` + +**Note:** + +Please add `aspectjVersion = '1.9.6'` to the `gradle.properties` file. The aspectj plugin works at the moment with gradle 5.x only if +you are using `java 8` as runtime. Please refer to [open issue](https://github.com/awslabs/aws-lambda-powertools-java/issues/146) for more details. \ No newline at end of file diff --git a/powertools-parameters/pom.xml b/powertools-parameters/pom.xml index e2abe43b9..c9ce32804 100644 --- a/powertools-parameters/pom.xml +++ b/powertools-parameters/pom.xml @@ -82,7 +82,6 @@ aspectjrt compile - org.junit.jupiter @@ -99,6 +98,11 @@ mockito-core test + + org.mockito + mockito-inline + test + org.apache.commons commons-lang3 @@ -109,6 +113,11 @@ assertj-core test + + org.aspectj + aspectjweaver + test + 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 0ff2b4b3d..5c60b7c78 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 @@ -104,7 +104,7 @@ public BaseProvider withMaxAge(int maxAge, ChronoUnit unit) { * @param transformerClass Class of the transformer to apply. For convenience, you can use {@link Transformer#json} or {@link Transformer#base64} shortcuts. * @return the provider itself in order to chain calls (eg.
provider.withTransformation(json).get("key", MyObject.class)
). */ - protected BaseProvider withTransformation(Class transformerClass) { + public BaseProvider withTransformation(Class transformerClass) { if (transformationManager == null) { throw new IllegalStateException("Trying to add transformation while no TransformationManager has been provided."); } 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 new file mode 100644 index 000000000..ef3d08b72 --- /dev/null +++ b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/Param.java @@ -0,0 +1,30 @@ +package software.amazon.lambda.powertools.parameters; + +import software.amazon.lambda.powertools.parameters.transform.Transformer; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * {@code Param} is used to signal that the annotated field should be + * populated with a value retrieved from a parameter store through a {@link ParamProvider}. + * + *

By default {@code Param} use {@link SSMProvider} as parameter provider. This can be overridden specifying + * the annotation variable {@code Param(provider = )}.
+ * The library provide a provider for AWS System Manager Parameters Store ({@link SSMProvider}) and a provider + * for AWS Secrets Manager ({@link SecretsProvider}). + * The user can implement a custom provider by extending the abstract class {@link BaseProvider}.

+ * + *

If the parameter value requires transformation before being assigned to the annotated field + * users can specify a {@link Transformer} + *

+ */ +@Retention(RetentionPolicy.RUNTIME) +@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 f2b50425b..524890a2e 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 @@ -18,6 +18,10 @@ import software.amazon.lambda.powertools.parameters.cache.CacheManager; import software.amazon.lambda.powertools.parameters.transform.TransformationManager; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.concurrent.ConcurrentHashMap; + /** * Utility class to retrieve instances of parameter providers. * Each instance is unique (singleton). @@ -27,8 +31,20 @@ public final class ParamManager { private static final CacheManager cacheManager = new CacheManager(); private static final TransformationManager transformationManager = new TransformationManager(); - private static SecretsProvider secretsProvider; - private static SSMProvider ssmProvider; + private static ConcurrentHashMap, BaseProvider> providers = new ConcurrentHashMap<>(); + + /** + * Get a concrete implementation of {@link BaseProvider}.
+ * You can specify {@link SecretsProvider} or {@link SSMProvider} or create your custom provider + * by extending {@link BaseProvider} if you need to integrate with a different parameter store. + * @return a {@link SecretsProvider} + */ + public static T getProvider(Class providerClass) { + if (providerClass == null) { + throw new IllegalStateException("providerClass cannot be null."); + } + return (T) providers.computeIfAbsent(providerClass, (k) -> createProvider(k)); + } /** * Get a {@link SecretsProvider} with default {@link SecretsManagerClient}.
@@ -36,13 +52,7 @@ public final class ParamManager { * @return a {@link SecretsProvider} */ public static SecretsProvider getSecretsProvider() { - if (secretsProvider == null) { - secretsProvider = SecretsProvider.builder() - .withCacheManager(cacheManager) - .withTransformationManager(transformationManager) - .build(); - } - return secretsProvider; + return getProvider(SecretsProvider.class); } /** @@ -51,13 +61,7 @@ public static SecretsProvider getSecretsProvider() { * @return a {@link SSMProvider} */ public static SSMProvider getSsmProvider() { - if (ssmProvider == null) { - ssmProvider = SSMProvider.builder() - .withCacheManager(cacheManager) - .withTransformationManager(transformationManager) - .build(); - } - return ssmProvider; + return getProvider(SSMProvider.class); } /** @@ -66,14 +70,11 @@ public static SSMProvider getSsmProvider() { * @return a {@link SecretsProvider} */ public static SecretsProvider getSecretsProvider(SecretsManagerClient client) { - if (secretsProvider == null) { - secretsProvider = SecretsProvider.builder() - .withClient(client) - .withCacheManager(cacheManager) - .withTransformationManager(transformationManager) - .build(); - } - return secretsProvider; + return (SecretsProvider) providers.computeIfAbsent(SecretsProvider.class, (k) -> SecretsProvider.builder() + .withClient(client) + .withCacheManager(cacheManager) + .withTransformationManager(transformationManager) + .build()); } /** @@ -82,14 +83,11 @@ public static SecretsProvider getSecretsProvider(SecretsManagerClient client) { * @return a {@link SSMProvider} */ public static SSMProvider getSsmProvider(SsmClient client) { - if (ssmProvider == null) { - ssmProvider = SSMProvider.builder() - .withClient(client) - .withCacheManager(cacheManager) - .withTransformationManager(transformationManager) - .build(); - } - return ssmProvider; + return (SSMProvider) providers.computeIfAbsent(SSMProvider.class, (k) -> SSMProvider.builder() + .withClient(client) + .withCacheManager(cacheManager) + .withTransformationManager(transformationManager) + .build()); } public static CacheManager getCacheManager() { @@ -99,4 +97,17 @@ public static CacheManager getCacheManager() { public static TransformationManager getTransformationManager() { return transformationManager; } + + private 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) { + throw new RuntimeException("Unexpected error occurred. Please raise issue at " + + "https://github.com/awslabs/aws-lambda-powertools-java/issues", e); + } + } + } 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 new file mode 100644 index 000000000..ea4d465cd --- /dev/null +++ b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/internal/LambdaParametersAspect.java @@ -0,0 +1,43 @@ +package software.amazon.lambda.powertools.parameters.internal; + +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.FieldSignature; +import software.amazon.lambda.powertools.parameters.*; + +@Aspect +public class LambdaParametersAspect { + + @Pointcut("get(* *) && @annotation(paramAnnotation)") + 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()) { + // No transformation + return provider.get(paramAnnotation.key()); + } else { + FieldSignature s = (FieldSignature) joinPoint.getSignature(); + if(String.class.isAssignableFrom(s.getFieldType())) { + // Basic transformation + return provider + .withTransformation(paramAnnotation.transformer()) + .get(paramAnnotation.key()); + } else { + // Complex transformation + return provider + .withTransformation(paramAnnotation.transformer()) + .get(paramAnnotation.key(), s.getFieldType()); + } + } + } + +} 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 813ed7638..0b4a2093f 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 @@ -28,6 +28,7 @@ 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; @@ -57,8 +58,7 @@ public class ParamManagerIntegrationTest { public void setup() throws IllegalAccessException { openMocks(this); - writeStaticField(ParamManager.class, "ssmProvider", null, true); - writeStaticField(ParamManager.class, "secretsProvider", null, true); + writeStaticField(ParamManager.class, "providers", new ConcurrentHashMap<>(), true); } @Test 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 new file mode 100644 index 000000000..b58ad7b3d --- /dev/null +++ b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/internal/AnotherObject.java @@ -0,0 +1,25 @@ +package software.amazon.lambda.powertools.parameters.internal; + +public class AnotherObject { + + public AnotherObject() {} + + private String another; + private int object; + + public String getAnother() { + return another; + } + + public void setAnother(String another) { + this.another = another; + } + + public int getObject() { + return object; + } + + public void setObject(int object) { + this.object = object; + } +} 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 new file mode 100644 index 000000000..e58ef746c --- /dev/null +++ b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/internal/CustomProvider.java @@ -0,0 +1,30 @@ +package software.amazon.lambda.powertools.parameters.internal; + +import software.amazon.lambda.powertools.parameters.BaseProvider; +import software.amazon.lambda.powertools.parameters.cache.CacheManager; + +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; + +public class CustomProvider extends BaseProvider { + + private final Map values = new HashMap<>(); + + public CustomProvider(CacheManager cacheManager) { + super(cacheManager); + values.put("/simple", "value"); + values.put("/base64", Base64.getEncoder().encodeToString("value".getBytes())); + values.put("/json", "{\"foo\":\"Foo\", \"bar\":42, \"baz\":123456789}"); + } + + @Override + protected String getValue(String key) { + return values.get(key); + } + + @Override + protected Map getMultipleValues(String path) { + return null; + } +} 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 new file mode 100644 index 000000000..d50dcbb10 --- /dev/null +++ b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/internal/LambdaParametersAspectTest.java @@ -0,0 +1,85 @@ +package software.amazon.lambda.powertools.parameters.internal; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import software.amazon.lambda.powertools.parameters.Param; +import software.amazon.lambda.powertools.parameters.ParamManager; +import software.amazon.lambda.powertools.parameters.SSMProvider; +import software.amazon.lambda.powertools.parameters.exception.TransformationException; +import software.amazon.lambda.powertools.parameters.transform.Base64Transformer; +import software.amazon.lambda.powertools.parameters.transform.JsonTransformer; +import software.amazon.lambda.powertools.parameters.transform.ObjectToDeserialize; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.Mockito.*; +import static org.mockito.MockitoAnnotations.openMocks; + +public class LambdaParametersAspectTest { + + @Mock + private SSMProvider defaultProvider; + + @Param(key = "/default") + private String defaultValue; + + @Param(key = "/simple", provider = CustomProvider.class) + private String param; + + @Param(key = "/base64", provider = CustomProvider.class, transformer = Base64Transformer.class) + private String basicTransform; + + @Param(key = "/json", provider = CustomProvider.class, transformer = JsonTransformer.class) + private ObjectToDeserialize complexTransform; + + @Param(key = "/json", provider = CustomProvider.class, transformer = JsonTransformer.class) + private AnotherObject wrongTransform; + + @BeforeEach + public void init() { + openMocks(this); + } + + @Test + public void testDefault_ShouldUseSSMProvider() { + try (MockedStatic mocked = mockStatic(ParamManager.class)) { + mocked.when(() -> ParamManager.getProvider(SSMProvider.class)).thenReturn(defaultProvider); + when(defaultProvider.get("/default")).thenReturn("value"); + + assertThat(defaultValue).isEqualTo("value"); + mocked.verify(times(1), () -> ParamManager.getProvider(SSMProvider.class)); + verify(defaultProvider, times(1)).get("/default"); + + mocked.reset(); + } + } + + @Test + public void testSimple() { + assertThat(param).isEqualTo("value"); + } + + @Test + public void testWithBasicTransform() { + assertThat(basicTransform).isEqualTo("value"); + } + + @Test + public void testWithComplexTransform() { + assertThat(complexTransform) + .isInstanceOf(ObjectToDeserialize.class) + .matches( + o -> o.getFoo().equals("Foo") && + o.getBar() == 42 && + o.getBaz() == 123456789); + } + + @Test + public void testWithComplexTransformWrongTargetClass_ShouldThrowException() { + assertThatExceptionOfType(TransformationException.class) + .isThrownBy(() -> {AnotherObject obj = wrongTransform; }); + } + +}