com.mgmtp.jfunk.core.reporting.CsvReporter.java Source code

Java tutorial

Introduction

Here is the source code for com.mgmtp.jfunk.core.reporting.CsvReporter.java

Source

/*
 * Copyright (c) 2015 mgm technology partners GmbH
 *
 * 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 com.mgmtp.jfunk.core.reporting;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.Lists.newArrayListWithCapacity;
import static org.apache.commons.lang3.StringUtils.isBlank;

import java.nio.charset.Charset;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.concurrent.ThreadSafe;
import javax.inject.Inject;
import javax.inject.Provider;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.text.StrBuilder;

import com.google.common.collect.ImmutableList;
import com.mgmtp.jfunk.common.JFunkConstants;
import com.mgmtp.jfunk.core.module.TestModule;
import com.mgmtp.jfunk.data.DataSet;

/**
 * Reporter for writing test data and/or configuration properties to a CSV file. This reporter only reports on module level. Steps
 * are not reported, even if annotated with {@link Reported}. An additional 'result' column is always written as the last column
 * with values {@code OK} or {@code ERROR}.
 * 
 * @author rnaegele
 */
@ThreadSafe
public class CsvReporter extends AbstractFileReporter {

    private static final String DEFAULT_DELIMITER = ";";
    private static final char NULL_CHAR = '\0';

    private final String delimiter;
    private final char quoteChar;
    private final boolean quoted;
    private final List<String> headers;

    private List<Column> columns;

    private final String dataSetKey;

    private String header;

    @Inject
    Provider<Map<String, DataSet>> currentDataSetsProvider;

    /**
     * Creates a new instance.
     * 
     * @param fileName
     *            the name of the file to write the report to (relative to the archive base directory); if {@code null} it is
     *            computed from the reporter's class name and the reporter's key
     * @param headers
     *            the header for the CSV report; if this param's value is {@code null}, all entries from the data set specified by
     *            parameter {@code dataSetKey} are written to the report with the header being the data set key plus the entry key
     *            seperated by a space character
     * @param delimiter
     *            the column delimiter
     * @param quoteChar
     *            the quote character, if '\0', no quoting happens
     * @param dataSetKey
     *            the data set key; must be specified if and only if {@code headers} is {@code null}
     * @param charset
     *            the charset for the report
     */
    CsvReporter(final String fileName, final List<String> headers, final String delimiter, final char quoteChar,
            final String dataSetKey, final Charset charset) {
        super(fileName, charset);

        if (headers == null) {
            checkArgument(dataSetKey != null, "If 'headers' is set to null, 'dataSetKey' must be set");
        } else {
            checkArgument(dataSetKey == null, "If 'headers' is not set to null, 'dataSetKey' must not be set");
        }

        this.delimiter = delimiter == null ? DEFAULT_DELIMITER : delimiter;
        this.quoteChar = quoteChar;
        this.quoted = quoteChar != NULL_CHAR;
        this.headers = headers != null ? ImmutableList.copyOf(headers) : null;
        this.dataSetKey = dataSetKey;
    }

    @Override
    public String getName() {
        return "CSV-Report";
    }

    @Override
    protected String getFileExtension() {
        return "csv";
    }

    private void initColumns() {
        StrBuilder sb = new StrBuilder(256);

        if (headers == null) {

            // Create a column for every data set key
            DataSet dataSet = currentDataSetsProvider.get().get(dataSetKey);
            checkNotNull(dataSet, "No data set available for key: " + dataSetKey);

            Set<String> keysSet = dataSet.getDataView().keySet();
            columns = newArrayListWithCapacity(keysSet.size());
            for (String key : keysSet) {
                columns.add(new Column(key, dataSetKey));
                appendEscapedAndQuoted(sb, dataSetKey + ' ' + key);
            }

        } else {

            columns = newArrayListWithCapacity(headers.size());
            for (String h : headers) {
                columns.add(new Column(h));
                appendEscapedAndQuoted(sb, h);
            }
        }

        // additional default columns
        appendEscapedAndQuoted(sb, JFunkConstants.CURRENT_MODULE_NAME);
        appendEscapedAndQuoted(sb, JFunkConstants.CURRENT_MODULE_RESULT);
        appendEscapedAndQuoted(sb, JFunkConstants.CURRENT_MODULE_ERROR);

        header = sb.toString();
    }

    @Override
    protected synchronized String getHeaderLine() {
        if (header == null) {
            initColumns();
        }
        return header;
    }

