Package org.apache.jmeter.save

Source Code of org.apache.jmeter.save.CSVSaveService

/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements.  See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.jmeter.save;

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.StringReader;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Vector;

import javax.swing.table.DefaultTableModel;

import org.apache.commons.collections.map.LinkedMap;
import org.apache.commons.lang.CharUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.jmeter.assertions.AssertionResult;
import org.apache.jmeter.reporters.ResultCollector;
import org.apache.jmeter.samplers.SampleEvent;
import org.apache.jmeter.samplers.SampleResult;
import org.apache.jmeter.samplers.SampleSaveConfiguration;
import org.apache.jmeter.samplers.StatisticalSampleResult;
import org.apache.jmeter.util.JMeterUtils;
import org.apache.jmeter.visualizers.Visualizer;
import org.apache.jorphan.logging.LoggingManager;
import org.apache.jorphan.reflect.Functor;
import org.apache.jorphan.util.JMeterError;
import org.apache.jorphan.util.JOrphanUtils;
import org.apache.log.Logger;
import org.apache.oro.text.regex.Pattern;
import org.apache.oro.text.regex.PatternMatcherInput;
import org.apache.oro.text.regex.Perl5Compiler;
import org.apache.oro.text.regex.Perl5Matcher;

/**
* This class provides a means for saving/reading test results as CSV files.
*/
// For unit tests, @see TestCSVSaveService
public final class CSVSaveService {
    private static final Logger log = LoggingManager.getLoggerForClass();

    // ---------------------------------------------------------------------
    // XML RESULT FILE CONSTANTS AND FIELD NAME CONSTANTS
    // ---------------------------------------------------------------------

    private static final String DATA_TYPE = "dataType"; // $NON-NLS-1$
    private static final String FAILURE_MESSAGE = "failureMessage"; // $NON-NLS-1$
    private static final String LABEL = "label"; // $NON-NLS-1$
    private static final String RESPONSE_CODE = "responseCode"; // $NON-NLS-1$
    private static final String RESPONSE_MESSAGE = "responseMessage"; // $NON-NLS-1$
    private static final String SUCCESSFUL = "success"; // $NON-NLS-1$
    private static final String THREAD_NAME = "threadName"; // $NON-NLS-1$
    private static final String TIME_STAMP = "timeStamp"; // $NON-NLS-1$

    // ---------------------------------------------------------------------
    // ADDITIONAL CSV RESULT FILE CONSTANTS AND FIELD NAME CONSTANTS
    // ---------------------------------------------------------------------

    private static final String CSV_ELAPSED = "elapsed"; // $NON-NLS-1$
    private static final String CSV_BYTES= "bytes"; // $NON-NLS-1$
    private static final String CSV_THREAD_COUNT1 = "grpThreads"; // $NON-NLS-1$
    private static final String CSV_THREAD_COUNT2 = "allThreads"; // $NON-NLS-1$
    private static final String CSV_SAMPLE_COUNT = "SampleCount"; // $NON-NLS-1$
    private static final String CSV_ERROR_COUNT = "ErrorCount"; // $NON-NLS-1$
    private static final String CSV_URL = "URL"; // $NON-NLS-1$
    private static final String CSV_FILENAME = "Filename"; // $NON-NLS-1$
    private static final String CSV_LATENCY = "Latency"; // $NON-NLS-1$
    private static final String CSV_ENCODING = "Encoding"; // $NON-NLS-1$
    private static final String CSV_HOSTNAME = "Hostname"; // $NON-NLS-1$

    // Used to enclose variable name labels, to distinguish from any of the above labels
    private static final String VARIABLE_NAME_QUOTE_CHAR = "\""// $NON-NLS-1$

    // Initial config from properties
    static private final SampleSaveConfiguration _saveConfig = SampleSaveConfiguration.staticConfig();

    // Date format to try if the time format does not parse as milliseconds
    // (this is the suggested value in jmeter.properties)
    private static final String DEFAULT_DATE_FORMAT_STRING = "MM/dd/yy HH:mm:ss"; // $NON-NLS-1$
    private static final DateFormat DEFAULT_DATE_FORMAT = new SimpleDateFormat(DEFAULT_DATE_FORMAT_STRING);

    /**
     * Private constructor to prevent instantiation.
     */
    private CSVSaveService() {
    }

    /**
     * Read Samples from a file; handles quoted strings.
     *
     * @param filename input file
     * @param visualizer where to send the results
     * @param resultCollector the parent collector
     * @throws IOException
     */
    public static void processSamples(String filename, Visualizer visualizer,
            ResultCollector resultCollector) throws IOException {
        BufferedReader dataReader = null;
        final boolean errorsOnly = resultCollector.isErrorLogging();
        final boolean successOnly = resultCollector.isSuccessOnlyLogging();
        try {
            dataReader = new BufferedReader(new FileReader(filename));
            dataReader.mark(400);// Enough to read the header column names
            // Get the first line, and see if it is the header
            String line = dataReader.readLine();
            if (line == null){
                throw new IOException(filename+": unable to read header line");
            }
            long lineNumber=1;
            SampleSaveConfiguration saveConfig = CSVSaveService.getSampleSaveConfiguration(line,filename);
            if (saveConfig == null) {// not a valid header
                log.info(filename+" does not appear to have a valid header. Using default configuration.");
                saveConfig = (SampleSaveConfiguration) resultCollector.getSaveConfig().clone(); // may change the format later
                dataReader.reset(); // restart from beginning
                lineNumber = 0;
            }
            String [] parts;
            final char delim = saveConfig.getDelimiter().charAt(0);
            // TODO: does it matter that an empty line will terminate the loop?
            // CSV output files should never contain empty lines, so probably not
            // If so, then need to check whether the reader is at EOF
            while ((parts = csvReadFile(dataReader, delim)).length != 0) {
                lineNumber++;
                SampleEvent event = CSVSaveService.makeResultFromDelimitedString(parts,saveConfig,lineNumber);
                if (event != null){
                    final SampleResult result = event.getResult();
                    if (ResultCollector.isSampleWanted(result.isSuccessful(),errorsOnly, successOnly)) {
                        visualizer.add(result);
                    }
                }
            }
        } finally {
            JOrphanUtils.closeQuietly(dataReader);
        }
    }

    /**
     * Make a SampleResult given a delimited string.
     *
     * @param inputLine - line from CSV file
     * @param saveConfig - configuration
     * @param lineNumber - line number for error reporting
     * @return SampleResult
     *
     * @deprecated Does not handle quoted strings; use {@link #processSamples(String, Visualizer, ResultCollector)} instead
     *
     * @throws JMeterError
     */
    public static SampleEvent makeResultFromDelimitedString(
            final String inputLine,
            final SampleSaveConfiguration saveConfig, // may be updated
            final long lineNumber) {
        /*
         * Bug 40772: replaced StringTokenizer with String.split(), as the
         * former does not return empty tokens.
         */
        // The \Q prefix is needed to ensure that meta-characters (e.g. ".") work.
        String parts[]=inputLine.split("\\Q"+saveConfig.getDelimiter());// $NON-NLS-1$
        return makeResultFromDelimitedString(parts, saveConfig, lineNumber);
    }

