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 }
|