/*
 * JBoss, Home of Professional Open Source
 * Copyright 2005, JBoss Inc., and individual contributors as indicated
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.remoting.loading;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Collections;
import java.util.Map;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.lang.ref.ReferenceQueue;

import org.jboss.logging.Logger;

/**
 * ClassByteClassLoader is a classloader that will allow dynamic adding of classes from a remote machine
 * to be added and visible locally.
 *
 * @author <a href="mailto:jhaynie@vocalocity.net">Jeff Haynie</a>
 * @version $Revision: 1.3.4.3 $
 */
public class ClassByteClassLoader extends ClassLoader
{
    private static final boolean logExtra = Boolean.getBoolean("jboss.remoting.classloader.debug");
    private static final Logger log = Logger.getLogger(ClassByteClassLoader.class);
    private final Map loadedClasses=Collections.synchronizedMap(new java.util.HashMap());
    private final Map loadedResources=Collections.synchronizedMap(new java.util.HashMap());
    private final ReferenceQueue queue = new ReferenceQueue();

    public ClassByteClassLoader ()
    {
        super();
    }

    public ClassByteClassLoader (ClassLoader parent)
    {
        super(parent);
    }

    /**
     * inner class will keep an local reference to the key
     * so we can lookup the resource File that we are using
     * on deletion to ensure we've cleaned it up
     */
    private final class MyRef extends WeakReference
    {
		private final String key;

		MyRef(String key, Object obj)
		{
			super(obj);
			this.key=key;
		}
	}
	/**
	 * clean an individual reference and delete any remaining
	 * resources that are still being referenced by it
	 */
	private void clean (MyRef myref)
	{
		loadedClasses.remove(myref.key);
		File f=(File)loadedResources.remove(myref.key);
		if (f!=null)
		{
		   f.delete();
		}
		myref.clear();
		myref=null;
	}
	/**
	 * will walk through all objects (if any) in the ReferenceQueue and
	 * make sure we've cleaned up our released class references
	 */
    private void performMaintenance ()
    {
		int count = 0;
		Reference ref = null;
		while ((ref = queue.poll())!=null)
		{
			count++;
			MyRef myref=(MyRef)ref;
			clean(myref);
		}

		if (count>0 && log.isDebugEnabled()) log.debug("ClassByteClassLoader reclaimed "+count+" objects");
	}


    public String toString()
    {
        return "ClassByteClassLoader ["+loadedClasses+"]";
    }

    protected void finalize () throws Throwable
    {
		// make sure we're OK on the reference queue
		performMaintenance();

        java.util.Iterator iter=loadedResources.values().iterator();
        while(iter.hasNext())
        {
            ((java.io.File)iter.next()).delete();
        }
        loadedResources.clear();
        loadedClasses.clear();
        super.finalize ();
    }

    public Class loadClass (String className, ClassBytes bytes[])
        throws ClassNotFoundException, java.io.IOException
    {
		// make sure we're OK on the reference queue
		performMaintenance();

        if (logExtra && log.isDebugEnabled())
        {
            log.debug("loadClass: "+className+", bytes: "+bytes);
        }
        if (bytes!=null)
        {
            for (int c=0;c<bytes.length;c++)
            {
                addClass(bytes[c]);
            }
        }
        Class cl = lookupCachedClass(className);
        if (cl!=null)
        {
			return cl;
        }
        cl = findLoadedClass(className);
        if (cl!=null)
        {
            return cl;
        }
        cl = ClassLoader.getSystemClassLoader().loadClass(className);
        if (cl!=null)
        {
            return cl;
        }
        cl = super.loadClass(className,true);
        if (cl!=null)
        {
            return cl;
        }
        throw new ClassNotFoundException(className);
    }
    private void addClassResource (String name, byte buf[])
        throws IOException
    {
		// make sure we're OK on the reference queue
		performMaintenance();

        OutputStream out=null;
        File file=null;
        try
        {
            file=File.createTempFile("cbc",".class");
            file.deleteOnExit();
            if (logExtra && log.isDebugEnabled())
            {
                log.debug("adding resource at: "+name+" to file: "+file);
            }
            out = new FileOutputStream(file);
            out.write(buf);
            out.flush();
        }
        catch (java.io.IOException ex)
        {
            file = null;
            throw ex;
        }
        finally
        {
            if (out!=null)
            {
                try { out.close(); } catch (Exception ig){ }
                out=null;
            }
            if (file!=null)
            {
                loadedResources.put(name,file);
            }
        }
    }

