Skip to content
44 changes: 44 additions & 0 deletions docs/content/utilities/parameters.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -264,4 +264,48 @@ 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;

}
```
15 changes: 14 additions & 1 deletion powertools-parameters/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,10 @@
<artifactId>aspectjrt</artifactId>
<scope>compile</scope>
</dependency>

<dependency>
Comment thread
vitodegiosa marked this conversation as resolved.
Outdated
<groupId>software.amazon.payloadoffloading</groupId>
<artifactId>payloadoffloading-common</artifactId>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>org.junit.jupiter</groupId>
Expand All @@ -99,6 +102,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 +117,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;
Comment thread
vitodegiosa marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,36 +13,50 @@
*/
package software.amazon.lambda.powertools.parameters;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
import software.amazon.awssdk.services.ssm.SsmClient;
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).
*/
public final class ParamManager {

private static final Log LOG = LogFactory.getLog(ParamManager.class);

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 +65,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 +74,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 +87,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 +101,18 @@ 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) {
LOG.error("Failed creating provider instance", 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()) {
Comment thread
vitodegiosa marked this conversation as resolved.
// 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;
}
}
Original file line number Diff line number Diff line change
@@ -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<String, String> 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<String, String> getMultipleValues(String path) {
return null;
}
}
Loading