org.perfcake.reporting.destination.c3chart.C3ChartDataFile.java Source code

Java tutorial

Introduction

Here is the source code for org.perfcake.reporting.destination.c3chart.C3ChartDataFile.java

Source

/*
 * -----------------------------------------------------------------------\
 * 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.PerfCakeConst;
import org.perfcake.PerfCakeException;
import org.perfcake.reporting.Measurement;
import org.perfcake.reporting.Quantity;
import org.perfcake.reporting.ReportingException;
import org.perfcake.util.Utils;

import io.vertx.core.json.Json;
import io.vertx.core.json.JsonArray;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;

/**
 * Representation of all data files needed to write a chart to the disk. Also handles directory creation and copies basic html files and their dependencies.
 *
 * @author <a href="mailto:marvenec@gmail.com">Martin Ve?ea</a>
 */
public class C3ChartDataFile {

    /**
     * A logger for the class.
     */
    private static final Logger log = LogManager.getLogger(C3ChartDataFile.class);

    /**
     * A list of file resources to be copied in the resulting report.
     */
    private static final String[] resourceFiles = new String[] { "c3.min.css", "c3.min.js", "d3.v3.min.js",
            "report.css", "report.js", "favicon.svg" };

    /**
     * The JavaScript file representing chart data. This is not set for charts created as a combination of existing ones.
     */
    private transient File dataFile = null;

    /**
     * A file channel for storing results.
     */
    private transient FileChannel outputChannel;

    /**
     * Target path for storing all data files related to the chart. These are the data itself (.js), the description file (.dat),
     * and the quick view file (.html).
     */
    private final Path target;

    /**
     * This is set to false after a first result line is obtained from the getResultLine() method.
     * In the method, this is used to display a complete warning message for the user to be able to fix
     * the scenario. But we do not want the warning to show every time as it would slow down the performance.
     */
    private boolean firstResultsLine = true;

    /**
     * Chart meta-data.
     */
    private C3Chart chart;

    /**
     * Creates a new data files structure and write basic structures to the drive.
     *
     * @param chart
     *       Chart meta-data.
     * @param target
     *       Root path where all the files and directories will be created. Attempts to create the missing directories.
     * @throws PerfCakeException
     *       In case of an I/O error.
     */
    C3ChartDataFile(final C3Chart chart, final Path target) throws PerfCakeException {
        this.chart = chart;
        this.target = target;

        createOutputFileStructure();

        writeDataHeader();
        C3ChartHtmlTemplates.writeQuickView(target, chart);
        writeDescriptor();
    }

    /**
     * Replaces the data file of the given chart with new data.
     *
     * @param chart
     *       Chart meta-data.
     * @param target
     *       Root path to an existing chart report.
     * @param newData
     *       New data to be written.
     * @throws PerfCakeException
     *       In case of an I/O error.
     */
    C3ChartDataFile(final C3Chart chart, final Path target, final C3ChartData newData) throws PerfCakeException {
        this.chart = chart;
        this.target = target;

        writeDataHeader();

        try (FileChannel dataOutput = FileChannel.open(getDataFile().toPath(), StandardOpenOption.APPEND);) {
            for (final JsonArray json : newData.getData()) {
                StringBuilder sb = new StringBuilder();
                sb.append(chart.getBaseName());
                sb.append(".push(");
                sb.append(json.encode());
                sb.append(");\n");

                dataOutput.write(
                        ByteBuffer.wrap(sb.toString().getBytes(Charset.forName(Utils.getDefaultEncoding()))));
            }
        } catch (IOException e) {
            throw new PerfCakeException("Unable to write new chart data: ", e);
        }
    }

    /**
     * Reads the chart meta-data from the existing directory structure. All internal structures are initialized.
     *
     * @param descriptorFile
     *       The direct pointer to the ${target}/data/${baseName}.json file.
     * @throws PerfCakeException
     *       In case of an I/O error.
     */
    C3ChartDataFile(final File descriptorFile) throws PerfCakeException {
        try {
            final String chartJson = Utils.readFilteredContent(descriptorFile.toURI().toURL());
            chart = Json.decodeValue(chartJson, C3Chart.class);

            target = descriptorFile.getParentFile().getParentFile().toPath();
        } catch (IOException e) {
            throw new PerfCakeException("Unable to read chart descriptor: ", e);
        }
    }

