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.reporters;
020:
021: import java.io.BufferedInputStream;
022: import java.io.BufferedOutputStream;
023: import java.io.BufferedReader;
024: import java.io.File;
025: import java.io.FileInputStream;
026: import java.io.FileNotFoundException;
027: import java.io.FileOutputStream;
028: import java.io.FileReader;
029: import java.io.IOException;
030: import java.io.OutputStreamWriter;
031: import java.io.PrintWriter;
032: import java.io.RandomAccessFile;
033: import java.io.Serializable;
034: import java.util.Collection;
035: import java.util.HashMap;
036: import java.util.HashSet;
037: import java.util.Iterator;
038: import java.util.Map;
039: import java.util.Set;
040:
041: import org.apache.avalon.framework.configuration.DefaultConfigurationSerializer;
042: import org.apache.jmeter.engine.event.LoopIterationEvent;
043: import org.apache.jmeter.engine.util.NoThreadClone;
044: import org.apache.jmeter.gui.GuiPackage;
045: import org.apache.jmeter.samplers.Clearable;
046: import org.apache.jmeter.samplers.Remoteable;
047: import org.apache.jmeter.samplers.SampleEvent;
048: import org.apache.jmeter.samplers.SampleListener;
049: import org.apache.jmeter.samplers.SampleResult;
050: import org.apache.jmeter.samplers.SampleSaveConfiguration;
051: import org.apache.jmeter.save.CSVSaveService;
052: import org.apache.jmeter.save.OldSaveService;
053: import org.apache.jmeter.save.SaveService;
054: import org.apache.jmeter.save.TestResultWrapper;
055: import org.apache.jmeter.testelement.TestElement;
056: import org.apache.jmeter.testelement.TestListener;
057: import org.apache.jmeter.testelement.property.BooleanProperty;
058: import org.apache.jmeter.testelement.property.ObjectProperty;
059: import org.apache.jmeter.visualizers.Visualizer;
060: import org.apache.jorphan.logging.LoggingManager;
061: import org.apache.jorphan.util.JMeterError;
062: import org.apache.jorphan.util.JOrphanUtils;
063: import org.apache.log.Logger;
064:
065: public class ResultCollector extends AbstractListenerElement implements
066: SampleListener, Clearable, Serializable, TestListener,
067: Remoteable, NoThreadClone {
068:
069: private static final Logger log = LoggingManager
070: .getLoggerForClass();
071:
072: private static final long serialVersionUID = 231L;
073:
074: // This string is used to identify local test runs, so must not be a valid host name
075: private static final String TEST_IS_LOCAL = "*local*"; // $NON-NLS-1$
076:
077: private static final String TESTRESULTS_START = "<testResults>"; // $NON-NLS-1$
078:
079: private static final String TESTRESULTS_START_V1_1_PREVER = "<testResults version=\""; // $NON-NLS-1$
080: private static final String TESTRESULTS_START_V1_1_POSTVER = "\">"; // $NON-NLS-1$
081:
082: private static final String TESTRESULTS_END = "</testResults>"; // $NON-NLS-1$
083:
084: private static final String XML_HEADER = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"; // $NON-NLS-1$
085:
086: private static final int MIN_XML_FILE_LEN = XML_HEADER.length()
087: + TESTRESULTS_START.length() + TESTRESULTS_END.length();
088:
089: public final static String FILENAME = "filename"; // $NON-NLS-1$
090:
091: private final static String SAVE_CONFIG = "saveConfig"; // $NON-NLS-1$
092:
093: private static final String ERROR_LOGGING = "ResultCollector.error_logging"; // $NON-NLS-1$
094:
095: private static final String SUCCESS_ONLY_LOGGING = "ResultCollector.success_only_logging"; // $NON-NLS-1$
096:
097: transient private DefaultConfigurationSerializer serializer;
098:
099: transient private volatile PrintWriter out;
100:
101: private boolean inTest = false;
102:
103: private static Map files = new HashMap();
104:
105: private Set hosts = new HashSet();
106:
107: protected boolean isStats = false;
108:
109: /**
110: * No-arg constructor.
111: */
112: public ResultCollector() {
113: // current = -1;
114: // serializer = new DefaultConfigurationSerializer();
115: setErrorLogging(false);
116: setSuccessOnlyLogging(false);
117: setProperty(new ObjectProperty(SAVE_CONFIG,
118: new SampleSaveConfiguration()));
119: }
120:
121: // Ensure that the sample save config is not shared between copied nodes
122: public Object clone() {
123: ResultCollector clone = (ResultCollector) super .clone();
124: clone.setSaveConfig((SampleSaveConfiguration) clone
125: .getSaveConfig().clone());
126: return clone;
127: }
128:
129: private void setFilenameProperty(String f) {
130: setProperty(FILENAME, f);
131: }
132:
133: public String getFilename() {
134: return getPropertyAsString(FILENAME);
135: }
136:
137: public boolean isErrorLogging() {
138: return getPropertyAsBoolean(ERROR_LOGGING);
139: }
140:
141: public void setErrorLogging(boolean errorLogging) {
142: setProperty(new BooleanProperty(ERROR_LOGGING, errorLogging));
143: }
144:
145: public void setSuccessOnlyLogging(boolean value) {
146: if (value) {
147: setProperty(new BooleanProperty(SUCCESS_ONLY_LOGGING, true));
148: } else {
149: removeProperty(SUCCESS_ONLY_LOGGING);
150: }
151: }
152:
153: public boolean isSuccessOnlyLogging() {
154: return getPropertyAsBoolean(SUCCESS_ONLY_LOGGING, false);
155: }
156:
157: /**
158: * Decides whether or not to a sample is wanted based on:
159: * - errorOnly
160: * - successOnly
161: * - sample success
162: *
163: * @param success is sample successful
164: * @return whether to log/display the sample
165: */
166: public boolean isSampleWanted(boolean success) {
167: boolean errorOnly = isErrorLogging();
168: boolean successOnly = isSuccessOnlyLogging();
169: return (!errorOnly && !successOnly) || (success && successOnly)
170: || (!success && errorOnly);
171: // successOnly and errorOnly cannot both be set
172: }
173:
174: /**
175: * Sets the filename attribute of the ResultCollector object.
176: *
177: * @param f
178: * the new filename value
179: */
180: public void setFilename(String f) {
181: if (inTest) {
182: return;
183: }
184: setFilenameProperty(f);
185: }
186:
187: public void testEnded(String host) {
188: hosts.remove(host);
189: if (hosts.size() == 0) {
190: finalizeFileOutput();
191: inTest = false;
192: }
193: }
194:
195: public void testStarted(String host) {
196: hosts.add(host);
197: try {
198: initializeFileOutput();
199: if (getVisualizer() != null) {
200: this .isStats = getVisualizer().isStats();
201: }
202: } catch (Exception e) {
203: log.error("", e);
204: }
205: inTest = true;
206: }
207:
208: public void testEnded() {
209: testEnded(TEST_IS_LOCAL);
210: }
211:
212: public void testStarted() {
213: testStarted(TEST_IS_LOCAL);
214: }
215:
216: /**
217: * Loads an existing sample data (JTL) file.
218: * This can be one of:
219: * - XStream format
220: * - Avalon format
221: * - CSV format
222: *
223: */
224: public void loadExistingFile() {
225: final Visualizer visualizer = getVisualizer();
226: if (visualizer == null) {
227: return; // No point reading the file if there's no visualiser
228: }
229: boolean parsedOK = false, errorDetected = false;
230: String filename = getFilename();
231: File file = new File(filename);
232: if (file.exists()) {
233: clearVisualizer();
234: BufferedReader dataReader = null;
235: BufferedInputStream bufferedInputStream = null;
236: try {
237: dataReader = new BufferedReader(new FileReader(file));
238: // Get the first line, and see if it is XML
239: String line = dataReader.readLine();
240: if (line == null) {
241: log.warn(filename + " is empty");
242: } else {
243: if (!line.startsWith("<?xml ")) {// No, must be CSV //$NON-NLS-1$
244: long lineNumber = 1;
245: SampleSaveConfiguration saveConfig = CSVSaveService
246: .getSampleSaveConfiguration(line,
247: filename);
248: if (saveConfig == null) {// not a valid header
249: saveConfig = (SampleSaveConfiguration) getSaveConfig()
250: .clone(); // CSVSaveService may change the format
251: } else { // header line has been processed, so read the next
252: line = dataReader.readLine();
253: lineNumber++;
254: }
255: while (line != null) { // Already read 1st line
256: SampleEvent event = CSVSaveService
257: .makeResultFromDelimitedString(
258: line, saveConfig,
259: lineNumber);
260: if (event != null) {
261: final SampleResult result = event
262: .getResult();
263: if (isSampleWanted(result
264: .isSuccessful())) {
265: visualizer.add(result);
266: }
267: }
268: line = dataReader.readLine();
269: lineNumber++;
270: }
271: parsedOK = true;
272: } else { // We are processing XML
273: try { // Assume XStream
274: bufferedInputStream = new BufferedInputStream(
275: new FileInputStream(file));
276: readSamples(
277: SaveService
278: .loadTestResults(bufferedInputStream),
279: visualizer);
280: parsedOK = true;
281: } catch (Exception e) {
282: log
283: .info("Failed to load "
284: + filename
285: + " using XStream, trying old XML format. Error was: "
286: + e);
287: try {
288: OldSaveService.processSamples(filename,
289: visualizer, this );
290: parsedOK = true;
291: } catch (Exception e1) {
292: log.warn("Error parsing Avalon XML. "
293: + e1.getLocalizedMessage());
294: }
295: }
296: }
297: }
298: } catch (IOException e) {
299: log.warn("Problem reading JTL file: " + file);
300: } catch (JMeterError e) {
301: log.warn("Problem reading JTL file: " + file);
302: } finally {
303: JOrphanUtils.closeQuietly(dataReader);
304: JOrphanUtils.closeQuietly(bufferedInputStream);
305: if (!parsedOK || errorDetected) {
306: GuiPackage
307: .showErrorMessage(
308: "Error loading results file - see log file",
309: "Result file loader");
310: }
311: }
312: } else {
313: GuiPackage.showErrorMessage(
314: "Error loading results file - could not open file",
315: "Result file loader");
316: }
317: }
318:
319: private static void writeFileStart(PrintWriter writer,
320: SampleSaveConfiguration saveConfig) {
321: if (saveConfig.saveAsXml()) {
322: writer.print(XML_HEADER);
323: // Write the EOL separately so we generate LF line ends on Unix and Windows
324: writer.print("\n"); // $NON-NLS-1$
325: String pi = saveConfig.getXmlPi();
326: if (pi.length() > 0) {
327: writer.println(pi);
328: }
329: // Can't do it as a static initialisation, because SaveService
330: // is being constructed when this is called
331: writer.print(TESTRESULTS_START_V1_1_PREVER);
332: writer.print(SaveService.getVERSION());
333: writer.print(TESTRESULTS_START_V1_1_POSTVER);
334: // Write the EOL separately so we generate LF line ends on Unix and Windows
335: writer.print("\n"); // $NON-NLS-1$
336: } else if (saveConfig.saveFieldNames()) {
337: writer.println(CSVSaveService
338: .printableFieldNamesToString(saveConfig));
339: }
340: }
341:
342: private static void writeFileEnd(PrintWriter pw,
343: SampleSaveConfiguration saveConfig) {
344: if (saveConfig.saveAsXml()) {
345: pw.print("\n"); // $NON-NLS-1$
346: pw.print(TESTRESULTS_END);
347: pw.print("\n");// Added in version 1.1 // $NON-NLS-1$
348: }
349: }
350:
351: private static synchronized PrintWriter getFileWriter(
352: String filename, SampleSaveConfiguration saveConfig)
353: throws IOException {
354: if (filename == null || filename.length() == 0) {
355: return null;
356: }
357: PrintWriter writer = (PrintWriter) files.get(filename);
358: boolean trimmed = true;
359:
360: if (writer == null) {
361: if (saveConfig.saveAsXml()) {
362: trimmed = trimLastLine(filename);
363: } else {
364: trimmed = new File(filename).exists();
365: }
366: // Find the name of the directory containing the file
367: // and create it - if there is one
368: File pdir = new File(filename).getParentFile();
369: if (pdir != null)
370: pdir.mkdirs();
371: writer = new PrintWriter(new OutputStreamWriter(
372: new BufferedOutputStream(new FileOutputStream(
373: filename, trimmed)), SaveService
374: .getFileEncoding("UTF-8")), true); // $NON-NLS-1$
375: files.put(filename, writer);
376: }
377: if (!trimmed) {
378: writeFileStart(writer, saveConfig);
379: }
380: return writer;
381: }
382:
383: // returns false if the file did not contain the terminator
384: private static boolean trimLastLine(String filename) {
385: RandomAccessFile raf = null;
386: try {
387: raf = new RandomAccessFile(filename, "rw"); // $NON-NLS-1$
388: long len = raf.length();
389: if (len < MIN_XML_FILE_LEN) {
390: return false;
391: }
392: raf.seek(len - TESTRESULTS_END.length() - 10);// TODO: may not work on all OSes?
393: String line;
394: long pos = raf.getFilePointer();
395: int end = 0;
396: while ((line = raf.readLine()) != null)// reads to end of line OR end of file
397: {
398: end = line.indexOf(TESTRESULTS_END);
399: if (end >= 0) // found the string
400: {
401: break;
402: }
403: pos = raf.getFilePointer();
404: }
405: if (line == null) {
406: log
407: .warn("Unexpected EOF trying to find XML end marker in "
408: + filename);
409: raf.close();
410: return false;
411: }
412: raf.setLength(pos + end);// Truncate the file
413: raf.close();
414: raf = null;
415: } catch (FileNotFoundException e) {
416: return false;
417: } catch (IOException e) {
418: log.warn("Error trying to find XML terminator "
419: + e.toString());
420: return false;
421: } finally {
422: try {
423: if (raf != null)
424: raf.close();
425: } catch (IOException e1) {
426: log.info("Could not close " + filename + " "
427: + e1.getLocalizedMessage());
428: }
429: }
430: return true;
431: }
432:
433: // Only called if visualizer is non-null
434: private void readSamples(TestResultWrapper testResults,
435: Visualizer visualizer) throws Exception {
436: Collection samples = testResults.getSampleResults();
437: Iterator iter = samples.iterator();
438: while (iter.hasNext()) {
439: SampleResult result = (SampleResult) iter.next();
440: if (isSampleWanted(result.isSuccessful())) {
441: visualizer.add(result);
442: }
443: }
444: }
445:
446: public void clearVisualizer() {
447: // current = -1;
448: if (getVisualizer() != null
449: && getVisualizer() instanceof Clearable) {
450: ((Clearable) getVisualizer()).clearData();
451: }
452: finalizeFileOutput();
453: }
454:
455: public void sampleStarted(SampleEvent e) {
456: }
457:
458: public void sampleStopped(SampleEvent e) {
459: }
460:
461: /**
462: * When a test result is received, display it and save it.
463: *
464: * @param event
465: * the sample event that was received
466: */
467: public void sampleOccurred(SampleEvent event) {
468: SampleResult result = event.getResult();
469:
470: if (isSampleWanted(result.isSuccessful())) {
471: sendToVisualizer(result);
472: if (out != null) {// no point otherwise
473: SampleSaveConfiguration config = getSaveConfig();
474: result.setSaveConfig(config);
475: try {
476: if (config.saveAsXml()) {
477: recordResult(event);
478: } else {
479: String savee = CSVSaveService
480: .resultToDelimitedString(event);
481: out.println(savee);
482: }
483: } catch (Exception err) {
484: log.error("Error trying to record a sample", err); // should throw exception back to caller
485: }
486: }
487: }
488: }
489:
490: protected final void sendToVisualizer(SampleResult r) {
491: if (getVisualizer() != null) {
492: getVisualizer().add(r);
493: }
494: }
495:
496: // Only called if out != null
497: private void recordResult(SampleEvent event) throws Exception {
498: SampleResult result = event.getResult();
499: if (!isResultMarked(result) && !this .isStats) {
500: if (SaveService.isSaveTestLogFormat20()) {
501: if (serializer == null) {
502: serializer = new DefaultConfigurationSerializer();
503: }
504: out.write(OldSaveService.getSerializedSampleResult(
505: result, serializer, getSaveConfig()));
506: } else {
507: SaveService.saveSampleResult(event, out);
508: }
509: }
510: }
511:
512: /**
513: * recordStats is used to save statistics generated by visualizers
514: *
515: * @param e
516: * @throws Exception
517: */
518: public void recordStats(TestElement e) throws Exception {
519: if (out == null) {
520: initializeFileOutput();
521: }
522: if (out != null) {
523: SaveService.saveTestElement(e, out);
524: }
525: }
526:
527: private synchronized boolean isResultMarked(SampleResult res) {
528: String filename = getFilename();
529: boolean marked = res.isMarked(filename);
530:
531: if (!marked) {
532: res.setMarked(filename);
533: }
534: return marked;
535: }
536:
537: private void initializeFileOutput() throws IOException {
538:
539: String filename = getFilename();
540: if (out == null && filename != null) {
541: if (out == null) {
542: try {
543: out = getFileWriter(filename, getSaveConfig());
544: } catch (FileNotFoundException e) {
545: out = null;
546: }
547: }
548: }
549: }
550:
551: private synchronized void finalizeFileOutput() {
552: if (out != null) {
553: writeFileEnd(out, getSaveConfig());
554: out.close();
555: files.remove(getFilename());
556: out = null;
557: }
558: }
559:
560: /*
561: * (non-Javadoc)
562: *
563: * @see TestListener#testIterationStart(LoopIterationEvent)
564: */
565: public void testIterationStart(LoopIterationEvent event) {
566: }
567:
568: /**
569: * @return Returns the saveConfig.
570: */
571: public SampleSaveConfiguration getSaveConfig() {
572: try {
573: return (SampleSaveConfiguration) getProperty(SAVE_CONFIG)
574: .getObjectValue();
575: } catch (ClassCastException e) {
576: setSaveConfig(new SampleSaveConfiguration());
577: return getSaveConfig();
578: }
579: }
580:
581: /**
582: * @param saveConfig
583: * The saveConfig to set.
584: */
585: public void setSaveConfig(SampleSaveConfiguration saveConfig) {
586: getProperty(SAVE_CONFIG).setObjectValue(saveConfig);
587: }
588:
589: // This is required so that
590: // @see org.apache.jmeter.gui.tree.JMeterTreeModel.getNodesOfType()
591: // can find the Clearable nodes - the userObject has to implement the interface.
592: public void clearData() {
593: }
594:
595: }
|