All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.klerch.alexa.state.handler.AWSS3StateHandler Maven / Gradle / Ivy

Go to download

This SDK is an extension to the Alexa Skills SDK for Java. It provides a framework for managing state of POJO models in a variety of persistence stores in an Alexa skill.

There is a newer version: 1.1.0
Show newest version
/**
 * 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.s3.AmazonS3; import com.amazonaws.services.s3.AmazonS3Client; import com.amazonaws.services.s3.model.S3Object; import io.klerch.alexa.state.utils.AlexaStateException; import io.klerch.alexa.state.model.AlexaScope; import io.klerch.alexa.state.model.AlexaStateModel; import org.apache.log4j.Logger; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.util.Optional; /** * As this handler works in the user and application scope it persists all models to an S3 bucket. * 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 S3 files also to your Alexa * session. For each individual scope (which is described by the Alexa User Id there will be a directory in your bucket which * then contains files - one for each instance of a saved model. */ public class AWSS3StateHandler extends AlexaSessionStateHandler { private final Logger log = Logger.getLogger(AWSS3StateHandler.class); private final AmazonS3 awsClient; private final String bucketName; private final String folderNameApp = "__application"; private final String fileExtension = "json"; /** * Takes the Alexa session. An AWS client for accessing the S3 bucket will make use * of all the defaults in your runtime environment in regards to AWS region and credentials. The * credentials of this client need permission for getting and putting objects to this bucket. * @param session The Alexa session of your current skill invocation. * @param bucketName The bucket where all saved states will go into. */ public AWSS3StateHandler(final Session session, final String bucketName) { this(session, new AmazonS3Client(), bucketName); } /** * Takes the Alexa session and an AWS client set up for the AWS region the given bucket is in. The * credentials of this client need permission for getting and putting objects to this bucket. * @param session The Alexa session of your current skill invocation. * @param awsClient An AWS client capable of getting and putting objects to the given bucket. * @param bucketName The bucket where all saved states will go into. */ public AWSS3StateHandler(final Session session, final AmazonS3 awsClient, final String bucketName) { super(session); this.awsClient = awsClient; this.bucketName = bucketName; } /** * Returns the AWS connection client used to write to and read from files in S3 bucket. * @return AWS connection client to S3 */ public AmazonS3 getAwsClient() { return this.awsClient; } /** * Returns the name of the S3 bucket which is used by this handler to store JSON files with * model states. * @return Name of the S3 bucket */ public String getBucketName() { return this.bucketName; } /** * {@inheritDoc} */ @Override public void writeModel(final AlexaStateModel model) throws AlexaStateException { // write to session super.writeModel(model); if (model.hasUserScopedField()) { final String filePath = getUserScopedFilePath(model.getClass(), model.getId()); // add json as new content of file final String fileContents = model.toJSON(AlexaScope.USER); // write all user-scoped attributes to file awsClient.putObject(bucketName, filePath, fileContents); } if (model.hasApplicationScopedField()) { // add primary keys as attributes final String filePath = getAppScopedFilePath(model.getClass(), model.getId()); // add json as new content of file final String fileContents = model.toJSON(AlexaScope.APPLICATION); // write all app-scoped attributes to file awsClient.putObject(bucketName, filePath, fileContents); } } /** * {@inheritDoc} */ @Override public void removeModel(AlexaStateModel model) throws AlexaStateException { super.removeModel(model); // removeState user-scoped file if (model.hasUserScopedField()) awsClient.deleteObject(bucketName, getUserScopedFilePath(model.getClass(), model.getId())); // removeState app-scoped file if (model.hasApplicationScopedField()) awsClient.deleteObject(bucketName, getAppScopedFilePath(model.getClass(), model.getId())); log.debug(String.format("Removed state from S3 for '%1$s'.", model)); } /** * {@inheritDoc} */ @Override public Optional readModel(final Class modelClass) throws AlexaStateException { return this.readModel(modelClass, null); } /** * {@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 // reading file from the bucket in the following lines. only if this is true model will be written back to session final TModel model = modelSession.orElse(createModel(modelClass, id)); // we need to remember if there will be something from S3 to be written to the model // in order to write those values back to the session at the end of this method Boolean modelChanged = false; // and if there are user-scoped fields ... if (model.hasUserScopedField() && fromS3FileContentsToModel(model, id, AlexaScope.USER)) { modelChanged = true; } // and if there are app-scoped fields ... if (model.hasApplicationScopedField() && fromS3FileContentsToModel(model, id, AlexaScope.APPLICATION)) { modelChanged = true; } // so if model changed from within something out of S3 we want this to be in the speechlet as well // this gives you access to user- and app-scoped attributes throughout a session without reading from S3 over and over again if (modelChanged) { super.writeModel(model); return Optional.of(model); } else { // if there was nothing received from S3 and there is nothing to return from session // then its not worth return the model. better indicate this model does not exist return modelSession.isPresent() ? Optional.of(model) : Optional.empty(); } } private boolean fromS3FileContentsToModel(final AlexaStateModel alexaStateModel, final String id, final AlexaScope scope) throws AlexaStateException { // read from item with scoped model final String filePath = AlexaScope.APPLICATION.includes(scope) ? getAppScopedFilePath(alexaStateModel.getClass(), id) : getUserScopedFilePath(alexaStateModel.getClass(), id); // extract values from json and assign it to model return awsClient.doesObjectExist(bucketName, filePath) && alexaStateModel.fromJSON(getS3FileContentsAsString(filePath), scope); } private String getS3FileContentsAsString(final String filePath) throws AlexaStateException { final S3Object file = awsClient.getObject(bucketName, filePath); final BufferedReader reader = new BufferedReader(new InputStreamReader(file.getObjectContent())); final StringBuilder sb = new StringBuilder(); String line; try { while ((line = reader.readLine()) != null) { sb.append(line); } } catch (IOException e) { final String error = String.format("Could not read from S3-file '%1$s' from Bucket '%2$s'.", filePath, bucketName); log.error(error, e); throw AlexaStateException.create(error).withCause(e).withHandler(this).build(); } final String fileContents = sb.toString(); return fileContents.isEmpty() ? "{}" : fileContents; } private String getUserScopedFilePath(final Class modelClass) { return getUserScopedFilePath(modelClass, null); } private String getUserScopedFilePath(final Class modelClass, final String id) { return session.getUser().getUserId() + "/" + TModel.getAttributeKey(modelClass, id) + "." + fileExtension; } private String getAppScopedFilePath(final Class modelClass) { return getAppScopedFilePath(modelClass, null); } private String getAppScopedFilePath(final Class modelClass, final String id) { return folderNameApp + "/" + TModel.getAttributeKey(modelClass, id) + "." + fileExtension; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy