// (c) MIT 2003.  All rights reserved.

// Copyright (C) 1998 Graham Kirby
// 
// This library is free software; you can redistribute it and/or modify it under the
// terms of the GNU Library General Public License as published by the Free Software
// Foundation; either version 2 of the License, or (at your option) any later
// version.
// 
// This library 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 Library General Public License for more details.
// 
// To receive a copy of the GNU Library General Public License, write to the Free
// Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
// USA.

package rec.util;

import java.io.*;
import java.lang.reflect.*;
import java.util.*;

/**
 * Provides support for dynamic access to the Java compiler by a running Java program.
 *
 * The class provides various combinations of input and output to the compiler:
 * the input can be a single class definition or an array of definitions, and the output
 * can be a single class, an array of classes, an array of files, or an array of byte codes.<P>
 *
 * The main limitation is that all of the classes being compiled in one call must belong
 * to the same package. If multiple packages are to be compiled, multiple calls to the
 * compiler package must be used, thus the class cannot cope with mutually recursive packages.<P>
 *
 * Optionally the names of the classes being compiled can be extracted automatically from
 * the class definitions: this is done using very simplistic parsing, which will fail if
 * the reserved word 'package' or 'class' occurs in a comment before its first use as
 * a reserved word.<P>
 *
 * Two mechanisms are used to access the compiler. First it tries to load the
 * package <CODE>sun.tools.javac</CODE> to invoke the compiler class directly. If that
 * fails, it attempts to start a Unix shell process to call the compiler's command line
 * interface. If both attempts fail, as they will do on some platforms, a
 * <A HREF="compiler.ComptionException.html">CompilationException</A> is thrown.
 *
 * @author	Graham Kirby (<A HREF="mailto:graham@dcs.st-and.ac.uk">graham@dcs.st-and.ac.uk</A>)
 * @version 1.1 17-Jul-98
 *
 */
public class DynamicCompiler {

  /**
   * Hashtable storing string values with keys as specified for <A HREF="#setValue">setValue</A>.
   */
  private Hashtable values;

  /**
   * Output stream used for compilation error messages.
   */
  public PrintStream outputStream;

  public DynamicCompiler() {

    values = new Hashtable();
    outputStream = System.out;

    // Read system dependent values from the environment.
    setValue ("userDirPath",   System.getProperty ("user.dir"));
    setValue ("javaClassPath", System.getProperty ("java.class.path"));
    setValue ("javaHomePath",  System.getProperty ("java.home"));
    setValue ("fileSeparator", System.getProperty ("file.separator"));
    setValue ("pathSeparator", System.getProperty ("path.separator"));

    // Default compiler binary path.
    setValue ("compilerPath",  getValue ("javaHomePath") + getValue ("fileSeparator") + "bin" + getValue ("fileSeparator") + "javac");

    // Other default values.
    setValue ("rootDirName",      "Transient");
    setValue ("sourceSuffix",     ".java");
    setValue ("compiledSuffix",   ".class");
    setValue ("shellInvokeOnly",  "false");
    setValue ("packageSeparator", ".");

    setValue ("javacMainClassName",    "sun.tools.javac.Main");
    setValue ("outputStreamClassName", "java.io.OutputStream");
    setValue ("stringClassName",       "java.lang.String");
    setValue ("compileCommandName",    "javac");
    setValue ("compileMethodName",     "compile");
  }

  /**
   * HashSets a new value for one of the system dependent values.
   *
   * @param valueName	a string specifying the name of the value to be set, with the following interpretation:<PRE>
   "userDirPath"           the path of the current user directory (read from environment).
   "javaClassPath"         the current Java class path (read from environment).
   "javaHomePath"          the path of the Java home directory (read from environment).
   "fileSeparator"         the file separator character on the current platform (read from environment).
   "pathSeparator"         the path separator character on the current platform (read from environment).
   "compilerPath"          the path of the Java compiler binary (calculated on assumption of standard Unix Java installation).
   "rootDirName"           the name of the root directory used for temporary source and class files, relative to the user directory.
   "sourceSuffix"          the suffix for java source file names.
   "compiledSuffix"        the suffix for java compiled file names.
   "shellInvokeOnly"       "true" if only the Unix shell invocation method is to be used.
   "packageSeparator"      the package separator character.
   "javacMainClassName"    the fully qualified name of the compilation class.
   "outputStreamClassName" the fully qualified name of the OutputStream class.
   "stringClassName"       the fully qualified name of the String class.
   "compileCommandName"    the compilation command.
   "compileMethodName"     the name of the compilation method.</PRE>
   *
   * @param newValue	the string value
   * @see				#getValue
   */
  public void setValue (String valueName, String newValue) {

    values.put (valueName, newValue);
  }

