View Javadoc

1   //Copyright (C) 2004, Brian Enigma <enigma at netninja.com>
2   //This file is part of MagicCodes.
3   //
4   //MagicCodes is free software; you can redistribute it and/or modify
5   //it under the terms of the GNU General Public License as published by
6   //the Free Software Foundation; either version 2 of the License, or
7   //(at your option) any later version.
8   //
9   //MagicCodes is distributed in the hope that it will be useful,
10  //but WITHOUT ANY WARRANTY; without even the implied warranty of
11  //MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  //GNU General Public License for more details.
13  //
14  //You should have received a copy of the GNU General Public License
15  //along with Foobar; if not, write to the Free Software
16  //Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17  package org.ninjasoft.magiccodes.logic;
18  
19  import java.util.*;
20  import java.util.jar.*;
21  import java.io.*;
22  
23  /***
24   * This class will (slightly inefficiently) look for all classes that implement
25   * a particular interface.  It is useful for plugins.  This is done by looking
26   * through the contents of all jar files in the classpath, as well as performing
27   * a recursive search for *.class files in the classpath directories.
28   * 
29   * This particular class may not work in restrictive ClassLoader environments
30   * such as Applets or WebStart.  (It may...but unlikely and untested.) 
31   * @author enigma
32   */
33  public class PluginDiscoverer {
34      /*** Print debugging statements? */
35      private static final boolean DEBUG = false;
36      /*** List of the folders in the classpath */
37      private Vector classpathFolders = new Vector();
38      /*** List of the jars in the classpath */
39      private Vector classpathJars = new Vector();
40      
41      /***
42       * Construct the object and parse apart the classpath into directories
43       * and jars.  The constructor simple parses and does not search because
44       * the parsing is quick and easy.  The actual search work should NOT be
45       * done in the constructor, but in the findMatchingPlugins class.
46       */
47      public PluginDiscoverer() {
48          String classpath = System.getProperty("java.class.path");
49          StringTokenizer st = new StringTokenizer(classpath, File.pathSeparator);
50          while (st.hasMoreTokens()) {
51              String item = st.nextToken();
52              File f = new File(item);
53              if (item.toLowerCase().endsWith(".jar") && f.isFile()) {
54                  classpathJars.add(item);
55              } else if (f.isDirectory()) {
56                  classpathFolders.add(item);
57              }
58          }
59      }
60  
61      /***
62       * Given the class/interface we want to match and the name of a class
63       * (which may be a filename such as "org/ninjasoft/Blah.class" or an 
64       * actual class name such as "org.ninjasoft.Blah"), determine if the
65       * class implements the interface.  
66       * @param interfaceClass Class object representing the interface we are looking for
67       * @param testClass Name of class or class file we are testing
68       * @return corrected/normalized name of class if matches, otherwise null
69       */
70      private String checkIfClassMatches(Class interfaceClass, String testClass) {
71          // Drop any trailing .class, if this is a filename
72          if (testClass.toLowerCase().endsWith(".class"))
73              testClass = testClass.substring(0, testClass.length() - 6);
74          // Normalize slashes to dots, if this is a filename
75          testClass = testClass.replace('//', '.').replace('/', '.');
76          // Drop any leading dots (for instance, if this was a filename that started with a slash)
77          while (testClass.startsWith("."))
78              testClass = testClass.substring(1);
79          // If it is an internal class, forget about it
80          if (testClass.indexOf('$') != -1)
81              return null;
82          if (DEBUG)
83              System.out.println(" class:" + testClass);
84          try{
85              Class testClassObj = Class.forName(testClass);
86              if (interfaceClass.isAssignableFrom(testClassObj)) {
87                  if (DEBUG)
88                      System.out.println("MATCH!");
89                  return testClass;
90              }
91          }catch(ClassNotFoundException e) {
92              //e.printStackTrace();
93              System.out.println("Unable to load class " + testClass);
94          }
95          return null;
96      }
97      
98      /***
99       * Find all classes in the class path that implement the interface defined
100      * in the Class object parameter.
101      * @param c a Class object representing the interface we are looking for
102      * @return an array of all the class names that implement that interface
103      */
104     public String[] findMatchingPlugins(Class c) {
105         Vector results = new Vector();
106         // Check the jar files
107         for (Iterator i = classpathJars.iterator(); i.hasNext(); ) {
108             String filename = (String) i.next();
109             if (DEBUG)
110                 System.out.println("jar: " + filename);
111             try{
112                 JarFile jar = new JarFile(filename);
113                 for (Enumeration item = jar.entries(); item.hasMoreElements(); ) {
114                     JarEntry entry = (JarEntry) item.nextElement();
115                     String name = entry.getName();
116                     if (name.toLowerCase().endsWith(".class")) {
117                         String classname = checkIfClassMatches(c, name);
118                         if (classname != null)
119                         	results.add(classname);
120                     }
121                 }
122             }catch(IOException e){
123                 //e.printStackTrace();
124                 System.out.println("Unable to open jar " + filename);
125             }
126         }
127         // Check the classpath folders
128         for (Iterator i = classpathFolders.iterator(); i.hasNext(); ) {
129             String folder = (String) i.next();
130             if (DEBUG)
131                 System.out.println("classfolder:" + folder);
132             recursePath(c, results, folder, "");
133         }
134         // Flatten the output
135         String[] resultArray = new String[results.size()];
136         for (int i=0; i<results.size(); i++)
137             resultArray[i] = (String) results.get(i);
138         return resultArray;
139     }
140     
141     /***
142      * Recurse a path, looking for class files
143      * @param c the interface we are looking for
144      * @param results where to put matching classes
145      * @param base base directory
146      * @param path current location in the traversal
147      */
148     private void recursePath(Class c, Vector results, String base, String path) {
149         File f = new File(base + File.separator + path);
150         if (!f.isDirectory())
151             return;
152         File[] matches = f.listFiles(new classFilter());
153         for (int i=0; i<matches.length; i++) {
154             String classname = path + File.separator + matches[i].getName();
155             classname = checkIfClassMatches(c, classname);
156             if (classname != null)
157                 results.add(classname);
158         }
159         matches = f.listFiles(new directoryFilter());
160         for (int i=0; i<matches.length; i++) {
161             String folder = path + File.separator + matches[i].getName();
162             recursePath(c, results, base, folder);
163         }
164     }
165     
166     /***
167      * File.listFiles filter that only matches class files
168      */
169     public class classFilter implements FilenameFilter {
170         public boolean accept(File dir, String name) {
171             if (name.toLowerCase().endsWith(".class"))
172                 return true;
173             return false;
174         }
175     }
176     
177     /***
178      * File.listFiles filter that only matches subdirectories
179      */
180     public class directoryFilter implements FilenameFilter {
181         public boolean accept(File dir, String name) {
182             if (new File(dir.getPath() + File.separator + name).isDirectory())
183                 return true;
184             return false;
185         }
186     }
187     
188     /***
189      * Simple main function for testing
190      * @param argv unused
191      * @throws Exception if there was a problem
192      */
193     public static void main(String[] argv) throws Exception {
194         PluginDiscoverer pd = new PluginDiscoverer();
195         Class pluginInterface = Class.forName("org.ninjasoft.magiccodes.plugins.Plugin");
196         String[] plugins = pd.findMatchingPlugins(pluginInterface);
197         System.out.println();
198         System.out.println("" + plugins.length + " plugin" + (plugins.length == 1 ? "" : "s") + " discovered" + (plugins.length == 0 ? "" : ":"));
199         for (int i=0; i<plugins.length; i++)
200             System.out.println(plugins[i]);
201     }
202 }