UMLGraph/src/main/java/org/umlgraph/doclet/ClassGraph.java

1079 lines
44 KiB
Java

/*
* Create a graphviz graph based on the classes in the specified java
* source files.
*
* (C) Copyright 2002-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.
*
*
*/
package org.umlgraph.doclet;
import static org.umlgraph.doclet.StringUtil.buildRelativePathFromClassNames;
import static org.umlgraph.doclet.StringUtil.escape;
import static org.umlgraph.doclet.StringUtil.fmt;
import static org.umlgraph.doclet.StringUtil.guilWrap;
import static org.umlgraph.doclet.StringUtil.guillemize;
import static org.umlgraph.doclet.StringUtil.htmlNewline;
import static org.umlgraph.doclet.StringUtil.removeTemplate;
import static org.umlgraph.doclet.StringUtil.splitPackageClass;
import static org.umlgraph.doclet.StringUtil.tokenize;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.Name;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.TypeParameterElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.NoType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.StandardLocation;
import org.umlgraph.doclet.util.ElementUtil;
import org.umlgraph.doclet.util.TagUtil;
import com.sun.source.util.DocTrees;
import jdk.javadoc.doclet.DocletEnvironment;
/**
* Class graph generation engine
*
* @depend - - - StringUtil
* @depend - - - Options
* @composed - - * ClassInfo
* @has - - - OptionProvider
*
* @version $Revision$
* @author <a href="http://www.spinellis.gr">Diomidis Spinellis</a>
*/
class ClassGraph {
enum Align {
LEFT, CENTER, RIGHT;
public final String lower;
private Align() {
this.lower = toString().toLowerCase();
}
};
private final Elements elementUtils;
private final DocTrees docTrees;
private final Types types;
private final JavaFileManager fileManager;
protected Map<Name, ClassInfo> classnames = new HashMap<>();
protected Set<Name> rootClasses;
protected Map<Name, TypeElement> rootClassdocs = new HashMap<>();
protected OptionProvider optionProvider;
protected PrintWriter w;
protected TypeElement collectionClassDoc;
protected TypeElement 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 final Name contextPackageName;
/**
* Create a new ClassGraph.
* <p>
* The packages passed as an argument are the ones specified on the command
* line.
* </p>
* <p>
* Local URLs will be generated for these packages.
* </p>
*
* @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(DocletEnvironment root, OptionProvider optionProvider, Element contextDoc) {
this.optionProvider = optionProvider;
this.elementUtils = root.getElementUtils();
this.docTrees = root.getDocTrees();
this.types = root.getTypeUtils();
this.fileManager = root.getJavaFileManager();
this.collectionClassDoc = elementUtils.getTypeElement("java.util.Collection");
this.mapClassDoc = elementUtils.getTypeElement("java.util.Map");
// to gather the packages containing specified classes, loop thru them and
// gather
// package definitions. User root.specifiedPackages is not safe, since the user
// may specify just a list of classes (human users usually don't, but automated
// tools do)
rootClasses = new HashSet<>();
for (Element classDoc : root.getIncludedElements()) {
if (classDoc instanceof TypeElement) {
rootClasses.add(((TypeElement) classDoc).getQualifiedName());
rootClassdocs.put(((TypeElement) classDoc).getQualifiedName(), (TypeElement) classDoc);
}
}
// determine the context path, relative to the root
if (contextDoc instanceof TypeElement) {
contextPackageName = ElementUtil.getPackageOf(elementUtils, contextDoc).getQualifiedName();
} else if (contextDoc instanceof PackageElement) {
contextPackageName = ((PackageElement) contextDoc).getQualifiedName();
} else {
contextPackageName = null; // Not available
}
Options opt = optionProvider.getGlobalOptions();
linePrefix = opt.compact ? "" : "\t";
linePostfix = opt.compact ? "" : "\n";
}
/** Return the class's name, possibly by stripping the leading path
* @param list */
private String qualifiedName(Options opt, Name className, List<? extends TypeParameterElement> typeParameters) {
String genericsInfo = "";
if (opt.hideGenerics || typeParameters == null || typeParameters.isEmpty()) {
// nothing to do
} else {
for (int i = 0; i < typeParameters.size(); i++) {
if (i == 0) {
genericsInfo += "<";
}
TypeParameterElement type = typeParameters.get(i);
List<? extends TypeMirror> mirrors = type.getBounds();
if (opt.showQualifiedGenerics && mirrors != null && !mirrors.isEmpty()) {
for (TypeMirror m : mirrors) {
genericsInfo += ElementUtil.getQualifiedName(types, m);
}
} else {
genericsInfo += type.getSimpleName();
}
if (i < typeParameters.size() - 1) {
genericsInfo += ", ";
}
if (i == typeParameters.size() - 1) {
genericsInfo += ">";
}
}
}
if (opt.showQualified) {
return className.toString() + genericsInfo;
}
StringBuilder buf = new StringBuilder(className.length());
qualifiedNameInner(opt, className, buf, 0, !opt.showQualified);
return buf.toString() + genericsInfo;
}
private static int qualifiedNameInner(Options opt, Name r, StringBuilder buf, int last, boolean strip) {
strip = strip && last < r.length() && Character.isLowerCase(r.charAt(last));
for (int i = last; i < r.length(); i++) {
char c = r.charAt(i);
if (c == '.' || c == '$') {
if (strip)
last = i + 1; // skip dot
strip = strip && last < r.length() && Character.isLowerCase(r.charAt(last));
continue;
}
if (Character.isJavaIdentifierPart(c))
continue;
buf.append(r, last, i);
last = i;
// Handle nesting of generics
if (c == '<') {
buf.append('<');
i = last = qualifiedNameInner(opt, r, buf, ++last, !opt.showQualifiedGenerics);
buf.append('>');
} else if (c == '>')
return i + 1;
}
buf.append(r, last, r.length());
return r.length();
}
/**
* Print the visibility adornment of element e prefixed by any stereotypes
*/
private String visibility(Options opt, Element e) {
return opt.showVisibility ? Visibility.get(e).symbol : " ";
}
/** Print the method parameter p */
private String parameter(Options opt, List<? extends VariableElement> params) {
StringBuilder par = new StringBuilder(1000);
for (int i = 0; i < params.size(); i++) {
par.append(params.get(i).getSimpleName() + typeAnnotation(opt, params.get(i).asType()));
if (i + 1 < params.size()) {
par.append(", ");
}
}
return par.toString();
}
/** Print a a basic type t */
private String type(Options opt, TypeMirror t, boolean generics) {
return ((generics ? opt.showQualifiedGenerics : opt.showQualified) ? //
ElementUtil.getQualifiedName(types, t) : ElementUtil.getSimpleName(types, t)) //
+ (opt.hideGenerics ? "" : typeParameters(opt, t instanceof DeclaredType ? (DeclaredType) t : null));
}
/** Print the parameters of the parameterized type t */
private String typeParameters(Options opt, DeclaredType t) {
if (t == null || t.getTypeArguments() == null || t.getTypeArguments().isEmpty()) {
return "";
}
StringBuffer tp = new StringBuffer(1000).append("&lt;");
List<? extends TypeMirror> args = t.getTypeArguments();
for (int i = 0; i < args.size(); i++) {
tp.append(type(opt, args.get(i), true));
if (i != args.size() - 1) {
tp.append(", ");
}
}
return tp.append("&gt;").toString();
}
/** Annotate an field/argument with its type t */
private String typeAnnotation(Options opt, TypeMirror t) {
if (t.getKind() == TypeKind.VOID) {
return "";
}
return " : " + type(opt, t, false) + ElementUtil.dimensions(t);
}
/** Print the class's attributes fd */
private void attributes(Options opt, List<VariableElement> fd) {
for (VariableElement f : fd) {
if (hidden(f)) {
continue;
}
stereotype(opt, f, Align.LEFT);
String att = visibility(opt, f) + f.getSimpleName();
if (opt.showType) {
att += typeAnnotation(opt, f.asType());
}
tableLine(Align.LEFT, att);
tagvalue(opt, f);
}
}
/** Print the class's operations m */
private boolean operations(Options opt, List<ExecutableElement> m) {
boolean printed = false;
for (ExecutableElement md : m) {
if (hidden(md)) {
continue;
}
// Filter-out static initializer method
if (md.getSimpleName().toString().equals("<clinit>") && md.getModifiers().contains(Modifier.STATIC) && Visibility.get(md) == Visibility.PACKAGE) {
continue;
}
Name name = "<init>".equals(md.getSimpleName().toString()) ? ElementUtil.containingTypeElement(md).getSimpleName() : md.getSimpleName();
stereotype(opt, md, Align.LEFT);
String op = visibility(opt, md) + name +
(opt.showType ? "(" + parameter(opt, md.getParameters()) + ")" + typeAnnotation(opt, md.getReturnType()) : "()");
tableLine(Align.LEFT, (md.getModifiers().contains(Modifier.ABSTRACT) ? Font.ABSTRACT : Font.NORMAL).wrap(opt, op));
printed = true;
tagvalue(opt, md);
}
return printed;
}
/** Print the common class node's properties */
private void nodeProperties(Options opt) {
Options def = opt.getGlobalOptions();
if (opt.nodeFontName != def.nodeFontName) {
w.print(",fontname=\"" + opt.nodeFontName + "\"");
}
if (opt.nodeFontColor != def.nodeFontColor) {
w.print(",fontcolor=\"" + opt.nodeFontColor + "\"");
}
if (opt.nodeFontSize != def.nodeFontSize) {
w.print(",fontsize=" + fmt(opt.nodeFontSize));
}
w.print(opt.shape.style);
w.println("];");
}
/**
* Return as a string the tagged values associated with c
*
* @param opt the Options used to guess font names
* @param c the Doc entry to look for @tagvalue
* @param prevterm the termination string for the previous element
* @param term the termination character for each tagged value
*/
private void tagvalue(Options opt, Element c) {
List<String> tags = TagUtil.getTag(docTrees, c, "tagvalue");
if (tags.isEmpty()) {
return;
}
for (String tag : tags) {
String t[] = tokenize(tag);
if (t.length != 2) {
System.err.println("@tagvalue expects two fields: " + tag);
continue;
}
tableLine(Align.RIGHT, Font.TAG.wrap(opt, "{" + t[0] + " = " + t[1] + "}"));
}
}
/**
* Return as a string the stereotypes associated with c terminated by the escape character term
*/
private void stereotype(Options opt, Element c, Align align) {
List<String> tags = TagUtil.getTag(docTrees, c, "stereotype");
if (tags.isEmpty()) {
return;
}
for (String tag : tags) {
String t[] = tokenize(tag);
if (t.length != 1) {
System.err.println("@stereotype expects one field: " + tag);
continue;
}
tableLine(align, guilWrap(opt, t[0]));
}
}
/** Return true if c has a @hidden tag associated with it */
private boolean hidden(Element c) {
Map<String, List<String>> tags = TagUtil.getTags(docTrees, c);
if (tags.get("hidden") != null || tags.get("view") != null) {
return true;
}
Options opt = optionProvider.getOptionsFor(docTrees, c instanceof TypeElement ? (TypeElement) c : ElementUtil.containingTypeElement(c));
return opt.matchesHideExpression(c.getSimpleName()) //
|| (opt.hidePrivateInner && c instanceof TypeElement && c.getModifiers().contains(Modifier.PRIVATE)
&& ((TypeElement) c).getEnclosingElement() != null);
}
protected ClassInfo getClassInfo(TypeElement cd, boolean create) {
return getClassInfo(cd, cd.getQualifiedName(), create);
}
protected ClassInfo getClassInfo(Name className, boolean create) {
return getClassInfo(null, className, create);
}
protected ClassInfo getClassInfo(TypeElement cd, Name className, boolean create) {
className = removeTemplate(elementUtils, className);
ClassInfo ci = classnames.get(className);
if (ci == null && create) {
boolean hidden = cd != null ? hidden(cd)
: optionProvider.getOptionsFor(className).matchesHideExpression(className);
ci = new ClassInfo(hidden);
classnames.put(className, ci);
}
return ci;
}
/**
* Return true if the class name is associated to an hidden class or matches a
* hide expression
*/
private boolean hidden(CharSequence className) {
ClassInfo ci = classnames.get(className);
return ci != null ? ci.hidden : optionProvider.getOptionsFor(className).matchesHideExpression(className);
}
/**
* Prints the class if needed.
* <p>
* A class is a rootClass if it's included among the classes returned by
* RootDoc.classes(), this information is used to properly compute relative
* links in diagrams for UMLDoc
*/
public String printClass(TypeElement c, boolean rootClass) {
ClassInfo ci = getClassInfo(c, true);
if (ci.nodePrinted || ci.hidden)
return ci.name;
Options opt = optionProvider.getOptionsFor(docTrees, c);
if (c.getKind() == ElementKind.ENUM && !opt.showEnumerations) {
return ci.name;
}
String className = c.toString();
// Associate classname's alias
w.println(linePrefix + "// " + className);
// Create label
w.print(linePrefix + ci.name + " [label=");
boolean showMembers = (opt.showAttributes && !ElementUtil.getFields(c).isEmpty())
|| (c.getKind() == ElementKind.ENUM && opt.showEnumConstants && !ElementUtil.getEnumConstants(c).isEmpty())
|| (opt.showOperations && !ElementUtil.getMethods(c).isEmpty())
|| (opt.showConstructors && !ElementUtil.getConstructors(c).isEmpty());
final String url = classToUrl(c, rootClass);
externalTableStart(opt, c.getQualifiedName(), url);
firstInnerTableStart(opt);
if (c.getKind() == ElementKind.INTERFACE) {
tableLine(Align.CENTER, guilWrap(opt, "interface"));
}
if (c.getKind() == ElementKind.ENUM) {
tableLine(Align.CENTER, guilWrap(opt, "enumeration"));
}
stereotype(opt, c, Align.CENTER);
Font font = c.getModifiers().contains(Modifier.ABSTRACT) && c.getKind() != ElementKind.INTERFACE ? Font.CLASS_ABSTRACT : Font.CLASS;
String qualifiedName = qualifiedName(opt, c.getQualifiedName(), c.getTypeParameters());
int idx = splitPackageClass(qualifiedName);
if (opt.showComment) {
tableLine(Align.LEFT, Font.CLASS.wrap(opt, htmlNewline(escape(TagUtil.getComment(docTrees, c)))));
} else if (opt.postfixPackage && idx > 0 && idx < (qualifiedName.length() - 1)) {
String packageName = qualifiedName.substring(0, idx);
String cn = qualifiedName.substring(idx + 1);
tableLine(Align.CENTER, font.wrap(opt, escape(cn)));
tableLine(Align.CENTER, Font.PACKAGE.wrap(opt, packageName));
} else {
tableLine(Align.CENTER, font.wrap(opt, escape(qualifiedName)));
}
tagvalue(opt, c);
firstInnerTableEnd(opt);
/*
* Warning: The boolean expressions guarding innerTableStart() in this block,
* should match those in the code block above marked:
* "Calculate the number of innerTable rows we will emmit"
*/
if (showMembers) {
if (opt.showAttributes) {
innerTableStart();
List<VariableElement> fields = ElementUtil.getFields(c);
// if there are no fields, print an empty line to generate proper HTML
if (fields.size() == 0) {
tableLine(Align.LEFT, "");
} else {
attributes(opt, fields);
}
innerTableEnd();
} else if (c.getKind() != ElementKind.ENUM && (opt.showConstructors || opt.showOperations)) {
// show an emtpy box if we don't show attributes but
// we show operations
innerTableStart();
tableLine(Align.LEFT, "");
innerTableEnd();
}
if (c.getKind() == ElementKind.ENUM && opt.showEnumConstants) {
innerTableStart();
List<VariableElement> ecs = ElementUtil.getEnumConstants(c);
// if there are no constants, print an empty line to generate proper HTML
if (ecs.size() == 0) {
tableLine(Align.LEFT, "");
} else {
for (VariableElement fd : ecs) {
tableLine(Align.LEFT, fd.getSimpleName());
}
}
innerTableEnd();
}
if (c.getKind() != ElementKind.ENUM && (opt.showConstructors || opt.showOperations)) {
innerTableStart();
boolean printedLines = false;
if (opt.showConstructors) {
printedLines |= operations(opt, ElementUtil.getConstructors(c));
}
if (opt.showOperations) {
printedLines |= operations(opt, ElementUtil.getMethods(c));
}
if (!printedLines) {
// if there are no operations nor constructors,
// print an empty line to generate proper HTML
tableLine(Align.LEFT, "");
}
innerTableEnd();
}
}
externalTableEnd();
if (url != null) {
w.print(", URL=\"" + url + "\"");
}
nodeProperties(opt);
// If needed, add a note for this node
int ni = 0;
List<String> tags = TagUtil.getTag(docTrees, c, "note");
for (String t : tags) {
String noteName = "n" + ni + "c" + ci.name;
w.print(linePrefix + "// Note annotation\n");
w.print(linePrefix + noteName + " [label=");
externalTableStart(UmlGraph.getCommentOptions(), c.getQualifiedName(), url);
innerTableStart();
tableLine(Align.LEFT, Font.CLASS.wrap(UmlGraph.getCommentOptions(), htmlNewline(escape(t))));
innerTableEnd();
externalTableEnd();
nodeProperties(UmlGraph.getCommentOptions());
ClassInfo ci1 = getClassInfo(c, true);
w.print(linePrefix + noteName + " -> " + ci1.name + "[arrowhead=none];\n");
ni++;
}
ci.nodePrinted = true;
return ci.name;
}
/**
* Print all relations for a given's class's tag
*
* @param tagname the tag containing the given relation
* @param from the source class
* @param edgetype the dot edge specification
*/
private void allRelation(Options opt, RelationType rt, TypeElement from) {
String tagname = rt.lower;
List<String> tags = TagUtil.getTag(docTrees, from, tagname);
for (String tag : tags) {
String t[] = tokenize(tag); // l-src label l-dst target
t = t.length == 1 ? new String[] { "-", "-", "-", t[0] } : t; // Shorthand
if (t.length != 4) {
System.err.println("Error in " + from + "\n" + tagname
+ " expects four fields (l-src label l-dst target): " + tag);
return;
}
TypeElement to = elementUtils.getTypeElement(t[3]);
if (to != null) {
if (hidden(to)) {
continue;
}
relation(opt, rt, from, to, t[0], t[1], t[2]);
} else {
Name t3 = elementUtils.getName(t[3]);
if (hidden(t3)) {
continue;
}
relation(opt, rt, from, from.getQualifiedName(), to, t3, t[0], t[1], t[2]);
}
}
}
/**
* Print the specified relation
*
* @param from the source class (may be null)
* @param fromName the source class's name
* @param to the destination class (may be null)
* @param toName the destination class's name
*/
private void relation(Options opt, RelationType rt, TypeElement from, Name fromName, TypeElement to, Name toName,
String tailLabel, String label, String headLabel) {
tailLabel = (tailLabel != null && !tailLabel.isEmpty()) ? ",taillabel=\"" + tailLabel + "\"" : "";
label = (label != null && !label.isEmpty()) ? ",label=\"" + guillemize(opt, label) + "\"" : "";
headLabel = (headLabel != null && !headLabel.isEmpty()) ? ",headlabel=\"" + headLabel + "\"" : "";
boolean unLabeled = tailLabel.isEmpty() && label.isEmpty() && headLabel.isEmpty();
ClassInfo ci1 = getClassInfo(from, fromName, true), ci2 = getClassInfo(to, toName, true);
String n1 = ci1.name, n2 = ci2.name;
// For ranking we need to output extends/implements backwards.
if (rt.backorder) { // Swap:
n1 = ci2.name;
n2 = ci1.name;
String tmp = tailLabel;
tailLabel = headLabel;
headLabel = tmp;
}
Options def = opt.getGlobalOptions();
// print relation
w.println(linePrefix + "// " + fromName + " " + rt.lower + " " + toName);
w.println(linePrefix + n1 + " -> " + n2 + " [" + rt.style
+ (opt.edgeColor != def.edgeColor ? ",color=\"" + opt.edgeColor + "\"" : "")
+ (unLabeled ? ""
: (opt.edgeFontName != def.edgeFontName ? ",fontname=\"" + opt.edgeFontName + "\"" : "")
+ (opt.edgeFontColor != def.edgeFontColor ? ",fontcolor=\"" + opt.edgeFontColor + "\""
: "")
+ (opt.edgeFontSize != def.edgeFontSize ? ",fontsize=" + fmt(opt.edgeFontSize) : ""))
+ tailLabel + label + headLabel + "];");
// update relation info
RelationDirection d = RelationDirection.BOTH;
if (rt == RelationType.NAVASSOC || rt == RelationType.DEPEND)
d = RelationDirection.OUT;
ci1.addRelation(toName, rt, d);
ci2.addRelation(fromName, rt, d.inverse());
}
/**
* Print the specified relation
*
* @param from the source class
* @param to the destination class
*/
private void relation(Options opt, RelationType rt, TypeElement from, TypeElement to, String tailLabel, String label,
String headLabel) {
relation(opt, rt, from, from.getQualifiedName(), to, to.getQualifiedName(), tailLabel, label, headLabel);
}
/** Print a class's relations */
public void printRelations(TypeElement c) {
Options opt = optionProvider.getOptionsFor(docTrees, c);
if (hidden(c) || "".equals(c.getSimpleName().toString())) { // avoid phantom classes, they may pop up when the source uses annotations
return;
}
// Print generalization (through the Java superclass)
TypeMirror clazz = c.getSuperclass();
Element scd;
if (clazz == null || clazz.getKind() == TypeKind.NONE || !(clazz instanceof DeclaredType)) {
scd = null;
} else {
scd = ((DeclaredType) clazz).asElement();
}
TypeElement s = scd instanceof TypeElement ? (TypeElement) scd : null;
TypeElement sc = s != null && !s.getQualifiedName().toString().equals(Object.class.getName()) ? s : null;
if (sc != null && c.getKind() != ElementKind.ENUM && !hidden(sc)) {
relation(opt, RelationType.EXTENDS, c, sc, null, null, null);
}
// Print generalizations (through @extends tags)
List<String> tags = TagUtil.getTag(docTrees, c, "extends");
for (String tag : tags) {
if (!hidden(tag)) {
relation(opt, RelationType.EXTENDS, c, elementUtils.getTypeElement(tag), null, null, null);
}
}
// Print realizations (Java interfaces)
for (TypeMirror iface : c.getInterfaces()) {
TypeElement ic = ElementUtil.getTypeElement(iface);
if (!hidden(ic)) {
relation(opt, RelationType.IMPLEMENTS, c, ic, null, null, null);
}
}
// Print other associations
allRelation(opt, RelationType.COMPOSED, c);
allRelation(opt, RelationType.NAVCOMPOSED, c);
allRelation(opt, RelationType.HAS, c);
allRelation(opt, RelationType.NAVHAS, c);
allRelation(opt, RelationType.ASSOC, c);
allRelation(opt, RelationType.NAVASSOC, c);
allRelation(opt, RelationType.DEPEND, c);
}
/** Print classes that were parts of relationships, but not parsed by javadoc */
public void printExtraClasses(DocletEnvironment root) {
Set<Name> names = new HashSet<>(classnames.keySet());
for (Name className : names) {
ClassInfo info = getClassInfo(className, true);
if (info.nodePrinted) {
continue;
}
TypeElement c = elementUtils.getTypeElement(className);
if (c != null) {
printClass(c, false);
continue;
}
// Handle missing classes:
Options opt = optionProvider.getOptionsFor(className);
if (opt.matchesHideExpression(className)) {
continue;
}
w.println(linePrefix + "// " + className);
w.print(linePrefix + info.name + "[label=");
externalTableStart(opt, className, classToUrl(className));
innerTableStart();
String qualifiedName = qualifiedName(opt, className, null);
int startTemplate = qualifiedName.indexOf('<');
int idx = qualifiedName.lastIndexOf('.', startTemplate < 0 ? qualifiedName.length() - 1 : startTemplate);
if (opt.postfixPackage && idx > 0 && idx < (qualifiedName.length() - 1)) {
String packageName = qualifiedName.substring(0, idx);
String cn = qualifiedName.substring(idx + 1);
tableLine(Align.CENTER, Font.CLASS.wrap(opt, escape(cn)));
tableLine(Align.CENTER, Font.PACKAGE.wrap(opt, packageName));
} else {
tableLine(Align.CENTER, Font.CLASS.wrap(opt, escape(qualifiedName)));
}
innerTableEnd();
externalTableEnd();
if (className == null || className.length() == 0)
w.print(",URL=\"" + classToUrl(className) + "\"");
nodeProperties(opt);
}
}
/**
* 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(TypeElement c) {
// check if the source is excluded from inference
if (hidden(c)) {
return;
}
Options opt = optionProvider.getOptionsFor(docTrees, c);
for (VariableElement field : ElementUtil.getFields(c)) {
if (hidden(field)) {
continue;
}
// skip statics
if (field.getModifiers().contains(Modifier.STATIC)) {
continue;
}
// skip primitives
FieldRelationInfo fri = getFieldRelationInfo(field);
if (fri == null) {
continue;
}
// check if the destination is excluded from inference
if (hidden(fri.cd)) {
continue;
}
// if source and dest are not already linked, add a dependency
RelationPattern rp = getClassInfo(c, true).getRelation(fri.cd.getQualifiedName());
if (rp == null) {
String destAdornment = fri.multiple ? "*" : "";
relation(opt, opt.inferRelationshipType, c, fri.cd, "", "", destAdornment);
}
}
}
/**
* Returns an array representing the imported classes of c. Disables the
* deprecation warning, which is output, because the imported classed are an
* implementation detail.
*/
List<TypeMirror> importedClasses(TypeElement c) {
JavaFileObject source;
try {
source = fileManager.getJavaFileForInput(StandardLocation.SOURCE_PATH, c.getQualifiedName().toString(), JavaFileObject.Kind.SOURCE);
} catch (IOException e) {
source = null;
}
if (source == null) {
return Collections.emptyList();
}
return Collections.emptyList(); // c.importedClasses();
}
/**
* 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(TypeElement c) {
if (hidden(c)) {
return;
}
Options opt = optionProvider.getOptionsFor(docTrees, c);
Set<TypeMirror> types = new HashSet<>();
// harvest method return and parameter types
for (ExecutableElement method : filterByVisibility(ElementUtil.getMethods(c), opt.inferDependencyVisibility)) {
types.add(method.getReturnType());
for (VariableElement parameter : method.getParameters()) {
types.add(parameter.asType());
}
}
// and the field types
if (!opt.inferRelationships) {
for (VariableElement field : filterByVisibility(ElementUtil.getFields(c), opt.inferDependencyVisibility)) {
types.add(field.asType());
}
}
// see if there are some type parameters
if (c.asType() instanceof DeclaredType) {
DeclaredType pt = (DeclaredType) c.asType();
types.addAll(pt.getTypeArguments());
}
// see if type parameters extend something
for (TypeParameterElement tv : c.getTypeParameters()) {
if (tv.getBounds().size() > 0) {
types.addAll(tv.getBounds());
}
}
// and finally check for explicitly imported classes (this
// assumes there are no unused imports...)
if (opt.useImports) {
types.addAll(importedClasses(c));
}
// compute dependencies
for (TypeMirror type : types) {
// skip primitives and type variables, as well as dependencies
// on the source class
if (ElementUtil.isPrimitive(type)
|| ElementUtil.isType(type, TypeKind.TYPEVAR) || ElementUtil.isType(type, TypeKind.WILDCARD)
|| type instanceof NoType
|| c.toString().equals(ElementUtil.getTypeElement(type).toString())) {
continue;
}
// check if the destination is excluded from inference
TypeElement fc = ElementUtil.getTypeElement(type);
if (hidden(fc)) {
continue;
}
// check if source and destination are in the same package and if we are allowed
// to infer dependencies between classes in the same package
if (!opt.inferDepInPackage && ElementUtil.getPackageOf(elementUtils, c).equals(ElementUtil.getPackageOf(elementUtils, fc))) {
continue;
}
// if source and dest are not already linked, add a dependency
RelationPattern rp = getClassInfo(c, true).getRelation(fc.getQualifiedName());
if (rp == null || rp.matchesOne(new RelationPattern(RelationDirection.OUT))) {
relation(opt, RelationType.DEPEND, c, fc, "", "", "");
}
}
}
/**
* Returns all program element docs that have a visibility greater or equal than
* the specified level
*/
private <T extends Element> List<T> filterByVisibility(List<T> docs, Visibility visibility) {
if (visibility == Visibility.PRIVATE) {
return docs;
}
List<T> filtered = new ArrayList<>();
for (T doc : docs) {
if (Visibility.get(doc).compareTo(visibility) > 0) {
filtered.add(doc);
}
}
return filtered;
}
private FieldRelationInfo getFieldRelationInfo(VariableElement field) {
TypeMirror type = field.asType();
if (ElementUtil.isPrimitive(type) || ElementUtil.isType(type, TypeKind.WILDCARD) || ElementUtil.isType(type, TypeKind.TYPEVAR)) {
return null;
}
if (ElementUtil.dimensions(type).endsWith("[]")) {
return new FieldRelationInfo(ElementUtil.getTypeElement(type), true);
}
Options opt = optionProvider.getOptionsFor(docTrees, ElementUtil.getTypeElement(type));
if (opt.matchesCollPackageExpression(ElementUtil.getQualifiedName(types, type))) {
List<? extends TypeMirror> argTypes = getInterfaceTypeArguments(collectionClassDoc, type);
if (argTypes != null && argTypes.size() == 1 && !argTypes.get(0).getKind().isPrimitive() && !ElementUtil.isType(argTypes.get(0), TypeKind.TYPEVAR)) {
TypeMirror arg = argTypes.get(0);
return new FieldRelationInfo(ElementUtil.getTypeElement(arg), true);
}
argTypes = getInterfaceTypeArguments(mapClassDoc, type);
if (argTypes != null && argTypes.size() == 2 && !argTypes.get(1).getKind().isPrimitive() && !ElementUtil.isType(argTypes.get(1), TypeKind.TYPEVAR)) {
TypeMirror arg = argTypes.get(1);
return new FieldRelationInfo(ElementUtil.getTypeElement(arg), true);
}
}
return new FieldRelationInfo(ElementUtil.getTypeElement(type), false);
}
private List<? extends TypeMirror> getInterfaceTypeArguments(Element iface, TypeMirror t) {
if (t instanceof DeclaredType) {
DeclaredType pt = (DeclaredType) t;
if (iface != null && iface.equals(pt.asElement())) {
return pt.getTypeArguments();
} else {
for (TypeMirror pti : ElementUtil.getInterfacesTypes(pt.asElement())) {
List<? extends TypeMirror> result = getInterfaceTypeArguments(iface, pti);
if (result != null) {
return result;
}
}
TypeMirror superType = ElementUtil.getSuperclassType(pt);
if (superType != null && superType.getKind() != TypeKind.NONE) {
return getInterfaceTypeArguments(iface, superType);
}
}
} else if (iface instanceof TypeElement) {
for (TypeMirror pti : ElementUtil.getInterfacesTypes((TypeElement) iface)) {
List<? extends TypeMirror> result = getInterfaceTypeArguments(iface, pti);
if (result != null) {
return result;
}
}
TypeElement superType = ElementUtil.getSuperclass((TypeElement) iface);
if (superType != null && superType.asType().getKind() != TypeKind.NONE) {
return getInterfaceTypeArguments(iface, superType.asType());
}
}
return null;
}
/** Convert the class name into a corresponding URL */
public String classToUrl(TypeElement cd, boolean rootClass) {
// building relative path for context and package diagrams
if (contextPackageName != null && rootClass) {
return buildRelativePathFromClassNames(
contextPackageName, ElementUtil.getPackageOf(elementUtils, cd).getQualifiedName().toString()) + cd.getSimpleName() + ".html";
}
return classToUrl(cd.getQualifiedName());
}
/** Convert the class name into a corresponding URL */
public String classToUrl(Name className) {
TypeElement classDoc = rootClassdocs.get(className);
if (classDoc != null) {
String docRoot = optionProvider.getGlobalOptions().apiDocRoot;
if (docRoot == null) {
return null;
}
return new StringBuilder(docRoot.length() + className.length() + 10).append(docRoot) //
.append(ElementUtil.getPackageOf(elementUtils, classDoc).getQualifiedName().toString().replace('.', '/')) //
.append('/').append(classDoc.getSimpleName()).append(".html").toString();
}
String docRoot = optionProvider.getGlobalOptions().getApiDocRoot(className);
if (docRoot == null) {
return null;
}
int split = splitPackageClass(className);
StringBuilder buf = new StringBuilder(docRoot.length() + className.length() + 10).append(docRoot);
if (split > 0) { // Avoid -1, and the extra slash then.
buf.append(className.toString().substring(0, split).replace('.', '/')).append('/');
}
return buf.append(className, Math.min(split + 1, className.length()), className.length()) //
.append(".html").toString();
}
/**
* Dot prologue
*
* @throws IOException
*/
public void prologue() throws IOException {
Options opt = optionProvider.getGlobalOptions();
OutputStream os;
if (opt.outputFileName.equals("-")) {
os = System.out;
} else {
// prepare output file. Use the output file name as a full path unless the
// output
// directory is specified
File file = new File(opt.outputDirectory, opt.outputFileName);
// make sure the output directory are there, otherwise create them
if (file.getParentFile() != null && !file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
os = new FileOutputStream(file);
}
// print prologue
w = new PrintWriter(new OutputStreamWriter(new BufferedOutputStream(os), opt.outputEncoding));
w.println("#!/usr/local/bin/dot\n" + "#\n" + "# Class diagram \n" + "# Generated by UMLGraph version "
+ Version.VERSION + " (http://www.spinellis.gr/umlgraph/)\n" + "#\n\n" + "digraph G {\n" + linePrefix
+ "graph [fontnames=\"svg\"]\n" + linePrefix + "edge [fontname=\"" + opt.edgeFontName + "\",fontsize="
+ fmt(opt.edgeFontSize) + ",labelfontname=\"" + opt.edgeFontName + "\",labelfontsize="
+ fmt(opt.edgeFontSize) + ",color=\"" + opt.edgeColor + "\"];\n" + linePrefix + "node [fontname=\""
+ opt.nodeFontName + "\",fontcolor=\"" + opt.nodeFontColor + "\",fontsize=" + fmt(opt.nodeFontSize)
+ ",shape=plaintext,margin=0,width=0,height=0];");
w.println(linePrefix + "nodesep=" + opt.nodeSep + ";");
w.println(linePrefix + "ranksep=" + opt.rankSep + ";");
if (opt.horizontal) {
w.println(linePrefix + "rankdir=LR;");
}
if (opt.bgColor != null) {
w.println(linePrefix + "bgcolor=\"" + opt.bgColor + "\";\n");
}
}
/** Dot epilogue */
public void epilogue() {
w.println("}\n");
w.flush();
w.close();
}
private void externalTableStart(Options opt, Name name, String url) {
String bgcolor = opt.nodeFillColor == null ? "" : (" bgcolor=\"" + opt.nodeFillColor + "\"");
String href = url == null ? "" : (" href=\"" + url + "\" target=\"_parent\"");
w.print("<<table title=\"" + name + "\" border=\"0\" cellborder=\"" + opt.shape.cellBorder()
+ "\" cellspacing=\"0\" " + "cellpadding=\"2\"" + bgcolor + href + ">" + linePostfix);
}
private void externalTableEnd() {
w.print(linePrefix + linePrefix + "</table>>");
}
private void innerTableStart() {
w.print(linePrefix + linePrefix + "<tr><td><table border=\"0\" cellspacing=\"0\" " + "cellpadding=\"1\">"
+ linePostfix);
}
/**
* Start the first inner table of a class.
*/
private void firstInnerTableStart(Options opt) {
w.print(linePrefix + linePrefix + "<tr>" + opt.shape.extraColumn()
+ "<td><table border=\"0\" cellspacing=\"0\" " + "cellpadding=\"1\">" + linePostfix);
}
private void innerTableEnd() {
w.print(linePrefix + linePrefix + "</table></td></tr>" + linePostfix);
}
/**
* End the first inner table of a class.
*/
private void firstInnerTableEnd(Options opt) {
w.print(linePrefix + linePrefix + "</table></td>" + opt.shape.extraColumn() + "</tr>" + linePostfix);
}
private void tableLine(Align align, CharSequence text) {
w.print(linePrefix + linePrefix //
+ "<tr><td align=\"" + align.lower + "\" balign=\"" + align.lower + "\"> " //
+ text // MAY contain markup!
+ " </td></tr>" + linePostfix);
}
private static class FieldRelationInfo {
TypeElement cd;
boolean multiple;
public FieldRelationInfo(TypeElement cd, boolean multiple) {
this.cd = cd;
this.multiple = multiple;
}
}
}