TemplateableViewGenerator.java
package org.jbehave.core.reporters;
import static java.util.Arrays.asList;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.Writer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.Formatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.SortedMap;
import java.util.TreeMap;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.jbehave.core.io.StoryNameResolver;
import org.jbehave.core.model.StoryLanes;
import org.jbehave.core.model.StoryMaps;
import org.jbehave.core.reporters.TemplateableViewGenerator.Reports.ViewType;
/**
* {@link ViewGenerator}, which uses the configured {@link TemplateProcessor} to generate the views from templates. The
* default view properties are overridable via the method {@link Properties} parameter. To override, specify the path to
* the new template under the appropriate key:
* <pre>
* "views": the path to global view template, including reports and maps views
* "maps": the path to the maps view template
* "reports": the path to the reports view template
* "decorated": the path to the template to generate a decorated (i.e. styled) single report
* "nonDecorated": the path to the template to generated a non decorated single report
* </pre>
*
* <p>The view generator provides the following resources:
* <pre>
* "decorateNonHtml" = "true"
* "defaultFormats" = "stats"
* "viewDirectory" = "view"
* </pre>
* </p>
*
* @author Mauro Talevi
*/
public class TemplateableViewGenerator implements ViewGenerator {
private final StoryNameResolver nameResolver;
private final TemplateProcessor processor;
private final Charset charset;
private Properties viewProperties;
private Reports reports;
public TemplateableViewGenerator(StoryNameResolver nameResolver, TemplateProcessor processor) {
this(nameResolver, processor, StandardCharsets.ISO_8859_1);
}
public TemplateableViewGenerator(StoryNameResolver nameResolver, TemplateProcessor processor, Charset charset) {
this.nameResolver = nameResolver;
this.processor = processor;
this.charset = charset;
}
@Override
public Properties defaultViewProperties() {
Properties properties = new Properties();
properties.setProperty("encoding", charset.displayName());
properties.setProperty("decorateNonHtml", "true");
properties.setProperty("defaultFormats", "stats");
properties.setProperty("version", jbehaveVersion());
properties.setProperty("reportsViewType", Reports.ViewType.LIST.name());
properties.setProperty("viewDirectory", "view");
return properties;
}
private String jbehaveVersion() {
try {
return IOUtils.resourceToString("jbehave.version", charset, getClass().getClassLoader());
} catch (IOException e) {
throw new RuntimeException("Failed to read JBehave version", e);
}
}
private Properties mergeWithDefault(Properties properties) {
Properties merged = defaultViewProperties();
merged.putAll(properties);
return merged;
}
private void addViewProperties(Map<String, Object> dataModel) {
dataModel.put("date", new Date());
dataModel.put("encoding", this.viewProperties.getProperty("encoding"));
dataModel.put("version", this.viewProperties.getProperty("version"));
}
@Override
public void generateMapsView(File outputDirectory, StoryMaps storyMaps, Properties viewProperties) {
this.viewProperties = mergeWithDefault(viewProperties);
String outputName = templateResource("viewDirectory") + "/maps.html";
String mapsTemplate = templateResource("maps");
Map<String, Object> dataModel = newDataModel();
addViewProperties(dataModel);
dataModel.put("storyLanes", new StoryLanes(storyMaps, nameResolver));
write(outputDirectory, outputName, mapsTemplate, dataModel);
generateViewsIndex(outputDirectory);
}
@Override
public void generateReportsView(File outputDirectory, List<String> formats, Properties viewProperties) {
this.viewProperties = mergeWithDefault(viewProperties);
String outputName = templateResource("viewDirectory") + "/reports.html";
String reportsTemplate = templateResource("reports");
List<String> mergedFormats = mergeFormatsWithDefaults(formats);
reports = createReports(readReportFiles(outputDirectory, outputName, mergedFormats));
reports.viewAs(ViewType.valueOf(viewProperties.getProperty("reportsViewType", Reports.ViewType.LIST.name())));
Map<String, Object> dataModel = newDataModel();
addViewProperties(dataModel);
dataModel.put("timeFormatter", new TimeFormatter());
dataModel.put("reports", reports);
dataModel.put("storyDurations", storyDurations(outputDirectory));
write(outputDirectory, outputName, reportsTemplate, dataModel);
generateViewsIndex(outputDirectory);
}
private Map<String,Long> storyDurations(File outputDirectory) {
Properties p = new Properties();
try {
p.load(new FileReader(new File(outputDirectory, "storyDurations.props")));
} catch (IOException e) {
// story durations file not found - carry on
}
Map<String,Long> durations = new HashMap<>();
for (Object key : p.keySet()) {
durations.put(toReportPath(key), toMillis(p.get(key)));
}
return durations;
}
private long toMillis(Object value) {
return Long.parseLong((String)value);
}
private String toReportPath(Object key) {
return FilenameUtils.getBaseName(((String)key).replace("/", "."));
}
private void generateViewsIndex(File outputDirectory) {
String outputName = templateResource("viewDirectory") + "/index.html";
String viewsTemplate = templateResource("views");
Map<String, Object> dataModel = newDataModel();
addViewProperties(dataModel);
write(outputDirectory, outputName, viewsTemplate, dataModel);
}
@Override
public ReportsCount getReportsCount() {
int stories = countStoriesWithScenarios();
int storiesExcluded = count("excluded", reports);
int storiesPending = count("pending", reports);
int scenarios = count("scenarios", reports);
int scenariosFailed = count("scenariosFailed", reports);
int scenariosExcluded = count("scenariosExcluded", reports);
int scenariosPending = count("scenariosPending", reports);
int stepsFailed = count("stepsFailed", reports);
return new ReportsCount(stories, storiesExcluded, storiesPending, scenarios, scenariosFailed,
scenariosExcluded, scenariosPending, stepsFailed);
}
private int countStoriesWithScenarios() {
int storyCount = 0;
for (Report report : reports.getReports()) {
Map<String, Integer> stats = report.getStats();
if (stats.containsKey("scenarios")) {
if (stats.get("scenarios") > 0) {
storyCount++;
}
}
}
return storyCount;
}
int count(String event, Reports reports) {
int count = 0;
for (Report report : reports.getReports()) {
Properties stats = report.asProperties("stats");
if (stats.containsKey(event)) {
count = count + Integer.parseInt((String) stats.get(event));
}
}
return count;
}
private List<String> mergeFormatsWithDefaults(List<String> formats) {
List<String> merged = new ArrayList<>();
merged.addAll(asList(templateResource("defaultFormats").split(",")));
merged.addAll(formats);
return merged;
}
Reports createReports(Map<String, List<File>> reportFiles) {
try {
String decoratedTemplate = templateResource("decorated");
String nonDecoratedTemplate = templateResource("nonDecorated");
String viewDirectory = templateResource("viewDirectory");
boolean decorateNonHtml = Boolean.valueOf(templateResource("decorateNonHtml"));
List<Report> reports = new ArrayList<>();
for (String name : reportFiles.keySet()) {
Map<String, File> filesByFormat = new HashMap<>();
for (File file : reportFiles.get(name)) {
String fileName = file.getName();
String format = FilenameUtils.getExtension(fileName);
Map<String, Object> dataModel = newDataModel();
dataModel.put("name", name);
dataModel.put("body", FileUtils.readFileToString(file, charset));
dataModel.put("format", format);
File outputDirectory = file.getParentFile();
String outputName = viewDirectory + "/" + fileName;
String template = decoratedTemplate;
if (!format.equals("html")) {
if (decorateNonHtml) {
outputName = outputName + ".html";
} else {
template = nonDecoratedTemplate;
}
}
File written = write(outputDirectory, outputName, template, dataModel);
filesByFormat.put(format, written);
}
reports.add(new Report(name, filesByFormat));
}
return new Reports(reports, nameResolver);
} catch (Exception e) {
throw new ReportCreationFailed(reportFiles, e);
}
}
SortedMap<String, List<File>> readReportFiles(File outputDirectory, final String outputName,
final List<String> formats) {
SortedMap<String, List<File>> reportFiles = new TreeMap<>();
if (outputDirectory == null || !outputDirectory.exists()) {
return reportFiles;
}
String[] fileNames = outputDirectory.list(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return !name.equals(outputName) && hasFormats(name, formats);
}
private boolean hasFormats(String name, List<String> formats) {
for (String format : formats) {
if (name.endsWith(format)) {
return true;
}
}
return false;
}
});
for (String fileName : fileNames) {
String name = FilenameUtils.getBaseName(fileName);
List<File> filesByName = reportFiles.get(name);
if (filesByName == null) {
filesByName = new ArrayList<>();
reportFiles.put(name, filesByName);
}
filesByName.add(new File(outputDirectory, fileName));
}
return reportFiles;
}
private File write(File outputDirectory, String outputName, String resource, Map<String, Object> dataModel) {
try {
File file = new File(outputDirectory, outputName);
file.getParentFile().mkdirs();
try (Writer writer = Files.newBufferedWriter(file.toPath(), charset)) {
processor.process(resource, dataModel, writer);
}
return file;
} catch (Exception e) {
throw new ViewGenerationFailedForTemplate(resource, e);
}
}
private String templateResource(String format) {
return viewProperties.getProperty(format);
}
private Map<String, Object> newDataModel() {
return new HashMap<>();
}
@SuppressWarnings("serial")
public static class ReportCreationFailed extends RuntimeException {
public ReportCreationFailed(Map<String, List<File>> reportFiles, Exception cause) {
super("Report creation failed from file " + reportFiles, cause);
}
}
@SuppressWarnings("serial")
public static class ViewGenerationFailedForTemplate extends RuntimeException {
public ViewGenerationFailedForTemplate(String resource, Exception cause) {
super(resource, cause);
}
}
public static class Reports {
public enum ViewType {
LIST
}
private final Map<String, Report> reports = new HashMap<>();
private final StoryNameResolver nameResolver;
private ViewType viewType = ViewType.LIST;
public Reports(List<Report> reports, StoryNameResolver nameResolver) {
this.nameResolver = nameResolver;
index(reports);
addTotalsReport();
}
public ViewType getViewType() {
return viewType;
}
public void viewAs(ViewType viewType) {
this.viewType = viewType;
}
public List<Report> getReports() {
List<Report> list = new ArrayList<>(reports.values());
Collections.sort(list);
return list;
}
public List<String> getReportNames() {
List<String> list = new ArrayList<>(reports.keySet());
Collections.sort(list);
return list;
}
public Report getReport(String name) {
return reports.get(name);
}
private void index(List<Report> reports) {
for (Report report : reports) {
report.nameAs(nameResolver.resolveName(report.getPath()));
this.reports.put(report.getName(), report);
}
}
private void addTotalsReport() {
Report report = totals(reports.values());
report.nameAs(nameResolver.resolveName(report.getPath()));
reports.put(report.getName(), report);
}
private Report totals(Collection<Report> values) {
Map<String, Integer> totals = new HashMap<>();
for (Report report : values) {
Map<String, Integer> stats = report.getStats();
for (String key : stats.keySet()) {
Integer total = totals.get(key);
if (total == null) {
total = 0;
}
total = total + stats.get(key);
totals.put(key, total);
}
}
return new Report("Totals", new HashMap<String, File>(), totals);
}
}
public static class Report implements Comparable<Report> {
private final String path;
private final Map<String, File> filesByFormat;
private Map<String, Integer> stats;
private String name;
public Report(String path, Map<String, File> filesByFormat) {
this(path, filesByFormat, null);
}
public Report(String path, Map<String, File> filesByFormat, Map<String, Integer> stats) {
this.path = path;
this.filesByFormat = filesByFormat;
this.stats = stats;
}
public String getPath() {
return path;
}
public String getName() {
return name != null ? name : path;
}
public void nameAs(String name) {
this.name = name;
}
public Map<String, File> getFilesByFormat() {
return filesByFormat;
}
public Properties asProperties(String format) {
Properties p = new Properties();
File stats = filesByFormat.get(format);
try {
InputStream inputStream = new FileInputStream(stats);
p.load(inputStream);
inputStream.close();
} catch (Exception e) {
// return empty map
}
return p;
}
public Map<String, Integer> getStats() {
if (stats == null) {
Properties p = asProperties("stats");
stats = new HashMap<>();
for (Enumeration<?> e = p.propertyNames(); e.hasMoreElements();) {
String key = (String) e.nextElement();
stats.put(key, valueOf(key, p));
}
}
return stats;
}
private Integer valueOf(String key, Properties p) {
try {
return Integer.valueOf(p.getProperty(key));
} catch (NumberFormatException e) {
return 0;
}
}
@Override
public int compareTo(Report that) {
return this.getName().compareTo(that.getName());
}
@Override
public String toString() {
return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE).append(path).toString();
}
}
public static class TimeFormatter {
public String formatMillis(long millis) {
int second = 1000;
int minute = 60 * second;
int hour = 60 * minute;
long hours = millis / hour;
long minutes = (millis % hour) / minute;
long seconds = ((millis % hour) % minute) / second;
long milliseconds = ((millis % hour) % minute % second);
Formatter formatter = new Formatter();
String result = formatter.format("%02d:%02d:%02d.%03d", hours, minutes, seconds, milliseconds).toString();
formatter.close();
return result;
}
}
}