ClasspathScanner.java
001 /*
002  *
003  * Licensed to the Apache Software Foundation (ASF) under one
004  * or more contributor license agreements.  See the NOTICE file
005  * distributed with this work for additional information
006  * regarding copyright ownership.  The ASF licenses this file
007  * to you under the Apache License, Version 2.0 (the
008  * "License"); you may not use this file except in compliance
009  * with the License.  You may obtain a copy of the License at
010  *
011  *   http://www.apache.org/licenses/LICENSE-2.0
012  *
013  * Unless required by applicable law or agreed to in writing,
014  * software distributed under the License is distributed on an
015  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
016  * KIND, either express or implied.  See the License for the
017  * specific language governing permissions and limitations
018  * under the License.
019  *
020  */
021 package org.apache.qpid.util;
022 
023 import java.io.File;
024 import java.util.*;
025 import java.util.regex.Matcher;
026 import java.util.regex.Pattern;
027 
028 import org.apache.log4j.Logger;
029 
030 /**
031  * An ClasspathScanner scans the classpath for classes that implement an interface or extend a base class and have names
032  * that match a regular expression.
033  *
034  <p/>In order to test whether a class implements an interface or extends a class, the class must be loaded (unless
035  * the class files were to be scanned directly). Using this collector can cause problems when it scans the classpath,
036  * because loading classes will initialize their statics, which in turn may cause undesired side effects. For this
037  * reason, the collector should always be used with a regular expression, through which the class file names are
038  * filtered, and only those that pass this filter will be tested. For example, if you define tests in classes that
039  * end with the keyword "Test" then use the regular expression "Test$" to match this.
040  *
041  <p><table id="crc"><caption>CRC Card</caption>
042  <tr><th> Responsibilities <th> Collaborations
043  <tr><td> Find all classes matching type and name pattern on the classpath.
044  </table>
045  *
046  * @todo Add logic to scan jars as well as directories.
047  */
048 public class ClasspathScanner
049 {
050     private static final Logger log = Logger.getLogger(ClasspathScanner.class);
051 
052     /**
053      * Scans the classpath and returns all classes that extend a specified class and match a specified name.
054      * There is an flag that can be used to indicate that only Java Beans will be matched (that is, only those classes
055      * that have a default constructor).
056      *
057      @param matchingClass  The class or interface to match.
058      @param matchingRegexp The regular expression to match against the class name.
059      @param beanOnly       Flag to indicate that onyl classes with default constructors should be matched.
060      *
061      @return All the classes that match this collector.
062      */
063     public static <T> Collection<Class<? extends T>> getMatches(Class<T> matchingClass, String matchingRegexp,
064         boolean beanOnly)
065     {
066         log.debug("public static <T> Collection<Class<? extends T>> getMatches(Class<T> matchingClass = " + matchingClass
067             ", String matchingRegexp = " + matchingRegexp + ", boolean beanOnly = " + beanOnly + "): called");
068 
069         // Build a compiled regular expression from the pattern to match.
070         Pattern matchPattern = Pattern.compile(matchingRegexp);
071 
072         String classPath = System.getProperty("java.class.path");
073         Map<String, Class<? extends T>> result = new HashMap<String, Class<? extends T>>();
074 
075         log.debug("classPath = " + classPath);
076 
077         // Find matching classes starting from all roots in the classpath.
078         for (String path : splitClassPath(classPath))
079         {
080             gatherFiles(new File(path)"", result, matchPattern, matchingClass);
081         }
082 
083         return result.values();
084     }
085 
086     /**
087      * Finds all matching classes rooted at a given location in the file system. If location is a directory it
088      * is recursively examined.
089      *
090      @param classRoot     The root of the current point in the file system being examined.
091      @param classFileName The name of the current file or directory to examine.
092      @param result        The accumulated mapping from class names to classes that match the scan.
093      *
094      * @todo Recursion ok as file system depth is not likely to exhaust the stack. Might be better to replace with
095      *       iteration.
096      */
097     private static <T> void gatherFiles(File classRoot, String classFileName, Map<String, Class<? extends T>> result,
098         Pattern matchPattern, Class<? extends T> matchClass)
099     {
100         log.debug("private static <T> void gatherFiles(File classRoot = " + classRoot + ", String classFileName = "
101             + classFileName + ", Map<String, Class<? extends T>> result, Pattern matchPattern = " + matchPattern
102             ", Class<? extends T> matchClass = " + matchClass + "): called");
103 
104         File thisRoot = new File(classRoot, classFileName);
105 
106         // If the current location is a file, check if it is a matching class.
107         if (thisRoot.isFile())
108         {
109             // Check that the file has a matching name.
110             if (matchesName(thisRoot.getName(), matchPattern))
111             {
112                 String className = classNameFromFile(thisRoot.getName());
113 
114                 // Check that the class has matching type.
115                 try
116                 {
117                     Class<?> candidateClass = Class.forName(className);
118 
119                     Class matchedClass = matchesClass(candidateClass, matchClass);
120 
121                     if (matchedClass != null)
122                     {
123                         result.put(className, matchedClass);
124                     }
125                 }
126                 catch (ClassNotFoundException e)
127                 {
128                     // Ignore this. The matching class could not be loaded.
129                     log.debug("Got ClassNotFoundException, ignoring.", e);
130                 }
131             }
132 
133             return;
134         }
135         // Otherwise the current location is a directory, so examine all of its contents.
136         else
137         {
138             String[] contents = thisRoot.list();
139 
140             if (contents != null)
141             {
142                 for (String content : contents)
143                 {
144                     gatherFiles(classRoot, classFileName + File.separatorChar + content, result, matchPattern, matchClass);
145                 }
146             }
147         }
148     }
149 
150     /**
151      * Checks if the specified class file name corresponds to a class with name matching the specified regular expression.
152      *
153      @param classFileName The class file name.
154      @param matchPattern  The regular expression pattern to match.
155      *
156      @return <tt>true</tt> if the class name matches, <tt>false</tt> otherwise.
157      */
158     private static boolean matchesName(String classFileName, Pattern matchPattern)
159     {
160         String className = classNameFromFile(classFileName);
161         Matcher matcher = matchPattern.matcher(className);
162 
163         return matcher.matches();
164     }
165 
166     /**
167      * Checks if the specified class to compare extends the base class being scanned for.
168      *
169      @param matchingClass The base class to match against.
170      @param toMatch       The class to match against the base class.
171      *
172      @return The class to check, cast as an instance of the class to match if the class extends the base class, or
173      *         <tt>null</tt> otherwise.
174      */
175     private static <T> Class<? extends T> matchesClass(Class<?> matchingClass, Class<? extends T> toMatch)
176     {
177         try
178         {
179             return matchingClass.asSubclass(toMatch);
180         }
181         catch (ClassCastException e)
182         {
183             return null;
184         }
185     }
186 
187     /**
188      * Takes a classpath (which is a series of paths) and splits it into its component paths.
189      *
190      @param classPath The classpath to split.
191      *
192      @return A list of the component paths that make up the class path.
193      */
194     private static List<String> splitClassPath(String classPath)
195     {
196         List<String> result = new LinkedList<String>();
197         String separator = System.getProperty("path.separator");
198         StringTokenizer tokenizer = new StringTokenizer(classPath, separator);
199 
200         while (tokenizer.hasMoreTokens())
201         {
202             result.add(tokenizer.nextToken());
203         }
204 
205         return result;
206     }
207 
208     /**
209      * Translates from the filename of a class to its fully qualified classname. Files are named using forward slash
210      * seperators and end in ".class", whereas fully qualified class names use "." sperators and no ".class" ending.
211      *
212      @param classFileName The filename of the class to translate to a class name.
213      *
214      @return The fully qualified class name.
215      */
216     private static String classNameFromFile(String classFileName)
217     {
218         log.debug("private static String classNameFromFile(String classFileName = " + classFileName + "): called");
219 
220         // Remove the .class ending.
221         String s = classFileName.substring(0, classFileName.length() ".class".length());
222 
223         // Turn / seperators in . seperators.
224         String s2 = s.replace(File.separatorChar, '.');
225 
226         // Knock off any leading . caused by a leading /.
227         if (s2.startsWith("."))
228         {
229             return s2.substring(1);
230         }
231 
232         return s2;
233     }
234 }