package com.ikokoon.serenity.process; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Enumeration; import java.util.List; import java.util.Set; import java.util.StringTokenizer; import java.util.TreeSet; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.zip.ZipEntry; import org.objectweb.asm.ClassVisitor; import com.ikokoon.serenity.Configuration; import com.ikokoon.serenity.instrumentation.VisitorFactory; import com.ikokoon.toolkit.Toolkit; /** * This class looks through the classpath and collects metrics on the classes that were not instantiated by the classloader during the unit tests and * creates a visitor chain for the class that will collect the complexity and dependency metrics for the class. * * @author Michael Couck * @since 24.07.09 * @version 01.00 */ public class Accumulator extends AProcess { /** The set of jars that are processed so we don't do the same jar more than once. */ private Set<String> jarsProcessed = new TreeSet<String>(); /** The set of classes that are processed so we don't process the files more than once. */ private Set<String> filesProcessed = new TreeSet<String>(); /** The chain of adapters for analysing the classes. */ private java.lang.Class<ClassVisitor>[] CLASS_ADAPTER_CLASSES; /** * Constructor takes the parent process. */ @SuppressWarnings("unchecked") public Accumulator(IProcess parent) { super(parent); CLASS_ADAPTER_CLASSES = Configuration.getConfiguration().classAdapters .toArray(new java.lang.Class[Configuration.getConfiguration().classAdapters.size()]); } /** * @inheritDoc */ public void execute() { super.execute(); String classpath = Configuration.getConfiguration().getClassPath(); logger.debug("Class path : " + File.pathSeparator + ", " + classpath); StringTokenizer stringTokenizer = new StringTokenizer(classpath, File.pathSeparator, false); while (stringTokenizer.hasMoreTokens()) { String token = stringTokenizer.nextToken(); File file = new File(token); logger.debug("Processing jar : " + token + ", file : " + file.getAbsolutePath()); if (!file.exists() || !file.canRead()) { logger.warn("Can't read file : " + file.getAbsolutePath()); continue; } if (file.isFile()) { if (token.endsWith(".jar") || token.endsWith(".zip") || token.endsWith(".war") || token.endsWith(".ear")) { logger.debug("Processing jar : " + file.getAbsolutePath()); processJar(file); } } else if (file.isDirectory()) { processDir(file); } } // Look for all jars below this directory to find some source List<File> list = new ArrayList<File>(); File baseDirectory = new File("."); logger.info("Looking for source in base directory : " + baseDirectory.getAbsolutePath()); Toolkit.findFiles(baseDirectory, new Toolkit.IFileFilter() { public boolean matches(File file) { if (file.getName().endsWith(".jar") || file.getName().endsWith(".zip")) { return true; } return false; } }, list); for (File file : list) { logger.debug("Processing jar : " + file.getAbsolutePath()); processJar(file); } } /** * Processes a directory on a file system, looks for class files and feeds the byte code into the adapter chain for collecting the metrics for the * class. * * @param file * the directory or file to look in for the class data */ void processDir(File file) { // Iteratively go through the directories if (file == null || !file.exists() || !file.canWrite()) { return; } if (file.isDirectory()) { File files[] = file.listFiles(); for (int j = 0; j < files.length; j++) { file = files[j]; processDir(file); } } else if (file.isFile() && file.canRead()) { String filePath = file.getAbsolutePath(); filePath = Toolkit.slashToDot(filePath); if (excluded(filePath)) { return; } byte[] classBytes = Toolkit.getContents(file).toByteArray(); ByteArrayOutputStream source = new ByteArrayOutputStream(); String className = null; // Strip the beginning of the path off the name for (String packageName : Configuration.getConfiguration().includedPackages) { if (filePath.indexOf(packageName) > -1) { int indexOfPackageName = filePath.indexOf(packageName); int classIndex = filePath.lastIndexOf(".class"); try { if (classIndex > -1) { className = filePath.substring(indexOfPackageName, classIndex); break; } } catch (Exception e) { logger.error("Exception reading the class files in a directory", e); } } } if (!filesProcessed.add(className)) { return; } processClass(className, classBytes, source); } } /** * Processes a jar or zip file or something like it, looks for class and feeds the byte code into the adapter chain for collecting the metrics for * the class. * * @param file * the file to look in for the class data */ private void processJar(File file) { // Don't process the jars more than once if (!jarsProcessed.add(file.getName())) { return; } JarFile jarFile = null; try { jarFile = new JarFile(file); } catch (Exception e) { logger.error("Exeption accessing the jar : " + file, e); return; } Enumeration<JarEntry> jarEntries = jarFile.entries(); while (jarEntries.hasMoreElements()) { JarEntry jarEntry = jarEntries.nextElement(); String entryName = jarEntry.getName(); String className = Toolkit.slashToDot(entryName); if (excluded(className)) { continue; } logger.info("Processsing entry : " + className); try { InputStream inputStream = jarFile.getInputStream(jarEntry); byte[] classFileBytes = Toolkit.getContents(inputStream).toByteArray(); ByteArrayOutputStream source = null; if (jarEntry.getName().indexOf("$") == -1) { source = getSource(jarFile, entryName); } else { source = new ByteArrayOutputStream(); } processClass(Toolkit.slashToDot(entryName), classFileBytes, source); } catch (IOException e) { logger.error("Exception reading entry : " + jarEntry + ", from file : " + jarFile, e); } } } private ByteArrayOutputStream getSource(JarFile jarFile, String entryName) throws IOException { // Look for the source String javaEntryName = entryName.substring(0, entryName.lastIndexOf('.')) + ".java"; // logger.warn("Looking for source : " + javaEntryName + ", " + entryName); ZipEntry javaEntry = jarFile.getEntry(javaEntryName); if (javaEntry != null) { // logger.warn("Got source : " + javaEntry); InputStream inputStream = jarFile.getInputStream(javaEntry); return Toolkit.getContents(inputStream); } return new ByteArrayOutputStream(); } private void processClass(String name, byte[] classBytes, ByteArrayOutputStream source) { if (name != null && name.endsWith(".class")) { name = name.substring(0, name.lastIndexOf('.')); } try { VisitorFactory.getClassVisitor(CLASS_ADAPTER_CLASSES, name, classBytes, source); } catch (Exception e) { int sourceLength = source != null ? source.size() : 0; logger.warn("Class name : " + name + ", length : " + classBytes.length + ", source : " + sourceLength); logger.error("Exception accululating data on class " + name, e); } } private boolean excluded(String name) { // Don't process anything that is not a class file or a Java file if (!name.endsWith(".class")) { logger.info("Not processing file : " + name); return true; } // Check that the class is included in the included packages if (!Configuration.getConfiguration().included(name)) { logger.info("File not included : " + name); return true; } // Check that the class is not excluded in the excluded packages if (Configuration.getConfiguration().excluded(name)) { logger.info("Excluded file : " + name); return true; } // Don't process the same class twice if (!filesProcessed.add(name)) { logger.info("Already done file : " + name); return true; } return false; } }