diff --git a/build.xml b/build.xml index 546f224..9b7a983 100644 --- a/build.xml +++ b/build.xml @@ -67,7 +67,11 @@ class Version { public static String VERSION = "${VERSION}";} - + + + + + diff --git a/src/gr/spinellis/umlgraph/doclet/ClassGraph.java b/src/gr/spinellis/umlgraph/doclet/ClassGraph.java index 109d172..148521a 100644 --- a/src/gr/spinellis/umlgraph/doclet/ClassGraph.java +++ b/src/gr/spinellis/umlgraph/doclet/ClassGraph.java @@ -56,6 +56,7 @@ import com.sun.javadoc.WildcardType; /** * Class graph generation engine * @depend - - - StringUtil + * @depend - - - Options * @composed - - * ClassInfo * @has - - - OptionProvider * @@ -89,16 +90,26 @@ class ClassGraph { protected ClassDoc mapClassDoc; protected String linePostfix; protected String linePrefix; - + + // used only when generating context class diagrams in UMLDoc, to generate the proper + // relative links to other classes in the image map + protected Doc contextDoc; + + /** - * Create a new ClassGraph. The packages passed as an - * argument are the ones specified on the command line. - * Local URLs will be generated for these packages. + * Create a new ClassGraph.

The packages passed as an + * argument are the ones specified on the command line.

+ *

Local URLs will be generated for these packages.

+ * @param root The root of docs as provided by the javadoc API + * @param optionProvider The main option provider + * @param contextDoc The current context for generating relative links, may be a ClassDoc + * or a PackageDoc (used by UMLDoc) */ - public ClassGraph(RootDoc root, OptionProvider optionProvider) throws IOException { + public ClassGraph(RootDoc root, OptionProvider optionProvider, Doc contextDoc) throws IOException { this.optionProvider = optionProvider; this.collectionClassDoc = root.classNamed("java.util.Collection"); this.mapClassDoc = root.classNamed("java.util.Map"); + this.contextDoc = contextDoc; specifiedPackages = new HashSet(); for (PackageDoc p : root.specifiedPackages()) @@ -447,7 +458,7 @@ class ClassGraph { w.println("\t// " + r); // Create label w.print("\t" + ci.name + " [label="); - externalTableStart(opt, c.qualifiedName()); + externalTableStart(opt, c.qualifiedName(), classToUrl(c)); innerTableStart(); if (c.isInterface()) tableLine(Align.CENTER, guilWrap(opt, "interface")); @@ -656,7 +667,7 @@ class ClassGraph { continue; w.println("\t// " + className); w.print("\t" + info.name + "[label="); - externalTableStart(opt, className); + externalTableStart(opt, className, classToUrl(className)); innerTableStart(); int idx = className.lastIndexOf("."); if(opt.postfixPackage && idx > 0 && idx < (className.length() - 1)) { @@ -847,24 +858,76 @@ class ClassGraph { return name.substring(0, openIdx); } - /** Return true if the class name has been specified in the command line */ + /** + * Return true if the class name has been specified in the command line. + * Use this only if the ClassDoc is not available, since this implementation won't handle + * properly inner classes (confuses the containing class as a package) + */ public boolean isSpecifiedPackage(String className) { int idx = className.lastIndexOf("."); String packageName = idx > 0 ? className.substring(0, idx) : className; return specifiedPackages.contains(packageName); } + + /** Return true if the class name has been specified in the command line */ + public boolean isSpecifiedPackage(ClassDoc cd) { + return specifiedPackages.contains(cd.containingPackage().name()); + } + /** Convert the class name into a corresponding URL */ + public String classToUrl(ClassDoc cd) { + // building relative path for context and package diagrams + if(contextDoc != null && isSpecifiedPackage(cd)) { + // determine the context path, relative to the root + String[] contextClassPath = null; + if(contextDoc instanceof ClassDoc) { + contextClassPath = ((ClassDoc) contextDoc).containingPackage().name().split("\\."); + } else if(contextDoc instanceof PackageDoc) { + contextClassPath = ((PackageDoc) contextDoc).name().split("\\."); + } else { + return classToUrl(cd.qualifiedName()); + } + + // path, relative to the root, of the destination class + String[] currClassPath = cd.containingPackage().name().split("\\."); + + // compute relative path between the context and the destination + // ... first, compute common part + int i = 0; + while(i < contextClassPath.length && i < currClassPath.length + && contextClassPath[i].equals(currClassPath[i])) + i++; + // ... go up with ".." to reach the common root + StringBuffer buf = new StringBuffer(); + if(i == contextClassPath.length) { + buf.append(".").append(FILE_SEPARATOR); + } else { + for (int j = i; j < currClassPath.length; j++) { + buf.append("..").append(FILE_SEPARATOR); + } + } + // ... go down from the common root to the destination + for (int j = i; j < currClassPath.length; j++) { + buf.append(currClassPath[j]).append(FILE_SEPARATOR); + } + buf.append(cd.name()).append(".html"); + return buf.toString(); + } else { + return classToUrl(cd.qualifiedName()); + } + } + /** Convert the class name into a corresponding URL */ public String classToUrl(String className) { - String result = null; - String docRoot = mapApiDocRoot(className); - if (docRoot != null) { - StringBuffer buf = new StringBuffer(docRoot); - buf.append(className.replace('.', FILE_SEPARATOR)); - buf.append(".html"); - result = buf.toString(); - } - return result; + String docRoot = mapApiDocRoot(className); + if (docRoot != null) { + StringBuffer buf = new StringBuffer(docRoot); + buf.append(className.replace('.', FILE_SEPARATOR)); + buf.append(".html"); + return buf.toString(); + } else { + return null; + } } /** @@ -944,12 +1007,11 @@ class ClassGraph { w.close(); } - private void externalTableStart(Options opt, String name) { + private void externalTableStart(Options opt, String name, String url) { String bgcolor = ""; if (opt.nodeFillColor != null) bgcolor = " bgcolor=\""+ opt.nodeFillColor + "\""; String href = ""; - String url = classToUrl(name); if (url != null) href = " href=\"" + url + "\""; w.print("< + * As such, can be viewed as a simplified version of a {@linkplain View} using a + * single {@linkplain ContextMatcher}, but provides some extra configuration + * such as context highlighting and output path configuration (and it is + * specified in code rather than in javadoc comments). + * @author wolf + * + */ +public class ContextView implements OptionProvider { + + private ClassDoc cd; + private OptionProvider parent; + private ContextMatcher matcher; + private String outputPath; + + public ContextView(String outputFolder, ClassDoc cd, RootDoc root, OptionProvider parent) + throws IOException { + this.parent = parent; + this.cd = cd; + this.matcher = new ContextMatcher(root, Pattern.compile(cd.qualifiedName()), + getGlobalOptions()); + this.outputPath = cd.containingPackage().name().replace('.', '/') + "/" + cd.name() + + ".dot"; + } + + public String getDisplayName() { + return "Context view for class " + cd; + } + + public Options getGlobalOptions() { + Options go = parent.getGlobalOptions(); + + go.setOption(new String[] { "-output", outputPath }); + go.setOption(new String[] { "-hide" }); + + return go; + } + + public Options getOptionsFor(ClassDoc cd) { + Options go = parent.getGlobalOptions(); + overrideForClass(go, cd); + return go; + } + + public Options getOptionsFor(String name) { + Options go = parent.getGlobalOptions(); + overrideForClass(go, name); + return go; + } + + public void overrideForClass(Options opt, ClassDoc cd) { + if (parent.getGlobalOptions().matchesHideExpression(cd.name()) || !matcher.matches(cd)) + opt.setOption(new String[] { "-hide" }); + if (cd.equals(this.cd)) + opt.nodeFillColor = "lemonChiffon"; + } + + public void overrideForClass(Options opt, String className) { + if (!matcher.matches(className)) + opt.setOption(new String[] { "-hide" }); + } + +} diff --git a/src/gr/spinellis/umlgraph/doclet/Options.java b/src/gr/spinellis/umlgraph/doclet/Options.java index 5441c28..d43932d 100644 --- a/src/gr/spinellis/umlgraph/doclet/Options.java +++ b/src/gr/spinellis/umlgraph/doclet/Options.java @@ -97,6 +97,8 @@ class Options implements Cloneable, OptionProvider { String inferRelationshipType; private Vector collPackages; public boolean compact; + // internal option, used by UMLDoc to generate relative links between classes + boolean relativeLinksForSourcePackages; Options() { showQualified = false; @@ -141,6 +143,7 @@ class Options implements Cloneable, OptionProvider { inferRelationshipType = "navassoc"; collPackages = new Vector(); compact = false; + relativeLinksForSourcePackages = false; } public Object clone() { diff --git a/src/gr/spinellis/umlgraph/doclet/PackageMatcher.java b/src/gr/spinellis/umlgraph/doclet/PackageMatcher.java new file mode 100644 index 0000000..ae8da2e --- /dev/null +++ b/src/gr/spinellis/umlgraph/doclet/PackageMatcher.java @@ -0,0 +1,26 @@ +package gr.spinellis.umlgraph.doclet; + +import com.sun.javadoc.ClassDoc; +import com.sun.javadoc.PackageDoc; + +public class PackageMatcher implements ClassMatcher { + protected PackageDoc packageDoc; + + public PackageMatcher(PackageDoc packageDoc) { + super(); + this.packageDoc = packageDoc; + } + + public boolean matches(ClassDoc cd) { + return cd.containingPackage().equals(packageDoc); + } + + public boolean matches(String name) { + for (ClassDoc cd : packageDoc.allClasses()) { + if (cd.qualifiedName().equals(name)) + return true; + } + return false; + } + +} diff --git a/src/gr/spinellis/umlgraph/doclet/PackageView.java b/src/gr/spinellis/umlgraph/doclet/PackageView.java new file mode 100644 index 0000000..34e7aac --- /dev/null +++ b/src/gr/spinellis/umlgraph/doclet/PackageView.java @@ -0,0 +1,67 @@ +package gr.spinellis.umlgraph.doclet; + +import com.sun.javadoc.ClassDoc; +import com.sun.javadoc.PackageDoc; +import com.sun.javadoc.RootDoc; + +/** + * A view designed for UMLDoc, filters out everything that it's not contained in + * the specified package. + *

+ * As such, can be viewed as a simplified version of a {@linkplain View} using a + * single {@linkplain ClassMatcher}, and provides some extra configuration such + * as output path configuration (and it is specified in code rather than in + * javadoc comments). + * @author wolf + * + */ +public class PackageView implements OptionProvider { + + private PackageDoc pd; + private OptionProvider parent; + private ClassMatcher matcher; + private String outputPath; + + public PackageView(String outputFolder, PackageDoc pd, RootDoc root, OptionProvider parent) { + this.parent = parent; + this.pd = pd; + this.matcher = new PackageMatcher(pd); + this.outputPath = pd.name().replace('.', '/') + "/" + pd.name() + ".dot"; + } + + public String getDisplayName() { + return "Package view for packages" + pd; + } + + public Options getGlobalOptions() { + Options go = parent.getGlobalOptions(); + + go.setOption(new String[] { "-output", outputPath }); + go.setOption(new String[] { "-hide" }); + + return go; + } + + public Options getOptionsFor(ClassDoc cd) { + Options go = parent.getGlobalOptions(); + overrideForClass(go, cd); + return go; + } + + public Options getOptionsFor(String name) { + Options go = parent.getGlobalOptions(); + overrideForClass(go, name); + return go; + } + + public void overrideForClass(Options opt, ClassDoc cd) { + if (!matcher.matches(cd) || parent.getGlobalOptions().matchesHideExpression(cd.name())) + opt.setOption(new String[] { "-hide" }); + } + + public void overrideForClass(Options opt, String className) { + if (!matcher.matches(className)) + opt.setOption(new String[] { "-hide" }); + } + +} diff --git a/src/gr/spinellis/umlgraph/doclet/UmlDoc.java b/src/gr/spinellis/umlgraph/doclet/UmlDoc.java new file mode 100644 index 0000000..8eef1ae --- /dev/null +++ b/src/gr/spinellis/umlgraph/doclet/UmlDoc.java @@ -0,0 +1,196 @@ +package gr.spinellis.umlgraph.doclet; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; + +import com.sun.javadoc.ClassDoc; +import com.sun.javadoc.LanguageVersion; +import com.sun.javadoc.PackageDoc; +import com.sun.javadoc.RootDoc; +import com.sun.tools.doclets.standard.Standard; + +/** + * Chaining doclet that runs the standart Javadoc doclet first, and on success, + * runs the generation of dot files by UMLGraph + * @author wolf + * + */ +public class UmlDoc { + public static int optionLength(String option) { + int result = Standard.optionLength(option); + if (result != 0) + return result; + else + return UmlGraph.optionLength(option); + } + + public static boolean start(RootDoc root) throws IOException { + if (Standard.start(root)) { + String outputFolder = findOutputPath(root.options()); + + Options opt = new Options(); + opt.setOptions(root.options()); + opt.relativeLinksForSourcePackages = true; + + generatePackageDiagrams(root, opt, outputFolder); + generateContextDiagrams(root, opt, outputFolder); + return true; + } else { + return false; + } + } + + public static LanguageVersion languageVersion() { + return Standard.languageVersion(); + } + + private static void generatePackageDiagrams(RootDoc root, Options opt, String outputFolder) + throws IOException { + if (opt.verbose2) + System.out.println("Examining packages"); + for (PackageDoc packageDoc : root.specifiedPackages()) { + if (opt.verbose2) + System.out.println("Altering javadocs for pacakge " + packageDoc); + OptionProvider view = new PackageView(outputFolder, packageDoc, root, opt); + UmlGraph.buildGraph(root, view, packageDoc); + runGraphviz(outputFolder, packageDoc.name(), packageDoc.name()); + alterHtmlDocs(outputFolder, packageDoc.name(), packageDoc.name(), + "package-summary.html", ""); + } + } + + private static void generateContextDiagrams(RootDoc root, Options opt, String outputFolder) + throws IOException { + if (opt.verbose2) + System.out.println("Examining classes"); + for (ClassDoc classDoc : root.classes()) { + if (!isIncluded(classDoc, root.specifiedPackages())) + continue; + if (opt.verbose2) + System.out.println("Altering class doc for " + classDoc); + ContextView view = new ContextView(outputFolder, classDoc, root, opt); + UmlGraph.buildGraph(root, view, classDoc); + runGraphviz(outputFolder, classDoc.containingPackage().name(), classDoc.name()); + alterHtmlDocs(outputFolder, classDoc.containingPackage().name(), classDoc.name(), + classDoc.name() + ".html", classDoc.name() + ""); + } + } + + private static void runGraphviz(String outputFolder, String packageName, String name) { + File dotFile = new File(outputFolder, packageName.replace(".", "/") + "/" + name + ".dot"); + File pngFile = new File(outputFolder, packageName.replace(".", "/") + "/" + name + ".png"); + File mapFile = new File(outputFolder, packageName.replace(".", "/") + "/" + name + ".map"); + + try { + String command = "dot -Tcmapx -o" + mapFile.getAbsolutePath() + " -Tpng -o" + + pngFile.getAbsolutePath() + " " + dotFile.getAbsolutePath(); + Process p = Runtime.getRuntime().exec(command); + int result = p.waitFor(); + if (result != 0) + System.out.println("Errors running Graphviz on " + dotFile); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private static void alterHtmlDocs(String outputFolder, String packageName, String name, + String htmlFileName, String matchLineEnd) throws IOException { + // setup files + File output = new File(outputFolder, packageName.replace(".", "/")); + File htmlFile = new File(output, htmlFileName); + File alteredFile = new File(htmlFile.getAbsolutePath() + ".uml"); + File mapFile = new File(output, name + ".map"); + if (!htmlFile.exists()) { + System.err.println("Expected file not found: " + htmlFile.getAbsolutePath()); + return; + } + + // parse & rewrite + BufferedWriter writer = null; + BufferedReader reader = null; + boolean matched = false; + try { + writer = new BufferedWriter(new FileWriter(alteredFile)); + reader = new BufferedReader(new FileReader(htmlFile)); + + String line; + while ((line = reader.readLine()) != null) { + writer.write(line); + writer.newLine(); + if (!matched && line.endsWith(matchLineEnd)) { + matched = true; + if (mapFile.exists()) + insertClientSideMap(mapFile, writer); + else + System.out.println("Could not find map file " + mapFile); + writer.write("

\"Package
"); + writer.newLine(); + } + } + } finally { + if (writer != null) + writer.close(); + if (reader != null) + reader.close(); + } + + // if altered, delete old file and rename new one to the old file name + if (matched) { + htmlFile.delete(); + alteredFile.renameTo(htmlFile); + } else { + System.out.println("Error, could not find a line that ends with '" + matchLineEnd + + "'.\n Class diagram reference not inserted"); + alteredFile.delete(); + } + } + + /** + * Reads the map file and outputs in to the specified writer + * @throws IOException + */ + private static void insertClientSideMap(File mapFile, BufferedWriter writer) throws IOException { + BufferedReader reader = null; + try { + reader = new BufferedReader(new FileReader(mapFile)); + String line = null; + while ((line = reader.readLine()) != null) { + writer.write(line); + writer.newLine(); + } + } finally { + if (reader != null) + reader.close(); + } + } + + /** + * Return true if the class name has been specified in the command line + */ + private static boolean isIncluded(ClassDoc cd, PackageDoc[] docs) { + for (PackageDoc pd : docs) { + for (ClassDoc pcd : pd.allClasses()) + if (pcd.equals(cd)) + return true; + } + return false; + } + + /** + * Returns the output path specified on the javadoc options + */ + private static String findOutputPath(String[][] options) { + for (int i = 0; i < options.length; i++) { + if (options[i][0].equals("-d")) + return options[i][1]; + } + return "."; + } + +} diff --git a/src/gr/spinellis/umlgraph/doclet/UmlGraph.java b/src/gr/spinellis/umlgraph/doclet/UmlGraph.java index 30cd02c..6d1ae17 100644 --- a/src/gr/spinellis/umlgraph/doclet/UmlGraph.java +++ b/src/gr/spinellis/umlgraph/doclet/UmlGraph.java @@ -25,6 +25,7 @@ import java.util.ArrayList; import java.util.List; import com.sun.javadoc.ClassDoc; +import com.sun.javadoc.Doc; import com.sun.javadoc.LanguageVersion; import com.sun.javadoc.RootDoc; @@ -52,10 +53,10 @@ public class UmlGraph { if(views == null) return false; if (views.length == 0) { - buildGraph(root, opt); + buildGraph(root, opt, null); } else { for (int i = 0; i < views.length; i++) { - buildGraph(root, views[i]); + buildGraph(root, views[i], null); } } @@ -76,13 +77,13 @@ public class UmlGraph { /** * Builds and outputs a single graph according to the view overrides */ - private static void buildGraph(RootDoc root, OptionProvider op) throws IOException { + public static void buildGraph(RootDoc root, OptionProvider op, Doc contextDoc) throws IOException { Options opt = op.getGlobalOptions(); if (opt.verbose2) System.out.println("Building " + op.getDisplayName()); ClassDoc[] classes = root.classes(); - ClassGraph c = new ClassGraph(root, op); + ClassGraph c = new ClassGraph(root, op, contextDoc); c.prologue(); for (int i = 0; i < classes.length; i++) { c.printClass(classes[i]); diff --git a/src/gr/spinellis/umlgraph/doclet/View.java b/src/gr/spinellis/umlgraph/doclet/View.java index 1447188..945214c 100644 --- a/src/gr/spinellis/umlgraph/doclet/View.java +++ b/src/gr/spinellis/umlgraph/doclet/View.java @@ -40,6 +40,7 @@ import com.sun.javadoc.Tag; * @depend - - - InterfaceMatcher * @depend - - - PatternMatcher * @depend - - - SubclassMatcher + * @depend - - - ContextMatcher * */ class View implements OptionProvider { diff --git a/src/gr/spinellis/umlgraph/test/RunDoc.java b/src/gr/spinellis/umlgraph/test/RunDoc.java new file mode 100644 index 0000000..5370cbb --- /dev/null +++ b/src/gr/spinellis/umlgraph/test/RunDoc.java @@ -0,0 +1,48 @@ +/* + * UmlGraph class diagram testing framework + * + * Contibuted by Andrea Aime + * (C) Copyright 2005 Diomidis Spinellis + * + * Permission to use, copy, and distribute this software and its + * documentation for any purpose and without fee is hereby granted, + * provided that the above copyright notice appear in all copies and that + * both that copyright notice and this permission notice appear in + * supporting documentation. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF + * MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE. + * + * $Id$ + * + */ + +package gr.spinellis.umlgraph.test; + +import java.io.File; +import java.io.PrintWriter; + +public class RunDoc { + + static String sourcesFolder = "src"; + + static String docFolder = "javadoc"; + + static PrintWriter pw = new PrintWriter(System.out); + + public static void main(String[] args) { + File outFolder = new File(docFolder); + if (!outFolder.exists()) + outFolder.mkdirs(); + String[] options = new String[] { "-docletpath", "build", "-private", "-d", docFolder, + "-sourcepath", sourcesFolder, "-subpackages", "gr.spinellis" }; + runDoclet(options); + } + + private static void runDoclet(String[] options) { + com.sun.tools.javadoc.Main.execute("UMLGraph test", pw, pw, pw, + "gr.spinellis.umlgraph.doclet.UmlDoc", options); + } + +} diff --git a/src/gr/spinellis/umlgraph/test/RunOne.java b/src/gr/spinellis/umlgraph/test/RunOne.java index 8fb6574..ce4e3ed 100644 --- a/src/gr/spinellis/umlgraph/test/RunOne.java +++ b/src/gr/spinellis/umlgraph/test/RunOne.java @@ -36,8 +36,8 @@ public class RunOne { if (!outFolder.exists()) outFolder.mkdirs(); - runView("gr.spinellis.views.ViewChildEmpty"); - runSingleClass("foo"); +// runView("gr.spinellis.views.ViewChildEmpty"); + runSingleClass("TestHiddenOp"); } public static void runView(String viewClass) { diff --git a/src/org/umlgraph/doclet/ClassGraph.java b/src/org/umlgraph/doclet/ClassGraph.java index 109d172..148521a 100644 --- a/src/org/umlgraph/doclet/ClassGraph.java +++ b/src/org/umlgraph/doclet/ClassGraph.java @@ -56,6 +56,7 @@ import com.sun.javadoc.WildcardType; /** * Class graph generation engine * @depend - - - StringUtil + * @depend - - - Options * @composed - - * ClassInfo * @has - - - OptionProvider * @@ -89,16 +90,26 @@ class ClassGraph { protected ClassDoc mapClassDoc; protected String linePostfix; protected String linePrefix; - + + // used only when generating context class diagrams in UMLDoc, to generate the proper + // relative links to other classes in the image map + protected Doc contextDoc; + + /** - * Create a new ClassGraph. The packages passed as an - * argument are the ones specified on the command line. - * Local URLs will be generated for these packages. + * Create a new ClassGraph.

The packages passed as an + * argument are the ones specified on the command line.

+ *

Local URLs will be generated for these packages.

+ * @param root The root of docs as provided by the javadoc API + * @param optionProvider The main option provider + * @param contextDoc The current context for generating relative links, may be a ClassDoc + * or a PackageDoc (used by UMLDoc) */ - public ClassGraph(RootDoc root, OptionProvider optionProvider) throws IOException { + public ClassGraph(RootDoc root, OptionProvider optionProvider, Doc contextDoc) throws IOException { this.optionProvider = optionProvider; this.collectionClassDoc = root.classNamed("java.util.Collection"); this.mapClassDoc = root.classNamed("java.util.Map"); + this.contextDoc = contextDoc; specifiedPackages = new HashSet(); for (PackageDoc p : root.specifiedPackages()) @@ -447,7 +458,7 @@ class ClassGraph { w.println("\t// " + r); // Create label w.print("\t" + ci.name + " [label="); - externalTableStart(opt, c.qualifiedName()); + externalTableStart(opt, c.qualifiedName(), classToUrl(c)); innerTableStart(); if (c.isInterface()) tableLine(Align.CENTER, guilWrap(opt, "interface")); @@ -656,7 +667,7 @@ class ClassGraph { continue; w.println("\t// " + className); w.print("\t" + info.name + "[label="); - externalTableStart(opt, className); + externalTableStart(opt, className, classToUrl(className)); innerTableStart(); int idx = className.lastIndexOf("."); if(opt.postfixPackage && idx > 0 && idx < (className.length() - 1)) { @@ -847,24 +858,76 @@ class ClassGraph { return name.substring(0, openIdx); } - /** Return true if the class name has been specified in the command line */ + /** + * Return true if the class name has been specified in the command line. + * Use this only if the ClassDoc is not available, since this implementation won't handle + * properly inner classes (confuses the containing class as a package) + */ public boolean isSpecifiedPackage(String className) { int idx = className.lastIndexOf("."); String packageName = idx > 0 ? className.substring(0, idx) : className; return specifiedPackages.contains(packageName); } + + /** Return true if the class name has been specified in the command line */ + public boolean isSpecifiedPackage(ClassDoc cd) { + return specifiedPackages.contains(cd.containingPackage().name()); + } + /** Convert the class name into a corresponding URL */ + public String classToUrl(ClassDoc cd) { + // building relative path for context and package diagrams + if(contextDoc != null && isSpecifiedPackage(cd)) { + // determine the context path, relative to the root + String[] contextClassPath = null; + if(contextDoc instanceof ClassDoc) { + contextClassPath = ((ClassDoc) contextDoc).containingPackage().name().split("\\."); + } else if(contextDoc instanceof PackageDoc) { + contextClassPath = ((PackageDoc) contextDoc).name().split("\\."); + } else { + return classToUrl(cd.qualifiedName()); + } + + // path, relative to the root, of the destination class + String[] currClassPath = cd.containingPackage().name().split("\\."); + + // compute relative path between the context and the destination + // ... first, compute common part + int i = 0; + while(i < contextClassPath.length && i < currClassPath.length + && contextClassPath[i].equals(currClassPath[i])) + i++; + // ... go up with ".." to reach the common root + StringBuffer buf = new StringBuffer(); + if(i == contextClassPath.length) { + buf.append(".").append(FILE_SEPARATOR); + } else { + for (int j = i; j < currClassPath.length; j++) { + buf.append("..").append(FILE_SEPARATOR); + } + } + // ... go down from the common root to the destination + for (int j = i; j < currClassPath.length; j++) { + buf.append(currClassPath[j]).append(FILE_SEPARATOR); + } + buf.append(cd.name()).append(".html"); + return buf.toString(); + } else { + return classToUrl(cd.qualifiedName()); + } + } + /** Convert the class name into a corresponding URL */ public String classToUrl(String className) { - String result = null; - String docRoot = mapApiDocRoot(className); - if (docRoot != null) { - StringBuffer buf = new StringBuffer(docRoot); - buf.append(className.replace('.', FILE_SEPARATOR)); - buf.append(".html"); - result = buf.toString(); - } - return result; + String docRoot = mapApiDocRoot(className); + if (docRoot != null) { + StringBuffer buf = new StringBuffer(docRoot); + buf.append(className.replace('.', FILE_SEPARATOR)); + buf.append(".html"); + return buf.toString(); + } else { + return null; + } } /** @@ -944,12 +1007,11 @@ class ClassGraph { w.close(); } - private void externalTableStart(Options opt, String name) { + private void externalTableStart(Options opt, String name, String url) { String bgcolor = ""; if (opt.nodeFillColor != null) bgcolor = " bgcolor=\""+ opt.nodeFillColor + "\""; String href = ""; - String url = classToUrl(name); if (url != null) href = " href=\"" + url + "\""; w.print("<
+ * As such, can be viewed as a simplified version of a {@linkplain View} using a + * single {@linkplain ContextMatcher}, but provides some extra configuration + * such as context highlighting and output path configuration (and it is + * specified in code rather than in javadoc comments). + * @author wolf + * + */ +public class ContextView implements OptionProvider { + + private ClassDoc cd; + private OptionProvider parent; + private ContextMatcher matcher; + private String outputPath; + + public ContextView(String outputFolder, ClassDoc cd, RootDoc root, OptionProvider parent) + throws IOException { + this.parent = parent; + this.cd = cd; + this.matcher = new ContextMatcher(root, Pattern.compile(cd.qualifiedName()), + getGlobalOptions()); + this.outputPath = cd.containingPackage().name().replace('.', '/') + "/" + cd.name() + + ".dot"; + } + + public String getDisplayName() { + return "Context view for class " + cd; + } + + public Options getGlobalOptions() { + Options go = parent.getGlobalOptions(); + + go.setOption(new String[] { "-output", outputPath }); + go.setOption(new String[] { "-hide" }); + + return go; + } + + public Options getOptionsFor(ClassDoc cd) { + Options go = parent.getGlobalOptions(); + overrideForClass(go, cd); + return go; + } + + public Options getOptionsFor(String name) { + Options go = parent.getGlobalOptions(); + overrideForClass(go, name); + return go; + } + + public void overrideForClass(Options opt, ClassDoc cd) { + if (parent.getGlobalOptions().matchesHideExpression(cd.name()) || !matcher.matches(cd)) + opt.setOption(new String[] { "-hide" }); + if (cd.equals(this.cd)) + opt.nodeFillColor = "lemonChiffon"; + } + + public void overrideForClass(Options opt, String className) { + if (!matcher.matches(className)) + opt.setOption(new String[] { "-hide" }); + } + +} diff --git a/src/org/umlgraph/doclet/Options.java b/src/org/umlgraph/doclet/Options.java index 5441c28..d43932d 100644 --- a/src/org/umlgraph/doclet/Options.java +++ b/src/org/umlgraph/doclet/Options.java @@ -97,6 +97,8 @@ class Options implements Cloneable, OptionProvider { String inferRelationshipType; private Vector collPackages; public boolean compact; + // internal option, used by UMLDoc to generate relative links between classes + boolean relativeLinksForSourcePackages; Options() { showQualified = false; @@ -141,6 +143,7 @@ class Options implements Cloneable, OptionProvider { inferRelationshipType = "navassoc"; collPackages = new Vector(); compact = false; + relativeLinksForSourcePackages = false; } public Object clone() { diff --git a/src/org/umlgraph/doclet/PackageMatcher.java b/src/org/umlgraph/doclet/PackageMatcher.java new file mode 100644 index 0000000..ae8da2e --- /dev/null +++ b/src/org/umlgraph/doclet/PackageMatcher.java @@ -0,0 +1,26 @@ +package gr.spinellis.umlgraph.doclet; + +import com.sun.javadoc.ClassDoc; +import com.sun.javadoc.PackageDoc; + +public class PackageMatcher implements ClassMatcher { + protected PackageDoc packageDoc; + + public PackageMatcher(PackageDoc packageDoc) { + super(); + this.packageDoc = packageDoc; + } + + public boolean matches(ClassDoc cd) { + return cd.containingPackage().equals(packageDoc); + } + + public boolean matches(String name) { + for (ClassDoc cd : packageDoc.allClasses()) { + if (cd.qualifiedName().equals(name)) + return true; + } + return false; + } + +} diff --git a/src/org/umlgraph/doclet/PackageView.java b/src/org/umlgraph/doclet/PackageView.java new file mode 100644 index 0000000..34e7aac --- /dev/null +++ b/src/org/umlgraph/doclet/PackageView.java @@ -0,0 +1,67 @@ +package gr.spinellis.umlgraph.doclet; + +import com.sun.javadoc.ClassDoc; +import com.sun.javadoc.PackageDoc; +import com.sun.javadoc.RootDoc; + +/** + * A view designed for UMLDoc, filters out everything that it's not contained in + * the specified package. + *

+ * As such, can be viewed as a simplified version of a {@linkplain View} using a + * single {@linkplain ClassMatcher}, and provides some extra configuration such + * as output path configuration (and it is specified in code rather than in + * javadoc comments). + * @author wolf + * + */ +public class PackageView implements OptionProvider { + + private PackageDoc pd; + private OptionProvider parent; + private ClassMatcher matcher; + private String outputPath; + + public PackageView(String outputFolder, PackageDoc pd, RootDoc root, OptionProvider parent) { + this.parent = parent; + this.pd = pd; + this.matcher = new PackageMatcher(pd); + this.outputPath = pd.name().replace('.', '/') + "/" + pd.name() + ".dot"; + } + + public String getDisplayName() { + return "Package view for packages" + pd; + } + + public Options getGlobalOptions() { + Options go = parent.getGlobalOptions(); + + go.setOption(new String[] { "-output", outputPath }); + go.setOption(new String[] { "-hide" }); + + return go; + } + + public Options getOptionsFor(ClassDoc cd) { + Options go = parent.getGlobalOptions(); + overrideForClass(go, cd); + return go; + } + + public Options getOptionsFor(String name) { + Options go = parent.getGlobalOptions(); + overrideForClass(go, name); + return go; + } + + public void overrideForClass(Options opt, ClassDoc cd) { + if (!matcher.matches(cd) || parent.getGlobalOptions().matchesHideExpression(cd.name())) + opt.setOption(new String[] { "-hide" }); + } + + public void overrideForClass(Options opt, String className) { + if (!matcher.matches(className)) + opt.setOption(new String[] { "-hide" }); + } + +} diff --git a/src/org/umlgraph/doclet/UmlDoc.java b/src/org/umlgraph/doclet/UmlDoc.java new file mode 100644 index 0000000..8eef1ae --- /dev/null +++ b/src/org/umlgraph/doclet/UmlDoc.java @@ -0,0 +1,196 @@ +package gr.spinellis.umlgraph.doclet; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; + +import com.sun.javadoc.ClassDoc; +import com.sun.javadoc.LanguageVersion; +import com.sun.javadoc.PackageDoc; +import com.sun.javadoc.RootDoc; +import com.sun.tools.doclets.standard.Standard; + +/** + * Chaining doclet that runs the standart Javadoc doclet first, and on success, + * runs the generation of dot files by UMLGraph + * @author wolf + * + */ +public class UmlDoc { + public static int optionLength(String option) { + int result = Standard.optionLength(option); + if (result != 0) + return result; + else + return UmlGraph.optionLength(option); + } + + public static boolean start(RootDoc root) throws IOException { + if (Standard.start(root)) { + String outputFolder = findOutputPath(root.options()); + + Options opt = new Options(); + opt.setOptions(root.options()); + opt.relativeLinksForSourcePackages = true; + + generatePackageDiagrams(root, opt, outputFolder); + generateContextDiagrams(root, opt, outputFolder); + return true; + } else { + return false; + } + } + + public static LanguageVersion languageVersion() { + return Standard.languageVersion(); + } + + private static void generatePackageDiagrams(RootDoc root, Options opt, String outputFolder) + throws IOException { + if (opt.verbose2) + System.out.println("Examining packages"); + for (PackageDoc packageDoc : root.specifiedPackages()) { + if (opt.verbose2) + System.out.println("Altering javadocs for pacakge " + packageDoc); + OptionProvider view = new PackageView(outputFolder, packageDoc, root, opt); + UmlGraph.buildGraph(root, view, packageDoc); + runGraphviz(outputFolder, packageDoc.name(), packageDoc.name()); + alterHtmlDocs(outputFolder, packageDoc.name(), packageDoc.name(), + "package-summary.html", ""); + } + } + + private static void generateContextDiagrams(RootDoc root, Options opt, String outputFolder) + throws IOException { + if (opt.verbose2) + System.out.println("Examining classes"); + for (ClassDoc classDoc : root.classes()) { + if (!isIncluded(classDoc, root.specifiedPackages())) + continue; + if (opt.verbose2) + System.out.println("Altering class doc for " + classDoc); + ContextView view = new ContextView(outputFolder, classDoc, root, opt); + UmlGraph.buildGraph(root, view, classDoc); + runGraphviz(outputFolder, classDoc.containingPackage().name(), classDoc.name()); + alterHtmlDocs(outputFolder, classDoc.containingPackage().name(), classDoc.name(), + classDoc.name() + ".html", classDoc.name() + ""); + } + } + + private static void runGraphviz(String outputFolder, String packageName, String name) { + File dotFile = new File(outputFolder, packageName.replace(".", "/") + "/" + name + ".dot"); + File pngFile = new File(outputFolder, packageName.replace(".", "/") + "/" + name + ".png"); + File mapFile = new File(outputFolder, packageName.replace(".", "/") + "/" + name + ".map"); + + try { + String command = "dot -Tcmapx -o" + mapFile.getAbsolutePath() + " -Tpng -o" + + pngFile.getAbsolutePath() + " " + dotFile.getAbsolutePath(); + Process p = Runtime.getRuntime().exec(command); + int result = p.waitFor(); + if (result != 0) + System.out.println("Errors running Graphviz on " + dotFile); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private static void alterHtmlDocs(String outputFolder, String packageName, String name, + String htmlFileName, String matchLineEnd) throws IOException { + // setup files + File output = new File(outputFolder, packageName.replace(".", "/")); + File htmlFile = new File(output, htmlFileName); + File alteredFile = new File(htmlFile.getAbsolutePath() + ".uml"); + File mapFile = new File(output, name + ".map"); + if (!htmlFile.exists()) { + System.err.println("Expected file not found: " + htmlFile.getAbsolutePath()); + return; + } + + // parse & rewrite + BufferedWriter writer = null; + BufferedReader reader = null; + boolean matched = false; + try { + writer = new BufferedWriter(new FileWriter(alteredFile)); + reader = new BufferedReader(new FileReader(htmlFile)); + + String line; + while ((line = reader.readLine()) != null) { + writer.write(line); + writer.newLine(); + if (!matched && line.endsWith(matchLineEnd)) { + matched = true; + if (mapFile.exists()) + insertClientSideMap(mapFile, writer); + else + System.out.println("Could not find map file " + mapFile); + writer.write("

\"Package
"); + writer.newLine(); + } + } + } finally { + if (writer != null) + writer.close(); + if (reader != null) + reader.close(); + } + + // if altered, delete old file and rename new one to the old file name + if (matched) { + htmlFile.delete(); + alteredFile.renameTo(htmlFile); + } else { + System.out.println("Error, could not find a line that ends with '" + matchLineEnd + + "'.\n Class diagram reference not inserted"); + alteredFile.delete(); + } + } + + /** + * Reads the map file and outputs in to the specified writer + * @throws IOException + */ + private static void insertClientSideMap(File mapFile, BufferedWriter writer) throws IOException { + BufferedReader reader = null; + try { + reader = new BufferedReader(new FileReader(mapFile)); + String line = null; + while ((line = reader.readLine()) != null) { + writer.write(line); + writer.newLine(); + } + } finally { + if (reader != null) + reader.close(); + } + } + + /** + * Return true if the class name has been specified in the command line + */ + private static boolean isIncluded(ClassDoc cd, PackageDoc[] docs) { + for (PackageDoc pd : docs) { + for (ClassDoc pcd : pd.allClasses()) + if (pcd.equals(cd)) + return true; + } + return false; + } + + /** + * Returns the output path specified on the javadoc options + */ + private static String findOutputPath(String[][] options) { + for (int i = 0; i < options.length; i++) { + if (options[i][0].equals("-d")) + return options[i][1]; + } + return "."; + } + +} diff --git a/src/org/umlgraph/doclet/UmlGraph.java b/src/org/umlgraph/doclet/UmlGraph.java index 30cd02c..6d1ae17 100644 --- a/src/org/umlgraph/doclet/UmlGraph.java +++ b/src/org/umlgraph/doclet/UmlGraph.java @@ -25,6 +25,7 @@ import java.util.ArrayList; import java.util.List; import com.sun.javadoc.ClassDoc; +import com.sun.javadoc.Doc; import com.sun.javadoc.LanguageVersion; import com.sun.javadoc.RootDoc; @@ -52,10 +53,10 @@ public class UmlGraph { if(views == null) return false; if (views.length == 0) { - buildGraph(root, opt); + buildGraph(root, opt, null); } else { for (int i = 0; i < views.length; i++) { - buildGraph(root, views[i]); + buildGraph(root, views[i], null); } } @@ -76,13 +77,13 @@ public class UmlGraph { /** * Builds and outputs a single graph according to the view overrides */ - private static void buildGraph(RootDoc root, OptionProvider op) throws IOException { + public static void buildGraph(RootDoc root, OptionProvider op, Doc contextDoc) throws IOException { Options opt = op.getGlobalOptions(); if (opt.verbose2) System.out.println("Building " + op.getDisplayName()); ClassDoc[] classes = root.classes(); - ClassGraph c = new ClassGraph(root, op); + ClassGraph c = new ClassGraph(root, op, contextDoc); c.prologue(); for (int i = 0; i < classes.length; i++) { c.printClass(classes[i]); diff --git a/src/org/umlgraph/doclet/View.java b/src/org/umlgraph/doclet/View.java index 1447188..945214c 100644 --- a/src/org/umlgraph/doclet/View.java +++ b/src/org/umlgraph/doclet/View.java @@ -40,6 +40,7 @@ import com.sun.javadoc.Tag; * @depend - - - InterfaceMatcher * @depend - - - PatternMatcher * @depend - - - SubclassMatcher + * @depend - - - ContextMatcher * */ class View implements OptionProvider { diff --git a/src/org/umlgraph/test/RunDoc.java b/src/org/umlgraph/test/RunDoc.java new file mode 100644 index 0000000..5370cbb --- /dev/null +++ b/src/org/umlgraph/test/RunDoc.java @@ -0,0 +1,48 @@ +/* + * UmlGraph class diagram testing framework + * + * Contibuted by Andrea Aime + * (C) Copyright 2005 Diomidis Spinellis + * + * Permission to use, copy, and distribute this software and its + * documentation for any purpose and without fee is hereby granted, + * provided that the above copyright notice appear in all copies and that + * both that copyright notice and this permission notice appear in + * supporting documentation. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF + * MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE. + * + * $Id$ + * + */ + +package gr.spinellis.umlgraph.test; + +import java.io.File; +import java.io.PrintWriter; + +public class RunDoc { + + static String sourcesFolder = "src"; + + static String docFolder = "javadoc"; + + static PrintWriter pw = new PrintWriter(System.out); + + public static void main(String[] args) { + File outFolder = new File(docFolder); + if (!outFolder.exists()) + outFolder.mkdirs(); + String[] options = new String[] { "-docletpath", "build", "-private", "-d", docFolder, + "-sourcepath", sourcesFolder, "-subpackages", "gr.spinellis" }; + runDoclet(options); + } + + private static void runDoclet(String[] options) { + com.sun.tools.javadoc.Main.execute("UMLGraph test", pw, pw, pw, + "gr.spinellis.umlgraph.doclet.UmlDoc", options); + } + +} diff --git a/src/org/umlgraph/test/RunOne.java b/src/org/umlgraph/test/RunOne.java index 8fb6574..ce4e3ed 100644 --- a/src/org/umlgraph/test/RunOne.java +++ b/src/org/umlgraph/test/RunOne.java @@ -36,8 +36,8 @@ public class RunOne { if (!outFolder.exists()) outFolder.mkdirs(); - runView("gr.spinellis.views.ViewChildEmpty"); - runSingleClass("foo"); +// runView("gr.spinellis.views.ViewChildEmpty"); + runSingleClass("TestHiddenOp"); } public static void runView(String viewClass) {