JsonAliasParser.java

package org.jbehave.core.parsers;

import static java.util.stream.Collectors.collectingAndThen;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toList;
import static org.apache.commons.lang3.Validate.isTrue;

import java.lang.reflect.Type;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;

import org.jbehave.core.configuration.Keywords;
import org.jbehave.core.model.Alias;
import org.jbehave.core.model.AliasVariant;
import org.jbehave.core.steps.StepType;

public class JsonAliasParser implements AliasParser {

    private final Keywords keywords;

    public JsonAliasParser(Keywords keywords) {
        this.keywords = keywords;
    }

    @SuppressWarnings("unchecked")
    @Override
    public Collection<Alias> parse(Set<String> aliasesAsStrings) {
        if (aliasesAsStrings.isEmpty()) {
            return Collections.emptyList();
        }

        Gson gson = new Gson();
        Type aliasType = new TypeToken<List<AliasContainer>>() {}.getType();

        return aliasesAsStrings.stream()
                               .map(json -> (List<AliasContainer>) gson.fromJson(json, aliasType))
                               .flatMap(List::stream)
                               .map(JsonAliasParser::validate)
                               .collect(groupingBy(AliasContainer::getName, collectingAndThen(toList(),
                                       JsonAliasParser::mergeAliases)))
                               .values()
                               .stream()
                               .map(this::convert)
                               .collect(toList());
    }

    // https://github.com/google/gson/issues/61
    private static AliasContainer validate(AliasContainer alias) {
        isTrue(alias.getName() != null, "The 'name' property that identifies step must be set");
        Set<AliasEntry> aliases = alias.getAliases();
        isTrue(aliases != null, "The 'aliases' property must be set");
        aliases.forEach(entry -> isTrue(entry.getName() != null, "The 'name' property of alias must be set"));
        return alias;
    }

    private static AliasContainer mergeAliases(List<AliasContainer> aliases) {
        if (aliases.size() == 1) {
            return aliases.get(0);
        }

        return aliases.stream().reduce((l, r) -> {
            l.getAliases().addAll(r.getAliases());
            return l;
        }).get();
    }

    private Alias convert(AliasContainer container) {
        String name = container.getName();
        StepType stepType = keywords.stepTypeFor(name);

        List<AliasVariant> variants = container.getAliases().stream().map(alias -> {
            String aliasName = alias.getName();
            StepType aliasType = keywords.stepTypeFor(alias.getName());

            if (aliasType != stepType) {
                throw new InvalidAliasType(aliasName, stepType);
            }

            return keywords.stepWithoutStartingWord(aliasName, aliasType);
        }).map(AliasVariant::new).collect(Collectors.toList());

        return new Alias(keywords.stepWithoutStartingWord(name, stepType), stepType, variants);
    }

    @SuppressWarnings("unused")
    private static final class AliasContainer {

        private String name;
        private Set<AliasEntry> aliases;

        private String getName() {
            return name;
        }

        private void setName(String name) {
            this.name = name;
        }

        private Set<AliasEntry> getAliases() {
            return aliases;
        }

        private void setAliases(Set<AliasEntry> aliases) {
            this.aliases = aliases;
        }

    }

    @SuppressWarnings("unused")
    private static final class AliasEntry {

        private String name;

        private String getName() {
            return name;
        }

        private void setName(String name) {
            this.name = name;
        }

        @Override
        public int hashCode() {
            return Objects.hash(name);
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }

            if (!(obj instanceof AliasEntry)) {
                return false;
            }

            AliasEntry other = (AliasEntry) obj;
            return Objects.equals(name, other.name);
        }

    }

    @SuppressWarnings("serial")
    public static class InvalidAliasType extends RuntimeException {

        public InvalidAliasType(String alias, StepType stepType) {
            super(String.format("The alias '%s' must be of type '%s'", alias, stepType));
        }

    }
}