/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2006, Red Hat Middleware LLC, and individual contributors
 * as indicated by the @author tags. See the copyright.txt file 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.net.URL;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import javax.management.AttributeNotFoundException;
import javax.management.InstanceNotFoundException;
import javax.management.MBeanException;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.management.ReflectionException;

import org.jboss.mx.loading.LoadMgr3.PkgClassLoader;
import org.jboss.mx.util.ObjectNameFactory;

/** A simple extension of UnifiedLoaderRepository3 that adds the notion of a
 * parent UnifiedLoaderRepository. Classes and resources are loaded from child
 * first and then the parent depending on the java2ParentDelegation flag.
 *
 * @author Scott.Stark@jboss.org
 * @version $Revision: 57200 $
 */
public class HeirarchicalLoaderRepository3 extends UnifiedLoaderRepository3
{
   private static ObjectName DEFAULT_LOADER_OBJECT_NAME = ObjectNameFactory.create(DEFAULT_LOADER_NAME);

   /** A ClassLoader override that prevents a child class loader from looking
    * beyond its URLs for classes.
    */
   static class NoParentClassLoader extends ClassLoader
   {
      NoParentClassLoader()
      {
         super(HeirarchicalLoaderRepository3.class.getClassLoader());
      }

      /** Override to always return null to force the UCL to only load from
       * its URLs.
       * @param name
       * @return
       */
      public URL getResource(String name)
      {
         return null;
      }
      /** Override to always throw a CNFE to force the UCL to only load from
       * its URLs.
       * @param name
       * @param resolve
       * @return nothing
       * @throws ClassNotFoundException always
       */ 
      protected synchronized Class loadClass(String name, boolean resolve)
            throws ClassNotFoundException
      {
         throw new ClassNotFoundException("NoParentClassLoader has no classes");
      }
      /** Override to always throw a CNFE to force the UCL to only load from
       * its URLs.
       * @param name
       * @return
       * @throws ClassNotFoundException
       */ 
      protected Class findClass(String name) throws ClassNotFoundException
      {
         throw new ClassNotFoundException("NoParentClassLoader has no classes");
      }
   }
   static class CacheClassLoader extends UnifiedClassLoader3
   {
      Class cacheClass;
      CacheClassLoader(Class cacheClass, LoaderRepository rep)
      {
         super(null, null, new NoParentClassLoader(), rep);
         this.cacheClass = cacheClass;
      }

      protected Class findClass(String name) throws ClassNotFoundException
      {
         Class c = cacheClass;
         if( name.equals(cacheClass.getName()) == false )
            c = null;
         return c;
      }
   }

   /** The repository to which we delegate if requested classes or resources
    are not available from this repository.
    */
   private UnifiedLoaderRepository3 parentRepository;
   /** A flag indicating if the standard parent delegation loading where the
    parent repository is used before this repository.
    */
   private boolean java2ParentDelegation;

   /** The package classloader */
   private PkgClassLoader packageClassLoader;
   
   /** Create a HeirarchicalLoaderRepository3 with an explicit parent.
    * 
    * @param parent
    * @throws AttributeNotFoundException
    * @throws InstanceNotFoundException
    * @throws MBeanException
    * @throws ReflectionException
    */ 
   public HeirarchicalLoaderRepository3(UnifiedLoaderRepository3 parent)
      throws AttributeNotFoundException, InstanceNotFoundException, MBeanException, ReflectionException
   {
      this.parentRepository = parent;
      init();
   }
   /** Create a HeirarchicalLoaderRepository3 with a parent obtained by querying
    * the server for the ServerConstants.DEFAULT_LOADER_NAME mbean.
    * 
    * @param server
    * @throws AttributeNotFoundException
    * @throws InstanceNotFoundException
    * @throws MBeanException
    * @throws ReflectionException
    */ 
   public HeirarchicalLoaderRepository3(MBeanServer server)
      throws AttributeNotFoundException, InstanceNotFoundException, MBeanException, ReflectionException
   {
      this(server, DEFAULT_LOADER_OBJECT_NAME);
   }
   /** Create a HeirarchicalLoaderRepository3 with a parent obtained by querying
    * the server for the parentName mbean.
    * 
    * @param server
    * @param parentName
    * @throws AttributeNotFoundException
    * @throws InstanceNotFoundException
    * @throws MBeanException
    * @throws ReflectionException
    */ 
   public HeirarchicalLoaderRepository3(MBeanServer server, ObjectName parentName)
      throws AttributeNotFoundException, InstanceNotFoundException, MBeanException, ReflectionException
   {
      this.parentRepository = (UnifiedLoaderRepository3) server.getAttribute(parentName,
                    "Instance");
      init();
   }

   /**
    * Initialisation
    */
   private void init()
   {
      // Include a class loader with a parent to the system class loader
      ClassLoader loader = RepositoryClassLoader.class.getClassLoader();
      RepositoryClassLoader ucl = null;
      if( loader instanceof RepositoryClassLoader )
         ucl = (RepositoryClassLoader) loader;
      else
         ucl = new UnifiedClassLoader3(null, null, HeirarchicalLoaderRepository3.this);
      packageClassLoader = new PkgClassLoader(ucl, 3);
   }
   