    /**
     * Reads the chart meta-data from the existing directory structure. All internal structures are initialized.
     *
     * @param target
     *       Root path to an existing chart report.
     * @param baseName
     *       The base name of the chart file data (i. e. ${target}/data/${baseName}.*).
     * @throws PerfCakeException
     *       In case of an I/O error.
     */
    C3ChartDataFile(final Path target, final String baseName) throws PerfCakeException {
        this(Paths.get(target.toString(), "data", baseName + ".json").toFile());
    }

    /**
     * Get chart meta-data.
     *
     * @return Chart meta-data.
     */
    public C3Chart getChart() {
        return chart;
    }

    /**
     * Gets the root path of this chart report.
     *
     * @return The root path of this chart report.
     */
    public Path getTarget() {
        return target;
    }

    /**
     * Gets the specific file with chart data.
     *
     * @return The specific file with chart data.
     */
    private File getDataFile() {
        if (dataFile == null) {
            final Path dataFilePath = Paths.get(target.toString(), "data", chart.getBaseName() + ".js");
            dataFile = dataFilePath.toFile();
        }

        return dataFile;
    }

    /**
     * Opens the data file for output of additional values.
     *
     * @throws PerfCakeException
     *       In case of an I/O error.
     */
    public void open() throws PerfCakeException {
        try {
            outputChannel = FileChannel.open(getDataFile().toPath(), StandardOpenOption.APPEND);
        } catch (final IOException e) {
            throw new PerfCakeException(
                    String.format("Cannot open data file %s for appending data.", dataFile.getAbsolutePath()), e);
        }

    }

    /**
     * Gets a JavaScript line to be written to the data file that represents the current Measurement.
     * All attributes required by the attributes list of this chart must be present in the measurement for the line to be returned.
     *
     * @param measurement
     *       The current measurement.
     * @return The line representing the data in measurement specified by the attributes list of this chart, or null when there was some of the attributes missing.
     */
    private String getResultLine(final Measurement measurement) {
        final StringBuilder sb = new StringBuilder();
        boolean missingAttributes = false;
        boolean isWarmUp = measurement.get(PerfCakeConst.WARM_UP_TAG) != null
                ? (Boolean) measurement.get(PerfCakeConst.WARM_UP_TAG)
                : false;

        sb.append(chart.getBaseName());
        sb.append(".push([");
        switch (chart.getxAxisType()) {
        case TIME:
            sb.append(measurement.getTime());
            break;
        case ITERATION:
            sb.append(measurement.getIteration());
            break;
        case PERCENTAGE:
            sb.append(measurement.getPercentage());
            break;
        }

        int nullFields = 0;
        for (final String attr : chart.getAttributes()) {
            if (chart.getAttributes().indexOf(attr) > 0) {
                boolean warmUpAttr = attr.endsWith(PerfCakeConst.WARM_UP_TAG); // warmUp is handled using separate columns with the same base name and suffix _warmUp
                String pureAttr = warmUpAttr
                        ? attr.substring(0, attr.length() - PerfCakeConst.WARM_UP_TAG.length() - 1)
                        : attr;

                sb.append(", ");

                // we do not have all required attributes, return an empty line
                if (!warmUpAttr && !measurement.getAll().containsKey(attr)) {
                    missingAttributes = true;
                    if (firstResultsLine) {
                        log.warn(String.format("Missing attribute %s, skipping the record.", attr));
                    }
                } else {
                    final Object data = measurement.get(pureAttr);

                    // we put null values in either all the fields with the _warmUp suffix, or the others depending whether we are in the warmUp phase
                    if (isWarmUp ^ warmUpAttr) {
                        nullFields++;
                        sb.append("null");
                    } else {
                        if (data instanceof String) {
                            sb.append("\"");
                            sb.append(((String) data).replaceAll("\"", "\\\""));
                            sb.append("\"");
                        } else if (data instanceof Quantity) {
                            sb.append(((Quantity) data).getNumber().toString());
                        } else {
                            if (data == null) {
                                nullFields++;
                                sb.append("null");
                            } else {
                                sb.append(data.toString());
                            }
                        }
                    }
                }
            }
        }

        firstResultsLine = false;

        // we must postpone the return for all misses to be shown
        // we also want to skip any records with just null values
        if (missingAttributes || chart.getAttributes().size() - nullFields <= 1) {
            return "";
        }

        sb.append("]);\n");

        return sb.toString();
    }

