package org.jbehave.core.reporters; import static org.jbehave.core.steps.StepCreator.PARAMETER_TABLE_END; import static org.jbehave.core.steps.StepCreator.PARAMETER_TABLE_START; import static org.jbehave.core.steps.StepCreator.PARAMETER_VALUE_END; import static org.jbehave.core.steps.StepCreator.PARAMETER_VALUE_START; import java.io.File; import java.io.FileWriter; import java.io.Writer; import java.text.MessageFormat; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.lang3.StringEscapeUtils; import org.apache.commons.lang3.StringUtils; import org.jbehave.core.annotations.AfterScenario.Outcome; import org.jbehave.core.configuration.Keywords; import org.jbehave.core.embedder.MetaFilter; import org.jbehave.core.model.ExamplesTable; import org.jbehave.core.model.GivenStories; import org.jbehave.core.model.Lifecycle; import org.jbehave.core.model.Meta; import org.jbehave.core.model.Narrative; import org.jbehave.core.model.OutcomesTable; import org.jbehave.core.model.Scenario; import org.jbehave.core.model.Story; import org.jbehave.core.model.StoryDuration; import freemarker.ext.beans.BeansWrapper; import freemarker.template.TemplateHashModel; import freemarker.template.TemplateModelException; /** * <p> * Story reporter that outputs to a template. * </p> */ public class TemplateableOutput implements StoryReporter { private final File file; private final Keywords keywords; private final TemplateProcessor processor; private final String templatePath; private OutputStory outputStory = new OutputStory(); private OutputScenario outputScenario = new OutputScenario(); private OutputStep failedStep; public TemplateableOutput(File file, Keywords keywords, TemplateProcessor processor, String templatePath) { this.file = file; this.keywords = keywords; this.processor = processor; this.templatePath = templatePath; } @Override public void storyNotAllowed(Story story, String filter) { this.outputStory.notAllowedBy = filter; } @Override public void beforeStory(Story story, boolean givenStory) { if (!givenStory) { this.outputStory = new OutputStory(); this.outputStory.description = story.getDescription().asString(); this.outputStory.path = story.getPath(); } if (!story.getMeta().isEmpty()) { this.outputStory.meta = new OutputMeta(story.getMeta()); } } @Override public void narrative(Narrative narrative) { if (!narrative.isEmpty()) { this.outputStory.narrative = new OutputNarrative(narrative); } } @Override public void lifecyle(Lifecycle lifecycle) { if(!lifecycle.isEmpty()){ this.outputStory.lifecycle = new OutputLifecycle(lifecycle); } } @Override public void scenarioNotAllowed(Scenario scenario, String filter) { this.outputScenario.notAllowedBy = filter; } @Override public void beforeScenario(String title) { if (this.outputScenario.currentExample == null) { this.outputScenario = new OutputScenario(); } this.outputScenario.title = title; } @Override public void beforeStep(String step) { } @Override public void successful(String step) { this.outputScenario.addStep(new OutputStep(step, "successful")); } @Override public void ignorable(String step) { this.outputScenario.addStep(new OutputStep(step, "ignorable")); } @Override public void comment(String step) { this.outputScenario.addStep(new OutputStep(step, "comment")); } @Override public void pending(String step) { this.outputScenario.addStep(new OutputStep(step, "pending")); } @Override public void notPerformed(String step) { this.outputScenario.addStep(new OutputStep(step, "notPerformed")); } @Override public void failed(String step, Throwable storyFailure) { this.failedStep = new OutputStep(step, "failed"); failedStep.failure = storyFailure; this.outputScenario.addStep(failedStep); } @Override public void failedOutcomes(String step, OutcomesTable table) { failed(step, table.failureCause()); this.failedStep.outcomes = table; } @Override public void givenStories(GivenStories givenStories) { if (!givenStories.getStories().isEmpty()) { this.outputScenario.givenStories = givenStories; } } @Override public void givenStories(List<String> storyPaths) { givenStories(new GivenStories(StringUtils.join(storyPaths, ","))); } @Override public void scenarioMeta(Meta meta) { if (!meta.isEmpty()) { this.outputScenario.meta = new OutputMeta(meta); } } @Override public void beforeExamples(List<String> steps, ExamplesTable table) { this.outputScenario.examplesSteps = steps; this.outputScenario.examplesTable = table; } @Override public void example(Map<String, String> parameters) { this.outputScenario.examples.add(parameters); this.outputScenario.currentExample = parameters; } @Override public void afterExamples() { this.outputScenario.currentExample = null; } @Override public void dryRun() { } @Override public void afterScenario() { if (this.outputScenario.currentExample == null) { this.outputStory.scenarios.add(outputScenario); } } @Override public void pendingMethods(List<String> methods) { this.outputStory.pendingMethods = methods; } @Override public void restarted(String step, Throwable cause) { this.outputScenario.addStep(new OutputRestart(step, cause.getMessage())); } @Override public void restartedStory(Story story, Throwable cause) { this.outputScenario.addStep(new OutputRestart(story.getName(), cause.getMessage())); } @Override public void storyCancelled(Story story, StoryDuration storyDuration) { this.outputStory.cancelled = true; this.outputStory.storyDuration = storyDuration; } @Override public void afterStory(boolean givenStory) { if (!givenStory) { Map<String, Object> model = newDataModel(); model.put("story", outputStory); model.put("keywords", new OutputKeywords(keywords)); BeansWrapper wrapper = BeansWrapper.getDefaultInstance(); TemplateHashModel enumModels = wrapper.getEnumModels(); TemplateHashModel escapeEnums; try { String escapeModeEnum = EscapeMode.class.getCanonicalName(); escapeEnums = (TemplateHashModel) enumModels.get(escapeModeEnum); model.put("EscapeMode", escapeEnums); } catch (TemplateModelException e) { throw new IllegalArgumentException(e); } write(file, templatePath, model); } } private File write(File file, String resource, Map<String, Object> dataModel) { try { file.getParentFile().mkdirs(); Writer writer = new FileWriter(file); processor.process(resource, dataModel, writer); writer.close(); return file; } catch (Exception e) { throw new RuntimeException(resource, e); } } private Map<String, Object> newDataModel() { return new HashMap<String, Object>(); } public static class OutputKeywords { private final Keywords keywords; public OutputKeywords(Keywords keywords) { this.keywords = keywords; } public String getLifecycle(){ return keywords.lifecycle(); } public String getBefore(){ return keywords.before(); } public String getAfter(){ return keywords.after(); } public String getMeta() { return keywords.meta(); } public String getMetaProperty() { return keywords.metaProperty(); } public String getNarrative() { return keywords.narrative(); } public String getInOrderTo() { return keywords.inOrderTo(); } public String getAsA() { return keywords.asA(); } public String getiWantTo() { return keywords.iWantTo(); } public String getSoThat() { return keywords.soThat(); } public String getScenario() { return keywords.scenario(); } public String getGivenStories() { return keywords.givenStories(); } public String getExamplesTable() { return keywords.examplesTable(); } public String getExamplesTableRow() { return keywords.examplesTableRow(); } public String getExamplesTableHeaderSeparator() { return keywords.examplesTableHeaderSeparator(); } public String getExamplesTableValueSeparator() { return keywords.examplesTableValueSeparator(); } public String getExamplesTableIgnorableSeparator() { return keywords.examplesTableIgnorableSeparator(); } public String getGiven() { return keywords.given(); } public String getWhen() { return keywords.when(); } public String getThen() { return keywords.then(); } public String getAnd() { return keywords.and(); } public String getIgnorable() { return keywords.ignorable(); } public String getPending() { return keywords.pending(); } public String getNotPerformed() { return keywords.notPerformed(); } public String getFailed() { return keywords.failed(); } public String getDryRun() { return keywords.dryRun(); } public String getStoryCancelled() { return keywords.storyCancelled(); } public String getDuration() { return keywords.duration(); } public String getOutcome(){ return keywords.outcome(); } public String getMetaFilter(){ return keywords.metaFilter(); } public String getYes() { return keywords.yes(); } public String getNo() { return keywords.no(); } } public static class OutputStory { private String description; private String path; private OutputMeta meta; private OutputNarrative narrative; private OutputLifecycle lifecycle; private String notAllowedBy; private List<String> pendingMethods; private List<OutputScenario> scenarios = new ArrayList<OutputScenario>(); private boolean cancelled; private StoryDuration storyDuration; public String getDescription() { return description; } public String getPath() { return path; } public OutputMeta getMeta() { return meta; } public OutputNarrative getNarrative() { return narrative; } public OutputLifecycle getLifecycle() { return lifecycle; } public String getNotAllowedBy() { return notAllowedBy; } public List<String> getPendingMethods() { return pendingMethods; } public List<OutputScenario> getScenarios() { return scenarios; } public boolean isCancelled() { return cancelled; } public StoryDuration getStoryDuration() { return storyDuration; } } public static class OutputMeta { private final Meta meta; public OutputMeta(Meta meta) { this.meta = meta; } public Map<String, String> getProperties() { Map<String, String> properties = new HashMap<String, String>(); for (String name : meta.getPropertyNames()) { properties.put(name, meta.getProperty(name)); } return properties; } } public static class OutputNarrative { private final Narrative narrative; public OutputNarrative(Narrative narrative) { this.narrative = narrative; } public String getInOrderTo() { return narrative.inOrderTo(); } public String getAsA() { return narrative.asA(); } public String getiWantTo() { return narrative.iWantTo(); } public String getSoThat(){ return narrative.soThat(); } public boolean isAlternative(){ return narrative.isAlternative(); } } public static class OutputLifecycle { private final Lifecycle lifecycle; public OutputLifecycle(Lifecycle lifecycle) { this.lifecycle = lifecycle; } public List<String> getBeforeSteps(){ return lifecycle.getBeforeSteps(); } public List<String> getAfterSteps(){ return lifecycle.getAfterSteps(); } public Set<Outcome> getOutcomes(){ return lifecycle.getOutcomes(); } public MetaFilter getMetaFilter(Outcome outcome){ return lifecycle.getMetaFilter(outcome); } public List<String> getAfterSteps(Outcome outcome){ return lifecycle.getAfterSteps(outcome); } public List<String> getAfterSteps(Outcome outcome, Meta meta){ return lifecycle.getAfterSteps(outcome, meta); } } public static class OutputScenario { private String title; private List<OutputStep> steps = new ArrayList<OutputStep>(); private OutputMeta meta; private GivenStories givenStories; private String notAllowedBy; private List<String> examplesSteps; private ExamplesTable examplesTable; private Map<String, String> currentExample; private List<Map<String, String>> examples = new ArrayList<Map<String, String>>(); private Map<Map<String, String>, List<OutputStep>> stepsByExample = new HashMap<Map<String, String>, List<OutputStep>>(); public String getTitle() { return title; } public void addStep(OutputStep outputStep) { if (examplesTable == null) { steps.add(outputStep); } else { List<OutputStep> currentExampleSteps = stepsByExample.get(currentExample); if (currentExampleSteps == null) { currentExampleSteps = new ArrayList<OutputStep>(); stepsByExample.put(currentExample, currentExampleSteps); } currentExampleSteps.add(outputStep); } } public List<OutputStep> getSteps() { return steps; } public List<OutputStep> getStepsByExample(Map<String, String> example) { List<OutputStep> steps = stepsByExample.get(example); if (steps == null) { return new ArrayList<OutputStep>(); } return steps; } public OutputMeta getMeta() { return meta; } public GivenStories getGivenStories() { return givenStories; } public String getNotAllowedBy() { return notAllowedBy; } public List<String> getExamplesSteps() { return examplesSteps; } public ExamplesTable getExamplesTable() { return examplesTable; } public List<Map<String, String>> getExamples() { return examples; } } public static class OutputRestart extends OutputStep { public OutputRestart(String step, String outcome) { super(step, outcome); } } public static class OutputStep { private final String step; private final String outcome; private Throwable failure; private OutcomesTable outcomes; private List<OutputParameter> parameters; private String stepPattern; private String tableAsString; private ExamplesTable table; public OutputStep(String step, String outcome) { this.step = step; this.outcome = outcome; parseTableAsString(); parseParameters(); createStepPattern(); } public String getStep() { return step; } public String getStepPattern() { return stepPattern; } public List<OutputParameter> getParameters() { return parameters; } public String getOutcome() { return outcome; } public Throwable getFailure() { return failure; } public String getFailureCause() { if (failure != null) { return new StackTraceFormatter(true).stackTrace(failure); } return ""; } public ExamplesTable getTable() { return table; } public OutcomesTable getOutcomes() { return outcomes; } public String getOutcomesFailureCause() { if (outcomes.failureCause() != null) { return new StackTraceFormatter(true).stackTrace(outcomes.failureCause()); } return ""; } /* * formatting without escaping doesn't make sense unless * we do a ftl text output format */ @Deprecated public String getFormattedStep(String parameterPattern) { return getFormattedStep(EscapeMode.NONE, parameterPattern); } public String getFormattedStep(EscapeMode outputFormat, String parameterPattern) { // note that escaping the stepPattern string only works // because placeholders for parameters do not contain // special chars (the placeholder is {0} etc) String escapedStep = escapeString(outputFormat, stepPattern); if (!parameters.isEmpty()) { try { return MessageFormat.format(escapedStep, formatParameters(outputFormat, parameterPattern)); } catch (RuntimeException e) { throw new StepFormattingFailed(stepPattern, parameterPattern, parameters, e); } } return escapedStep; } private String escapeString(EscapeMode outputFormat, String string) { if(outputFormat==EscapeMode.HTML) { return StringEscapeUtils.escapeHtml4(string); } else if(outputFormat==EscapeMode.XML) { return StringEscapeUtils.escapeXml(string); } else { return string; } } private Object[] formatParameters(EscapeMode outputFormat, String parameterPattern) { Object[] arguments = new Object[parameters.size()]; for (int a = 0; a < parameters.size(); a++) { arguments[a] = MessageFormat.format(parameterPattern, escapeString(outputFormat, parameters.get(a).getValue())); } return arguments; } private void parseParameters() { // first, look for parameterized scenarios parameters = findParameters(PARAMETER_VALUE_START + PARAMETER_VALUE_START, PARAMETER_VALUE_END + PARAMETER_VALUE_END); // second, look for normal scenarios if (parameters.isEmpty()) { parameters = findParameters(PARAMETER_VALUE_START, PARAMETER_VALUE_END); } } private List<OutputParameter> findParameters(String start, String end) { List<OutputParameter> parameters = new ArrayList<OutputParameter>(); Matcher matcher = Pattern.compile("(" + start + ".*?" + end + ")(\\W|\\Z)", Pattern.DOTALL).matcher(step); while (matcher.find()) { parameters.add(new OutputParameter(step, matcher.start(), matcher.end())); } return parameters; } private void parseTableAsString() { if (step.contains(PARAMETER_TABLE_START) && step.contains(PARAMETER_TABLE_END)) { tableAsString = StringUtils.substringBetween(step, PARAMETER_TABLE_START, PARAMETER_TABLE_END); table = new ExamplesTable(tableAsString); } } private void createStepPattern() { this.stepPattern = step; if (tableAsString != null) { this.stepPattern = StringUtils.replaceOnce(stepPattern, PARAMETER_TABLE_START + tableAsString + PARAMETER_TABLE_END, ""); } for (int count = 0; count < parameters.size(); count++) { String value = parameters.get(count).toString(); this.stepPattern = stepPattern.replace(value, "{" + count + "}"); } } @SuppressWarnings("serial") public static class StepFormattingFailed extends RuntimeException { public StepFormattingFailed(String stepPattern, String parameterPattern, List<OutputParameter> parameters, RuntimeException cause) { super("Failed to format step '" + stepPattern + "' with parameter pattern '" + parameterPattern + "' and parameters: " + parameters, cause); } } } public static class OutputParameter { private final String parameter; public OutputParameter(String pattern, int start, int end) { this.parameter = pattern.substring(start, end).trim(); } public String getValue() { String value = StringUtils.remove(parameter, PARAMETER_VALUE_START); value = StringUtils.remove(value, PARAMETER_VALUE_END); return value; } @Override public String toString() { return parameter; } } }