1 /*
   2  * Copyright (c) 2003, 2004, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores,
  20  * CA 94065 USA or visit www.oracle.com if you need additional information or
  21  * have any questions.
  22  *
  23  */
  24 
  25 import java.io.*;
  26 import java.util.*;
  27 
  28 
  29 /**
  30 <p> This class finds transitive closure of dependencies from a given
  31 root set of classes. If your project has lots of .class files and you
  32 want to ship only those .class files which are used (transitively)
  33 from a root set of classes, then you can use this utility.  </p> <p>
  34 How does it work?</p>
  35 
  36 <p> We walk through all constant pool entries of a given class and
  37 find all modified UTF-8 entries. Anything that looks like a class name is
  38 considered as a class and we search for that class in the given
  39 classpath. If we find a .class of that name, then we add that class to
  40 list.</p>
  41 
  42 <p> We could have used CONSTANT_ClassInfo type constants only. But
  43 that will miss classes used through Class.forName or xyz.class
  44 construct.  But, if you refer to a class name in some other string we
  45 would include it as dependency :(. But this is quite unlikely
  46 anyway. To look for exact Class.forName argument(s) would involve
  47 bytecode analysis. Also, we handle only simple reflection. If you
  48 accept name of a class from externally (for eg properties file or
  49 command line args for example, this utility will not be able to find
  50 that dependency. In such cases, include those classes in the root set.
  51 </p>
  52 */
  53 
  54 public class ClosureFinder {
  55     private Collection roots;            // root class names Collection<String>
  56     private Map        visitedClasses;   // set of all dependencies as a Map
  57     private String     classPath;        // classpath to look for .class files
  58     private String[]   pathComponents;   // classpath components
  59     private static final boolean isWindows = File.separatorChar != '/';
  60 
  61     public ClosureFinder(Collection roots, String classPath) {
  62         this.roots = roots;
  63         this.classPath = classPath;
  64         parseClassPath();
  65     }
  66 
  67     // parse classPath into pathComponents array
  68     private void parseClassPath() {
  69         List paths = new ArrayList();
  70         StringTokenizer st = new StringTokenizer(classPath, File.pathSeparator);
  71         while (st.hasMoreTokens())
  72             paths.add(st.nextToken());
  73 
  74         Object[] arr = paths.toArray();
  75         pathComponents = new String[arr.length];
  76         System.arraycopy(arr, 0, pathComponents, 0, arr.length);
  77     }
  78 
  79     // if output is aleady not computed, compute it now
  80     // result is a map from class file name to base path where the .class was found
  81     public Map find() {
  82         if (visitedClasses == null) {
  83             visitedClasses = new HashMap();
  84             computeClosure();
  85         }
  86         return visitedClasses;
  87     }
  88 
  89     // compute closure for all given root classes
  90     private void computeClosure() {
  91         for (Iterator rootsItr = roots.iterator(); rootsItr.hasNext();) {
  92             String name = (String) rootsItr.next();
  93             name = name.substring(0, name.indexOf(".class"));
  94             computeClosure(name);
  95         }
  96     }
  97 
  98 
  99     // looks up for .class in pathComponents and returns
 100     // base path if found, else returns null
 101     private String lookupClassFile(String classNameAsPath) {
 102         for (int i = 0; i < pathComponents.length; i++) {
 103             File f =  new File(pathComponents[i] + File.separator +
 104                                classNameAsPath + ".class");
 105             if (f.exists()) {
 106                 if (isWindows) {
 107                     String name = f.getName();
 108                     // Windows reports special devices AUX,NUL,CON as files
 109                     // under any directory. It does not care about file extention :-(
 110                     if (name.compareToIgnoreCase("AUX.class") == 0 ||
 111                         name.compareToIgnoreCase("NUL.class") == 0 ||
 112                         name.compareToIgnoreCase("CON.class") == 0) {
 113                         return null;
 114                     }
 115                 }
 116                 return pathComponents[i];
 117             }
 118         }
 119         return null;
 120     }
 121 
 122 
 123     // from JVM spec. 2'nd edition section 4.4
 124     private static final int CONSTANT_Class = 7;
 125     private static final int CONSTANT_FieldRef = 9;
 126     private static final int CONSTANT_MethodRef = 10;
 127     private static final int CONSTANT_InterfaceMethodRef = 11;
 128     private static final int CONSTANT_String = 8;
 129     private static final int CONSTANT_Integer = 3;
 130     private static final int CONSTANT_Float = 4;
 131     private static final int CONSTANT_Long = 5;
 132     private static final int CONSTANT_Double = 6;
 133     private static final int CONSTANT_NameAndType = 12;
 134     private static final int CONSTANT_Utf8 = 1;
 135 
 136     // whether a given string may be a class name?
 137     private boolean mayBeClassName(String internalClassName) {
 138         int len = internalClassName.length();
 139         for (int s = 0; s < len; s++) {
 140             char c = internalClassName.charAt(s);
 141             if (!Character.isJavaIdentifierPart(c) && c != '/')
 142                 return false;
 143         }
 144         return true;
 145     }
 146 
 147     // compute closure for a given class
 148     private void computeClosure(String className) {
 149         if (visitedClasses.get(className) != null) return;
 150         String basePath = lookupClassFile(className);
 151         if (basePath != null) {
 152             visitedClasses.put(className, basePath);
 153             try {
 154                 File classFile = new File(basePath + File.separator + className + ".class");
 155                 FileInputStream fis = new FileInputStream(classFile);
 156                 DataInputStream dis = new DataInputStream(fis);
 157                 // look for .class signature
 158                 if (dis.readInt() != 0xcafebabe) {
 159                     System.err.println(classFile.getAbsolutePath() + " is not a valid .class file");
 160                     return;
 161                 }
 162 
 163                 // ignore major and minor version numbers
 164                 dis.readShort();
 165                 dis.readShort();
 166 
 167                 // read number of constant pool constants
 168                 int numConsts = (int) dis.readShort();
 169                 String[] strings = new String[numConsts];
 170 
 171                 // zero'th entry is unused
 172                 for (int cpIndex = 1; cpIndex < numConsts; cpIndex++) {
 173                     int constType = (int) dis.readByte();
 174                     switch (constType) {
 175                     case CONSTANT_Class:
 176                     case CONSTANT_String:
 177                         dis.readShort(); // string name index;
 178                         break;
 179 
 180                     case CONSTANT_FieldRef:
 181                     case CONSTANT_MethodRef:
 182                     case CONSTANT_InterfaceMethodRef:
 183                     case CONSTANT_NameAndType:
 184                     case CONSTANT_Integer:
 185                     case CONSTANT_Float:
 186                         // all these are 4 byte constants
 187                         dis.readInt();
 188                         break;
 189 
 190                     case CONSTANT_Long:
 191                     case CONSTANT_Double:
 192                         // 8 byte constants
 193                         dis.readLong();
 194                         // occupies 2 cp entries
 195                         cpIndex++;
 196                         break;
 197 
 198 
 199                     case CONSTANT_Utf8: {
 200                         strings[cpIndex] = dis.readUTF();
 201                         break;
 202                     }
 203 
 204                     default:
 205                         System.err.println("invalid constant pool entry");
 206                         return;
 207                     }
 208                 }
 209 
 210             // now walk thru the string constants and look for class names
 211             for (int s = 0; s < numConsts; s++) {
 212                 if (strings[s] != null && mayBeClassName(strings[s]))
 213                     computeClosure(strings[s].replace('/', File.separatorChar));
 214             }
 215 
 216             } catch (IOException exp) {
 217                 // ignore for now
 218             }
 219 
 220         }
 221     }
 222 
 223     // a sample main that accepts roots classes in a file and classpath as args
 224     public static void main(String[] args) {
 225         if (args.length != 2) {
 226             System.err.println("Usage: ClosureFinder <root class file> <class path>");
 227             System.exit(1);
 228         }
 229 
 230         List roots = new ArrayList();
 231         try {
 232             FileInputStream fis = new FileInputStream(args[0]);
 233             DataInputStream dis = new DataInputStream(fis);
 234             String line = null;
 235             while ((line = dis.readLine()) != null) {
 236                 if (isWindows) {
 237                     line = line.replace('/', File.separatorChar);
 238                 }
 239                 roots.add(line);
 240             }
 241         } catch (IOException exp) {
 242             System.err.println(exp.getMessage());
 243             System.exit(2);
 244         }
 245 
 246         ClosureFinder cf = new ClosureFinder(roots, args[1]);
 247         Map out = cf.find();
 248         Iterator res = out.keySet().iterator();
 249         for(; res.hasNext(); ) {
 250             String className = (String) res.next();
 251             System.out.println(className + ".class");
 252         }
 253     }
 254 }