  /**
   * Returns the current value of one of the system dependent values.
   *
   * @param valueName	a string specifying the name of the value to be read, with interpretation as for <A HREF="#setValue">setValue</A>.
   * @return			the value corresponding to the named parameter, or empty string if there is no match
   * @see				#setValue
   */
  public String getValue (String valueName) {

    Object val = values.get (valueName);

    if ( val == null ) return "";
    else               return (String) val;
  }

  /**
   * Compiles the given single class definition.
   *
   * @param classDefn	a string containing the complete definition of the class to be compiled
   * @return			the compiled class
   * @exception CompilationException	if the class definition was invalid or invocation of the compiler failed
   *
   * @see				#compileClass(java.lang.String, java.lang.String)
   * @see				#compileClass(java.lang.String[], java.lang.String)
   * @see				#compileClass(java.lang.String[], java.lang.String, java.lang.String[])
   * @see				#compileClasses
   */
  public Class compileClass (String classDefn) throws CompilationException {

    // Wrap the single class definition in an array.
    String[] classDefns = { classDefn };

    // Compile class definition and return compiled class.
    return compileClasses (classDefns)[ 0 ];
  }

  /**
   * Compiles the given single class definition, allowing the class name to be specified explicitly.
   * This caters for cases where automatic class name extraction does not work.
   *
   * @param classDefn	a string containing the complete definition of the class to be compiled
   * @param className	a string specifying the fully qualified name of the class to be compiled
   * @return			the compiled class
   * @exception CompilationException	if the class definition was invalid or invocation of the compiler failed
   *
   * @see				#compileClass(java.lang.String)
   * @see				#compileClass(java.lang.String[], java.lang.String)
   * @see				#compileClass(java.lang.String[], java.lang.String, java.lang.String[])
   * @see				#compileClasses
   */
  public Class compileClass (String classDefn, String className) throws CompilationException {

    // Wrap the single class definition and class name in arrays.
    String[] classNames = { className };
    String[] classDefns = { classDefn };

    // Compile class definition and return compiled class.
    return compileClasses (classDefns, classNames)[ 0 ];
  }

  /**
   * Compiles the given set of class definitions and returns a particular compiled class.
   *
   * @param classDefns	an array of strings containing the complete definitions of the classes to be compiled
   * @param mainClassName	a string specifying the fully qualified name of the class to be returned
   * @return				the specified compiled class
   * @exception CompilationException	if the class definition was invalid or invocation of the compiler failed
   *
   * @see				#compileClass(java.lang.String)
   * @see				#compileClass(java.lang.String, java.lang.String)
   * @see				#compileClass(java.lang.String[], java.lang.String, java.lang.String[])
   * @see				#compileClasses
   */
  public Class compileClass (String[] classDefns, String mainClassName) throws CompilationException, ClassNotFoundException {

    // Compile the class definitions.
    Class[] compiledClasses = compileClasses (classDefns);

    // Return the specified compiled class.
    return findNamedClass (compiledClasses, mainClassName);
  }

