/*
 * @(#)ClassLoadHelper.java
 *
 * Copyright (C) 2000,,2003 2002 Matt Albrecht
 * groboclown@users.sourceforge.net
 * http://groboutils.sourceforge.net
 *
 *  Permission is hereby granted, free of charge, to any person obtaining a
 *  copy of this software and associated documentation files (the "Software"),
 *  to deal in the Software without restriction, including without limitation
 *  the rights to use, copy, modify, merge, publish, distribute, sublicense,
 *  and/or sell copies of the Software, and to permit persons to whom the 
 *  Software is furnished to do so, subject to the following conditions:
 *
 *  The above copyright notice and this permission notice shall be included in 
 *  all copies or substantial portions of the Software. 
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
 *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL 
 *  THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
 *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
 *  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
 *  DEALINGS IN THE SOFTWARE.
 */

package net.sourceforge.groboutils.util.classes.v1;

import java.util.Vector;
import java.util.Hashtable;
import java.util.Enumeration;

import java.io.InputStream;
import java.io.IOException;

import java.net.URL;

import java.lang.reflect.Method;

import org.apache.log4j.Logger;



/**
 * Utility class for loading classes and creating instances.  Much of the
 * basic operation have been ripped from
 * <tt>net.groboutils.util.classes.v1.ClassUtil</tt> in the GroboUtils package.
 * If the helper's class loader is <tt>null</tt>, then it will use the Thread's
 * context ClassLoader.
 * <P>
 * Note that resource loading is very tricky.  Finding the right classloader
 * and right methods to invoke is JDK dependent.
 *
 * @author  Matt Albrecht <a href="mailto:groboclown@users.sourceforge.net">groboclown@users.sourceforge.net</a>
 * @version $Date: 2003/02/10 22:52:36 $
 * @since   March 16, 2002
 */
public class ClassLoadHelper
{
    private static final Logger LOG = Logger.getLogger(
        SPILoader.class.getName() );
    
    
    private static Method M_GET_CONTEXT_CLASSLOADER =
        discoverContextClassloaderMethod();
    private static Method M_GET_RESOURCES =
        discoverGetResourcesMethod();
    private static Method M_GET_SYSTEM_RESOURCES =
        discoverGetSystemResourcesMethod();
    
    private ClassLoader classLoader = null;
    
    
    /**
     * Default constructor - will use the Thread's context class loader for
     * each class discovery.
     */
    public ClassLoadHelper()
    {
        this( (ClassLoader)null );
    }
    
    
    /**
     * Use the given class's classloader.
     *
     * @param clazz the class to pull the classloader from.
     * @exception NullPointerException if <tt>clazz</tt> is <tt>null</tt>.
     */
    public ClassLoadHelper( Class clazz )
    {
        this( clazz.getClassLoader() );
    }
    
    
    /**
     * Loads the helper with the given class loader.  If the given class loader
     * is <tt>null</tt>, then it will use the Thread's context class loader.
     *
     * @param cl the classloader to pull all requested classes from, or
     *      it will use the thread's context class loader if <tt>cl</tt> is
     *      <tt>null</tt>.
     * @see java.lang.Thread#getContextClassLoader()
     */
    public ClassLoadHelper( ClassLoader cl )
    {
        this.classLoader = cl;
    }
    
    
     