   // Public --------------------------------------------------------

   public RepositoryClassLoader newClassLoader(final URL url, boolean addToRepository)
      throws Exception
   {
      UnifiedClassLoader3 ucl = null;
      if( java2ParentDelegation == false )
         ucl = new UnifiedClassLoader3(url, null, new NoParentClassLoader(), this);
      else
         ucl = new UnifiedClassLoader3(url, null, this);

      if( addToRepository )
      {
         this.addClassLoader(ucl);
      }
      return ucl;
   }
   public RepositoryClassLoader newClassLoader(final URL url, final URL origURL, boolean addToRepository)
      throws Exception
   {
      UnifiedClassLoader3 ucl = null;
      if( java2ParentDelegation == false )
         ucl = new UnifiedClassLoader3(url, origURL, new NoParentClassLoader(), this);
      else
         ucl = new UnifiedClassLoader3(url, origURL, this);

      if( addToRepository )
      {
         this.addClassLoader(ucl);
      }
      return ucl;
   }

   /** Get the use parent first flag. This indicates whether the parent
    * repository is consulted first for resource and class loading or if the
    * HeirchicalLoaderRepository is consulted first.
    *
    * @return true if the parent repository is consulted first, false if the
    * HeirchicalLoaderRepository is consulted first.
    */
   public boolean getUseParentFirst()
   {
      return java2ParentDelegation;
   }
   /** Set the use parent first flag. This indicates whether the parent
    * repository is consulted first for resource and class loading or if the
    * HeirchicalLoaderRepository is consulted first.
    *
    * @param flag true if the parent repository is consulted first, false if the
    * HeirchicalLoaderRepository is consulted first.
    */
   public void setUseParentFirst(boolean flag)
   {
      java2ParentDelegation = flag;
   }

   /** Load a class using the repository class loaders.
    *
    * @param name The name of the class
    * @param resolve an obsolete unused parameter from ClassLoader.loadClass
    * @param scl The asking class loader
    * @return The loaded class
    * @throws ClassNotFoundException If the class could not be found.
    */
   public Class loadClass(String name, boolean resolve, ClassLoader scl)
      throws ClassNotFoundException
   {
      Class foundClass = null;

      if( java2ParentDelegation == true )
      {
         try
         {
            // Try the parent repository first
            foundClass = parentRepository.loadClass(name, resolve, scl);
         }
         catch(ClassNotFoundException e)
         {
            // Next try our repository
            if( foundClass == null )
               foundClass = super.loadClass(name, resolve, scl);
         }
      }
      else
      {
         try
         {
            // Try this repository first
            foundClass = super.loadClass(name, resolve, scl);
         }
         catch(ClassNotFoundException e)
         {
            // Next try our parent repository
            if( foundClass == null )
               foundClass = parentRepository.loadClass(name, resolve, scl);
         }
      }

      if( foundClass != null )
         return foundClass;

      /* If we reach here, all of the classloaders currently in the VM don't
         know about the class
      */
      throw new ClassNotFoundException(name);
   }

   /** Override getCachedClass to return the parent repository cached class
    * if java2ParentDelegation=true, followed by this repository's cached
    * value. Else, if java2ParentDelegation=false, only check this repository's
    * cache to attempt to load the class from the child repository before
    * going to the parent cache.
    * 
    * @param classname
    * @return the cached class if found, null otherwise
    */ 
   public Class getCachedClass(String classname)
   {
      Class clazz = null;
      if( java2ParentDelegation == true )
      {
         // Try the parent repository
         clazz = parentRepository.getCachedClass(classname);
         // Next try our parent repository
         if( clazz == null )
            clazz = super.getCachedClass(classname);
      }
      else
      {
         // Try this repository
         clazz = super.getCachedClass(classname);
      }
      return clazz;
   }

   /** Find a resource from this repository. This first looks to this
    * repository and then the parent repository.
    * @param name The name of the resource
    * @param scl The asking class loader
    * @return An URL for reading the resource, or <code>null</code> if the
    *          resource could not be found.
    */
   public URL getResource(String name, ClassLoader scl)
   {
      URL resource = null;

      if( java2ParentDelegation == true )
      {
         /* Try our parent repository. This cannot use the getResource method
         because we do not want the parent repository to load the resource via
         our scoped class loader
         */
         resource = getParentResource(name, scl);
         // Next try this repository
         if( resource == null )
            resource = super.getResource(name, scl);
      }
      else
      {
         // Try this repository
         resource = super.getResource(name, scl);
         // Next try our parent repository
         if( resource == null )
         {
            /* Try our parent repository. This cannot use the getResource method
            because we do not want the parent repository to load the resource via
            our scoped class loader
            */
            resource = getParentResource(name, scl);
         }
      }

      return resource;
   }

