Deck

 

Chapter 9: Downloadable Classes........................................................................................................

THE CLASSLOADER CLASS......................................................................................................................................

The Primordial Class Loader...................................................................................................................................

A FILE-BASED CLASSLOADER................................................................................................................................

LOADING AGENTS.....................................................................................................................................................

THE BASE AGENT CLASS........................................................................................................................................

AN EXAMPLE AGENT: FILEFINDER....................................................................................................................

CONCLUSION...............................................................................................................................................................

EXERCISES...................................................................................................................................................................


Chapter 9: Downloadable Classes

The crux of our agent system is the ability of agents to find agent servers, load themselves across the network, and run on the agent server machine with a fair degree of freedom.  So far, we’ve talked about standalone Java apps, network connections, user interfaces ... all the pieces we need but one - the ability to turn an indistinct lump of bytes into a runnable Java class.

In order to understand what class loading involves we have to take a little closer look inside the Java interpreter.  What happens when, for example, the interpreter runs across the following statement:

Car car = new Car();

If this is the first time the interpreter has run across the class called Car, it initially has no idea how to make this thing.  It has no internal representation of the Car class.  It has to build that internal representation, which in Java is a class called Class.  Table 9.1 shows the methods for the Class class.

Table 9.1: The Java Class class.

Return Name  Argument        Description

static Class       forName           String className          Returns the Java Class associated with the specified name.

ClassLoader      getClassLoader             Returns the ClassLoader that was used to create this Class.

Class[]  getInterfaces                 Returns the interfaces that this class implements.

String   getName                       Returns the String name of this class.

Class    getSuperclass               Returns the Class that this class "extends".

boolean            isInterface                     Returns true if this Class is an interface.

Object  newInstance                  Returns an Object that is a new instance of this Class.

String   toString                        Returns the name of the object with either "class" or "interface" prepended.

The Class class is entirely devoted to what in C++ would be called run-time type information (RTTI).  What this mostly amounts to is the information contained in the first line of the class declaration.  For instance, consider the class Convertible declared as follows:

public class Convertible extends Object implements Car {

getInterfaces returns an array of Classes with one member, the interface Car.

getName returns the String “Convertible”.

getSuperClass returns the Class Object.

isInterface returns false.

toString returns the String “class Convertible”.

This leaves us with the more interesting methods: forName, newInstance, and getClassLoader (which we’ll talk about later). 

forName is used to get the Class object associated with a particular class.  If, for instance, we wanted to know whether Convertible was a class or interface, we might write the following code:

Class c = Class.forName(“Convertible”);

if( c.isInterface() == true )

     System.out.println( “Convertible is an interface” );

else

     System.out.println( “Convertible is a class” );

This code only works because forName is a static method.  Feed it a class or interface name and it returns the Class that has that name.

Like forName, newInstance is another key piece of the internal workings of the Java interpreter.  It creates a new instance of the class.  Consider the following code:

Convertible conv = new Convertible();

Using forName and newInstance we could re-write this as:

Class c = Class.forName( “Convertible” );

Object o = c.newInstance();

Convertible conv = (Convertible)o;

That’s all the new operator really is - a combination of forName, newInstance and a typecast.

newInstance brings us to an interesting question.  Why does Java use the Class class to represent interfaces as well as classes?  You’ll remember from chapters 1 and 2 that Java thinks of classes as having two distinct pieces - interface and implementation.  A Java interface is just that, the classes interface, while a Java class is both, interface and implementation.  If you look at the methods in Class, you can see that but for newInstance, these methods all deal with the definition of the class (its interface) rather than its implementation.

Thus, for every class, or interface that a Java applet (or application) uses, there is a corresponding Class object that exists within the Java interpreter which the interpreter uses to instantiate the class (whenever we use new) and which we can get access to via Class.forName.  This points us toward one of the key steps in our Agent system.  Say, for example, we write an Agent declared as follows:

public class FileFinder extends Agent implements Runnable {

In order to instantiate and run a FileFinder on our AgentServer, within the AgentServer we have to first create an object of type Class that represents the run-time type information for the FileFinder class.  For this, we need to write a new type of object - a ClassLoader.

The ClassLoader Class

The ClassLoader class is designed specifically to allow applications, or applets, to obtain raw class data, a lump of bytes, from some unknown source and turn it into a Java Class object.  It is one of the things that make Java absolutely unique among today’s development tools. 

To understand just what this means, consider the analogous case in, for example, Borland C++.  In BC++ you compile a class in a “.cpp” source file into a binary “.obj” file.  In order to duplicate the functionality of Java’s ClassLoader, BC++ would have to provide a class that allowed you to read a “.obj” file from disk, and instantiate an object from it.  No C++ class library currently provides this, in large part because the “.obj” files have a huge amount of application and operating-system-specific information in them.  Writing such a class for a C++ class library would require replicating a large part of the compiler’s object file linker and the operating system’s executable file loader.

ClassLoader is an abstract class whose methods are detailed in Table 9.2.

Table 9.2: The ClassLoader class.

Return   Method Argument          Description

            constructor                   Creates a new ClassLoader.

abstract Class   loadClass          String name, boolean resolve      Gets the named Class.

Class    defineClass       byte data[], int offset, int length   Converts the byte array into an unresolved Class.

void      resolveClass      Class  c Resolves all the references in the specified class.

Class    findSystemClass            String name       Loads the specified class via the primordial Class loader.

Loading classes splits pretty neatly into three steps:

* Get a lump of bytes that contains the class data into a byte array

* Create a Java class from the lump of bytes (defineClass)

* Resolve all the references within the new class (resolveClass)

Once the class has been created, and resolved, we can instantiate it, and use it as we would any other class.  As you can see, the heavy lifting in this exercise is done by defineClass and resolveClass, which Java already provides for us.  Where we are left to our own devices is in getting the class data into a byte array.  This is by design.  Java leaves the door open for this class data to spring from ANY source.  The obvious sources for class data are the local file-system and the network, but the truth is that, if we were clever enough, we could just build a byte array from thin air and make a class from it via ClassLoader.  The class data source is not important.

So ClassLoader leaves us on our own when it comes to making the byte array that contains the class data the we’re trying to load.  This is the functionality that we have to write into our loadClass method.  loadClass is the heart of any new ClassLoader.  Because it is declared as abstract, this method must be implemented by any class which subclasses ClassLoader.  It is responsible not only for getting the lump of raw class data, but also (via defineClass and resolveClass) for creating the Class object that represents all the run-time type information for the new class.

The Primordial Class Loader

There are two types of class loading, primordial and ClassLoader.  The primordial class loader handles the loading of classes that are local to the workstation, and living somewhere along the classpath.  Thus, if you have Java installed on your workstation and you run the HotJava browser, all the classes used by HotJava will be loaded via the primordial Class loader.  If, on the other hand, while using HotJava, you connect to a page that contains an applet, that applet will be loaded using a network ClassLoader that HotJava has implemented.  The primordial class loader is not an instance of ClassLoader. 

A File-Based ClassLoader

Now that we have the neccessary background, let’s write our own file-based ClassLoader.  What we’ll do is take it in three steps.  First we’ll write a ClassLoader that reads a single .class file from disk, and creates one instance of the class contained in that file.  Next, we’ll check this single class to see if it’s runnable (implements Runnable or extends Thread) and set it running if it is.  Finally, we’ll modify our ClassLoader to deal with a whole set of class files rather than just the single class.  When we’re done, we’ll have all the tools neccessary to write the ClassLoader for the Agent system.  Listing 9.1 shows the standalone Java application that uses our ClassLoaders.

Listing 9.1: A standalone application that creates ClassLoaders.

package chap9;

 

import java.awt.*;

import java.lang.*;

import agent.util.*;

import java.util.*;

import java.net.*;

import java.io.*;

 

/** A standalone application for testing class loading

functions.

@author John Rodley

@version 1.0 12/1/1995

*/

public class ch9_fig1 extends Thread {

public static ServerFrame f;

static Panel p;

MenuBar m;

public static boolean bRun = true;

static List li;

 

/** The main method for this standalone application.  Creates

one of our ch9_fig1 class, then sets it running via Thread.start.

@see Thread

*/

public static void main(String argv[] ) {

  ch9_fig1 as = new ch9_fig1();

  as.start();

  }

 

/** Cause the main thread to exit by setting static variable bRun to

false.

*/

public static void quit() {

  bRun = false;

  }

 

/** Constructor creates a frame window (the window the user

sees) adds a menubar and loadtest menuitems to it.

@see ServerFrame

@see List

@see Frame

@see Panel

@see MenuBar

@see MenuItem

*/

public ch9_fig1() {

  f = new ServerFrame();

  f.resize(450, 300);

  f.setTitle( "Chapter 9, Listing 9.1 - A file-based ClassLoader" );

  li = new List(10, false);

  li.show();

  f.add( "Center", li );

  f.show();

  m = new MenuBar();

  f.setMenuBar( m );

  Menu m1 = new Menu("File");

  m.add(m1);

  MenuItem m2 = new MenuItem( "Load test 1" );

  m1.add( m2 );

  m2 = new MenuItem( "Load test 2" );

  m1.add( m2 );

  m2 = new MenuItem( "Load test 3" );

  m1.add( m2 );

  m2 = new MenuItem( "Exit" );

  m1.add( m2 );

  }

 

/** Add a String to our debugging list box.

@param  str The string we want to appear in the list box.

*/

public static void show( String str ) {

  System.out.println( "show "+str );

  li.addItem( str );

  f.repaint(); 

  }

 

/** The main loop for the AgentServer.  Sits in a loop,

sleeping for 1 second, then waking up to check whether the user

interface has been terminated.

@see Thread.sleep

*/

public void run() {

  while( bRun == true ) {

    try {

    Thread.sleep( 1000 );

      } catch( Exception e ) { }

    }

  System.out.println( "out of run loop" );

  f.dispose();

  System.exit(0);

  }

 

/** Test the loading of classes at this site by allowing the

user to choose a class file to load, then creating/parsing a

LoadMessage from that class file.  DEBUGGING method.

@see ch9_fig1_FileLoader

@see FileDialog

*/

public static void LoadTest1() {

  System.out.println( "Load test" );

 

  FileDialog fd = new FileDialog(f, "LoadTest");

  fd.setFile( "*.class" );

  fd.setDirectory( "/agent/classes/beta/agent" );

  fd.show();

  if( fd.getFile() != null ) {

    System.out.println( "Load test - "+fd.getFile() );

    if( fd.getFile() == null )

      return;

    ch9_fig1_FileLoader fl = new ch9_fig1_FileLoader(

                              fd.getFile() );

    Class c = fl.getTheClass();

    try {

      Object o = c.newInstance();

      ch9_fig1.show("Successfully created new instance of "+c);

      }

    catch( Exception e ) {

      ch9_fig1.show( "Failed to create object from class file "

                      +fd.getFile() );

      }

    }

  else

    System.out.println( "getFile == null" );

  }

 

/** Test the loading of classes at this site by allowing the

user to choose a class file to load, then creating an object

from that class file. If the new object is an instance of

either Runnable or Thread, we start it running in its own

thread.

@see ch9_fig1_FileLoader

@see FileDialog

*/

public static void LoadTest2() {

  System.out.println( "Load test" );

 

  FileDialog fd = new FileDialog(f, "LoadTest");

  fd.setDirectory( "/agent/classes/beta/agent" );

  fd.setFile( "*.class" );

  fd.show();

  if( fd.getFile() != null ) {

    System.out.println( "Load test - "+fd.getFile() );

    if( fd.getFile() == null )

      return;

    ch9_fig1_FileLoader fl = new ch9_fig1_FileLoader(

                     fd.getFile() );

    Class c = fl.getTheClass();

    try {

      Object o = c.newInstance();

      ch9_fig1.show("Successfully created new instance of "+c);

      if( o instanceof Thread ) {

        Thread t = (Thread)o;

        t.start();

        ch9_fig1.show("Successfully started Thread");

        }

      else

        {

        if( o instanceof Runnable ) {

          Thread t = new Thread( (Runnable)o );

          t.start();

          ch9_fig1.show("Successfully started Runnable");

          }

        }

      }

    catch( Exception e ) {

      ch9_fig1.show( "Failed to create object from class file "+fd.getFile() );

      }

    }

  else

    System.out.println( "getFile == null" );

  }

 

 

/** Test the loading of classes at this site by allowing the

user to choose a class file to load, then creating an object

from it.  If the new object is an instance of either Runnable

or Thread we set it running in its own thread.

@see ch9_fig3_FileLoader

@see FileDialog

*/

public static void LoadTest3() {

  System.out.println( "Load test" );

 

  FileDialog fd = new FileDialog(f, "LoadTest");

  fd.setDirectory( "\temp" );

  fd.setFile( "*.class" );

  fd.show();

  if( fd.getFile() != null ) {

    System.out.println( "Load test - "+fd.getFile() );

    if( fd.getFile() == null )

      return;

    ch9_fig3_FileLoader fl = new ch9_fig3_FileLoader(

                     fd.getDirectory(), fd.getFile() );

    Class c = fl.getLeadClass();

    try {

      Object o = c.newInstance();

      ch9_fig1.show("Successfully created new instance of "+c);

      if( o instanceof Thread ) {

        Thread t = (Thread)o;

        t.start();

        ch9_fig1.show("Successfully started Thread");

        }

      else

        {

        if( o instanceof Runnable ) {

          Thread t = new Thread( (Runnable)o );

          t.start();

          ch9_fig1.show("Successfully started Runnable");

          }

        }

      }

    catch( Exception e ) {

      ch9_fig1.show( "Failed to create object from class file "+fd.getFile() );

      }

    }

  else

    System.out.println( "getFile == null" );

  }

}

 

/** The main window functionality - window creationg/refresh

handling, application exit handling ...

@see Frame

*/

class ServerFrame extends Frame {

 

/** Handle any events that might come up.  Right now, only

deals with WINDOW_DESTROY, which is what happens when the user

tries to close the application via the system menu.

*/