  /**
   * Compiles the given set of class definitions and returns a particular compiled class, allowing all class names to be specified explicitly.
   * This caters for cases where automatic class name extraction does not work.
   *
   * @param classDefns	an array of strings containing the complete definitions of the classes to be compiled
   * @param mainClassName	a string specifying the fully qualified name of the class to be returned
   * @param classNames	an array of strings specifying the fully qualified names of the classes to be compiled
   * @return				the specified compiled class
   * @exception CompilationException	if the class definition was invalid or invocation of the compiler failed
   *
   * @see				#compileClass(java.lang.String)
   * @see				#compileClass(java.lang.String, java.lang.String)
   * @see				#compileClass(java.lang.String[], java.lang.String, java.lang.String[])
   * @see				#compileClasses
   */
  public Class compileClass (String[] classDefns, String mainClassName, String[] classNames) throws CompilationException, ClassNotFoundException {

    // Compile the class definitions.
    Class[] compiledClasses = compileClasses (classDefns, classNames);

    // Return the specified compiled class.
    return findNamedClass (compiledClasses, mainClassName);
  }

  /**
   * Compiles the given set of class definitions.
   *
   * @param classDefns	an array of strings containing the complete definitions of the classes to be compiled
   * @return				an array containing the compiled classes
   * @exception CompilationException	if the class definition was invalid or invocation of the compiler failed
   *
   * @see				#compileClass
   * @see				#compileClasses(java.lang.String[], java.lang.String[])
   */
  public Class[] compileClasses (String[] classDefns) throws CompilationException {

    // Extract the class names from the class definitions.
    String[] fullClassNames = extractFullClassNames (classDefns);

    // Return the compiled classes.
    return compileClasses (classDefns, fullClassNames);
  }

  /**
   * Compiles the given set of class definitions, allowing the class names to be specified explicitly.
   * This caters for cases where automatic class name extraction does not work.
   *
   * @param classDefns	an array of strings containing the complete definitions of the classes to be compiled
   * @param classNames	an array of strings specifying the fully qualified names of the classes to be compiled
   * @return				an array containing the compiled classes
   * @exception CompilationException	if any of the class definitions were invalid or invocation of the compiler failed
   *
   * @see				#compileClass
   * @see				#compileClasses(java.lang.String[])
   */
  public Class[] compileClasses (String[] classDefns, String[] classNames) throws CompilationException {

    // Get the root directory.
    File rootDir = new File (getValue ("userDirPath"), getValue ("rootDirName"));

    // Compile the class definitions and generate .class files.
    compile (classDefns, classNames, rootDir);

    // Create a class loader that loads from the root directory.
    DirSpecificClassLoader rootDirLoader = new DirSpecificClassLoader (rootDir, this);

    // Create array to hold the compiled classes.
    Class[] compiledClasses = new Class[ classNames.length ];

    try {

      // Load each compiled class into the array.
      for ( int i = 0; i < classNames.length; i++ ) {

	// Load the class and resolve external references.
	compiledClasses[ i ] = rootDirLoader.loadClass (classNames[ i ], true);
      }
    }
    catch (ClassNotFoundException e) { throw new CompilationException ("class loading failed: " + e.getMessage()); }

    // Return the compiled classes.
    return compiledClasses;
  }

  /**
   * Compiles the given set of class definitions to byte arrays.
   *
   * @param classDefns	an array of strings containing the complete definitions of the classes to be compiled
   * @return				an array of arrays containing the byte codes of the compiled classes
   * @exception CompilationException	if the class definition was invalid or invocation of the compiler failed
   *
   * @see				#compileClass
   * @see				#compileClasses
   * @see				#compileClassesToFiles
   * @see				#compileClassesToBytes(java.lang.String[], java.lang.String[])
   */
  public byte[][] compileClassesToBytes (String[] classDefns) throws CompilationException {

    // Extract the class names from the class definitions.
    String[] fullClassNames = extractFullClassNames (classDefns);

    // Return the compiled classes as byte arrays.
    return compileClassesToBytes (classDefns, fullClassNames);
  }

