TemplateableViewGenerator.java

  1. package org.jbehave.core.reporters;

  2. import static java.util.Arrays.asList;

  3. import java.io.File;
  4. import java.io.FileInputStream;
  5. import java.io.FileReader;
  6. import java.io.FilenameFilter;
  7. import java.io.IOException;
  8. import java.io.InputStream;
  9. import java.io.Writer;
  10. import java.nio.charset.Charset;
  11. import java.nio.charset.StandardCharsets;
  12. import java.nio.file.Files;
  13. import java.util.ArrayList;
  14. import java.util.Collection;
  15. import java.util.Collections;
  16. import java.util.Date;
  17. import java.util.Enumeration;
  18. import java.util.Formatter;
  19. import java.util.HashMap;
  20. import java.util.List;
  21. import java.util.Map;
  22. import java.util.Properties;
  23. import java.util.SortedMap;
  24. import java.util.TreeMap;

  25. import org.apache.commons.io.FileUtils;
  26. import org.apache.commons.io.FilenameUtils;
  27. import org.apache.commons.io.IOUtils;
  28. import org.apache.commons.lang3.builder.ToStringBuilder;
  29. import org.apache.commons.lang3.builder.ToStringStyle;
  30. import org.jbehave.core.io.StoryNameResolver;
  31. import org.jbehave.core.model.StoryLanes;
  32. import org.jbehave.core.model.StoryMaps;
  33. import org.jbehave.core.reporters.TemplateableViewGenerator.Reports.ViewType;

  34. /**
  35.  * {@link ViewGenerator}, which uses the configured {@link TemplateProcessor} to generate the views from templates. The
  36.  * default view properties are overridable via the method {@link Properties} parameter. To override, specify the path to
  37.  * the new template under the appropriate key:
  38.  * <pre>
  39.  * &quot;views&quot;: the path to global view template, including reports and maps views
  40.  * &quot;maps&quot;: the path to the maps view template
  41.  * &quot;reports&quot;: the path to the reports view template
  42.  * &quot;decorated&quot;: the path to the template to generate a decorated (i.e. styled) single report
  43.  * &quot;nonDecorated&quot;: the path to the template to generated a non decorated single report
  44.  * </pre>
  45.  *
  46.  * <p>The view generator provides the following resources:
  47.  * <pre>
  48.  * &quot;decorateNonHtml&quot; = &quot;true&quot;
  49.  * &quot;defaultFormats&quot; = &quot;stats&quot;
  50.  * &quot;viewDirectory&quot; = &quot;view&quot;
  51.  * </pre>
  52.  * </p>
  53.  *
  54.  * @author Mauro Talevi
  55.  */
  56. public class TemplateableViewGenerator implements ViewGenerator {

  57.     private final StoryNameResolver nameResolver;
  58.     private final TemplateProcessor processor;
  59.     private final Charset charset;
  60.     private Properties viewProperties;
  61.     private Reports reports;

  62.     public TemplateableViewGenerator(StoryNameResolver nameResolver, TemplateProcessor processor) {
  63.         this(nameResolver, processor, StandardCharsets.ISO_8859_1);
  64.     }

  65.     public TemplateableViewGenerator(StoryNameResolver nameResolver, TemplateProcessor processor, Charset charset) {
  66.         this.nameResolver = nameResolver;
  67.         this.processor = processor;
  68.         this.charset = charset;
  69.     }

  70.     @Override
  71.     public Properties defaultViewProperties() {
  72.         Properties properties = new Properties();
  73.         properties.setProperty("encoding", charset.displayName());
  74.         properties.setProperty("decorateNonHtml", "true");
  75.         properties.setProperty("defaultFormats", "stats");
  76.         properties.setProperty("version", jbehaveVersion());
  77.         properties.setProperty("reportsViewType", Reports.ViewType.LIST.name());
  78.         properties.setProperty("viewDirectory", "view");
  79.         return properties;
  80.     }

  81.     private String jbehaveVersion() {
  82.         try {
  83.             return IOUtils.resourceToString("jbehave.version", charset, getClass().getClassLoader());
  84.         } catch (IOException e) {
  85.             throw new RuntimeException("Failed to read JBehave version", e);
  86.         }
  87.     }

  88.     private Properties mergeWithDefault(Properties properties) {
  89.         Properties merged = defaultViewProperties();
  90.         merged.putAll(properties);
  91.         return merged;
  92.     }

  93.     private void addViewProperties(Map<String, Object> dataModel) {
  94.         dataModel.put("date", new Date());
  95.         dataModel.put("encoding", this.viewProperties.getProperty("encoding"));
  96.         dataModel.put("version", this.viewProperties.getProperty("version"));
  97.     }

  98.     @Override
  99.     public void generateMapsView(File outputDirectory, StoryMaps storyMaps, Properties viewProperties) {
  100.         this.viewProperties = mergeWithDefault(viewProperties);
  101.         String outputName = templateResource("viewDirectory") + "/maps.html";
  102.         String mapsTemplate = templateResource("maps");
  103.         Map<String, Object> dataModel = newDataModel();
  104.         addViewProperties(dataModel);
  105.         dataModel.put("storyLanes", new StoryLanes(storyMaps, nameResolver));
  106.         write(outputDirectory, outputName, mapsTemplate, dataModel);
  107.         generateViewsIndex(outputDirectory);
  108.     }

  109.     @Override
  110.     public void generateReportsView(File outputDirectory, List<String> formats, Properties viewProperties) {
  111.         this.viewProperties = mergeWithDefault(viewProperties);
  112.         String outputName = templateResource("viewDirectory") + "/reports.html";
  113.         String reportsTemplate = templateResource("reports");
  114.         List<String> mergedFormats = mergeFormatsWithDefaults(formats);
  115.         reports = createReports(readReportFiles(outputDirectory, outputName, mergedFormats));
  116.         reports.viewAs(ViewType.valueOf(viewProperties.getProperty("reportsViewType", Reports.ViewType.LIST.name())));
  117.         Map<String, Object> dataModel = newDataModel();
  118.         addViewProperties(dataModel);
  119.         dataModel.put("timeFormatter", new TimeFormatter());
  120.         dataModel.put("reports", reports);
  121.         dataModel.put("storyDurations", storyDurations(outputDirectory));
  122.         write(outputDirectory, outputName, reportsTemplate, dataModel);
  123.         generateViewsIndex(outputDirectory);
  124.     }

  125.     private Map<String,Long> storyDurations(File outputDirectory) {
  126.         Properties p = new Properties();
  127.         try {
  128.             p.load(new FileReader(new File(outputDirectory, "storyDurations.props")));
  129.         } catch (IOException e) {
  130.             // story durations file not found - carry on
  131.         }
  132.         Map<String,Long> durations = new HashMap<>();
  133.         for (Object key : p.keySet()) {
  134.             durations.put(toReportPath(key), toMillis(p.get(key)));
  135.         }
  136.         return durations;
  137.     }

  138.     private long toMillis(Object value) {
  139.         return Long.parseLong((String)value);
  140.     }

  141.     private String toReportPath(Object key) {
  142.         return FilenameUtils.getBaseName(((String)key).replace("/", "."));
  143.     }

  144.     private void generateViewsIndex(File outputDirectory) {
  145.         String outputName = templateResource("viewDirectory") + "/index.html";
  146.         String viewsTemplate = templateResource("views");
  147.         Map<String, Object> dataModel = newDataModel();
  148.         addViewProperties(dataModel);
  149.         write(outputDirectory, outputName, viewsTemplate, dataModel);
  150.     }

  151.     @Override
  152.     public ReportsCount getReportsCount() {
  153.         int stories = countStoriesWithScenarios();
  154.         int storiesExcluded = count("excluded", reports);
  155.         int storiesPending = count("pending", reports);
  156.         int scenarios = count("scenarios", reports);
  157.         int scenariosFailed = count("scenariosFailed", reports);
  158.         int scenariosExcluded = count("scenariosExcluded", reports);
  159.         int scenariosPending = count("scenariosPending", reports);
  160.         int stepsFailed = count("stepsFailed", reports);
  161.         return new ReportsCount(stories, storiesExcluded, storiesPending, scenarios, scenariosFailed,
  162.                 scenariosExcluded, scenariosPending, stepsFailed);
  163.     }

  164.     private int countStoriesWithScenarios() {
  165.         int storyCount = 0;
  166.         for (Report report : reports.getReports()) {
  167.             Map<String, Integer> stats = report.getStats();
  168.             if (stats.containsKey("scenarios")) {
  169.                 if (stats.get("scenarios") > 0) {
  170.                     storyCount++;
  171.                 }
  172.             }
  173.         }
  174.         return storyCount;
  175.     }
  176.    
  177.     int count(String event, Reports reports) {
  178.         int count = 0;
  179.         for (Report report : reports.getReports()) {
  180.             Properties stats = report.asProperties("stats");
  181.             if (stats.containsKey(event)) {
  182.                 count = count + Integer.parseInt((String) stats.get(event));
  183.             }
  184.         }
  185.         return count;
  186.     }

  187.     private List<String> mergeFormatsWithDefaults(List<String> formats) {
  188.         List<String> merged = new ArrayList<>();
  189.         merged.addAll(asList(templateResource("defaultFormats").split(",")));
  190.         merged.addAll(formats);
  191.         return merged;
  192.     }

  193.     Reports createReports(Map<String, List<File>> reportFiles) {
  194.         try {
  195.             String decoratedTemplate = templateResource("decorated");
  196.             String nonDecoratedTemplate = templateResource("nonDecorated");
  197.             String viewDirectory = templateResource("viewDirectory");
  198.             boolean decorateNonHtml = Boolean.valueOf(templateResource("decorateNonHtml"));
  199.             List<Report> reports = new ArrayList<>();
  200.             for (String name : reportFiles.keySet()) {
  201.                 Map<String, File> filesByFormat = new HashMap<>();
  202.                 for (File file : reportFiles.get(name)) {
  203.                     String fileName = file.getName();
  204.                     String format = FilenameUtils.getExtension(fileName);
  205.                     Map<String, Object> dataModel = newDataModel();
  206.                     dataModel.put("name", name);
  207.                     dataModel.put("body", FileUtils.readFileToString(file, charset));
  208.                     dataModel.put("format", format);
  209.                     File outputDirectory = file.getParentFile();
  210.                     String outputName = viewDirectory + "/" + fileName;
  211.                     String template = decoratedTemplate;
  212.                     if (!format.equals("html")) {
  213.                         if (decorateNonHtml) {
  214.                             outputName = outputName + ".html";
  215.                         } else {
  216.                             template = nonDecoratedTemplate;
  217.                         }
  218.                     }
  219.                     File written = write(outputDirectory, outputName, template, dataModel);
  220.                     filesByFormat.put(format, written);
  221.                 }
  222.                 reports.add(new Report(name, filesByFormat));
  223.             }
  224.             return new Reports(reports, nameResolver);
  225.         } catch (Exception e) {
  226.             throw new ReportCreationFailed(reportFiles, e);
  227.         }
  228.     }

  229.     SortedMap<String, List<File>> readReportFiles(File outputDirectory, final String outputName,
  230.             final List<String> formats) {
  231.         SortedMap<String, List<File>> reportFiles = new TreeMap<>();
  232.         if (outputDirectory == null || !outputDirectory.exists()) {
  233.             return reportFiles;
  234.         }
  235.         String[] fileNames = outputDirectory.list(new FilenameFilter() {
  236.             @Override
  237.             public boolean accept(File dir, String name) {
  238.                 return !name.equals(outputName) && hasFormats(name, formats);
  239.             }

  240.             private boolean hasFormats(String name, List<String> formats) {
  241.                 for (String format : formats) {
  242.                     if (name.endsWith(format)) {
  243.                         return true;
  244.                     }
  245.                 }
  246.                 return false;
  247.             }
  248.         });
  249.         for (String fileName : fileNames) {
  250.             String name = FilenameUtils.getBaseName(fileName);
  251.             List<File> filesByName = reportFiles.get(name);
  252.             if (filesByName == null) {
  253.                 filesByName = new ArrayList<>();
  254.                 reportFiles.put(name, filesByName);
  255.             }
  256.             filesByName.add(new File(outputDirectory, fileName));
  257.         }
  258.         return reportFiles;
  259.     }

  260.     private File write(File outputDirectory, String outputName, String resource, Map<String, Object> dataModel) {
  261.         try {
  262.             File file = new File(outputDirectory, outputName);
  263.             file.getParentFile().mkdirs();
  264.             try (Writer writer = Files.newBufferedWriter(file.toPath(), charset)) {
  265.                 processor.process(resource, dataModel, writer);
  266.             }
  267.             return file;
  268.         } catch (Exception e) {
  269.             throw new ViewGenerationFailedForTemplate(resource, e);
  270.         }
  271.     }

  272.     private String templateResource(String format) {
  273.         return viewProperties.getProperty(format);
  274.     }

  275.     private Map<String, Object> newDataModel() {
  276.         return new HashMap<>();
  277.     }

  278.     @SuppressWarnings("serial")
  279.     public static class ReportCreationFailed extends RuntimeException {

  280.         public ReportCreationFailed(Map<String, List<File>> reportFiles, Exception cause) {
  281.             super("Report creation failed from file " + reportFiles, cause);
  282.         }
  283.     }

  284.     @SuppressWarnings("serial")
  285.     public static class ViewGenerationFailedForTemplate extends RuntimeException {

  286.         public ViewGenerationFailedForTemplate(String resource, Exception cause) {
  287.             super(resource, cause);
  288.         }

  289.     }

  290.     public static class Reports {
  291.         public enum ViewType {
  292.             LIST
  293.         }

  294.         private final Map<String, Report> reports = new HashMap<>();
  295.         private final StoryNameResolver nameResolver;
  296.         private ViewType viewType = ViewType.LIST;

  297.         public Reports(List<Report> reports, StoryNameResolver nameResolver) {
  298.             this.nameResolver = nameResolver;
  299.             index(reports);
  300.             addTotalsReport();
  301.         }
  302.        
  303.         public ViewType getViewType() {
  304.             return viewType;
  305.         }
  306.        
  307.         public void viewAs(ViewType viewType) {
  308.             this.viewType = viewType;
  309.         }
  310.        
  311.         public List<Report> getReports() {
  312.             List<Report> list = new ArrayList<>(reports.values());
  313.             Collections.sort(list);
  314.             return list;
  315.         }

  316.         public List<String> getReportNames() {
  317.             List<String> list = new ArrayList<>(reports.keySet());
  318.             Collections.sort(list);
  319.             return list;
  320.         }

  321.         public Report getReport(String name) {
  322.             return reports.get(name);
  323.         }

  324.         private void index(List<Report> reports) {
  325.             for (Report report : reports) {
  326.                 report.nameAs(nameResolver.resolveName(report.getPath()));
  327.                 this.reports.put(report.getName(), report);
  328.             }
  329.         }

  330.         private void addTotalsReport() {
  331.             Report report = totals(reports.values());
  332.             report.nameAs(nameResolver.resolveName(report.getPath()));
  333.             reports.put(report.getName(), report);
  334.         }

  335.         private Report totals(Collection<Report> values) {
  336.             Map<String, Integer> totals = new HashMap<>();
  337.             for (Report report : values) {
  338.                 Map<String, Integer> stats = report.getStats();
  339.                 for (String key : stats.keySet()) {
  340.                     Integer total = totals.get(key);
  341.                     if (total == null) {
  342.                         total = 0;
  343.                     }
  344.                     total = total + stats.get(key);
  345.                     totals.put(key, total);
  346.                 }
  347.             }
  348.             return new Report("Totals", new HashMap<String, File>(), totals);
  349.         }

  350.     }

  351.     public static class Report implements Comparable<Report> {

  352.         private final String path;
  353.         private final Map<String, File> filesByFormat;
  354.         private Map<String, Integer> stats;
  355.         private String name;

  356.         public Report(String path, Map<String, File> filesByFormat) {
  357.             this(path, filesByFormat, null);
  358.         }

  359.         public Report(String path, Map<String, File> filesByFormat, Map<String, Integer> stats) {
  360.             this.path = path;
  361.             this.filesByFormat = filesByFormat;
  362.             this.stats = stats;
  363.         }

  364.         public String getPath() {
  365.             return path;
  366.         }

  367.         public String getName() {
  368.             return name != null ? name : path;
  369.         }

  370.         public void nameAs(String name) {
  371.             this.name = name;
  372.         }

  373.         public Map<String, File> getFilesByFormat() {
  374.             return filesByFormat;
  375.         }

  376.         public Properties asProperties(String format) {
  377.             Properties p = new Properties();
  378.             File stats = filesByFormat.get(format);
  379.             try {
  380.                 InputStream inputStream = new FileInputStream(stats);
  381.                 p.load(inputStream);
  382.                 inputStream.close();
  383.             } catch (Exception e) {
  384.                 // return empty map
  385.             }
  386.             return p;
  387.         }

  388.         public Map<String, Integer> getStats() {
  389.             if (stats == null) {
  390.                 Properties p = asProperties("stats");
  391.                 stats = new HashMap<>();
  392.                 for (Enumeration<?> e = p.propertyNames(); e.hasMoreElements();) {
  393.                     String key = (String) e.nextElement();
  394.                     stats.put(key, valueOf(key, p));
  395.                 }
  396.             }
  397.             return stats;
  398.         }

  399.         private Integer valueOf(String key, Properties p) {
  400.             try {
  401.                 return Integer.valueOf(p.getProperty(key));
  402.             } catch (NumberFormatException e) {
  403.                 return 0;
  404.             }
  405.         }

  406.         @Override
  407.         public int compareTo(Report that) {
  408.             return this.getName().compareTo(that.getName());
  409.         }

  410.         @Override
  411.         public String toString() {
  412.             return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE).append(path).toString();
  413.         }
  414.     }

  415.     public static class TimeFormatter {

  416.         public String formatMillis(long millis) {
  417.             int second = 1000;
  418.             int minute = 60 * second;
  419.             int hour = 60 * minute;
  420.             long hours = millis / hour;
  421.             long minutes = (millis % hour) / minute;
  422.             long seconds = ((millis % hour) % minute) / second;
  423.             long milliseconds = ((millis % hour) % minute % second);
  424.             Formatter formatter = new Formatter();
  425.             String result = formatter.format("%02d:%02d:%02d.%03d", hours, minutes, seconds, milliseconds).toString();
  426.             formatter.close();
  427.             return result;
  428.         }

  429.     }
  430. }