 public synchronized boolean handleEvent(Event evt) {

  if( evt.id == Event.MOUSE_UP ) {

    return( true );

    }

  else

    {  

    if( evt.target instanceof Frame ) {

      if( evt.id == Event.WINDOW_DESTROY ) {

        ch9_fig1.quit();

        System.out.println( "window destroy "+evt );

        return( true );

        }

      else

        return super.handleEvent(evt);

      }

    else

      return super.handleEvent(evt);

    }    

  }

 

/** Handle any menu item picks.  Right now, only has, Exit

and Load Test 1, 2 and 3.  Exit sets the agentServers bRun

flag to false, causing the AgentServer.run to fall out of the

endless while loop.  Load Test 1, 2 and 3 call, in that order

ch9_fig1.LoadTest1, 2 and 3.

@see ch9_fig1

@see ch9_fig1.LoadTest1

@see ch9_fig1.LoadTest2

@see ch9_fig1.LoadTest3

*/

public boolean action( Event evt, Object o ) {     

  if( evt.target instanceof MenuItem )

    {

    if( evt.arg.toString().compareTo( "Exit" ) == 0 )

      {

      ch9_fig1.quit();

      System.out.println( "action event "+evt );

      }

    else

      {

      if(evt.arg.toString().compareTo( "Load test 1" ) == 0 )

        {

        ch9_fig1.LoadTest1();

        }

      else {

        if(evt.arg.toString().compareTo( "Load test 2" ) == 0 )

          {

          ch9_fig1.LoadTest2();

          }

        else {

          if(evt.arg.toString().compareTo( "Load test 3" )==0)

            {

            ch9_fig1.LoadTest3();

            }

          }

        }

      }

    }

  return( true );

  }

 

  }

 

/** A ClassLoader that serves to load a SINGLE class from a

class file.  This class may ONLY reference classes that can be

loaded through the primordial class loader.  Thus, this class

can have only a single public class and NO private classes,

because this ClassLoader doesn't know how to load private

classes.

@version 1.0 1/1/1996

@author John Rodley

*/

class ch9_fig1_FileLoader extends ClassLoader {

  Class ourClass;

  String fileName;

 

  public ch9_fig1_FileLoader(String file) {

    super();

    fileName = new String( file );

    byte classBytes[] = LoadFileBytes();

    ourClass = loadFromByteLump(classBytes,true);   

    }

 

/** This ClassLoader only serves a single class, returned by

this method.

@return Class The Class object that this ClassLoader created.

*/

  public Class getTheClass() {

    return( ourClass );

    }

 

/** Load the specified class file into a byte array and return

that byte array.  All class files are read into the load

message via this method.

@return byte[]  The array of bytes that this class file is made

up of.

*/

  public byte[] LoadFileBytes() {

    byte bret[] = null;

    System.out.println( "Loading class from file "+fileName );

    try {

      FileInputStream fi = new FileInputStream( fileName );

      int filesize = fi.available();

      bret = new byte[filesize];

      fi.read( bret );

      } catch( IOException e ) {

        System.out.println("LoadClass "+fileName+" ex "+e );}

    return( bret );

    }

 

/** Create a Class from an array of bytes.  Define the class,

and resolve references if specified.

@param  lump  An array of bytes that represent the class file

data.

@param  resolve A boolean, if true we try to resolve all

references within this class file, otherwise, we don't.

@return Class The Class object that we created from the lump of

bytes.  

*/

  public synchronized Class loadFromByteLump( byte[] lump,

                                        boolean resolve ) {

    Class c;

    c = defineClass(lump, 0, lump.length);

    if( resolve )

      resolveClass( c );

    return( c );

    }

 

/** Load the named class and resolve references if the caller

specifies.

@param  name  The name of the class, as in "java.lang.String".

@param  resolve If true, we try to resolve all references in

this class.

@return Class The Class object that we created from this class

file.

*/

  public synchronized Class loadClass(String name,

                                 boolean resolve) {

    Class c = null;

 

    System.out.println( "loadClass("+name+")");

    try {

      c = findSystemClass( name );

      System.out.println( "Resolved "+name+" locally" );     

      }

    catch( ClassNotFoundException e ) {

      System.out.println( "Resolving "+name+" remotely" );

      if( name.compareTo( ourClass.getName() ) == 0 ) {

        c = ourClass;

        resolveClass(c);

        }

      }

    return c;

    }

  }

 

/** A ClassLoader that can load an entire package (subdirectory

of classes) given the filename of a single class file in that

package.  Keeps a Hashtable of class names, and tries to load

any unresolved references as class files from the same

directory as the one that the original class file came from.

@version 1.0 1/1/1996

@author John Rodley

*/

class ch9_fig3_FileLoader extends ClassLoader {

  Class ourClass;

  Hashtable cache;

  String directory;

 

/** constructor saves the directory dir for use in finding new

classes supplied to loadClass by the interpreter.  Loads file

immediately so that the caller can instantiate it and get the

whole loading thing going.  file is considered the "lead" class

in this operation.

@param  dir A String directory name gotten from the FileDialog.

@param  file  The fully qualified filename of the lead class.

*/

  public ch9_fig3_FileLoader(String dir, String file) {

    super();

    directory = new String( dir );

    cache = new Hashtable();

    byte classBytes[] = LoadFileBytes(file);

    ourClass = loadFromByteLump(classBytes,true);    

    }

 

/** Return the lead class.

@return Class A Class object created from the class file

supplied to the constructor.

*/

