RegexStoryParser.java

package org.jbehave.core.parsers;

import static java.util.Arrays.asList;
import static java.util.regex.Pattern.DOTALL;
import static java.util.regex.Pattern.compile;
import static org.apache.commons.lang3.StringUtils.removeStart;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang3.StringUtils;
import org.jbehave.core.annotations.AfterScenario.Outcome;
import org.jbehave.core.annotations.Scope;
import org.jbehave.core.io.LoadFromClasspath;
import org.jbehave.core.io.ResourceLoader;
import org.jbehave.core.model.Description;
import org.jbehave.core.model.ExamplesTable;
import org.jbehave.core.model.ExamplesTableFactory;
import org.jbehave.core.model.GivenStories;
import org.jbehave.core.model.Lifecycle;
import org.jbehave.core.model.Lifecycle.Steps;
import org.jbehave.core.model.Meta;
import org.jbehave.core.model.Narrative;
import org.jbehave.core.model.Scenario;
import org.jbehave.core.model.Story;
import org.jbehave.core.model.TableTransformers;

/**
 * Pattern-based story parser, which uses the keywords provided to parse the
 * textual story into a {@link Story}.
 */
public class RegexStoryParser extends AbstractRegexParser implements StoryParser {

    public static final ResourceLoader DEFAULT_RESOURCE_LOADER = new LoadFromClasspath();
    public static final TableTransformers DEFAULT_TABLE_TRANSFORMERS = new TableTransformers();
    private final ExamplesTableFactory tableFactory;

    public RegexStoryParser() {
        this(new ExamplesTableFactory(DEFAULT_RESOURCE_LOADER, DEFAULT_TABLE_TRANSFORMERS));
    }

    public RegexStoryParser(ExamplesTableFactory tableFactory) {
        super(tableFactory.keywords());
        this.tableFactory = tableFactory;
    }

    @Override
    public Story parseStory(String storyAsText) {
        return parseStory(storyAsText, null);
    }

    @Override
    public Story parseStory(String storyAsText, String storyPath) {
        Description description = parseDescriptionFrom(storyAsText);
        Meta meta = parseStoryMetaFrom(storyAsText);
        Narrative narrative = parseNarrativeFrom(storyAsText);
        GivenStories givenStories = parseGivenStories(storyAsText);
        Lifecycle lifecycle = parseLifecycle(storyAsText);
        if (lifecycle != null) {
            ExamplesTable storyExamplesTable = lifecycle.getExamplesTable();
            if (!storyExamplesTable.isEmpty()) {
                useExamplesTableForGivenStories(givenStories, storyExamplesTable);
            }
        }
        List<Scenario> scenarios = parseScenariosFrom(storyAsText);
        return new Story(storyPath, description, meta, narrative, givenStories, lifecycle, scenarios);
    }

    private Description parseDescriptionFrom(String storyAsText) {
        Matcher findingDescription = findingDescription().matcher(storyAsText);
        if (findingDescription.matches()) {
            return new Description(findingDescription.group(1).trim());
        }
        return Description.EMPTY;
    }

    private Meta parseStoryMetaFrom(String storyAsText) {
        Matcher findingMeta = findingStoryMeta().matcher(preScenarioText(storyAsText));
        if (findingMeta.matches()) {
            String meta = findingMeta.group(1).trim();
            return Meta.createMeta(meta, keywords());
        }
        return Meta.EMPTY;
    }

    private String preScenarioText(String storyAsText) {
        String[] split = storyAsText.split(keywords().scenario());
        return split.length > 0 ? split[0] : storyAsText;
    }

    private Narrative parseNarrativeFrom(String storyAsText) {
        Matcher findingNarrative = findingNarrative().matcher(storyAsText);
        if (findingNarrative.matches()) {
            String narrative = findingNarrative.group(1).trim();
            return createNarrative(narrative);
        }
        return Narrative.EMPTY;
    }

    @SuppressWarnings("checkstyle:LocalVariableName")
    private Narrative createNarrative(String narrative) {
        Matcher findingElements = findingNarrativeElements().matcher(narrative);
        if (findingElements.matches()) {
            String inOrderTo = findingElements.group(1).trim();
            String asA = findingElements.group(2).trim();
            String iWantTo = findingElements.group(3).trim();
            return new Narrative(inOrderTo, asA, iWantTo);
        }
        Matcher findingAlternativeElements = findingAlternativeNarrativeElements().matcher(narrative);
        if (findingAlternativeElements.matches()) {            
            String asA = findingAlternativeElements.group(1).trim();
            String iWantTo = findingAlternativeElements.group(2).trim();
            String soThat = findingAlternativeElements.group(3).trim();
            return new Narrative("", asA, iWantTo, soThat);
        }
        return Narrative.EMPTY;
    }
    