    /**
     * Make a SampleResult given a set of tokens
     * @param parts tokens parsed from the input
     * @param saveConfig the save configuration (may be updated)
     * @param lineNumber
     * @return the sample result
     *
     * @throws JMeterError
     */
    private static SampleEvent makeResultFromDelimitedString(
            final String[] parts,
            final SampleSaveConfiguration saveConfig, // may be updated
            final long lineNumber) {

        SampleResult result = null;
        String hostname = "";// $NON-NLS-1$
        long timeStamp = 0;
        long elapsed = 0;
        String text = null;
        String field = null; // Save the name for error reporting
        int i=0;

        try {
            if (saveConfig.saveTimestamp()){
                field = TIME_STAMP;
                text = parts[i++];
                if (saveConfig.printMilliseconds()) {
                    try {
                        timeStamp = Long.parseLong(text);
                    } catch (NumberFormatException e) {// see if this works
                        log.warn(e.toString());
                        // method is only ever called from one thread at a time
                        // so it's OK to use a static DateFormat
                        Date stamp = DEFAULT_DATE_FORMAT.parse(text);
                        timeStamp = stamp.getTime();
                        log.warn("Setting date format to: "+DEFAULT_DATE_FORMAT_STRING);
                        saveConfig.setFormatter(DEFAULT_DATE_FORMAT);
                    }
                } else if (saveConfig.formatter() != null) {
                    Date stamp = saveConfig.formatter().parse(text);
                    timeStamp = stamp.getTime();
                } else { // can this happen?
                    final String msg = "Unknown timestamp format";
                    log.warn(msg);
                    throw new JMeterError(msg);
                }
            }

            if (saveConfig.saveTime()) {
                field = CSV_ELAPSED;
                text = parts[i++];
                elapsed = Long.parseLong(text);
            }

            if (saveConfig.saveSampleCount()) {
                result = new StatisticalSampleResult(timeStamp, elapsed);
            } else {
                result = new SampleResult(timeStamp, elapsed);
            }

            if (saveConfig.saveLabel()) {
                field = LABEL;
                text = parts[i++];
                result.setSampleLabel(text);
            }
            if (saveConfig.saveCode()) {
                field = RESPONSE_CODE;
                text = parts[i++];
                result.setResponseCode(text);
            }

            if (saveConfig.saveMessage()) {
                field = RESPONSE_MESSAGE;
                text = parts[i++];
                result.setResponseMessage(text);
            }

            if (saveConfig.saveThreadName()) {
                field = THREAD_NAME;
                text = parts[i++];
                result.setThreadName(text);
            }

            if (saveConfig.saveDataType()) {
                field = DATA_TYPE;
                text = parts[i++];
                result.setDataType(text);
            }

            if (saveConfig.saveSuccess()) {
                field = SUCCESSFUL;
                text = parts[i++];
                result.setSuccessful(Boolean.valueOf(text).booleanValue());
            }

            if (saveConfig.saveAssertionResultsFailureMessage()) {
                i++;
                // TODO - should this be restored?
            }

            if (saveConfig.saveBytes()) {
                field = CSV_BYTES;
                text = parts[i++];
                result.setBytes(Integer.parseInt(text));
            }

            if (saveConfig.saveThreadCounts()) {
                field = CSV_THREAD_COUNT1;
                text = parts[i++];
                result.setGroupThreads(Integer.parseInt(text));

                field = CSV_THREAD_COUNT2;
                text = parts[i++];
                result.setAllThreads(Integer.parseInt(text));
            }

            if (saveConfig.saveUrl()) {
                i++;
                // TODO: should this be restored?
            }

            if (saveConfig.saveFileName()) {
                field = CSV_FILENAME;
                text = parts[i++];
                result.setResultFileName(text);
            }
            if (saveConfig.saveLatency()) {
                field = CSV_LATENCY;
                text = parts[i++];
                result.setLatency(Long.parseLong(text));
            }

            if (saveConfig.saveEncoding()) {
                field = CSV_ENCODING;
                text = parts[i++];
                result.setEncodingAndType(text);
            }

            if (saveConfig.saveSampleCount()) {
                field = CSV_SAMPLE_COUNT;
                text = parts[i++];
                result.setSampleCount(Integer.parseInt(text));
                field = CSV_ERROR_COUNT;
                text = parts[i++];
                result.setErrorCount(Integer.parseInt(text));
            }

            if (saveConfig.saveHostname()) {
                field = CSV_HOSTNAME;
                hostname = parts[i++];
            }

            if (i + saveConfig.getVarCount() < parts.length){
                log.warn("Line: "+lineNumber+". Found "+parts.length+" fields, expected "+i+". Extra fields have been ignored.");
            }

        } catch (NumberFormatException e) {
            log.warn("Error parsing field '" + field + "' at line " + lineNumber + " " + e);
            throw new JMeterError(e);
        } catch (ParseException e) {
            log.warn("Error parsing field '" + field + "' at line " + lineNumber + " " + e);
            throw new JMeterError(e);
        } catch (ArrayIndexOutOfBoundsException e){
            log.warn("Insufficient columns to parse field '" + field + "' at line " + lineNumber);
            throw new JMeterError(e);
        }
        return new SampleEvent(result,"",hostname);
    }

    /**
     * Generates the field names for the output file
     *
     * @return the field names as a string
     */
    public static String printableFieldNamesToString() {
        return printableFieldNamesToString(_saveConfig);
    }

