PrintStreamOutput.java
package org.jbehave.core.reporters;
import static org.apache.commons.lang3.StringUtils.substringBetween;
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_NEWLINE;
import static org.jbehave.core.steps.StepCreator.PARAMETER_VALUE_START;
import static org.jbehave.core.steps.StepCreator.PARAMETER_VERBATIM_END;
import static org.jbehave.core.steps.StepCreator.PARAMETER_VERBATIM_START;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.stream.Stream;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.apache.commons.text.CaseUtils;
import org.jbehave.core.annotations.AfterScenario;
import org.jbehave.core.annotations.Scope;
import org.jbehave.core.configuration.Keywords;
import org.jbehave.core.embedder.MetaFilter;
import org.jbehave.core.failures.KnownFailure;
import org.jbehave.core.failures.UUIDExceptionWrapper;
import org.jbehave.core.model.ExamplesTable;
import org.jbehave.core.model.GivenStories;
import org.jbehave.core.model.GivenStory;
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.OutcomesTable.Outcome;
import org.jbehave.core.model.Scenario;
import org.jbehave.core.model.Step;
import org.jbehave.core.model.Story;
import org.jbehave.core.model.StoryDuration;
import org.jbehave.core.model.Verbatim;
import org.jbehave.core.steps.StepCollector.Stage;
import org.jbehave.core.steps.StepCreator.PendingStep;
import org.jbehave.core.steps.Timing;
/**
* <p>
* Abstract story reporter that outputs to a PrintStream.
* </p>
* <p>
* The output of the reported event is configurable via:
* <ul>
* <li>custom output patterns, providing only the patterns that differ from
* default</li>
* <li>keywords localised for different languages, providing the formatOutcome Locale</li>
* <li>flag to report failure trace</li>
* </ul>
* </p>
* <p>
* Let's look at example of providing custom output patterns, e.g. for the
* failed event. <br/>
* we'd need to provide the custom pattern, say we want to have something like
* "(step being executed) <<< FAILED", keyed on the method name:
*
* <pre>
* Properties patterns = new Properties();
* patterns.setProperty("failed", "{0} <<< {1}");
* </pre>
*
* The pattern is by default processed and formatted by the
* {@link MessageFormat}. Both the
* {@link #format(String key, String defaultPattern, Object... args)} and
* {@link #lookupPattern(String key, String defaultPattern)} methods are
* override-able and a different formatter or pattern lookup can be used by
* subclasses.
* </p>
* <p>
* If the keyword "FAILED" (or any other keyword used by the reporter) needs to
* be expressed in a different language, all we need to do is to provide an
* instance of {@link org.jbehave.core.i18n.LocalizedKeywords} using the
* appropriate {@link Locale}, e.g.
*
* <pre>
* Keywords keywords = new LocalizedKeywords(new Locale("it"));
* </pre>
*
* </p>
*/
public abstract class PrintStreamOutput extends NullStoryReporter {
private static final String EMPTY = "";
public static final String NL = "\n";
public enum Format {
TXT {
@Override
public Object escapeValue(Object object) {
return object;
}
},
HTML {
@Override
public Object escapeValue(Object object) {
return EscapeMode.HTML.escapeString(asString(object));
}
},
XML {
@Override
public Object escapeValue(Object object) {
return EscapeMode.XML.escapeString(asString(object));
}
},
JSON {
@Override
public Object escapeValue(Object object) {
return EscapeMode.JSON.escapeString(asString(object));
}
};
public abstract Object escapeValue(Object object);
private static String asString(Object object) {
return object != null ? object.toString() : EMPTY;
}
}
private final Format format;
private final PrintStream output;
private final Properties outputPatterns;
private final Keywords keywords;
private ThreadLocal<Boolean> reportFailureTrace = new ThreadLocal<>();
private ThreadLocal<Boolean> compressFailureTrace = new ThreadLocal<>();
private ThreadLocal<Throwable> cause = new ThreadLocal<>();
private ThreadLocal<Boolean> dryRun = ThreadLocal.withInitial(() -> false);
protected PrintStreamOutput(Format format, PrintStream output, Properties defaultPatterns,
Properties outputPatterns, Keywords keywords) {
this(format, output, mergePatterns(defaultPatterns, outputPatterns), keywords, false,
false);
}
protected PrintStreamOutput(Format format, PrintStream output, Properties defaultPatterns,
Properties outputPatterns, Keywords keywords, boolean reportFailureTrace, boolean compressFailureTrace) {
this(format, output, mergePatterns(defaultPatterns, outputPatterns), keywords, reportFailureTrace,
compressFailureTrace);
}
protected PrintStreamOutput(Format format, PrintStream output, Properties outputPatterns, Keywords keywords,
boolean reportFailureTrace, boolean compressFailureTrace) {
this.format = format;
this.output = output;
this.outputPatterns = outputPatterns;
this.keywords = keywords;
doReportFailureTrace(reportFailureTrace);
doCompressFailureTrace(compressFailureTrace);
}
private static Properties mergePatterns(Properties defaultPatterns, Properties outputPatterns) {
Properties patterns = new Properties();
patterns.putAll(defaultPatterns);
// override any default pattern
patterns.putAll(outputPatterns);
return patterns;
}
@Override
public void beforeStep(Step step) {
print(format("beforeStep", "{0}\n", step.getStepAsString()));
}
@Override
public void successful(String step) {
print(format("successful", "{0}\n", step));
}
@Override
public void ignorable(String step) {
print(format("ignorable", "{0}\n", step));
}
@Override
public void comment(String step) {
print(format("comment", "{0}\n", step));
}
@Override
public void pending(PendingStep step) {
print(format("pending", "{0} ({1})\n({2})\n", step.stepAsString(), keywords.pending(),
step.getPendingMethod()));
}
@Override
public void notPerformed(String step) {
print(format("notPerformed", "{0} ({1})\n", step, keywords.notPerformed()));
}
@Override
public void failed(String step, Throwable storyFailure) {
// storyFailure be used if a subclass has rewritten the "failed" pattern
// to have a {3} as WebDriverHtmlOutput (jbehave-web) does.
if (storyFailure instanceof UUIDExceptionWrapper) {
this.cause.set(storyFailure.getCause());
print(format("failed", "{0} ({1})\n({2})\n", step, keywords.failed(), storyFailure.getCause(),
((UUIDExceptionWrapper) storyFailure).getUUID()));
} else {
throw new ClassCastException(storyFailure + " should be an instance of UUIDExceptionWrapper");
}
}
@Override
public void failedOutcomes(String step, OutcomesTable table) {
failed(step, table.failureCause());
printOutcomesTable(table);
}
private void printOutcomesTable(OutcomesTable table) {
print(format("outcomesTableStart", NL));
List<Outcome<?>> rows = table.getOutcomes();
print(format("outcomesTableHeadStart", "|"));
for (String field : table.getOutcomeFields()) {
print(format("outcomesTableHeadCell", "{0}|", field));
}
print(format("outcomesTableHeadEnd", NL));
print(format("outcomesTableBodyStart", EMPTY));
for (Outcome<?> outcome : rows) {
print(format("outcomesTableRowStart", "|", outcome.isVerified() ? "verified" : "notVerified"));
print(format("outcomesTableCell", "{0}|", outcome.getDescription()));
print(format("outcomesTableCell", "{0}|", renderOutcomeValue(outcome.getValue(), table)));
print(format("outcomesTableCell", "{0}|", outcome.getMatcher()));
print(format("outcomesTableCell", "{0}|", (outcome.isVerified() ? keywords.yes() : keywords.no())));
print(format("outcomesTableRowEnd", NL));
}
print(format("outcomesTableBodyEnd", NL));
print(format("outcomesTableEnd", NL));
}
private Object renderOutcomeValue(Object value, OutcomesTable outcomesTable) {
if (value instanceof Date) {
return new SimpleDateFormat(outcomesTable.getFormat(Date.class)).format(value);
} else {
return value;
}
}
@Override
public void storyExcluded(Story story, String filter) {
print(format("filter", "{0}\n", filter));
}
@Override
public void storyCancelled(Story story, StoryDuration storyDuration) {
print(format("storyCancelled", "{0}: {1} ({2} s)\n", keywords.storyCancelled(), keywords.duration(),
storyDuration.getDurationInSecs()));
}
@Override
public void beforeStory(Story story, boolean givenStory) {
print(format("beforeStory", "{0}\n{1}\n({2})\n", story.getId(), story.getDescription().asString(),
story.getPath()));
if (dryRun.get()) {
print(format("dryRun", "{0}\n", keywords.dryRun()));
}
printMeta(story.getMeta());
}
@Override
public void beforeScenarios() {
print(format("beforeScenarios", ""));
}
@Override
public void afterScenarios() {
print(format("afterScenarios", ""));
}
@Override
public void narrative(Narrative narrative) {
if (!narrative.isEmpty()) {
if (!narrative.isAlternative()) {
print(format("narrative", "{0}\n{1} {2}\n{3} {4}\n{5} {6}\n", keywords.narrative(),
keywords.inOrderTo(), narrative.inOrderTo(), keywords.asA(), narrative.asA(),
keywords.iWantTo(), narrative.iWantTo()));
} else {
print(format("narrative", "{0}\n{1} {2}\n{3} {4}\n{5} {6}\n", keywords.narrative(), keywords.asA(),
narrative.asA(), keywords.iWantTo(), narrative.iWantTo(), keywords.soThat(),
narrative.soThat()));
}
}
}
@Override
public void lifecycle(Lifecycle lifecycle) {
if (!lifecycle.isEmpty()) {
print(format("lifecycleStart", "{0}\n", keywords.lifecycle()));
ExamplesTable lifecycleExamplesTable = lifecycle.getExamplesTable();
if (!lifecycleExamplesTable.isEmpty()) {
print(formatTable(lifecycleExamplesTable));
}
if (lifecycle.hasBeforeSteps()) {
print(format("lifecycleBeforeStart", "{0}\n", keywords.before()));
for (Scope scope : lifecycle.getScopes()) {
printWithScope(lifecycle.getBeforeSteps(scope), scope);
}
print(format("lifecycleBeforeEnd", NL));
}
if (lifecycle.hasAfterSteps()) {
print(format("lifecycleAfterStart", "{0}\n", keywords.after()));
for (Scope scope : lifecycle.getScopes()) {
printOutcomes(lifecycle, scope);
}
print(format("lifecycleAfterEnd", NL));
}
print(format("lifecycleEnd", NL));
}
}
private void printOutcomes(Lifecycle lifecycle, Scope scope) {
for (AfterScenario.Outcome outcome : lifecycle.getOutcomes()) {
List<String> afterSteps = lifecycle.getAfterSteps(scope, outcome);
if (!afterSteps.isEmpty()) {
print(format("lifecycleAfterScopeStart", "{0} {1}\n", keywords.scope(), formatScope(scope)));
print(format("lifecycleOutcomeStart", "{0} {1}\n", keywords.outcome(), formatOutcome(outcome)));
MetaFilter metaFilter = lifecycle.getMetaFilter(outcome);
if (!metaFilter.isEmpty()) {
print(format("lifecycleMetaFilter", "{0} {1}\n", keywords.metaFilter(), metaFilter.asString()));
}
printLifecycleSteps(afterSteps);
print(format("lifecycleOutcomeEnd", "\n"));
print(format("lifecycleAfterScopeEnd", "\n"));
}
}
}
private void printWithScope(List<String> steps, Scope scope) {
if (!steps.isEmpty()) {
print(format("lifecycleBeforeScopeStart", "{0} {1}\n", keywords.scope(), formatScope(scope)));
printLifecycleSteps(steps);
print(format("lifecycleBeforeScopeEnd", "\n"));
}
}
private String formatScope(Scope scope) {
switch (scope) {
case SCENARIO: return keywords.scopeScenario();
case STORY: return keywords.scopeStory();
default: return scope.name();
}
}
private String formatOutcome(AfterScenario.Outcome outcome) {
switch (outcome) {
case ANY:
return keywords.outcomeAny();
case SUCCESS:
return keywords.outcomeSuccess();
case FAILURE:
return keywords.outcomeFailure();
default:
return outcome.name();
}
}
private void printLifecycleSteps(List<String> steps) {
for (String step : steps) {
print(format("lifecycleStep", "{0}\n", step));
}
}
private void printMeta(Meta meta) {
if (!meta.isEmpty()) {
print(format("metaStart", "{0}\n", keywords.meta()));
for (String name : meta.getPropertyNames()) {
print(format("metaProperty", "{0}{1} {2}", keywords.metaProperty(), name, meta.getProperty(name)));
}
print(format("metaEnd", NL));
}
}
@Override
public void beforeScenarioSteps(Stage stage, Lifecycle.ExecutionType type) {
printScenarioSteps("before", stage, type);
}
@Override
public void afterScenarioSteps(Stage stage, Lifecycle.ExecutionType type) {
printScenarioSteps("after", stage, type);
}
private void printScenarioSteps(String stepsStage, Stage stage, Lifecycle.ExecutionType type) {
printSteps(stepsStage, "Scenario", stage, type);
}
@Override
public void beforeComposedSteps() {
print(format("beforeComposedSteps", ""));
}
@Override
public void afterComposedSteps() {
print(format("afterComposedSteps", ""));
}
@Override
public void beforeStoriesSteps(Stage stage) {
printStoriesSteps("before", stage);
}
@Override
public void afterStoriesSteps(Stage stage) {
printStoriesSteps("after", stage);
}
private void printStoriesSteps(String stepsStage, Stage stage) {
printSteps(stepsStage, "Stories", stage, null);
}
@Override
public void beforeStorySteps(Stage stage, Lifecycle.ExecutionType type) {
printStorySteps("before", stage, type);
}
@Override
public void afterStorySteps(Stage stage, Lifecycle.ExecutionType type) {
printStorySteps("after", stage, type);
}
private void printStorySteps(String stepsStage, Stage stage, Lifecycle.ExecutionType type) {
printSteps(stepsStage, "Story", stage, type);
}
private void printSteps(String stepsStage, String parent, Stage stage, Lifecycle.ExecutionType type) {
String stageName = stage != null ? CaseUtils.toCamelCase(stage.name(), true) : EMPTY;
String typeName = type != null ? CaseUtils.toCamelCase(type.name(), true) : EMPTY;
print(format(stepsStage + stageName + typeName + parent + "Steps", ""));
}
@Override
public void afterStory(boolean givenOrRestartingStory) {
print(format("afterStory", NL));
// take care not to close System.out
// which is used for ConsoleOutput
if (!givenOrRestartingStory && output != System.out) {
output.close();
}
}
@Override
public void beforeGivenStories() {
print(format("beforeGivenStories", ""));
}
@Override
public void givenStories(GivenStories givenStories) {
print(format("givenStoriesStart", "{0}\n", keywords.givenStories()));
for (GivenStory givenStory : givenStories.getStories()) {
print(format("givenStory", "{0}{1}\n", givenStory.asString(),
(givenStory.hasAnchor() ? givenStory.getParameters() : "")));
}
print(format("givenStoriesEnd", NL));
}
@Override
public void givenStories(List<String> storyPaths) {
givenStories(new GivenStories(StringUtils.join(storyPaths, ",")));
}
@Override
public void afterGivenStories() {
print(format("afterGivenStories", ""));
}
@Override
public void scenarioExcluded(Scenario scenario, String filter) {
print(format("filter", "{0}\n", filter));
}
@Override
public void beforeScenario(Scenario scenario) {
cause.set(null);
print(format("beforeScenario", "{0} {1} {2}\n", scenario.getId(), keywords.scenario(), scenario.getTitle()));
printMeta(scenario.getMeta());
}
@Override
public void afterScenario(Timing timing) {
print(format("numericParameter", EMPTY, "start", timing.getStart()));
print(format("numericParameter", EMPTY, "end", timing.getEnd()));
if (cause.get() != null && !(cause.get() instanceof KnownFailure) && reportFailureTrace()) {
print(format("afterScenarioWithFailure", "\n{0}\n",
new StackTraceFormatter(compressFailureTrace()).stackTrace(cause.get())));
} else {
print(format("afterScenario", NL));
}
}
@Override
public void beforeExamples(List<String> steps, ExamplesTable table) {
print(format("beforeExamples", "{0}\n", keywords.examplesTable()));
print(format("examplesStepsStart", EMPTY));
for (String step : steps) {
print(format("examplesStep", "{0}\n", step));
}
print(format("examplesStepsEnd", EMPTY));
print(formatTable(table));
print(format("exampleScenariosStart", EMPTY));
}
@Override
public void example(Map<String, String> tableRow, int exampleIndex) {
print(format("example", "\n{0} {1}\n", keywords.examplesTableRow(), tableRow));
print(output, format("beforeExampleParameters", EMPTY));
tableRow.entrySet().forEach(
cell -> print(output, format("exampleParameter", EMPTY, cell.getKey(), cell.getValue())));
print(output, format("afterExampleParameters", EMPTY));
}
@Override
public void afterExamples() {
print(format("exampleScenariosEnd", EMPTY));
print(format("afterExamples", NL));
}
@Override
public void dryRun() {
dryRun.set(true);
}
@Override
public void restarted(String step, Throwable cause) {
print(format("restarted", "{0} {1}\n", step, cause.getMessage()));
}
@Override
public void restartedStory(Story story, Throwable cause) {
print(format("restartedStory", "{0} {1}\n", story.getPath(), cause.getMessage()));
}
/**
* Formats event output by key, usually equal to the method name.
*
* @param key the event key
* @param defaultPattern the default pattern to return if a custom pattern
* is not found
* @param args the args used to format output
* @return A formatted event output
*/
protected String format(String key, String defaultPattern, Object... args) {
String escape = escape(defaultPattern);
String s = lookupPattern(key, escape);
Object[] objects = escapeAll(args);
return MessageFormat.format(s, objects);
}
protected String formatTable(ExamplesTable table) {
OutputStream formatted = new ByteArrayOutputStream();
PrintStream out = new PrintStream(formatted);
print(out, format("examplesTableStart", NL));
List<Map<String, String>> rows = table.getRows();
List<String> headers = table.getHeaders();
print(out, format("examplesTableHeadStart", "|"));
for (String header : headers) {
print(out, format("examplesTableHeadCell", "{0}|", header));
}
print(out, format("examplesTableHeadEnd", NL));
print(out, format("examplesTableBodyStart", EMPTY));
for (Map<String, String> row : rows) {
print(out, format("examplesTableRowStart", "|"));
for (String header : headers) {
print(out, format("examplesTableCell", "{0}|", row.get(header)));
}
print(out, format("examplesTableRowEnd", NL));
}
print(out, format("examplesTableBodyEnd", EMPTY));
print(out, format("examplesTableEnd", EMPTY));
return formatted.toString();
}
protected String formatVerbatim(Verbatim verbatim) {
OutputStream formatted = new ByteArrayOutputStream();
PrintStream out = new PrintStream(formatted);
print(out, format("verbatimStart", NL));
print(out, verbatim.getContent());
print(out, format("verbatimEnd", NL));
return formatted.toString();
}
private Object[] escapeAll(Object... args) {
return escape(format, args);
}
private String escape(String defaultPattern) {
return (String) escapeAll(defaultPattern)[0];
}
/**
* Escapes args' string values according to format
*
* @param format the Format used by the PrintStream
* @param args the array of args to escape
* @return The cloned and escaped array of args
*/
protected Object[] escape(final Format format, Object... args) {
// Transformer that escapes HTML,XML,JSON strings
return Stream.of(args).map(format::escapeValue).toArray();
}
/**
* Looks up the format pattern for the event output by key, conventionally
* equal to the method name. The pattern is used by the
* {#format(String,String,Object...)} method and by default is formatted
* using the {@link MessageFormat#format(String, Object...)} method. If no
* pattern is found for key or needs to be overridden, the default pattern
* should be returned.
*
* @param key the format pattern key
* @param defaultPattern the default pattern if no pattern is
* @return The format patter for the given key
*/
protected String lookupPattern(String key, String defaultPattern) {
if (outputPatterns.containsKey(key)) {
return outputPatterns.getProperty(key);
}
return defaultPattern;
}
public boolean reportFailureTrace() {
Boolean reportFailure = reportFailureTrace.get();
if (reportFailure != null) {
return reportFailure;
}
return false;
}
public PrintStreamOutput doReportFailureTrace(boolean reportFailureTrace) {
this.reportFailureTrace.set(reportFailureTrace);
return this;
}
public boolean compressFailureTrace() {
return compressFailureTrace.get();
}
public PrintStreamOutput doCompressFailureTrace(boolean compressFailureTrace) {
this.compressFailureTrace.set(compressFailureTrace);
return this;
}
protected void overwritePattern(String key, String pattern) {
outputPatterns.put(key, pattern);
}
/**
* Prints text to output stream, replacing parameter start and end
* placeholders
*
* @param text the String to print
*/
protected void print(String text) {
String tableStart = format(PARAMETER_TABLE_START, PARAMETER_TABLE_START);
String tableEnd = format(PARAMETER_TABLE_END, PARAMETER_TABLE_END);
boolean containsTable = text.contains(tableStart) && text.contains(tableEnd);
String verbatimStart = format(PARAMETER_VERBATIM_START, PARAMETER_VERBATIM_START);
String verbatimEnd = format(PARAMETER_VERBATIM_END, PARAMETER_VERBATIM_END);
boolean containsVerbatim = text.contains(verbatimStart) && text.contains(verbatimEnd);
String textToPrint;
if (containsTable) {
textToPrint = transformPrintingTable(text, tableStart, tableEnd);
} else if (containsVerbatim) {
textToPrint = transformPrintingVerbatim(text, verbatimStart, verbatimEnd);
} else {
textToPrint = text;
}
print(output, textToPrint
.replace(format(PARAMETER_VALUE_START, PARAMETER_VALUE_START),
format("parameterValueStart", EMPTY))
.replace(format(PARAMETER_VALUE_END, PARAMETER_VALUE_END),
format("parameterValueEnd", EMPTY))
.replace(format(PARAMETER_VALUE_NEWLINE, PARAMETER_VALUE_NEWLINE),
format("parameterValueNewline", NL)));
}
protected void print(PrintStream output, String text) {
output.print(text);
}
protected String transformPrintingTable(String text, String tableStart, String tableEnd) {
String tableAsString = substringBetween(text, tableStart, tableEnd);
return text
.replace(tableAsString, formatTable(new ExamplesTable(tableAsString)))
.replace(tableStart, format("parameterValueStart", EMPTY))
.replace(tableEnd, format("parameterValueEnd", EMPTY));
}
protected String transformPrintingVerbatim(String text, String verbatimStart, String verbatimEnd) {
String verbatimAsString = substringBetween(text, verbatimStart, verbatimEnd);
return text
.replace(verbatimAsString, formatVerbatim(new Verbatim(verbatimAsString)))
.replace(verbatimStart, format("parameterValueStart", EMPTY))
.replace(verbatimEnd, format("parameterValueEnd", EMPTY));
}
@Override
public String toString() {
return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE).append(format).append(output).toString();
}
}