    private GivenStories parseGivenStories(String storyAsText) {
        String scenarioKeyword = keywords().scenario();
        // use text before scenario keyword, if found
        String beforeScenario = "";
        if (StringUtils.contains(storyAsText, scenarioKeyword)) {
            beforeScenario = StringUtils.substringBefore(storyAsText, scenarioKeyword);
        }
        Matcher findingGivenStories = findingStoryGivenStories().matcher(beforeScenario);
        String givenStories = findingGivenStories.find() ? findingGivenStories.group(1).trim() : NONE;
        return new GivenStories(givenStories);
    }

    private Lifecycle parseLifecycle(String storyAsText) {
        String scenarioKeyword = keywords().scenario();
        // use text before scenario keyword, if found
        String beforeScenario = "";
        if (StringUtils.contains(storyAsText, scenarioKeyword)) {
            beforeScenario = StringUtils.substringBefore(storyAsText, scenarioKeyword);
        }
        Matcher findingLifecycle = findingLifecycle().matcher(beforeScenario);
        String lifecycle;
        ExamplesTable examplesTable;
        if (findingLifecycle.find()) {
            lifecycle = findingLifecycle.group(1).trim();
            String examplesTableAsString = findExamplesTable(findingLifecycle.group(0));
            examplesTable = parseExamplesTable(examplesTableAsString);
        } else {
            lifecycle = NONE;
            examplesTable = ExamplesTable.EMPTY;
        }
        Matcher findingBeforeAndAfter = compile(
                ".*" + keywords().before() + "(.*)\\s*" + keywords().after() + "(.*)\\s*", DOTALL).matcher(lifecycle);
        if (findingBeforeAndAfter.matches()) {
            String beforeLifecycle = findingBeforeAndAfter.group(1).trim();
            List<Steps> beforeSteps = parseBeforeLifecycle(beforeLifecycle);
            String afterLifecycle = findingBeforeAndAfter.group(2).trim();
            List<Steps> afterSteps = parseAfterLifecycle(afterLifecycle);
            return new Lifecycle(examplesTable, beforeSteps, afterSteps);
        }
        Matcher findingBefore = compile(".*" + keywords().before() + "(.*)\\s*", DOTALL).matcher(lifecycle);
        if (findingBefore.matches()) {
            String beforeLifecycle = findingBefore.group(1).trim();
            List<Steps> beforeSteps = parseBeforeLifecycle(beforeLifecycle);
            return new Lifecycle(examplesTable, beforeSteps, Arrays.<Steps>asList());
        }
        Matcher findingAfter = compile(".*" + keywords().after() + "(.*)\\s*", DOTALL).matcher(lifecycle);
        if (findingAfter.matches()) {
            List<Steps> beforeSteps = asList();
            String afterLifecycle = findingAfter.group(1).trim();
            List<Steps> afterSteps = parseAfterLifecycle(afterLifecycle);
            return new Lifecycle(examplesTable, beforeSteps, afterSteps);
        }
        return new Lifecycle(examplesTable);
    }

    private List<Steps> parseBeforeLifecycle(String lifecycleAsText) {
        List<Steps> list = new ArrayList<>();
        for (String byScope : lifecycleAsText.split(keywords().scope())) {
            byScope = byScope.trim();
            if (byScope.isEmpty()) {
                continue;
            }
            Scope scope = parseScope(findScope(keywords().scope() + byScope));
            Steps steps = new Steps(scope, findSteps(startingWithNL(byScope)));
            list.add(steps);
        }
        return list;
    }

    private List<Steps> parseAfterLifecycle(String lifecycleAsText) {
        List<Steps> list = new ArrayList<>();
        for (String byScope : lifecycleAsText.split(keywords().scope())) {
            byScope = byScope.trim();
            if (byScope.isEmpty()) {
                continue;
            }
            Scope scope = parseScope(findScope(keywords().scope() + byScope));
            for (String byOutcome : byScope.split(keywords().outcome())) {
                byOutcome = byOutcome.trim();
                if (byOutcome.isEmpty()) {
                    continue;
                }
                String outcomeAsText = findOutcome(byOutcome);
                String filtersAsText = findFilters(removeStart(byOutcome, outcomeAsText));
                List<String> steps = findSteps(startingWithNL(removeStart(byOutcome, filtersAsText)));
                list.add(new Steps(scope, parseOutcome(outcomeAsText), parseFilters(filtersAsText), steps));
            }
        }
        return list;
    }