  public Class getLeadClass() {

    return( ourClass );

    }

 

/** Load the specified class file into a byte array and return

that byte array.  All class files are read into the load

message via this method.

*/

  public byte[] LoadFileBytes(String fileName) {

    byte bret[] = null;

    System.out.println( "Loading class from file "+fileName );

    try {

      FileInputStream fi = new FileInputStream( fileName );

      int filesize = fi.available();

      bret = new byte[filesize];

      fi.read( bret );

      } catch( IOException e ) {

        System.out.println("LoadClass "+fileName+" ex "+e );}

    return( bret );

    }

 

/** Return the byte lump that corresponds to the CLASS NAME

supplied.

@param  name  The CLASS name of the class we want to load.  We

create the fully-qualified filename by appending the class name

and ".class" to the directory we saved in the constructor.

@return the byte array we created from this class file.

*/

  private byte loadClassData(String name)[] {

    System.out.println( "loading secondary class "+name );

    String filename = new String( directory+name+".class" );

    System.out.println( "loading secondary class filename "+filename );

    return( LoadFileBytes(filename));

    }

 

/** Create a Class from an array of bytes.  Defines the class,

adds the class name to the Hashtable so that we only load it

once, and resolve references if specified. 

@param lump The byte array that contains the class data read

from the .class file.

@param  resolve A flag telling us whether to try to resolve

this Class now.

@return Class A Class object created from the byte lump.

*/

  public synchronized Class loadFromByteLump( byte[] lump,

                                        boolean resolve ) {

    Class c;

    c = defineClass(lump, 0, lump.length);

    if( resolve )

      resolveClass( c );

    return( c );

    }

 

/** Load the named class.  This method is called by the

interpreter whenever it runs into a class name it needs to

resolve.  Checks with the primordial class loader first via

findSystemClass, then checks our Hashtable to see if we've

loaded the desired class already, then calls loadClassData to

actually load the thing from disk.

@param  name  The class name (NOT THE FILE NAME).

@param  resolve A flag telling us whether to resolve this class

or not.

@return A Class representing the class named in name.

*/

  public synchronized Class loadClass(String name,

                                 boolean resolve) {

    Class c = null;

 

    System.out.println( "loadClass("+name+")");

    try {

      c = findSystemClass( name );

      System.out.println( "Resolved "+name+" locally" );     

      }

    catch( ClassNotFoundException e ) {

      if(( c = (Class)cache.get(name)) == null ) {

        System.out.println( "Resolving "+name+" remotely" );

        byte[] b = loadClassData( name );

        c = loadFromByteLump( b, true );

        }

      }

    return c;

    }

  }

 

We use a standalone application rather than an applet because, again, security restrictions make it nearly impossible for applets to create their own ClassLoaders.  The purpose of this standalone application is to allow the user to choose a class file from all the files on the system, load it, and instantiate it.  The basic skeleton for our standalone application should look familiar. 

A single public class, ch9_fig1, with a public static main method gets the whole thing going.  The run method for the class contains a simple loop that sleeps and checks the state of the bRun boolean variable.  When the run method returns, the application exits.  The ch9_fig1 constructor builds the user interface.  The user interface consists of the following elements:

* A menubar which contains 1 item

    The File menu which contains 4 items

        Load Test 1     Calls LoadTest1, the single-class, non-threaded class-load.

        Load Test 2     Calls LoadTest2, the single-class, threaded class-load.

        Load Test 3     Calls LoadTest3, the multi-class, threaded class-load.

        Exit     Causes the application to exit.

* A list box that contains our debugging output via ch9_fig1.show.

The ServerFrame class contains our user interface and the entry point to our three styles of class loading.  The Load Test menu choices are linked to the LoadTest methods in ServerFrame.action, where we check the String value of the selected item and call the selected method.

ServerFrame.action calls either ch9_fig1.LoadTest1, ch9_fig1.LoadTest2, or ch9_fig1.LoadTest3 when the user selects one of the Load Test menu items.  Within ch9_fig1, the three LoadTest methods are very similar, so let’s look at LoadTest1 first.  LoadTest1 puts up a FileDialog, that shows all the .class files in a particular directory.  Figure 9.1 shows the FileDialog in action.

Figure 9.1: The class loader FileDialog in action.

<ch9_fig1.tif>

Most GUIs provide a file dialog as a standard, high-level element that can be instantiated with a single call.  Java’s FileDialog will use this standard element, the same way it uses the supplied buttons and checkboxes.  We instantiate this FileDialog, give it a title, a starting directory and a filespec to list, in this case *.class.  The FileDialog does not appear until we call FileDialog.show.  This simply displays the dialog box, allowing the user to make his choice.  FileDialog.show does not return until the user has pressed either OK or Cancel.  Thus, when we reach the call fd.getFile, there should be a class file selected.  If the user hit Cancel, fd.getFile will return null, and we simply exit the method.

If the user actually selects a file, we instantiate our new ClassLoader, passing it the name of the file.  The constructor actually creates the Class object, which we can then retrieve by calling our own method, ch9_fig1_FileLoader.getTheClass.  We then try to instantiate this class.  If the instantiation works, we display one message in the list box.  If it fails, we display another.

So much for the preliminaries.  The heart of this application is our implementation of ClassLoader, ch9_fig1_FileLoader.  This particular ClassLoader crams all the functionality into the constructor.  Our constructor takes the name of the class file and loads that file into a byte array (via LoadFileBytes), then creates a Class from that byte array (via loadFromByteLump).  We store the new Class object in ourClass, to be returned whenever getTheClass is called.

LoadFileBytes is a simple method.  It instantiates a File, using the name of the class file that was supplied to our constructor.  It creates a byte array big enough to hold the file, then reads the file into that array, and returns the array.

loadFromByteLump is an equally simple method, but it contains the key method in this whole class loading sequence - ClassLoader.defineClass.  defineClass takes a byte array (presumably containing class data) and turns it into a Java Class object.  Simple as that - byte array in, Class object out.

The Class that exists after the call to defineClass is not quite complete though, because it contains a zillion references to other classes, some of which may also need to be loaded.  The Class cannot be instantiated until these references are all resolved.  We can do this by calling resolveClass.

resolveClass goes through the whole Class, calling ClassLoader.loadClass for each Class that gets referenced.  This is why we need to check if the Class has already been loaded by the primordial class loader.  Figure 9.2 shows the debugging output for a single run of our ClassLoader run against our LoaderTest class.  Notice that ClassLoader.loadClass gets called not only for LoaderTest, but also for Object (LoaderTest’s superclass), System (because of our static call to System.out.println) and PrintStream (because System.out is an instance of PrintStream).

We now have a Class.  That is, we have the run-time type information neccessary to make objects of type LoaderTest.  All that’s left is to actually make one.  In our response to the OK button in our file dialog, we call loadClass which returns an instance of the class Class.  We instantiate our new Class by calling Class.newInstance() which gives us back an instance of Object, the base Java class.

Now that we have a way of loading a class, all we need is a class to load.  Listing 9.2 shows a simple, standalone class, LoaderTest, that is suitable for loading via our new ClassLoader.

Listing 9.2: LoaderTest.java, a simple non-threaded class that prints a message to standard out when it gets instantiated.

/** A class that merely reports, to standard output whenever it

gets instantiated.  Used to test our ClassLoader.

*/

public class LoaderTest {

     public LoaderTest() {

           System.out.println( "loadertest constructor" );

           }

     }

 

We place the class file we get when we compile this class into a directory, \temp, that is purposely outside our classpath.  If this were within classpath, we might be able to load it with the primordial class loader, which we don’t want.  Figure 9.2 shows one run of our application and its standard output loading LoaderTest.class from the \temp directory via the the “Load Test 1” menu choice.

Figure 9.2: The class loader application window and the standard output of the application having loader LoaderTest.class.

<ch9_fig2.tif>

Notice that the message in our constructor prints out.  Our class has been successfully instantiated.  Listing 9.3 shows the LoadTest2 method which is a version of LoadTest1 modified slightly to give our newly loaded classes a little bit of freedom. 

Listing 9.3: the LoadTest2 method.

/** Test the loading of classes at this site by allowing the

user to choose a class file to load, then creating an object

from that class file. If the new object is an instance of

either Runnable or Thread, we start it running in its own

thread.

@see ch9_fig1_FileLoader

@see FileDialog

*/

public static void LoadTest2() {

  System.out.println( "Load test" );

 

  FileDialog fd = new FileDialog(f, "LoadTest");

  fd.setDirectory( "/agent/classes/beta/agent" );

  fd.setFile( "*.class" );

  fd.show();

  if( fd.getFile() != null ) {

    System.out.println( "Load test - "+fd.getFile() );

    if( fd.getFile() == null )

      return;

    ch9_fig1_FileLoader fl = new ch9_fig1_FileLoader(

                     fd.getFile() );

    Class c = fl.getTheClass();

    try {

      Object o = c.newInstance();

      ch9_fig1.show("Successfully created new instance of "+c);

      if( o instanceof Thread ) {

        Thread t = (Thread)o;

        t.start();

        ch9_fig1.show("Successfully started Thread");

        }

      else

        {

        if( o instanceof Runnable ) {

          Thread t = new Thread( (Runnable)o );

          t.start();

          ch9_fig1.show("Successfully started Runnable");

          }

        }

      }

    catch( Exception e ) {

      ch9_fig1.show( "Failed to create object from class file "+fd.getFile() );

      }

    }

  else

    System.out.println( "getFile == null" );

  }

In LoadTest2, we test the object that comes back from Class.newInstance to see whether it’s an instance of Thread, or if it implements the Runnable interface.  If the object is an instance of Thread, we start it running by invoking Thread.start.  If the object implements Runnable, we start it up by creating a Thread object with our new Object as its argument, and then calling Thread.start.  In order to test this method, we write two new test classes - LoaderTest1 - shown in Listing 9.4 and LoaderTest2 shown in Listing 9.5.

Listing 9.4: LoaderTest1.java, a simple Threaded class that prints a message to standard out.

/** A simple, threaded class that reports its instantiation to

standard out.  When it gets run via Thread.start, it sits in a

loop, sleeping for 1 second then reporting to standard out.

*/

public class LoaderTest1 extends Thread {