    /**
     * Generates the field names for the output file
     *
     * @return the field names as a string
     */
    public static String printableFieldNamesToString(SampleSaveConfiguration saveConfig) {
        StringBuffer text = new StringBuffer();
        String delim = saveConfig.getDelimiter();

        if (saveConfig.saveTimestamp()) {
            text.append(TIME_STAMP);
            text.append(delim);
        }

        if (saveConfig.saveTime()) {
            text.append(CSV_ELAPSED);
            text.append(delim);
        }

        if (saveConfig.saveLabel()) {
            text.append(LABEL);
            text.append(delim);
        }

        if (saveConfig.saveCode()) {
            text.append(RESPONSE_CODE);
            text.append(delim);
        }

        if (saveConfig.saveMessage()) {
            text.append(RESPONSE_MESSAGE);
            text.append(delim);
        }

        if (saveConfig.saveThreadName()) {
            text.append(THREAD_NAME);
            text.append(delim);
        }

        if (saveConfig.saveDataType()) {
            text.append(DATA_TYPE);
            text.append(delim);
        }

        if (saveConfig.saveSuccess()) {
            text.append(SUCCESSFUL);
            text.append(delim);
        }

        if (saveConfig.saveAssertionResultsFailureMessage()) {
            text.append(FAILURE_MESSAGE);
            text.append(delim);
        }

        if (saveConfig.saveBytes()) {
            text.append(CSV_BYTES);
            text.append(delim);
        }

        if (saveConfig.saveThreadCounts()) {
            text.append(CSV_THREAD_COUNT1);
            text.append(delim);
            text.append(CSV_THREAD_COUNT2);
            text.append(delim);
        }

        if (saveConfig.saveUrl()) {
            text.append(CSV_URL);
            text.append(delim);
        }

        if (saveConfig.saveFileName()) {
            text.append(CSV_FILENAME);
            text.append(delim);
        }

        if (saveConfig.saveLatency()) {
            text.append(CSV_LATENCY);
            text.append(delim);
        }

        if (saveConfig.saveEncoding()) {
            text.append(CSV_ENCODING);
            text.append(delim);
        }

        if (saveConfig.saveSampleCount()) {
            text.append(CSV_SAMPLE_COUNT);
            text.append(delim);
            text.append(CSV_ERROR_COUNT);
            text.append(delim);
        }

        if (saveConfig.saveHostname()) {
            text.append(CSV_HOSTNAME);
            text.append(delim);
        }

        for (int i = 0; i < SampleEvent.getVarCount(); i++){
            text.append(VARIABLE_NAME_QUOTE_CHAR);
            text.append(SampleEvent.getVarName(i));
            text.append(VARIABLE_NAME_QUOTE_CHAR);
            text.append(delim);
        }

        String resultString = null;
        int size = text.length();
        int delSize = delim.length();

        // Strip off the trailing delimiter
        if (size >= delSize) {
            resultString = text.substring(0, size - delSize);
        } else {
            resultString = text.toString();
        }
        return resultString;
    }

    // Map header names to set() methods
    private static final LinkedMap headerLabelMethods = new LinkedMap();

    // These entries must be in the same order as columns are saved/restored.

    static {
            headerLabelMethods.put(TIME_STAMP, new Functor("setTimestamp"));
            headerLabelMethods.put(CSV_ELAPSED, new Functor("setTime"));
            headerLabelMethods.put(LABEL, new Functor("setLabel"));
            headerLabelMethods.put(RESPONSE_CODE, new Functor("setCode"));
            headerLabelMethods.put(RESPONSE_MESSAGE, new Functor("setMessage"));
            headerLabelMethods.put(THREAD_NAME, new Functor("setThreadName"));
            headerLabelMethods.put(DATA_TYPE, new Functor("setDataType"));
            headerLabelMethods.put(SUCCESSFUL, new Functor("setSuccess"));
            headerLabelMethods.put(FAILURE_MESSAGE, new Functor("setAssertionResultsFailureMessage"));
            headerLabelMethods.put(CSV_BYTES, new Functor("setBytes"));
            // Both these are needed in the list even though they set the same variable
            headerLabelMethods.put(CSV_THREAD_COUNT1,new Functor("setThreadCounts"));
            headerLabelMethods.put(CSV_THREAD_COUNT2,new Functor("setThreadCounts"));
            headerLabelMethods.put(CSV_URL, new Functor("setUrl"));
            headerLabelMethods.put(CSV_FILENAME, new Functor("setFileName"));
            headerLabelMethods.put(CSV_LATENCY, new Functor("setLatency"));
            headerLabelMethods.put(CSV_ENCODING, new Functor("setEncoding"));
            // Both these are needed in the list even though they set the same variable
            headerLabelMethods.put(CSV_SAMPLE_COUNT, new Functor("setSampleCount"));
            headerLabelMethods.put(CSV_ERROR_COUNT, new Functor("setSampleCount"));
            headerLabelMethods.put(CSV_HOSTNAME, new Functor("setHostname"));
    }

