package gherkin.parser;
import gherkin.I18n;
import gherkin.formatter.Formatter;
import gherkin.lexer.I18nLexer;
import gherkin.lexer.Listener;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Parser implements Listener {
List<Machine> machines = new ArrayList<Machine>();
private final boolean throwOnError;
private final String machineName;
private FormatterListener listener;
private I18nLexer lexer;
private String featureURI;
private Integer lineOffset;
private final Formatter formatter;
public Parser(Formatter formatter) {
this(formatter, true);
}
public Parser(Formatter formatter, boolean throwOnError) {
this(formatter, throwOnError, "root");
}
public Parser(Formatter formatter, boolean throwOnError, String machineName) {
this(formatter, throwOnError, machineName, false);
}
public Parser(Formatter formatter, boolean throwOnError, String machineName, boolean forceRubyDummy) {
this(formatter, throwOnError, machineName, forceRubyDummy, "en");
}
public Parser(Formatter formatter, boolean throwOnError, String machineName, boolean forceRubyDummy, String isoCode) {
if (formatter == null) throw new NullPointerException("formatter");
this.formatter = formatter;
this.listener = new FormatterListener(formatter);
this.throwOnError = throwOnError;
this.machineName = machineName;
this.lexer = new I18nLexer(this, forceRubyDummy, isoCode);
}
/**
* @param featureURI the URI where the gherkin originated from. Typically a file path.
* @param lineOffset the line offset within the uri document the gherkin was taken from. Typically 0.
*/
public void parse(String gherkin, String featureURI, Integer lineOffset) {
formatter.uri(featureURI);
this.featureURI = featureURI;
this.lineOffset = lineOffset;
pushMachine(machineName);
try {
lexer.scan(gherkin);
} finally {
popMachine();
}
}
public I18n getI18nLanguage() {
return lexer.getI18nLanguage();
}
private void pushMachine(String machineName) {
machines.add(new Machine(this, machineName, featureURI));
}
private void popMachine() {
machines.remove(machines.size() - 1);
}
@Override
public void tag(String tag, Integer line) {
if (event("tag", line)) {
listener.tag(tag, line);
}
}
@Override
public void docString(String contentType, String content, Integer line) {
if (event("doc_string", line)) {
listener.docString(contentType, content, line);
}
}
@Override
public void feature(String keyword, String name, String description, Integer line) {
if (event("feature", line)) {
listener.feature(keyword, name, description, line);
}
}
@Override
public void background(String keyword, String name, String description, Integer line) {
if (event("background", line)) {
listener.background(keyword, name, description, line);
}
}
@Override
public void scenario(String keyword, String name, String description, Integer line) {
if (event("scenario", line)) {
listener.scenario(keyword, name, description, line);
}
}
@Override
public void scenarioOutline(String keyword, String name, String description, Integer line) {
if (event("scenario_outline", line)) {
listener.scenarioOutline(keyword, name, description, line);
}
}
@Override
public void examples(String keyword, String name, String description, Integer line) {
if (event("examples", line)) {
listener.examples(keyword, name, description, line);
}
}
@Override
public void step(String keyword, String name, Integer line) {
if (event("step", line)) {
listener.step(keyword, name, line);
}
}
@Override
public void comment(String comment, Integer line) {
if (event("comment", line)) {
listener.comment(comment, line);
}
}
@Override
public void row(List<String> cells, Integer line) {
if (event("row", line)) {
listener.row(cells, line);
}
}
@Override
public void eof() {
if (event("eof", 1)) {
listener.eof();
}
}
private boolean event(String event, Integer line) {
try {
machine().event(event, line);
return true;
} catch (ParseError e) {
if (throwOnError) {
throw e;
} else {
int l = lineOffset + line;
listener.syntaxError(e.getState(), event, e.getLegalEvents(), featureURI, l);
return false;
}
}
}
private Machine machine() {
return machines.get(machines.size() - 1);
}
private static class Machine {
private static final Pattern PUSH = Pattern.compile("push\\((.+)\\)");
private static final Map<String, Map<String, Map<String, String>>> TRANSITION_MAPS = new HashMap<String, Map<String, Map<String, String>>>();
private final Parser parser;
private final String name;
private final String uri;
private String state;
private Map<String, Map<String, String>> transitionMap;
public Machine(Parser parser, String name, String uri) {
if (uri == null) {
throw new NullPointerException("uri");
}
this.parser = parser;
this.name = name;
this.state = name;
this.uri = uri;
this.transitionMap = transitionMap(name);
}
public void event(String event, Integer line) {
Map<String, String> states = transitionMap.get(state);
if (states == null) {
throw new RuntimeException("Unknown getState: " + state + " for machine " + name);
}
String newState = states.get(event);
if (newState == null) {
throw new RuntimeException("Unknown transition: " + event + " among " + states + " for machine " + name + " in getState " + state);
}
if ("E".equals(newState)) {
throw new ParseError(state, event, expectedEvents(), uri, line);
} else {
Matcher push = PUSH.matcher(newState);
if (push.matches()) {
parser.pushMachine(push.group(1));
parser.event(event, line);
} else if ("pop()".equals(newState)) {
parser.popMachine();
parser.event(event, line);
} else {
state = newState;
}
}
}
private List<String> expectedEvents() {
List<String> result = new ArrayList<String>();
for (String event : transitionMap.get(state).keySet()) {
if (!transitionMap.get(state).get(event).equals("E")) {
result.add(event);
}
}
Collections.sort(result);
result.remove("eof");
return result;
}
private Map<String, Map<String, String>> transitionMap(String name) {
Map<String, Map<String, String>> map = TRANSITION_MAPS.get(name);
if (map == null) {
map = buildTransitionMap(name);
TRANSITION_MAPS.put(name, map);
}
return map;
}
private Map<String, Map<String, String>> buildTransitionMap(String name) {
Map<String, Map<String, String>> result = new HashMap<String, Map<String, String>>();
List<List<String>> transitionTable = new StateMachineReader(name).transitionTable();
List<String> events = transitionTable.get(0).subList(1, transitionTable.get(0).size());
for (List<String> actions : transitionTable.subList(1, transitionTable.size())) {
Map<String, String> transitions = new HashMap<String, String>();
int col = 1;
for (String event : events) {
transitions.put(event, actions.get(col++));
}
String state = actions.get(0);
result.put(state, transitions);
}
return result;
}
}
}