/*
 * 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.mx.loading;

import java.io.IOException;
import java.io.InputStream;
import java.io.ByteArrayOutputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.MalformedURLException;
import java.security.CodeSource;
import java.security.PermissionCollection;
import java.security.Policy;
import java.security.ProtectionDomain;
import java.security.Permission;
import java.security.cert.Certificate;
import java.util.Enumeration;
import java.util.Vector;
import java.util.HashSet;
import java.util.Collections;
import java.util.Set;
import javax.management.MalformedObjectNameException;
import javax.management.MBeanServer;
import javax.management.ObjectName;

import org.jboss.logging.Logger;
import org.jboss.util.loading.Translatable;
import org.jboss.util.loading.Translator;
import EDU.oswego.cs.dl.util.concurrent.ConcurrentReaderHashMap;

/**
* A ClassLoader which loads classes from a single URL in conjunction with
* the {@link LoaderRepository}. Notice that this classloader does
* not work independently of the repository. A repository reference
* must be provided via the constructor or the classloader must be explicitly
* registered to the repository before any attempt to load a class.
*
* At this point this is little more than an abstract class maintained as the
* interface for class loaders as the algorithm of the UnifiedLoaderRepository
* fails with deadlocks, and several other class loading exceptions in multi-
* threaded environments.
*
* @author <a href="marc.fleury@jboss.org">Marc Fleury</a>
* @author <a href="christoph.jung@jboss.org">Christoph G. Jung</a>
* @author <a href="scott.stark@jboss.org">Scott Stark</a>
* @author <a href="juha@jboss.org">Juha Lindfors</a>
* @author <a href="bill@jboss.org">Bill Burke</a>
* @version <tt>$Revision: 1.9.4.30 $</tt>
*/
public class UnifiedClassLoader
   extends URLClassLoader
   implements UnifiedClassLoaderMBean, Translatable
{
   // Static --------------------------------------------------------

   private static final Logger log = Logger.getLogger(UnifiedClassLoader.class);
   /** The value returned by {@link #getURLs}. */
   private static final URL[] EMPTY_URL_ARRAY = {};

   // Attributes ----------------------------------------------------

   /** Reference to the unified repository. */
   protected LoaderRepository repository = null;
   /** The location where unregister is called from */
   protected Exception unregisterTrace;

   /** One URL per ClassLoader in our case */
   protected URL url = null;
   /** An optional URL from which url may have been copied. It is used to
    allow the security permissions to be based on a static url namespace. */
   protected URL origURL = null;
   /** The relative order in which this class loader was added to the ULR */
   private int addedOrder;
   /** Names of classes which have resulted in CNFEs in loadClassLocally */
   private Set classBlacklist = Collections.synchronizedSet(new HashSet());
   /** Names of resources that were not found in loadResourceLocally */
   private Set resourceBlackList = Collections.synchronizedSet(new HashSet());
   /** A HashMap<String, URL> for resource found in loadResourceLocally */
   private ConcurrentReaderHashMap resourceCache = new ConcurrentReaderHashMap();

   // Constructors --------------------------------------------------
   /**
    * Construct a <tt>UnifiedClassLoader</tt> without registering it to the
    * classloader repository.
    *
    * @param url   the single URL to load classes from.
    */
   public UnifiedClassLoader(URL url)
   {
      this(url, (URL) null);
   }
   /**
    * Construct a <tt>UnifiedClassLoader</tt> without registering it to the
    * classloader repository.
    *
    * @param url   the single URL to load classes from.
    * @param origURL the possibly null original URL from which url may
    * be a local copy or nested jar.
    */
   public UnifiedClassLoader(URL url, URL origURL)
   {
      this(url, origURL, UnifiedClassLoader.class.getClassLoader());
   }

   /**  Construct a UnifiedClassLoader without registering with the
    * classloader repository.
    *
    * @param url   the single URL to load classes from.
    * @param origURL the possibly null original URL from which url may
    * be a local copy or nested jar.
    * @param parent the parent class loader to use
    */
   public UnifiedClassLoader(URL url, URL origURL, ClassLoader parent)
   {
      super(url == null ? new URL[]{} : new URL[] {url}, parent);

      log.debug("New jmx UCL with url " + url);
      this.url = url;
      this.origURL = origURL;
   }

   /**
    * Construct a <tt>UnifiedClassLoader</tt> and registers it to the given
    * repository.
    *
    * @param   url   The single URL to load classes from.
    * @param   repository the repository this classloader delegates to
    */
   public UnifiedClassLoader(URL url, LoaderRepository repository)
   {
      this(url, null, repository);
   }
   /**
    * Construct a <tt>UnifiedClassLoader</tt> and registers it to the given
    * repository.
    * @param url The single URL to load classes from.
    * @param origURL the possibly null original URL from which url may
    * be a local copy or nested jar.
    * @param repository the repository this classloader delegates to
    * be a local copy or nested jar.
    */
   public UnifiedClassLoader(URL url, URL origURL, LoaderRepository repository)
   {
      this(url, origURL);

      // set the repository reference
      this.setRepository(repository);

      // register this loader to the given repository
      repository.addClassLoader(this);
   }

   /**
    * UnifiedClassLoader constructor that can be used to
    * register with a particular Loader Repository identified by ObjectName.
    *
    * @param url an <code>URL</code> value
    * @param server a <code>MBeanServer</code> value
    * @param repositoryName an <code>ObjectName</code> value
    * @exception Exception if an error occurs
    */
   public UnifiedClassLoader(final URL url, final MBeanServer server,
      final ObjectName repositoryName) throws Exception
   {
      this(url, null, server, repositoryName);
   }
   /**
    * UnifiedClassLoader constructor that can be used to
    * register with a particular Loader Repository identified by ObjectName.
    *
    * @param url an <code>URL</code> value
    * @param origURL the possibly null original URL from which url may
    * be a local copy or nested jar.
    * @param server a <code>MBeanServer</code> value
    * @param repositoryName an <code>ObjectName</code> value
    * @exception Exception if an error occurs
    */
   public UnifiedClassLoader(final URL url, final URL origURL,
      final MBeanServer server, final ObjectName repositoryName) throws Exception
   {
      this(url, origURL);
      LoaderRepository rep = (LoaderRepository)server.invoke(repositoryName,
                    "registerClassLoader",
                    new Object[] {this},
                    new String[] {getClass().getName()});
      this.setRepository(rep);
   }

   // Public --------------------------------------------------------

   public int getAddedOrder()
   {
      return addedOrder;
   }
   public void setAddedOrder(int addedOrder)
   {
      this.addedOrder = addedOrder;
   }

   /** Obtain the ObjectName under which the UCL can be registered with the
    JMX server. This creates a name of the form "jmx.loading:UCL=hashCode"
    since we don't currently care that UCL be easily queriable.
    */
   public ObjectName getObjectName() throws MalformedObjectNameException
   {
      String name = "jmx.loading:UCL="+Integer.toHexString(super.hashCode());
      return new ObjectName(name);
   }

   public void unregister()
   {
      log.debug("Unregistering ucl="+this);
      if (repository != null)
      {
         repository.removeClassLoader(this);
      }
      this.resourceBlackList.clear();
      this.classBlacklist.clear();
      this.resourceCache.clear();
      this.origURL = null;
      this.url = null;
      this.repository = null;
      this.unregisterTrace = new Exception();
   }

   /** Append the given url to the URLs used for class and resource loading
    * @param url the URL to load from
    */
   public void addURL(URL url)
   {
      if( url == null )
         throw new IllegalArgumentException("url cannot be null");

      if( repository.addClassLoaderURL(this, url) == true )
      {
         log.debug("Added url: "+url+", to ucl: "+this);
         // Strip any query parameters
         String query = url.getQuery();
         if( query != null )
         {
            String ext = url.toExternalForm();
            String ext2 = ext.substring(0, ext.length() - query.length() - 1);
            try
            {
               url = new URL (ext2);
            }
            catch(MalformedURLException e)
            {
               log.warn("Failed to strip query from: "+url, e);
            }
         }
         super.addURL(url);
         clearBlacklists();
      }
      else if( log.isTraceEnabled() )
      {
         log.trace("Ignoring duplicate url: "+url+", for ucl: "+this);
      }
   }

   public void clearBlacklists()
   {
      resourceBlackList.clear();
      classBlacklist.clear();
   }

   public LoaderRepository getLoaderRepository()
   {
      return repository;
   }

   public void setRepository(LoaderRepository repository)
   {
      log.debug("setRepository, r="+repository+", ucl="+this);
      this.repository = repository;
   }

   /** Called to attempt to load a class from the set of URLs associated with
    * the UCL.
    */
   public Class loadClassLocally(String name, boolean resolve)
      throws ClassNotFoundException
   {
      boolean trace = log.isTraceEnabled();
      if( trace )
         log.trace("loadClassLocally, name="+name);
      if (classBlacklist.contains(name))
      {
         if( trace )
            log.trace("Class in blacklist, name="+name);
         throw new ClassNotFoundException("Class Not Found(blacklist): " + name);
      }

      Class clazz = null;
      try
      {
         clazz = super.loadClass(name, resolve);
         return clazz;
      }
      catch (ClassNotFoundException cnfe)
      {
         classBlacklist.add(name);
         // If this is an array class, use Class.forName to resolve it
         if( name.charAt(0) == '[' )
         {
            clazz = Class.forName(name, true, this);
            classBlacklist.remove(name);
            return clazz;
         }
         if( trace )
            log.trace("CFNE: Adding to blacklist: "+name);
         throw cnfe;
      }
   }

   /**
   * Provides the same functionality as {@link java.net.URLClassLoader#getResource}.
   */
   public URL getResourceLocally(String name)
   {
      URL resURL = (URL) resourceCache.get(name);
      if (resURL != null)
         return resURL;
      if (resourceBlackList.contains(name))
         return null;
      resURL = super.getResource(name);
      if( log.isTraceEnabled() == true )
         log.trace("getResourceLocally("+this+"), name="+name+", resURL:"+resURL);
      if (resURL == null)
         resourceBlackList.add(name);
      else
         resourceCache.put(name, resURL);
      return resURL;
   }

   /** Get the URL associated with the UCL.
    */
   public URL getURL()
   {
      return url;
   }
   /** Get the original URL associated with the UCL. This may be null.
    */
   public URL getOrigURL()
   {
      return origURL;
   }

   /**
   * This method simply invokes the super.getURLs() method to access the
   * list of URLs that make up the UnifiedClassLoader classpath.
   */
   public URL[] getClasspath()
   {
      return super.getURLs();
   }

   // URLClassLoader overrides --------------------------------------

   /** The only caller of this method should be the VM initiated
   * loadClassInternal() method. This method attempts to acquire the
   * UnifiedLoaderRepository2 lock and then asks the repository to
   * load the class.
   *
   * <p>Forwards request to {@link LoaderRepository}.
   */
   public Class loadClass(String name, boolean resolve)
      throws ClassNotFoundException
   {
      Class c = repository.loadClass(name, resolve, this);
      return c;
   }

   /**
   * Attempts to load the resource from its URL and if not found
   * forwards to the request to {@link LoaderRepository}.
   */
   public URL getResource(String name)
   {
      URL res = repository.getResource(name, this);
      return res;
   }

   /** Find all resource URLs for the given name. This overrides the
    * URLClassLoader version to look for resources in the repository.
    *
    * @param name the name of the resource
    * @return Enumeration<URL>
    * @throws java.io.IOException
    */
   public Enumeration findResources(String name) throws IOException
   {
      Vector resURLs = new Vector();
      repository.getResources(name, this, resURLs);
      return resURLs.elements();
   }

   /**
   * Provides the same functionality as {@link java.net.URLClassLoader#findResources}.
   */
   public Enumeration findResourcesLocally(String name) throws IOException
   {
      Enumeration resURLs = super.findResources(name);
      return resURLs;
   }

   /** This is here to document that this must delegate to the
   super implementation to perform identity based hashing. Using
   URL based hashing caused conflicts with the Class.forName(String,
   boolean, ClassLoader).
   */
   public final int hashCode()
   {
      int hash = super.hashCode();
      return hash;
   }

   /** This is here to document that this must delegate to the
   super implementation to perform identity based equality. Using
   URL based equality caused conflicts with the Class.forName(String,
   boolean, ClassLoader).
   */
   public final boolean equals(Object other)
   {
      boolean equals = super.equals(other);
      return equals;
   }
   /**
   * Return all library URLs associated with this UnifiedClassLoader
   *
   * <p>Do not remove this method without running the WebIntegrationTestSuite
   */
   public URL[] getAllURLs()
   {
      return repository.getURLs();
   }

   /**
   * Return an empty URL array to force the RMI marshalling subsystem to
   * use the <tt>java.server.codebase</tt> property as the annotated codebase.
   *
   * <p>Do not remove this method without discussing it on the dev list.
   *
   * @return Empty URL[]
   */
   public URL[] getURLs()
   {
      return EMPTY_URL_ARRAY;
   }

   public Package getPackage(String name)
   {
      return super.getPackage(name);
   }
   public Package[] getPackages()
   {
      return super.getPackages();
   }

   /**
   * Retruns a string representaion of this UCL.
   */
   public String toString()
   {
      return super.toString() + "{ url=" + getURL() + " }";
   }

    /** Called by loadClassLocally to find the requested class within this
     * class loaders class path.
     *
     * @param name the name of the class
     * @return the resulting class
     * @exception ClassNotFoundException if the class could not be found
     */
   protected Class findClass(String name) throws ClassNotFoundException
   {
      boolean trace = log.isTraceEnabled();
      if( trace )
         log.trace("findClass, name="+name);
      if (classBlacklist.contains(name))
      {
         if( trace )
            log.trace("Class in blacklist, name="+name);
         throw new ClassNotFoundException("Class Not Found(blacklist): " + name);
      }

      Translator translator = repository.getTranslator();
      if (translator != null)
      {
         // Obtain the transformed class bytecode 
         try
         {
            // Obtain the raw bytecode from the classpath
            byte[] rawcode = loadByteCode(name);
            ProtectionDomain pd = getProtectionDomain();
            byte[] bytecode = translator.transform(this, name, null, pd, rawcode);
            // If there was no transform use the raw bytecode
            if( bytecode == null )
               bytecode = rawcode;
            // Define the class package and instance
            definePackage(name);
            return defineClass(name, bytecode, 0, bytecode.length, pd);
         }
         catch(ClassNotFoundException e)
         {
            throw e;
         }
         catch (Throwable ex)
         {
            throw new ClassNotFoundException(name, ex);
         }
      }

      Class clazz = null;
      try
      {
         clazz = super.findClass(name);
      }
      catch(ClassNotFoundException e)
      {
         if( trace )
            log.trace("CFNE: Adding to blacklist: "+name);
         classBlacklist.add(name);
         throw e;
      }
      return clazz;
   }

   /**
    * Define the package for the class if not already done
    *
    * @todo this properly
    * @param className the class name
    */
   protected void definePackage(String className)
   {
      int i = className.lastIndexOf('.');
      if (i == -1)
         return;

      try
      {
         definePackage(className.substring(0, i), null, null, null, null, null, null, null);
      }
      catch (IllegalArgumentException alreadyDone)
      {
      }
   }

   /** Override the permissions accessor to use the CodeSource
    based on the original URL if one exists. This allows the
    security policy to be defined in terms of the static URL
    namespace rather than the local copy or nested URL.
    This builds a PermissionCollection from:
    1. The origURL CodeSource
    2. The argument CodeSource
    3. The Policy.getPermission(origURL CodeSource)

    This is necessary because we cannot define the CodeSource the
    SecureClassLoader uses to register the class under.

    @param cs the location and signatures of the codebase.
    */
   protected PermissionCollection getPermissions(CodeSource cs)
   {
      CodeSource permCS = cs;
      if( origURL != null )
      {
         permCS = new CodeSource(origURL, cs.getCertificates());
      }
      Policy policy = Policy.getPolicy();
      PermissionCollection perms = super.getPermissions(permCS);
      PermissionCollection perms2 = super.getPermissions(cs);
      PermissionCollection perms3 = policy.getPermissions(permCS);
      Enumeration iter = perms2.elements();
      while( iter.hasMoreElements() )
         perms.add((Permission) iter.nextElement());
      iter = perms3.elements();
      while( iter.hasMoreElements() )
         perms.add((Permission) iter.nextElement());
      if( log.isTraceEnabled() )
         log.trace("getPermissions, url="+url+", origURL="+origURL+" -> "+perms);
      return perms;
   }

   /**
    * Determine the protection domain. If we are a copy of the original
    * deployment, use the original url as the codebase.
    * @return the protection domain
    * @todo certificates and principles?
    */
   protected ProtectionDomain getProtectionDomain()
   {
      CodeSource cs = new CodeSource(origURL != null ? origURL : url, (Certificate[]) null);
      PermissionCollection permissions = Policy.getPolicy().getPermissions(cs);
      if (log.isTraceEnabled())
         log.trace("getProtectionDomain, url=" + url + ", origURL=" + origURL +
                   " codeSource=" + cs + " permissions=" + permissions);
      return new ProtectionDomain(cs, permissions);
   }
   /**
    * Determine the protection domain. If we are a copy of the original
    * deployment, use the original url as the codebase.
    * @return the protection domain
    * @todo certificates and principles?
    */
   protected ProtectionDomain getProtectionDomain(URL codesourceUrl)
   {
      CodeSource cs = new CodeSource(codesourceUrl, (Certificate[]) null);
      PermissionCollection permissions = Policy.getPolicy().getPermissions(cs);
      if (log.isTraceEnabled())
         log.trace("getProtectionDomain, url=" + url + ", origURL=" + origURL +
                   " codeSource=" + cs + " permissions=" + permissions);
      return new ProtectionDomain(cs, permissions);
   }

   /** Obtain the bytecode for the indicated class from this class loaders
    * classpath.
    * 
    * @param classname
    * @return the bytecode array if found
    * @exception ClassNotFoundException - if the class resource could not
    *    be found
    */ 
   protected byte[] loadByteCode(String classname)
      throws ClassNotFoundException, IOException
   {
      byte[] bytecode = null;
      String classRsrcName = classname.replace('.', '/') + ".class";
      URL classURL = this.getResourceLocally(classRsrcName);
      if( classURL == null )
      {
         String msg = "Failed to find: "+classname+" as resource: "+classRsrcName;
         throw new ClassNotFoundException(msg);
      }

      // Load the class bytecode
      InputStream is = null;
      try
      {
         is = classURL.openStream();
         ByteArrayOutputStream baos = new ByteArrayOutputStream();
         byte[] tmp = new byte[1024];
         int read = 0;
         while( (read = is.read(tmp)) > 0 )
         {
            baos.write(tmp, 0, read);
         }
         bytecode = baos.toByteArray();
      }
      finally
      {
         if( is != null )
            is.close();
      }

      return bytecode;
   }
 
}