    //----------------------------
    // Public methods
    
    
    /**
     * Loads the requested class from the helper's classloader, and returns
     * the Class instance, or <tt>null</tt> if the class could not be
     * found.
     *
     * @param name the name of the class to load.
     * @return the discovered Class, or <tt>null</tt> if it could not be found.
     */
    public Class getClass( String name )
    {
        return getClass( name, true );
    }
    
    
    /**
     * Loads the requested class from the helper's classloader, and returns
     * the Class instance, or <tt>null</tt> if the class could not be
     * found.
     *
     * @param name the name of the class to load.
     * @param swallowExceptions <tt>true</tt> if this method is to return
     * @return the discovered Class, or <tt>null</tt> if it could not be found
     *      and <tt>swallowExceptions</tt> is true.
     * @exception IllegalStateException if there was an error during
     *      initialization and <tt>swallowExceptions</tt> is <tt>false</tt>.
     */
    public Class getClass( String name, boolean swallowExceptions )
    {
        Class c = null;
        
        if (name != null)
        {
            Throwable error = null;
            try
            {
                c = loadClass( name );
            }
            catch (ClassNotFoundException cnfe)
            {
                error = cnfe;
            }
            catch (LinkageError le)
            {
                error = le;
            }
            catch (IllegalArgumentException iae)
            {
                error = iae;
            }
            if (error != null)
            {
                LOG.info( "getClass( "+name+" ) threw exception", error );
                
                if (!swallowExceptions)
                {
                    throw new IllegalStateException( error.toString() );
                }
            }
        }
        else
        {
            c = null;
        }
        return c;
    }
    
    
    /**
     * Creates a new instance of the class with the given <tt>className</tt>
     * using the default constructor.  If there was an error during the
     * creation, such as the class was not found, the class does not have
     * a default constructor, or the constructor threw an exception, then
     * <tt>null</tt> is returned.
     *
     * @param className the name of the class to create an instance.
     * @return the new instance, or <tt>null</tt> if there was a problem.
     * @see #getClass( String )
     * @see #createObject( String, boolean )
     * @see #createObject( Class )
     * @see #createObject( Class, boolean )
     */
    public Object createObject( String className )
    {
        return createObject( getClass( className ), true );
    }
    
    
    /**
     * Creates a new instance of the class with the given <tt>className</tt>
     * using the default constructor.  If there was an error during the
     * creation, such as the class was not found, the class does not have
     * a default constructor, or the constructor threw an exception, then an
     * IllegalStateException will be thrown only if <tt>swallowExceptions</tt>
     * is <tt>false</tt>; otherwise, <tt>null</tt> will be returned.
     *
     * @param className the name of the class to create an instance.
     * @param swallowExceptions <tt>true</tt> if this method is to return
     *      <tt>null</tt> on any exceptions, or <tt>false</tt> if it should
     *      throw an IllegalStateException on any error.
     * @return the new instance.
     * @exception IllegalStateException if there was an error during
     *      initialization and <tt>swallowExceptions</tt> is <tt>false</tt>.
     * @see #getClass( String )
     * @see #createObject( String )
     * @see #createObject( Class )
     * @see #createObject( Class, boolean )
     */
    public Object createObject( String className, boolean swallowExceptions )
    {
        return createObject( getClass( className, swallowExceptions ),
            swallowExceptions );
    }
    
    
    /**
     * Creates an Object from the given Class, using its default constructor.
     * All creation exceptions are swallowed.  If the object could not
     * be created, then <tt>null</tt> is returned.
     *
     * @param c the Class object from which a new instance will be created
     *      using its default constructor.
     * @return the instantiated object, or <tt>null</tt> if <tt>c</tt> is
     *      <tt>null</tt>, or if there was an error during initialization.
     */
    public Object createObject( Class c )
    {
        return createObject( c, true );
    }
    
    
    /**
     * Creates an Object from the given Class, using its default constructor.
     * If there was an error during the
     * creation, such as the class was not found, the class does not have
     * a default constructor, or the constructor threw an exception, then an
     * IllegalStateException will be thrown only if <tt>swallowExceptions</tt>
     * is <tt>false</tt>; otherwise, <tt>null</tt> will be returned.
     *
     * @param c the Class object from which a new instance will be created
     *      using its default constructor.
     * @param swallowExceptions <tt>true</tt> if this method is to return
     *      <tt>null</tt> on any exceptions, or <tt>false</tt> if it should
     *      throw an IllegalStateException on any error.
     * @return the instantiated object, or <tt>null</tt> if <tt>c</tt> is
     *      <tt>null</tt>, or if there was an error during initialization and
     *      <tt>swallowExceptions</tt> is <tt>true</tt>.
     * @exception IllegalStateException if there was an error during
     *      initialization and <tt>swallowExceptions</tt> is <tt>false</tt>.
     */
    public Object createObject( Class c, boolean swallowExceptions )
    {
        if (c == null)
        {
            return null;
        }
        
        Throwable error = null;
        Object obj = null;
        try
        {
            obj = c.newInstance();
        }
        catch (InstantiationException ie)
        {
            error = ie;
        }
        catch (IllegalAccessException iae)
        {
            error = iae;
        }
        catch (NoSuchMethodError nsme)
        {
            error = nsme;
        }
        if (error != null)
        {
            LOG.info( "createObject( "+c+" ) threw exception", error );
            
            if (!swallowExceptions)
            {
                // Note about error.getMessage(): JDK 1.1 may have a 'null'
                // error message thrown above.  Some test cases have
                // encountered this.
                
                throw new IllegalStateException( "Could not instantiate " +
                    c.getName() + ": " + error.toString() );
            }
        }
        // else
        return obj;
    }
    
    
    /**
     * Loads an object using the {@link #createObject( String, boolean )}
     * method above, using the given System property's value as the
     * class name.  If the System property is not defined, then it resorts to
     * the default class.
     *
     * @param propertyClassName the System Property name, whose value will be
     *      used as a fully-qualified Class name to load and instantiate and
     *      return.
     * @param defaultClass if the System Property <tt>propertyClassName</tt>
     *      is not defined, then this will be the class to instantiate and
     *      return.
     * @param swallowExceptions <tt>true</tt> if this method is to return
     *      <tt>null</tt> on any exceptions, or <tt>false</tt> if it should
     *      throw an IllegalStateException on any error.
     * @return the instantiated class.
     * @exception IllegalStateException if there was an error during
     *      initialization and <tt>swallowExceptions</tt> is <tt>false</tt>.
     */
    public Object createObjectFromProperty( String propertyClassName,
            Class defaultClass, boolean swallowExceptions )
    {
        return createObjectFromProperty( propertyClassName, defaultClass,
            null, swallowExceptions );
    }
    
    
    /**
     * Loads an object using the {@link #createObject( String, boolean )}
     * method above, using the given Hashtable's property's value as the
     * class name.  If the Hashtable property is not defined, then it resorts to
     * the default class.  If the Hashtable is <tt>null</tt>, then the
     * System property will be used instead.
     *
     * @param propertyClassName the System Property name, whose value will be
     *      used as a fully-qualified Class name to load and instantiate and
     *      return.
     * @param defaultClass if the System Property <tt>propertyClassName</tt>
     *      is not defined, then this will be the class to instantiate and
     *      return.
     * @param properties a Hashtable of String -&gt; String mappings.
     * @param swallowExceptions <tt>true</tt> if this method is to return
     *      <tt>null</tt> on any exceptions, or <tt>false</tt> if it should
     *      throw an IllegalStateException on any error.
     * @return the instantiated class.
     * @exception IllegalStateException if there was an error during
     *      initialization and <tt>swallowExceptions</tt> is <tt>false</tt>.
     */
    public Object createObjectFromProperty( String propertyClassName,
            Class defaultClass, Hashtable properties,
            boolean swallowExceptions )
    {
        Object o = null;
        String cname = null;
        if (properties == null)
        {
            cname = System.getProperty( propertyClassName );
        }
        else
        {
            cname = (String)properties.get( propertyClassName );
        }
        if (cname == null)
        {
            o = createObject( defaultClass, swallowExceptions );
        }
        else
        {
            o = createObject( cname, swallowExceptions );
        }
        return o;
    }
    
    
    /**
     * Loads a resource with the given name, using the correct ClassLoader.
     * Does not swallow exceptions.  See the JDK documentation on resources
     * (they are pretty much files that are in the classpath of the
     * classloader).  Yes, this can be used successfully to get a class file
     * (well, JDK 1.1 throws a SecurityException if this is attempted).
     *
     * @param name absolute referece to the expected resource.
     * @return the resource as an InputStream, which may possibly be
     *      <tt>null</tt>.
     * @exception IOException if an I/O error occurs.
     * @see java.lang.ClassLoader#getResource( String )
     * @see java.lang.ClassLoader#getResourceAsStream( String )
     */
    public InputStream getResourceAsStream( String name )
            throws IOException
    {
        ClassLoader cl = getClassLoader();
        InputStream is = null;
        if (cl == null)
        {
            name = getAbsoluteResourceName( name );
            is = this.getClass().getResourceAsStream( name );
        }
        else
        {
            is = cl.getResourceAsStream( name );
        }
        return is;
    }
    
    
    /**
     * Loads a resource with the given name, using the correct ClassLoader.
     * Does not swallow exceptions.  See the JDK documentation on resources
     * (they are pretty much files that are in the classpath of the
     * classloader).  Yes, this can be used successfully to get a class file
     * (well, JDK 1.1 throws a SecurityException if this is attempted).
     *
     * @param name absolute referece to the expected resource.
     * @return the resource name as an URL, which may possibly be
     *      <tt>null</tt>.
     * @exception IOException if an I/O error occurs.
     * @see java.lang.ClassLoader#getResource( String )
     * @see java.lang.ClassLoader#getResourceAsStream( String )
     */
    public URL getResource( String name )
            throws IOException
    {
        return getResource( name, getClassLoader() );
    }
    
    
    /**
     * Loads a resource with the given name, using the given ClassLoader.
     * Does not swallow exceptions.  See the JDK documentation on resources
     * (they are pretty much files that are in the classpath of the
     * classloader).  Yes, this can be used successfully to get a class file
     * (well, JDK 1.1 throws a SecurityException if this is attempted).
     *
     * @param name absolute referece to the expected resource.
     * @param cl the classloader to load the reference from.
     * @return the resource name as an URL, which may possibly be
     *      <tt>null</tt>.
     * @exception IOException if an I/O error occurs.
     * @see java.lang.ClassLoader#getResource( String )
     * @see java.lang.ClassLoader#getResourceAsStream( String )
     */
    public URL getResource( String name, ClassLoader cl )
            throws IOException
    {
        LOG.debug("Enter getResource( "+name+" )");
        URL url = null;
        if (cl != null)
        {
            // JDK 1.2+ can be awfully fickle.  Allow for an alternative
            // if this fails, through the use of the next IF statement.
            url = cl.getResource( name );
        }
        
        if (url == null)
        {
            name = getAbsoluteResourceName( name );
            url = this.getClass().getResource( name );
        }
        LOG.debug("Exit getResource( "+name+" ) = '"+url+"'");
        return url;
    }
    
    
    /**
     * Loads a resource with the given name, using the correct ClassLoader.
     * Does not swallow exceptions.  See the JDK documentation on resources
     * (they are pretty much files that are in the classpath of the
     * classloader).  Yes, this can be used successfully to get a class file
     * (well, JDK 1.1 throws a SecurityException if this is attempted).
     *
     * @param name absolute referece to the expected resource.
     * @return the resource name as an URL, which may possibly be
     *      <tt>null</tt>.
     * @exception IOException if an I/O error occurs.
     * @see java.lang.ClassLoader#getResource( String )
     * @see java.lang.ClassLoader#getResourceAsStream( String )
     */
    public URL getSystemResource( String name )
            throws IOException
    {
        LOG.debug("Enter getSystemResource( "+name+" )");
        URL url = ClassLoader.getSystemResource( name );
        LOG.debug("Exit getSystemResource( "+name+" ) = '"+url+"'");
        return url;
    }
    
    
    /**
     * Loads a resource with the given name, using the correct ClassLoader.
     * Does not swallow exceptions.  See the JDK documentation on resources
     * (they are pretty much files that are in the classpath of the
     * classloader).  Yes, this can be used successfully to get a class file
     * (well, JDK 1.1 throws a SecurityException if this is attempted).
     *
     * @param name absolute referece to the expected resource.
     * @return the list of resource URLs, which may NOT be <tt>null</tt>
     *      (implementation ensures it is not null).
     * @exception IOException if an I/O error occurs.
     * @see java.lang.ClassLoader#getResource( String )
     * @see java.lang.ClassLoader#getResources( String )
     * @see java.lang.ClassLoader#getResourceAsStream( String )
     */
    public Enumeration getResources( String name )
            throws IOException
    {
        return getResources( name, getClassLoader() );
    }
    
    
    /**
     * Loads a resource with the given name, using the correct ClassLoader.
     * Does not swallow exceptions.  See the JDK documentation on resources
     * (they are pretty much files that are in the classpath of the
     * classloader).  Yes, this can be used successfully to get a class file
     * (well, JDK 1.1 throws a SecurityException if this is attempted).
     *
     * @param name absolute referece to the expected resource.
     * @param cl the classloader to load the references from.
     * @return a non-null list of resource URLs for the resource name.
     * @exception IOException if an I/O error occurs.
     * @see java.lang.ClassLoader#getResource( String )
     * @see java.lang.ClassLoader#getResources( String )
     * @see java.lang.ClassLoader#getResourceAsStream( String )
     */
    public Enumeration getResources( String name, ClassLoader cl )
            throws IOException
    {
        Enumeration enum = null;
        if (M_GET_RESOURCES != null && cl != null)
        {
            try
            {
                LOG.debug("Getting resources for "+name);
                enum = (Enumeration)M_GET_RESOURCES.invoke( cl,
                    new Object[] { name } );
                //LOG.debug("Found resources "+enum);
            }
            catch (java.lang.reflect.InvocationTargetException ite)
            {
                Throwable t = ite.getTargetException();
                if (t instanceof IOException)
                {
                    throw (IOException)t;
                }
                // else
                LOG.info( "getResources( "+name+" ) threw exception.", t );
                
                enum = null;
            }
            catch (Exception e)
            {
                LOG.info( "invoke on getResources( "+name+
                    " ) threw exception.", e );
                
                enum = null;
            }
        }
        
        
        // Yes, for invalid resources this will be incredibly inefficient.
        // However, we want to be as robust as possible for those resource
        // names that ARE valid.  JDK 1.2+ can be sooo fickle.
        if (enum == null || !enum.hasMoreElements())
        {
            LOG.debug("Resource enum is null or contains nothing.");
            
            // Try system resources next
            enum = getSystemResources( name );
            
            if (enum == null || !enum.hasMoreElements())
            {
                // Try a single resource last
                Vector v = new Vector();
                URL url = getResource( name, cl );
                if (url != null)
                {
                    LOG.debug( "classloader getResource returned "+url );
                    v.addElement( url );
                }
                // else give up
                enum = v.elements();
            }
        }
        
        return enum;
    }
    
    
    /**
     * Get the resource associated with the given name from the System
     * classloader.  This will never return <tt>null</tt>.
     *
     * @param name absolute referece to the expected resource.
     * @return a non-null list of URLs matching the resource.
     * @exception IOException if an I/O error occurs.
     * @see java.lang.ClassLoader#getResource( String )
     * @see java.lang.ClassLoader#getResources( String )
     * @see java.lang.ClassLoader#getResourceAsStream( String )
     */
    public Enumeration getSystemResources( String name )
            throws IOException
    {
        Enumeration enum = null;
        if (M_GET_SYSTEM_RESOURCES != null)
        {
            try
            {
                LOG.debug("Getting system resources for "+name);
                // static method
                enum = (Enumeration)M_GET_SYSTEM_RESOURCES.invoke( null,
                    new Object[] { name } );
                //LOG.debug("Found system resources "+enum);
            }
            catch (java.lang.reflect.InvocationTargetException ite)
            {
                Throwable t = ite.getTargetException();
                if (t instanceof IOException)
                {
                    throw (IOException)t;
                }
                // else
                LOG.info( "getSystemResources( "+name+
                    " ) threw exception.", t );
                
                enum = null;
            }
            catch (Exception e)
            {
                LOG.info( "invoke on getResources( "+name+
                    " ) threw exception.", e );
                
                enum = null;
            }
        }
        
        
        // Yes, for invalid resources this will be incredibly inefficient.
        // However, we want to be as robust as possible for those resource
        // names that ARE valid.
        if (enum == null || !enum.hasMoreElements())
        {
            LOG.debug("Resource enum is null or contains nothing.");
            Vector v = new Vector();
            URL url = getSystemResource( name );
            if (url != null)
            {
                LOG.debug( "classloader getSystemResource returned "+url );
                v.addElement( url );
            }
            // else give up
            
            enum = v.elements();
        }
        
        return enum;
    }

     
     
