//$Header: /home/cvs/jakarta-jmeter/src/jorphan/org/apache/jorphan/reflect/ClassFinder.java,v 1.9 2004/02/11 23:57:23 sebb Exp $
/*
 * Copyright 2001-2004 The Apache Software Foundation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
*/

package org.getopt.luke;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.zip.ZipFile;

//import org.apache.jorphan.logging.LoggingManager;
//import org.apache.log.Logger;

/**
 * This class finds classes that implement one or more specified interfaces.
 *
 * @author  Burt Beckwith
 * @author  Michael Stover (mstover1 at apache.org)
 * @version $Revision: 1.9 $
 */
public final class ClassFinder
{
  //  transient private static Logger log = LoggingManager.getLoggerForClass();
    private ClassFinder()
    {
    }

    // static only
    /**
     * Convenience method for <code>findClassesThatExtend(Class[],
     * boolean)</code> with the option to include inner classes in the search
     * set to false.
     *
     * @return ArrayList containing discovered classes.
     */
    public static List findClassesThatExtend(
        String[] paths,
        Class[] superClasses)
        throws IOException, ClassNotFoundException
    {
        return findClassesThatExtend(paths, superClasses, false);
    }
    
    
    /**
     * Convenience method that finds classes on the standard java classpath
     * @param superClasses
     * @return
     * @throws IOException
     * @throws ClassNotFoundException
     */
    public static List findClassesThatExtend(Class[]superClasses) throws IOException, ClassNotFoundException
	{
		String javaClassPath = System.getProperty("java.class.path");
		String paths[]=javaClassPath.split(File.pathSeparator);
		return ClassFinder.findClassesThatExtend(paths,superClasses);
	}
    /**
     * Convenience method that finds classes on the standard java classpath
     * @param superClass
     * @return
     * @throws IOException
     * @throws ClassNotFoundException
     */
    public static List findClassesThatExtend(Class superClass) throws IOException, ClassNotFoundException
	{
		String javaClassPath = System.getProperty("java.class.path");
		String paths[]=javaClassPath.split(""+File.pathSeparatorChar);
		Class superClasses[]=new Class[1];
		superClasses[0]=superClass;
		return ClassFinder.findClassesThatExtend(paths,superClasses);    	
	}
    
    /**
     * Convenience method to get a list of classes that can be instantiated
     * @param superclass an interface or base class
     * @return
     * @throws IOException
     * @throws ClassNotFoundException
     */
    public static Class[] getInstantiableSubclasses(Class superclass) throws IOException, ClassNotFoundException
    {
    	ArrayList instantiableSubclasses=new ArrayList();
        List classes=findClassesThatExtend(superclass);
        for (Iterator iter = classes.iterator(); iter.hasNext();)
		{
			String className = (String) iter.next();
			try
			{
				Class clazz=Class.forName(className);
				int modifiers=clazz.getModifiers();
				if(!Modifier.isAbstract(modifiers))
				{
					instantiableSubclasses.add(clazz);				
				}
			}
			catch(Throwable ignore)
			{
				
			}
		}
        return (Class[]) instantiableSubclasses.toArray(new Class[0]);
    }

