io.klerch.alexa.state.handler.AlexaSessionStateHandler Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of alexa-skills-kit-states-java Show documentation
Show all versions of alexa-skills-kit-states-java Show documentation
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.
/**
* 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 io.klerch.alexa.state.model.AlexaScope;
import io.klerch.alexa.state.model.AlexaStateModel;
import io.klerch.alexa.state.model.AlexaStateModelFactory;
import io.klerch.alexa.state.model.AlexaStateObject;
import io.klerch.alexa.state.utils.AlexaStateException;
import org.apache.commons.lang3.Validate;
import org.apache.log4j.Logger;
import java.util.*;
import java.util.stream.Collectors;
/**
* As this handler works in the session scope it persists all models to the attributes of an associate Alexa Session object.
* This handler reads and writes state for AlexaStateModels and considers all its fields annotated with AlexaSaveState-tags.
*/
public class AlexaSessionStateHandler implements AlexaStateHandler {
private final Logger log = Logger.getLogger(AlexaSessionStateHandler.class);
final Session session;
/**
* Initializes a handler and applies an Alexa Session to persist models.
* @param session The Alexa session of your current skill invocation.
*/
public AlexaSessionStateHandler(final Session session) {
this.session = session;
}
/**
* {@inheritDoc}
*/
@Override
public Session getSession() {
return this.session;
}
/**
* {@inheritDoc}
*/
@Override
public TModel createModel(final Class modelClass) {
// acts just like a factory for custom models
return AlexaStateModelFactory.createModel(modelClass, this, null);
}
/**
* {@inheritDoc}
*/
@Override
public TModel createModel(final Class modelClass, final String id) {
// acts just like a factory for custom models
return AlexaStateModelFactory.createModel(modelClass, this, id);
}
/**
* {@inheritDoc}
*/
@Override
public void writeModel(final AlexaStateModel model) throws AlexaStateException {
Validate.notNull(model, "Model to write must not be null.");
writeModels(Collections.singletonList(model));
}
/**
* {@inheritDoc}
*/
@Override
public void writeModels(final Collection models) throws AlexaStateException {
Validate.notNull(models, "Collection of models to write must not be null.");
models.forEach(model -> {
try {
// scope annotations will be ignored as there is only one context you can saveState attributes
// thus scope will always be session
session.setAttribute(model.getAttributeKey(), model.toMap(AlexaScope.SESSION));
log.debug(String.format("Wrote state to session attributes for '%1$s'.", model));
} catch (final AlexaStateException e) {
log.error(e);
}
});
}
/**
* {@inheritDoc}
*/
@Override
public void writeValue(final String id, final Object value) throws AlexaStateException {
writeValue(id, value, AlexaScope.SESSION);
}
/**
* {@inheritDoc}
*/
@Override
public void writeValue(final String id, final Object value, final AlexaScope scope) throws AlexaStateException {
Validate.notBlank(id, "Id of single state object must not be blank.");
Validate.notNull(scope, "Scope of single state object must not be null.");
writeValue(new AlexaStateObject(id, value, scope));
}
/**
* {@inheritDoc}
*/
@Override
public void writeValue(final AlexaStateObject stateObject) throws AlexaStateException {
Validate.notNull(stateObject, "State object must not be null.");
writeValues(Collections.singleton(stateObject));
}
/**
* {@inheritDoc}
*/
@Override
public void writeValues(final Collection stateObjects) throws AlexaStateException {
Validate.notNull(stateObjects, "List of state objects to write to persistence store must not be null.");
stateObjects.stream()
.filter(o -> AlexaScope.SESSION.includes(o.getScope()))
.forEach(stateObject -> session.setAttribute(stateObject.getId(), stateObject.getValue()));
}
/**
* {@inheritDoc}
*/
@Override
public void removeModel(final AlexaStateModel model) throws AlexaStateException {
Validate.notNull(model, "Model to be removed must not be null.");
removeModels(Collections.singletonList(model));
}
@Override
public void removeModels(Collection models) throws AlexaStateException {
Validate.notNull(models, "Collection of models to be removed must not be null.");
final List ids = models.stream().map(AlexaStateModel::getAttributeKey).collect(Collectors.toList());
removeValues(ids);
}
/**
* {@inheritDoc}
*/
@Override
public void removeValue(final String id) throws AlexaStateException {
removeValues(Collections.singletonList(id));
}
@Override
public void removeValues(final Collection ids) throws AlexaStateException {
Validate.notNull(ids, "Collection of ids whose values to be removed must not be null.");
ids.forEach(session::removeAttribute);
}
/**
* {@inheritDoc}
*/
@Override
public Optional readModel(final Class modelClass) throws AlexaStateException {
return readModel(modelClass, null);
}
/**
* {@inheritDoc}
*/
@Override
@SuppressWarnings("unchecked")
public Optional readModel(final Class modelClass, final String id) throws AlexaStateException {
final Object o = session.getAttribute(TModel.getAttributeKey(modelClass, id));
if (o == null) {
log.info(String.format("Could not find state for '%1$s' in session attributes.", TModel.getAttributeKey(modelClass, id)));
return Optional.empty();
}
if (o instanceof Map, ?>) {
final Map, ?> childAttributes = (Map, ?>) o;
final TModel model = AlexaStateModelFactory.createModel(modelClass, this, id);
if (model != null) {
model.getSaveStateFields(AlexaScope.SESSION).parallelStream()
.filter(field -> childAttributes.containsKey(field.getName()))
.forEach(field -> {
try {
model.set(field, childAttributes.get(field.getName()));
} catch (AlexaStateException e) {
log.error(String.format("Could not set value for '%1$s' of model '%2$s'", field.getName(), model), e);
}
});
log.debug(String.format("Read state for '%1$s' in session attributes.", model));
}
return model != null ? Optional.of(model) : Optional.empty();
}
else if (o instanceof String) {
final TModel model = AlexaStateModelFactory.createModel(modelClass, this, id);
model.fromJSON((String)o);
log.debug(String.format("Read state for '%1$s' in session attributes.", model));
return Optional.of(model);
}
else {
// if not a map than expect it to be the model
// this only happens if a model was added to the session before its json-serialization
final TModel model = (TModel)o;
log.debug(String.format("Read state for '%1$s' in session attributes.", model));
return Optional.of(model);
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean exists(final Class modelClass) throws AlexaStateException {
return exists(TModel.getAttributeKey(modelClass));
}
/**
* {@inheritDoc}
*/
@Override
public boolean exists(final Class modelClass, final String id) throws AlexaStateException {
return exists(TModel.getAttributeKey(modelClass, id));
}
/**
* {@inheritDoc}
*/
@Override
public boolean exists(final Class modelClass, final AlexaScope scope) throws AlexaStateException {
return exists(TModel.getAttributeKey(modelClass), scope);
}
/**
* {@inheritDoc}
*/
@Override
public boolean exists(final Class modelClass, final String id, final AlexaScope scope) throws AlexaStateException {
return exists(TModel.getAttributeKey(modelClass, id), scope);
}
/**
* {@inheritDoc}
*/
@Override
public boolean exists(final String id) throws AlexaStateException {
return exists(id, AlexaScope.SESSION);
}
/**
* {@inheritDoc}
*/
@Override
public boolean exists(final String id, final AlexaScope scope) throws AlexaStateException {
return readValue(id, scope).isPresent();
}
/**
* {@inheritDoc}
*/
@Override
public Optional readValue(final String id) throws AlexaStateException {
return readValue(id, AlexaScope.SESSION);
}
/**
* {@inheritDoc}
*/
@Override
public Optional readValue(final String id, final AlexaScope scope) throws AlexaStateException {
final Map result = readValues(Collections.singletonList(id), scope);
return result.isEmpty() ? Optional.empty() : Optional.of(result.get(id));
}
/**
* {@inheritDoc}
*/
@Override
public Map readValues(final Collection ids) throws AlexaStateException {
return readValues(ids, AlexaScope.SESSION);
}
/**
* {@inheritDoc}
*/
@Override
public Map readValues(final Collection ids, final AlexaScope scope) throws AlexaStateException {
final Map idsInScope = new HashMap<>();
ids.forEach(id -> idsInScope.putIfAbsent(id, scope));
return readValues(idsInScope);
}
/**
* {@inheritDoc}
*/
@Override
public Map readValues(Map idsInScope) throws AlexaStateException {
final Map stateObjectMap = new HashMap<>();
idsInScope.forEach((k,v) -> {
// do for session-scoped keys only
if (existsInSession(k, v)) {
// read from session and wrap value in state object
stateObjectMap.putIfAbsent(k, new AlexaStateObject(k, session.getAttribute(k), v));
}
});
return stateObjectMap;
}
private boolean existsInSession(final String id, final AlexaScope scope) {
return AlexaScope.SESSION.includes(scope) && session.getAttributes().containsKey(id);
}
}