  /**
   * Compiles the given set of class definitions to byte arrays, allowing the class names to be specified explicitly.
   * This caters for cases where automatic class name extraction does not work.
   *
   * @param classDefns	an array of strings containing the complete definitions of the classes to be compiled
   * @param classNames	an array of strings specifying the fully qualified names of the classes to be compiled
   * @return				an array of arrays containing the byte codes of the compiled classes
   * @exception CompilationException	if the class definition was invalid or invocation of the compiler failed
   *
   * @see				#compileClass
   * @see				#compileClasses
   * @see				#compileClassesToFiles
   * @see				#compileClassesToBytes(java.lang.String[])
   */
  public byte[][] compileClassesToBytes (String[] classDefns, String[] classNames) throws CompilationException {

    // Get the root directory.
    File rootDir = new File (getValue ("userDirPath"), getValue ("rootDirName"));

    // Compile the class definitions and generate .class files.
    compile (classDefns, classNames, rootDir);

    // Create array to hold the byte codes.
    byte[][] classBytes = new byte[ classDefns.length ][];

    try {

      // Load each byte array into the array.
      for (int i = 0; i < classDefns.length; i++) classBytes[ i ] = findClassBytes (classNames[ i ], rootDir );
    }
    catch (ClassNotFoundException e) { throw new CompilationException ("byte code not found"); }

    // Return the compiled byte codes.
    return classBytes;
  }

  /**
   * Compiles the given set of class definitions to <CODE>.class</CODE> files.
   *
   * @param classDefns	an array of strings containing the complete definitions of the classes to be compiled
   * @return				an array of files containing the byte codes of the compiled classes
   * @exception CompilationException	if the class definition was invalid or invocation of the compiler failed
   *
   * @see				#compileClass
   * @see				#compileClasses
   * @see				#compileClassesToBytes
   * @see				#compileClassesToFiles(java.lang.String[], java.lang.String[])
   */
  public File[] compileClassesToFiles (String[] classDefns) throws CompilationException {

    // Extract the class names from the class definitions.
    String[] fullClassNames = extractFullClassNames (classDefns);

    // Return the compiled classes as .class files.
    return compileClassesToFiles (classDefns, fullClassNames);
  }

  /**
   * Compiles the given set of class definitions to <CODE>.class</CODE> files, allowing the class names to be specified explicitly.
   * This caters for cases where automatic class name extraction does not work.
   *
   * @param classDefns	an array of strings containing the complete definitions of the classes to be compiled
   * @param classNames	an array of strings specifying the fully qualified names of the classes to be compiled
   * @return				an array of files containing the byte codes of the compiled classes
   * @exception CompilationException	if the class definition was invalid or invocation of the compiler failed
   *
   * @see				#compileClass
   * @see				#compileClasses
   * @see				#compileClassesToFiles
   * @see				#compileClassesToBytes(java.lang.String[])
   */
  public File[] compileClassesToFiles (String[] classDefns, String[] classNames) throws CompilationException {

    // Get the top-level root directory.
    File rootDir = new File (getValue ("userDirPath"), getValue ("rootDirName"));

    // Compile the class definitions and generate .class files.
    compile (classDefns, classNames, rootDir);

    // Create array to hold the .class files.
    File[] classFiles = new File[ classDefns.length ];

    try {

      // Load each .class file into the array.
      for (int i = 0; i < classDefns.length; i++) classFiles[ i ] = findClassFile (classNames[ i ], rootDir );
    }
    catch (ClassNotFoundException e) { throw new CompilationException ("class file not found"); }

    // Return the .class files.
    return classFiles;
  }

