View Javadoc
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&lt;ClassDependencyRelationShip&gt; 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 }