281 } |
313 } |
282 } |
314 } |
283 |
315 |
284 private final List<Archive> sourceLocations = new ArrayList<>(); |
316 private final List<Archive> sourceLocations = new ArrayList<>(); |
285 private boolean run() throws IOException { |
317 private boolean run() throws IOException { |
|
318 // parse classfiles and find all dependencies |
286 findDependencies(); |
319 findDependencies(); |
287 Analyzer analyzer = new Analyzer(options.verbose); |
320 |
|
321 Analyzer analyzer = new Analyzer(options.verbose, new Analyzer.Filter() { |
|
322 @Override |
|
323 public boolean accepts(Location origin, Archive originArchive, Location target, Archive targetArchive) { |
|
324 if (options.findJDKInternals) { |
|
325 // accepts target that is JDK class but not exported |
|
326 return isJDKArchive(targetArchive) && |
|
327 !((JDKArchive) targetArchive).isExported(target.getClassName()); |
|
328 } else if (options.filterSameArchive) { |
|
329 // accepts origin and target that from different archive |
|
330 return originArchive != targetArchive; |
|
331 } |
|
332 return true; |
|
333 } |
|
334 }); |
|
335 |
|
336 // analyze the dependencies |
288 analyzer.run(sourceLocations); |
337 analyzer.run(sourceLocations); |
|
338 |
|
339 // output result |
289 if (options.dotOutputDir != null) { |
340 if (options.dotOutputDir != null) { |
290 Path dir = Paths.get(options.dotOutputDir); |
341 Path dir = Paths.get(options.dotOutputDir); |
291 Files.createDirectories(dir); |
342 Files.createDirectories(dir); |
292 generateDotFiles(dir, analyzer); |
343 generateDotFiles(dir, analyzer); |
293 } else { |
344 } else { |
294 printRawOutput(log, analyzer); |
345 printRawOutput(log, analyzer); |
295 } |
346 } |
296 return true; |
347 return true; |
297 } |
348 } |
298 |
349 |
|
350 private void generateSummaryDotFile(Path dir, Analyzer analyzer) throws IOException { |
|
351 // If verbose mode (-v or -verbose option), |
|
352 // the summary.dot file shows package-level dependencies. |
|
353 Analyzer.Type summaryType = |
|
354 (options.verbose == PACKAGE || options.verbose == SUMMARY) ? SUMMARY : PACKAGE; |
|
355 Path summary = dir.resolve("summary.dot"); |
|
356 try (PrintWriter sw = new PrintWriter(Files.newOutputStream(summary)); |
|
357 SummaryDotFile dotfile = new SummaryDotFile(sw, summaryType)) { |
|
358 for (Archive archive : sourceLocations) { |
|
359 if (!archive.isEmpty()) { |
|
360 if (options.verbose == PACKAGE || options.verbose == SUMMARY) { |
|
361 if (options.showLabel) { |
|
362 // build labels listing package-level dependencies |
|
363 analyzer.visitDependences(archive, dotfile.labelBuilder(), PACKAGE); |
|
364 } |
|
365 } |
|
366 analyzer.visitDependences(archive, dotfile, summaryType); |
|
367 } |
|
368 } |
|
369 } |
|
370 } |
|
371 |
299 private void generateDotFiles(Path dir, Analyzer analyzer) throws IOException { |
372 private void generateDotFiles(Path dir, Analyzer analyzer) throws IOException { |
300 Path summary = dir.resolve("summary.dot"); |
|
301 boolean verbose = options.verbose == Analyzer.Type.VERBOSE; |
|
302 DotGraph<?> graph = verbose ? new DotSummaryForPackage() |
|
303 : new DotSummaryForArchive(); |
|
304 for (Archive archive : sourceLocations) { |
|
305 analyzer.visitArchiveDependences(archive, graph); |
|
306 if (verbose || options.showLabel) { |
|
307 // traverse detailed dependences to generate package-level |
|
308 // summary or build labels for edges |
|
309 analyzer.visitDependences(archive, graph); |
|
310 } |
|
311 } |
|
312 try (PrintWriter sw = new PrintWriter(Files.newOutputStream(summary))) { |
|
313 graph.writeTo(sw); |
|
314 } |
|
315 // output individual .dot file for each archive |
373 // output individual .dot file for each archive |
316 if (options.verbose != Analyzer.Type.SUMMARY) { |
374 if (options.verbose != SUMMARY) { |
317 for (Archive archive : sourceLocations) { |
375 for (Archive archive : sourceLocations) { |
318 if (analyzer.hasDependences(archive)) { |
376 if (analyzer.hasDependences(archive)) { |
319 Path dotfile = dir.resolve(archive.getFileName() + ".dot"); |
377 Path dotfile = dir.resolve(archive.getName() + ".dot"); |
320 try (PrintWriter pw = new PrintWriter(Files.newOutputStream(dotfile)); |
378 try (PrintWriter pw = new PrintWriter(Files.newOutputStream(dotfile)); |
321 DotFileFormatter formatter = new DotFileFormatter(pw, archive)) { |
379 DotFileFormatter formatter = new DotFileFormatter(pw, archive)) { |
322 analyzer.visitDependences(archive, formatter); |
380 analyzer.visitDependences(archive, formatter); |
323 } |
381 } |
324 } |
382 } |
325 } |
383 } |
326 } |
384 } |
|
385 // generate summary dot file |
|
386 generateSummaryDotFile(dir, analyzer); |
327 } |
387 } |
328 |
388 |
329 private void printRawOutput(PrintWriter writer, Analyzer analyzer) { |
389 private void printRawOutput(PrintWriter writer, Analyzer analyzer) { |
|
390 RawOutputFormatter depFormatter = new RawOutputFormatter(writer); |
|
391 RawSummaryFormatter summaryFormatter = new RawSummaryFormatter(writer); |
330 for (Archive archive : sourceLocations) { |
392 for (Archive archive : sourceLocations) { |
331 RawOutputFormatter formatter = new RawOutputFormatter(writer); |
393 if (!archive.isEmpty()) { |
332 analyzer.visitArchiveDependences(archive, formatter); |
394 analyzer.visitDependences(archive, summaryFormatter, SUMMARY); |
333 if (options.verbose != Analyzer.Type.SUMMARY) { |
395 if (analyzer.hasDependences(archive) && options.verbose != SUMMARY) { |
334 analyzer.visitDependences(archive, formatter); |
396 analyzer.visitDependences(archive, depFormatter); |
335 } |
397 } |
336 } |
398 } |
337 } |
399 } |
|
400 } |
|
401 |
338 private boolean isValidClassName(String name) { |
402 private boolean isValidClassName(String name) { |
339 if (!Character.isJavaIdentifierStart(name.charAt(0))) { |
403 if (!Character.isJavaIdentifierStart(name.charAt(0))) { |
340 return false; |
404 return false; |
341 } |
405 } |
342 for (int i=1; i < name.length(); i++) { |
406 for (int i=1; i < name.length(); i++) { |
646 } else { |
745 } else { |
647 files.add(Paths.get(p)); |
746 files.add(Paths.get(p)); |
648 } |
747 } |
649 for (Path f : files) { |
748 for (Path f : files) { |
650 if (Files.exists(f)) { |
749 if (Files.exists(f)) { |
651 result.add(new Archive(f, ClassFileReader.newInstance(f))); |
750 result.add(Archive.getInstance(f)); |
652 } |
751 } |
653 } |
752 } |
654 } |
753 } |
655 } |
754 } |
656 return result; |
755 return result; |
657 } |
|
658 |
|
659 /** |
|
660 * If the given archive is JDK archive and non-null Profile, |
|
661 * this method returns the profile name only if -profile option is specified; |
|
662 * a null profile indicates it accesses a private JDK API and this method |
|
663 * will return "JDK internal API". |
|
664 * |
|
665 * For non-JDK archives, this method returns the file name of the archive. |
|
666 */ |
|
667 private String getProfileArchiveInfo(Archive source, Profile profile) { |
|
668 if (options.showProfile && profile != null) |
|
669 return profile.toString(); |
|
670 |
|
671 if (source instanceof JDKArchive) { |
|
672 return profile == null ? "JDK internal API (" + source.getFileName() + ")" : ""; |
|
673 } |
|
674 return source.getFileName(); |
|
675 } |
|
676 |
|
677 /** |
|
678 * Returns the profile name or "JDK internal API" for JDK archive; |
|
679 * otherwise empty string. |
|
680 */ |
|
681 private String profileName(Archive archive, Profile profile) { |
|
682 if (archive instanceof JDKArchive) { |
|
683 return Objects.toString(profile, "JDK internal API"); |
|
684 } else { |
|
685 return ""; |
|
686 } |
|
687 } |
756 } |
688 |
757 |
689 class RawOutputFormatter implements Analyzer.Visitor { |
758 class RawOutputFormatter implements Analyzer.Visitor { |
690 private final PrintWriter writer; |
759 private final PrintWriter writer; |
|
760 private String pkg = ""; |
691 RawOutputFormatter(PrintWriter writer) { |
761 RawOutputFormatter(PrintWriter writer) { |
692 this.writer = writer; |
762 this.writer = writer; |
693 } |
763 } |
694 |
|
695 private String pkg = ""; |
|
696 @Override |
764 @Override |
697 public void visitDependence(String origin, Archive source, |
765 public void visitDependence(String origin, Archive originArchive, |
698 String target, Archive archive, Profile profile) { |
766 String target, Archive targetArchive) { |
699 if (options.findJDKInternals && |
767 String tag = toTag(target, targetArchive); |
700 !(archive instanceof JDKArchive && profile == null)) { |
768 if (options.verbose == VERBOSE) { |
701 // filter dependences other than JDK internal APIs |
769 writer.format(" %-50s -> %-50s %s%n", origin, target, tag); |
702 return; |
|
703 } |
|
704 if (options.verbose == Analyzer.Type.VERBOSE) { |
|
705 writer.format(" %-50s -> %-50s %s%n", |
|
706 origin, target, getProfileArchiveInfo(archive, profile)); |
|
707 } else { |
770 } else { |
708 if (!origin.equals(pkg)) { |
771 if (!origin.equals(pkg)) { |
709 pkg = origin; |
772 pkg = origin; |
710 writer.format(" %s (%s)%n", origin, source.getFileName()); |
773 writer.format(" %s (%s)%n", origin, originArchive.getName()); |
711 } |
774 } |
712 writer.format(" -> %-50s %s%n", |
775 writer.format(" -> %-50s %s%n", target, tag); |
713 target, getProfileArchiveInfo(archive, profile)); |
776 } |
714 } |
777 } |
715 } |
778 } |
716 |
779 |
|
780 class RawSummaryFormatter implements Analyzer.Visitor { |
|
781 private final PrintWriter writer; |
|
782 RawSummaryFormatter(PrintWriter writer) { |
|
783 this.writer = writer; |
|
784 } |
717 @Override |
785 @Override |
718 public void visitArchiveDependence(Archive origin, Archive target, Profile profile) { |
786 public void visitDependence(String origin, Archive originArchive, |
719 writer.format("%s -> %s", origin.getPathName(), target.getPathName()); |
787 String target, Archive targetArchive) { |
720 if (options.showProfile && profile != null) { |
788 writer.format("%s -> %s", originArchive.getName(), targetArchive.getPathName()); |
721 writer.format(" (%s)%n", profile); |
789 if (options.showProfile && JDKArchive.isProfileArchive(targetArchive)) { |
722 } else { |
790 writer.format(" (%s)", target); |
723 writer.format("%n"); |
791 } |
724 } |
792 writer.format("%n"); |
725 } |
793 } |
726 } |
794 } |
727 |
795 |
728 class DotFileFormatter extends DotGraph<String> implements AutoCloseable { |
796 class DotFileFormatter implements Analyzer.Visitor, AutoCloseable { |
729 private final PrintWriter writer; |
797 private final PrintWriter writer; |
730 private final String name; |
798 private final String name; |
731 DotFileFormatter(PrintWriter writer, Archive archive) { |
799 DotFileFormatter(PrintWriter writer, Archive archive) { |
732 this.writer = writer; |
800 this.writer = writer; |
733 this.name = archive.getFileName(); |
801 this.name = archive.getName(); |
734 writer.format("digraph \"%s\" {%n", name); |
802 writer.format("digraph \"%s\" {%n", name); |
735 writer.format(" // Path: %s%n", archive.getPathName()); |
803 writer.format(" // Path: %s%n", archive.getPathName()); |
736 } |
804 } |
737 |
805 |
738 @Override |
806 @Override |
739 public void close() { |
807 public void close() { |
740 writer.println("}"); |
808 writer.println("}"); |
741 } |
809 } |
742 |
810 |
743 @Override |
811 @Override |
744 public void visitDependence(String origin, Archive source, |
812 public void visitDependence(String origin, Archive originArchive, |
745 String target, Archive archive, Profile profile) { |
813 String target, Archive targetArchive) { |
746 if (options.findJDKInternals && |
814 String tag = toTag(target, targetArchive); |
747 !(archive instanceof JDKArchive && profile == null)) { |
815 writer.format(" %-50s -> \"%s\";%n", |
748 // filter dependences other than JDK internal APIs |
816 String.format("\"%s\"", origin), |
749 return; |
817 tag.isEmpty() ? target |
750 } |
818 : String.format("%s (%s)", target, tag)); |
751 // if -P option is specified, package name -> profile will |
819 } |
752 // be shown and filter out multiple same edges. |
820 } |
753 String name = getProfileArchiveInfo(archive, profile); |
821 |
754 writeEdge(writer, new Edge(origin, target, getProfileArchiveInfo(archive, profile))); |
822 class SummaryDotFile implements Analyzer.Visitor, AutoCloseable { |
755 } |
823 private final PrintWriter writer; |
|
824 private final Analyzer.Type type; |
|
825 private final Map<Archive, Map<Archive,StringBuilder>> edges = new HashMap<>(); |
|
826 SummaryDotFile(PrintWriter writer, Analyzer.Type type) { |
|
827 this.writer = writer; |
|
828 this.type = type; |
|
829 writer.format("digraph \"summary\" {%n"); |
|
830 } |
|
831 |
756 @Override |
832 @Override |
757 public void visitArchiveDependence(Archive origin, Archive target, Profile profile) { |
833 public void close() { |
758 throw new UnsupportedOperationException(); |
834 writer.println("}"); |
759 } |
835 } |
760 } |
836 |
761 |
|
762 class DotSummaryForArchive extends DotGraph<Archive> { |
|
763 @Override |
837 @Override |
764 public void visitDependence(String origin, Archive source, |
838 public void visitDependence(String origin, Archive originArchive, |
765 String target, Archive archive, Profile profile) { |
839 String target, Archive targetArchive) { |
766 Edge e = findEdge(source, archive); |
840 String targetName = type == PACKAGE ? target : targetArchive.getName(); |
767 assert e != null; |
841 if (type == PACKAGE) { |
768 // add the dependency to the label if enabled and not compact1 |
842 String tag = toTag(target, targetArchive, type); |
769 if (profile == Profile.COMPACT1) { |
843 if (!tag.isEmpty()) |
770 return; |
844 targetName += " (" + tag + ")"; |
771 } |
845 } else if (options.showProfile && JDKArchive.isProfileArchive(targetArchive)) { |
772 e.addLabel(origin, target, profileName(archive, profile)); |
846 targetName += " (" + target + ")"; |
773 } |
847 } |
774 @Override |
848 String label = getLabel(originArchive, targetArchive); |
775 public void visitArchiveDependence(Archive origin, Archive target, Profile profile) { |
849 writer.format(" %-50s -> \"%s\"%s;%n", |
776 // add an edge with the archive's name with no tag |
850 String.format("\"%s\"", origin), targetName, label); |
777 // so that there is only one node for each JDK archive |
851 } |
778 // while there may be edges to different profiles |
852 |
779 Edge e = addEdge(origin, target, ""); |
853 String getLabel(Archive origin, Archive target) { |
780 if (target instanceof JDKArchive) { |
854 if (edges.isEmpty()) |
781 // add a label to print the profile |
855 return ""; |
782 if (profile == null) { |
856 |
783 e.addLabel("JDK internal API"); |
857 StringBuilder label = edges.get(origin).get(target); |
784 } else if (options.showProfile && !options.showLabel) { |
858 return label == null ? "" : String.format(" [label=\"%s\",fontsize=9]", label.toString()); |
785 e.addLabel(profile.toString()); |
859 } |
786 } |
860 |
787 } |
861 Analyzer.Visitor labelBuilder() { |
788 } |
862 // show the package-level dependencies as labels in the dot graph |
789 } |
863 return new Analyzer.Visitor() { |
790 |
864 @Override |
791 // DotSummaryForPackage generates the summary.dot file for verbose mode |
865 public void visitDependence(String origin, Archive originArchive, String target, Archive targetArchive) { |
792 // (-v or -verbose option) that includes all class dependencies. |
866 edges.putIfAbsent(originArchive, new HashMap<>()); |
793 // The summary.dot file shows package-level dependencies. |
867 edges.get(originArchive).putIfAbsent(targetArchive, new StringBuilder()); |
794 class DotSummaryForPackage extends DotGraph<String> { |
868 StringBuilder sb = edges.get(originArchive).get(targetArchive); |
795 private String packageOf(String cn) { |
869 String tag = toTag(target, targetArchive, PACKAGE); |
796 int i = cn.lastIndexOf('.'); |
870 addLabel(sb, origin, target, tag); |
797 return i > 0 ? cn.substring(0, i) : "<unnamed>"; |
871 } |
798 } |
872 |
799 @Override |
873 void addLabel(StringBuilder label, String origin, String target, String tag) { |
800 public void visitDependence(String origin, Archive source, |
874 label.append(origin).append(" -> ").append(target); |
801 String target, Archive archive, Profile profile) { |
875 if (!tag.isEmpty()) { |
802 // add a package dependency edge |
876 label.append(" (" + tag + ")"); |
803 String from = packageOf(origin); |
877 } |
804 String to = packageOf(target); |
878 label.append("\\n"); |
805 Edge e = addEdge(from, to, getProfileArchiveInfo(archive, profile)); |
879 } |
806 |
880 }; |
807 // add the dependency to the label if enabled and not compact1 |
881 } |
808 if (!options.showLabel || profile == Profile.COMPACT1) { |
882 } |
809 return; |
883 |
810 } |
884 /** |
811 |
885 * Test if the given archive is part of the JDK |
812 // trim the package name of origin to shorten the label |
886 */ |
813 int i = origin.lastIndexOf('.'); |
887 private boolean isJDKArchive(Archive archive) { |
814 String n1 = i < 0 ? origin : origin.substring(i+1); |
888 return JDKArchive.class.isInstance(archive); |
815 e.addLabel(n1, target, profileName(archive, profile)); |
889 } |
816 } |
890 |
817 @Override |
891 /** |
818 public void visitArchiveDependence(Archive origin, Archive target, Profile profile) { |
892 * If the given archive is JDK archive, this method returns the profile name |
819 // nop |
893 * only if -profile option is specified; it accesses a private JDK API and |
820 } |
894 * the returned value will have "JDK internal API" prefix |
821 } |
895 * |
822 abstract class DotGraph<T> implements Analyzer.Visitor { |
896 * For non-JDK archives, this method returns the file name of the archive. |
823 private final Set<Edge> edges = new LinkedHashSet<>(); |
897 */ |
824 private Edge curEdge; |
898 private String toTag(String name, Archive source, Analyzer.Type type) { |
825 public void writeTo(PrintWriter writer) { |
899 if (!isJDKArchive(source)) { |
826 writer.format("digraph \"summary\" {%n"); |
900 return source.getName(); |
827 for (Edge e: edges) { |
901 } |
828 writeEdge(writer, e); |
902 |
829 } |
903 JDKArchive jdk = (JDKArchive)source; |
830 writer.println("}"); |
904 boolean isExported = false; |
831 } |
905 if (type == CLASS || type == VERBOSE) { |
832 |
906 isExported = jdk.isExported(name); |
833 void writeEdge(PrintWriter writer, Edge e) { |
907 } else { |
834 writer.format(" %-50s -> \"%s\"%s;%n", |
908 isExported = jdk.isExportedPackage(name); |
835 String.format("\"%s\"", e.from.toString()), |
909 } |
836 e.tag.isEmpty() ? e.to |
910 Profile p = getProfile(name, type); |
837 : String.format("%s (%s)", e.to, e.tag), |
911 if (isExported) { |
838 getLabel(e)); |
912 // exported API |
839 } |
913 return options.showProfile && p != null ? p.profileName() : ""; |
840 |
914 } else { |
841 Edge addEdge(T origin, T target, String tag) { |
915 return "JDK internal API (" + source.getName() + ")"; |
842 Edge e = new Edge(origin, target, tag); |
916 } |
843 if (e.equals(curEdge)) { |
917 } |
844 return curEdge; |
918 |
845 } |
919 private String toTag(String name, Archive source) { |
846 |
920 return toTag(name, source, options.verbose); |
847 if (edges.contains(e)) { |
921 } |
848 for (Edge e1 : edges) { |
922 |
849 if (e.equals(e1)) { |
923 private Profile getProfile(String name, Analyzer.Type type) { |
850 curEdge = e1; |
924 String pn = name; |
851 } |
925 if (type == CLASS || type == VERBOSE) { |
852 } |
926 int i = name.lastIndexOf('.'); |
853 } else { |
927 pn = i > 0 ? name.substring(0, i) : ""; |
854 edges.add(e); |
928 } |
855 curEdge = e; |
929 return Profile.getProfile(pn); |
856 } |
|
857 return curEdge; |
|
858 } |
|
859 |
|
860 Edge findEdge(T origin, T target) { |
|
861 for (Edge e : edges) { |
|
862 if (e.from.equals(origin) && e.to.equals(target)) { |
|
863 return e; |
|
864 } |
|
865 } |
|
866 return null; |
|
867 } |
|
868 |
|
869 String getLabel(Edge e) { |
|
870 String label = e.label.toString(); |
|
871 return label.isEmpty() ? "" : String.format("[label=\"%s\",fontsize=9]", label); |
|
872 } |
|
873 |
|
874 class Edge { |
|
875 final T from; |
|
876 final T to; |
|
877 final String tag; // optional tag |
|
878 final StringBuilder label = new StringBuilder(); |
|
879 Edge(T from, T to, String tag) { |
|
880 this.from = from; |
|
881 this.to = to; |
|
882 this.tag = tag; |
|
883 } |
|
884 void addLabel(String s) { |
|
885 label.append(s).append("\\n"); |
|
886 } |
|
887 void addLabel(String origin, String target, String profile) { |
|
888 label.append(origin).append(" -> ").append(target); |
|
889 if (!profile.isEmpty()) { |
|
890 label.append(" (" + profile + ")"); |
|
891 } |
|
892 label.append("\\n"); |
|
893 } |
|
894 @Override @SuppressWarnings("unchecked") |
|
895 public boolean equals(Object o) { |
|
896 if (o instanceof DotGraph<?>.Edge) { |
|
897 DotGraph<?>.Edge e = (DotGraph<?>.Edge)o; |
|
898 return this.from.equals(e.from) && |
|
899 this.to.equals(e.to) && |
|
900 this.tag.equals(e.tag); |
|
901 } |
|
902 return false; |
|
903 } |
|
904 @Override |
|
905 public int hashCode() { |
|
906 int hash = 7; |
|
907 hash = 67 * hash + Objects.hashCode(this.from) + |
|
908 Objects.hashCode(this.to) + Objects.hashCode(this.tag); |
|
909 return hash; |
|
910 } |
|
911 } |
|
912 } |
930 } |
913 } |
931 } |