mirror of https://github.com/dspinellis/UMLGraph
1079 lines
44 KiB
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("<");
|
|
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(">").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;
|
|
}
|
|
}
|
|
}
|