    /**
     * Find classes in the provided path(s)/jar(s) that extend the class(es).
     *
     * @return ArrayList containing discovered classes
     */
    private static String[] addJarsInPath(String[] paths)
    {
        Set fullList = new HashSet();
        for (int i = 0; i < paths.length; i++)
        {
            fullList.add(paths[i]);
            if (!paths[i].endsWith(".jar"))
            {
                File dir = new File(paths[i]);
                if (dir.exists() && dir.isDirectory())
                {
                    String[] jars = dir.list(new FilenameFilter()
                    {
                        public boolean accept(File f, String name)
                        {
                            if (name.endsWith(".jar"))
                            {
                                return true;
                            }
                            return false;
                        }
                    });
                    for (int x = 0; x < jars.length; x++)
                    {
                        fullList.add(jars[x]);
                    }
                }
            }
        }
        return (String[]) fullList.toArray(new String[0]);
    }
    public static List findClassesThatExtend(
        String[] strPathsOrJars,
        Class[] superClasses,
        boolean innerClasses)
        throws IOException, ClassNotFoundException
    {
        List listPaths = null;
        ArrayList listClasses = null;
        List listSuperClasses = null;
        strPathsOrJars = addJarsInPath(strPathsOrJars);
/*        if (log.isDebugEnabled())
        {
            for (int k = 0; k < strPathsOrJars.length; k++)
            {
                log.debug("strPathsOrJars : " + strPathsOrJars[k]);
            }
        }
    */
        listPaths = getClasspathMatches(strPathsOrJars);
/*        if (log.isDebugEnabled())
        {
            Iterator tIter = listPaths.iterator();
            for (; tIter.hasNext();)
            {
                log.debug("listPaths : " + tIter.next());
            }
        }
*/        listClasses = new ArrayList();
        listSuperClasses = new ArrayList();
        for (int i = 0; i < superClasses.length; i++)
        {
            listSuperClasses.add(superClasses[i].getName());
        }
        // first get all the classes
        findClassesInPaths(listPaths, listClasses);
/*        if (log.isDebugEnabled())
        {
            Iterator tIter = listClasses.iterator();
            for (; tIter.hasNext();)
            {
                log.debug("listClasses : " + tIter.next());
            }
        }
*/        List subClassList =
            findAllSubclasses(listSuperClasses, listClasses, innerClasses);
        return subClassList;
    }

    private static List getClasspathMatches(String[] strPathsOrJars)
    {
        ArrayList listPaths = null;
        StringTokenizer stPaths = null;
        String strPath = null;
        int i;
        listPaths = new ArrayList();
//        log.debug("Classpath = " + System.getProperty("java.class.path"));
        stPaths =
            new StringTokenizer(
                System.getProperty("java.class.path"),
                System.getProperty("path.separator"));
        if (strPathsOrJars != null)
        {
            strPathsOrJars = fixDotDirs(strPathsOrJars);
            strPathsOrJars = fixSlashes(strPathsOrJars);
            strPathsOrJars = fixEndingSlashes(strPathsOrJars);
        }
	/*	if (log.isDebugEnabled())
		{
		    for (i = 0; i < strPathsOrJars.length; i++)
		    {
				log.debug("strPathsOrJars[" + i + "] : " + strPathsOrJars[i]);
			}
		}
	*/	
        // find all jar files or paths that end with strPathOrJar
        while (stPaths.hasMoreTokens())
        {
            strPath = fixDotDir((String) stPaths.nextToken());
            strPath = fixSlashes(strPath);
            strPath = fixEndingSlashes(strPath);
            if (strPathsOrJars == null)
            {
	//			log.debug("Adding: " + strPath);
                listPaths.add(strPath);
            }
            else
            {
                boolean found=false;
                for (i = 0; i < strPathsOrJars.length; i++)
                {
                    if (strPath.endsWith(strPathsOrJars[i]))
                    {
                    	found=true;
      //                  log.debug("Adding "+strPath+" found at "+i);
                        listPaths.add(strPath);
                        break;// no need to look further
                    }
                }
                if (!found){
        //        	log.debug("Did not find: "+strPath);
                }
            }
        }
        return listPaths;
    }
    
