1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 */
18 package org.apache.river.tool.classdepend;
19
20 import org.apache.river.tool.classdepend.ClassDependParameters.CDPBuilder;
21 import java.io.BufferedInputStream;
22 import java.io.File;
23 import java.io.IOException;
24 import java.io.InputStream;
25 import java.net.MalformedURLException;
26 import java.net.URL;
27 import java.net.URLClassLoader;
28 import java.util.Collection;
29 import java.util.HashMap;
30 import java.util.HashSet;
31 import java.util.Iterator;
32 import java.util.Map;
33 import java.util.Set;
34 import java.util.StringTokenizer;
35 import java.util.regex.Pattern;
36
37 /**
38 * Provides a utility for computing which classes are depended on by a set of
39 * classes. This class is not thread safe.
40 */
41 public class ClassDepend {
42
43 /** The system classpath. */
44 private static final String systemClasspath =
45 System.getProperty("java.class.path");
46
47 /**
48 * The class loader used to load classes being checked for dependencies.
49 */
50 private final ClassLoader loader;
51
52 /**
53 * The class loader for classes that should be excluded because they are
54 * considered part of the platform.
55 */
56 private final ClassLoader platformLoader;
57
58 /**
59 * Used to compute the classes available in the classpath for a package.
60 */
61 private final PackageClasses packageClasses;
62
63 private volatile boolean printClassesWithFileSeparator = false;
64
65 /**
66 * Public Factory method for creating a new instance of ClassDepend.
67 *
68 * The <code>classpath</code> argument
69 * specifies the classpath that will be used to look up the class bytecode
70 * for classes whose dependencies are being computed. If the value
71 * specified is <code>null</code>, then the system class loader will be
72 * used. Otherwise, a {@link URLClassLoader} will be constructed using the
73 * URLs specified by <code>classpath</code> and with a parent class loader
74 * that is the parent of the system class loader. The
75 * <code>platform</code> argument specifies the classpath that will be used
76 * to find classes that should be considered part of the platform and
77 * should be excluded from consideration when computing dependencies. If
78 * the value specified is <code>null</code>, then the parent of the system
79 * class loader will be used. Otherwise, a <code>URLClassLoader</code>
80 * will be constructed using the URLs specified by <code>platform</code>
81 * and with a parent class loader that is the parent of the system class
82 * loader.
83 *
84 * @param classpath the classpath for finding classes, or
85 * <code>null</code>
86 * @param platform the classpath for finding platform classes, or
87 * <code>null</code>
88 * @param warn print warnings instead of throwing an exception when a Class
89 * can't be found or when ClassLoading fails.
90 * @return ClassDepend
91 * @throws java.net.MalformedURLException
92 * @throws java.io.IOException
93 */
94 public static ClassDepend newInstance(String classpath, String platform, boolean warn)
95 throws MalformedURLException, IOException{
96 /* Explanation for us mere mortals.
97 * Ternary conditional operator:
98 * Object ob = expression ? this if true : else this;
99 * ClassDepend classdepend = if warn not true, then new ClassDepend(), else
100 * new anonymous class that extends ClassDepend
101 * with noteClassNotFound and
102 * noteClassLoadingFailed method overrides.
103 *
104 * This prevents exceptions from being thrown and prints warnings
105 * instead on the System err
106 *
107 * Using a Factory method to return a new instance allows
108 * us to return different versions of ClassDepend, as we have
109 * here by overriding the default methods for debugging.
110 */
111 ClassDepend classDepend = !warn
112 ? new ClassDepend(classpath, platform)
113 : new ClassDepend(classpath, platform) {
114 protected void noteClassNotFound(String name) {
115 System.err.println("Warning: Class not found: " + name);
116 }
117 protected void noteClassLoadingFailed(String name, IOException e) {
118 System.err.println("Warning: Problem loading class "
119 + name + ": " + e.getMessage());
120 }
121 };
122 return classDepend;
123 }
124
125 public static void main(String[] args) {
126 try {
127 CDPBuilder cdpb = new CDPBuilder();
128 String classpath = null;
129 String platform = null;
130 Set rootClasses = new HashSet();
131 boolean recurse = true;
132 boolean warn = false; //supress exceptions, print to error, warn instead
133 boolean files = false; //print class with file path separator
134 boolean graph = false; //print dependency relation ships between classes.
135 for (int i = 0; i < args.length; i++) {
136 String arg = args[i];
137 if (arg.equals("-cp")) {
138 classpath = args[++i];
139 } else if (arg.equals("-platform")) {
140 platform = args[++i];
141 } else if (arg.equals("-exclude")) {
142 cdpb.addOutsidePackageOrClass(args[++i]);
143 } else if (arg.equals("-norecurse")) {
144 recurse = false;
145 } else if (arg.equals("-warn")) {
146 warn = true;
147 } else if (arg.equals("-files")) {
148 files = true;
149 } else if (arg.equals("-graph")) {
150 graph = true;
151 } else if (arg.equals("-excljava")) {
152 cdpb.excludePlatformClasses(true);
153 } else if (arg.startsWith("-")) {
154 throw new IllegalArgumentException("Bad option: " + arg);
155 } else {
156 rootClasses.add(arg);
157 }
158 }
159 ClassDependParameters cdp = cdpb.build();
160 ClassDepend classDepend = ClassDepend.newInstance(classpath, platform, warn);
161 Set result = classDepend
162 .filterClassDependencyRelationShipMap(
163 classDepend.getDependencyRelationshipMap(rootClasses, recurse),
164 cdp);
165 Iterator results = result.iterator();
166 while (results.hasNext()){
167 Object rezult = results.next();
168 if ( !(rezult instanceof ClassDependencyRelationship )) continue;
169 ClassDependencyRelationship cl = (ClassDependencyRelationship) rezult;
170 String str = cl.toString();
171 if (files) {
172 str = str.replace('.', File.separatorChar).concat(".class");
173 System.out.println(str);
174 }
175 if (graph) {
176 Set deps = cl.getProviders();
177 Iterator itr = deps.iterator();
178 while (itr.hasNext()){
179 Object dep = itr.next();
180 if ( result.contains(dep)) {
181 System.out.println("\"" + cl + "\""+ " -> " +
182 "\"" + dep + "\"" + ";");
183 }
184 }
185 }
186 }
187 } catch (Throwable e) {
188 e.printStackTrace();
189 System.exit(1);
190 }
191 }
192
193 /**
194 * Creates an instance of this class. The <code>classpath</code> argument
195 * specifies the classpath that will be used to look up the class bytecode
196 * for classes whose dependencies are being computed. If the value
197 * specified is <code>null</code>, then the system class loader will be
198 * used. Otherwise, a {@link URLClassLoader} will be constructed using the
199 * URLs specified by <code>classpath</code> and with a parent class loader
200 * that is the parent of the system class loader. The
201 * <code>platform</code> argument specifies the classpath that will be used
202 * to find classes that should be considered part of the platform and
203 * should be excluded from consideration when computing dependencies. If
204 * the value specified is <code>null</code>, then the parent of the system
205 * class loader will be used. Otherwise, a <code>URLClassLoader</code>
206 * will be constructed using the URLs specified by <code>platform</code>
207 * and with a parent class loader that is the parent of the system class
208 * loader.
209 *
210 * @param classpath the classpath for finding classes, or
211 * <code>null</code>
212 * @param platform the classpath for finding platform classes, or
213 * <code>null</code>
214 * @throws MalformedURLException if the URLs specified in the arguments
215 * are malformed
216 * @throws IOException if an I/O error occurs while accessing files in the
217 * classpath
218 */
219 ClassDepend(String classpath, String platform)
220 throws MalformedURLException, IOException {
221 if (classpath == null) {
222 classpath = systemClasspath;
223 }
224 ClassLoader system = ClassLoader.getSystemClassLoader();
225 ClassLoader parent = system.getParent();
226 loader = (systemClasspath.equals(classpath))
227 ? system
228 : new URLClassLoader(getClasspathURLs(classpath), parent);
229 packageClasses = new PackageClasses(classpath);
230 platformLoader = (platform == null)
231 ? parent
232 : new URLClassLoader(getClasspathURLs(platform), parent);
233 //System.out.println(platformLoader.toString());
234 }
235
236 /**
237 * This method builds the entire DependencyRelationShipMap that can be
238 * used to analyse class dependencies.
239 *
240 * Computes information about class dependencies. The computation of
241 * dependencies starts with the classes that match the names in
242 * <code>rootClasses</code>. Classes are found in a URL class loader by the
243 * <code>classpath</code> specified in the constructor.
244 *
245 * @param rootClasses
246 * @param recurse If true, this option causes ClassDepend to inspect class
247 * files for dependencies.
248 * If false, ClassDepend doesn't inspect class files, it simply
249 * gathers the names of class files from within Package directories and
250 * JAR files.
251 * @return result
252 * @throws java.io.IOException
253 * @throws java.lang.ClassNotFoundException
254 * @see ClassDependencyRelationship
255 */
256 public Map getDependencyRelationshipMap(Collection rootClasses, boolean recurse)
257 throws IOException, ClassNotFoundException {
258 Map result = new HashMap(); // May be changed to ConcurrentHashMap for Java 5
259 Set seen = new HashSet();
260 Set compute = computeClasses(rootClasses);
261 while (!compute.isEmpty()) {
262 Set computeNext = new HashSet(); //built from discovered dependencies
263 Iterator computeIterator = compute.iterator();
264 while (computeIterator.hasNext()) {
265 String name = (String) computeIterator.next();
266 if ( !seen.contains(name)){
267 seen.add(name);
268 if (rootClasses.contains(name)){
269 // Put all root classes into ClassDependencyRelationship containers
270 ClassDependencyRelationship rootClass = new ClassDependencyRelationship(name, true);
271 result.put(name, rootClass);
272 }
273 Set providerClassNames = new HashSet();
274 String resource = getResourceName(name);
275 if (recurse) {
276 InputStream in = loader.getResourceAsStream(resource);
277 if (in == null) {
278 noteClassNotFound(name);
279 } else {
280 try {
281 // Discover the referenced classes by loading classfile and inspecting
282 providerClassNames = ReferencedClasses.compute(
283 new BufferedInputStream(in));
284 computeNext.addAll(providerClassNames);
285 } catch (IOException e) {
286 noteClassLoadingFailed(name, e);
287 } finally {
288 try {
289 in.close();
290 } catch (IOException e) {
291 }
292 }
293 }
294 } else if (loader.getResource(resource) == null) {
295 noteClassNotFound(name);
296 }
297 /* Now we add all the provider classes to the dependant
298 * this is useful for edges or classes of interest where
299 * we my want to pick points to recurse through dependents
300 * instead of providers.
301 */
302 Iterator iter = providerClassNames.iterator();
303 while (iter.hasNext()){
304 String provider = (String) iter.next();
305 ClassDependencyRelationship providerClass;
306 if (!result.containsKey(provider)){
307 providerClass = new ClassDependencyRelationship(provider);
308 result.put(provider, providerClass);
309 }else{
310 providerClass = (ClassDependencyRelationship) result.get(provider);
311 }
312 ((ClassDependencyRelationship) result.get(name)).addProvider(providerClass);
313 }
314 }
315 }
316 /* The old list is exhausted, lets iterate through our newly
317 * discovered collection.
318 */
319 compute = computeNext;
320 }
321 return result;
322 }
323
324 /**
325 * This method applies optional filters to provide methods to support the
326 * original API of ClassDep.
327 * @param dependencyRelationShipMap The initial map before filtering.
328 * @param cdp The parameters for filtration.
329 * @see ClassDependParameters
330 * @see ClassDependencyRelationship
331 * @return Set<ClassDependencyRelationShip> result The result after filtration.
332 */
333 public Set filterClassDependencyRelationShipMap(Map dependencyRelationShipMap, ClassDependParameters cdp){
334 Set result = new HashSet(); // final result
335 Set preliminaryResult = new HashSet();
336
337 Pattern excludePattern = createPattern(cdp.outsidePackagesOrClasses());
338 Pattern includePattern = createPattern(cdp.insidePackages());
339 Pattern hidePattern = createPattern(cdp.hidePackages());
340 Pattern showPattern = createPattern(cdp.showPackages());
341 Collection classRelations = dependencyRelationShipMap.values();
342 // get the root class set.
343 Set rootClasses = new HashSet();
344 {
345 Iterator itr = classRelations.iterator();
346 while (itr.hasNext()){
347 ClassDependencyRelationship cdr = (ClassDependencyRelationship) itr.next();
348 if (cdr.isRootClass()){
349 rootClasses.add(cdr);
350 }
351 }
352 }
353 // traverse the tree from root dependant to providers
354 while ( !rootClasses.isEmpty() ){
355 Set computeNext = new HashSet();
356 Iterator computeIterator = rootClasses.iterator();
357 while (computeIterator.hasNext()){
358 ClassDependencyRelationship cdr = (ClassDependencyRelationship) computeIterator.next();
359 String name = cdr.toString();
360 // filter out the classes as requested
361 if ( !preliminaryResult.contains(cdr) &&
362 ( !cdp.excludePlatformClasses() || !classPresent(name, platformLoader) ) &&
363 !matches(name, excludePattern) &&
364 ( cdp.insidePackages().size() == 0 || matches(name, includePattern)))
365 {
366 // TODO remove the outer parent class if requested
367 preliminaryResult.add(cdr);
368 computeNext.addAll(cdr.getProviders());
369 }
370 }
371 rootClasses = computeNext;
372 }
373 // populate the result with the edge classes if requested
374 if (cdp.edges()){
375 Iterator itr = preliminaryResult.iterator();
376 while (itr.hasNext()) {
377 ClassDependencyRelationship cdr = (ClassDependencyRelationship) itr.next();
378 result.addAll(cdr.getProviders());
379 }
380 /* edge classes aren't in the filtered preliminary result set ;),
381 * so remove it just in case some classes from the preliminary
382 * result set snuck back in via the provider Sets;
383 */
384 result.removeAll(preliminaryResult);
385 }else{
386 result = preliminaryResult;
387 }
388
389 /* If we have shows or hides, we want to remove these from
390 * the result Collection, under certain conditions.
391 */
392 Set remove = new HashSet();
393 Iterator itr = result.iterator();
394 while (itr.hasNext()){
395 ClassDependencyRelationship cdr = (ClassDependencyRelationship) itr.next();
396 String name = cdr.toString();
397 if(( matches(name,hidePattern)|| ( showPattern != null && !matches(name, showPattern))))
398 {
399 remove.add(cdr);
400 }
401 }
402 result.removeAll(remove);
403 return result;
404 }
405
406 /**
407 * Called when the specified class is not found. <p>
408 *
409 * This implementation throws a <code>ClassNotFoundException</code>.
410 *
411 * @param name the class name
412 * @throws ClassNotFoundException to signal that processing should
413 * terminate and the exception should be thrown to the caller
414 */
415 protected void noteClassNotFound(String name)
416 throws ClassNotFoundException
417 {
418 throw new ClassNotFoundException("Class not found: " + name);
419 }
420
421 /**
422 * Called when attempts to load the bytecodes for the specified class fail.
423 *
424 * @param name the class name
425 * @param e the exception caused by the failure
426 * @throws IOException to signal that processing should terminate and the
427 * exception should be thrown to the caller
428 */
429 protected void noteClassLoadingFailed(String name, IOException e)
430 throws IOException
431 {
432 throw e;
433 }
434
435 /**
436 * Returns the classes in the classpath that match the specified names by
437 * expanding package wildcards.
438 */
439 private Set computeClasses(Collection names)
440 throws IOException
441 {
442 Set result = new HashSet();
443 Iterator namesIterator = names.iterator();
444 while (namesIterator.hasNext()) {
445 String name = (String) namesIterator.next();
446 if (name.endsWith(".*")) {
447 name = name.substring(0, name.length() - 2);
448 result.addAll(packageClasses.compute(false, name));
449 } else if (name.endsWith(".**")) {
450 name = name.substring(0, name.length() - 3);
451 result.addAll(packageClasses.compute(true, name));
452 } else {
453 result.add(name);
454 }
455 }
456 return result;
457 }
458
459 /** Returns the URLs associated with a classpath. */
460 private URL[] getClasspathURLs(String classpath)
461 throws MalformedURLException
462 {
463 StringTokenizer tokens =
464 new StringTokenizer(classpath, File.pathSeparator);
465 URL[] urls = new URL[tokens.countTokens()];
466 for (int i = 0; tokens.hasMoreTokens(); i++) {
467 String file = tokens.nextToken();
468 try {
469 urls[i] = new File(file).toURI().toURL();
470 } catch (MalformedURLException e) {
471 urls[i] = new URL(file);
472 }
473 }
474 return urls;
475 }
476
477 /** Checks if the class is present in the given loader. */
478 private boolean classPresent(String name, ClassLoader loader) {
479 return loader.getResource(getResourceName(name)) != null;
480 }
481
482 /** Returns the name of the resource associated with a class name. */
483 private String getResourceName(String classname) {
484 return classname.replace('.', '/').concat(".class");
485 }
486
487 /**
488 * Creates a pattern that matches class names for any of the names in the
489 * argument. Returns null if the argument is empty. xNames that end in
490 * '.*' match classes in the package, names that end in '.**' match classes
491 * in the package and it's subpackage. Other names match the class.
492 * @param names
493 * @return Pattern
494 */
495 public Pattern createPattern(Collection names) {
496 if (names.isEmpty()) {
497 return null;
498 }
499 StringBuffer sb = new StringBuffer();
500 boolean first = true;
501 Iterator namesItr = names.iterator();
502 while (namesItr.hasNext()) {
503 String name = (String) namesItr.next();
504 if (!first) {
505 sb.append('|');
506 } else {
507 first = false;
508 }
509 if (name.endsWith(".*")) {
510 sb.append(
511 quote( name.substring(0, name.length() - 1)) +
512 "[^.]+");
513 } else if (name.endsWith(".**")) {
514 sb.append(
515 quote(name.substring(0, name.length() - 2)) +
516 ".+");
517 } else {
518 sb.append(quote(name));
519 }
520 }
521 return Pattern.compile(sb.toString());
522 }
523
524 /**
525 * Checks if the string matches the pattern, returning false if the pattern
526 * is null.
527 * @param string
528 * @param pattern
529 * @return True if the Pattern Matches
530 */
531 public boolean matches(String string, Pattern pattern) {
532 return pattern != null && pattern.matcher(string).matches();
533 }
534
535 /**
536 * Returns a literal pattern String for the specified String.
537 * Added to backport Java 1.5 sources to 1.4. adds the functionality
538 * of java.util.regex.Pattern.quote() method missing from Java 1.4 version
539 *
540 * This method produces a String that can be used to create a
541 * Pattern that would match the string s as if it were a literal pattern.
542 * Metacharacters or escape sequences in the input sequence
543 * will be given no special meaning.
544 * @param s - The String to be literalised
545 * @return A literal string replacement
546 */
547
548 private String quote(String s) {
549 StringBuffer sb = new StringBuffer(s.length() * 2).append("\\Q");
550 int previousEndQuotationIndex = 0;
551 int endQuotationIndex = 0;
552 while ((endQuotationIndex = s.indexOf("\\E", previousEndQuotationIndex)) >= 0) {
553 sb.append(s.substring(previousEndQuotationIndex, endQuotationIndex));
554 sb.append("\\E\\\\E\\Q");
555 previousEndQuotationIndex = endQuotationIndex + 2;
556 }
557 sb.append(s.substring(previousEndQuotationIndex));
558 sb.append("\\E");
559 String literalPattern = sb.toString();
560 return literalPattern;
561 }
562
563 public boolean printClassesWithFileSeparator() {
564 return printClassesWithFileSeparator;
565 }
566
567 public void setPrintClassesWithFileSeparator(boolean printClassesWithFileSeparator) {
568 this.printClassesWithFileSeparator = printClassesWithFileSeparator;
569 }
570 }