package com.ikokoon.serenity;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import java.util.Date;
import org.apache.log4j.Logger;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import com.ikokoon.serenity.instrumentation.VisitorFactory;
import com.ikokoon.serenity.persistence.DataBaseOdb;
import com.ikokoon.serenity.persistence.DataBaseRam;
import com.ikokoon.serenity.persistence.DataBaseToolkit;
import com.ikokoon.serenity.persistence.IDataBase;
import com.ikokoon.serenity.process.Accumulator;
import com.ikokoon.serenity.process.Aggregator;
import com.ikokoon.serenity.process.Cleaner;
import com.ikokoon.serenity.process.Listener;
import com.ikokoon.serenity.process.Reporter;
import com.ikokoon.toolkit.LoggingConfigurator;
import com.ikokoon.toolkit.Toolkit;
/**
* This class is the entry point for the Serenity code coverage/complexity/dependency/profiling functionality. This class is called by the JVM on
* startup. The agent then has first access to the byte code for all classes that are loaded. During this loading the byte code can be enhanced.
*
* @author Michael Couck
* @since 12.07.09
* @version 01.00
*/
public class Transformer implements ClassFileTransformer, IConstants {
/** The logger. */
private static Logger LOGGER;
/** During tests there can be more than one shutdown hook added. */
private static boolean INITIALISED = false;
/** The chain of adapters for analysing the classes. */
private static Class<ClassVisitor>[] CLASS_ADAPTER_CLASSES;
/** The shutdown hook will clean, accumulate and aggregate the data. */
private static Thread shutdownHook;
/**
* This method is called by the JVM at startup. This method will only be called if the command line for starting the JVM has the following on it:
* -javaagent:serenity/serenity.jar. This instruction tells the JVM that there is an agent that must be used. In the META-INF directory of the jar
* specified there must be a MANIFEST.MF file. In this file the instructions must be something like the following:
*
* Manifest-Version: 1.0 <br>
* Boot-Class-Path: asm-3.1.jar and so on..., in the case that the required libraries are not on the classpath, which they should be<br>
* Premain-Class: com.ikokoon.serenity.Transformer
*
* Another line in the manifest can start an agent after the JVM has been started, but not for all JVMs. So not very useful.
*
* These instructions tell the JVM to call this method when loading class files.
*
* @param args
* a set of arguments that the JVM will call the method with
* @param instrumentation
* the instrumentation implementation of the JVM
*/
@SuppressWarnings("unchecked")
public static void premain(String args, Instrumentation instrumentation) {
if (!INITIALISED) {
INITIALISED = true;
LoggingConfigurator.configure();
CLASS_ADAPTER_CLASSES = Configuration.getConfiguration().classAdapters.toArray(new Class[Configuration.getConfiguration().classAdapters
.size()]);
LOGGER = Logger.getLogger(Transformer.class);
if (instrumentation != null) {
instrumentation.addTransformer(new Transformer());
}
String cleanClasses = Configuration.getConfiguration().getProperty(IConstants.CLEAN_CLASSES);
if (cleanClasses != null && cleanClasses.equals(Boolean.TRUE.toString())) {
File serenityDirectory = new File(IConstants.SERENITY_DIRECTORY);
Toolkit.deleteFiles(serenityDirectory, ".class");
if (!serenityDirectory.exists()) {
if (!serenityDirectory.mkdirs()) {
LOGGER.warn("Didn't re-create Serenity directory : " + serenityDirectory.getAbsolutePath());
}
}
}
File file = new File(IConstants.DATABASE_FILE_ODB);
Toolkit.deleteFile(file, 3);
// This is the underlying database that will persist the data to the file system
IDataBase odbDataBase = IDataBase.DataBaseManager.getDataBase(DataBaseOdb.class, IConstants.DATABASE_FILE_ODB, null);
DataBaseToolkit.clear(odbDataBase);
// This is the ram database that will hold all the data in memory for better performance
IDataBase ramDataBase = IDataBase.DataBaseManager.getDataBase(DataBaseRam.class, IConstants.DATABASE_FILE_RAM, odbDataBase);
DataBaseToolkit.clear(ramDataBase);
Collector.initialize(ramDataBase);
Profiler.initialize(ramDataBase);
new Listener(null, ramDataBase).execute();
addShutdownHook(ramDataBase);
}
}
/**
* This method adds the shutdown hook that will clean and accumulate the data when the Jvm shuts down.
*
* @param dataBase
* the database to get the data from
*/
private static void addShutdownHook(final IDataBase dataBase) {
shutdownHook = new Thread() {
public void run() {
Date start = new Date();
LOGGER.warn("Starting accumulation : " + start);
long processStart = System.currentTimeMillis();
new Accumulator(null).execute();
LOGGER.warn("Accumlulator : " + (System.currentTimeMillis() - processStart));
processStart = System.currentTimeMillis();
new Cleaner(null, dataBase).execute();
LOGGER.warn("Cleaner : " + (System.currentTimeMillis() - processStart));
processStart = System.currentTimeMillis();
new Aggregator(null, dataBase).execute();
LOGGER.warn("Aggregator : " + (System.currentTimeMillis() - processStart));
processStart = System.currentTimeMillis();
new Reporter(null, dataBase).execute();
LOGGER.warn("Reporter : " + (System.currentTimeMillis() - processStart));
processStart = System.currentTimeMillis();
dataBase.close();
LOGGER.warn("Close database : " + (System.currentTimeMillis() - processStart));
Date end = new Date();
long million = 1000 * 1000;
long duration = end.getTime() - start.getTime();
LOGGER.warn("Finished accumulation : " + end + ", duration : " + duration + " millis");
LOGGER.warn("Total memory : " + (Runtime.getRuntime().totalMemory() / million) + ", max memory : "
+ (Runtime.getRuntime().maxMemory() / million) + ", free memory : " + (Runtime.getRuntime().freeMemory() / million));
}
};
Runtime.getRuntime().addShutdownHook(shutdownHook);
}
protected static void removeShutdownHook() {
Runtime.getRuntime().removeShutdownHook(shutdownHook);
}
/**
* This method transforms the classes that are specified.
*/
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classBytes)
throws IllegalClassFormatException {
if (Configuration.getConfiguration().excluded(className)) {
LOGGER.info("Excluded class : " + className);
return classBytes;
}
if (Configuration.getConfiguration().included(className)) {
LOGGER.info("Enhancing class : " + className);
ByteArrayOutputStream source = new ByteArrayOutputStream(0);
ClassWriter writer = (ClassWriter) VisitorFactory.getClassVisitor(CLASS_ADAPTER_CLASSES, className, classBytes, source);
byte[] enhancedClassBytes = writer.toByteArray();
String writeClasses = Configuration.getConfiguration().getProperty(IConstants.WRITE_CLASSES);
if (writeClasses != null && writeClasses.equals(Boolean.TRUE.toString())) {
writeClass(className, enhancedClassBytes);
}
return enhancedClassBytes;
} else {
LOGGER.info("Class not included : " + className);
}
return classBytes;
}
/**
* This method writes the transformed classes to the file system so they can be viewed later.
*
* @param className
* the name of the class file
* @param classBytes
* the bytes of byte code to write
*/
private void writeClass(String className, byte[] classBytes) {
// Write the class so we can check it with JD decompiler visually
String directoryPath = Toolkit.dotToSlash(Toolkit.classNameToPackageName(className));
String fileName = className.replaceFirst(Toolkit.classNameToPackageName(className), "") + ".class";
File directory = new File(IConstants.SERENITY_DIRECTORY + File.separator + directoryPath);
if (!directory.exists()) {
directory.mkdirs();
LOGGER.debug(directory.getAbsolutePath());
}
File file = new File(directory, fileName);
Toolkit.setContents(file, classBytes);
}
}