001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: *
017: */
018:
019: package org.apache.jmeter.save;
020:
021: import java.io.FileWriter;
022: import java.io.IOException;
023: import java.text.DateFormat;
024: import java.text.ParseException;
025: import java.text.SimpleDateFormat;
026: import java.util.Date;
027: import java.util.Vector;
028:
029: import org.apache.commons.collections.map.LinkedMap;
030: import org.apache.jmeter.assertions.AssertionResult;
031: import org.apache.jmeter.samplers.SampleEvent;
032: import org.apache.jmeter.samplers.SampleResult;
033: import org.apache.jmeter.samplers.SampleSaveConfiguration;
034: import org.apache.jmeter.samplers.StatisticalSampleResult;
035: import org.apache.jmeter.util.JMeterUtils;
036: import org.apache.jorphan.logging.LoggingManager;
037: import org.apache.jorphan.reflect.Functor;
038: import org.apache.jorphan.util.JMeterError;
039: import org.apache.log.Logger;
040: import org.apache.oro.text.regex.Pattern;
041: import org.apache.oro.text.regex.PatternMatcherInput;
042: import org.apache.oro.text.regex.Perl5Compiler;
043: import org.apache.oro.text.regex.Perl5Matcher;
044:
045: /**
046: * This class provides a means for saving/reading test results as CSV files.
047: */
048: public final class CSVSaveService {
049: private static final Logger log = LoggingManager
050: .getLoggerForClass();
051:
052: // ---------------------------------------------------------------------
053: // XML RESULT FILE CONSTANTS AND FIELD NAME CONSTANTS
054: // ---------------------------------------------------------------------
055:
056: private static final String DATA_TYPE = "dataType"; // $NON-NLS-1$
057: private static final String FAILURE_MESSAGE = "failureMessage"; // $NON-NLS-1$
058: private static final String LABEL = "label"; // $NON-NLS-1$
059: private static final String RESPONSE_CODE = "responseCode"; // $NON-NLS-1$
060: private static final String RESPONSE_MESSAGE = "responseMessage"; // $NON-NLS-1$
061: private static final String SUCCESSFUL = "success"; // $NON-NLS-1$
062: private static final String THREAD_NAME = "threadName"; // $NON-NLS-1$
063: private static final String TIME_STAMP = "timeStamp"; // $NON-NLS-1$
064:
065: // ---------------------------------------------------------------------
066: // ADDITIONAL CSV RESULT FILE CONSTANTS AND FIELD NAME CONSTANTS
067: // ---------------------------------------------------------------------
068:
069: private static final String CSV_ELAPSED = "elapsed"; // $NON-NLS-1$
070: private static final String CSV_BYTES = "bytes"; // $NON-NLS-1$
071: private static final String CSV_THREAD_COUNT1 = "grpThreads"; // $NON-NLS-1$
072: private static final String CSV_THREAD_COUNT2 = "allThreads"; // $NON-NLS-1$
073: private static final String CSV_SAMPLE_COUNT = "SampleCount"; // $NON-NLS-1$
074: private static final String CSV_ERROR_COUNT = "ErrorCount"; // $NON-NLS-1$
075: private static final String CSV_URL = "URL"; // $NON-NLS-1$
076: private static final String CSV_FILENAME = "Filename"; // $NON-NLS-1$
077: private static final String CSV_LATENCY = "Latency"; // $NON-NLS-1$
078: private static final String CSV_ENCODING = "Encoding"; // $NON-NLS-1$
079: private static final String CSV_HOSTNAME = "Hostname"; // $NON-NLS-1$
080:
081: // Initial config from properties
082: static private final SampleSaveConfiguration _saveConfig = SampleSaveConfiguration
083: .staticConfig();
084:
085: // Date format to try if the time format does not parse as milliseconds
086: // (this is the suggested value in jmeter.properties)
087: private static final String DEFAULT_DATE_FORMAT_STRING = "MM/dd/yy HH:mm:ss"; // $NON-NLS-1$
088: private static final DateFormat DEFAULT_DATE_FORMAT = new SimpleDateFormat(
089: DEFAULT_DATE_FORMAT_STRING);
090:
091: /**
092: * Private constructor to prevent instantiation.
093: */
094: private CSVSaveService() {
095: }
096:
097: /**
098: * Make a SampleResult given a delimited string.
099: *
100: * @param inputLine - line from CSV file
101: * @param saveConfig - configuration
102: * @param lineNumber - line number for error reporting
103: * @return SampleResult or null if header line detected
104: *
105: * @throws JMeterError
106: */
107: public static SampleEvent makeResultFromDelimitedString(
108: final String inputLine,
109: final SampleSaveConfiguration saveConfig, // may be updated
110: final long lineNumber) {
111:
112: SampleResult result = null;
113: String hostname = "";// $NON-NLS-1$
114: long timeStamp = 0;
115: long elapsed = 0;
116: /*
117: * Bug 40772: replaced StringTokenizer with String.split(), as the
118: * former does not return empty tokens.
119: */
120: // The \Q prefix is needed to ensure that meta-characters (e.g. ".") work.
121: String parts[] = inputLine.split("\\Q"
122: + saveConfig.getDelimiter());// $NON-NLS-1$
123: String text = null;
124: String field = null; // Save the name for error reporting
125: int i = 0;
126:
127: try {
128: if (saveConfig.saveTimestamp()) {
129: field = TIME_STAMP;
130: text = parts[i++];
131: if (saveConfig.printMilliseconds()) {
132: try {
133: timeStamp = Long.parseLong(text);
134: } catch (NumberFormatException e) {// see if this works
135: log.warn(e.toString());
136: Date stamp = DEFAULT_DATE_FORMAT.parse(text);
137: timeStamp = stamp.getTime();
138: log.warn("Setting date format to: "
139: + DEFAULT_DATE_FORMAT_STRING);
140: saveConfig.setFormatter(DEFAULT_DATE_FORMAT);
141: }
142: } else if (saveConfig.formatter() != null) {
143: Date stamp = saveConfig.formatter().parse(text);
144: timeStamp = stamp.getTime();
145: } else { // can this happen?
146: final String msg = "Unknown timestamp format";
147: log.warn(msg);
148: throw new JMeterError(msg);
149: }
150: }
151:
152: if (saveConfig.saveTime()) {
153: field = CSV_ELAPSED;
154: text = parts[i++];
155: elapsed = Long.parseLong(text);
156: }
157:
158: if (saveConfig.saveSampleCount()) {
159: result = new StatisticalSampleResult(timeStamp, elapsed);
160: } else {
161: result = new SampleResult(timeStamp, elapsed);
162: }
163:
164: if (saveConfig.saveLabel()) {
165: field = LABEL;
166: text = parts[i++];
167: result.setSampleLabel(text);
168: }
169: if (saveConfig.saveCode()) {
170: field = RESPONSE_CODE;
171: text = parts[i++];
172: result.setResponseCode(text);
173: }
174:
175: if (saveConfig.saveMessage()) {
176: field = RESPONSE_MESSAGE;
177: text = parts[i++];
178: result.setResponseMessage(text);
179: }
180:
181: if (saveConfig.saveThreadName()) {
182: field = THREAD_NAME;
183: text = parts[i++];
184: result.setThreadName(text);
185: }
186:
187: if (saveConfig.saveDataType()) {
188: field = DATA_TYPE;
189: text = parts[i++];
190: result.setDataType(text);
191: }
192:
193: if (saveConfig.saveSuccess()) {
194: field = SUCCESSFUL;
195: text = parts[i++];
196: result.setSuccessful(Boolean.valueOf(text)
197: .booleanValue());
198: }
199:
200: if (saveConfig.saveAssertionResultsFailureMessage()) {
201: i++;
202: // TODO - should this be restored?
203: }
204:
205: if (saveConfig.saveBytes()) {
206: field = CSV_BYTES;
207: text = parts[i++];
208: result.setBytes(Integer.parseInt(text));
209: }
210:
211: if (saveConfig.saveThreadCounts()) {
212: field = CSV_THREAD_COUNT1;
213: text = parts[i++];
214: result.setGroupThreads(Integer.parseInt(text));
215:
216: field = CSV_THREAD_COUNT2;
217: text = parts[i++];
218: result.setAllThreads(Integer.parseInt(text));
219: }
220:
221: if (saveConfig.saveUrl()) {
222: i++;
223: // TODO: should this be restored?
224: }
225:
226: if (saveConfig.saveFileName()) {
227: field = CSV_FILENAME;
228: text = parts[i++];
229: result.setResultFileName(text);
230: }
231: if (saveConfig.saveLatency()) {
232: field = CSV_LATENCY;
233: text = parts[i++];
234: result.setLatency(Long.parseLong(text));
235: }
236:
237: if (saveConfig.saveEncoding()) {
238: field = CSV_ENCODING;
239: text = parts[i++];
240: result.setEncodingAndType(text);
241: }
242:
243: if (saveConfig.saveSampleCount()) {
244: field = CSV_SAMPLE_COUNT;
245: text = parts[i++];
246: result.setSampleCount(Integer.parseInt(text));
247: field = CSV_ERROR_COUNT;
248: text = parts[i++];
249: result.setErrorCount(Integer.parseInt(text));
250: }
251:
252: if (saveConfig.saveHostname()) {
253: field = CSV_HOSTNAME;
254: hostname = parts[i++];
255: }
256:
257: } catch (NumberFormatException e) {
258: log.warn("Error parsing field '" + field + "' at line "
259: + lineNumber + " " + e);
260: throw new JMeterError(e);
261: } catch (ParseException e) {
262: log.warn("Error parsing field '" + field + "' at line "
263: + lineNumber + " " + e);
264: throw new JMeterError(e);
265: } catch (ArrayIndexOutOfBoundsException e) {
266: log.warn("Insufficient columns to parse field '" + field
267: + "' at line " + lineNumber);
268: throw new JMeterError(e);
269: }
270: return new SampleEvent(result, "", hostname);
271: }
272:
273: /**
274: * Generates the field names for the output file
275: *
276: * @return the field names as a string
277: */
278: public static String printableFieldNamesToString() {
279: return printableFieldNamesToString(_saveConfig);
280: }
281:
282: /**
283: * Generates the field names for the output file
284: *
285: * @return the field names as a string
286: */
287: public static String printableFieldNamesToString(
288: SampleSaveConfiguration saveConfig) {
289: StringBuffer text = new StringBuffer();
290: String delim = saveConfig.getDelimiter();
291:
292: if (saveConfig.saveTimestamp()) {
293: text.append(TIME_STAMP);
294: text.append(delim);
295: }
296:
297: if (saveConfig.saveTime()) {
298: text.append(CSV_ELAPSED);
299: text.append(delim);
300: }
301:
302: if (saveConfig.saveLabel()) {
303: text.append(LABEL);
304: text.append(delim);
305: }
306:
307: if (saveConfig.saveCode()) {
308: text.append(RESPONSE_CODE);
309: text.append(delim);
310: }
311:
312: if (saveConfig.saveMessage()) {
313: text.append(RESPONSE_MESSAGE);
314: text.append(delim);
315: }
316:
317: if (saveConfig.saveThreadName()) {
318: text.append(THREAD_NAME);
319: text.append(delim);
320: }
321:
322: if (saveConfig.saveDataType()) {
323: text.append(DATA_TYPE);
324: text.append(delim);
325: }
326:
327: if (saveConfig.saveSuccess()) {
328: text.append(SUCCESSFUL);
329: text.append(delim);
330: }
331:
332: if (saveConfig.saveAssertionResultsFailureMessage()) {
333: text.append(FAILURE_MESSAGE);
334: text.append(delim);
335: }
336:
337: if (saveConfig.saveBytes()) {
338: text.append(CSV_BYTES);
339: text.append(delim);
340: }
341:
342: if (saveConfig.saveThreadCounts()) {
343: text.append(CSV_THREAD_COUNT1);
344: text.append(delim);
345: text.append(CSV_THREAD_COUNT2);
346: text.append(delim);
347: }
348:
349: if (saveConfig.saveUrl()) {
350: text.append(CSV_URL);
351: text.append(delim);
352: }
353:
354: if (saveConfig.saveFileName()) {
355: text.append(CSV_FILENAME);
356: text.append(delim);
357: }
358:
359: if (saveConfig.saveLatency()) {
360: text.append(CSV_LATENCY);
361: text.append(delim);
362: }
363:
364: if (saveConfig.saveEncoding()) {
365: text.append(CSV_ENCODING);
366: text.append(delim);
367: }
368:
369: if (saveConfig.saveSampleCount()) {
370: text.append(CSV_SAMPLE_COUNT);
371: text.append(delim);
372: text.append(CSV_ERROR_COUNT);
373: text.append(delim);
374: }
375:
376: if (saveConfig.saveHostname()) {
377: text.append(CSV_HOSTNAME);
378: text.append(delim);
379: }
380:
381: String resultString = null;
382: int size = text.length();
383: int delSize = delim.length();
384:
385: // Strip off the trailing delimiter
386: if (size >= delSize) {
387: resultString = text.substring(0, size - delSize);
388: } else {
389: resultString = text.toString();
390: }
391: return resultString;
392: }
393:
394: // Map header names to set() methods
395: private static final LinkedMap headerLabelMethods = new LinkedMap();
396:
397: // These entries must be in the same order as columns are saved/restored.
398:
399: static {
400: headerLabelMethods.put(TIME_STAMP, new Functor("setTimestamp"));
401: headerLabelMethods.put(CSV_ELAPSED, new Functor("setTime"));
402: headerLabelMethods.put(LABEL, new Functor("setLabel"));
403: headerLabelMethods.put(RESPONSE_CODE, new Functor("setCode"));
404: headerLabelMethods.put(RESPONSE_MESSAGE, new Functor(
405: "setMessage"));
406: headerLabelMethods.put(THREAD_NAME,
407: new Functor("setThreadName"));
408: headerLabelMethods.put(DATA_TYPE, new Functor("setDataType"));
409: headerLabelMethods.put(SUCCESSFUL, new Functor("setSuccess"));
410: headerLabelMethods.put(FAILURE_MESSAGE, new Functor(
411: "setAssertionResultsFailureMessage"));
412: headerLabelMethods.put(CSV_BYTES, new Functor("setBytes"));
413: // Both these are needed in the list even though they set the same variable
414: headerLabelMethods.put(CSV_THREAD_COUNT1, new Functor(
415: "setThreadCounts"));
416: headerLabelMethods.put(CSV_THREAD_COUNT2, new Functor(
417: "setThreadCounts"));
418: headerLabelMethods.put(CSV_URL, new Functor("setUrl"));
419: headerLabelMethods
420: .put(CSV_FILENAME, new Functor("setFileName"));
421: headerLabelMethods.put(CSV_LATENCY, new Functor("setLatency"));
422: headerLabelMethods
423: .put(CSV_ENCODING, new Functor("setEncoding"));
424: // Both these are needed in the list even though they set the same variable
425: headerLabelMethods.put(CSV_SAMPLE_COUNT, new Functor(
426: "setSampleCount"));
427: headerLabelMethods.put(CSV_ERROR_COUNT, new Functor(
428: "setSampleCount"));
429: headerLabelMethods
430: .put(CSV_HOSTNAME, new Functor("setHostname"));
431: }
432:
433: /**
434: * Parse a CSV header line
435: * @param headerLine from CSV file
436: * @param filename name of file (for log message only)
437: * @return config corresponding to the header items found or null if not a header line
438: */
439: public static SampleSaveConfiguration getSampleSaveConfiguration(
440: String headerLine, String filename) {
441: String[] parts = splitHeader(headerLine, _saveConfig
442: .getDelimiter()); // Try default delimiter
443:
444: String delim = null;
445:
446: if (parts == null) {
447: Perl5Matcher matcher = JMeterUtils.getMatcher();
448: PatternMatcherInput input = new PatternMatcherInput(
449: headerLine);
450: Pattern pattern = JMeterUtils.getPatternCache()
451: // This assumes the header names are all single words with no spaces
452: // word followed by 0 or more repeats of (non-word char + word)
453: // where the non-word char (\2) is the same
454: // e.g. abc|def|ghi but not abd|def~ghi
455: .getPattern("\\w+((\\W)\\w+)?(\\2\\w+)*", // $NON-NLS-1$
456: Perl5Compiler.READ_ONLY_MASK);
457: if (matcher.matches(input, pattern)) {
458: delim = matcher.getMatch().group(2);
459: parts = splitHeader(headerLine, delim);// now validate the result
460: }
461: }
462:
463: if (parts == null) {
464: return null; // failed to recognise the header
465: }
466:
467: // We know the column names all exist, so create the config
468: SampleSaveConfiguration saveConfig = new SampleSaveConfiguration(
469: false);
470:
471: for (int i = 0; i < parts.length; i++) {
472: Functor set = (Functor) headerLabelMethods.get(parts[i]);
473: set.invoke(saveConfig, new Boolean[] { Boolean.TRUE });
474: }
475:
476: if (delim != null) {
477: log.warn("Default delimiter '" + _saveConfig.getDelimiter()
478: + "' did not work; using alternate '" + delim
479: + "' for reading " + filename);
480: saveConfig.setDelimiter(delim);
481: }
482: return saveConfig;
483: }
484:
485: private static String[] splitHeader(String headerLine, String delim) {
486: String parts[] = headerLine.split("\\Q" + delim);// $NON-NLS-1$
487: int previous = -1;
488: // Check if the line is a header
489: for (int i = 0; i < parts.length; i++) {
490: final String label = parts[i];
491: int current = headerLabelMethods.indexOf(label);
492: if (current == -1) {
493: return null; // unknown column name
494: }
495: if (current <= previous) {
496: log.warn("Column header number " + (i + 1) + " name "
497: + label + " is out of order.");
498: return null; // out of order
499: }
500: previous = current;
501: }
502: return parts;
503: }
504:
505: /**
506: * Method will save aggregate statistics as CSV. For now I put it here.
507: * Not sure if it should go in the newer SaveService instead of here.
508: * if we ever decide to get rid of this class, we'll need to move this
509: * method to the new save service.
510: * @param data
511: * @param writer
512: * @throws IOException
513: */
514: public static void saveCSVStats(Vector data, FileWriter writer)
515: throws IOException {
516: for (int idx = 0; idx < data.size(); idx++) {
517: Vector row = (Vector) data.elementAt(idx);
518: for (int idy = 0; idy < row.size(); idy++) {
519: if (idy > 0) {
520: writer.write(","); // $NON-NLS-1$
521: }
522: Object item = row.elementAt(idy);
523: writer.write(String.valueOf(item));
524: }
525: writer.write(System.getProperty("line.separator")); // $NON-NLS-1$
526: }
527: }
528:
529: /**
530: * Convert a result into a string, where the fields of the result are
531: * separated by the default delimiter.
532: *
533: * @param event
534: * the sample event to be converted
535: * @return the separated value representation of the result
536: */
537: public static String resultToDelimitedString(SampleEvent event) {
538: return resultToDelimitedString(event, event.getResult()
539: .getSaveConfig().getDelimiter());
540: }
541:
542: /**
543: * Convert a result into a string, where the fields of the result are
544: * separated by a specified String.
545: *
546: * @param event
547: * the sample event to be converted
548: * @param delimiter
549: * the separation string
550: * @return the separated value representation of the result
551: */
552: public static String resultToDelimitedString(SampleEvent event,
553: String delimiter) {
554: StringBuffer text = new StringBuffer();
555: SampleResult sample = event.getResult();
556: SampleSaveConfiguration saveConfig = sample.getSaveConfig();
557:
558: if (saveConfig.saveTimestamp()) {
559: if (saveConfig.printMilliseconds()) {
560: text.append(sample.getTimeStamp());
561: text.append(delimiter);
562: } else if (saveConfig.formatter() != null) {
563: String stamp = saveConfig.formatter().format(
564: new Date(sample.getTimeStamp()));
565: text.append(stamp);
566: text.append(delimiter);
567: }
568: }
569:
570: if (saveConfig.saveTime()) {
571: text.append(sample.getTime());
572: text.append(delimiter);
573: }
574:
575: if (saveConfig.saveLabel()) {
576: text.append(sample.getSampleLabel());
577: text.append(delimiter);
578: }
579:
580: if (saveConfig.saveCode()) {
581: text.append(sample.getResponseCode());
582: text.append(delimiter);
583: }
584:
585: if (saveConfig.saveMessage()) {
586: text.append(sample.getResponseMessage());
587: text.append(delimiter);
588: }
589:
590: if (saveConfig.saveThreadName()) {
591: text.append(sample.getThreadName());
592: text.append(delimiter);
593: }
594:
595: if (saveConfig.saveDataType()) {
596: text.append(sample.getDataType());
597: text.append(delimiter);
598: }
599:
600: if (saveConfig.saveSuccess()) {
601: text.append(sample.isSuccessful());
602: text.append(delimiter);
603: }
604:
605: if (saveConfig.saveAssertionResultsFailureMessage()) {
606: String message = null;
607: AssertionResult[] results = sample.getAssertionResults();
608:
609: if (results != null) {
610: // Find the first non-null message
611: for (int i = 0; i < results.length; i++) {
612: message = results[i].getFailureMessage();
613: if (message != null)
614: break;
615: }
616: }
617:
618: if (message != null) {
619: text.append(message);
620: }
621: text.append(delimiter);
622: }
623:
624: if (saveConfig.saveBytes()) {
625: text.append(sample.getBytes());
626: text.append(delimiter);
627: }
628:
629: if (saveConfig.saveThreadCounts()) {
630: text.append(sample.getGroupThreads());
631: text.append(delimiter);
632: text.append(sample.getAllThreads());
633: text.append(delimiter);
634: }
635: if (saveConfig.saveUrl()) {
636: text.append(sample.getURL());
637: text.append(delimiter);
638: }
639:
640: if (saveConfig.saveFileName()) {
641: text.append(sample.getResultFileName());
642: text.append(delimiter);
643: }
644:
645: if (saveConfig.saveLatency()) {
646: text.append(sample.getLatency());
647: text.append(delimiter);
648: }
649:
650: if (saveConfig.saveEncoding()) {
651: text.append(sample.getDataEncoding());
652: text.append(delimiter);
653: }
654:
655: if (saveConfig.saveSampleCount()) {// Need both sample and error count to be any use
656: text.append(sample.getSampleCount());
657: text.append(delimiter);
658: text.append(sample.getErrorCount());
659: text.append(delimiter);
660: }
661:
662: if (saveConfig.saveHostname()) {
663: text.append(event.getHostname());
664: text.append(delimiter);
665: }
666:
667: String resultString = null;
668: int size = text.length();
669: int delSize = delimiter.length();
670:
671: // Strip off the trailing delimiter
672: if (size >= delSize) {
673: resultString = text.substring(0, size - delSize);
674: } else {
675: resultString = text.toString();
676: }
677: return resultString;
678: }
679: }
|