     public LoaderTest1() {

           System.out.println( "loadertest (subclasses Thread) constructor" );

           }

     public void run() {

           while( true ) {

                System.out.println( "Thread Subclass running" );

                try {

                     Thread.sleep( 1000 );

                     } catch( InterruptedException e ) {}

                }

           }

     }

 

Listing 9.5: LoaderTest2.java, a simple Runnable class that prints a message to standard out.

/** A simple, Runnable class that reports its instantiation to

standard out.  When it gets run via Thread.start, it sits in a

loop, sleeping for 700 milliseconds then reporting to standard

out.

*/

public class LoaderTest2 implements Runnable {

     public LoaderTest2() {

           System.out.println(

      "loadertest2 (implements Runnable) constructor" );

           }

     public void run() {

           while( true ) {

                System.out.println( "Runnable interface running" );

                try {

                     Thread.sleep( 700 );

                     } catch( InterruptedException e ) {}

                }

           }

     }

 

LoaderTest1 merely extends Thread.  Once started, via Thread.start, its run method sits in a loop printing a message to standard output every second, just so that we know that it instantiated and was able to run in its own thread.  Figure 9.3 shows the application window and the standard output after LoaderTest1.class has been loaded via the “Load Test 2” menu choice. 

Figure 9.3: The class loader application window and standard out having loaded the Thread subclass LoaderTest1.class.

<ch9_fig3.tif>

LoaderTest2 is virtually the same code as LoaderTest1 with the class implementing Runnable rather than extending Thread.

Figure 9.4: The class loader application window and standard out having loaded the Runnable class LoaderTest2.class.

<ch9_fig4.tif>

The three examples we’ve seen so far have all loaded and run a single, self-contained class - a class with no references to any class that is not available via the primordial class loader.  But what if our class wants to load a private class?  In Listing 9.6, LoaderTest3 modifies our previous LoaderTest class to reference a new private class, OtherClass, which ends up in its own .class file. 

Listing 9.6: LoaderTest3 references OtherClass.

/** A simple, Runnable class that reports its instantiation to

standard out.  When it gets run via Thread.start, it sits in a

loop, sleeping for 700 milliseconds then reporting to standard

out.  It also references OtherClass, so that our ClassLoader

has to load multiple class files, i.e. it has to respond to

loadClass when called by the interpreter.

*/

public class LoaderTest3 implements Runnable {

     public LoaderTest3() {

           System.out.println( "loadertest3 (implements Runnable) constructor" );

           }

     public void run() {

    OtherClass oc = new OtherClass();

           while( true ) {

                System.out.println( "Runnable interface running" );

                try {

        oc.Ping();

                     Thread.sleep( 700 );

                     } catch( InterruptedException e ) {}

                }

           }

     }

 

/** Our other class, simply reports every invocation of Ping to

standard out.

*/

class OtherClass {

  int iteration = 0;

  public void Ping() {

    System.out.println( "OtherClass iteration "+iteration);

    iteration++;

    }

  }

 

Figure 9.5 shows what happens when we try to load the new LoaderTest3 class file using the old, single-class, ClassLoader, ch9_fig1_FileLoader. 

Figure 9.5: The class loader application window and standard out when trying to load Runnable class LoaderTest3.class which also references OtherClass.class via the single class ClassLoader.

<ch9_fig5.tif>

What happens is that when, within LoaderTest3.run, we tried to instantiate OtherClass, the Java interpreter called ch9_fig1_FileLoader.loadClass (our implementation of ClassLoader.loadClass) passing the String “OtherClass” as the argument.  The interpreter expects our ClassLoader to load this class.  But ch9_fig1_FileLoader can only load one class.  When loadClass is called, we compare the class name (“OtherClass”) to the name of the class we’ve loaded (“LoaderTest3”).  That isn’t it, so we return null.  The interpreter fails to find the class, and throws a NoClassDefFoundError.  All very simple.

What we need to do is to write a new ClassLoader that can load the other classes (such as OtherClass) that our original class (LoaderTest3) might need.  Listing 9.7 shows our new ClassLoader, ch9_fig3_FileLoader and Listing 9.8 shows the new method, LoadTest3 that invokes this new ClassLoader.

Listing 9.7: The new, multi-class ClassLoader.

/** A ClassLoader that can load an entire package (subdirectory

of classes) given the filename of a single class file in that

package.  Keeps a Hashtable of class names, and tries to load

any unresolved references as class files from the same

directory as the one that the original class file came from.

@version 1.0 1/1/1996

@author John Rodley

*/

class ch9_fig3_FileLoader extends ClassLoader {

  Class ourClass;

  Hashtable cache;

  String directory;

 

/** constructor saves the directory dir for use in finding new

classes supplied to loadClass by the interpreter.  Loads file

immediately so that the caller can instantiate it and get the

whole loading thing going.  file is considered the "lead" class

in this operation.

@param  dir A String directory name gotten from the FileDialog.

@param  file  The fully qualified filename of the lead class.

*/

  public ch9_fig3_FileLoader(String dir, String file) {

    super();

    directory = new String( dir );

    cache = new Hashtable();

    byte classBytes[] = LoadFileBytes(file);

    ourClass = loadFromByteLump(classBytes,true);   

    }

 

/** Return the lead class.

@return Class A Class object created from the class file

supplied to the constructor.

*/

  public Class getLeadClass() {

    return( ourClass );

    }

 

/** Load the specified class file into a byte array and return

that byte array.  All class files are read into the load

message via this method.

*/

  public byte[] LoadFileBytes(String fileName) {

    byte bret[] = null;

    System.out.println( "Loading class from file "+fileName );

    try {

      FileInputStream fi = new FileInputStream( fileName );

      int filesize = fi.available();

      bret = new byte[filesize];

      fi.read( bret );

      } catch( IOException e ) {

        System.out.println("LoadClass "+fileName+" ex "+e );}

    return( bret );

    }

 

/** Return the byte lump that corresponds to the CLASS NAME

supplied.

@param  name  The CLASS name of the class we want to load.  We

create the fully-qualified filename by appending the class name

and ".class" to the directory we saved in the constructor.

@return the byte array we created from this class file.

*/

  private byte loadClassData(String name)[] {

    System.out.println( "loading secondary class "+name );

    String filename = new String( directory+name+".class" );

    System.out.println( "loading secondary class filename "+filename );

    return( LoadFileBytes(filename));

    }

 

/** Create a Class from an array of bytes.  Defines the class,

adds the class name to the Hashtable so that we only load it

once, and resolve references if specified. 

@param lump The byte array that contains the class data read

from the .class file.

@param  resolve A flag telling us whether to try to resolve

this Class now.

@return Class A Class object created from the byte lump.

*/

  public synchronized Class loadFromByteLump( byte[] lump,

                                        boolean resolve ) {

    Class c;

    c = defineClass(lump, 0, lump.length);

    if( resolve )

      resolveClass( c );

    return( c );

    }

 

/** Load the named class.  This method is called by the

interpreter whenever it runs into a class name it needs to

resolve.  Checks with the primordial class loader first via

findSystemClass, then checks our Hashtable to see if we've

loaded the desired class already, then calls loadClassData to

actually load the thing from disk.

@param  name  The class name (NOT THE FILE NAME).

@param  resolve A flag telling us whether to resolve this class

or not.

@return A Class representing the class named in name.

*/