    private String findScope(String lifecycleAsText) {
        Matcher findingScope = findingLifecycleScope().matcher(lifecycleAsText.trim());
        if (findingScope.matches()) {
            return findingScope.group(1).trim();
        }
        return NONE;
    }

    private Scope parseScope(String scopeAsText) {
        if (scopeAsText.trim().equals(keywords().scopeStep())) {
            return Scope.STEP;
        } else if (scopeAsText.trim().equals(keywords().scopeScenario())) {
            return Scope.SCENARIO;
        } else if (scopeAsText.trim().equals(keywords().scopeStory())) {
            return Scope.STORY;
        }
        return Scope.SCENARIO;
    }

    private String findOutcome(String stepsByOutcome) {
        Matcher findingOutcome = findingLifecycleOutcome().matcher(stepsByOutcome);
        if (findingOutcome.matches()) {
            return findingOutcome.group(1).trim();
        }
        return keywords().outcomeAny();
    }

    private Outcome parseOutcome(String outcomeAsText) {
        if (outcomeAsText.equals(keywords().outcomeSuccess())) {
            return Outcome.SUCCESS;
        } else if (outcomeAsText.equals(keywords().outcomeFailure())) {
            return Outcome.FAILURE;
        }
        return Outcome.ANY;
    }

    private String findFilters(String stepsByFilters) {
        Matcher findingFilters = findingLifecycleFilters().matcher(stepsByFilters.trim());
        if (findingFilters.matches()) {
            return findingFilters.group(1).trim();
        }
        return NONE;
    }

    private String parseFilters(String filtersAsText) {
        return removeStart(filtersAsText, keywords().metaFilter()).trim();
    }

    private List<Scenario> parseScenariosFrom(String storyAsText) {
        List<Scenario> parsed = new ArrayList<>();
        for (String scenarioAsText : splitScenarios(storyAsText)) {
            parsed.add(parseScenario(scenarioAsText));
        }
        return parsed;
    }

    private List<String> splitScenarios(String storyAsText) {
        String scenarioKeyword = keywords().scenario();

        // use text after scenario keyword, if found
        if (StringUtils.contains(storyAsText, scenarioKeyword)) {
            storyAsText = StringUtils.substringAfter(storyAsText, scenarioKeyword);
        }

        return splitElements(storyAsText, scenarioKeyword);
    }

    private Scenario parseScenario(String scenarioAsText) {
        String title = findScenarioTitle(scenarioAsText);
        String scenarioWithoutKeyword = removeStart(scenarioAsText, keywords().scenario()).trim();
        String scenarioWithoutTitle = removeStart(scenarioWithoutKeyword, title);
        scenarioWithoutTitle = startingWithNL(scenarioWithoutTitle);
        Meta meta = findScenarioMeta(scenarioWithoutTitle);
        String examplesTableAsString = findExamplesTable(scenarioWithoutTitle);
        ExamplesTable examplesTable = parseExamplesTable(examplesTableAsString);
        GivenStories givenStories = findScenarioGivenStories(scenarioWithoutTitle);
        useExamplesTableForGivenStories(givenStories, examplesTable);
        List<String> rawSteps = new ArrayList<>();
        if (examplesTableAsString.trim().isEmpty()) {
            rawSteps.addAll(findSteps(scenarioWithoutTitle));
        } else {
            int afterExampleIndex = scenarioWithoutTitle.indexOf(examplesTableAsString)
                    + examplesTableAsString.length();
            rawSteps.addAll(findSteps(scenarioWithoutTitle.substring(0, afterExampleIndex)));
        }
        return new Scenario(title, meta, givenStories, examplesTable, rawSteps);
    }

    private void useExamplesTableForGivenStories(GivenStories givenStories, ExamplesTable examplesTable) {
        if (givenStories.requireParameters()) {
            givenStories.useExamplesTable(examplesTable);
        }
    }

    private String findScenarioTitle(String scenarioAsText) {
        Matcher findingTitle = findingScenarioTitle().matcher(scenarioAsText);
        return findingTitle.find() ? findingTitle.group(1).trim() : NONE;
    }

    private Meta findScenarioMeta(String scenarioAsText) {
        Matcher findingMeta = findingScenarioMeta().matcher(scenarioAsText);
        if (findingMeta.matches()) {
            String meta = findingMeta.group(1).trim();
            return Meta.createMeta(meta, keywords());
        }
        return Meta.EMPTY;
    }

    private String findExamplesTable(String scenarioAsText) {
        Matcher findingTable = findingExamplesTable().matcher(scenarioAsText);
        return findingTable.find() ? findingTable.group(1).trim() : NONE;
    }