   /** Find all resource URLs for the given name. This is entails an
    * exhuastive search of this and the parent repository and is an expensive
    * operation.
    *
    * @param name the resource name
    * @param cl the requesting class loader
    * @param urls a list into which the located resource URLs will be placed
    */
   public void getResources(String name, ClassLoader cl, List urls)
   {
      if( java2ParentDelegation == true )
      {
         // Get the parent repository resources
         parentRepository.getResources(name, cl, urls);
         // Next get this repositories resources
         super.getResources(name, cl, urls);
      }
      else
      {
         // Get this repositories resources
         super.getResources(name, cl, urls);
         // Next get the parent repository resources
         parentRepository.getResources(name, cl, urls);
      }
   }

   /** Obtain a listing of the URLs for all UnifiedClassLoaders associated with
    *the repository
    */
   public URL[] getURLs()
   {
      URL[] ourURLs = super.getURLs();
      URL[] parentURLs = parentRepository.getURLs();
      int size = ourURLs.length + parentURLs.length;
      URL[] urls = new URL[size];
      System.arraycopy(ourURLs, 0, urls, 0, ourURLs.length);
      System.arraycopy(parentURLs, 0, urls, ourURLs.length, parentURLs.length);
      return urls;
   }

   /** Called by LoadMgr to locate a previously loaded class. This looks
    * first to this repository and then the parent repository.
    *@return the cached class if found, null otherwise
    */
   public Class loadClassFromCache(String name)
   {
      Class foundClass = null;

      if( java2ParentDelegation == true )
      {
         // Try this repository
         foundClass = parentRepository.loadClassFromCache(name);
         // Next try our parent repository
         if( foundClass == null )
            foundClass = super.loadClassFromCache(name);
      }
      else
      {
         // Try this repository
         foundClass = super.loadClassFromCache(name);
         /* We do not try the parent repository cache as this does not allow
         the child repository to override classes in the parent
         */
      }
      return foundClass;
   }

   /** Called by LoadMgr to obtain all class loaders. This returns a set of
    * PkgClassLoader with the HeirarchicalLoaderRepository3 ordered ahead of
    * the parent repository pkg class loaders
    *@return Set<PkgClassLoader>
    */
   public Set getPackageClassLoaders(String name)
   {
      Set pkgSet = super.getPackageClassLoaders(name);
      Set parentPkgSet = parentRepository.getPackageClassLoaders(name);
      GetClassLoadersAction action = new GetClassLoadersAction(name, pkgSet,
         parentPkgSet);
      Set theSet = (Set) AccessController.doPrivileged(action);
      return theSet;

   }

   public int compare(LoaderRepository lr)
   {
      if (lr == this)
         return 0;
      return reverseCompare(lr);
   }
   
   protected int reverseCompare(LoaderRepository lr)
   {
      // If it is not our parent we don't care
      if (lr != parentRepository)
         return 0;
      
      // The order depends upon the delegation model
      if (java2ParentDelegation)
         return +1;
      else
         return -1;
   }

   /** A subset of the functionality found in getResource(String, ClassLoader),
    * but this version queries the parentRepository and does not use the scl
    * to avoid leaking class loaders across scoped.
    * 
    * @param name - the resource name
    * @param scl - the requesting class loader
    * @return the resource URL if found, null otherwise
    */
   private URL getParentResource(String name, ClassLoader scl)
   {
      // Not found in classloader, ask the global cache
      URL resource = parentRepository.getResourceFromGlobalCache(name);

      // The cache has it, we are done
      if( resource != null )
         return resource;

      // Not visible in global cache, iterate on all classloaders
      resource = parentRepository.getResourceFromRepository(name, scl);

      return resource;
   }

   private class GetClassLoadersAction implements PrivilegedAction
   {
      private String name;
      Set pkgSet;
      Set parentPkgSet;

      GetClassLoadersAction(String name, Set pkgSet, Set parentPkgSet)
      {
         this.name = name;
         this.pkgSet = pkgSet;
         this.parentPkgSet = parentPkgSet;
      }

      public Object run()
      {
         // Build a set of PkgClassLoader
         Set theSet = ClassLoaderUtils.newPackageSet();
         if( pkgSet != null )
         {
            Iterator iter = pkgSet.iterator();
            while( iter.hasNext() )
            {
               RepositoryClassLoader ucl = (RepositoryClassLoader) iter.next();
               PkgClassLoader pkgUcl = new PkgClassLoader(ucl, 0);
               theSet.add(pkgUcl);
            }
         }

         if( java2ParentDelegation == false )
         {
            Class cacheClass = parentRepository.loadClassFromCache(name);
            if( cacheClass != null )
            {
               RepositoryClassLoader ucl = new CacheClassLoader(cacheClass, HeirarchicalLoaderRepository3.this);
               PkgClassLoader pkgUcl = new PkgClassLoader(ucl, 1);
               theSet.add(pkgUcl);
            }
         }

         if( parentPkgSet != null )
         {
            Iterator iter = parentPkgSet.iterator();
            while( iter.hasNext() )
            {
               RepositoryClassLoader ucl = (RepositoryClassLoader) iter.next();
               PkgClassLoader pkgUcl = new PkgClassLoader(ucl, 2);
               theSet.add(pkgUcl);
            }
         }

         if( java2ParentDelegation == false )
         {
            theSet.add(packageClassLoader);
         }

         return theSet;
      }
   }

}
