package ring.commands;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
/**
* Package-level indexer class that indexes Command objects from a given Java package.
* Much of class's code was implemented by studying and/or modifying parts of the code from
* the ClassList.java file by Kris Dover (krisdover@hotmail.com), a class for obtaining a
* list of classes from the classpath. ClassList.java can be downloaded at
* http://www.xmlizer.biz/java/classloader/ClassList.java
* @author projectmoon
*
*/
class PackageIndexer implements CommandIndexer {
private ArrayList<Command> commandList = new ArrayList<Command>();
private boolean indexed = false;
private Properties props;
private Set<String> indexedPackages = new HashSet<String>();
/**
* Creates new CommandIndexer without a package specified.
*/
public PackageIndexer() {
}
/**
* Indexes commands from the packages specified in the properties.
*/
public void index() throws IllegalStateException {
if (props == null) {
throw new IllegalStateException("PackageIndexer: No properties! Can't index without them!");
}
//Set up the packages we are going to index first.
String pkgList = props.getProperty("pkgIndexer.packages");
if (pkgList == null) {
System.err.println(this + ": Couldn't find pkgIndexer.packages property. Stopping.");
return;
}
String[] packages = pkgList.split(";");
for (String pkg : packages) {
indexedPackages.add(pkg);
System.out.println(" [PackageIndexer]: Indexing commands from " + pkg);
}
//Now onward with processing the classpath.
Object[] classPath = getClassPathEntries();
for (Object o : classPath) {
String classPathEntry = null;
if (o instanceof URL) {
URL url = (URL)o;
classPathEntry = url.getFile();
}
else if (o instanceof String) {
classPathEntry = (String)o;
}
assert(classPathEntry != null);
List<String> classFiles = getEntryFiles(classPathEntry);
//classFiles should always exist, even if it is just an empty list.
assert (classFiles != null);
processClassFiles(classFiles);
}
indexed = true;
}
/**
* Helper method to process found files from the classpath.
* @param classFiles An enumeration containing either Strings or JarEntry objects.
*/
private void processClassFiles(List<String> classFiles) {
if (classFiles == null) {
throw new IllegalArgumentException("Class file list cannot be null!");
}
for (String filename : classFiles) {
String className = filename.replace('/', '.').substring(0, filename.length() - 6);
String classPackage = className.substring(0, className.lastIndexOf("."));
if (indexedPackages.contains(classPackage)) {
attemptInstantiateCommand(className);
}
}
}
/**
* Returns the class path entries for the PackageIndexer. It tries to cast the
* class loader to a URLClassLoader. If that fails, it will tokenize the system
* classpath.
* @return An array of classpath entries as either URLs or Strings.
*/
private Object[] getClassPathEntries() {
try {
URLClassLoader urlLoader = (URLClassLoader)PackageIndexer.class.getClassLoader();
return urlLoader.getURLs();
}
catch (ClassCastException e) {
return System.getProperty("java.class.path", "").split(File.pathSeparator);
}
}
/**
* Gets a list of String filenames representing class files from the given classpath entry.
* This method will work on classpath entries that are jar files or simple directory structures.
* The filenames returned are guaranteed to end in a .class extension, but are not guaranteed to
* be actual Java class files.
* @param classPathEntry
* @return A list of class files from the given entry.
*/
private List<String> getEntryFiles(String classPathEntry) {
//Get a list of filenames from a jar file.
if (classPathEntry.endsWith(".jar")) {
return getEntryFilesFromJar(classPathEntry);
}
//Or, get a list of filenames from a starting directory.
else {
return getEntryFilesFromDirectory(classPathEntry);
}
}
/**
* Gets a list of class file entries from a jar file.
* @param classPathEntry
* @return
*/
private List<String> getEntryFilesFromJar(String classPathEntry) {
try {
JarFile jar = new JarFile(classPathEntry);
Enumeration<JarEntry> entries = jar.entries();
ArrayList<String> entryList = new ArrayList<String>(jar.size());
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
if (entry.getName().endsWith(".class"))
entryList.add(entry.getName());
}
entryList.trimToSize();
return entryList;
}
catch (IOException e) {
e.printStackTrace();
return new ArrayList<String>(0);
}
}
/**
* Gets a list of file entries from a starting directory.
* @param classPathEntry
* @return
*/
private List<String> getEntryFilesFromDirectory(String classPathEntry) {
File f = new File(classPathEntry);
List<File> files = getFilesRecursively(f);
List<String> filenames = new ArrayList<String>(files.size());
//This loop removes the first part of the absolute path corresponding to
//The class path entry. This allows us to find the actual class fully-qualified
//class names (FQCNs)
for (File file : files) {
filenames.add(file.getAbsolutePath().substring(classPathEntry.length()));
}
return filenames;
}
/**
* Gets a list of File objects ending with .class recursively, from a given starting
* File.
* @param start
* @return The list of all .class files found.
*/
private List<File> getFilesRecursively(File start) {
ArrayList<File> files = new ArrayList<File>();
if (start.isDirectory()) {
File[] dirFiles = start.listFiles();
for (File file : dirFiles) {
if (file.isDirectory()) {
files.addAll(getFilesRecursively(file));
}
else {
if (file.getAbsolutePath().endsWith(".class"))
files.add(file);
}
}
}
else {
if (start.getAbsolutePath().endsWith(".class"))
files.add(start);
}
return files;
}
/**
* Attempts to instantiate a Command object from the given class name String.
* To be instantiated, the object must implement the Command interface.
* @param className
*/
private void attemptInstantiateCommand(String className) {
Class<?> cmdInterface = Command.class;
try {
Class<?> c = Class.forName(className);
//If the sub-class c is assignable from the Command
//interface, we can add it to the map.
if (cmdInterface.isAssignableFrom(c) && !c.isInterface()) {
Command cmd = (Command)c.newInstance();
commandList.add(cmd);
}
}
catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
catch (InstantiationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
catch (IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/**
* Returns the list of commands. If the index() method has not been
* called, this method will automatically call it.
* @return The list of indexed package commands.
*/
public List<Command> getCommands() throws IllegalStateException {
if (props == null) {
throw new IllegalStateException("PackageIndexer: No properties! Can't get commands without them!");
}
if (!indexed) {
index();
}
return commandList;
}
/**
* Gets this plugin's properties.
*/
public Properties getProperties() {
return props;
}
/**
* Sets this plugin's properties.
*/
public void setProperties(Properties props) {
this.props = props;
}
/**
* Returns "[PackageIndexer]"
*/
public String toString() {
return "[PackageIndexer]";
}
}