/******************************************************************************* * Copyright (c) 2010 Eric Bodden. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Eric Bodden - initial API and implementation ******************************************************************************/ package de.bodden.tamiflex.playin; import static de.bodden.tamiflex.normalizer.Hasher.containsGeneratedClassName; import static de.bodden.tamiflex.normalizer.ReferencedGeneratedClasses.nameOfGeneratedClassReferenced; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.InputStream; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.security.ProtectionDomain; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import org.objectweb.asm.ClassVisitor; import de.bodden.tamiflex.normalizer.ClassRenamer; import de.bodden.tamiflex.normalizer.Hasher; import de.bodden.tamiflex.normalizer.NameExtractor; public class ClassReplacer implements ClassFileTransformer { private static final String ASM_PKGNAME = ClassVisitor.class.getPackage().getName().replace('.', '/'); /** The class loader that ought to be used to replace classes. */ protected final ClassLoader loader; /** If true, the agent will issue no warnings. **/ protected final boolean verbose; /** A mapping from class names of generated classes to the original class bytes of the respective class, i.e., * the bytes as they were just about to be loaded on the current execution. * See {@link Hasher#containsGeneratedClassName(String)} to determine if a class is generated in this sense. */ protected Map<String,byte[]> generatedClassNameToOriginalBytes = new HashMap<String, byte[]>(); public int numInvoked, numSuccess; public ClassReplacer(String srcPath, boolean verbose) { this.verbose = verbose; this.loader = createClassLoader(srcPath); } public byte[] transform(ClassLoader ldr, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { numInvoked++; if(className==null) { className = NameExtractor.extractName(classfileBuffer); } try{ if(containsGeneratedClassName(className)) storeClassBytesOfGeneratedClass(className, classfileBuffer); return tryToReplaceClassBytes(className); } catch (RuntimeException e) { e.printStackTrace(); throw e; } catch (Error e) { e.printStackTrace(); throw e; } } private byte[] tryToReplaceClassBytes(final String className) { try { if(className.startsWith(Agent.PKGNAME)) return null; if(className.startsWith(ASM_PKGNAME)) return null; //check if the class is generated, and if so generate a hashed class name and use that name in what follows String classNameInFileSystem; boolean isGeneratedClass; byte[] originalBytes; synchronized(this) { originalBytes = generatedClassNameToOriginalBytes.get(className); isGeneratedClass = Hasher.containsGeneratedClassName(className); if(isGeneratedClass) { byte[] originalClassBytes = originalBytes; //generate hash numbers based on the contents of all generated classes seen so far Hasher.generateHashNumber(className, originalClassBytes); //we will load the class file using the hashed name classNameInFileSystem = Hasher.hashedClassNameForGeneratedClassName(className); } else { classNameInFileSystem = className; } } String classFileName = classNameInFileSystem+".class"; InputStream is = loader.getResourceAsStream(classFileName); if(is==null) { if(verbose) { if(isGeneratedClass) System.err.println("WARNING: Cannot find class "+classNameInFileSystem+". Will use original class "+className+" instead."); else System.err.println("WARNING: Cannot find class "+classNameInFileSystem+". Will use original class instead."); } //leave bytecodes unchanged return null; } else { try { //read the class file from disk ByteArrayOutputStream bos = new ByteArrayOutputStream(is.available()); int bytesRead; byte[] buffer = new byte[is.available()]; while((bytesRead=is.read(buffer))>-1) { bos.write(buffer,0,bytesRead); } byte[] readBytes = bos.toByteArray(); //if the class is generated, replace... //1) the hashed name of the class itself by className, the name that the context expects, and //2) the hashed names of all other referenced generated classes by the actual names that // the context provides if(isGeneratedClass) { String refOrig = nameOfGeneratedClassReferenced(className, originalBytes); String refHashed = nameOfGeneratedClassReferenced(classNameInFileSystem, readBytes); Map<String,String> fromTo = new HashMap<String, String>(); //rename declaring class fromTo.put(classNameInFileSystem, className); //rename referenced class, if any if(refOrig!=null) { assert refHashed!=null : "Class "+refOrig+" references "+refOrig+" but hashed class "+classNameInFileSystem+" has no references!?"; fromTo.put(refHashed,refOrig); } synchronized (this) { readBytes = ClassRenamer.replaceClassNamesInBytes(fromTo, readBytes); } } numSuccess++; return readBytes; } finally { is.close(); } } } catch (Exception e) { //print the exception before we re-throw it because otherwise the transformation //framework may just swallow it RuntimeException e2 = new RuntimeException("Exception in class dumper",e); e2.printStackTrace(); throw e2; } } /** * Returns a class loader that loads classes <i>only</i> from the * provided <code>srcPath</code>. This path has to be in standard classpath * format, separated by {@link File#pathSeparator}. Note that this class loader * does <i>not</i> delegate! * @param srcPath The path to load from. * @return the class loader */ private URLClassLoader createClassLoader(String srcPath) { String[] pathSegments = srcPath.split(File.pathSeparator); URL[] urls = new URL[pathSegments.length]; int i=0; for (String segment : pathSegments) { try { urls[i++] = new File(segment).toURI().toURL(); } catch (MalformedURLException e) { e.printStackTrace(); } } URLClassLoader loader = new URLClassLoader(urls,null); return loader; } /** * Stores a mapping from <code>className</code> to a copy of <code>classfileBuffer</code> into * {@link #generatedClassNameToOriginalBytes}. In cases where this map already holds a mapping * for <code>className</code> and this mapping points to an array with different contents, then * this method issues a warning. The warning is only issued if {@link #verbose} is true. */ private synchronized void storeClassBytesOfGeneratedClass(final String className, byte[] classfileBuffer) { if(generatedClassNameToOriginalBytes.containsKey(className) && !Arrays.equals(classfileBuffer, generatedClassNameToOriginalBytes.get(className))) { if(verbose) System.err.println("WARNING: There exist two different classes with name "+className); } else { byte[] copy = new byte[classfileBuffer.length]; System.arraycopy(classfileBuffer, 0, copy, 0, classfileBuffer.length); generatedClassNameToOriginalBytes.put(className, copy); } } }