package org.jbehave.core.parsers; import static java.util.regex.Pattern.DOTALL; import static java.util.regex.Pattern.compile; import static org.apache.commons.lang3.StringUtils.removeStart; import java.io.File; import java.util.ArrayList; 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.configuration.Configuration; import org.jbehave.core.configuration.Keywords; import org.jbehave.core.i18n.LocalizedKeywords; 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 implements StoryParser { private static final String NONE = ""; private final Keywords keywords; private final ExamplesTableFactory tableFactory; public RegexStoryParser() { this(new TableTransformers()); } public RegexStoryParser(TableTransformers tableTransformers) { this(new LocalizedKeywords(), tableTransformers); } public RegexStoryParser(Keywords keywords, TableTransformers tableTransformers) { this(keywords, new ExamplesTableFactory(keywords, tableTransformers)); } public RegexStoryParser(ExamplesTableFactory tableFactory) { this(tableFactory.keywords(), tableFactory); } public RegexStoryParser(Keywords keywords, ExamplesTableFactory tableFactory) { this.keywords = keywords; this.tableFactory = tableFactory; // must ensure that both are using same keywords this.tableFactory.useKeywords(keywords); } public RegexStoryParser(Configuration configuration) { this.keywords = configuration.keywords(); this.tableFactory = new ExamplesTableFactory(configuration); } public Story parseStory(String storyAsText) { return parseStory(storyAsText, null); } 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); List<Scenario> scenarios = parseScenariosFrom(storyAsText); Story story = new Story(storyPath, description, meta, narrative, givenStories, lifecycle, scenarios); if (storyPath != null) { story.namedAs(new File(storyPath).getName()); } return story; } 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; } 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 = findingLifecycle.find() ? findingLifecycle.group(1).trim() : NONE; Matcher findingBeforeAndAfter = compile(".*" + keywords.before() + "(.*)\\s*" + keywords.after() + "(.*)\\s*", DOTALL).matcher(lifecycle); if ( findingBeforeAndAfter.matches() ){ String beforeLifecycle = findingBeforeAndAfter.group(1).trim(); Steps beforeSteps = parseBeforeLifecycle(beforeLifecycle); String afterLifecycle = findingBeforeAndAfter.group(2).trim(); Steps[] afterSteps = parseAfterLifecycle(afterLifecycle); return new Lifecycle(beforeSteps, afterSteps); } Matcher findingBefore = compile(".*" + keywords.before() + "(.*)\\s*", DOTALL).matcher(lifecycle); if ( findingBefore.matches() ){ String beforeLifecycle = findingBefore.group(1).trim(); Steps beforeSteps = parseBeforeLifecycle(beforeLifecycle); return new Lifecycle(beforeSteps, new Steps(new ArrayList<String>())); } Matcher findingAfter = compile(".*" + keywords.after() + "(.*)\\s*", DOTALL).matcher(lifecycle); if ( findingAfter.matches() ){ Steps beforeSteps = Steps.EMPTY; String afterLifecycle = findingAfter.group(1).trim(); Steps[] afterSteps = parseAfterLifecycle(afterLifecycle); return new Lifecycle(beforeSteps, afterSteps); } return Lifecycle.EMPTY; } private Steps parseBeforeLifecycle(String lifecycleAsText) { return new Steps(findSteps(startingWithNL(lifecycleAsText))); } private Steps[] parseAfterLifecycle(String lifecycleAsText) { List<Steps> list = new ArrayList<Steps>(); for (String byOutcome : lifecycleAsText.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(parseOutcome(outcomeAsText), parseFilters(filtersAsText), steps)); } return list.toArray(new Steps[list.size()]); } 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<Scenario>(); for (String scenarioAsText : splitScenarios(storyAsText)) { parsed.add(parseScenario(scenarioAsText)); } return parsed; } private List<String> splitScenarios(String storyAsText) { List<String> scenarios = new ArrayList<String>(); String scenarioKeyword = keywords.scenario(); // use text after scenario keyword, if found if (StringUtils.contains(storyAsText, scenarioKeyword)) { storyAsText = StringUtils.substringAfter(storyAsText, scenarioKeyword); } for (String scenarioAsText : storyAsText.split(scenarioKeyword)) { if (scenarioAsText.trim().length() > 0) { scenarios.add(scenarioKeyword + "\n" + scenarioAsText); } } return scenarios; } 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); ExamplesTable examplesTable = findExamplesTable(scenarioWithoutTitle); GivenStories givenStories = findScenarioGivenStories(scenarioWithoutTitle); if (givenStories.requireParameters()) { givenStories.useExamplesTable(examplesTable); } List<String> steps = findSteps(scenarioWithoutTitle); return new Scenario(title, meta, givenStories, examplesTable, steps); } private String startingWithNL(String text) { if ( !text.startsWith("\n") ){ // always ensure starts with newline return "\n" + text; } return text; } 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 ExamplesTable findExamplesTable(String scenarioAsText) { Matcher findingTable = findingExamplesTable().matcher(scenarioAsText); String tableInput = findingTable.find() ? findingTable.group(1).trim() : NONE; 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); } private List<String> findSteps(String stepsAsText) { Matcher matcher = findingSteps().matcher(stepsAsText); List<String> steps = new ArrayList<String>(); int startAt = 0; while (matcher.find(startAt)) { steps.add(StringUtils.substringAfter(matcher.group(1), "\n")); startAt = matcher.start(4); } return steps; } // 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 findingLifecycleOutcome() { String startingWords = concatenateWithOr("\\n", "", keywords.startingWords()); String outcomes = concatenateWithOr(keywords.outcomeAny(), keywords.outcomeSuccess(), keywords.outcomeFailure()); return compile("\\s*("+ outcomes +")\\s*(" + keywords.metaFilter() + "|" + startingWords + ").*", DOTALL); } private Pattern findingLifecycleFilters() { String startingWords = concatenateWithOr("\\n", "", keywords.startingWords()); String filters = concatenateWithOr(keywords.metaFilter()); return compile("\\s*("+ filters +"[\\w\\+\\-\\_\\s]*)(" + startingWords + ").*", DOTALL); } private Pattern findingScenarioTitle() { String startingWords = concatenateWithOr("\\n", "", keywords.startingWords()); return compile(keywords.scenario() + "((.)*?)\\s*(" + keywords.meta() + "|" + startingWords + ").*", DOTALL); } private Pattern findingScenarioMeta() { String startingWords = concatenateWithOr("\\n", "", keywords.startingWords()); return compile(".*" + keywords.meta() + "(.*?)\\s*(" + keywords.givenStories() + "|" + startingWords + ").*", DOTALL); } private Pattern findingScenarioGivenStories() { String startingWords = concatenateWithOr("\\n", "", keywords.startingWords()); return compile("\\n" + keywords.givenStories() + "((.|\\n)*?)\\s*(" + startingWords + ").*", DOTALL); } private Pattern findingSteps() { String initialStartingWords = concatenateWithOr("\\n", "", keywords.startingWords()); String followingStartingWords = concatenateWithOr("\\n", "\\s", keywords.startingWords()); return compile( "((" + initialStartingWords + ")\\s(.)*?)\\s*(\\Z|" + followingStartingWords + "|\\n" + keywords.examplesTable() + ")", DOTALL); } private Pattern findingExamplesTable() { return compile("\\n" + keywords.examplesTable() + "\\s*(.*)", DOTALL); } private String concatenateWithOr(String... keywords) { return concatenateWithOr(null, null, keywords); } private String concatenateWithOr(String beforeKeyword, String afterKeyword, String[] keywords) { StringBuilder builder = new StringBuilder(); String before = beforeKeyword != null ? beforeKeyword : NONE; String after = afterKeyword != null ? afterKeyword : NONE; for (String keyword : keywords) { builder.append(before).append(keyword).append(after).append("|"); } return StringUtils.removeEnd(builder.toString(), "|"); // remove last "|" } }