    /**
     * Get all interfaces that the class implements, including parent
     * interfaces. This keeps us from having to instantiate and check
     * instanceof, which wouldn't work anyway since instanceof requires a
     * hard-coded class or interface name.
     *
     * @param  theClass     the class to get interfaces for
     * @param  hInterfaces  a Map to store the discovered interfaces in
     *
     * NOTUSED
    private static void getAllInterfaces(Class theClass, Map hInterfaces)
    {
        Class[] interfaces = theClass.getInterfaces();
        for (int i = 0; i < interfaces.length; i++)
        {
            hInterfaces.put(interfaces[i].getName(), interfaces[i]);
            getAllInterfaces(interfaces[i], hInterfaces);
        }
    }
    */
    private static String[] fixDotDirs(String[] paths)
    {
        for (int i = 0; i < paths.length; i++)
        {
            paths[i] = fixDotDir(paths[i]);
        }
        return paths;
    }
    private static String fixDotDir(String path)
    {
    	if (path == null) return null;
        if (path.equals("."))
        {
            return System.getProperty("user.dir");
        }
        else
        {
            return path.trim();
        }
    }
    private static String[] fixEndingSlashes(String[] strings)
    {
        String[] strNew = new String[strings.length];
        for (int i = 0; i < strings.length; i++)
        {
            strNew[i] = fixEndingSlashes(strings[i]);
        }
        return strNew;
    }
    private static String fixEndingSlashes(String string)
    {
        if (string.endsWith("/") || string.endsWith("\\"))
        {
            string = string.substring(0, string.length() - 1);
            string = fixEndingSlashes(string);
        }
        return string;
    }
    private static String[] fixSlashes(String[] strings)
    {
        String[] strNew = new String[strings.length];
        for (int i = 0; i < strings.length; i++)
        {
            strNew[i] = fixSlashes(strings[i]) /*.toLowerCase()*/;
        }
        return strNew;
    }
    private static String fixSlashes(String str)
    {
        // replace \ with /
        str = str.replace('\\', '/');
        // compress multiples into singles;
        // do in 2 steps with dummy string
        // to avoid infinte loop
        str = replaceString(str, "//", "_____");
        str = replaceString(str, "_____", "/");
        return str;
    }
    private static String replaceString(
        String s,
        String strToFind,
        String strToReplace)
    {
        int index;
        int currentPos;
        StringBuffer buffer = null;
        if (s.indexOf(strToFind) == -1)
        {
            return s;
        }
        currentPos = 0;
        buffer = new StringBuffer();
        while (true)
        {
            index = s.indexOf(strToFind, currentPos);
            if (index == -1)
            {
                break;
            }
            buffer.append(s.substring(currentPos, index));
            buffer.append(strToReplace);
            currentPos = index + strToFind.length();
        }
        buffer.append(s.substring(currentPos));
        return buffer.toString();
    }
    
    /**
     *   NOTUSED
     *  * Determine if the class implements the interface.
     *
     * @param  theClass      the class to check
     * @param  theInterface  the interface to look for
     * @return               boolean true if it implements
     *
    private static boolean classImplementsInterface(
        Class theClass,
        Class theInterface)
    {
        HashMap mapInterfaces = new HashMap();
        String strKey = null;
        // pass in the map by reference since the method is recursive
        getAllInterfaces(theClass, mapInterfaces);
        Iterator iterInterfaces = mapInterfaces.keySet().iterator();
        while (iterInterfaces.hasNext())
        {
            strKey = (String) iterInterfaces.next();
            if (mapInterfaces.get(strKey) == theInterface)
            {
                return true;
            }
        }
        return false;
    }
    */
    
    /**
     * Convenience method for <code>findAllSubclasses(List, List,
     * boolean)</code> with the option to include inner classes in the search
     * set to false.
     *
     * @param  listSuperClasses  the base classes to find subclasses for
     * @param  listAllClasses    the collection of classes to search in
     * @return                   ArrayList of the subclasses
     *
     * NOTUSED
    private static ArrayList findAllSubclasses(
        List listSuperClasses,
        List listAllClasses)
    {
        return findAllSubclasses(listSuperClasses, listAllClasses, false);
    }
    */
    
    /**
     * Finds all classes that extend the classes in the listSuperClasses
     * ArrayList, searching in the listAllClasses ArrayList.
     *
     * @param  listSuperClasses  the base classes to find subclasses for
     * @param  listAllClasses    the collection of classes to search in
     * @param  innerClasses      indicate whether to include inner classes in
     *                           the search
     *@return                    ArrayList of the subclasses
     */
    private static ArrayList findAllSubclasses(
        List listSuperClasses,
        List listAllClasses,
        boolean innerClasses)
    {
        Iterator iterClasses = null;
        ArrayList listSubClasses = null;
        String strClassName = null;
        Class tempClass = null;
        listSubClasses = new ArrayList();
        iterClasses = listSuperClasses.iterator();
        while (iterClasses.hasNext())
        {
            strClassName = (String) iterClasses.next();
            // only check classes if they are not inner classes
            // or we intend to check for inner classes
            if ((strClassName.indexOf("$") == -1) || innerClasses)
            {
                // might throw an exception, assume this is ignorable
                try
                {
                    tempClass =
                        Class.forName(
                            strClassName,
                            false,
                            Thread.currentThread().getContextClassLoader());
                    findAllSubclassesOneClass(
                        tempClass,
                        listAllClasses,
                        listSubClasses,
                        innerClasses);
                    // call by reference - recursive
                }
                catch (Throwable ignored)
                {
                }
            }
        }
        return listSubClasses;
    }
    