  public synchronized Class loadClass(String name,

                                 boolean resolve) {

    Class c = null;

 

    System.out.println( "loadClass("+name+")");

    try {

      c = findSystemClass( name );

      System.out.println( "Resolved "+name+" locally" );     

      }

    catch( ClassNotFoundException e ) {

      if(( c = (Class)cache.get(name)) == null ) {

        System.out.println( "Resolving "+name+" remotely" );

        byte[] b = loadClassData( name );

        c = loadFromByteLump( b, true );

        }

      }

    return c;

    }

  }

 

Listing 9.8: LoadTest3, the method within ch9_fig1 that invokes our new ClassLoader.

 

/** Test the loading of classes at this site by allowing the

user to choose a class file to load, then creating an object

from it.  If the new object is an instance of either Runnable

or Thread we set it running in its own thread.

@see ch9_fig3_FileLoader

@see FileDialog

*/

public static void LoadTest3() {

  System.out.println( "Load test" );

 

  FileDialog fd = new FileDialog(f, "LoadTest");

  fd.setDirectory( "\temp" );

  fd.setFile( "*.class" );

  fd.show();

  if( fd.getFile() != null ) {

    System.out.println( "Load test - "+fd.getFile() );

    if( fd.getFile() == null )

      return;

    ch9_fig3_FileLoader fl = new ch9_fig3_FileLoader(

                     fd.getDirectory(), fd.getFile() );

    Class c = fl.getLeadClass();

    try {

      Object o = c.newInstance();

      ch9_fig1.show("Successfully created new instance of "+c);

      if( o instanceof Thread ) {

        Thread t = (Thread)o;

        t.start();

        ch9_fig1.show("Successfully started Thread");

        }

      else

        {

        if( o instanceof Runnable ) {

          Thread t = new Thread( (Runnable)o );

          t.start();

          ch9_fig1.show("Successfully started Runnable");

          }

        }

      }

    catch( Exception e ) {

      ch9_fig1.show( "Failed to create object from class file "+fd.getFile() );

      }

    }

  else

    System.out.println( "getFile == null" );

  }

}

The key elements of this new setup are the directory supplied to the ClassLoader constructor, the loadClass method and the Hashtable, cache.  The one thing we absolutely need to know when loading ‘secondary’ classes like OtherClass is what directory they’re in.  The file name of the file we need to load, by definition, must be <directory>/<classname>.class.  So when we invoke the ClassLoader from LoadTest3, we pass in the directory name, which we obtain from FileDialog.getDirectory.  Then, when the interpreter invokes ch9_fig3_FileLoader.loadClass with OtherClass as the argument, we simply append OtherClass.class to the directory and we have the name of the file that needs to be loaded.  Figure 9.6 shows the application window and standard output when we load LoaderTest3.class via the “Load Test 3” menu choice.

Figure 9.6: The class loader application window and standard out having loaded the Runnable class LoaderTest3.class which also references OtherClass.class with the new multi-class ClassLoader.

<ch9_fig6.tif>

The one thing we haven’t talked about is the Hashtable, cache.  The Hashtable is only there to prevent us from loading classes over and over again.  Once a class is loaded, we add it to the Hashtable, and then, whenever the Class is requested in the future, we simply retrieve the stored Class from the Hashtable rather than loading it from disk, or over the network again.

This ability to load and run classes is the heart of any browser’s Java-bility.  Browsers provide a lot more service to the classes they load than our AgentServers do.  Browsers also make a few more assumptions about the classes that they load, specifically that each sub-classes Applet, but the basic theory is no different - Load a class, find out what it is, and set it running if possible.

Loading Agents

So now we know how to take a lump of bytes and instantiate a Java class from it.  We’re still one short step away from having a useful Agent, primarily because our loaded class is isolated.  It’s loaded, and possibly running in its own thread, but it has no connection to the AgentServer - no way to request the kind of services that an AgentServer might reasonably be expected to supply.  This is painfully obvious from the examples of Listing 9.5 and 9.6 when the loaded class reports its instantiation and every pass through its run loop.  Ideally, we’d have it report to the list box on the applications frame window, but the loaded class has no connection to that list box.  It doesn’t know anything about the ch9_fig1 class.  It needs an interface to that class.  For Applets, that interface is the AppletContext interface.  In our AgentServer application, that interface is the AgentContext interface.

In Chapter 4 we talked about how Applets communicate with their browser via the AppletContext interface.  Essentially, the AppletContext is the environment in which the an Applet executes.  All the services the Applet can get from a browser are provided through the AppletContext.  In the AgentServer, we’ll implement something very similar to the AppletContext, the AgentContext.  The AgentContext is simply a way for the AgentServer to help the Agent do its job.  The AgentContext source is listed back in Chapter 3, Listing 3.1.  Its methods are summarized again here in Table 9.3.

Table 9.3: The AgentContext interface methods.

dispatch           Tells the AgentServer to re-dispatch this Agent to all the AgentServers in its list.  This is how the Agent multiplies across the network.

writeOutput       Writes a line of HTML output to the output file the AgentServer has opened for us.  The Agent has NO other access to the opened file.

getResultsURL  Gets a String URL of the output file.  The Agent sends this back to the AgentLauncher via reportFinish.

reportStart        Tells the AgentLauncher that this Agent is running.

reportFinish       Tells the AgentLauncher that this Agent has finished, and gives the output file URL if any.

For security reasons, we’ve designed our AgentContext to supply a very high level of service to the Agent.  For instance, the fact that the Agent doesn’t doesn’t directly create, or write to, the results file means that we can completely eliminate file IO for Agents, the same way Netscape does for applet.  The same applies to network access.  The Agent dispatches, and reports back not directly via Socket, but via methods in the AgentContext, so we could reasonably eliminate network IO from the services available to Agents.

The Base Agent Class

All Agents must subclass the base class Agent, shown in back in Chapter 3, Listing 3.2. Agent is almost purely abstract.  The only bit of implementation written into it merely sets an AgentContext for the Agent to use.  Realistically, Agent could as easily have been written as an interface.  In fact, you should thing of the Agent class and the AgentContext interface as two ends of the same conversation.  The Agent “sees” the AgentServer through the AgentContext interface, and the AgentServer “sees” the Agent through the abstract Agent base class.

It’s important to realize that an Agent has to be able to run in two very different environments: within an Applet at the AgentLauncher, and within its own thread at the AgentServer.  Thus you have two sets of methods, one to be used by the AgentLauncher, and one by the AgentServer.

From these methods, we can now see the series of interactions an Agent has with its hosts, AgentLauncher and AgentServer:

* AgentLauncher calls Agent.configure, which gets the arguments from the user.

* AgentLauncher calls getArguments, which packages the arguments the user set as a Vector of Strings.

* At this point, the AgentLauncher dispatches the Agent.  It arrives on an AgentServer where it gets instantiated.  At that point, there is an Object of type Agent on the AgentServer.  It has not been set running yet.

* AgentServer calls setArguments to give the Agent the arguments it needs to perform its task.

* AgentServer calls setAgentContext to give the Agent a way to talk to the AgentServer.

* At this point, the AgentServer can set the Agent running in its own thread because the Agent now has a way to report its existence back to the AgentLauncher - via AgentContext.reportStart and reportFinish.

The driving design ideal behind the Agent is that Agents are entirely self-contained.  Neither the AgentServer, nor the AgentLauncher knows anything more about the Agent than it absolutely has to.  Thus, the Agent is required to obtain its own arguments from the user on the AgentLauncher.  This is pure O-O, and pretty neat, but it does have one unpleasant implication: when the Agent gets transmitted across the network, it drags this configuration code with it despite the fact that it will probably never be run out on the AgentServer.

An Example Agent: FileFinder

Well, we’ve talked around the issue long enough.  It’s time to write a real, live Agent and see what it looks like, and how it works.  Listing 9.?? shows our FileFinder class, a relatively simple Agent that merely tries to match the files it finds out on an AgentServer against any of a series of file specifications the user on the AgentLauncher provides.

Listing 9.9: The FileFinder Agent.

package agent.FileFinder;

 

import java.lang.*;

import java.util.*;

import java.awt.*;

import java.io.*;

import agent.Agent.*;

import agent.FileFinder.*;

// To catch the definition of AgentContext

import agent.Server.AgentContext;    

 

/** An Agent subclass for finding files that match a particular

set of filename filters.

@version 1.1

@author John Rodley

*/

public class FileFinder extends Agent {

     ConfigurationDialog cfd;

     Vector args;

 

/** Constructor - does nothing by design, but it's useful to

leave the println in there just to convince yourself that the

Agent has been instantiated on the AgentServer.

*/

     public FileFinder() {

           System.out.println( "FileFinder constructor" );

           }

 

/** Put up a ConfigurationDialog that gets the arguments this

Agent needs to run on an AgentServer.

@param  frame The frame window of the browser, needed for the

dialog constructor.

*/

     public void configure( Frame frame ) {

           cfd = new ConfigurationDialog( frame );

           cfd.show();

           }

 

/** Return whatever arguments the configure method got from the

user as a Vector of Strings.

@return A Vector of Strings that are only meaningful to the

Agent itself, not to either the AgentLauncher or AgentServer.

*/

     public Vector getArguments() {

           return( cfd.args );          

           }

 

/** Configure the Agent with the specified Vector of Strings as

'arguments'. Called by the AgentLauncher, passing the arguments

it pried out of the LoadMessage.

@param  ar  A Vector of Strings identical to the one returned

to the AgentLauncher by getArguments. 

*/

     public void setArguments( Vector ar ) {

           args = ar;

           }

 

/** The run loop for this Agent.  Gets the top-level directory

which this Agent is allowed to read from the properties file

via the key acl.read, and checks all the files in that

directory against the filenamefilter specified by the user back

on the AgentLauncher.

*/

     public void run() {

           String topDirectory = System.getProperty( "acl.read" );

           if( topDirectory == null ) {

                System.out.println( "can't read this machine" );

                return;

                }

           System.out.println( "got value "+topDirectory

                      +" for property acl.read" );

           boolean keepGoing = true;

           String currentDirectory = new String(topDirectory);

           ac.reportStart( "" );

           while( keepGoing ) {

                System.out.println( "currentDirectory = "+currentDirectory );

                File f = new File(currentDirectory);

                AgainstArgs aa = new AgainstArgs( args );

                String filelist[] = f.list( aa );

                System.out.println( "filelist = "+filelist );

                keepGoing =false;

                if( filelist.length == 0 ) {

                     ac.reportFinish( "XXXXX", null, 0, "no results, sorry" );

                     }

                else {

                     // Start the HTML file

                     ac.writeOutput(

  "<HTML><HEAD><TITLE>FileFinderOutput</TITLE></HEAD><BODY>" );

                     // Start an unordered list

                     ac.writeOutput( "<UL>" );   

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

                           System.out.println( "filelist["+i+"] = "+filelist[i] );

                           String s = new String("<LI>"+filelist[i]+"</LI>" );

                           ac.writeOutput( s );

                           }

                     // End the unordered list

                     ac.writeOutput( "</UL>" );

                     // End the HTML file

                     String s = new String( "</BODY></HTML>" );

                     ac.writeOutput( s );

                     try {

                           Thread.sleep( 10000 );

                     } catch( InterruptedException e ){System.out.println("ex "+e); }

                     keepGoing =false;

                     ac.reportFinish( "XXXXX", ac.getResultsURL(""), 100,

            "This is the comment" );

                     }

                }

           ac.dispatch();

           }

  }

 

/** A dialog box for configuring a FileFinder Agent.  Allows

the user to enter up to 7 filenames to search for.

@see Dialog

*/

class ConfigurationDialog extends Dialog {

  Label theLabel;

  Button theButton;

