/*
* -----------------------------------------------------------------------\
* PerfCake
*
* Copyright (C) 2010 - 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* -----------------------------------------------------------------------/
*/
package org.perfcake.reporting.destination.c3chart;
import org.perfcake.PerfCakeException;
import org.perfcake.reporting.destination.ChartDestination;
import org.perfcake.util.Utils;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
/**
* Renders HTML and JavaScript templates to the final chart report.
*
* @author <a href="mailto:marvenec@gmail.com">Martin Večeřa</a>
*/
public class C3ChartHtmlTemplates {
/**
* Gets JavaScript import template.
*
* @param location
* JavaScript file location.
* @return JavaScript import template.
* @throws PerfCakeException
* When it was not possible to read or parse the template.
*/
private static String getJsImport(final String location) throws PerfCakeException {
final Properties props = new Properties();
props.setProperty("location", location);
return Utils.readTemplateFromResource("/c3chart/js-import.html", props);
}
/**
* Gets a chart div element.
*
* @param baseName
* Base name of the chart.
* @return The chart div element.
* @throws PerfCakeException
* When it was not possible to read or parse the template.
*/
private static String getChartDiv(final String baseName) throws PerfCakeException {
final Properties props = new Properties();
props.setProperty("baseName", baseName);
return Utils.readTemplateFromResource("/c3chart/chart-div.html", props);
}
/**
* Gets a heading element.
*
* @param level
* Heading level.
* @param id
* Anchor id.
* @param heading
* Heding label.
* @return The heading element.
* @throws PerfCakeException
* When it was not possible to read or parse the template.
*/
private static String getHeading(final int level, final String id, final String heading) throws PerfCakeException {
final Properties props = new Properties();
props.setProperty("level", String.valueOf(level));
props.setProperty("id", id);
props.setProperty("heading", heading);
return Utils.readTemplateFromResource("/c3chart/heading.html", props);
}
/**
* Gets an entry of the table of content.
*
* @param id
* Anchor id.
* @param label
* Link label.
* @return The entry of the table of content.
* @throws PerfCakeException
* When it was not possible to read or parse the template.
*/
private static String getTocEntry(final String id, final String label) throws PerfCakeException {
final Properties props = new Properties();
props.setProperty("id", id);
props.setProperty("label", label);
return Utils.readTemplateFromResource("/c3chart/toc-entry.html", props);
}
/**
* Gets a complete table of content.
*
* @param headings
* Map of anchor ids and heading labels.
* @return The complete table of content.
* @throws PerfCakeException
* When it was not possible to read or parse the template.
*/
private static String getToc(final Map<String, String> headings) throws PerfCakeException {
final StringBuilder leftEntries = new StringBuilder();
final StringBuilder rightEntries = new StringBuilder();
final int half = headings.size() / 2 + headings.size() % 2;
int counter = 0;
for (final Map.Entry<String, String> entry : headings.entrySet()) {
if (counter < half) {
leftEntries.append(getTocEntry(entry.getKey(), entry.getValue()));
} else {
rightEntries.append(getTocEntry(entry.getKey(), entry.getValue()));
}
counter++;
}
final Properties props = new Properties();
props.setProperty("leftEntries", leftEntries.toString());
props.setProperty("rightEntries", rightEntries.toString());
return Utils.readTemplateFromResource("/c3chart/toc.html", props);
}
/**
* Writes the main index file with the complete report.
*
* @param target
* Root path to an existing chart report.
* @param charts
* List fo charts to be put in the report.
* @throws PerfCakeException
* WHen it was not possible to read or parse the index file template, or when it was not possible to write the index file.
*/
static void writeIndex(final Path target, final List<C3Chart> charts) throws PerfCakeException {
final Path indexFile = Paths.get(target.toString(), "index.html");
final Properties indexProps = new Properties();
indexProps.setProperty("js", getChartsImports(charts)); // javascript imports
indexProps.setProperty("loader", getChartsScript(charts)); // javascript to render charts
indexProps.setProperty("chart", getChartsDiv(charts)); // html content
Utils.copyTemplateFromResource("/c3chart/index.html", indexFile, indexProps);
}
/**
* Gets the JavaScript for all the charts in the given list.
*
* @param charts
* The list of charts.
* @return The JavaScript for all the charts in the given list.
* @throws PerfCakeException
* When it was not possible to read or parse the template.
*/
private static String getChartsScript(final List<C3Chart> charts) throws PerfCakeException {
final StringBuilder sb = new StringBuilder();
for (final C3Chart chart : charts) {
sb.append(getSingleChartScript(chart));
}
return sb.toString();
}
/**
* Gets the JavaScript for the given chart meta-data.
*
* @param chart
* Chart meta-data.
* @return The JavaScript for the given chart meta-data.
* @throws PerfCakeException
* When it was not possible to read or parse the template.
*/
private static String getSingleChartScript(final C3Chart chart) throws PerfCakeException {
final Properties props = getChartProperties(chart);
return Utils.readTemplateFromResource("/c3chart/chart.js", props);
}
/**
* Gets all the chart data imports.
*
* @param charts
* List of charts to be imported.
* @return All the chart data imports.
* @throws PerfCakeException
* When it was not possible to read or parse the template.
*/
private static String getChartsImports(final List<C3Chart> charts) throws PerfCakeException {
final StringBuilder sb = new StringBuilder();
for (final C3Chart chart : charts) {
sb.append(getJsImport("data/" + chart.getBaseName() + ".js"));
}
return sb.toString();
}
/**
* Gets the piece of index file with all the charts.
*
* @param charts
* The chart to be placed in the report.
* @return The piece of index file with all the charts.
* @throws PerfCakeException
* When it was not possible to read or parse the template.
*/
private static String getChartsDiv(final List<C3Chart> charts) throws PerfCakeException {
final StringBuilder sb = new StringBuilder();
final List<String> groups = new ArrayList<>();
final Map<String, List<C3Chart>> chartsByGroup = new HashMap<>();
final Map<String, List<C3Chart>> chartsByGroupCombined = new HashMap<>();
final Map<String, String> toc = new LinkedHashMap<>();
for (final C3Chart chart : charts) {
if (!groups.contains(chart.getGroup())) {
groups.add(chart.getGroup());
}
if (chart.isCombined()) {
chartsByGroupCombined.putIfAbsent(chart.getGroup(), new ArrayList<>());
chartsByGroupCombined.get(chart.getGroup()).add(chart);
} else {
chartsByGroup.putIfAbsent(chart.getGroup(), new ArrayList<>());
chartsByGroup.get(chart.getGroup()).add(chart);
}
}
// list all groups
for (final String group : groups) {
sb.append(getHeading(2, "", "Charts for group: " + group));
// plain charts, i.e. not created by combining others
if (chartsByGroup.get(group) != null) {
sb.append(getHeading(3, "", "Plain results"));
for (final C3Chart chart : chartsByGroup.get(group)) {
final String label = chart.getName() + " (created: " + getCreatedAsString(chart) + ")";
sb.append(getHeading(4, chart.getBaseName(), label));
sb.append(getChartDiv(chart.getBaseName()));
toc.put(chart.getBaseName(), label);
}
}
// combined charts next
if (chartsByGroupCombined.get(group) != null) {
sb.append(getHeading(3, "", "Combined results"));
for (final C3Chart chart : chartsByGroupCombined.get(group)) {
sb.append(getHeading(4, chart.getBaseName(), chart.getName()));
sb.append(getChartDiv(chart.getBaseName()));
toc.put(chart.getBaseName(), chart.getName());
}
}
}
sb.insert(0, getToc(toc));
sb.insert(0, getHeading(2, "", "Table of Contents"));
return sb.toString();
}
/**
* Gets the creation date and time as localized string.
*
* @return The creation date and time as localized string.
*/
static String getCreatedAsString(final C3Chart chart) {
final ZonedDateTime ldt = ZonedDateTime.ofInstant(Instant.ofEpochMilli(chart.getCreated()), ZoneId.systemDefault());
return DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).format(ldt);
}
/**
* Writes a quick view HTML file that can display the chart during the test run.
*
* @throws PerfCakeException
* When it was not possible to store the quick view file.
*/
static void writeQuickView(final Path target, final C3Chart chart) throws PerfCakeException {
final Path quickViewFile = Paths.get(target.toString(), "data", chart.getBaseName() + ".html");
final Properties quickViewProps = getChartProperties(chart);
Utils.copyTemplateFromResource("/c3chart/quick-view.html", quickViewFile, quickViewProps);
}
/**
* Converts chart meta-data to properties.
*
* @param chart
* Chart meta-data.
* @return Properties pre-filled from the chart meta-data.
*/
private static Properties getChartProperties(final C3Chart chart) {
final Properties props = new Properties();
props.setProperty("baseName", chart.getBaseName());
props.setProperty("xAxis", chart.getxAxis());
props.setProperty("yAxis", chart.getyAxis());
props.setProperty("chartName", chart.getName());
props.setProperty("height", String.valueOf(chart.getHeight()));
props.setProperty("type", chart.getType() == ChartDestination.ChartType.BAR ? "type: 'bar'," : "");
switch (chart.getxAxisType()) {
case TIME:
props.setProperty("format", "ms2hms");
props.setProperty("xAxisKey", C3ChartHelper.COLUMN_TIME);
break;
case ITERATION:
props.setProperty("format", "function(x) { return x; }");
props.setProperty("xAxisKey", C3ChartHelper.COLUMN_ITERATION);
break;
case PERCENTAGE:
props.setProperty("format", "function(x) { return '' + x + '%'; }");
props.setProperty("xAxisKey", C3ChartHelper.COLUMN_PERCENT);
break;
}
return props;
}
}