    /**
     * Convenience method for <code>findAllSubclassesOneClass(Class, List, List,
     * boolean)</code> with option to include inner classes in the search set to
     * false.
     *
     * @param  theClass        the parent class
     * @param  listAllClasses  the collection of classes to search in
     * @param  listSubClasses  the collection of discovered subclasses
     *
     *   NOTUSED
    private static void findAllSubclassesOneClass(
        Class theClass,
        List listAllClasses,
        List listSubClasses)
    {
        findAllSubclassesOneClass(
            theClass,
            listAllClasses,
            listSubClasses,
            false);
    }
    */
    
    /**
     * Finds all classes that extend the class, searching in the listAllClasses
     * ArrayList.
     *
     * @param  theClass        the parent class
     * @param  listAllClasses  the collection of classes to search in
     * @param  listSubClasses  the collection of discovered subclasses
     * @param  innerClasses    indicates whether inners classes should be
     *                         included in the search
     */
    private static void findAllSubclassesOneClass(
        Class theClass,
        List listAllClasses,
        List listSubClasses,
        boolean innerClasses)
    {
        Iterator iterClasses = null;
        String strClassName = null;
        Class c = null;
        boolean bIsSubclass = false;
        iterClasses = listAllClasses.iterator();
        while (iterClasses.hasNext())
        {
            strClassName = (String) iterClasses.next();
            // only check classes if they are not inner classes
            // or we intend to check for inner classes
            if ((strClassName.indexOf("$") == -1) || innerClasses)
            {
                // might throw an exception, assume this is ignorable
                try
                {
                    c =
                        Class.forName(
                            strClassName,
                            false,
                            Thread.currentThread().getContextClassLoader());

                    if (!c.isInterface()
                        && !Modifier.isAbstract(c.getModifiers()))
                    {
                        bIsSubclass = theClass.isAssignableFrom(c);
                    }
                    else
                    {
                        bIsSubclass = false;
                    }
                    if (bIsSubclass)
                    {
                        listSubClasses.add(strClassName);
                    }
                }
                catch (Throwable ignored)
                {
                }
            }
        }
    }
    
    /**
     * Converts a class file from the text stored in a Jar file to a version
     * that can be used in Class.forName().
     *
     * @param  strClassName  the class name from a Jar file
     * @return               String the Java-style dotted version of the name
     */
    private static String fixClassName(String strClassName)
    {
        strClassName = strClassName.replace('\\', '.');
        strClassName = strClassName.replace('/', '.');
        strClassName = strClassName.substring(0, strClassName.length() - 6);
        // remove ".class"
        return strClassName;
    }
    
    private static void findClassesInOnePath(String strPath, List listClasses)
        throws IOException
    {
        File file = null;
        ZipFile zipFile = null;
        Enumeration entries = null;
        String strEntry = null;
        file = new File(strPath);
        if (file.isDirectory())
        {
            findClassesInPathsDir(strPath, file, listClasses);
        }
        else if (file.exists())
        {
            zipFile = new ZipFile(file);
            entries = zipFile.entries();
            while (entries.hasMoreElements())
            {
                strEntry = entries.nextElement().toString();
                if (strEntry.endsWith(".class"))
                {
                    listClasses.add(fixClassName(strEntry));
                }
            }
        }
    }

    private static void findClassesInPaths(List listPaths, List listClasses)
        throws IOException
    {
        Iterator iterPaths = listPaths.iterator();
        while (iterPaths.hasNext())
        {
            findClassesInOnePath((String) iterPaths.next(), listClasses);
        }
    }

    private static void findClassesInPathsDir(
        String strPathElement,
        File dir,
        List listClasses)
        throws IOException
    {
        File file = null;
        String[] list = dir.list();
        for (int i = 0; i < list.length; i++)
        {
            file = new File(dir, list[i]);
            if (file.isDirectory())
            {
                findClassesInPathsDir(strPathElement, file, listClasses);
            }
            else if (
                file.exists()
                    && (file.length() != 0)
                    && list[i].endsWith(".class"))
            {
                listClasses.add(
                    file
                        .getPath()
                        .substring(
                            strPathElement.length() + 1,
                            file.getPath().lastIndexOf("."))
                        .replace(File.separator.charAt(0), '.'));
            }
        }
    }
}