    @Override
    public void addResult(final ReportContext context) {
        if (!(TestModule.class.isAssignableFrom(context.getTestObjectType()))) {
            // we are only interested in the data for each module
            return;
        }

        log.debug("Adding result to reporter '{}'", getName());

        synchronized (this) {
            if (columns == null) {
                initColumns();
            }

            Map<String, DataSet> dataSets = currentDataSetsProvider.get();
            StrBuilder sb = new StrBuilder(256);
            for (Column column : columns) {
                String dsKey = column.dataSetKey;
                String value;

                if (StringUtils.isNotBlank(dsKey)) {
                    // get value from data set
                    DataSet ds = dataSets.get(dsKey);
                    checkNotNull(ds, "No data set available for key: " + dsKey);
                    value = ds.getValue(column.key);
                } else {
                    // get property
                    value = configProvider.get().get(column.key);
                }

                appendEscapedAndQuoted(sb, value);
            }

            // additional result column
            appendEscapedAndQuoted(sb, context.getTestObjectName());
            appendEscapedAndQuoted(sb, context.isSuccess() ? JFunkConstants.OK : JFunkConstants.ERROR);

            if (context.isSuccess()) {
                appendEscapedAndQuoted(sb, "");
            } else {
                Throwable th = context.getThrowable();
                String msg = th.getMessage();

                Throwable root = th;
                while (root.getCause() != null) {
                    root = root.getCause();
                }

                String rootMsg = root.getMessage();
                if (rootMsg != null && !rootMsg.equals(msg)) {
                    msg += " - Root Message: " + rootMsg;
                }

                if (isBlank(msg)) {
                    msg = th.getClass().getName();
                }
                appendEscapedAndQuoted(sb, msg);
            }

            if (sb.isEmpty()) {
                log.info("Ignoring empty row in report");
            } else {
                String line = sb.toString();
                reportLines.add(line);
            }
        }
    }

    private void appendEscapedAndQuoted(final StrBuilder sb, final String value) {
        boolean foundLineBreak = false;

        sb.appendSeparator(delimiter);

        if (quoted) {
            sb.append(quoteChar);
        }

        if (value != null) {
            for (int i = 0, len = value.length(); i < len; ++i) {
                char c = value.charAt(i);
                switch (c) {
                case '\r':
                case '\n':
                    foundLineBreak = true;
                    continue;
                default:
                    if (foundLineBreak) {
                        sb.append(' ');
                        foundLineBreak = false;
                    }
                    if (quoted && c == quoteChar) { // can't have this as case because it is no constant expression
                        sb.append(c); // escape double quote, i. e. add quote character again
                    }
                    break;
                }
                sb.append(c);
            }
        }

        if (quoted) {
            sb.append(quoteChar);
        }
    }

    static class Column {
        // Contains the name of the data set or {@code null} if {@link #key} represents a property.
        String dataSetKey;

        // Contains either a property or a data set entry key
        String key;

        Column(final String header) {
            String[] a = header.trim().split(" ");
            if (a.length == 2) {
                dataSetKey = a[0];
                this.key = a[1];
            } else if (a.length == 1) {
                this.key = header;
            } else {
                throw new IllegalArgumentException(
                        "Only one space is allowed as a separator: " + key + " contains more than one space");
            }
        }

        Column(final String key, final String dataSetKey) {
            this.key = key;
            this.dataSetKey = dataSetKey;
        }
    }

    /**
     * Creates a {@link CsvReporterBuilder} for a CSV report based on the {@link DataSet} with the specified key.
     * 
     * @param dataSetKey
     *            the data set key
     * @return the builder
     */
    public static CsvReporterBuilder forDataSet(final String dataSetKey) {
        return new CsvReporterBuilder(dataSetKey);
    }

    /**
     * Creates a {@link CsvReporterBuilder} for a CSV report based on the specified list of headers.
     * 
     * @param headers
     *            the list of headers
     * @return the builder
     */
    public static CsvReporterBuilder withHeaders(final List<String> headers) {
        return new CsvReporterBuilder(headers);
    }

    /**
     * A builder for creating {@link CsvReporter} instances.
     * 
     * @author rnaegele
     */
    public static class CsvReporterBuilder {

        private String dataSetKey;
        private List<String> headers;
        private String delimiter;
        private char quoteChar;
        private String fileName;
        private Charset charset;

        CsvReporterBuilder(final String dataSetKey) {
            this.dataSetKey = dataSetKey;
        }

        CsvReporterBuilder(final List<String> headers) {
            this.headers = headers;
        }

        /**
         * Sets the delimiter and returns the underlining builder instance.
         * 
         * @param aDelimiter
         *            the column delimiter
         * @return the builder
         */
        public CsvReporterBuilder delimitedBy(final String aDelimiter) {
            delimiter = aDelimiter;
            return this;
        }

        /**
         * Sets the quote character and returns the underlining builder instance.
         * 
         * @param aQuoteChar
         *            the quote character, if '\0', no quoting happens
         * @return the builder
         */
        public CsvReporterBuilder quotedWith(final char aQuoteChar) {
            quoteChar = aQuoteChar;
            return this;
        }

        /**
         * Sets the character set and returns the underlining builder instance.
         * 
         * @param aCharset
         *            the charset for the report
         * @return the builder
         */
        public CsvReporterBuilder withCharset(final Charset aCharset) {
            charset = aCharset;
            return this;
        }

        /**
         * Sets the file name and returns the underlining builder instance.
         * 
         * @param aFileName
         *            the name of the file to write the report to (relative to the archive base directory); if {@code null} it is
         *            computed from the reporter's class name and the reporter's key
         * @return the builder
         */
        public CsvReporterBuilder writtenTo(final String aFileName) {
            fileName = aFileName;
            return this;
        }

        /**
         * Creates a new {@link CsvReporter} instance from the underlying builder.
         * 
         * @return the reporter instance
         */
        public CsvReporter create() {
            return new CsvReporter(fileName, headers, delimiter, quoteChar, dataSetKey, charset);
        }
    }
}