io.klerch.alexa.state.handler.AlexaSessionStateHandler.java Source code

Java tutorial

Introduction

Here is the source code for io.klerch.alexa.state.handler.AlexaSessionStateHandler.java

Source

/**
 * 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 extends AlexaStateModel> TModel createModel(final Class<TModel> modelClass) {
        // acts just like a factory for custom models
        return AlexaStateModelFactory.createModel(modelClass, this, null);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public <TModel extends AlexaStateModel> TModel createModel(final Class<TModel> 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<AlexaStateModel> 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<AlexaStateObject> 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<AlexaStateModel> models) throws AlexaStateException {
        Validate.notNull(models, "Collection of models to be removed must not be null.");
        final List<String> 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<String> 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 <TModel extends AlexaStateModel> Optional<TModel> readModel(final Class<TModel> modelClass)
            throws AlexaStateException {
        return readModel(modelClass, null);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @SuppressWarnings("unchecked")
    public <TModel extends AlexaStateModel> Optional<TModel> readModel(final Class<TModel> 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 <TModel extends AlexaStateModel> boolean exists(final Class<TModel> modelClass)
            throws AlexaStateException {
        return exists(TModel.getAttributeKey(modelClass));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public <TModel extends AlexaStateModel> boolean exists(final Class<TModel> modelClass, final String id)
            throws AlexaStateException {
        return exists(TModel.getAttributeKey(modelClass, id));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public <TModel extends AlexaStateModel> boolean exists(final Class<TModel> modelClass, final AlexaScope scope)
            throws AlexaStateException {
        return exists(TModel.getAttributeKey(modelClass), scope);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public <TModel extends AlexaStateModel> boolean exists(final Class<TModel> 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<AlexaStateObject> readValue(final String id) throws AlexaStateException {
        return readValue(id, AlexaScope.SESSION);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Optional<AlexaStateObject> readValue(final String id, final AlexaScope scope)
            throws AlexaStateException {
        final Map<String, AlexaStateObject> result = readValues(Collections.singletonList(id), scope);
        return result.isEmpty() ? Optional.empty() : Optional.of(result.get(id));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Map<String, AlexaStateObject> readValues(final Collection<String> ids) throws AlexaStateException {
        return readValues(ids, AlexaScope.SESSION);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Map<String, AlexaStateObject> readValues(final Collection<String> ids, final AlexaScope scope)
            throws AlexaStateException {
        final Map<String, AlexaScope> idsInScope = new HashMap<>();
        ids.forEach(id -> idsInScope.putIfAbsent(id, scope));
        return readValues(idsInScope);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Map<String, AlexaStateObject> readValues(Map<String, AlexaScope> idsInScope) throws AlexaStateException {
        final Map<String, AlexaStateObject> 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);
    }
}