    /**
     * Parse a CSV header line
     * @param headerLine from CSV file
     * @param filename name of file (for log message only)
     * @return config corresponding to the header items found or null if not a header line
     */
    public static SampleSaveConfiguration getSampleSaveConfiguration(String headerLine, String filename){
        String[] parts = splitHeader(headerLine,_saveConfig.getDelimiter()); // Try default delimiter

        String delim = null;

        if (parts == null){
            Perl5Matcher matcher = JMeterUtils.getMatcher();
            PatternMatcherInput input = new PatternMatcherInput(headerLine);
            Pattern pattern = JMeterUtils.getPatternCache()
            // This assumes the header names are all single words with no spaces
            // word followed by 0 or more repeats of (non-word char + word)
            // where the non-word char (\2) is the same
            // e.g.  abc|def|ghi but not abd|def~ghi
                    .getPattern("\\w+((\\W)\\w+)?(\\2\\w+)*(\\2\"\\w+\")*", // $NON-NLS-1$
                            // last entries may be quoted strings
                    Perl5Compiler.READ_ONLY_MASK);
            if (matcher.matches(input, pattern)) {
                delim = matcher.getMatch().group(2);
                parts = splitHeader(headerLine,delim);// now validate the result
            }
        }

        if (parts == null) {
            return null; // failed to recognise the header
        }

        // We know the column names all exist, so create the config
        SampleSaveConfiguration saveConfig=new SampleSaveConfiguration(false);

        int varCount = 0;
        for(int i=0;i<parts.length;i++){
            String label = parts[i];
            if (isVariableName(label)){
                varCount++;
            } else {
                Functor set = (Functor) headerLabelMethods.get(label);
                set.invoke(saveConfig,new Boolean[]{Boolean.TRUE});
            }
        }

        if (delim != null){
            log.warn("Default delimiter '"+_saveConfig.getDelimiter()+"' did not work; using alternate '"+delim+"' for reading "+filename);
            saveConfig.setDelimiter(delim);
        }

        saveConfig.setVarCount(varCount);

        return saveConfig;
    }

    private static String[] splitHeader(String headerLine, String delim) {
        String parts[]=headerLine.split("\\Q"+delim);// $NON-NLS-1$
        int previous = -1;
        // Check if the line is a header
        for(int i=0;i<parts.length;i++){
            final String label = parts[i];
            // Check for Quoted variable names
            if (isVariableName(label)){
                previous=Integer.MAX_VALUE; // they are always last
                continue;
            }
            int current = headerLabelMethods.indexOf(label);
            if (current == -1){
                return null; // unknown column name
            }
            if (current <= previous){
                log.warn("Column header number "+(i+1)+" name "+ label + " is out of order.");
                return null; // out of order
            }
            previous = current;
        }
        return parts;
    }

    /**
     * Check if the label is a variable name, i.e. is it enclosed in double-quotes?
     *
     * @param label column name from CSV file
     * @return if the label is enclosed in double-quotes
     */
    private static boolean isVariableName(final String label) {
        return label.length() > 2
            && label.startsWith(VARIABLE_NAME_QUOTE_CHAR)
            && label.endsWith(VARIABLE_NAME_QUOTE_CHAR);
    }

    /**
     * Method will save aggregate statistics as CSV. For now I put it here.
     * Not sure if it should go in the newer SaveService instead of here.
     * if we ever decide to get rid of this class, we'll need to move this
     * method to the new save service.
     * @param data vector of data rows
     * @param writer output file
     * @throws IOException
     */
    public static void saveCSVStats(Vector data, FileWriter writer) throws IOException {
        saveCSVStats(data, writer, null);
    }

