Skip to content

feat: Parameters injection #201

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Dec 2, 2020
Merged
104 changes: 103 additions & 1 deletion docs/content/utilities/parameters.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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");
```
```

## 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<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {

// 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<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {

@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
<build>
<plugins>
...
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.11</version>
<configuration>
...
<aspectLibraries>
...
<aspectLibrary>
<groupId>software.amazon.lambda</groupId>
<artifactId>powertools-parameters</artifactId>
</aspectLibrary>
</aspectLibraries>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
...
</plugins>
</build>
```
**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.
11 changes: 10 additions & 1 deletion powertools-parameters/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@
<artifactId>aspectjrt</artifactId>
<scope>compile</scope>
</dependency>

<!-- Test dependencies -->
<dependency>
<groupId>org.junit.jupiter</groupId>
Expand All @@ -99,6 +98,11 @@
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
Expand All @@ -109,6 +113,11 @@
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
Expand Up @@ -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. <pre>provider.withTransformation(json).get("key", MyObject.class)</pre>).
*/
protected BaseProvider withTransformation(Class<? extends Transformer> transformerClass) {
public BaseProvider withTransformation(Class<? extends Transformer> transformerClass) {
if (transformationManager == null) {
throw new IllegalStateException("Trying to add transformation while no TransformationManager has been provided.");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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}.
*
* <p>By default {@code Param} use {@link SSMProvider} as parameter provider. This can be overridden specifying
* the annotation variable {@code Param(provider = <Class-of-the-provider>)}.<br/>
* 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}.</p>
*
* <p>If the parameter value requires transformation before being assigned to the annotated field
* users can specify a {@link Transformer}
* </p>
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Param {
String key();
Class<? extends BaseProvider> provider() default SSMProvider.class;
Class<? extends Transformer> transformer() default Transformer.class;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -27,22 +31,28 @@ 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<Class<? extends BaseProvider>, BaseProvider> providers = new ConcurrentHashMap<>();

/**
* Get a concrete implementation of {@link BaseProvider}.<br/>
* 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 extends BaseProvider> T getProvider(Class<T> 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}.<br/>
* 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() {
if (secretsProvider == null) {
secretsProvider = SecretsProvider.builder()
.withCacheManager(cacheManager)
.withTransformationManager(transformationManager)
.build();
}
return secretsProvider;
return getProvider(SecretsProvider.class);
}

/**
Expand All @@ -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);
}

/**
Expand All @@ -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());
}

/**
Expand All @@ -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() {
Expand All @@ -99,4 +97,17 @@ public static CacheManager getCacheManager() {
public static TransformationManager getTransformationManager() {
return transformationManager;
}

private static <T extends BaseProvider> T createProvider(Class<T> providerClass) {
try {
Constructor<T> 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);
}
}

}
Original file line number Diff line number Diff line change
@@ -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());
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading