io.klerch.alexa.state.handler.AWSDynamoStateHandler Maven / Gradle / Ivy
Show all versions of alexa-skills-kit-states-java Show documentation
/**
* Made by Kay Lerch (https://twitter.com/KayLerch)
*
* Attached license applies.
* This library is licensed under GNU GENERAL PUBLIC LICENSE Version 3 as of 29 June 2007
*/
package io.klerch.alexa.state.handler;
import com.amazon.speech.speechlet.Session;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient;
import com.amazonaws.services.dynamodbv2.model.*;
import com.amazonaws.services.dynamodbv2.util.TableUtils;
import io.klerch.alexa.state.model.AlexaStateObject;
import io.klerch.alexa.state.utils.AlexaStateException;
import io.klerch.alexa.state.model.AlexaStateModel;
import io.klerch.alexa.state.model.AlexaScope;
import org.apache.log4j.Logger;
import java.util.*;
import java.util.stream.Collectors;
/**
* As this handler works in the user and application scope it persists all models to a AWS DynamoDB table.
* This handler reads and writes state for AlexaStateModels and considers all its fields annotated with AlexaSaveState-tags.
* This handler derives from the AlexaSessionStateHandler thus it reads and writes state out of DynamoDB also to your Alexa
* session.
*/
public class AWSDynamoStateHandler extends AlexaSessionStateHandler {
private final Logger log = Logger.getLogger(AWSDynamoStateHandler.class);
private final AmazonDynamoDB awsClient;
private final String tableName;
private final long readCapacityUnits;
private final long writeCapacityUnits;
private static final String tablePrefix = "alexa-";
// context value for the primary index for each item saved in application scope
static final String attributeValueApp = "__application";
// column-name for primary index of the dynamo table to store the context key (mostly user-id)
static final String pkUser = "amzn-user-id";
// column-name for secondary index of the dynamo table to store the object identifier (aka id)
static final String pkModel = "model-class";
// column-name for table attribute used to store the state value (model JSON, single value)
private static final String attributeKeyState = "state";
// flag that indicates if existence of table is approved to avoid multiple checks in
// dynamodb in single instance lifetime
private Boolean tableExistenceApproved = false;
/**
* The most convenient constructor just takes the Alexa session. An AWS client for accessing DynamoDB
* will make use of all defaults in regards to credentials and region. The credentials used in this client need permission for reading, writing and
* removing items from a DynamoDB table and also the right to create a table. On the very first read or
* write operation of this handler it creates a table named like your Alexa App Id.
* The table created consist of a hash-key and a sort-key.
* If you don't want this handler to auto-create a table provide the name of an existing table in DynamoDB
* in another constructor.
*
* @param session The Alexa session of your current skill invocation.
*/
public AWSDynamoStateHandler(final Session session) {
this(session, new AmazonDynamoDBClient(), null, 10L, 5L);
}
/**
* Takes the Alexa session and a AWS client which is set up for
* the correct AWS region. The credentials used in this client need permission for reading, writing and
* removing items from a DynamoDB table and also the right to create a table. On the very first read or
* write operation of this handler it creates a table named like your Alexa App Id.
* The table created consist of a hash-key and a sort-key.
* If you don't want this handler to auto-create a table provide the name of an existing table in DynamoDB
* in another constructor.
*
* @param session The Alexa session of your current skill invocation.
* @param awsClient An AWS client capable of creating DynamoDB table plus reading, writing and removing items.
*/
public AWSDynamoStateHandler(final Session session, final AmazonDynamoDB awsClient) {
this(session, awsClient, null, 10L, 5L);
}
/**
* Takes the Alexa session and a table. An AWS client for accessing DynamoDB
* will make use of all defaults in regards to credentials and region. The credentials used in this client need permission for reading, writing and
* removing items from the given DynamoDB table. The table needs a string hash-key with name model-class and a string sort-key
* of name amzn-user-id. The option of providing an existing table to this handler prevents it from checking its existence
* which might end up with a better performance. You also don't need to provide permission of creating a DynamoDB table to
* the credentials of the given AWS client.
*
* @param session The Alexa session of your current skill invocation.
* @param tableName An existing table accessible by the client and with string hash-key named model-class and a string sort-key named amzn-user-id.
*/
public AWSDynamoStateHandler(final Session session, final String tableName) {
this(session, new AmazonDynamoDBClient(), tableName, 10L, 5L);
}
/**
* Takes the Alexa session and a AWS client which is set up for
* the correct AWS region. The credentials used in this client need permission for reading, writing and
* removing items from the given DynamoDB table. The table needs a string hash-key with name model-class and a string sort-key
* of name amzn-user-id. The option of providing an existing table to this handler prevents it from checking its existence
* which might end up with a better performance. You also don't need to provide permission of creating a DynamoDB table to
* the credentials of the given AWS client.
*
* @param session The Alexa session of your current skill invocation.
* @param awsClient An AWS client capable of reading, writing and removing items of the given DynamoDB table.
* @param tableName An existing table accessible by the client and with string hash-key named model-class and a string sort-key named amzn-user-id.
*/
public AWSDynamoStateHandler(final Session session, final AmazonDynamoDB awsClient, final String tableName) {
this(session, awsClient, tableName, 10L, 5L);
}
/**
* Takes the Alexa session, an AWS client which is set up for
* the correct AWS region. The credentials used in this client need permission for reading, writing and
* removing items from a DynamoDB table and also the right to create a table. On the very first read or
* write operation of this handler it creates a table named like your Alexa App Id.
* The table created consist of a hash-key and a sort-key.
* If you don't want this handler to auto-create a table provide the name of an existing table in DynamoDB
* in another constructor.
*
* @param session The Alexa session of your current skill invocation.
* @param awsClient An AWS client capable of creating DynamoDB table plus reading, writing and removing items.
* @param readCapacityUnits Read capacity for the table which is applied only on creation of table (what happens at the very first read or write operation with this handler)
* @param writeCapacityUnits Write capacity for the table which is applied only on creation of table (what happens at the very first read or write operation with this handler)
*/
public AWSDynamoStateHandler(final Session session, final AmazonDynamoDB awsClient, final long readCapacityUnits, final long writeCapacityUnits) {
this(session, awsClient, null, readCapacityUnits, writeCapacityUnits);
}
private AWSDynamoStateHandler(final Session session, final AmazonDynamoDB awsClient, final String tableName, final long readCapacityUnits, final long writeCapacityUnits) {
super(session);
this.awsClient = awsClient;
// assume table exists if table name provided.
this.tableExistenceApproved = tableName != null && !tableName.isEmpty();
this.tableName = tableName != null ? tableName : tablePrefix + session.getApplication().getApplicationId();
this.readCapacityUnits = readCapacityUnits;
this.writeCapacityUnits = writeCapacityUnits;
}
/**
* Returns the AWS connection client used to write to and read from items in DynamoDB table.
*
* @return AWS connection client to DynamoDB
*/
public AmazonDynamoDB getAwsClient() {
return this.awsClient;
}
/**
* Returns the name of the DynamoDB table which is used by this handler to store items with
* model states.
*
* @return name of the DynamoDB table
*/
String getTableName() {
return this.tableName;
}
/**
* {@inheritDoc}
*/
@Override
public void writeModels(final Collection models) throws AlexaStateException {
// write to session
super.writeModels(models);
final List items = new ArrayList<>();
// go for each model asked to be saved
for (final AlexaStateModel model : models) {
// convert model to a dynamo-item having in place all attributes
getItems(model, true).forEach(item ->
// wrap each model in a write-request and collect all of them
items.add(new WriteRequest(new PutRequest(item)))
);
}
// write batch of write-request to dynamo
writeItemsToDb(items);
}
/**
* {@inheritDoc}
*/
@Override
public void writeValues(final Collection stateObjects) throws AlexaStateException {
// write to session
super.writeValues(stateObjects);
final List items = new ArrayList<>();
stateObjects.stream()
// select only USER or APPLICATION scoped state objects
.filter(stateObject -> stateObject.getScope().isIn(AlexaScope.USER, AlexaScope.APPLICATION))
// go for each state object to be saved
.forEach(stateObject -> {
final String id = stateObject.getId();
final Object value = stateObject.getValue();
final AlexaScope scope = stateObject.getScope();
// set primary keys which differ depending on the scope to save value for
final Map item = AlexaScope.USER.includes(scope) ?
getUserScopedKeyAttributes(id) : getAppScopedKeyAttributes(id);
// add an attribute which holds the actual value
item.put(attributeKeyState, new AttributeValue(String.valueOf(value)));
// wrap each value in a write-request and collect all of them
items.add(new WriteRequest(new PutRequest(item)));
});
// write batch of write-requests to dynamo
writeItemsToDb(items);
}
/**
* {@inheritDoc}
*/
@Override
public void removeValues(final Collection ids) throws AlexaStateException {
super.removeValues(ids);
final List items = new ArrayList<>();
ids.forEach(id -> {
// removeState user-scoped item
items.add(new WriteRequest(new DeleteRequest(getUserScopedKeyAttributes(id))));
// removeState app-scoped item
items.add(new WriteRequest(new DeleteRequest(getAppScopedKeyAttributes(id))));
});
writeItemsToDb(items);
}
/**
* {@inheritDoc}
*/
@Override
public boolean exists(final String id, final AlexaScope scope) throws AlexaStateException {
if (scope.includes(AlexaScope.SESSION)) {
return super.exists(id, scope);
} else {
return readValueFromDb(id, scope).isPresent();
}
}
String getAttributeKeyState() {
return attributeKeyState;
}
/**
* {@inheritDoc}
*/
@Override
public Optional readModel(final Class modelClass, final String id) throws AlexaStateException {
// if there is nothing for this model in the session ...
final Optional modelSession = super.readModel(modelClass, id);
// create new model with given id. for now we assume a model exists for this id. we find out by
// querying dynamodb in the following lines. only if this is true model will be written back to session
final TModel model = modelSession.orElse(createModel(modelClass, id));
// get read-request items (could be two - one for user-scoped item, one for app-scoped item)
final List