    private ExamplesTable parseExamplesTable(String tableInput) {
        return tableFactory.createExamplesTable(tableInput);
    }

    private GivenStories findScenarioGivenStories(String scenarioAsText) {
        Matcher findingGivenStories = findingScenarioGivenStories().matcher(scenarioAsText);
        String givenStories = findingGivenStories.find() ? findingGivenStories.group(1).trim() : NONE;
        return new GivenStories(givenStories);
    }

    // Regex Patterns

    private Pattern findingDescription() {
        String metaOrNarrativeOrLifecycleOrScenario = concatenateWithOr(keywords().meta(), keywords().narrative(),
                keywords().lifecycle(), keywords().scenario());
        return compile("(.*?)(" + metaOrNarrativeOrLifecycleOrScenario + ").*", DOTALL);
    }

    private Pattern findingStoryMeta() {
        String narrativeOrLifecycleOrGivenStories = concatenateWithOr(keywords().narrative(), keywords().lifecycle(),
                keywords().givenStories());
        return compile(".*" + keywords().meta() + "(.*?)\\s*(\\Z|" + narrativeOrLifecycleOrGivenStories + ").*",
                DOTALL);
    }

    private Pattern findingNarrative() {
        String givenStoriesOrLifecycleOrScenario = concatenateWithOr(keywords().givenStories(), keywords().lifecycle(),
                keywords().scenario());
        return compile(".*" + keywords().narrative() + "(.*?)\\s*(" + givenStoriesOrLifecycleOrScenario + ").*",
                DOTALL);
    }

    private Pattern findingNarrativeElements() {
        return compile(".*" + keywords().inOrderTo() + "(.*)\\s*" + keywords().asA() + "(.*)\\s*" + keywords().iWantTo()
                + "(.*)", DOTALL);
    }

    private Pattern findingAlternativeNarrativeElements() {
        return compile(
                ".*" + keywords().asA() + "(.*)\\s*" + keywords().iWantTo() + "(.*)\\s*" + keywords().soThat() + "(.*)",
                DOTALL);
    }
    
    private Pattern findingStoryGivenStories() {
        String lifecycleOrScenario = concatenateWithOr(keywords().lifecycle(), keywords().scenario());
        return compile(".*" + keywords().givenStories() + "(.*?)\\s*(\\Z|" + lifecycleOrScenario + ").*", DOTALL);
    }
    
    private Pattern findingLifecycle() {
        return compile(".*" + keywords().lifecycle() + "\\s*(.*)", DOTALL);
    }

    private Pattern findingLifecycleScope() {
        String startingWords = concatenateStartingWords();
        return compile(keywords().scope() + "((.)*?)\\s*(" + keywords().outcome() + "|" + keywords().metaFilter() + "|"
                + startingWords + ").*", DOTALL);
    }

    private Pattern findingLifecycleOutcome() {
        String startingWords = concatenateStartingWords();
        String outcomes = concatenateWithOr(keywords().outcomeAny(), keywords().outcomeSuccess(),
                keywords().outcomeFailure());
        return compile("\\s*(" + outcomes + ")\\s*(" + keywords().metaFilter() + "|" + startingWords + ").*", DOTALL);
    }

    private Pattern findingLifecycleFilters() {
        String startingWords = concatenateStartingWords();
        String filters = concatenateWithOr(keywords().metaFilter());
        return compile("\\s*(" + filters + "[\\w\\+\\-\\_\\s]*)(" + startingWords + ").*", DOTALL);
    }

    private Pattern findingScenarioTitle() {
        String startingWords = concatenateStartingWords();
        return compile(keywords().scenario() + "(.*?)\\s*(" + keywords().meta() + "|" + keywords().givenStories() + "|"
                + startingWords + "|$).*", DOTALL);
    }

    private Pattern findingScenarioMeta() {
        String startingWords = concatenateStartingWords();
        return compile(
                ".*" + keywords().meta() + "(.*?)\\s*(" + keywords().givenStories() + "|" + startingWords + "|$).*",
                DOTALL);
    }

    private Pattern findingScenarioGivenStories() {
        String startingWords = concatenateStartingWords();
        return compile("\\n" + keywords().givenStories() + "((.|\\n)*?)\\s*(" + startingWords + ").*", DOTALL);
    }

    private Pattern findingExamplesTable() {
        return compile("\\n" + keywords().examplesTable() + "\\s*(.*?)(?:\\n" + keywords().ignorable() + ".*)?$",
                DOTALL);
    }

    private String concatenateWithOr(String... keywords) {
        return concatenateWithOr(NONE, asList(keywords));
    }
}