    /**
     * Method will save aggregate statistics as CSV. For now I put it here.
     * Not sure if it should go in the newer SaveService instead of here.
     * if we ever decide to get rid of this class, we'll need to move this
     * method to the new save service.
     * @param data vector of data rows
     * @param writer output file
     * @param headers header names (if non-null)
     * @throws IOException
     */
    public static void saveCSVStats(Vector data, FileWriter writer, String headers[]) throws IOException {
        final char DELIM = ',';
        final String LINE_SEP = System.getProperty("line.separator"); // $NON-NLS-1$
        final char SPECIALS[] = new char[] {DELIM, QUOTING_CHAR};
        if (headers != null){
            for (int i=0; i < headers.length; i++){
                if (i>0) {
                    writer.write(DELIM);
                }
                writer.write(quoteDelimiters(headers[i],SPECIALS));
            }
            writer.write(LINE_SEP);
        }
        for (int idx=0; idx < data.size(); idx++) {
            Vector row = (Vector)data.elementAt(idx);
            for (int idy=0; idy < row.size(); idy++) {
                if (idy > 0) {
                    writer.write(DELIM);
                }
                Object item = row.elementAt(idy);
                writer.write( quoteDelimiters(String.valueOf(item),SPECIALS));
            }
            writer.write(LINE_SEP);
        }
    }

    /**
     * Method saves aggregate statistics as CSV from a table model.
     * Same as {@link #saveCSVStats(Vector, FileWriter, String[])} except
     * that there is no need to create a Vector containing the data.
     *
     * @param model table model containing the data
     * @param writer output file
     * @throws IOException
     */
    public static void saveCSVStats(DefaultTableModel model, FileWriter writer) throws IOException {
        final char DELIM = ',';
        final String LINE_SEP = System.getProperty("line.separator"); // $NON-NLS-1$
        final char SPECIALS[] = new char[] {DELIM, QUOTING_CHAR};
        final int columns = model.getColumnCount();
        final int rows = model.getRowCount();
        for (int i=0; i < columns; i++){
            if (i>0) {
                writer.write(DELIM);
            }
            writer.write(quoteDelimiters(model.getColumnName(i),SPECIALS));
        }
        writer.write(LINE_SEP);
        for (int row=0; row < rows; row++) {
            for (int column=0; column < columns; column++) {
                if (column > 0) {
                    writer.write(DELIM);
                }
                Object item = model.getValueAt(row, column);
                writer.write( quoteDelimiters(String.valueOf(item),SPECIALS));
            }
            writer.write(LINE_SEP);
        }
    }

    /**
     * Convert a result into a string, where the fields of the result are
     * separated by the default delimiter.
     *
     * @param event
     *            the sample event to be converted
     * @return the separated value representation of the result
     */
    public static String resultToDelimitedString(SampleEvent event) {
        return resultToDelimitedString(event, event.getResult().getSaveConfig().getDelimiter());
    }