  /**
   * Compiles the given set of class definitions to <CODE>.class</CODE> files.
   *
   * @param classDefns		an array of strings containing the complete definitions of the classes to be compiled
   * @param fullClassNames	an array of strings specifying the fully qualified names of the classes to be compiled
   * @param rootDir			the directory forming the root of the directory hierarchy containing source and compiled files
   * @exception CompilationException	if the class definition was invalid or invocation of the compiler failed
   */
  protected void compile (String[] classDefns, String[] fullClassNames, File rootDir) throws CompilationException {

    // Check number of class definitions and class names match and are non-zero.
    if ( classDefns == null || fullClassNames == null ||
	 classDefns.length == 0 || ( classDefns.length != fullClassNames.length ) )
      throw new CompilationException ("incorrect number of class names");

    try {

      // Flag recording whether compilation has been successful.
      boolean compiled = false;

      // Construct the arguments to the compilation command.
      // The first four arguments specify the class path and the output directory; the rest are the source file names.
      String[] argv = new String[ fullClassNames.length + 4 ];

      // The compilation class path includes the standard class path and the output directory.
      argv[ 0 ] = "-classpath";
      argv[ 1 ] = getValue ("javaClassPath") + getValue ("pathSeparator") + rootDir.getAbsolutePath();

      // Output to the root directory.
      argv[ 2 ] = "-d";
      argv[ 3 ] = rootDir.getAbsolutePath();

      // Write the class definitions into corresponding source files. 
      generateSourceFiles (classDefns, fullClassNames, rootDir, argv);

      // Compile source files. First, try to invoke compiler class directly (unless the 'shellInvokeOnly' flag is set.
      // If that does not work, try to execute a shell command to invoke the compiler.
      if (getValue ("shellInvokeOnly") == "false") {

	try {

	  // Try to load the compiler class.
	  Class javacMain = Class.forName (getValue ("javacMainClassName"));

	  // Construct the parameters and types for the compiler class constructor.
	  Object[] compilerConstructorParams =    { outputStream, getValue ("compileCommandName") };
	  Class[] compilerConstructorParamTypes = { Class.forName (getValue ("outputStreamClassName")), Class.forName (getValue ("stringClassName")) };

	  // Get the compiler class constructor.
	  Constructor compilerConstructor = javacMain.getConstructor (compilerConstructorParamTypes);

	  // Create a new instance of the compiler class.
	  Object compiler = compilerConstructor.newInstance (compilerConstructorParams);

	  // Construct the parameters and types for the compilation method.
	  Object[] compileParams =    { argv };
	  Class[] compileParamTypes = { argv.getClass() };

	  // Get the compilation method.
	  Method compile = javacMain.getMethod (getValue ("compileMethodName"), compileParamTypes);

	  // Invoke the compilation method of the compiler object.
	  compiled = ((Boolean) compile.invoke (compiler, compileParams)).booleanValue();
	}
	catch (Exception e) {

	  // Ignore any errors; the next step is to try to execute a shell command to invoke the compiler.
	}
      }

      if ( ! compiled ) {

	// Have not managed to compile successfully yet.

	// Construct a shell command to compile the source files.
	String compileCommand = getValue ("compilerPath") +
	  " -classpath " + getValue ("javaClassPath") + getValue ("pathSeparator") + rootDir.getAbsolutePath() +
	  " -d " + rootDir.getAbsolutePath();

	// Add the full paths of the source files.
	for (int i = 4; i < argv.length; i++) compileCommand += " " + argv[ i ];

	// Attempt to execute the compile command in a shell.
	Exec.setVerbose (false);
	if ( ! Exec.execWait( compileCommand ) ) throw new CompilationException ("compilation failed");
      }
    }

    catch (IOException e) { throw new CompilationException ("could not create source files: " + e.getMessage()); }
  }

  /**
   * Creates a nested directory structure corresponding to the packages in the given full class name, rooted at the given directory.
   *
   * @param classDefns		an array of strings containing the complete definitions of the classes to be compiled
   * @param fullClassNames	an array of strings specifying the fully qualified names of the classes to be compiled
   * @param rootDir			the directory in which source and compiled files are to be created
   * @param argv				an array of strings that will contain the arguments to the compilation command
   * @exception CompilationException	if the attempt to generate the source files fails
   */
  protected void generateSourceFiles (String[] classDefns, String[] fullClassNames, File rootDir, String[] argv) throws CompilationException, IOException {

    // Check all classes are in the same package, and extract the local class names.
    // For example, if the full class name is "x.y.myClass", the local class name is "myClass".
    String[] localClassNames = extractLocalClassNames (fullClassNames);

    // Create the directory structure corresponding to the packages.
    File packageDir = getPackageDirectory (fullClassNames[ 0 ], rootDir);

    for ( int i = 0; i < localClassNames.length; i++ ) {

      // Construct name for the source file.
      String sourceFileName = localClassNames[ i ] + getValue ("sourceSuffix");

      // Delete the source file if it already exists.
      File sourceFile = new File (packageDir, sourceFileName);

      if ( sourceFile.exists() )
	if ( ! sourceFile.delete() ) throw new CompilationException ("deletion of existing source class file failed");

      // Write the source code to the file.
      PrintWriter printWriter = new PrintWriter (new FileOutputStream (sourceFile));

      printWriter.println (classDefns[ i ]);
      printWriter.flush();

      // Record the full path of source file in the list of arguments to the compilation command.
      argv[ 4 + i ] = sourceFile.getAbsolutePath();
    }
  }

