/* 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
// ----------------------------------------------------------------------------
