##### Coverage Report - org.jbehave.core.reporters.PrintStreamOutput

Classes in this File | Line Coverage | Branch Coverage | Complexity
PrintStreamOutput |
93% | 145/155
80% | 42/52
| 1.75;1.75
PrintStreamOutput$1 |
100% | 6/6
80% | 4/5
| 1.75;1.75
PrintStreamOutput$2 |
100% | 1/1
N/A | N/A
| 1.75;1.75
PrintStreamOutput$Format |
100% | 2/2
N/A | N/A
| 1.75;1.75

1 |  |
     package org.jbehave.core.reporters;
2 |  |
3 |  |
     import java.io.ByteArrayOutputStream;
4 |  |
     import java.io.OutputStream;
5 |  |
     import java.io.PrintStream;
6 |  |
     import java.text.MessageFormat;
7 |  |
     import java.text.SimpleDateFormat;
8 |  |
     import java.util.Arrays;
9 |  |
     import java.util.Date;
10 |  |
     import java.util.List;
11 |  |
     import java.util.Locale;
12 |  |
     import java.util.Map;
13 |  |
     import java.util.Properties;
14 |  |
15 |  |
     import org.apache.commons.collections.CollectionUtils;
16 |  |
     import org.apache.commons.collections.Transformer;
17 |  |
     import org.apache.commons.lang.ArrayUtils;
18 |  |
     import org.apache.commons.lang.StringUtils;
19 |  |
     import org.apache.commons.lang.builder.ToStringBuilder;
20 |  |
     import org.apache.commons.lang.builder.ToStringStyle;
21 |  |
     import org.jbehave.core.configuration.Keywords;
22 |  |
     import org.jbehave.core.failures.KnownFailure;
23 |  |
     import org.jbehave.core.failures.UUIDExceptionWrapper;
24 |  |
     import org.jbehave.core.model.ExamplesTable;
25 |  |
     import org.jbehave.core.model.GivenStories;
26 |  |
     import org.jbehave.core.model.GivenStory;
27 |  |
     import org.jbehave.core.model.Meta;
28 |  |
     import org.jbehave.core.model.Narrative;
29 |  |
     import org.jbehave.core.model.OutcomesTable;
30 |  |
     import org.jbehave.core.model.OutcomesTable.Outcome;
31 |  |
     import org.jbehave.core.model.Scenario;
32 |  |
     import org.jbehave.core.model.Story;
33 |  |
     import org.jbehave.core.model.StoryDuration;
34 |  |
35 |  |
     import static org.apache.commons.lang.StringEscapeUtils.escapeHtml;
36 |  |
     import static org.apache.commons.lang.StringEscapeUtils.escapeXml;
37 |  |
     import static org.apache.commons.lang.StringUtils.substringBetween;
38 |  |
     import static org.jbehave.core.steps.StepCreator.PARAMETER_TABLE_END;
39 |  |
     import static org.jbehave.core.steps.StepCreator.PARAMETER_TABLE_START;
40 |  |
     import static org.jbehave.core.steps.StepCreator.PARAMETER_VALUE_END;
41 |  |
     import static org.jbehave.core.steps.StepCreator.PARAMETER_VALUE_NEWLINE;
42 |  |
     import static org.jbehave.core.steps.StepCreator.PARAMETER_VALUE_START;
