blob: 7732d0e3a221f9f62bf4e3a5ce938e6187068efd [file] [log] [blame]
/* Copyright (C) 2003 Vladimir Roubtsov. All rights reserved.
*
* This program and the accompanying materials are made available under
* the terms of the Common Public License v1.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/cpl-v10.html
*
* $Id: InstrClassLoader.java,v 1.1.1.1.2.2 2004/07/16 23:32:03 vlad_r Exp $
*/
package com.vladium.emma.rt;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.CodeSource;
import java.security.cert.Certificate;
import java.util.Map;
import com.vladium.logging.Logger;
import com.vladium.util.ByteArrayOStream;
import com.vladium.util.asserts.$assert;
import com.vladium.emma.IAppConstants;
import com.vladium.emma.filter.IInclExclFilter;
// ----------------------------------------------------------------------------
/**
* @author Vlad Roubtsov, (C) 2003
*/
public
final class InstrClassLoader extends URLClassLoader
{
// public: ................................................................
// TODO: proper security [use PrivilegedAction as needed]
// TODO: [see above as well] need to keep track of res URLs to support [path] exclusion patterns
// TODO: improve error handling so it is clear when errors come from buggy instrumentation
public static final String PROPERTY_FORCED_DELEGATION_FILTER = "clsload.forced_delegation_filter";
public static final String PROPERTY_THROUGH_DELEGATION_FILTER = "clsload.through_delegation_filter";
public InstrClassLoader (final ClassLoader parent, final File [] classpath,
final IInclExclFilter forcedDelegationFilter,
final IInclExclFilter throughDelegationFilter,
final IClassLoadHook hook, final Map cache)
throws MalformedURLException
{
// setting ClassLoader.parent to null disables the standard delegation
// behavior in a few places, including URLClassLoader.getResource():
super (filesToURLs (classpath), null);
// TODO: arg validation
m_hook = hook;
m_cache = cache; // can be null
m_forcedDelegationFilter = forcedDelegationFilter;
m_throughDelegationFilter = throughDelegationFilter;
m_parent = parent;
m_bufPool = new PoolEntry [BAOS_POOL_SIZE];
m_log = Logger.getLogger ();
}
/**
* Overrides java.lang.ClassLoader.loadClass() to change the usual parent-child
* delegation rules just enough to be able to 'replace' the parent loader. This
* also has the effect of detecting 'system' classes without doing any class
* name-based matching.
*/
public synchronized final Class loadClass (final String name, final boolean resolve)
throws ClassNotFoundException
{
final boolean trace1 = m_log.atTRACE1 ();
if (trace1) m_log.trace1 ("loadClass", "(" + name + ", " + resolve + "): nest level " + m_nestLevel);
Class c = null;
// first, check if this class has already been defined by this classloader
// instance:
c = findLoadedClass (name);
if (c == null)
{
Class parentsVersion = null;
if (m_parent != null)
{
try
{
parentsVersion = m_parent.loadClass (name); // note: it is important that this does not init the class
if ((parentsVersion.getClassLoader () != m_parent) ||
((m_forcedDelegationFilter == null) || m_forcedDelegationFilter.included (name)))
{
// (a) m_parent itself decided to delegate: use parent's version
// (b) the class was on the forced delegation list: use parent's version
c = parentsVersion;
if (trace1) m_log.trace1 ("loadClass", "using parent's version for [" + name + "]");
}
}
catch (ClassNotFoundException cnfe)
{
// if the class was on the forced delegation list, error out:
if ((m_forcedDelegationFilter == null) || m_forcedDelegationFilter.included (name))
throw cnfe;
}
}
if (c == null)
{
try
{
// either (a) m_parent was null or (b) it could not load 'c'
// or (c) it will define 'c' itself if allowed to. In any
// of these cases I attempt to define my own version:
c = findClass (name);
}
catch (ClassNotFoundException cnfe)
{
// this is a difficult design point unless I resurrect the -lx option
// and document how to use it [which will confuse most users anyway]
// another alternative would be to see if parent's version is included by
// the filter and print a warning; still, it does not help with JAXP etc
if (parentsVersion != null)
{
final boolean delegate = (m_throughDelegationFilter == null) || m_throughDelegationFilter.included (name);
if (delegate)
{
c = parentsVersion;
if (trace1) m_log.trace1 ("loadClass", "[delegation filter] using parent's version for [" + name + "]");
}
else
throw cnfe;
}
else
throw cnfe;
}
}
}
if (c == null) throw new ClassNotFoundException (name);
if (resolve) resolveClass (c); // this never happens in J2SE JVMs
return c;
}
// TODO: remove this in the release build
public final URL getResource (final String name)
{
final boolean trace1 = m_log.atTRACE1 ();
if (trace1) m_log.trace1 ("getResource", "(" + name + "): nest level " + m_nestLevel);
final URL result = super.getResource (name);
if (trace1 && (result != null)) m_log.trace1 ("loadClass", "[" + name + "] found in " + result);
return result;
}
// protected: .............................................................
protected final Class findClass (final String name)
throws ClassNotFoundException
{
final boolean trace1 = m_log.atTRACE1 ();
if (trace1) m_log.trace1 ("findClass", "(" + name + "): nest level " + m_nestLevel);
final boolean useClassCache = (m_cache != null);
final ClassPathCacheEntry entry = useClassCache ? (ClassPathCacheEntry) m_cache.remove (name) : null;
byte [] bytes;
int length;
URL classURL = null;
if (entry != null) // cache hit
{
++ m_cacheHits;
// used cached class def bytes, no need to repeat disk I/O:
try
{
classURL = new URL (entry.m_srcURL);
}
catch (MalformedURLException murle) // this should never happen
{
if ($assert.ENABLED)
{
murle.printStackTrace (System.out);
}
}
PoolEntry buf = null;
try
{
buf = acquirePoolEntry ();
final ByteArrayOStream baos = buf.m_baos; // reset() has been called on this
// the original class definition:
bytes = entry.m_bytes;
length = bytes.length;
if ((m_hook != null) && m_hook.processClassDef (name, bytes, length, baos)) // note: this can overwrite 'bytes'
{
// the instrumented class definition:
bytes = baos.getByteArray ();
length = baos.size ();
if (trace1) m_log.trace1 ("findClass", "defining [cached] instrumented [" + name + "] {" + length + " bytes }");
}
else
{
if (trace1) m_log.trace1 ("findClass", "defining [cached] [" + name + "] {" + length + " bytes }");
}
return defineClass (name, bytes, length, classURL);
}
catch (IOException ioe)
{
throw new ClassNotFoundException (name);
}
finally
{
if (buf != null) releasePoolEntry (buf);
}
}
else // cache miss
{
if (useClassCache) ++ m_cacheMisses;
// .class files are not guaranteed to be loadable as resources;
// but if Sun's code does it...
final String classResource = name.replace ('.', '/') + ".class";
// even thought normal delegation is disabled, this will find bootstrap classes:
classURL = getResource (classResource); // important to hook into URLClassLoader's overload of this so that Class-Path manifest attributes are processed etc
if (trace1 && (classURL != null)) m_log.trace1 ("findClass", "[" + name + "] found in " + classURL);
if (classURL == null)
throw new ClassNotFoundException (name);
else
{
InputStream in = null;
PoolEntry buf = null;
try
{
in = classURL.openStream ();
buf = acquirePoolEntry ();
final ByteArrayOStream baos = buf.m_baos; // reset() has been called on this
readFully (in, baos, buf.m_buf);
in.close (); // don't keep the file handle across reentrant calls
in = null;
// the original class definition:
bytes = baos.getByteArray ();
length = baos.size ();
baos.reset (); // reuse this for processClassDef below
if ((m_hook != null) && m_hook.processClassDef (name, bytes, length, baos)) // note: this can overwrite 'bytes'
{
// the instrumented class definition:
bytes = baos.getByteArray ();
length = baos.size ();
if (trace1) m_log.trace1 ("findClass", "defining instrumented [" + name + "] {" + length + " bytes }");
}
else
{
if (trace1) m_log.trace1 ("findClass", "defining [" + name + "] {" + length + " bytes }");
}
return defineClass (name, bytes, length, classURL);
}
catch (IOException ioe)
{
throw new ClassNotFoundException (name);
}
finally
{
if (buf != null) releasePoolEntry (buf);
if (in != null) try { in.close (); } catch (Exception ignore) {}
}
}
}
}
public void debugDump (final PrintWriter out)
{
if (out != null)
{
out.println (this + ": " + m_cacheHits + " class cache hits, " + m_cacheMisses + " misses");
}
}
// package: ...............................................................
// private: ...............................................................
private static final class PoolEntry
{
PoolEntry (final int baosCapacity, final int bufSize)
{
m_baos = new ByteArrayOStream (baosCapacity);
m_buf = new byte [bufSize];
}
void trim (final int baosCapacity, final int baosMaxCapacity)
{
if (m_baos.capacity () > baosMaxCapacity)
{
m_baos = new ByteArrayOStream (baosCapacity);
}
}
ByteArrayOStream m_baos;
final byte [] m_buf;
} // end of nested class
/*
* 'srcURL' may be null
*/
private Class defineClass (final String className, final byte [] bytes, final int length, final URL srcURL)
{
// support ProtectionDomains with non-null class source URLs:
// [however, disable anything related to sealing or signing]
final CodeSource csrc = new CodeSource (srcURL, (Certificate[])null);
// allow getPackage() to return non-null on the class we are about to
// define (however, don't bother emulating the original manifest info since
// we may be altering manifest content anyway):
final int lastDot = className.lastIndexOf ('.');
if (lastDot >= 0)
{
final String packageName = className.substring (0, lastDot);
final Package pkg = getPackage (packageName);
if (pkg == null)
{
definePackage (packageName,
IAppConstants.APP_NAME, IAppConstants.APP_VERSION, IAppConstants.APP_COPYRIGHT,
IAppConstants.APP_NAME, IAppConstants.APP_VERSION, IAppConstants.APP_COPYRIGHT,
srcURL);
}
}
return defineClass (className, bytes, 0, length, csrc);
}
private static URL [] filesToURLs (final File [] classpath)
throws MalformedURLException
{
if ((classpath == null) || (classpath.length == 0))
return EMPTY_URL_ARRAY;
final URL [] result = new URL [classpath.length];
for (int f = 0; f < result.length ; ++ f)
{
result [f] = classpath [f].toURL (); // note: this does proper dir encoding
}
return result;
}
/**
* Reads the entire contents of a given stream into a flat byte array.
*/
private static void readFully (final InputStream in, final ByteArrayOStream out, final byte [] buf)
throws IOException
{
for (int read; (read = in.read (buf)) >= 0; )
{
out.write (buf, 0, read);
}
}
/*
* not MT-safe; must be called from loadClass() only
*/
private PoolEntry acquirePoolEntry ()
{
PoolEntry result;
if (m_nestLevel >= BAOS_POOL_SIZE)
{
result = new PoolEntry (BAOS_INIT_SIZE, BAOS_INIT_SIZE);
}
else
{
result = m_bufPool [m_nestLevel];
if (result == null)
{
result = new PoolEntry (BAOS_INIT_SIZE, BAOS_INIT_SIZE);
m_bufPool [m_nestLevel] = result;
}
else
{
result.m_baos.reset ();
}
}
++ m_nestLevel;
return result;
}
/*
* not MT-safe; must be called from loadClass() only
*/
private void releasePoolEntry (final PoolEntry buf)
{
if (-- m_nestLevel < BAOS_POOL_SIZE)
{
buf.trim (BAOS_INIT_SIZE, BAOS_MAX_SIZE);
}
}
private final ClassLoader m_parent;
private final IInclExclFilter m_forcedDelegationFilter;
private final IInclExclFilter m_throughDelegationFilter;
private final Map /* classJavaName:String -> ClassPathCacheEntry */ m_cache; // can be null
private final IClassLoadHook m_hook;
private final PoolEntry [] m_bufPool;
private final Logger m_log; // a loader instance is used concurrently but cached its log config at construction time
private int m_nestLevel;
private int m_cacheHits, m_cacheMisses;
private static final int BAOS_INIT_SIZE = 32 * 1024;
private static final int BAOS_MAX_SIZE = 1024 * 1024;
private static final int BAOS_POOL_SIZE = 8;
private static final URL [] EMPTY_URL_ARRAY = new URL [0];
} // end of class
// ----------------------------------------------------------------------------