package hudson.plugins.performance.parsers;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.plugins.performance.reports.PerformanceReport;
import hudson.plugins.performance.reports.UriReport;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Hashtable;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* An abstraction for parsing data to PerformanceReport instances. This class
* provides functionality that optimizes the parsing process, such as caching as
* well as saving/loaded parsed data in serialized form to/from disc.
*
* @author Guus der Kinderen, guus.der.kinderen@gmail.com
*/
public abstract class AbstractParser extends PerformanceReportParser {
private static final Logger LOGGER = Logger.getLogger(JMeterParser.class.getName());
/**
* A suffix to be used for files in which a serialized PerformanceReport instance is stored.
*/
private static final String SERIALIZED_DATA_FILE_SUFFIX = ".serialized";
/**
* A cache that contains serialized PerformanceReport instances. This cache intends to limit disc IO.
*/
private static final Cache<String, PerformanceReport> CACHE = CacheBuilder.newBuilder().maximumSize(1000).softValues().build();
public AbstractParser(String glob) {
super(glob);
}
@Override
public Collection<PerformanceReport> parse(Run<?, ?> build, Collection<File> reports, TaskListener listener) throws IOException {
final List<PerformanceReport> result = new ArrayList<PerformanceReport>();
for (File reportFile : reports) {
// Attempt to load previously serialized instances from file or cache.
final PerformanceReport deserializedReport = loadSerializedReport(reportFile);
if (deserializedReport != null) {
result.add(deserializedReport);
continue;
}
// When serialized data cannot be used, the original JMeter files are to be processed.
try {
listener.getLogger().println("Performance: Parsing JMeter report file '" + reportFile + "'.");
final PerformanceReport report = parse(reportFile);
result.add(report);
saveSerializedReport(reportFile, report);
} catch (Throwable e) {
listener.getLogger().println("Performance: Failed to parse file '" + reportFile + "': " + e.getMessage());
e.printStackTrace(listener.getLogger());
}
}
return result;
}
/**
* Performs the actual parsing of data. When the implementation throws any
* exception, the input file is ignored. This does not abort parsing of
* subsequent files.
*
* @param reportFile The source file (cannot be null).
* @return The parsed data (never null).
* @throws Throwable On any exception.
*/
abstract PerformanceReport parse(File reportFile) throws Exception;
/**
* Returns a PerformanceReport instance for the provided report file, based on
* previously serialized data.
* <p>
* This method first attempts to load data from an internal cache. If the data
* is not in cache, data is obtained from a file on disc.
* <p>
* When no PerformanceReport instance has previously been serialized (or when
* such data cannot be read, for instance because of class file changes), this
* method returns null.
*
* @param reportFile Report for which to return data. Cannot be null.
* @return deserialized data, possibly null.
*/
protected static PerformanceReport loadSerializedReport(File reportFile) {
if (reportFile == null) {
throw new NullPointerException("Argument 'reportFile' cannot be null.");
}
final String serialized = reportFile.getPath() + SERIALIZED_DATA_FILE_SUFFIX;
ObjectInputStream in = null;
synchronized (CACHE) {
try {
PerformanceReport report = CACHE.getIfPresent(serialized);
if (report == null) {
in = new ObjectInputStreamWithClassMapping(new BufferedInputStream(new FileInputStream(serialized)));
report = (PerformanceReport) in.readObject();
CACHE.put(serialized, report);
}
return report;
} catch (FileNotFoundException ex) {
// That's OK
} catch (Exception ex) {
LOGGER.log(Level.WARNING, "Reading serialized PerformanceReport instance from file '" + serialized + "' failed.", ex);
} finally {
if (in != null) {
try {
in.close();
} catch (IOException ex) {
LOGGER.log(Level.WARNING, "Unable to close inputstream after attempt to read data from file '" + serialized + "'.", ex);
}
}
}
return null;
}
}
/**
* Saves a PerformanceReport instance as serialized data into a file on disc.
*
* @param reportFile The file from which the original data is obtained (<em>not</em>
* the file into which serialized data is to be saved!) Cannot be
* null.
* @param report The instance to serialize. Cannot be null.
*/
protected static void saveSerializedReport(File reportFile, PerformanceReport report) {
if (reportFile == null) {
throw new NullPointerException("Argument 'reportFile' cannot be null.");
}
if (report == null) {
throw new NullPointerException("Argument 'report' cannot be null.");
}
final String serialized = reportFile.getPath() + SERIALIZED_DATA_FILE_SUFFIX;
synchronized (CACHE) {
CACHE.put(serialized, report);
}
ObjectOutputStream out = null;
try {
out = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(serialized)));
out.writeObject(report);
} catch (Exception ex) {
LOGGER.log(Level.WARNING, "Saving serialized PerformanceReport instance to file '" + serialized + "' failed.", ex);
} finally {
if (out != null) {
try {
out.close();
} catch (IOException ex) {
LOGGER.log(Level.WARNING, "Unable to close outputstream after attempt to write data to file '" + serialized + "'.", ex);
}
}
}
}
public static class ObjectInputStreamWithClassMapping extends ObjectInputStream {
protected Hashtable<String, Class> classMapping = new Hashtable<String, Class>();
public ObjectInputStreamWithClassMapping(InputStream in) throws IOException {
super(in);
classMapping.put("hudson.plugins.performance.PerformanceReport", PerformanceReport.class);
classMapping.put("hudson.plugins.performance.UriReport", UriReport.class);
classMapping.put("hudson.plugins.performance.UriReport$Sample", UriReport.Sample.class);
}
@Override
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException,
ClassNotFoundException {
return (classMapping.containsKey(desc.getName())) ?
classMapping.get(desc.getName()) :
super.resolveClass(desc);
}
}
}