diff --git a/src/gr/spinellis/umlgraph/doclet/ClassGraph.java b/src/gr/spinellis/umlgraph/doclet/ClassGraph.java index c54b6c1..513b842 100644 --- a/src/gr/spinellis/umlgraph/doclet/ClassGraph.java +++ b/src/gr/spinellis/umlgraph/doclet/ClassGraph.java @@ -24,9 +24,7 @@ import com.sun.javadoc.*; import java.io.*; import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.regex.PatternSyntaxException; +import java.util.regex.*; /** * Class graph generation engine @@ -38,6 +36,14 @@ import java.util.regex.PatternSyntaxException; * @author Diomidis Spinellis */ class ClassGraph { + private static Map associationMap = new HashMap(); + static { + associationMap.put("assoc", "arrowhead=none"); + associationMap.put("navassoc", "arrowhead=open"); + associationMap.put("has", "arrowhead=none, arrowtail=ediamond"); + associationMap.put("composed", "arrowhead=none, arrowtail=diamond"); + associationMap.put("depend", "arrowhead=open, style=dashed"); + } private Map classnames = new HashMap(); private String apiDocRoot; private Map apiDocMap = new HashMap(); @@ -49,16 +55,20 @@ class ClassGraph { private Set specifiedPackages; private OptionProvider optionProvider; private PrintWriter w; + private ClassDoc collectionClassDoc; + private ClassDoc mapClassDoc; /** * 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. */ - public ClassGraph(PackageDoc[] packages, OptionProvider optionProvider) throws IOException { + public ClassGraph(RootDoc root, OptionProvider optionProvider) throws IOException { this.optionProvider = optionProvider; + this.collectionClassDoc = root.classNamed("java.util.Collection"); + this.mapClassDoc = root.classNamed("java.util.Map"); specifiedPackages = new HashSet(); - for (PackageDoc p : packages) + for (PackageDoc p : root.specifiedPackages()) specifiedPackages.add(p.name()); Options opt = optionProvider.getGlobalOptions(); apiDocRoot = fixApiDocRoot(opt.apiDocRoot); @@ -452,47 +462,61 @@ class ClassGraph { * Print all relations for a given's class's tag * @param tagname the tag containing the given relation * @param from the source class - * @param name the source class internal name + * @param fromName the source class internal name * @param edgetype the dot edge specification */ - private void relation(Options opt, String tagname, ClassDoc from, String name, String edgetype) { + private void relation(Options opt, String tagname, ClassDoc from, String fromName) { for (Tag tag : from.tags(tagname)) { String t[] = StringUtil.tokenize(tag.text()); // l-src label l-dst target if (t.length != 4) { System.err.println("Error in " + from + "\n" + tagname + " expects four fields (l-src label l-dst target): " + tag.text()); return; } - String destName = t[3]; - String nodeName = null; + String dest = t[3]; + String destName = null; ClassDoc to = from.findClass(t[3]); if(to != null) { - destName = to.toString(); - nodeName = getNodeName(destName); + dest = to.toString(); + destName = getNodeName(dest); } else { - nodeName = getNodeName(t[3]); + destName = getNodeName(t[3]); } - if(hidden(destName)) + if(hidden(dest)) continue; - w.println("\t// " + from + " " + tagname + " " +destName); - w.println("\t" + name + " -> " + nodeName + " [" + - "taillabel=\"" + t[0] + "\", " + - "label=\"" + guillemize(opt, t[1]) + "\", " + - "headlabel=\"" + t[2] + "\", " + - "fontname=\"" + opt.edgeFontName + "\", " + - "fontcolor=\"" + opt.edgeFontColor + "\", " + - "fontsize=" + opt.edgeFontSize + ", " + - "color=\"" + opt.edgeColor + "\", " + - edgetype + "];" - ); + relation(opt, tagname, from.toString(), fromName, dest, destName, t[0], t[1], t[2]); } } + private void relation(Options opt, String tagname, String from, String fromName, + String dest, String destName, String tailLabel, String label, String headLabel) { + // print relation + String edgetype = associationMap.get(tagname); + w.println("\t// " + from + " " + tagname + " " + dest); + w.println("\t" + fromName + " -> " + destName + " [" + + "taillabel=\"" + tailLabel + "\", " + + "label=\"" + guillemize(opt, label) + "\", " + + "headlabel=\"" + headLabel + "\", " + + "fontname=\"" + opt.edgeFontName + "\", " + + "fontcolor=\"" + opt.edgeFontColor + "\", " + + "fontsize=" + opt.edgeFontSize + ", " + + "color=\"" + opt.edgeColor + "\", " + + edgetype + "];" + ); + + // update relation info + getClassInfo(from).addRelation(dest); + if(!tagname.equals("navassoc") && !tagname.equals("depend")) + getClassInfo(dest).addRelation(from); + + } + /** Print a class's relations */ public void printRelations(ClassDoc c) { Options opt = optionProvider.getOptionsFor(c); if (hidden(c)) return; + String className = c.toString(); String cs = getNodeName(c); // Print generalization (through the Java superclass) @@ -500,26 +524,35 @@ class ClassGraph { if (s != null && !s.toString().equals("java.lang.Object") && !c.isEnum() && - !hidden(s.asClassDoc())) + !hidden(s.asClassDoc())) { + ClassDoc sc = s.asClassDoc(); w.println("\t//" + c + " extends " + s + "\n" + - "\t" + getNodeName(s.asClassDoc()) + " -> " + cs + " [dir=back,arrowtail=empty];"); + "\t" + getNodeName(sc) + " -> " + cs + " [dir=back,arrowtail=empty];"); + getClassInfo(className).addRelation(sc.toString()); + } // Print generalizations (through @extends tags) for (Tag tag : c.tags("extends")) - if (!hidden(tag.text())) + if (!hidden(tag.text())) { w.println("\t//" + c + " extends " + tag.text() + "\n" + "\t" + getNodeName(tag.text()) + " -> " + cs + " [dir=back,arrowtail=empty];"); + getClassInfo(className).addRelation(tag.text()); + } // Print realizations (Java interfaces) - for (Type iface : c.interfaceTypes()) - if (!hidden(iface.asClassDoc())) - w.println("\t" + getNodeName(iface.asClassDoc()) + " -> " + cs + " [dir=back,arrowtail=empty,style=dashed];" + - "\t//" + c + " implements " + iface.asClassDoc()); + for (Type iface : c.interfaceTypes()) { + ClassDoc ic = iface.asClassDoc(); + if (!hidden(ic)) { + w.println("\t" + getNodeName(ic) + " -> " + cs + " [dir=back,arrowtail=empty,style=dashed];" + + "\t//" + c + " implements " + ic); + getClassInfo(className).addRelation(ic.toString()); + } + } // Print other associations - relation(opt, "assoc", c, cs, "arrowhead=none"); - relation(opt, "navassoc", c, cs, "arrowhead=open"); - relation(opt, "has", c, cs, "arrowhead=none, arrowtail=ediamond"); - relation(opt, "composed", c, cs, "arrowhead=none, arrowtail=diamond"); - relation(opt, "depend", c, cs, "arrowhead=open, style=dashed"); + relation(opt, "assoc", c, cs); + relation(opt, "navassoc", c, cs); + relation(opt, "has", c, cs); + relation(opt, "composed", c, cs); + relation(opt, "depend", c, cs); } /** Print classes that were parts of relationships, but not parsed by javadoc */ @@ -543,6 +576,145 @@ class ClassGraph { } } } + + /** + * Prints associations recovered from the fields of a class. An association is inferred only + * if another relation between the two classes is not already in the graph. + * @param classes + */ + public void printInferredRelations(ClassDoc[] classes) { + for (ClassDoc c : classes) { + Options opt = optionProvider.getOptionsFor(c); + + // check if the source is excluded from inference + String sourceName = c.toString(); + if(hidden(c) || opt.machesHideInferExpression(sourceName)) + continue; + + for (FieldDoc field : c.fields()) { + // skip primitives + FieldRelationInfo fri = getFieldRelationInfo(field); + if(fri == null) + continue; + + // check if the destination is excluded from inference + String dest = fri.cd.toString(); + Options destOpt = optionProvider.getOptionsFor(fri.cd); + if(hidden(fri.cd) || destOpt.machesHideInferExpression(dest)) + continue; + + String destAdornment = fri.multiple ? "*": ""; + relation(opt, "navassoc", sourceName, getNodeName(c), dest, getNodeName(dest), "", "", destAdornment); + } + } + } + + /** + * Prints dependencies recovered from the methods of a class. A + * dependency is inferred only if another relation between the two + * classes is not already in the graph. + * @param classes + */ + public void printInferredDependencies(ClassDoc[] classes) { + for (ClassDoc c : classes) { + Options opt = optionProvider.getOptionsFor(c); + + String sourceName = c.toString(); + if (hidden(c) || opt.machesHideInferExpression(sourceName)) + continue; + + Set types = new HashSet(); + // harvest method return and parameter types + for (MethodDoc method : c.methods()) { + + types.add(method.returnType()); + for (Parameter parameter : method.parameters()) { + types.add(parameter.type()); + } + } + // and the field types + for (FieldDoc field : c.fields()) { + types.add(field.type()); + } + // finally, see if there are some type parameters + if(c.asParameterizedType() != null) { + ParameterizedType pt = c.asParameterizedType(); + types.addAll(Arrays.asList(pt.typeArguments())); + } + + // compute dependencies + for (Type type : types) { + // skip primitives and type variables, as well as dependencies + // on the source class + if(type.isPrimitive() || type instanceof WildcardType || type instanceof TypeVariable || c.equals(type.asClassDoc())) + continue; + + // check if the destination is excluded from inference + ClassDoc fc = type.asClassDoc(); + String destName = fc.toString(); + Options destOpt = optionProvider.getOptionsFor(fc); + if (hidden(fc) || destOpt.machesHideInferExpression(destName)) + continue; + + // if source and dest are not already linked, add a + // dependency + if (!getClassInfo(sourceName).isRelated(destName)) { + relation(opt, "depend", sourceName, getNodeName(sourceName), destName, + getNodeName(destName), "", "", ""); + } + } + + } + } + + + private FieldRelationInfo getFieldRelationInfo(FieldDoc field) { + Type type = field.type(); + if(type.isPrimitive() || type instanceof WildcardType || type instanceof TypeVariable) + return null; + + if (type.dimension().endsWith("[]")) { + return new FieldRelationInfo(type.asClassDoc(), true); + } + + Type[] argTypes = getInterfaceTypeArguments(collectionClassDoc, type); + if(argTypes != null && argTypes.length == 1) { + return new FieldRelationInfo(argTypes[0].asClassDoc(), true); + } + argTypes = getInterfaceTypeArguments(mapClassDoc, type); + if(argTypes != null && argTypes.length == 2) { + return new FieldRelationInfo(argTypes[1].asClassDoc(), true); + } + + return new FieldRelationInfo(type.asClassDoc(), false); + } + + private Type[] getInterfaceTypeArguments(ClassDoc iface, Type t) { + if (t instanceof ParameterizedType) { + ParameterizedType pt = (ParameterizedType) t; + if (iface.equals(t.asClassDoc())) { + return pt.typeArguments(); + } else { + for (Type pti : pt.interfaceTypes()) { + Type[] result = getInterfaceTypeArguments(iface, pti); + if (result != null) + return result; + } + if (pt.superclassType() != null) + return getInterfaceTypeArguments(iface, pt.superclassType()); + } + } else if (t instanceof ClassDoc) { + ClassDoc cd = (ClassDoc) t; + for (Type pti : cd.interfaceTypes()) { + Type[] result = getInterfaceTypeArguments(iface, pti); + if (result != null) + return result; + } + if (cd.superclassType() != null) + return getInterfaceTypeArguments(iface, cd.superclassType()); + } + return null; + } /** Removes the template specs from a class name. */ private String removeTemplate(String name) { @@ -640,4 +812,14 @@ class ClassGraph { w.flush(); w.close(); } + + private static class FieldRelationInfo { + ClassDoc cd; + boolean multiple; + + public FieldRelationInfo(ClassDoc cd, boolean multiple) { + this.cd = cd; + this.multiple = multiple; + } + } } diff --git a/src/org/umlgraph/doclet/ClassGraph.java b/src/org/umlgraph/doclet/ClassGraph.java index c54b6c1..513b842 100644 --- a/src/org/umlgraph/doclet/ClassGraph.java +++ b/src/org/umlgraph/doclet/ClassGraph.java @@ -24,9 +24,7 @@ import com.sun.javadoc.*; import java.io.*; import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.regex.PatternSyntaxException; +import java.util.regex.*; /** * Class graph generation engine @@ -38,6 +36,14 @@ import java.util.regex.PatternSyntaxException; * @author Diomidis Spinellis */ class ClassGraph { + private static Map associationMap = new HashMap(); + static { + associationMap.put("assoc", "arrowhead=none"); + associationMap.put("navassoc", "arrowhead=open"); + associationMap.put("has", "arrowhead=none, arrowtail=ediamond"); + associationMap.put("composed", "arrowhead=none, arrowtail=diamond"); + associationMap.put("depend", "arrowhead=open, style=dashed"); + } private Map classnames = new HashMap(); private String apiDocRoot; private Map apiDocMap = new HashMap(); @@ -49,16 +55,20 @@ class ClassGraph { private Set specifiedPackages; private OptionProvider optionProvider; private PrintWriter w; + private ClassDoc collectionClassDoc; + private ClassDoc mapClassDoc; /** * 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. */ - public ClassGraph(PackageDoc[] packages, OptionProvider optionProvider) throws IOException { + public ClassGraph(RootDoc root, OptionProvider optionProvider) throws IOException { this.optionProvider = optionProvider; + this.collectionClassDoc = root.classNamed("java.util.Collection"); + this.mapClassDoc = root.classNamed("java.util.Map"); specifiedPackages = new HashSet(); - for (PackageDoc p : packages) + for (PackageDoc p : root.specifiedPackages()) specifiedPackages.add(p.name()); Options opt = optionProvider.getGlobalOptions(); apiDocRoot = fixApiDocRoot(opt.apiDocRoot); @@ -452,47 +462,61 @@ class ClassGraph { * Print all relations for a given's class's tag * @param tagname the tag containing the given relation * @param from the source class - * @param name the source class internal name + * @param fromName the source class internal name * @param edgetype the dot edge specification */ - private void relation(Options opt, String tagname, ClassDoc from, String name, String edgetype) { + private void relation(Options opt, String tagname, ClassDoc from, String fromName) { for (Tag tag : from.tags(tagname)) { String t[] = StringUtil.tokenize(tag.text()); // l-src label l-dst target if (t.length != 4) { System.err.println("Error in " + from + "\n" + tagname + " expects four fields (l-src label l-dst target): " + tag.text()); return; } - String destName = t[3]; - String nodeName = null; + String dest = t[3]; + String destName = null; ClassDoc to = from.findClass(t[3]); if(to != null) { - destName = to.toString(); - nodeName = getNodeName(destName); + dest = to.toString(); + destName = getNodeName(dest); } else { - nodeName = getNodeName(t[3]); + destName = getNodeName(t[3]); } - if(hidden(destName)) + if(hidden(dest)) continue; - w.println("\t// " + from + " " + tagname + " " +destName); - w.println("\t" + name + " -> " + nodeName + " [" + - "taillabel=\"" + t[0] + "\", " + - "label=\"" + guillemize(opt, t[1]) + "\", " + - "headlabel=\"" + t[2] + "\", " + - "fontname=\"" + opt.edgeFontName + "\", " + - "fontcolor=\"" + opt.edgeFontColor + "\", " + - "fontsize=" + opt.edgeFontSize + ", " + - "color=\"" + opt.edgeColor + "\", " + - edgetype + "];" - ); + relation(opt, tagname, from.toString(), fromName, dest, destName, t[0], t[1], t[2]); } } + private void relation(Options opt, String tagname, String from, String fromName, + String dest, String destName, String tailLabel, String label, String headLabel) { + // print relation + String edgetype = associationMap.get(tagname); + w.println("\t// " + from + " " + tagname + " " + dest); + w.println("\t" + fromName + " -> " + destName + " [" + + "taillabel=\"" + tailLabel + "\", " + + "label=\"" + guillemize(opt, label) + "\", " + + "headlabel=\"" + headLabel + "\", " + + "fontname=\"" + opt.edgeFontName + "\", " + + "fontcolor=\"" + opt.edgeFontColor + "\", " + + "fontsize=" + opt.edgeFontSize + ", " + + "color=\"" + opt.edgeColor + "\", " + + edgetype + "];" + ); + + // update relation info + getClassInfo(from).addRelation(dest); + if(!tagname.equals("navassoc") && !tagname.equals("depend")) + getClassInfo(dest).addRelation(from); + + } + /** Print a class's relations */ public void printRelations(ClassDoc c) { Options opt = optionProvider.getOptionsFor(c); if (hidden(c)) return; + String className = c.toString(); String cs = getNodeName(c); // Print generalization (through the Java superclass) @@ -500,26 +524,35 @@ class ClassGraph { if (s != null && !s.toString().equals("java.lang.Object") && !c.isEnum() && - !hidden(s.asClassDoc())) + !hidden(s.asClassDoc())) { + ClassDoc sc = s.asClassDoc(); w.println("\t//" + c + " extends " + s + "\n" + - "\t" + getNodeName(s.asClassDoc()) + " -> " + cs + " [dir=back,arrowtail=empty];"); + "\t" + getNodeName(sc) + " -> " + cs + " [dir=back,arrowtail=empty];"); + getClassInfo(className).addRelation(sc.toString()); + } // Print generalizations (through @extends tags) for (Tag tag : c.tags("extends")) - if (!hidden(tag.text())) + if (!hidden(tag.text())) { w.println("\t//" + c + " extends " + tag.text() + "\n" + "\t" + getNodeName(tag.text()) + " -> " + cs + " [dir=back,arrowtail=empty];"); + getClassInfo(className).addRelation(tag.text()); + } // Print realizations (Java interfaces) - for (Type iface : c.interfaceTypes()) - if (!hidden(iface.asClassDoc())) - w.println("\t" + getNodeName(iface.asClassDoc()) + " -> " + cs + " [dir=back,arrowtail=empty,style=dashed];" + - "\t//" + c + " implements " + iface.asClassDoc()); + for (Type iface : c.interfaceTypes()) { + ClassDoc ic = iface.asClassDoc(); + if (!hidden(ic)) { + w.println("\t" + getNodeName(ic) + " -> " + cs + " [dir=back,arrowtail=empty,style=dashed];" + + "\t//" + c + " implements " + ic); + getClassInfo(className).addRelation(ic.toString()); + } + } // Print other associations - relation(opt, "assoc", c, cs, "arrowhead=none"); - relation(opt, "navassoc", c, cs, "arrowhead=open"); - relation(opt, "has", c, cs, "arrowhead=none, arrowtail=ediamond"); - relation(opt, "composed", c, cs, "arrowhead=none, arrowtail=diamond"); - relation(opt, "depend", c, cs, "arrowhead=open, style=dashed"); + relation(opt, "assoc", c, cs); + relation(opt, "navassoc", c, cs); + relation(opt, "has", c, cs); + relation(opt, "composed", c, cs); + relation(opt, "depend", c, cs); } /** Print classes that were parts of relationships, but not parsed by javadoc */ @@ -543,6 +576,145 @@ class ClassGraph { } } } + + /** + * Prints associations recovered from the fields of a class. An association is inferred only + * if another relation between the two classes is not already in the graph. + * @param classes + */ + public void printInferredRelations(ClassDoc[] classes) { + for (ClassDoc c : classes) { + Options opt = optionProvider.getOptionsFor(c); + + // check if the source is excluded from inference + String sourceName = c.toString(); + if(hidden(c) || opt.machesHideInferExpression(sourceName)) + continue; + + for (FieldDoc field : c.fields()) { + // skip primitives + FieldRelationInfo fri = getFieldRelationInfo(field); + if(fri == null) + continue; + + // check if the destination is excluded from inference + String dest = fri.cd.toString(); + Options destOpt = optionProvider.getOptionsFor(fri.cd); + if(hidden(fri.cd) || destOpt.machesHideInferExpression(dest)) + continue; + + String destAdornment = fri.multiple ? "*": ""; + relation(opt, "navassoc", sourceName, getNodeName(c), dest, getNodeName(dest), "", "", destAdornment); + } + } + } + + /** + * Prints dependencies recovered from the methods of a class. A + * dependency is inferred only if another relation between the two + * classes is not already in the graph. + * @param classes + */ + public void printInferredDependencies(ClassDoc[] classes) { + for (ClassDoc c : classes) { + Options opt = optionProvider.getOptionsFor(c); + + String sourceName = c.toString(); + if (hidden(c) || opt.machesHideInferExpression(sourceName)) + continue; + + Set types = new HashSet(); + // harvest method return and parameter types + for (MethodDoc method : c.methods()) { + + types.add(method.returnType()); + for (Parameter parameter : method.parameters()) { + types.add(parameter.type()); + } + } + // and the field types + for (FieldDoc field : c.fields()) { + types.add(field.type()); + } + // finally, see if there are some type parameters + if(c.asParameterizedType() != null) { + ParameterizedType pt = c.asParameterizedType(); + types.addAll(Arrays.asList(pt.typeArguments())); + } + + // compute dependencies + for (Type type : types) { + // skip primitives and type variables, as well as dependencies + // on the source class + if(type.isPrimitive() || type instanceof WildcardType || type instanceof TypeVariable || c.equals(type.asClassDoc())) + continue; + + // check if the destination is excluded from inference + ClassDoc fc = type.asClassDoc(); + String destName = fc.toString(); + Options destOpt = optionProvider.getOptionsFor(fc); + if (hidden(fc) || destOpt.machesHideInferExpression(destName)) + continue; + + // if source and dest are not already linked, add a + // dependency + if (!getClassInfo(sourceName).isRelated(destName)) { + relation(opt, "depend", sourceName, getNodeName(sourceName), destName, + getNodeName(destName), "", "", ""); + } + } + + } + } + + + private FieldRelationInfo getFieldRelationInfo(FieldDoc field) { + Type type = field.type(); + if(type.isPrimitive() || type instanceof WildcardType || type instanceof TypeVariable) + return null; + + if (type.dimension().endsWith("[]")) { + return new FieldRelationInfo(type.asClassDoc(), true); + } + + Type[] argTypes = getInterfaceTypeArguments(collectionClassDoc, type); + if(argTypes != null && argTypes.length == 1) { + return new FieldRelationInfo(argTypes[0].asClassDoc(), true); + } + argTypes = getInterfaceTypeArguments(mapClassDoc, type); + if(argTypes != null && argTypes.length == 2) { + return new FieldRelationInfo(argTypes[1].asClassDoc(), true); + } + + return new FieldRelationInfo(type.asClassDoc(), false); + } + + private Type[] getInterfaceTypeArguments(ClassDoc iface, Type t) { + if (t instanceof ParameterizedType) { + ParameterizedType pt = (ParameterizedType) t; + if (iface.equals(t.asClassDoc())) { + return pt.typeArguments(); + } else { + for (Type pti : pt.interfaceTypes()) { + Type[] result = getInterfaceTypeArguments(iface, pti); + if (result != null) + return result; + } + if (pt.superclassType() != null) + return getInterfaceTypeArguments(iface, pt.superclassType()); + } + } else if (t instanceof ClassDoc) { + ClassDoc cd = (ClassDoc) t; + for (Type pti : cd.interfaceTypes()) { + Type[] result = getInterfaceTypeArguments(iface, pti); + if (result != null) + return result; + } + if (cd.superclassType() != null) + return getInterfaceTypeArguments(iface, cd.superclassType()); + } + return null; + } /** Removes the template specs from a class name. */ private String removeTemplate(String name) { @@ -640,4 +812,14 @@ class ClassGraph { w.flush(); w.close(); } + + private static class FieldRelationInfo { + ClassDoc cd; + boolean multiple; + + public FieldRelationInfo(ClassDoc cd, boolean multiple) { + this.cd = cd; + this.multiple = multiple; + } + } }