1 /* 2 * Copyright (c) 2009, 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, CA 94065 USA 20 * or visit www.oracle.com if you need additional information or have any 21 * questions. 22 */ 23 package com.sun.classanalyzer; 24 25 import com.sun.classanalyzer.AnnotatedDependency.*; 26 import com.sun.classanalyzer.Module.*; 27 import com.sun.classanalyzer.ModuleInfo.*; 28 import java.io.IOException; 29 import java.io.File; 30 import java.io.PrintWriter; 31 import java.util.*; 32 import java.util.jar.JarEntry; 33 import java.util.jar.JarFile; 34 35 /** 36 * Analyze the class dependencies of all classes in a given classpath 37 * and assign classes and resource files into modules as defined 38 * in the input configuration files. 39 * 40 * The ClassAnalyzer tool will generate the following reports 41 * modules.list 42 * modules.summary 43 * modules.dot 44 * and for each module named <m>, 45 * <m>.classlist 46 * <m>.resources 47 * <m>.summary 48 * <m>.dependencies 49 * 50 * If -moduleinfo option is specified, <m>/module-info.java 51 * will be created under the given directory. 52 * 53 * The -update option can be specified to perform an incremental analysis 54 * rather than parsing all class files. 55 * 56 * @author Mandy Chung 57 */ 58 public class ClassAnalyzer { 59 60 public static void main(String[] args) throws Exception { 61 String jdkhome = null; 62 String cparg = null; 63 List<String> configs = new ArrayList<String>(); 64 List<String> depconfigs = new ArrayList<String>(); 65 String version = null; 66 String classlistDir = "."; 67 String minfoDir = null; 68 String nonCorePkgsFile = null; 69 ClassPaths cpaths = null; 70 boolean mergeModules = true; 71 boolean apiOnly = false; 72 boolean showDynamic = false; 73 boolean update = false; 74 75 // process arguments 76 int i = 0; 77 while (i < args.length) { 78 String arg = args[i++]; 79 if (arg.equals("-jdkhome")) { 80 if (cparg != null) { 81 error("Both -jdkhome and -classpath are set"); 82 } 83 jdkhome = getOption(args, i++); 84 cpaths = ClassPaths.newJDKClassPaths(jdkhome); 85 } else if (arg.equals("-classpath")) { 86 if (jdkhome != null) { 87 error("Both -jdkhome and -classpath are set"); 88 } 89 cparg = getOption(args, i++); 90 cpaths = ClassPaths.newInstance(cparg); 91 } else if (arg.equals("-config")) { 92 configs.add(getOption(args, i++)); 93 } else if (arg.equals("-depconfig")) { 94 depconfigs.add(getOption(args, i++)); 95 } else if (arg.equals("-properties")) { 96 Module.setModuleProperties(getOption(args, i++)); 97 } else if (arg.equals("-output")) { 98 classlistDir = getOption(args, i++); 99 } else if (arg.equals("-update")) { 100 update = true; 101 } else if (arg.equals("-moduleinfo")) { 102 minfoDir = getOption(args, i++); 103 } else if (arg.equals("-version")) { 104 version = getOption(args, i++); 105 } else if (arg.equals("-nomerge")) { 106 // analyze the fine-grained module dependencies 107 mergeModules = false; 108 } else if (arg.equals("-api")) { 109 // analyze the fine-grained module dependencies 110 apiOnly = true; 111 } else if (arg.equals("-showdynamic")) { 112 showDynamic = true; 113 } else if (arg.equals("-noncorepkgs")) { 114 nonCorePkgsFile = getOption(args, i++); 115 } else { 116 error("Invalid option: " + arg); 117 } 118 } 119 120 if (jdkhome == null && cparg == null) { 121 error("-jdkhome and -classpath not set"); 122 } 123 124 if (configs.isEmpty()) { 125 error("-config not set"); 126 } 127 128 if (version == null) { 129 error("-version not set"); 130 } 131 132 ModuleBuilder builder; 133 if (jdkhome != null) { 134 PlatformModuleBuilder pmb = 135 new PlatformModuleBuilder(configs, depconfigs, mergeModules, version); 136 if (nonCorePkgsFile != null) { 137 pmb.readNonCorePackagesFrom(nonCorePkgsFile); 138 } 139 builder = pmb; 140 } else { 141 builder = new ModuleBuilder(configs, depconfigs, mergeModules, version); 142 } 143 144 ClassAnalyzer analyzer = new ClassAnalyzer(cpaths, builder, classlistDir); 145 // parse class and resource files 146 analyzer.run(update, apiOnly); 147 148 // print reports and module-info.java 149 analyzer.generateReports(classlistDir, showDynamic); 150 if (minfoDir != null) { 151 analyzer.printModuleInfos(minfoDir); 152 } 153 } 154 private final ClassPaths cpaths; 155 private final ModuleBuilder builder; 156 private final File classlistDir; 157 private final File moduleList; 158 private final Set<Module> updatedModules; // updated modules 159 160 ClassAnalyzer(ClassPaths cpaths, ModuleBuilder builder, String clistDir) { 161 this.cpaths = cpaths; 162 this.builder = builder; 163 this.classlistDir = new File(clistDir); 164 this.moduleList = new File(clistDir, "modules.list"); 165 this.updatedModules = new TreeSet<Module>(); 166 } 167 168 void run(boolean update, boolean apiOnly) throws IOException { 169 if (update) { 170 // incremental 171 if (!moduleList.exists()) { 172 // fall back to the default - analyze the entire jdk 173 update = false; 174 } 175 } 176 // parse class and resource files 177 processClassPaths(update, apiOnly); 178 179 // build modules & packages 180 builder.run(); 181 182 if (update) { 183 updatedModules.addAll(cpaths.getModules()); 184 } else { 185 updatedModules.addAll(builder.getModules()); 186 } 187 } 188 189 public void generateReports(String output, boolean showDynamic) 190 throws IOException { 191 File outputDir = new File(output); 192 if (!outputDir.exists()) 193 Files.mkdirs(outputDir); 194 195 if (updatedModules.size() > 0) { 196 printModulesList(); 197 } 198 199 // only print classlist of the recompiled modules 200 for (Module m : updatedModules) { 201 // write classlist and resourcelist of a module 202 ClassListWriter writer = new ClassListWriter(outputDir, m); 203 writer.printClassList(); 204 writer.printResourceList(); 205 writer.printDependencies(); 206 207 // write the summary and modules.list files 208 printModuleSummary(outputDir, m); 209 printModuleList(outputDir, m); 210 } 211 212 printSummary(outputDir, showDynamic); 213 } 214 215 void processClassPaths(boolean update, boolean apiOnly) throws IOException { 216 // TODO: always parseDeps? 217 boolean parseDeps = update == false; 218 ClassPaths.Filter filter = null; 219 220 long timestamp = update ? moduleList.lastModified() : -1L; 221 if (timestamp > 0) { 222 // for incremental build, only update the modules with 223 // recompiled classes or resources files. 224 final long ts = timestamp; 225 filter = new ClassPaths.Filter() { 226 227 @Override 228 public boolean accept(File f) { 229 return (f.isDirectory() 230 ? true 231 : f.lastModified() > ts); 232 } 233 234 @Override 235 public boolean accept(JarFile jf, JarEntry e) throws IOException { 236 long lastModified = e.getTime(); 237 return lastModified <= 0 || lastModified > ts; 238 } 239 }; 240 241 // load modules from the existing class list and resource list 242 builder.loadModulesFrom(classlistDir); 243 } 244 245 // parse class and resource files 246 cpaths.parse(filter, parseDeps, apiOnly); 247 if (Trace.traceOn) { 248 cpaths.printStats(); 249 } 250 } 251 252 public void printModuleInfos(String minfoDir) throws IOException { 253 for (Module m : updatedModules) { 254 ModuleInfo minfo = m.getModuleInfo(); 255 File mdir = new File(minfoDir, m.name()); 256 PrintWriter writer = new PrintWriter(Files.resolve(mdir, "module-info", "java")); 257 try { 258 writer.println(minfo.toString()); 259 } finally { 260 writer.close(); 261 } 262 } 263 } 264 265 public void printSummary(File dir, boolean showDynamic) throws IOException { 266 printModulesSummary(dir, showDynamic); 267 printModulesDot(dir, showDynamic); 268 printPackagesSummary(dir); 269 } 270 271 private static String getOption(String[] args, int index) { 272 if (index < args.length) { 273 return args[index]; 274 } else { 275 usage(); 276 } 277 return null; 278 } 279 280 private void printModuleSummary(File dir, Module m) throws IOException { 281 PrintWriter summary = 282 new PrintWriter(Files.resolve(dir, m.name(), "summary")); 283 try { 284 ModuleInfo mi = m.getModuleInfo(); 285 long total = 0L; 286 int count = 0; 287 summary.format("%10s\t%10s\t%s%n", "Bytes", "Classes", "Package name"); 288 for (PackageInfo info : mi.packages()) { 289 if (info.count > 0) { 290 summary.format("%10d\t%10d\t%s%n", 291 info.filesize, info.count, info.pkgName); 292 total += info.filesize; 293 count += info.count; 294 } 295 } 296 summary.format("%nTotal: %d bytes (uncompressed) %d classes%n", 297 total, count); 298 } finally { 299 summary.close(); 300 } 301 } 302 303 private void printModuleList(File dir, Module m) throws IOException { 304 String s = Module.getModuleProperty(m.name() + ".modules.list"); 305 if (s == null || Boolean.parseBoolean(s) == false) { 306 return; 307 } 308 309 PrintWriter mlist = new PrintWriter(Files.resolve(dir, m.name(), "modules.list")); 310 try { 311 Set<Module> deps = m.getModuleInfo().dependences( 312 new Dependence.Filter() { 313 314 @Override 315 public boolean accept(Dependence d) { 316 return !d.isOptional(); 317 } 318 }); 319 for (Module dm : deps) { 320 mlist.format("%s\n", dm.name()); 321 } 322 } finally { 323 mlist.close(); 324 } 325 } 326 327 private void printModuleGroup(Module group, PrintWriter writer) { 328 ModuleVisitor<Set<Module>> visitor = new ModuleVisitor<Set<Module>>() { 329 330 public void preVisit(Module p, Set<Module> leafnodes) { 331 } 332 333 public void visited(Module p, Module m, Set<Module> leafnodes) { 334 if (m.members().isEmpty()) { 335 leafnodes.add(m); 336 } 337 } 338 339 public void postVisit(Module p, Set<Module> leafnodes) { 340 } 341 }; 342 343 Set<Module> visited = new TreeSet<Module>(); 344 Set<Module> members = new TreeSet<Module>(); 345 group.visitMembers(visited, visitor, members); 346 347 // prints leaf members that are the modules defined in 348 // the modules.config files 349 writer.format("%s ", group); 350 for (Module m : members) { 351 writer.format("%s ", m); 352 } 353 writer.println(); 354 } 355 356 public void printModulesList() throws IOException { 357 // print module group / members relationship in 358 // the dependences order so that its dependences are listed first 359 PrintWriter writer = new PrintWriter(moduleList); 360 try { 361 for (Module m : builder.getModules()) { 362 printModuleGroup(m, writer); 363 } 364 } finally { 365 writer.close(); 366 } 367 } 368 369 public void printModulesSummary(File dir, boolean showDynamic) throws IOException { 370 // print summary of dependencies 371 PrintWriter writer = new PrintWriter(new File(dir, "modules.summary")); 372 try { 373 for (Module m : builder.getModules()) { 374 ModuleInfo mi = m.getModuleInfo(); 375 for (Dependence dep : mi.requires()) { 376 if (!dep.getModule().isBase()) { 377 String prefix = ""; 378 if (dep.isOptional()) { 379 prefix = "[optional] "; 380 } 381 382 Module other = dep.getModule(); 383 writer.format("%s%s -> %s%n", prefix, m, other); 384 } 385 } 386 } 387 } finally { 388 writer.close(); 389 } 390 } 391 392 private void printModulesDot(File dir, boolean showDynamic) throws IOException { 393 PrintWriter writer = new PrintWriter(new File(dir, "modules.dot")); 394 try { 395 writer.println("digraph jdk {"); 396 for (Module m : builder.getModules()) { 397 ModuleInfo mi = m.getModuleInfo(); 398 for (Dependence dep : mi.requires()) { 399 if (!dep.getModule().isBase()) { 400 String style = ""; 401 String color = ""; 402 String property = ""; 403 if (dep.isOptional()) { 404 style = "style=dotted"; 405 } 406 if (style.length() > 0 || color.length() > 0) { 407 String comma = ""; 408 if (style.length() > 0 && color.length() > 0) { 409 comma = ", "; 410 } 411 property = String.format(" [%s%s%s]", style, comma, color); 412 } 413 Module other = dep.getModule(); 414 writer.format(" \"%s\" -> \"%s\"%s;%n", m, other, property); 415 } 416 } 417 } 418 writer.println("}"); 419 } finally { 420 writer.close(); 421 } 422 } 423 424 private void printPackagesSummary(File dir) throws IOException { 425 // print package / module relationship 426 PrintWriter writer = new PrintWriter(new File(dir, "modules.pkginfo")); 427 try { 428 // packages that are splitted among multiple modules 429 writer.println("Packages splitted across modules:-\n"); 430 writer.format("%-60s %s\n", "Package", "Module"); 431 432 Map<String, Set<Module>> splitPackages = builder.getSplitPackages(); 433 for (Map.Entry<String, Set<Module>> e : splitPackages.entrySet()) { 434 String pkgname = e.getKey(); 435 writer.format("%-60s", pkgname); 436 for (Module m : e.getValue()) { 437 writer.format(" %s", m); 438 } 439 writer.println(); 440 } 441 442 writer.println("\nPackage-private dependencies:-"); 443 for (String pkgname : splitPackages.keySet()) { 444 for (Klass k : Klass.getAllClasses()) { 445 if (k.getPackageName().equals(pkgname)) { 446 Module m = k.getModule(); 447 // check if this klass references a package-private 448 // class that is in a different module 449 for (Klass other : k.getReferencedClasses()) { 450 if (other.getModule() != m 451 && !other.isPublic() 452 && other.getPackageName().equals(pkgname)) { 453 String from = k.getClassName() + " (" + m + ")"; 454 writer.format("%-60s -> %s (%s)\n", from, other, other.getModule()); 455 } 456 } 457 } 458 } 459 } 460 } finally { 461 writer.close(); 462 } 463 464 } 465 466 private static void error(String msg) { 467 System.err.println("ERROR: " + msg); 468 System.out.println(usage()); 469 System.exit(-1); 470 } 471 472 private static String usage() { 473 StringBuilder sb = new StringBuilder(); 474 sb.append("Usage: ClassAnalyzer <options>\n"); 475 sb.append("Options: \n"); 476 sb.append("\t-jdkhome <JDK home> where all jars will be parsed\n"); 477 sb.append("\t-classpath <classpath> where classes and jars will be parsed\n"); 478 sb.append("\t Either -jdkhome or -classpath option can be used.\n"); 479 sb.append("\t-config <module config file>\n"); 480 sb.append("\t This option can be repeated for multiple module config files\n"); 481 sb.append("\t-output <output dir>\n"); 482 sb.append("\t-update update modules with newer files\n"); 483 sb.append("\t-moduleinfo <output dir of module-info.java>\n"); 484 sb.append("\t-properties module's properties\n"); 485 sb.append("\t-noncorepkgs NON_CORE_PKGS.gmk\n"); 486 sb.append("\t-version <module's version>\n"); 487 sb.append("\t-showdynamic show dynamic dependencies in the reports\n"); 488 sb.append("\t-nomerge specify not to merge modules\n"); 489 return sb.toString(); 490 } 491 }