  /**
   * Checks all the given full class names correspond to the same package, and extracts the local class names.
   *
   * @param fullClassNames	an array of strings specifying the fully qualified names of the classes to be compiled
   * @return					an array of strings containing the local class names
   * @exception CompilationException	if not all the full class names are in the same package
   */
  protected String[] extractLocalClassNames (String[] fullClassNames) throws CompilationException {

    // Get the first full class name.
    String firstFullClassName = fullClassNames[0];

    // Array to hold the local class names.
    String[] localClassNames = new String[ fullClassNames.length ];

    // Look for the last occurrence of a dot in the full class name.
    int lastDotIndex = firstFullClassName.lastIndexOf (getValue ("packageSeparator"));

    // Get the full package name, allowing for there being no package.
    // For example, if the full class name is "x.y.myClass", the full package name is "x.y".
    String fullPackageName = "";
    if (lastDotIndex > -1) fullPackageName = firstFullClassName.substring (0, lastDotIndex);
    int fullPackageNameLength = fullPackageName.length();

    // Extract all the local class names, checking that the full package name is the same in each case.
    for (int i = 0; i < fullClassNames.length; i++) {

      String fullClassName = fullClassNames[ i ];

      // Check the package is the same.
      if (fullClassName.length() < fullPackageNameLength ||
	  (! fullClassName.substring (0, fullPackageNameLength).equals (fullPackageName)))
	throw new CompilationException ("not all classes are in the same package");

      // Extract the local class name.
      if (fullPackageNameLength > 0) localClassNames[ i ] = fullClassName.substring (fullPackageNameLength + 1);
      else                           localClassNames[ i ] = fullClassName;

    }

    return localClassNames;
  }

  /**
   * Creates a nested directory structure corresponding to the packages in the given full class name, rooted at the given directory.
   *
   * @param fullClassName	a string specifying a fully qualified class name
   * @param rootDir		the directory to be the root of the hierarchy
   * @return				the directory corresponding to the most deeply nested package
   * @exception CompilationException	if the attempt to create the directories fails
   */
  protected File getPackageDirectory (String fullClassName, File rootDir) throws CompilationException {

    // Create tokenizer to extract the package name components.
    StringTokenizer st = new StringTokenizer (fullClassName, getValue ("packageSeparator"));

    // Attempt to create the root directory if it does not already exist.
    if ( !rootDir.exists() )
      if ( !rootDir.mkdir() ) throw new CompilationException ("creation of root directory failed");

    // Start at the root directory.
    File packageDir = rootDir;

    // Get the first token.
    String t = st.nextToken();

    // Add the sub-directories for the package structure in turn, if any.
    // The last token is ignored since it is the local class name.
    while ( st.hasMoreTokens() ) {

      // Generate the name of the next directory.
      packageDir = new File (packageDir, t);

      // Attempt to create the package directory if it does not already exist.
      if ( !packageDir.exists() )
	if ( !packageDir.mkdir() ) throw new CompilationException ("creation of root directory failed");

      // Get the next token.
      t = st.nextToken();
    }

    // Return the deepest level directory.
    return packageDir;
  }