    /**
     * Appends results to this chart based on the given Measurement.
     *
     * @param measurement
     *       The Measurement to be stored.
     * @throws ReportingException
     *       When it was not possible to write the data.
     */
    void appendResult(final Measurement measurement) throws ReportingException {
        final String line = getResultLine(measurement);
        if (!"".equals(line)) {
            try {
                outputChannel.write(ByteBuffer.wrap(line.getBytes(Charset.forName(Utils.getDefaultEncoding()))));
            } catch (final IOException ioe) {
                throw new ReportingException(String.format("Could not append data to the chart file %s.",
                        getDataFile().getAbsolutePath()), ioe);
            }
        }
    }

    /**
     * Closes the output channel.
     *
     * @throws PerfCakeException
     *       In case of an I/O error.
     */
    public void close() throws PerfCakeException {
        try {
            outputChannel.close();
        } catch (final IOException e) {
            throw new PerfCakeException(
                    String.format("Cannot close output channel to the file %s.", getDataFile().getAbsolutePath()),
                    e);
        }
    }

    /**
     * Writes the initial header and array definition to the JavaScript data file.
     *
     * @throws PerfCakeException
     *       When it was not possible to write the data.
     */
    private void writeDataHeader() throws PerfCakeException {
        final StringBuilder dataHeader = new StringBuilder("var ");
        dataHeader.append(chart.getBaseName());
        dataHeader.append(" = [ [ ");

        boolean first = true;
        for (final String attr : chart.getAttributes()) {
            if (first) {
                dataHeader.append("'");
                first = false;
            } else {
                dataHeader.append(", '");
            }
            dataHeader.append(attr);
            dataHeader.append("'");
        }
        dataHeader.append(" ] ];\n");

        dataHeader.append("\n");
        Utils.writeFileContent(getDataFile(), dataHeader.toString());
    }

    /**
     * Serializes chart meta-data as JSON to the output directory structure.
     *
     * @throws PerfCakeException
     *       In case of an I/O error.
     */
    private void writeDescriptor() throws PerfCakeException {
        final Path descriptorFile = Paths.get(target.toString(), "data", chart.getBaseName() + ".json");
        Utils.writeFileContent(descriptorFile, Json.encode(chart));
    }

    /**
     * Creates output file structure including all needed CSS and JS files.
     *
     * @throws PerfCakeException
     *       When it was not possible to create any of the directories or files.
     */
    private void createOutputFileStructure() throws PerfCakeException {
        if (!target.toFile().exists()) {
            if (!target.toFile().mkdirs()) {
                throw new PerfCakeException(
                        "Could not create output directory: " + target.toFile().getAbsolutePath());
            }
        } else {
            if (!target.toFile().isDirectory()) {
                throw new PerfCakeException("Could not create output directory. It already exists as a file: "
                        + target.toFile().getAbsolutePath());
            }
        }

        File dir = Paths.get(target.toString(), "data").toFile();
        if (!dir.exists() && !dir.mkdirs()) {
            throw new PerfCakeException("Could not create data directory: " + dir.getAbsolutePath());
        }

        dir = Paths.get(target.toString(), "src").toFile();
        if (!dir.exists() && !dir.mkdirs()) {
            throw new PerfCakeException("Could not create source directory: " + dir.getAbsolutePath());
        }

        try {
            for (final String resourceFileName : resourceFiles) {
                copyResourceFile(resourceFileName);
            }
        } catch (final IOException e) {
            throw new PerfCakeException("Cannot copy necessary chart resources to the output path: ", e);
        }
    }

    /**
     * Copies the given resource file to the target chart report.
     *
     * @param resourceFileName
     *       The name of the resource.
     * @throws IOException
     *       When it was not possible to copy the resource.
     */
    private void copyResourceFile(final String resourceFileName) throws IOException {
        Files.copy(getClass().getResourceAsStream("/c3chart/" + resourceFileName),
                Paths.get(target.toString(), "src", resourceFileName), StandardCopyOption.REPLACE_EXISTING);
    }

}