    /**
     * Convert a result into a string, where the fields of the result are
     * separated by a specified String.
     *
     * @param event
     *            the sample event to be converted
     * @param delimiter
     *            the separation string
     * @return the separated value representation of the result
     */
    public static String resultToDelimitedString(SampleEvent event, final String delimiter) {

        /*
         * Class to handle generating the delimited string.
         * - adds the delimiter if not the first call
         * - quotes any strings that require it
         */
        final class StringQuoter{
            final StringBuffer sb = new StringBuffer();
            private final char[] specials;
            private boolean addDelim;
            public StringQuoter(char delim) {
                specials = new char[] {delim, QUOTING_CHAR, CharUtils.CR, CharUtils.LF};
                addDelim=false; // Don't add delimiter first time round
            }

            private void addDelim(){
                if (addDelim){
                    sb.append(specials[0]);
                } else {
                    addDelim = true;
                }
            }
            // These methods handle parameters that could contain delimiters or quotes:
            public void append(String s){
                addDelim();
                //if (s == null) return;
                sb.append(quoteDelimiters(s,specials));
            }
            public void append(Object obj){
                append(String.valueOf(obj));
            }

            // These methods handle parameters that cannot contain delimiters or quotes
            public void append(int i){
                addDelim();
                sb.append(i);
            }
            public void append(long l){
                addDelim();
                sb.append(l);
            }
            public void append(boolean b){
                addDelim();
                sb.append(b);
            }

            public String toString(){
                return sb.toString();
            }
        }

        StringQuoter text = new StringQuoter(delimiter.charAt(0));

        SampleResult sample = event.getResult();
        SampleSaveConfiguration saveConfig = sample.getSaveConfig();

        if (saveConfig.saveTimestamp()) {
            if (saveConfig.printMilliseconds()){
                text.append(sample.getTimeStamp());
            } else if (saveConfig.formatter() != null) {
                String stamp = saveConfig.formatter().format(new Date(sample.getTimeStamp()));
                text.append(stamp);
            }
        }

        if (saveConfig.saveTime()) {
            text.append(sample.getTime());
        }

        if (saveConfig.saveLabel()) {
            text.append(sample.getSampleLabel());
        }

        if (saveConfig.saveCode()) {
            text.append(sample.getResponseCode());
        }

        if (saveConfig.saveMessage()) {
            text.append(sample.getResponseMessage());
        }

        if (saveConfig.saveThreadName()) {
            text.append(sample.getThreadName());
        }

        if (saveConfig.saveDataType()) {
            text.append(sample.getDataType());
        }

        if (saveConfig.saveSuccess()) {
            text.append(sample.isSuccessful());
        }

        if (saveConfig.saveAssertionResultsFailureMessage()) {
            String message = null;
            AssertionResult[] results = sample.getAssertionResults();

            if (results != null) {
                // Find the first non-null message
                for (int i = 0; i < results.length; i++){
                    message = results[i].getFailureMessage();
                    if (message != null) {
                        break;
                    }
                }
            }

            if (message != null) {
                text.append(message);
            } else {
                text.append(""); // Need to append something so delimiter is added
            }
        }

        if (saveConfig.saveBytes()) {
            text.append(sample.getBytes());
        }

        if (saveConfig.saveThreadCounts()) {
            text.append(sample.getGroupThreads());
            text.append(sample.getAllThreads());
        }
        if (saveConfig.saveUrl()) {
            text.append(sample.getURL());
        }

        if (saveConfig.saveFileName()) {
            text.append(sample.getResultFileName());
        }

        if (saveConfig.saveLatency()) {
            text.append(sample.getLatency());
        }

        if (saveConfig.saveEncoding()) {
            text.append(sample.getDataEncodingWithDefault());
        }

        if (saveConfig.saveSampleCount()) {// Need both sample and error count to be any use
            text.append(sample.getSampleCount());
            text.append(sample.getErrorCount());
        }

        if (saveConfig.saveHostname()) {
            text.append(event.getHostname());
        }

        for (int i=0; i < SampleEvent.getVarCount(); i++){
            text.append(event.getVarValue(i));
        }

        return text.toString();
    }

    // =================================== CSV quote/unquote handling ==============================

    /*
     * Private versions of what might eventually be part of Commons-CSV or Commons-Lang/Io...
     */

    /*
     * <p>
     * Returns a <code>String</code> value for a character-delimited column value
     * enclosed in the quote character, if required.
     * </p>
     *
     * <p>
     * If the value contains a special character,
     * then the String value is returned enclosed in the quote character.
     * </p>
     *
     * <p>
     * Any quote characters in the value are doubled up.
     * </p>
     *
     * <p>
     * If the value does not contain any special characters,
     * then the String value is returned unchanged.
     * </p>
     *
     * <p>
     * N.B. The list of special characters includes the quote character.
     * </p>
     *
     * @param input the input column String, may be null (without enclosing delimiters)
     * @param specialChars special characters; second one must be the quote character
     * @return the input String, enclosed in quote characters if the value contains a special character,
     * <code>null</code> for null string input
     */
    private static String quoteDelimiters(String input, char[] specialChars) {
        if (StringUtils.containsNone(input, specialChars)) {
            return input;
        }
        StringBuffer buffer = new StringBuffer(input.length() + 10);
        final char quote = specialChars[1];
        buffer.append(quote);
        for (int i = 0; i < input.length(); i++) {
            char c = input.charAt(i);
            if (c == quote) {
                buffer.append(quote); // double the quote char
            }
            buffer.append(c);
        }
        buffer.append(quote);
        return buffer.toString();
    }