    //----------------------------
    // Protected methods
    
    
    /**
     * Gets the correct class loader.  May return null.
     *
     * @return the ClassLoader
     */
    protected ClassLoader getClassLoader()
    {
        ClassLoader cl = this.classLoader;
        if (cl == null)
        {
            cl = getThreadClassLoader( Thread.currentThread() );
            if (cl == null)
            {
                // JDK 1.1 may return NULL here.
                cl = this.getClass().getClassLoader();
            }
        }
        return cl;
    }
    
    
    /**
     * Loads a class with the given name, using the correct ClassLoader.
     * Does not swallow exceptions.
     *
     * @exception ClassNotFoundException if the class name is not known by the
     *      class loader.
     * @exception LinkageError if there was a basic class loader error.
     * @exception IllegalArgumentException if the class doesn't smell right to
     *      JDK 1.1.
     */
    protected Class loadClass( String name )
            throws ClassNotFoundException, LinkageError,
                IllegalArgumentException
    {
        ClassLoader cl = getClassLoader();
        Class c = null;
        if (name != null)
        {
            if (cl == null)
            {
                c = Class.forName( name );
            }
            else
            {
                c = cl.loadClass( name );
            }
        }
        return c;
    }
    
    
    /**
     * Use reflection to get the thread (context) class loader.
     */
    protected static ClassLoader getThreadClassLoader( Thread t )
    {
        ClassLoader cl = null;
        if (M_GET_CONTEXT_CLASSLOADER != null)
        {
            try
            {
                cl = (ClassLoader)M_GET_CONTEXT_CLASSLOADER.invoke(t, null);
            }
            catch (Exception e)
            {
                cl = null;
            }
        }
        return cl;
    }
    
    
    /**
     * 
     */
    protected static Method discoverContextClassloaderMethod()
    {
        Method m;
        try
        {
            Class c = Thread.class;
            m = c.getDeclaredMethod( "getContextClassLoader", new Class[0] );
        }
        catch (Exception e)
        {
            // discovery method: exception is expected where this is not
            // supoorted.
            // LOG.info( "discoverContextClassloaderMethod() threw exception.",
            //     e );
            
            m = null;
        }
        return m;
    }
    
    
    /**
     * 
     */
    protected static Method discoverGetResourcesMethod()
    {
        Method m;
        try
        {
            Class c = ClassLoader.class;
            m = c.getDeclaredMethod( "getResources",
                new Class[] { String.class } );
        }
        catch (Exception e)
        {
            // discovery method: exception is expected where this is not
            // supoorted.
            // LOG.info( "discoverGetResourcesMethod() threw exception.",
            //     e );
            
            m = null;
        }
        return m;
    }
    
    
    /**
     * 
     */
    protected static Method discoverGetSystemResourcesMethod()
    {
        Method m;
        try
        {
            Class c = ClassLoader.class;
            m = c.getDeclaredMethod( "getSystemResources",
                new Class[] { String.class } );
        }
        catch (Exception e)
        {
            // discovery method: exception is expected where this is not
            // supoorted.
            // LOG.info( "discoverGetSystemResourcesMethod() threw exception.",
            //     e );
            
            m = null;
        }
        return m;
    }

    
    /**
     * 
     */
    protected String getAbsoluteResourceName( String name )
    {
        // Using 'Class' getResourceAsStream, which is relative to its
        // package, whereas the ClassLoader is an absolute name.  So,
        // ensure that the name is absolute to be compatible.
        if (name != null && name.length() > 0 && name.charAt( 0 ) != '/')
        {
            name = '/' + name;
        }
        return name;
    }
}
 