  /**
   * Extracts the full class names corresponding to the given class definitions.
   * It will not cope if the word 'package' or 'class' occurs in a comment before its first occurrence as a reserved word.
   *
   * @param classDefns	an array of strings containing the complete definitions of the classes
   * @return				an array of strings containing the corresponding full class names
   */
  protected String[] extractFullClassNames (String[] classDefns) {

    // Initialise array to hold full class names.
    String[] fullClassNames = new String[ classDefns.length ];

    for (int i = 0; i < fullClassNames.length; i++) {

      // Get the class definition.
      String classDefn = classDefns[ i ];

      // Create a tokenizer that splits on spaces, tabs and newlines.
      StringTokenizer st = new StringTokenizer (classDefn, " \t\n\r;");

      String localClassName = "";
      String fullClassName =  "";
      String packageName =    "";

      // Get the first token.
      String token = st.nextToken();

      // Iterate through the class definition looking for package and class declarations.
      while (st.hasMoreTokens()) {

	// Check for 'package' reserved word and record the package name if found.
	if (token.equals ("package")) packageName = st.nextToken();

	// Check for 'class' reserved word and record the class name if found.
	if (token.equals ("class")) {

	  localClassName = st.nextToken();

	  // Construct the full class name from the package name, if any, and the local class name.
	  if (packageName.equals ("")) fullClassName = localClassName;
	  else                         fullClassName = packageName + "." + localClassName;

	  // Can now stop reading the class definition.
	  break;
	}

	// Get the next token.
	token = st.nextToken();
      }

      // Record the full class name.
      fullClassNames[ i ] = fullClassName;
    }

    return fullClassNames;
  }

  /**
   * Finds the named class in the given array.
   *
   * @param classes		an array of classes
   * @param fullClassName	a full class name
   * @return				the class with the specified name
   * @exception ClassNotFoundException	if the named class is not found
   */
  protected Class findNamedClass (Class[] classes, String fullClassName) throws ClassNotFoundException {

    for (int i = 0; i < classes.length; i++)
      if (classes[i].getName().equals (fullClassName)) return classes[i];

    throw new ClassNotFoundException();
  }

  /**
   * Finds the .class file corresponding to the given class name.
   *
   * @param fullClassName	a full class name
   * @param rootDir		the directory that is the root of the hierarchy
   * @return				the file containing the byte code for the given class
   * @exception ClassNotFoundException	if the file is not found
   */
  protected File findClassFile (String fullClassName, File rootDir) throws ClassNotFoundException {

    // Create tokenizer to extract the package name components.
    StringTokenizer st = new StringTokenizer (fullClassName, getValue ("packageSeparator"));

    // Start at the root directory.
    File packageDir = rootDir;

    // Get the first token.
    String s = st.nextToken();

    // Add the sub-directories for the package structure in turn, if any.
    while ( st.hasMoreTokens() ) {

      // Generate the name of the next directory.
      packageDir = new File (packageDir, s);

      // Get the next token.
      s = st.nextToken();
    }

    // Get the class file.
    File classFile = new File (packageDir, s + getValue ("compiledSuffix"));

    // Fail if the class file does not exist in the specified class directory.
    if ( ! classFile.exists() ) throw new ClassNotFoundException ("class file not found");

    return classFile;
  }

  /**
   * Finds the byte array corresponding to the given class name.
   *
   * @param fullClassName	a full class name
   * @param rootDir		the directory that is the root of the hierarchy
   * @return				an array containing the byte code for the given class
   * @exception ClassNotFoundException	if the file is not found
   */
  protected byte[] findClassBytes (String fullClassName, File rootDir) throws ClassNotFoundException {

    byte[] classBytes;

    // Get the file containing the byte code.
    File classFile = findClassFile (fullClassName, rootDir);

    try {

      // Create an input stream from the file.
      FileInputStream classFileInputStream = new FileInputStream (classFile);

      // Get the file length.
      int fileLength = classFileInputStream.available();

      // Create an array the same length as the file to hold the compiled class.
      classBytes = new byte[ fileLength ];

      // Read class definition into the array.
      classFileInputStream.read (classBytes);
    }
    catch (FileNotFoundException e) { throw new ClassNotFoundException ("class file not found: " + e.getMessage()); }
    catch (IOException e)           { throw new ClassNotFoundException ("IO error: " + e.getMessage()); }

    return classBytes;
  }
}

