// JarLoader.java
import java.io.*;
import java.util.*;
import java.util.jar.*;
// for main test only
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
/**
* A JarLoader object is able to load all the jar files in a given
* directory containing classes of a given type. It can also be set
* to continuously track that directory for new jar files that are
* added later.
* Listeners can be added to JarLoader objects which will be notified
* when the list of valid jar files in that directory changes.
*<br>
* Also contains a static method for dynamically loading a given jar file.
*
* @author Melinda Green - Superliminal Software http://www.superliminal.com
*/
public class JarLoader {
/**
* When added as JarLoader listeners, objects implementing this interface
* are informed whenever the contained list of loaded classes changes.
*/
public interface ListListener {
/**
* Notification method implemented by jarLoader list listeners.
* These implementations typically follow up with calls to
* <code>getLoadedClassNames()</code> and/or
* <code>getClass(String name)</code> to discover the changes.
*/
public void listChanged(JarLoader loader);
}
private Class target;
final private String jarClassKey;
private boolean watchForChanges;
private HashMap name2class = new HashMap();
private int last_jar_file_count = -1; //tracks when listeners need to be notified.
private File directory;
public File getDirectory() { return directory; }
/**
* Used to discover the names of all classes currently
* loaded by this JarLoader instance.
* @return an array of the names of all the
* classes currently loaded by this JarLoader.
* Note, this list is suitable for display in a JComboBox.
*/
public String[] getLoadedClassNames() {
Object keys[] = null;
keys = name2class.keySet().toArray(new String[0]);
return (String[])keys;
}
/**
* Retrives the Class object for a previously loaded class.
* @param name is the name of a previously loaded class
* presumably reported in a previous call to getLoadedClassNames.
* @return the class object corrisponding to the loaded
* class with the given name, or null if not found.
*/
public Class getClass(String name) {
return (Class)name2class.get(name);
}
/**
* Creates a JarLoader to which loads from and possibly tracks jar
* files of a given type in a directory.
* The loaded classes are available via <code>getClass(String name)</code>
* where "name" was presumably gotten from <code>getLoadedClassNames()</code>.
* @param jarClassKey is the key to look for in each jar's manifest file.
* JarLoader will load one class from each jar file who's manifest file
* contains a key/value attribute pair of the following form:
* <jarClassKey>: <Class name>
* <br>Example: <code>new JarLoader("My-Component", MyComponent.class, "plugins", false)</code><br>
* will load all classes of type MyComponent identified by the
* "My-Component" key in each jar file's manifest file for each jar file
* found in the "plugins" directory.
* @param containing is a base class or interface which loaded classes
* must inherit from or implement in order to be loaded.
* @param dir is the directory in which to look for jar files.
* @param watchDir is a flag stating whether this JarLoader should
* continuously monitor the given directory for changes in the list of
* loadable jar files found there.
*/
public JarLoader(String jarClassKey, Class containing, String dir, boolean watchDir) {
directory = new File(dir);
if(!directory.isDirectory())
throw new java.security.InvalidParameterException(directory + " not a directory");
this.jarClassKey = jarClassKey;
target = containing;
watchForChanges = watchDir;
load(); //always loads jar files in given directory at least once.
if(watchForChanges) {
new Thread() {
public void run() {
//System.out.println("Starting watcher");
while(true) {
try { Thread.sleep(1000); }
catch(InterruptedException ie) {}
load(); //reload jar files if directory changed
}
}
}.start();
}
}
/**
* The directory version of <code>load(File...)</code> which loads all conforming
* Classes found in the jar files of the directory with which this JarLoader
* instance was created.
*/
private void load() {
File jar_files[] = directory.listFiles(new FileFilter() {
public boolean accept(File pathname) {
return pathname.getName().endsWith(".jar");
}
});
if(jar_files.length == last_jar_file_count)
return; //nothing to do so go back to sleep
name2class.clear();
//System.out.println("found " + jar_files.length + " jar files");
last_jar_file_count = jar_files.length; // remember for next time
for(int i=0; i<jar_files.length; i++) {
Class loaded_class = load(jar_files[i], jarClassKey, name2class);
if(loaded_class!=null && target.isAssignableFrom(loaded_class)) {
name2class.put(loaded_class.getName(), loaded_class);
}
}
fireListChanged();
}
/**
* The workhorse of JarLoader which performs the actual dynamic loading
* of a single jar file.
* @param jarFile is a java File object representing the jar file to load.
* @param jarClassKey is the key to look for in each jar's manifest file.
* @param name2class is an optional HashMap which, if supplied, will return
* null if the class in the given jar file has the same name as a String
* key in the HashMap. In all cases the given HashMap is left unchanged.
* @return The Class loaded from the jar file or null on error.
*/
public static Class load(File jarFile, String jarClassKey, HashMap name2class) {
JarFile the_jar = null;
try {
final JarFile ajar = new JarFile(jarFile);
the_jar = ajar; // so it can be closed regardless of exceptions
Manifest manifest = ajar.getManifest();
Map map = manifest.getEntries();
Attributes att = manifest.getMainAttributes();
final String loaded_class_name = att.getValue(jarClassKey);
if(loaded_class_name == null) {
System.out.println("can't find class to load in manifest at the key: " +
jarClassKey);
return null;
}
if(name2class!=null && name2class.get(loaded_class_name) != null)
return (Class)name2class.get(loaded_class_name); // already have this one
//System.out.println("loading " + loaded_class_name);
Class loaded_class = new ClassLoader() {
public Class findClass(String name) {
JarEntry loaded_class_entry = ajar.getJarEntry(name);
if(loaded_class_entry == null)
return(null);
try {
InputStream is = ajar.getInputStream(loaded_class_entry);
int available = is.available();
byte data[] = new byte[available];
is.read(data);
return defineClass(name, data, 0, data.length);
}
catch(IOException ioe) {
System.out.println("Exception: " + ioe);
return(null);
}
}
}.loadClass(loaded_class_name);
return loaded_class;
}
catch(Exception e) {
System.out.println("Exception: " + e);
return null;
}
finally { //insures jar file is always closed regardless of exceptions
if(the_jar != null) {
try { the_jar.close(); }
catch(IOException ioe) {}
}
}
} // end load(File...)
//Listener handling methods
private Vector listListeners = new Vector();
public void removeListListener(JarLoader.ListListener ll) { listListeners.remove(ll); }
private void fireListChanged() {
for(Enumeration e=listListeners.elements(); e.hasMoreElements(); ) {
((ListListener)e.nextElement()).listChanged(this);
}
}
/**
* Adds a listener to be notified when conforming jar files are added
* or removed from the given directory.
* The given listeners are notified immediately in order to let them
* do any initializations involving the currently loaded classes.
*/
public void addListListener(JarLoader.ListListener ll) {
listListeners.add(ll);
ll.listChanged(this);
}
//EVERYTHING FROM HERE DOWN IN THIS FILE IS FOR TESTING ONLY
//
//INSTRUCTIONS:
//In order to run this test you will need to compile this file and then
//create two jar files for the two concrete "loadable" classes
//defined below. To do that, you can use Sun's "jar" tool to package
//each loadable class into a jar file. To make that simple, extract
//the following text into the named files but without indentation:
//
//LoadableEntry1.txt:
// Manifest-Version: 1.0
// Loadable-Class: JarLoader$TestLoadable1
//
//LoadableEntry1.txt:
// Manifest-Version: 1.0
// Loadable-Class: JarLoader$TestLoadable2
//
//Note the extra blank line at the end of each file (three lines total).
//Next, create a file named maketest.bat" (for Windows) containing
//the following command:
// c:/jdk1.3/bin/jar cfm test1.jar LoadableEntry1.txt *TestLoadable1.class
// c:/jdk1.3/bin/jar cfm test2.jar LoadableEntry2.txt *TestLoadable2.class
//With your correct path to the "jar" program.
//That batch file will create proper jar files which you can move in and
//out of the program directory while running the test program resulting
//in controll buttons being created and deleted in response.
//You can then rerun the batch file if you change the names of your
//loadable classes or make other changes that affect the jar files.
/**
* A loadable test base class.
* Jar files containing classes derived from this base class.
* will be loaded by the main method.
*/
private interface TestLoadableBase {
public void print();
}
/**
* An example loadable test class.
* NOTE: Since this one happens to be an inner class,
* it's important that it be static.
* Otherwise it would not be instantiatiable outside of a parent instance.
*/
private static class TestLoadable1 implements TestLoadableBase {
public TestLoadable1() {} //empty constructor required for class to be accessible
public void print() { System.out.println("print called on a TestLoadable1"); }
}
/**
* Another example loadable test class.
*/
private static class TestLoadable2 implements TestLoadableBase {
public TestLoadable2() {} //empty constructor required for class to be accessible
public void print() { System.out.println("print called on a TestLoadable2"); }
}
/**
* A test application for JarLoader which loads and monitors the jar
* files in the current directory. A GUI is presented with interactive
* objects dynamically loaded jar files. Adding and removing those jar files
* from the current directory while the program is running will create and
* delete those GUI representations.
* @param args unused.
*/
public static void main(String args[]) {
final int WIN_WIDTH = 500, WIN_HEIGHT = 300, BUTT_WIDTH = 200, BUTT_HEIGHT = 30;
final HashMap butt2position = new HashMap();
JarLoader the_loader = new JarLoader("Loadable-Class", TestLoadableBase.class, ".", true);
final JPanel class_list = new JPanel(null);
class_list.setSize(new Dimension(WIN_WIDTH, WIN_HEIGHT));
JarLoader.ListListener loader_listener = new ListListener() {
public void listChanged(final JarLoader loader) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
//System.out.println("List changed");
class_list.removeAll();
String available_classes[] = loader.getLoadedClassNames();
for(int i=0; i<available_classes.length; i++) {
//Note that all final variables in this block are
//local to the button being constructed.
final String class_name = available_classes[i];
final JButton my_butt = new JButton(class_name);
final Point bounds_start = new Point();
final Point drag_start = new Point();
Point pos = (Point)butt2position.get(class_name);
if(pos == null) {
pos = new Point(
(int)(Math.random() * (WIN_WIDTH - BUTT_WIDTH)),
(int)(Math.random() * (WIN_HEIGHT - BUTT_HEIGHT)));
butt2position.put(class_name, pos); // so visible don't move when others are added or removed
}
my_butt.setBounds(pos.x, pos.y, BUTT_WIDTH, BUTT_HEIGHT);
my_butt.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent ae) {
Class loadable_class = loader.getClass(class_name);
try {
TestLoadableBase loadable = (TestLoadableBase)loadable_class.newInstance();
loadable.print();
}
catch(Exception e) {System.out.println("Exception: " + e);}
}
});
my_butt.addMouseListener(new MouseAdapter() {
public void mousePressed(MouseEvent me) {
drag_start.x = me.getX();
drag_start.y = me.getY();
bounds_start.x = my_butt.getBounds().x;
bounds_start.y = my_butt.getBounds().y;
}
});
my_butt.addMouseMotionListener(new MouseMotionAdapter() {
public void mouseDragged(MouseEvent me) {
//NOTE: I can't figure out why the reported positions on
//the next line are wrong and get worse the more you drag!
//System.out.println("MouseEvent position = " + me.getPoint());
int xdif = me.getX() - drag_start.x;
int ydif = me.getY() - drag_start.y;
Point pos = (Point)butt2position.get(class_name);
pos.x = bounds_start.x + xdif;
pos.y = bounds_start.y + ydif;
my_butt.setBounds(pos.x, pos.y, BUTT_WIDTH, BUTT_HEIGHT);
}
});
class_list.add(my_butt);
}
class_list.repaint();
}
});
}
};
the_loader.addListListener(loader_listener);
JPanel content = new JPanel();
content.setLayout(new BorderLayout());
JTextArea info = new JTextArea(
"The following combo box contains a list of all currently loaded " +
"classes of type 'TestLoadableBase' from the jar files found in this directory. " +
"Adding and removing TestLoadableBase jar files will cause the contents " +
"of the combo box to change immediately to reflect the available " +
"jar files. \n\n" +
"Selecting a TestLoadable class in the combo box causes an instance " +
"of that class to be created and its 'print' method to be called.\n\n" +
"Drag the buttons around to adjust their positions."
);
info.setLineWrap(true);
info.setWrapStyleWord(true);
content.add("North", info);
content.add("Center", class_list);
JFrame frame = new JFrame("JarLoader Test");
frame.getContentPane().add(content);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(new Dimension(500, 500));
frame.show();
} //end main
}