  TextField tf[] = new TextField[7];

  Panel ButtonPanel;

  Panel TextFieldPanel;

  public Vector args;

 

/** constructor create a dialog with a certain title, lay it

out border style, add a prompt, 7 TextFields for entering the

file specs and OK and Cancel buttons.

@param  parent  The Frame that is the parent of this dialog.

*/

  public ConfigurationDialog(Frame parent) {

     super(parent, "Configure File Finder", true);

       setLayout(new BorderLayout());

     theLabel = new Label( "Enter up to 7 file specifications:" );

       add("North",theLabel);

     TextFieldPanel = new Panel();

       TextFieldPanel.setLayout( new GridLayout(7, 1 ));

     add("Center", TextFieldPanel );

       for( int i = 0; i < 7; i++ ) {

             tf[i] = new TextField( "", 25 );

           TextFieldPanel.add( tf[i] );

          }

     Dimension d = tf[0].preferredSize();

     ButtonPanel = new Panel();

       add( "South", ButtonPanel );

     theButton = new Button( "Ok" );

       ButtonPanel.add( theButton );

     setResizable(false);

    }

 

/** Deal with the user hitting either OK or Cancel.  In either

case, fill the argument Vector with whatever's in the

TextFields and dispose of the dialog.

*/

  public boolean action(Event e, Object o) {

     if( e.target instanceof Button )

          {

           args = new Vector(1);

          for( int i = 0; i < 7; i++ ) {

                  if( tf[i].getText().length() > 0 &&

                  (tf[i].getText().compareTo("") != 0 ))

                     {

                     byte b[] = new byte[tf[i].getText().length()];

                      tf[i].getText().getBytes( 0, b.length, b, 0 );     

                     args.addElement( b );

                     }

                }

             }

     dispose();

       return true;

     }

     }

 

 

/** A FilenameFilter to use to screen files against the file

specs the user has configured this FileFinder Agent with.

Accepts the file if it's in the list, rejects it otherwise.

*/

class AgainstArgs implements FilenameFilter {

     Vector args;

 

/** constructor, stores the filename list Vector for later use

by accept.

@param  arglist The Vector of filenames.

*/

     public AgainstArgs( Vector arglist ) {

           args = arglist;

           }

 

/** Return true if the filename supplied matches one of the

files named in the Vector of filenames the user configured this

Agent with.

@param  f The file as a File object.

@param  filename  The name of the file.

*/

     public boolean accept( File f, String filename ) {

           for( int i = 0; i < args.size(); i++ ) {

                String s = new String( (byte [])args.elementAt(i), 0);

                if( filename.compareTo(s) == 0 ){

                     return( true );

                     }

                }

           return( false );

           }

     }

Our Agent consists of three classes - FileFinder, ConfigurationDialog and AgainstArgs.

We’ve already talked about the configuration process back in Chapter 3 and dialog boxes in general in Chapter 5, so we won’t waste a lot of time talking about the ConfigurationDialog class here.  All you should probably note there is that in the action method we save the contents of the TextFields so that the FileFinder.getArguments method can get them later.

The AgainstArgs class is an implementation of the FilenameFilter interface.  What the FilenameFilter interface provides is a way of plucking just the files you want out of an arbitrarily large directory of files.  The FilenameFilter interface has only one method, accept.  accept takes a file name as an argument and returns true if the file should be part of the list, false otherwise.  Our implementation of accept simply does a String.compareTo of the supplied filename against each of the (up to 7) file names the user supplied to the ConfigurationDialog.

With those two classes out of the way, it’s time to look at the FileFinder itself.  As a subclass of Agent FileFinder has to implement all of Agents methods except setAgentContext.  configure (called on the AgentLauncher) merely runs the ConfigurationDialog.  getArguments (also called on the AgentLauncher) merely returns the Vector of String filenames the user entered in the ConfigurationDialog.  setArguments (called on the AgentServer) is a little trickier. 

In order to explain the setArguments method, we have to backtrack a bit, and look at the LoadMessage class.  Back in Chapter 7 we talked about the messages AgentServers exchange and listed them in dreary detail.  The one message we didn’t talk about was the LoadMessage, shown in Listing 9.10.

Listing 9.10: The LoadMessage.

package agent.util;

 

import java.awt.*;

import java.lang.*;

import agent.util.*;

import java.util.*;

import java.net.*;

import java.io.*;

 

/** A message that tells the receiver to load, instantiate and

run the class that is supplied in the message.  Message

format:

 

 The command  Load  4 bytes

 The length   10 bytes ascii int

 

 

         The dispatching agent server

Field hdr   Dsrv  4 bytes

length            4 bytes

server-name:port  length bytes

 

         The arguments (unlimited repetitions)

Field hdr Arg_    4 bytes

length            4 bytes ascii int

Argument data     length bytes

 

         The signature 

Field hdr Sig_    4 bytes

The length        4 bytes

The sig data     length bytes

 

         The run id  

Field hdr ID__    4 bytes

The length        4 bytes

The run id        length bytes

 

         The class data (unlimited repetitions)

Field hdr Clas    4 bytes

length            10 bytes ascii int

the class data    length bytes

 

Class contains both message construction and message parsing

methods. Also contains some message processing in the form of

class loading.  Essentially, the receiver of a load message is

looking to receive a Java Class, so the parse method

implements this.

 

@version 1.0 1/1/1996

@author John Rodley

@see DispatchMessage

@see Message

*/

public class LoadMessage extends Message{

  byte args[][];

  public Vector vargs;

  byte sig[];

  byte leadclas[];

  Vector otherclasses = new Vector(1);

  byte id[];

  byte bdsrv[];

 

  String ssig;

  public String sid;

  String sname;

  public String dispatching_server_name;

  public int dispatching_server_port;

 

  int i, j;

  public static final int PREFIX_SIZE=4;

  public static final String LOAD_PREFIX = new String("Load");

  public static final String ARG_PREFIX = new String("Arg_");

  public static final String CLASS_PREFIX = new String("Clas");

  public static final String SIG_PREFIX = new String("Sig_");

  public static final String ID_PREFIX = new String("ID__");

  public static final String DSRV_PREFIX = new String("DSrv");

 

  public static final int LOADLEN_SIZE=10;

  public static final int ARGLEN_SIZE=4;

  public static final int CLASSLEN_SIZE=10;

  public static final int SIGLEN_SIZE=4;

  public static final int IDLEN_SIZE = 4;

  public static final int DSRVLEN_SIZE = 4;

  public static final String ClassPath =

       new String( "/agent/classes/rel/" );

 

/** The do-nothing constructor.  Called when parsing a Load

Message */

  public LoadMessage() { };

 

/** This is the constructor used by an AgentServer that wishes

to SEND a load message.  Supply the name of the lead class,

the id, the signature, arguments, dispatching server and port.

*/

  public LoadMessage( String name, String ID, String thesig,

      Vector args, String dispatchServer, int dispatchPort )

   {

    StringTokenizer st = new StringTokenizer( name, "." );

    Vector v = new Vector(1);

    while( st.hasMoreElements())

      v.addElement( st.nextElement());

    String filename = new String("");

    for( int i = 0; i < v.size(); i++ ) {

      filename = new String(filename+(String)v.elementAt(i));

      if( i < (v.size()-1))

        filename = filename+"/";

      System.out.println( "filename = "+filename );

      }      

    // filename is the name of the root class of this Agent

    // the class that needs to be instantiated on the AgentServer

    filename = filename+".class";

    leadclas = LoadClassFromFile( ClassPath+filename );

    LoadClassesFromDirectory( filename );

    vargs = new Vector(1);

    for( int i = 0; i < args.size(); i++ )

      vargs.addElement( args.elementAt(i) );

    sid = new String( ID );

    ssig = new String( thesig );

    dispatching_server_name = dispatchServer;

    dispatching_server_port = dispatchPort;

    }

 

/** parse the supplied byte array as if it were a load

message.  Start parsing at the supplied currentOffset.

currentOffset should point to the FIRST byte of the SECOND

field in a load message, i.e. to the first character in the

word "DSrv".  Fills the instance variables:

  dispatching_server_name

  dispatching_server_port

  vargs

  leadclass

  otherclasses

  ssig

  sid

with data from the message.  Instantiates the lead class if

the message is OK, but does not call the run method.

*/

public Object parse(byte b[], int currentOffset) {

String command;

byte sig[];

String s;

Class leadC = null;

 

   

  // Followed by some number of byte array arguments

  s = new String( b, 0, currentOffset, PREFIX_SIZE );

  currentOffset += PREFIX_SIZE;

 

  if( s.compareTo( DSRV_PREFIX ) == 0 )

    {

    // the next thing is ASCII 4 bytes of length

    String sl = new String( b, 0, currentOffset, DSRVLEN_SIZE);

    currentOffset+=DSRVLEN_SIZE;

    Integer length = new Integer( sl );

    bdsrv = new byte[length.intValue()];

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

      bdsrv[i] = b[currentOffset++]; 

    StringTokenizer st =

          new StringTokenizer( new String(bdsrv,0), ":" );     

    dispatching_server_name =

          new String( (String)st.nextElement());

    Integer iport = new Integer( (String)st.nextElement());  

    dispatching_server_port = iport.intValue();

 

    s = new String( b, 0, currentOffset, PREFIX_SIZE );

    currentOffset += PREFIX_SIZE;

    }

  else {

    System.out.println( "out of sync at DSRV" );

    return( null );

    }

 

  vargs = new Vector(1);

  int numargs = 0;

  while( s.compareTo( ARG_PREFIX ) == 0 )

    {

    // the next thing is ASCII 4 bytes of length

    String sl =

        new String( b, 0, currentOffset, ARGLEN_SIZE);

    currentOffset+=ARGLEN_SIZE;

    Integer length = new Integer( sl );

    byte arg[] = new byte[length.intValue()];

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

      arg[i] = b[currentOffset++];       

    vargs.addElement( arg );

 

    s = new String( b, 0, currentOffset, PREFIX_SIZE );

    currentOffset += PREFIX_SIZE;

    }

 

// s is already set to the command name

  if( s.compareTo( SIG_PREFIX ) == 0 ) {

    // the next thing is ASCII 4 bytes of length

    String sl = new String( b, 0, currentOffset, SIGLEN_SIZE );

    currentOffset+=SIGLEN_SIZE;

    Integer length = new Integer( sl );

    sig = new byte[length.intValue()];

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

      sig[i] = b[currentOffset++];       

    ssig = new String( sig, 0 );

    }

  else {

    System.out.println( "out of sync at sig" );

    return( null );

    }

 

  s = new String( b, 0, currentOffset, PREFIX_SIZE );

  currentOffset += PREFIX_SIZE;

  if( s.compareTo( ID_PREFIX ) == 0 )

    {

    String sl = new String( b, 0, currentOffset,IDLEN_SIZE );

    currentOffset += IDLEN_SIZE;

    Integer length = new Integer( sl );

    id = new byte[length.intValue()];

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

      id[i] = b[currentOffset++];

      }

    sid = new String( id, 0 );

    }