    // State of the parser
    private static final int INITIAL=0, PLAIN = 1, QUOTED = 2, EMBEDDEDQUOTE = 3;
    public static final char QUOTING_CHAR = '"';
    /**
     * Reads from file and splits input into strings according to the delimiter,
     * taking note of quoted strings.
     * <p>
     * Handles DOS (CRLF), Unix (LF), and Mac (CR) line-endings equally.
     * <p>
     * N.B. a blank line is returned as a zero length array, whereas "" is returned
     * as an empty string. This is inconsistent.
     * @param infile input file  - must support mark(1)
     * @param delim delimiter (e.g. comma)
     * @return array of strings
     * @throws IOException also for unexpected quote characters
     */
    public static String[] csvReadFile(BufferedReader infile, char delim) throws IOException {
        int ch;
        int state = INITIAL;
        List list = new ArrayList();
        ByteArrayOutputStream baos = new ByteArrayOutputStream(200);
        boolean push = false;
        while(-1 != (ch=infile.read())){
            push = false;
            switch(state){
            case INITIAL:
                if (ch == QUOTING_CHAR){
                    state = QUOTED;
                } else if (isDelimOrEOL(delim, ch)) {
                    push = true;
                } else {
                    baos.write(ch);
                    state = PLAIN;
                }
                break;
            case PLAIN:
                if (ch == QUOTING_CHAR){
                    baos.write(ch);
                    throw new IOException("Cannot have quote-char in plain field:["+baos.toString()+"]");
                } else if (isDelimOrEOL(delim, ch)) {
                    push = true;
                    state = INITIAL;
                } else {
                    baos.write(ch);
                }
                break;
            case QUOTED:
                if (ch == QUOTING_CHAR){
                    state=EMBEDDEDQUOTE;
                } else {
                    baos.write(ch);
                }
                break;
            case EMBEDDEDQUOTE:
                if (ch == QUOTING_CHAR){
                    baos.write(QUOTING_CHAR); // doubled quote => quote
                    state = QUOTED;
                } else if (isDelimOrEOL(delim, ch)) {
                    push = true;
                    state = INITIAL;
                } else {
                    baos.write(QUOTING_CHAR);
                    throw new IOException("Cannot have single quote-char in quoted field:["+baos.toString()+"]");
                }
                break;
            } // switch(state)
            if (push) {
                if (ch == '\r') {// Remove following \n if present
                    infile.mark(1);
                    if (infile.read() != '\n'){
                        infile.reset(); // did not find \n, put the character back
                    }
                }
                String s = baos.toString();
                list.add(s);
                baos.reset();
            }
            if ((ch == '\n' || ch == '\r') && state != QUOTED) {
                break;
            }
        } // while not EOF
        if (ch == -1){// EOF (or end of string) so collect any remaining data
            if (state == QUOTED){
                throw new IOException(state+" Missing trailing quote-char in quoted field:[\""+baos.toString()+"]");
            }
            // Do we have some data, or a trailing empty field?
            if (baos.size() > 0 // we have some data
                    || push     // we've started a field
                    || state == EMBEDDEDQUOTE // Just seen ""
                ) {
                list.add(baos.toString());
            }
        }
        return (String[]) list.toArray(new String[list.size()]);
    }

    private static boolean isDelimOrEOL(char delim, int ch) {
        return ch == delim || ch == '\n' || ch == '\r';
    }

    /**
     * Reads from String and splits into strings according to the delimiter,
     * taking note of quoted strings.
     *
     * Handles DOS (CRLF), Unix (LF), and Mac (CR) line-endings equally.
     *
     * @param line input line
     * @param delim delimiter (e.g. comma)
     * @return array of strings
     * @throws IOException also for unexpected quote characters
     */
    public static String[] csvSplitString(String line, char delim) throws IOException {
        return csvReadFile(new BufferedReader(new StringReader(line)), delim);
    }
}
TOP

Related Classes of org.apache.jmeter.save.CSVSaveService

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.