io.klerch.alexa.tellask.util.resource.YamlReader.java Source code

Java tutorial

Introduction

Here is the source code for io.klerch.alexa.tellask.util.resource.YamlReader.java

Source

/**
 * Created by Kay Lerch (https://twitter.com/KayLerch)
 * <p>
 * Contribute to https://github.com/KayLerch/alexa-skills-kit-tellask-java
 * <p>
 * Attached license applies.
 * This source is licensed under GNU GENERAL PUBLIC LICENSE Version 3 as of 29 June 2007
 */
package io.klerch.alexa.tellask.util.resource;

import io.klerch.alexa.tellask.model.AlexaOutput;
import io.klerch.alexa.tellask.schema.UtteranceReader;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.log4j.Logger;
import org.yaml.snakeyaml.Yaml;

import java.util.*;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * This reader processes input coming from the resource streams of
 * UtteranceReader to extract the utterances and reprompts and optionally
 * picks one of them randomly.
 */
public class YamlReader {
    private static final Logger LOG = Logger.getLogger(YamlReader.class);

    private final UtteranceReader utteranceReader;
    private final Map<String, List<Object>> phrases = new HashMap<>();
    private final String locale;
    private final Map<?, ?> content;

    /**
     * A new YAMLReader needs an UtteranceReader so it can obtain YAML content with utterances.
     * @param utteranceReader the UtteranceReader reading ot YAML content with utterances
     * @param locale Localized skills will have multiple YAML files (one for each language). By
     *               giving this reader a locale it lets the UtteranceReader read out the appropriate
     *               YAML file.
     */
    public YamlReader(final UtteranceReader utteranceReader, final String locale) {
        this.utteranceReader = utteranceReader;
        this.locale = locale;
        // leverage reader to get yaml with utterances
        content = new Yaml().loadAs(utteranceReader.read(locale), Map.class);
    }

    /**
     * Returns a set of utterances for an intent which is defined in the AlexaOutput.
     * @param output The AlexaOutput containing the intent name whose utterances are to be returned.
     * @return set of utterances for an intent which is defined in the AlexaOutput
     */
    public List<String> getUtterances(final AlexaOutput output) {
        return getUtterances(output.getIntentName());
    }

    /**
     * Returns an utterance randomly picked from a set of utterances for an intent which is defined in the AlexaOutput.
     * @param output The AlexaOutput containing the intent name.
     * @return utterance randomly picked from a set of utterances for an intent which is defined in the AlexaOutput
     */
    public Optional<String> getRandomUtterance(final AlexaOutput output) {
        return getRandomUtterance(output.getIntentName());
    }

    /**
     * Returns a set of utterances for a given intent.
     * @param intentName the intent name whose utterances are to be returned
     * @return set of utterances for the given intent
     */
    public List<String> getUtterances(final String intentName) {
        return getPhrasesForIntent(intentName, 0);
    }

    /**
     * Returns an utterance randomly picked from a set of utterances for a given intent.
     * @param intentName The intent name.
     * @return utterance randomly picked from a set of utterances for the given intent
     */
    public Optional<String> getRandomUtterance(final String intentName) {
        return getRandomOf(getPhrasesForIntent(intentName, 0));
    }

    /**
     * Returns a set of reprompt utterances for an intent which is defined in the AlexaOutput.
     * @param output The AlexaOutput containing the intent name whose reprompt utterances are to be returned.
     * @return set of reprompt utterances for an intent which is defined in the AlexaOutput
     */
    public List<String> getReprompts(final AlexaOutput output) {
        return getReprompts(output.getIntentName());
    }

    /**
     * Returns a reprompt utterance randomly picked from a set of reprompt utterances for an intent which is defined in the AlexaOutput.
     * @param output The AlexaOutput containing the intent name.
     * @return reprompt utterance randomly picked from a set of reprompt utterances for an intent which is defined in the AlexaOutput
     */
    public Optional<String> getRandomReprompt(final AlexaOutput output) {
        return getRandomReprompt(output.getIntentName());
    }

    /**
     * Returns a set of reprompt utterances for a given intent.
     * @param intentName the intent name whose reprompt utterances are to be returned
     * @return set of reprompt utterances for the given intent
     */
    public List<String> getReprompts(final String intentName) {
        return getPhrasesForIntent(intentName, 1);
    }

