package gherkin.formatter; import gherkin.deps.com.google.gson.Gson; import gherkin.deps.com.google.gson.GsonBuilder; import gherkin.deps.net.iharder.Base64; import gherkin.formatter.model.Background; import gherkin.formatter.model.Examples; import gherkin.formatter.model.Feature; import gherkin.formatter.model.Match; import gherkin.formatter.model.Result; import gherkin.formatter.model.Scenario; import gherkin.formatter.model.ScenarioOutline; import gherkin.formatter.model.Step; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class JSONFormatter implements Reporter, Formatter { private final List<Map<String, Object>> featureMaps = new ArrayList<Map<String, Object>>(); private final NiceAppendable out; private Map<String, Object> featureMap; private String uri; private List<Map> beforeHooks = new ArrayList<Map>(); private enum Phase {step, match, embedding, output, result}; /** * In order to handle steps being added all at once, this method determines allows methods to * opperator correctly if * * step * match * embedding * output * result * step * match * embedding * output * result * * or if * * step * step * match * embedding * output * result * match * embedding * output * result * * is called * * @return the correct step for the current operation based on past method calls to the formatter interface */ private Map getCurrentStep(Phase phase) { String target = phase.ordinal() <= Phase.match.ordinal()?Phase.match.name():Phase.result.name(); Map lastWithValue = null; for (Map stepOrHook : getSteps()) { if (stepOrHook.get(target) == null) { return stepOrHook; } else { lastWithValue = stepOrHook; } } return lastWithValue; } public JSONFormatter(Appendable out) { this.out = new NiceAppendable(out); } @Override public void uri(String uri) { this.uri = uri; } @Override public void feature(Feature feature) { featureMap = feature.toMap(); featureMap.put("uri", uri); featureMaps.add(featureMap); } @Override public void background(Background background) { getFeatureElements().add(background.toMap()); } @Override public void scenario(Scenario scenario) { getFeatureElements().add(scenario.toMap()); if (beforeHooks.size() > 0) { getFeatureElement().put("before", beforeHooks); beforeHooks = new ArrayList<Map>(); } } @Override public void scenarioOutline(ScenarioOutline scenarioOutline) { getFeatureElements().add(scenarioOutline.toMap()); } @Override public void examples(Examples examples) { getAllExamples().add(examples.toMap()); } @Override public void step(Step step) { getSteps().add(step.toMap()); } @Override public void match(Match match) { getCurrentStep(Phase.match).put("match", match.toMap()); } @Override public void embedding(String mimeType, byte[] data) { final Map<String, String> embedding = new HashMap<String, String>(); embedding.put("mime_type", mimeType); embedding.put("data", Base64.encodeBytes(data)); getEmbeddings().add(embedding); } @Override public void write(String text) { getOutput().add(text); } @Override public void result(Result result) { getCurrentStep(Phase.result).put("result", result.toMap()); } @Override public void before(Match match, Result result) { beforeHooks.add(buildHookMap(match,result)); } @Override public void after(Match match, Result result) { List<Map> hooks = getFeatureElement().get("after"); if (hooks == null) { hooks = new ArrayList<Map>(); getFeatureElement().put("after", hooks); } hooks.add(buildHookMap(match,result)); } private Map buildHookMap(final Match match, final Result result) { final Map hookMap = new HashMap(); hookMap.put("match", match.toMap()); hookMap.put("result", result.toMap()); return hookMap; } public void appendDuration(final int timestamp) { final Map result = (Map) getCurrentStep(Phase.result).get("result"); // check to make sure result exists (scenario outlines do not have results yet) if (result != null) { //convert to nanoseconds final long nanos = timestamp * 1000000000L; result.put("duration", nanos); } } @Override public void eof() { } @Override public void done() { out.append(gson().toJson(featureMaps)); // We're *not* closing the stream here. // https://github.com/cucumber/gherkin/issues/151 // https://github.com/cucumber/cucumber-jvm/issues/96 } @Override public void close() { out.close(); } @Override public void syntaxError(String state, String event, List<String> legalEvents, String uri, Integer line) { throw new UnsupportedOperationException(); } @Override public void startOfScenarioLifeCycle(Scenario scenario) { // NoOp } @Override public void endOfScenarioLifeCycle(Scenario scenario) { // NoOp } private List<Map<String, Object>> getFeatureElements() { List<Map<String, Object>> featureElements = (List) featureMap.get("elements"); if (featureElements == null) { featureElements = new ArrayList<Map<String, Object>>(); featureMap.put("elements", featureElements); } return featureElements; } private Map<Object, List<Map>> getFeatureElement() { if (getFeatureElements().size() > 0) { return (Map) getFeatureElements().get(getFeatureElements().size() - 1); } else { return null; } } private List<Map> getAllExamples() { List<Map> allExamples = getFeatureElement().get("examples"); if (allExamples == null) { allExamples = new ArrayList<Map>(); getFeatureElement().put("examples", allExamples); } return allExamples; } private List<Map> getSteps() { List<Map> steps = getFeatureElement().get("steps"); if (steps == null) { steps = new ArrayList<Map>(); getFeatureElement().put("steps", steps); } return steps; } private List<Map<String, String>> getEmbeddings() { List<Map<String, String>> embeddings = (List<Map<String, String>>) getCurrentStep(Phase.embedding).get("embeddings"); if (embeddings == null) { embeddings = new ArrayList<Map<String, String>>(); getCurrentStep(Phase.embedding).put("embeddings", embeddings); } return embeddings; } private List<String> getOutput() { List<String> output = (List<String>) getCurrentStep(Phase.output).get("output"); if (output == null) { output = new ArrayList<String>(); getCurrentStep(Phase.output).put("output", output); } return output; } protected Gson gson() { return new GsonBuilder().setPrettyPrinting().create(); } }