  else {

    System.out.println( "out of sync at ID" );

    return( null );

    }

 

  s = new String( b, 0, currentOffset, PREFIX_SIZE );

  currentOffset += PREFIX_SIZE;

  int classnum = 0;

  otherclasses = new Vector(1);

  while( s.compareTo( CLASS_PREFIX ) == 0 )

    {

    // the next thing is ASCII 10 bytes of length

    String sl = new String( b, 0, currentOffset,CLASSLEN_SIZE);

    currentOffset += CLASSLEN_SIZE;

    Integer length = new Integer( sl );

    byte bclas[] = new byte[length.intValue()];

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

      bclas[i] = b[currentOffset++]; 

      }

    // Only instantiate the first class in the message

    if( classnum == 0 )

      leadclas = bclas;

    else {

      otherclasses.addElement(bclas);

      }

    s = new String( b, 0, currentOffset, PREFIX_SIZE );

    currentOffset += PREFIX_SIZE;

    classnum++;

    }

  if( classnum == 0 )

    System.out.println( "out of sync at class" );

  else {

    AgentLoader al = new AgentLoader(otherclasses);

    leadC = al.loadFromByteLump(leadclas, true);

    try {

      Object o = leadC.newInstance();

      return( o );

      } catch( Exception e1 )

        { System.out.println( "new instance exception" ); }

    }

  return( null );

  }

 

/** Load the instance variable otherclasses with byte arrays

that are filled from ALL the class files (except the lead

class) in the directory embedded in the supplied class name.

*/

public void LoadClassesFromDirectory( String leadclass ) {

  System.out.println( "LoadClassesFromDir "+leadclass);

 

  int j = leadclass.lastIndexOf('/');

  String absoluteDir = leadclass.substring(0,j);

  System.out.println( "absoluteDir = "+absoluteDir );

  File f = new File( absoluteDir );

  if( f.isDirectory() != true ) {

    System.out.println( absoluteDir+" not a directory" );

    return;

    }

  ExtensionFilter ef = new ExtensionFilter( ".class" );

  String sa[] = f.list( ef );

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

    if( leadclass.compareTo(absoluteDir+"/"+sa[i]) == 0 )

      // lead class is already loaded

      {

      System.out.println( "Skipping leadclass "+sa[i] );

      continue;

      }

    otherclasses.addElement(

         LoadClassFromFile( absoluteDir+"/"+sa[i] ));  

    }

  }

 

/** Load the specified class file into a byte array and return

that byte array.  All class files are read into the load

message via this method.

*/

public byte[] LoadClassFromFile( String name ) {

  byte bret[] = null;

  System.out.println( "Loading class from file "+name );

  try {

    FileInputStream fi = new FileInputStream( name );

    int filesize = fi.available();

    byte[] lump = new byte[filesize];

    fi.read( lump );

    String prefix = makePrefix( CLASS_PREFIX,

                            filesize, CLASSLEN_SIZE );

    bret = new byte[prefix.length()+lump.length];

    prefix.getBytes( 0, prefix.length(), bret, 0 );

    for( i = 0; i < lump.length; i++ )

      bret[prefix.length()+i] = lump[i];

    } catch( IOException e ) {

      System.out.println("LoadClass "+name+" ex "+e );}

  return( bret );

  }

 

/** Actually fill the byte array 'msg' with ALL the bytes that

make up this load message.  Expects the intance variables:

  dispatching_server_name

  dispatching_server_port

  vargs

  leadclass

  otherclasses

  ssig

  sid

to already be filled with valid data.

*/

public void createMessage() {

  String s;

  int totallength = 0;

 

  if( leadclas == null )

    System.out.println( "No lead class loaded" );

 

// the load command

  totallength = PREFIX_SIZE+LOADLEN_SIZE;

 

// the dispatching server

  String srv = new String( dispatching_server_name+":"+

                      dispatching_server_port );

  s = makePrefix( DSRV_PREFIX, srv.length(), DSRVLEN_SIZE );

  bdsrv = new byte[s.length()+srv.length()];

  s.getBytes(0,s.length(),bdsrv, 0 );

  srv.getBytes(0,srv.length(), bdsrv, s.length());

  totallength += bdsrv.length; 

 

// the arguments

  args = new byte[vargs.size()][];

  for( i = 0; i < vargs.size(); i++ ) {

    byte ba[] = (byte [])vargs.elementAt(i);

    s = makePrefix( ARG_PREFIX, ba.length, ARGLEN_SIZE );

    args[i] = new byte[s.length()+ba.length];

    s.getBytes( 0, s.length(), args[i], 0 );

    for( int k = 0; k < ba.length; k++ )

      args[i][k+s.length()] = ba[k];

    totallength += args[i].length;

    }

 

// the signature

  s = makePrefix( SIG_PREFIX, 40, SIGLEN_SIZE );

  sig = new byte[s.length()+40];

  s.getBytes( 0, s.length(), sig, 0 );

  totallength += sig.length;

 

// the ID

  s = makePrefix( ID_PREFIX, sid.length(), IDLEN_SIZE );

  id = new byte[s.length()+sid.length()];

  s.getBytes( 0, s.length(), id, 0 );

  sid.getBytes( 0, sid.length(), id, s.length());

  totallength += id.length;

 

// the raw class data is already set via LoadClassFromFile

  totallength += leadclas.length;

  for( i = 0; i < otherclasses.size(); i++ ) {

    byte ba[] = (byte [])otherclasses.elementAt(i);

    totallength += ba.length;

    }

 

  s = makePrefix( LOAD_PREFIX, totallength, 10 );

  command = new byte[s.length()];

  s.getBytes( 0, s.length(), command, 0 );

 

  msg = new byte[totallength];

  int currentOffset = 0;

  for( i = 0; i < command.length; i++ )

    msg[currentOffset++] = command[i];

  for( i = 0; i < bdsrv.length; i++ )

    msg[currentOffset++] = bdsrv[i];

  for( j = 0; j < args.length; j++ ) {

    for( i = 0; i < args[j].length; i++ )

      msg[currentOffset++] = args[j][i];

    }

  for( i = 0; i < sig.length; i++ )

    msg[currentOffset++] = sig[i];

  for( i = 0; i < id.length; i++ )

    msg[currentOffset++] = id[i];

  for( i = 0; i < leadclas.length; i++ )

    msg[currentOffset++] = leadclas[i];

  for( i = 0; i < otherclasses.size(); i++ ) {

    byte ba[] = (byte [])otherclasses.elementAt(i);

    for( j = 0; j < ba.length; j++ )

      msg[currentOffset++] = ba[j];

    }

   

  }

}

 

/** A class to implement a class loader for loading agents on

an AgentServer.  The AgentServer uses this classloader to

resolve references that occur when it loads an Agent from a

load message.

@see LoadMessage

@version 1.0 1/1/1996

@author John Rodley

*/

class AgentLoader extends ClassLoader {

        static Hashtable cache = new Hashtable();

  Vector otherclasses;

  Vector classes = new Vector(1);

 

/** Take a vector of byte arrays filled with all the classes

UNIQUE TO THIS AGENT (except the lead class) and create a

class for each one, without resolving the references in those

classes.  Reference resolution is only done when the lead

class is instantiated.

*/              

  public AgentLoader(Vector oc) {

    super();

    otherclasses = oc;

    for( int i = 0; i < otherclasses.size(); i++ ) {

      Class c = loadFromByteLump(

           (byte[])otherclasses.elementAt(i), false );

      classes.addElement(c);

      }

    }

 

/** Load the specified class into the Java system.  This

should only happen ONCE for any class.  Ideally, we shouldn't

be loading all the classes every time this gets called,but

we're boxed into this by the fact that the message design

doesn't 'label' the class bytecodes with the class name, thus

we can't pick an individual byte lump to load.  Message

structure should be revisited in light of this.

 

Resolves references for the named class, but not for other

classes.  Any other classes referenced by the named class will

have their references resolved automatically by the single

call to resolveClass.

*/

  private byte loadClassData(String name)[] {

    for( int i = 0; i < otherclasses.size(); i++ )

      {

      Class c = loadFromByteLump(

                 (byte[])otherclasses.elementAt(i), false );

      System.out.println( "loaded "+c );

      if( c.getName().compareTo(name) == 0 )

        {

        System.out.println( "match "+c );

        resolveClass(c);

        return( (byte[])otherclasses.elementAt(i) );

        }

      else

        System.out.println( "no match "+c );

      }

    return( null );

    }

 

/** Create a Class from an array of bytes.  Defines the class,

adds the class name to the hashtable so that we only load it

once, and resolve references if specified. 

*/