43 |  |
44 |  |
     /**
45 |  |
      * <p>
46 |  |
      * Abstract story reporter that outputs to a PrintStream.
47 |  |
      * </p>
48 |  |
      * <p>
49 |  |
      * The output of the reported event is configurable via:
50 |  |
      * <ul>
51 |  |
      * <li>custom output patterns, providing only the patterns that differ from
52 |  |
      * default</li>
53 |  |
      * <li>keywords localised for different languages, providing the i18n Locale</li>
54 |  |
      * <li>flag to report failure trace</li>
55 |  |
      * </ul>
56 |  |
      * </p>
57 |  |
      * <p>
58 |  |
      * Let's look at example of providing custom output patterns, e.g. for the
59 |  |
      * failed event. <br/>
60 |  |
      * we'd need to provide the custom pattern, say we want to have something like
61 |  |
      * "(step being executed) <<< FAILED", keyed on the method name:
62 |  |
      *
63 |  |
      * <pre>
64 |  |
      * Properties patterns = new Properties();
65 |  |
      * patterns.setProperty(&quot;failed&quot;, &quot;{0} &lt;&lt;&lt; {1}&quot;);
66 |  |
      * </pre>
67 |  |
      *
68 |  |
      * The pattern is by default processed and formatted by the
69 |  |
      * {@link MessageFormat}. Both the
70 |  |
      * {@link #format(String key, String defaultPattern, Object... args)} and
71 |  |
      * {@link #lookupPattern(String key, String defaultPattern)} methods are
72 |  |
      * override-able and a different formatter or pattern lookup can be used by
73 |  |
      * subclasses.
74 |  |
      * </p>
75 |  |
      * <p>
76 |  |
      * If the keyword "FAILED" (or any other keyword used by the reporter) needs to
77 |  |
      * be expressed in a different language, all we need to do is to provide an
78 |  |
      * instance of {@link org.jbehave.core.i18n.LocalizedKeywords} using the
79 |  |
      * appropriate {@link Locale}, e.g.
80 |  |
      *
81 |  |
      * <pre>
82 |  |
      * Keywords keywords = new LocalizedKeywords(new Locale(&quot;it&quot;));
83 |  |
      * </pre>
84 |  |
      *
85 |  |
      * </p>
86 |  |
      */
