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 }