    public java.io.InputStream getResourceAsStream (String name)
    {
		// make sure we're OK on the reference queue
		performMaintenance();

        String denormalized = name.replace('/','.').substring(0,name.length()-6);
        java.io.File file = (java.io.File)loadedResources.get(denormalized);
        if (logExtra && log.isDebugEnabled())
        {
            log.debug("getResourceAsStream =>"+denormalized+" = "+file);
        }
        if (file!=null && file.exists())
        {
            try
            {
                return new java.io.BufferedInputStream(new java.io.FileInputStream(file));
            }
            catch (Exception ex)
            {

            }
        }
        return super.getResourceAsStream(name);
    }

    public void addClass (ClassBytes classBytes)
        throws java.io.IOException
    {
		// make sure we're OK on the reference queue
		performMaintenance();

        Class cl=null;
        String name = classBytes.getClassName();
        if (loadedClasses.containsKey(name)==false)
        {
            byte buf[] = classBytes.getClassBytes();
            boolean array = ClassUtil.isArrayClass(name);
            String cn = (array) ? ClassUtil.getArrayClassPart(name) : name;
            if (logExtra && log.isDebugEnabled())
            {
                log.debug("  add class: "+name+", array?"+array+", using as: "+cn);
            }
			cl = defineClass(cn,buf,0,buf.length);
			resolveClass(cl);
			addClassResource(cn,buf);
			loadedClasses.put(cn,new MyRef(cn,cl));
        }
    }

    /**
     * lookup a cached class and return null if not found
     */
    private Class lookupCachedClass (String cn)
    {
        Class cl = null;
        MyRef ref = (MyRef)loadedClasses.get(cn);
        if (ref!=null)
        {
			// make sure we've not gotten cleared
			cl = (Class)ref.get();
			if (cl==null)
			{
				// oops, we've been cleared
				clean(ref);
			}
		}
		return cl;
	}

    /**
     * Finds the specified class. This method should be overridden
     * by class loader implementations that follow the new delegation model
     * for loading classes, and will be called by the <code>loadClass</code>
     * method after checking the parent class loader for the requested class.
     * The default implementation throws <code>ClassNotFoundException</code>.
     *
     * @param  name the name of the class
     * @return the resulting <code>Class</code> object
     * @exception ClassNotFoundException if the class could not be found
     * @since  1.2
     */
    protected Class findClass (String name) throws ClassNotFoundException
    {
		// make sure we're OK on the reference queue
		performMaintenance();

        boolean array = ClassUtil.isArrayClass(name);
        String cn = (array) ? ClassUtil.getArrayClassPart(name) : name;
        if (logExtra && log.isDebugEnabled())
        {
            log.debug("++ loadClass: "+name+", array?"+array+", normalized: ["+cn+"]");
        }
        Class cl = lookupCachedClass(cn);
        // search the mapped classes first
        if (cl==null)
        {
			// search already loaded classes
			cl = findLoadedClass(cn);
		}
		if (cl!=null)
		{
			if (array)
			{
				// we have to create an instance from the Class Part and return the
				// class ref from it
				Object obj = java.lang.reflect.Array.newInstance(cl,1);
				return obj.getClass();
			}
			return cl;
		}
        if (logExtra && log.isDebugEnabled())
        {
            log.debug("++ findClass: "+name+" not found, throwing ClassNotFoundException");
        }
        throw new ClassNotFoundException(name);
    }
}