  public synchronized Class loadFromByteLump( byte[] lump,

                                        boolean resolve ) {

    Class c;

    c = defineClass(lump, 0, lump.length);

    cache.put(c.getName(), c );

    if( resolve )

      resolveClass( c );

    return( c );

    }

 

/** Top-level method called by Java when classes within an

Agent need to be loaded.  We call it once for the lead class,

any other calls are generated internally by Java via the call

we make to resolveClass when loading the lead class.  The

return value is a fully resolved Class that can be instantiated.

This is the implementation of a ClassLoader method.

*/

  public synchronized Class loadClass(String name,

                                 boolean resolve) {

    Class c;

 

    System.out.println( "loadClass("+name+")");

    try {

      c = findSystemClass( name );

      System.out.println( "Resolved "+name+" locally" );     

    } catch( ClassNotFoundException e ) {

      System.out.println( "Resolving "+name+" remotely" );     

                  c = (Class)cache.get(name);

                  if (c == null) {

        byte data[] = loadClassData(name);

        c = loadFromByteLump( data, resolve );

        }

      resolveClass(c);

      }

                return c;

    }

  }

 

 

/** A filename filter that simply looks for file names with a

particular extension.  If the extension of the file name

supplied  to accept matches the extension supplied to the

constructor, the file is accepted.

 

@version 1.0

@author John Rodley

*/

class ExtensionFilter implements FilenameFilter {

  String soughtExtension;

  public ExtensionFilter(String ext) {

    soughtExtension = new String(ext);

    }

 

/** If the extension of the file name supplied

to accept matches soughtExtension the file is

accepted - accept returns true.

*/

  public boolean accept( File f, String name ) {

    int i = name.lastIndexOf( '.' );

    if( i > 0 ) {

      String extension = name.substring(i);

      if( extension.compareTo(soughtExtension) == 0 ) {

        System.out.println( "accepting "+name);

        return( true );

        }

      }

    System.out.println( "rejecting "+name );

    return( false );

    }

  }

 

The LoadMessage carries the actual Agent class data from one AgentServer to another.  Thus, when an AgentServer receives a LoadMessage, it knows to pull the class data out of that message, instantiate it and start it running as an Agent.   Like all messages, LoadMessage has two ends - creation and parsing.  A LoadMessage is created on the dispatching AgentServer and parsed on another AgentServer.  We’ve gone over the basics of message creation back in Chapter 7.  The unique twists LoadMessage adds are the two methods, LoadClassFromFile and LoadClassesFromDirectory.  LoadMessage has two types of classes, the lead class of which there can only be one, and all the other classes.  This is just like back in Listing 9.7 where our multi-class, ClassLoader instantiated needed one “lead class” to start the whole process off, then it loaded all the other classes from the same directory.  Same theory here, only in the LoadMessage constructor we’re not trying to load the classes into a Class object - we’re trying to put them into a message to transmit over the net.  So the constructor calls LoadClassFromFile for the lead class, then LoadClassesFromDirectory for all the other classes.  This puts all the class data into byte array Vectors, which createMessage uses to fill up the actual net-transmittable message. 

The arguments are passed in as a Vector of byte arrays, so createMessage has a simple, if tedious job to do, running through the byte arrays containing the message data, attaching a field header to each, and copying the fields into the final message byte array.  And that takes care of LoadMessage creation.

On the other end, in order to start up the Agent, the AgentServer which receives the LoadMessage has to parse it.   If you look closely at LoadMessage.parse, you’ll see that it takes each field preceded by an ARG_PREFIX, and turns it into an array of bytes that it adds to the vargs Vector.  It takes the first CLAS field it finds and puts that into the leadclas byte array, then puts all the other class fields into the otherclasses byte array Vector.  When we have the lead class stored in the leadclas byte array, and all the other classes stored in the byte arrays of otherclasses, we have everything we need to successfully instantiate our new Agent, which we do at the bottom of parse. 

There is, however, one crucial difference between the classes we’ve pried out of the LoadMessage and the classes we load from files in Listing 9.7.  Remember that in order to use a class, all the references must be resolved.  This call to resolveClass will cause the interpreter to call ClassLoader.loadClass for each class that needs to be loaded.  In Listing 9.7, we know which file to load when the interpreter asks us to load a class (via ClassLoader.loadClass) because the file name is the class name.  In our Vector of class data files, we have no idea what the names of each of the classes is.  In fact, we must instantiate the classes in order to find out their names.  This leads to the unorthodox design of the AgentLoader, also shown in Listing 9.10.

LoadMessage.parse creates an instance of AgentLoader in order to instantiate the Agent.  The constructor to AgentLoader takes the Vector of “other” classes as its only argument.  It then runs through that array instantiating each class without resolving that class.  Thus, when the constructor is finished, it has a Vector of unresolved classes stored in the Hashtable cache.  Notice, we haven’t tried to instantiate the lead class yet.  When LoadMessage.parse invokes loadFromByteLump with the lead class as its argument it also tells loadFromByteLump to resolve the lead class. 

The call to resolveClass within loadFromByteLump causes a chain reaction of calls to loadClass.  The interpreter will call loadClass for each of the “other” classes with the resolve flag set to true.  Each of the other classes is already loaded, they, in turn just need to be resolved.  And so, by a most complicated route, our Agent is finally instantiated.

Listing 9.11 shows the section of code in the AgentServer where we turn a LoadMessage into a running Agent.

Listing 9.11: SocketHandler.ClientProcess from Listing 7.11.  Turning a LoadMessage into a running Agent.

/** parse and process a message from the client.  Follows the

basic system of: read 4 bytes and figure out the message type,

instantiate a message class appropriate to the type, then tell

that new instance to parse the lump.  Deal with the parsed

message by looking at public members of the new class and

acting appropriately.

@param b An array of bytes read over the socket.

@param numbytes The number of read-bytes in b.

 

*/

public boolean ClientProcess( byte b[], int numbytes ) {

 

String command;

int currentOffset = 0;

String s;

int messageStart;

boolean bret = false;

 

while( currentOffset != numbytes ) {

  messageStart = currentOffset;

  System.out.println( "currentOffset "+currentOffset+

      " numbytes "+numbytes );

  // Every message starts with 4 bytes of command

  command = new String( b, 0, currentOffset, 4 );

  currentOffset += LoadMessage.PREFIX_SIZE;

 

  // Followed by 10 bytes ascii length - length of the whole message

  // including command

  String sLength = new String( b, 0, currentOffset, 10 );

  currentOffset += LoadMessage.LOADLEN_SIZE;

  System.out.println( "got "+command+" of length "+sLength );

 

  if( command.compareTo( LoadMessage.LOAD_PREFIX ) == 0 ) {

    LoadMessage lm = new LoadMessage();

    Object o = lm.parse( b, currentOffset );

    if( o instanceof Agent ) {

      Agent a = (Agent)o;

      AgentServer.currentAgentServer.addRunningAgent(lm.sid);

      a.setAgentContext( new SepContext( lm.sid,

        lm.dispatching_server_name,

          lm.dispatching_server_port ));

      a.setArguments( lm.vargs );

      a.start();

      }

Notice that LoadMessage.parse does not start the Agent running in its own thread.  Why not?  Because the Agent hasn’t been configured yet.  Our instantiated FileFinder has no idea which files to find.  It needs that list of files that the user entered into the ConfigurationDialog and which the AgentServer who created the LoadMessage stuffed into the LoadMessage.  parse pulls these filenames out into a Vector of byte arrays, instantiates the FileFinder and returns the instantiated FileFinder to ClientProcess.  ClientProcess then gives the filenames to the new FileFinder via setArguments and starts the FileFinder running in its own thread via Thread.start.

Having finished our long digression into LoadMessages and SocketHandlers, we can now polish off the FileFinder class with a short discussion of its key method - run.  run contains the guts of the FileFinder, the part that does the actual work the user wants done out on the AgentServer.  When SocketHandler.ClientProcess calls Thread.start, our FileFinder.run method starts running in its own thread.  We want to start our file find at a particular sub-directory that the AgentServer can set, so we get the value for acl.read from the System properties file.  We report back to the AgentLauncher (via AgentContext.reportStart) that we’ve started work.  At that point, the AgentLauncher should start running the digging animation.  Then we create a File object from the starting directory, and run File.list against it using our AgainstArgs FilenameFilter to actually decide which files remain in the list.  The String array filelist that File.list returns contains only those files that match the users criteria.  If there are no files in the list, we report no-results back to the AgentLauncher.  If there are files in the list, we write each filename out to the results file, then report the results file URL back to the AgentLauncher via AgentContext.reportFinish.  And finally, we re-dispatch ourselves off to the rest of the network.

Conclusion

If you’ve made it this far without tearing your hair out, congratulate yourself.  Class loading is undoubtedly one of the most difficult topics in all of Java.  In this chapter we’ve seen how Java encapsulates runtime type information in the class Class.  We’ve also seen how Java provides an easy way to instantiate classes from arbitrary sources via the ClassLoader class.  Along the way we’ve implemented both file-based and network-based ClassLoaders and seen how the lack of file-name to classname linkage in the network based ClassLoader forced an unorthodox sequence of class instantiation and reference resolution.

In fact, we now have a functional Agent system.  Our Agents can be picked, configured and dispatched.  Once dispatched, they can traverses the net, instantiating on each AgentServer they encounter and redispatching to points further out on the net.  While running, they can report back whatever results they might have produced.

Exercises

SCOTT: ARE WE STILL DOING EXERCISES???  I NOTICED THEY DIDN’T MAKE IT TO GALLEYS.  LET ME KNOW AND I’LL DO EM UP FOR THIS.