/*
* 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.CharArrayWriter;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.swing.table.DefaultTableModel;
import org.apache.commons.collections.map.LinkedMap;
import org.apache.commons.lang3.CharUtils;
import org.apache.commons.lang3.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$
private static final String CSV_IDLETIME = "IdleTime"; // $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 formats to try if the time format does not parse as milliseconds
private static final String DATE_FORMAT_STRINGS[] = {
"yyyy/MM/dd HH:mm:ss.SSS", // $NON-NLS-1$
"yyyy/MM/dd HH:mm:ss", // $NON-NLS-1$
"yyyy-MM-dd HH:mm:ss.SSS", // $NON-NLS-1$
"yyyy-MM-dd HH:mm:ss", // $NON-NLS-1$
"MM/dd/yy HH:mm:ss" // $NON-NLS-1$ (for compatibility, this is the original default)
};
private static final String LINE_SEP = System.getProperty("line.separator"); // $NON-NLS-1$
/**
* 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 InputStreamReader(
new FileInputStream(filename), SaveService.getFileEncoding("UTF-8")));
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 set of tokens
*
* @param parts
* tokens parsed from the input
* @param saveConfig
* the save configuration (may be updated)
* @param lineNumber the line number (for error reporting)
* @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); // see if this works
} catch (NumberFormatException e) { // it did not, let's try some other formats
log.warn(e.toString());
boolean foundMatch = false;
for(String fmt : DATE_FORMAT_STRINGS) {
SimpleDateFormat dateFormat = new SimpleDateFormat(fmt);
dateFormat.setLenient(false);
try {
Date stamp = dateFormat.parse(text);
timeStamp = stamp.getTime();
// method is only ever called from one thread at a time
// so it's OK to use a static DateFormat
log.warn("Setting date format to: " + fmt);
saveConfig.setFormatter(dateFormat);
foundMatch = true;
break;
} catch (ParseException e1) {
log.info(text+" did not match "+fmt);
}
}
if (!foundMatch) {
throw new ParseException("No date-time format found matching "+text,-1);
}
}
} 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 (saveConfig.saveIdleTime()) {
field = CSV_IDLETIME;
text = parts[i++];
result.setIdleTime(Long.parseLong(text));
}
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) {
StringBuilder text = new StringBuilder();
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);
}
if (saveConfig.saveIdleTime()) {
text.append(CSV_IDLETIME);
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"));
headerLabelMethods.put(CSV_IDLETIME, new Functor("setIdleTime"));
}
/**
* 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
* List of data rows
* @param writer
* output file
* @throws IOException
*/
public static void saveCSVStats(List<?> 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
* List of data rows
* @param writer
* output file
* @param headers
* header names (if non-null)
* @throws IOException
*/
public static void saveCSVStats(List<?> data, FileWriter writer,
String headers[]) throws IOException {
final char DELIM = ',';
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++) {
List<?> row = (List<?>) data.get(idx);
for (int idy = 0; idy < row.size(); idy++) {
if (idy > 0) {
writer.write(DELIM);
}
Object item = row.get(idy);
writer.write(quoteDelimiters(String.valueOf(item), SPECIALS));
}
writer.write(LINE_SEP);
}
}
/**
* Method saves aggregate statistics (with header names) as CSV from a table
* model. Same as {@link #saveCSVStats(List, FileWriter, String[])} except
* that there is no need to create a List 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 {
saveCSVStats(model, writer, true);
}
/**
* Method saves aggregate statistics as CSV from a table model. Same as
* {@link #saveCSVStats(List, FileWriter, String[])} except that there is
* no need to create a List containing the data.
*
* @param model
* table model containing the data
* @param writer
* output file
* @param saveHeaders
* whether or not to save headers
* @throws IOException
*/
public static void saveCSVStats(DefaultTableModel model, FileWriter writer,
boolean saveHeaders) throws IOException {
final char DELIM = ',';
final char SPECIALS[] = new char[] { DELIM, QUOTING_CHAR };
final int columns = model.getColumnCount();
final int rows = model.getRowCount();
if (saveHeaders) {
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());
}
/*
* Class to handle generating the delimited string. - adds the delimiter
* if not the first call - quotes any strings that require it
*/
static final class StringQuoter {
private final StringBuilder sb;
private final char[] specials;
private boolean addDelim;
public StringQuoter(char delim) {
sb = new StringBuilder(150);
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);
}
@Override
public String toString() {
return sb.toString();
}
}
/**
* 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) {
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());
}
if (saveConfig.saveIdleTime()) {
text.append(event.getResult().getIdleTime());
}
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;
}
StringBuilder buffer = new StringBuilder(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>
* A blank line - or a quoted blank line - both return an array containing
* a single empty String.
* @param infile
* input file - must support mark(1)
* @param delim
* delimiter (e.g. comma)
* @return array of strings, will be empty if there is no data, i.e. if the input is at EOF.
* @throws IOException
* also for unexpected quote characters
*/
public static String[] csvReadFile(BufferedReader infile, char delim)
throws IOException {
int ch;
int state = INITIAL;
List<String> list = new ArrayList<String>();
CharArrayWriter baos = new CharArrayWriter(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("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 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);
}
}