    /**
     * Returns a reprompt utterance randomly picked from a set of reprompt utterances for a given intent.
     * @param intentName The intent name.
     * @return reprompt utterance randomly picked from a set of reprompt utterances for the given intent
     */
    public Optional<String> getRandomReprompt(final String intentName) {
        return getRandomOf(getPhrasesForIntent(intentName, 1));
    }

    private List<Object> loadUtterances(final String intentName) {
        // flatten yaml strings values beneath intent node of interest
        return content.entrySet().stream().filter(k -> k.getKey().equals(intentName))
                .flatMap(k -> flatten(k.getValue())).collect(Collectors.toList());
    }

    /**
     * Recursively go along yaml nodes beneath the given one to flatten string values
     * @param o YAML node point of start
     * @return flattened values beneath given YAML node
     */
    private Stream<Object> flatten(final Object o) {
        if (o instanceof Map<?, ?>) {
            return ((Map<?, ?>) o).values().stream().flatMap(this::flatten);
        }
        return Stream.of(o);
    }

    @SuppressWarnings("unchecked")
    private List<String> getPhrasesForIntent(final String intentName, final Integer index) {
        Validate.notBlank(intentName, "Intent name is null or empty.");
        // return list of utterances if already read out and saved to local list
        final List<Object> contents = phrases.entrySet().stream().filter(k -> k.getKey().equals(intentName))
                .findFirst()
                // otherwise load utterances from resource of utterance reader
                .orElse(new AbstractMap.SimpleEntry<>(intentName, loadUtterances(intentName))).getValue();
        // cache the result
        phrases.putIfAbsent(intentName, contents);

        final List<String> utterances = new ArrayList<>();

        if (contents.size() > index) {
            // group node assumed to be an array list
            final Object assumedUtteranceCollection = contents.get(index);

            if (assumedUtteranceCollection instanceof ArrayList) {
                // parse each phrase as string and add to return collection
                ((ArrayList) assumedUtteranceCollection).stream().map(resolveMultiPhrases).map(resolvePlaceholders)
                        .forEach(utterance -> utterances.add(String.valueOf(utterance)));
            } else if (assumedUtteranceCollection instanceof String) {
                utterances.add(String.valueOf(assumedUtteranceCollection));
            }
        }
        return utterances;
    }

    private Function<Object, String> resolveMultiPhrases = (final Object utterance) -> {
        final StringBuffer buffer = new StringBuffer();
        // extract all the phrase collection (e.g. [Hello|Hi|Welcome] found in the utterance
        final Matcher multiPhrases = Pattern.compile("\\[(.*?)\\]").matcher(utterance.toString());
        // for any of the multi-phrases ...
        while (multiPhrases.find()) {
            final String multiPhrase = multiPhrases.group(1);
            // single phrases are delimited by pipes
            final List<String> multiPhraseCollection = Arrays.asList(multiPhrase.split("\\|"));
            // pick random phrase out of the collection
            final String randomPhrase = getRandomOf(multiPhraseCollection).orElse("");
            if (StringUtils.isBlank(randomPhrase)) {
                LOG.warn(
                        "Empty multi-phrase collection found in one of your utterances. Got replaced by an empty string in speechlet response.");
            }
            multiPhrases.appendReplacement(buffer, randomPhrase);
        }
        multiPhrases.appendTail(buffer);
        return buffer.toString();
    };

    private Function<Object, String> resolvePlaceholders = (final Object utterance) -> {
        final StringBuffer buffer = new StringBuffer();
        // extract all the placeholders (e.g. ${placeholder}) found in the utterance
        final Matcher placeholders = Pattern.compile("\\$\\{(.*?)\\}").matcher(utterance.toString());
        // for any of the placeholders ...
        while (placeholders.find()) {
            final String placeholderName = placeholders.group(1);
            final List<String> placeholderValues = getPhrasesForIntent(placeholderName, 0);

            Validate.notEmpty(placeholderValues,
                    "Utterance placeholder with name '" + placeholderName + "' could not be resolved.");
            placeholders.appendReplacement(buffer, placeholderValues.get(0));
        }
        placeholders.appendTail(buffer);
        return resolveMultiPhrases.apply(buffer.toString());
    };

    private Optional<String> getRandomOf(final List<String> list) {
        return list.isEmpty() ? Optional.empty() : Optional.of(list.get(new Random().nextInt(list.size())));
    }
}