87 |  |
     public abstract class PrintStreamOutput implements StoryReporter {
88 |  |
89 |  |
         private static final String EMPTY = "";
90 |  |
91 | 5 |
         public enum Format {
92 | 1 |
             TXT, HTML, XML
93 |  |
         }
94 |  |
95 |  |
         private final Format format;
96 |  |
         private final PrintStream output;
97 |  |
         private final Properties outputPatterns;
98 |  |
         private final Keywords keywords;
99 | 390 |
         private ThreadLocal<Boolean> reportFailureTrace = new ThreadLocal<Boolean>();
100 | 390 |
         private ThreadLocal<Boolean> compressFailureTrace = new ThreadLocal<Boolean>();
101 | 390 |
         private ThreadLocal<Throwable> cause = new ThreadLocal<Throwable>();
102 |  |
103 |  |
         protected PrintStreamOutput(Format format, PrintStream output, Properties outputPatterns, Keywords keywords,
104 | 390 |
                 boolean reportFailureTrace, boolean compressFailureTrace) {
105 | 390 |
             this.format = format;
106 | 390 |
             this.output = output;
107 | 390 |
             this.outputPatterns = outputPatterns;
108 | 390 |
             this.keywords = keywords;
109 | 390 |
             doReportFailureTrace(reportFailureTrace);
110 | 390 |
             doCompressFailureTrace(compressFailureTrace);
111 | 390 |
         }
112 |  |
113 |  |
         public void beforeStep(String step) {
114 | 3 |
         }
115 |  |
116 |  |
         public void successful(String step) {
117 | 140 |
             print(format("successful", "{0}\n", step));
118 | 140 |
         }
119 |  |
120 |  |
         public void ignorable(String step) {
121 | 12 |
             print(format("ignorable", "{0}\n", step));
122 | 12 |
         }
123 |  |
124 |  |
         public void pending(String step) {
125 | 28 |
             print(format("pending", "{0} ({1})\n", step, keywords.pending()));
126 | 28 |
         }
127 |  |
128 |  |
         public void notPerformed(String step) {
129 | 16 |
             print(format("notPerformed", "{0} ({1})\n", step, keywords.notPerformed()));
130 | 16 |
         }
131 |  |
132 |  |
         public void failed(String step, Throwable storyFailure) {
133 |  |
             // storyFailure be used if a subclass has rewritten the "failed" pattern
134 |  |
             // to have a {3} as WebDriverHtmlOutput (jbehave-web) does.
135 | 23 |
             if (storyFailure instanceof UUIDExceptionWrapper) {
136 | 23 |
                 this.cause.set(storyFailure.getCause());
137 | 23 |
                 print(format("failed", "{0} ({1})\n({2})\n", step, keywords.failed(), storyFailure.getCause(),
138 |  |
                         ((UUIDExceptionWrapper) storyFailure).getUUID()));
139 |  |
             } else {
140 | 0 |
                 throw new ClassCastException(storyFailure + " should be an instance of UUIDExceptionWrapper");
141 |  |
             }
142 | 23 |
         }
143 |  |
144 |  |
         public void failedOutcomes(String step, OutcomesTable table) {
145 | 13 |
             failed(step, table.failureCause());
146 | 13 |
             print(table);
147 | 13 |
         }
148 |  |
149 |  |
         private void print(OutcomesTable table) {
150 | 13 |
             print(format("outcomesTableStart", "\n"));
151 | 13 |
             List<Outcome<?>> rows = table.getOutcomes();
152 | 13 |
             print(format("outcomesTableHeadStart", "|"));
153 | 13 |
             for (String field : table.getOutcomeFields()) {
154 | 52 |
                 print(format("outcomesTableHeadCell", "{0}|", field));
155 |  |
             }
156 | 13 |
             print(format("outcomesTableHeadEnd", "\n"));
157 | 13 |
             print(format("outcomesTableBodyStart", EMPTY));
158 | 13 |
             for (Outcome<?> outcome : rows) {
159 | 25 |
                 print(format("outcomesTableRowStart", "|", outcome.isVerified() ? "verified" : "notVerified"));
160 | 25 |
                 print(format("outcomesTableCell", "{0}|", outcome.getDescription()));
161 | 25 |
                 print(format("outcomesTableCell", "{0}|", renderOutcomeValue(outcome.getValue(), table.getDateFormat())));
162 | 25 |
                 print(format("outcomesTableCell", "{0}|", outcome.getMatcher()));
163 | 25 |
                 print(format("outcomesTableCell", "{0}|", (outcome.isVerified() ? keywords.yes() : keywords.no())));
164 | 25 |
                 print(format("outcomesTableRowEnd", "\n"));
165 |  |
             }
166 | 13 |
             print(format("outcomesTableBodyEnd", "\n"));
167 | 13 |
             print(format("outcomesTableEnd", "\n"));
168 | 13 |
         }
169 |  |
170 |  |
         private Object renderOutcomeValue(Object value, String dateFormat) {
171 | 25 |
             if(value instanceof Date) {
172 | 13 |
                 return new SimpleDateFormat(dateFormat).format(value);
173 |  |
             } else {
174 | 12 |
                 return value;
175 |  |
             }
176 |  |
         }
177 |  |
178 |  |
         public void storyNotAllowed(Story story, String filter) {
179 | 0 |
             print(format("filter", "{0}\n", filter));
180 | 0 |
         }
181 |  |
182 |  |
         public void storyCancelled(Story story, StoryDuration storyDuration) {
183 | 15 |
             print(format("storyCancelled", "{0}: {1} ({2} s)\n", keywords.storyCancelled(), keywords.duration(),
184 |  |
                     storyDuration.getDurationInSecs()));
185 | 15 |
         }
186 |  |
187 |  |
         public void beforeStory(Story story, boolean givenStory) {
188 | 27 |
             print(format("beforeStory", "{0}\n({1})\n", story.getDescription().asString(), story.getPath()));
189 | 27 |
             if (!story.getMeta().isEmpty()) {
190 | 15 |
                 Meta meta = story.getMeta();
191 | 15 |
                 print(meta);
192 |  |
             }
193 | 27 |
         }
194 |  |
195 |  |
         public void narrative(Narrative narrative) {
196 | 18 |
             if (!narrative.isEmpty()) {
197 | 12 |
                 print(format("narrative", "{0}\n{1} {2}\n{3} {4}\n{5} {6}\n", keywords.narrative(), keywords.inOrderTo(),
198 |  |
                         narrative.inOrderTo(), keywords.asA(), narrative.asA(), keywords.iWantTo(), narrative.iWantTo()));
199 |  |
             }
200 | 18 |
         }
201 |  |
202 |  |
         private void print(Meta meta) {
203 | 15 |
             print(format("metaStart", "{0}\n", keywords.meta()));
204 | 15 |
             for (String name : meta.getPropertyNames()) {
205 | 30 |
                 print(format("metaProperty", "{0}{1} {2}", keywords.metaProperty(), name, meta.getProperty(name)));
206 |  |
             }
207 | 15 |
             print(format("metaEnd", "\n"));
208 | 15 |
         }
209 |  |
210 |  |
         public void afterStory(boolean givenStory) {
211 | 27 |
             print(format("afterStory", "\n"));
212 |  |
             // take care not to close System.out
213 |  |
             // which is used for ConsoleOutput
214 | 27 |
             if(!givenStory && output!=System.out) {
215 | 23 |
                 output.close();
216 |  |
             }
217 | 27 |
         }
218 |  |
219 |  |
         public void givenStories(GivenStories givenStories) {
220 | 12 |
             print(format("givenStoriesStart", "{0}\n", keywords.givenStories()));
221 | 12 |
             for (GivenStory givenStory : givenStories.getStories()) {
222 | 24 |
                 print(format("givenStory", "{0} {1}\n", givenStory.asString(),
223 |  |
                         (givenStory.hasAnchor() ? givenStory.getParameters() : "")));
224 |  |
             }
225 | 12 |
             print(format("givenStoriesEnd", "\n"));
226 | 12 |
         }
227 |  |
228 |  |
         public void givenStories(List<String> storyPaths) {
229 | 12 |
             givenStories(new GivenStories(StringUtils.join(storyPaths, ",")));
230 | 12 |
         }
231 |  |
232 |  |
         public void scenarioNotAllowed(Scenario scenario, String filter) {
233 | 3 |
             print(format("filter", "{0}\n", filter));
234 | 3 |
         }
235 |  |
236 |  |
         public void beforeScenario(String title) {
237 | 35 |
             cause.set(null);
238 | 35 |
             print(format("beforeScenario", "{0} {1}\n", keywords.scenario(), title));
239 | 35 |
         }
240 |  |
241 |  |
         public void scenarioMeta(Meta meta) {
242 | 6 |
             if (!meta.isEmpty()) {
243 | 0 |
                 print(meta);
244 |  |
             }
245 | 6 |
         }
246 |  |
247 |  |
         public void afterScenario() {
248 | 34 |
             if (cause.get() != null && reportFailureTrace.get() && !(cause.get() instanceof KnownFailure)) {
249 | 4 |
                 print(format("afterScenarioWithFailure", "\n{0}\n",
250 |  |
                         new StackTraceFormatter(compressFailureTrace()).stackTrace(cause.get())));
251 |  |
             } else {
252 | 30 |
                 print(format("afterScenario", "\n"));
253 |  |
             }
254 | 34 |
         }
255 |  |
256 |  |
         public void beforeExamples(List<String> steps, ExamplesTable table) {
257 | 12 |
             print(format("beforeExamples", "{0}\n", keywords.examplesTable()));
258 | 12 |
             for (String step : steps) {
259 | 24 |
                 print(format("examplesStep", "{0}\n", step));
260 |  |
             }
261 | 12 |
             print(formatTable(table));
262 | 12 |
         }
263 |  |
264 |  |
         public void example(Map<String, String> tableRow) {
265 | 24 |
             print(format("example", "\n{0} {1}\n", keywords.examplesTableRow(), tableRow));
266 | 24 |
         }
267 |  |
268 |  |
         public void afterExamples() {
269 | 12 |
             print(format("afterExamples", "\n"));
270 | 12 |
         }
271 |  |
272 |  |
         public void dryRun() {
273 | 12 |
             print(format("dryRun", "{0}\n", keywords.dryRun()));
274 | 12 |
         }
275 |  |
276 |  |
         public void pendingMethods(List<String> methods) {
277 | 12 |
             for (String method : methods) {
278 | 24 |
                 print(format("pendingMethod", "{0}\n", method));
279 |  |
             }
280 | 12 |
         }
281 |  |
282 |  |
         public void restarted(String step, Throwable cause) {
283 | 12 |
             print(format("restarted", "{0} {1}\n", step, cause.getMessage()));
284 | 12 |
         }
285 |  |
286 |  |
         /**
287 |  |
          * Formats event output by key, usually equal to the method name.
288 |  |
          *
289 |  |
          * @param key the event key
290 |  |
          * @param defaultPattern the default pattern to return if a custom pattern
291 |  |
          *            is not found
292 |  |
          * @param args the args used to format output
293 |  |
          * @return A formatted event output
294 |  |
          */
295 |  |
         protected String format(String key, String defaultPattern, Object... args) {
296 | 8216 |
             String escape = escape(defaultPattern);
297 | 8216 |
             String s = lookupPattern(key, escape);
298 | 8216 |
             Object[] objects = escapeAll(args);
299 | 8216 |
             return MessageFormat.format(s, objects);
300 |  |
         }
301 |  |
302 |  |
         protected String formatTable(ExamplesTable table) {
303 | 12 |
             OutputStream formatted = new ByteArrayOutputStream();
304 | 12 |
             PrintStream out = new PrintStream(formatted);
305 | 12 |
             out.print(format("examplesTableStart", "\n"));
306 | 12 |
             List<Map<String, String>> rows = table.getRows();
307 | 12 |
             List<String> headers = table.getHeaders();
308 | 12 |
             out.print(format("examplesTableHeadStart", "|"));
309 | 12 |
             for (String header : headers) {
310 | 24 |
                 out.print(format("examplesTableHeadCell", "{0}|", header));
311 |  |
             }
312 | 12 |
             out.print(format("examplesTableHeadEnd", "\n"));
313 | 12 |
             out.print(format("examplesTableBodyStart", EMPTY));
314 | 12 |
             for (Map<String, String> row : rows) {
315 | 24 |
                 out.print(format("examplesTableRowStart", "|"));
316 | 24 |
                 for (String header : headers) {
317 | 48 |
                     out.print(format("examplesTableCell", "{0}|", row.get(header)));
318 |  |
                 }
319 | 24 |
                 out.print(format("examplesTableRowEnd", "\n"));
320 |  |
             }
321 | 12 |
             out.print(format("examplesTableBodyEnd", ""));
322 | 12 |
             out.print(format("examplesTableEnd", ""));
323 | 12 |
             return formatted.toString();
324 |  |
         }
325 |  |
326 |  |
         private String escape(String defaultPattern) {
327 | 8216 |
             return (String) escapeAll(defaultPattern)[0];
328 |  |
         }
329 |  |
330 |  |
         private Object[] escapeAll(Object... args) {
331 | 16432 |
             return escape(format, args);
332 |  |
         }
333 |  |
334 |  |
         /**
335 |  |
          * Escapes args' string values according to format
336 |  |
          *
337 |  |
          * @param format the Format used by the PrintStream
338 |  |
          * @param args the array of args to escape
339 |  |
          * @return The cloned and escaped array of args
340 |  |
          */
341 |  |
         protected Object[] escape(final Format format, Object... args) {
342 |  |
             // Transformer that escapes HTML and XML strings
343 | 16432 |
             Transformer escapingTransformer = new Transformer() {
344 |  |
                 public Object transform(Object object) {
345 | 9374 |
                     switch (format) {
346 |  |
                     case HTML:
347 | 2957 |
                         return escapeHtml(asString(object));
348 |  |
                     case XML:
349 | 911 |
                         return escapeXml(asString(object));
350 |  |
                     default:
351 | 5506 |
                         return object;
352 |  |
                     }
353 |  |
                 }
354 |  |
355 |  |
                 private String asString(Object object) {
356 | 3868 |
                     return (object != null ? object.toString() : EMPTY);
357 |  |
                 }
358 |  |
             };
359 | 16432 |
             List<?> list = Arrays.asList(ArrayUtils.clone(args));
360 | 16432 |
             CollectionUtils.transform(list, escapingTransformer);
361 | 16432 |
             return list.toArray();
362 |  |
         }
363 |  |
364 |  |
         /**
365 |  |
          * Looks up the format pattern for the event output by key, conventionally
366 |  |
          * equal to the method name. The pattern is used by the
367 |  |
          * {#format(String,String,Object...)} method and by default is formatted
368 |  |
          * using the {@link MessageFormat#format(String, Object...)} method. If no
369 |  |
          * pattern is found for key or needs to be overridden, the default pattern
370 |  |
          * should be returned.
371 |  |
          *
372 |  |
          * @param key the format pattern key
373 |  |
          * @param defaultPattern the default pattern if no pattern is
374 |  |
          * @return The format patter for the given key
375 |  |
          */
376 |  |
         protected String lookupPattern(String key, String defaultPattern) {
377 | 8230 |
             if (outputPatterns.containsKey(key)) {
378 | 3741 |
                 return outputPatterns.getProperty(key);
379 |  |
             }
380 | 4489 |
             return defaultPattern;
381 |  |
         }
382 |  |
383 |  |
         public boolean reportFailureTrace() {
384 | 1 |
             return reportFailureTrace.get();
385 |  |
         }
386 |  |
387 |  |
         public PrintStreamOutput doReportFailureTrace(boolean reportFailureTrace) {
388 | 420 |
             this.reportFailureTrace.set(reportFailureTrace);
389 | 420 |
             return this;
390 |  |
         }
391 |  |
392 |  |
         public boolean compressFailureTrace() {
393 | 5 |
             return compressFailureTrace.get();
394 |  |
         }
395 |  |
396 |  |
         public PrintStreamOutput doCompressFailureTrace(boolean compressFailureTrace) {
397 | 420 |
             this.compressFailureTrace.set(compressFailureTrace);
398 | 420 |
             return this;
399 |  |
         }
400 |  |
401 |  |
         protected void overwritePattern(String key, String pattern) {
402 | 7 |
             outputPatterns.put(key, pattern);
403 | 7 |
         }
404 |  |
405 |  |
         /**
406 |  |
          * Prints text to output stream, replacing parameter start and end
407 |  |
          * placeholders
408 |  |
          *
409 |  |
          * @param text the String to print
410 |  |
          */
411 |  |
         protected void print(String text) {
412 | 892 |
             if (containsTable(text)) {
413 | 0 |
                 String tableStart = format(PARAMETER_TABLE_START, PARAMETER_TABLE_START);
414 | 0 |
                 String tableEnd = format(PARAMETER_TABLE_END, PARAMETER_TABLE_END);
415 | 0 |
                 String tableAsString = substringBetween(text, tableStart, tableEnd);
416 | 0 |
                 output.print(text
417 |  |
                         .replace(tableAsString, formatTable(new ExamplesTable(tableAsString)))
418 |  |
                         .replace(tableStart, format("parameterValueStart", EMPTY))
419 |  |
                         .replace(tableEnd, format("parameterValueEnd", EMPTY))
420 |  |
                         .replace(format(PARAMETER_VALUE_NEWLINE, PARAMETER_VALUE_NEWLINE),
421 |  |
                                 format("parameterValueNewline", "\n")));
422 | 0 |
             } else {
423 | 892 |
                 output.print(text
424 |  |
                         .replace(format(PARAMETER_VALUE_START, PARAMETER_VALUE_START), format("parameterValueStart", EMPTY))
425 |  |
                         .replace(format(PARAMETER_VALUE_END, PARAMETER_VALUE_END), format("parameterValueEnd", EMPTY))
426 |  |
                         .replace(format(PARAMETER_VALUE_NEWLINE, PARAMETER_VALUE_NEWLINE),
427 |  |
                                 format("parameterValueNewline", "\n")));
428 |  |
             }
429 | 892 |
         }
430 |  |
431 |  |
         private boolean containsTable(String text) {
432 | 892 |
             String tableStart = format(PARAMETER_TABLE_START, PARAMETER_TABLE_START);
433 | 892 |
             String tableEnd = format(PARAMETER_TABLE_END, PARAMETER_TABLE_END);
434 | 892 |
             return text.contains(tableStart) && text.contains(tableEnd);
435 |  |
         }
436 |  |
437 |  |
         @Override
438 |  |
         public String toString() {
439 | 0 |
             return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE).append(format).append(output).toString();
440 |  |
         }
441 |  |
442 |  |
     }