0001: /*
0002: * Create a graphviz graph based on the classes in the specified java
0003: * source files.
0004: *
0005: * (C) Copyright 2002-2005 Diomidis Spinellis
0006: *
0007: * Permission to use, copy, and distribute this software and its
0008: * documentation for any purpose and without fee is hereby granted,
0009: * provided that the above copyright notice appear in all copies and that
0010: * both that copyright notice and this permission notice appear in
0011: * supporting documentation.
0012: *
0013: * THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR IMPLIED
0014: * WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
0015: * MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
0016: *
0017: * $Id: ClassGraph.java,v 1.95 2007/12/06 07:11:49 dds Exp $
0018: *
0019: */
0020:
0021: package org.umlgraph.doclet;
0022:
0023: import java.io.BufferedOutputStream;
0024: import java.io.File;
0025: import java.io.FileOutputStream;
0026: import java.io.OutputStream;
0027: import java.io.IOException;
0028: import java.io.OutputStreamWriter;
0029: import java.io.PrintWriter;
0030: import java.util.ArrayList;
0031: import java.util.Arrays;
0032: import java.util.HashMap;
0033: import java.util.HashSet;
0034: import java.util.List;
0035: import java.util.Map;
0036: import java.util.Set;
0037: import java.util.regex.Pattern;
0038:
0039: import com.sun.javadoc.ClassDoc;
0040: import com.sun.javadoc.ConstructorDoc;
0041: import com.sun.javadoc.Doc;
0042: import com.sun.javadoc.FieldDoc;
0043: import com.sun.javadoc.MethodDoc;
0044: import com.sun.javadoc.PackageDoc;
0045: import com.sun.javadoc.Parameter;
0046: import com.sun.javadoc.ParameterizedType;
0047: import com.sun.javadoc.ProgramElementDoc;
0048: import com.sun.javadoc.RootDoc;
0049: import com.sun.javadoc.Tag;
0050: import com.sun.javadoc.Type;
0051: import com.sun.javadoc.TypeVariable;
0052: import com.sun.javadoc.WildcardType;
0053:
0054: /**
0055: * Class graph generation engine
0056: * @depend - - - StringUtil
0057: * @depend - - - Options
0058: * @composed - - * ClassInfo
0059: * @has - - - OptionProvider
0060: *
0061: * @version $Revision: 1.95 $
0062: * @author <a href="http://www.spinellis.gr">Diomidis Spinellis</a>
0063: */
0064: class ClassGraph {
0065: protected static final char FILE_SEPARATOR = '/';
0066:
0067: enum Font {
0068: NORMAL, ABSTRACT, CLASS, CLASS_ABSTRACT, TAG, PACKAGE
0069: }
0070:
0071: enum Align {
0072: LEFT, CENTER, RIGHT
0073: };
0074:
0075: public static Map<RelationType, String> associationMap = new HashMap<RelationType, String>();
0076: static {
0077: associationMap.put(RelationType.ASSOC, "arrowhead=none");
0078: associationMap.put(RelationType.NAVASSOC, "arrowhead=open");
0079: associationMap.put(RelationType.HAS,
0080: "arrowhead=none, arrowtail=ediamond");
0081: associationMap.put(RelationType.COMPOSED,
0082: "arrowhead=none, arrowtail=diamond");
0083: associationMap.put(RelationType.DEPEND,
0084: "arrowhead=open, style=dashed");
0085: }
0086: protected Map<String, ClassInfo> classnames = new HashMap<String, ClassInfo>();
0087: protected Set<String> rootClasses;
0088: protected Map<String, ClassDoc> rootClassdocs = new HashMap<String, ClassDoc>();
0089: protected OptionProvider optionProvider;
0090: protected PrintWriter w;
0091: protected ClassDoc collectionClassDoc;
0092: protected ClassDoc mapClassDoc;
0093: protected String linePostfix;
0094: protected String linePrefix;
0095:
0096: // used only when generating context class diagrams in UMLDoc, to generate the proper
0097: // relative links to other classes in the image map
0098: protected Doc contextDoc;
0099:
0100: /**
0101: * Create a new ClassGraph. <p>The packages passed as an
0102: * argument are the ones specified on the command line.</p>
0103: * <p>Local URLs will be generated for these packages.</p>
0104: * @param root The root of docs as provided by the javadoc API
0105: * @param optionProvider The main option provider
0106: * @param contextDoc The current context for generating relative links, may be a ClassDoc
0107: * or a PackageDoc (used by UMLDoc)
0108: */
0109: public ClassGraph(RootDoc root, OptionProvider optionProvider,
0110: Doc contextDoc) {
0111: this .optionProvider = optionProvider;
0112: this .collectionClassDoc = root
0113: .classNamed("java.util.Collection");
0114: this .mapClassDoc = root.classNamed("java.util.Map");
0115: this .contextDoc = contextDoc;
0116:
0117: // to gather the packages containing specified classes, loop thru them and gather
0118: // package definitions. User root.specifiedPackages is not safe, since the user
0119: // may specify just a list of classes (human users usually don't, but automated tools do)
0120: rootClasses = new HashSet<String>();
0121: for (ClassDoc classDoc : root.classes()) {
0122: rootClasses.add(classDoc.qualifiedName());
0123: rootClassdocs.put(classDoc.qualifiedName(), classDoc);
0124: }
0125:
0126: Options opt = optionProvider.getGlobalOptions();
0127: if (opt.compact) {
0128: linePrefix = "";
0129: linePostfix = "";
0130: } else {
0131: linePrefix = "\t";
0132: linePostfix = "\n";
0133: }
0134: }
0135:
0136: /** Return the class's name, possibly by stripping the leading path */
0137: private String qualifiedName(Options opt, String r) {
0138: if (!opt.showQualified) {
0139: // Create readable string by stripping leading path
0140: for (;;) {
0141: int dotpos = r.lastIndexOf('.');
0142: if (dotpos == -1)
0143: break; // Work done!
0144: /*
0145: * Change all occurences of
0146: * "p1.p2.myClass<S extends dummy.Otherclass>" into
0147: * "myClass<S extends Otherclass>"
0148: */
0149: int start = dotpos;
0150: while (start > 0
0151: && Character.isJavaIdentifierPart(r
0152: .charAt(start - 1)))
0153: start--;
0154: r = r.substring(0, start) + r.substring(dotpos + 1);
0155: }
0156: }
0157: return r;
0158: }
0159:
0160: /**
0161: * Escape <, >, and & characters in the string with
0162: * the corresponding HTML entity code.
0163: */
0164: private String escape(String s) {
0165: final Pattern toEscape = Pattern.compile("[&<>]");
0166:
0167: if (toEscape.matcher(s).find()) {
0168: StringBuffer sb = new StringBuffer(s);
0169: for (int i = 0; i < sb.length();) {
0170: switch (sb.charAt(i)) {
0171: case '&':
0172: sb.replace(i, i + 1, "&");
0173: i += "&".length();
0174: break;
0175: case '<':
0176: sb.replace(i, i + 1, "<");
0177: i += "<".length();
0178: break;
0179: case '>':
0180: sb.replace(i, i + 1, ">");
0181: i += ">".length();
0182: break;
0183: default:
0184: i++;
0185: }
0186: }
0187: return sb.toString();
0188: } else
0189: return s;
0190: }
0191:
0192: /**
0193: * Convert embedded newlines into HTML line breaks
0194: */
0195: private String htmlNewline(String s) {
0196: if (s.indexOf('\n') == -1)
0197: return (s);
0198:
0199: StringBuffer sb = new StringBuffer(s);
0200: for (int i = 0; i < sb.length();) {
0201: if (sb.charAt(i) == '\n') {
0202: sb.replace(i, i + 1, "<br/>");
0203: i += "<br/>".length();
0204: } else
0205: i++;
0206: }
0207: return sb.toString();
0208: }
0209:
0210: /**
0211: * Convert < and > characters in the string to the respective guillemot characters.
0212: */
0213: private String guillemize(Options opt, String s) {
0214: StringBuffer r = new StringBuffer(s);
0215: for (int i = 0; i < r.length();)
0216: switch (r.charAt(i)) {
0217: case '<':
0218: r.replace(i, i + 1, opt.guilOpen);
0219: i += opt.guilOpen.length();
0220: break;
0221: case '>':
0222: r.replace(i, i + 1, opt.guilClose);
0223: i += opt.guilClose.length();
0224: break;
0225: default:
0226: i++;
0227: break;
0228: }
0229: return r.toString();
0230: }
0231:
0232: /**
0233: * Wraps a string in Guillemot (or an ASCII substitute) characters.
0234: *
0235: * @param str the <code>String</code> to be wrapped.
0236: * @return the wrapped <code>String</code>.
0237: */
0238: private String guilWrap(Options opt, String str) {
0239: return opt.guilOpen + str + opt.guilClose;
0240: }
0241:
0242: /**
0243: * Print the visibility adornment of element e prefixed by
0244: * any stereotypes
0245: */
0246: private String visibility(Options opt, ProgramElementDoc e) {
0247: if (!opt.showVisibility)
0248: return " ";
0249: if (e.isPrivate())
0250: return "- ";
0251: else if (e.isPublic())
0252: return "+ ";
0253: else if (e.isProtected())
0254: return "# ";
0255: else if (e.isPackagePrivate())
0256: return "~ ";
0257: else
0258: return " ";
0259: }
0260:
0261: /** Print the method parameter p */
0262: private String parameter(Options opt, Parameter p[]) {
0263: String par = "";
0264: for (int i = 0; i < p.length; i++) {
0265: par += p[i].name() + typeAnnotation(opt, p[i].type());
0266: if (i + 1 < p.length)
0267: par += ", ";
0268: }
0269: return par;
0270: }
0271:
0272: /** Print a a basic type t */
0273: private String type(Options opt, Type t) {
0274: String type = "";
0275: if (opt.showQualified)
0276: type = t.qualifiedTypeName();
0277: else
0278: type = t.typeName();
0279: type += typeParameters(opt, t.asParameterizedType());
0280: return type;
0281: }
0282:
0283: /** Print the parameters of the parameterized type t */
0284: private String typeParameters(Options opt, ParameterizedType t) {
0285: String tp = "";
0286: if (t == null)
0287: return tp;
0288: Type args[] = t.typeArguments();
0289: tp += "<";
0290: for (int i = 0; i < args.length; i++) {
0291: tp += type(opt, args[i]);
0292: if (i != args.length - 1)
0293: tp += ", ";
0294: }
0295: tp += ">";
0296: return tp;
0297: }
0298:
0299: /** Annotate an field/argument with its type t */
0300: private String typeAnnotation(Options opt, Type t) {
0301: String ta = "";
0302: if (t.typeName().equals("void"))
0303: return ta;
0304: ta += " : ";
0305: ta += type(opt, t);
0306: ta += t.dimension();
0307: return ta;
0308: }
0309:
0310: /** Print the class's attributes fd */
0311: private void attributes(Options opt, FieldDoc fd[]) {
0312: for (FieldDoc f : fd) {
0313: if (hidden(f))
0314: continue;
0315: String att = "";
0316: stereotype(opt, f, Align.LEFT);
0317: att = visibility(opt, f) + f.name();
0318: if (opt.showType)
0319: att += typeAnnotation(opt, f.type());
0320: tableLine(Align.LEFT, att);
0321: tagvalue(opt, f);
0322: }
0323: }
0324:
0325: /*
0326: * The following two methods look similar, but can't
0327: * be refactored into one, because their common interface,
0328: * ExecutableMemberDoc, doesn't support returnType for ctors.
0329: */
0330:
0331: /** Print the class's constructors m */
0332: private boolean operations(Options opt, ConstructorDoc m[]) {
0333: boolean printed = false;
0334: for (ConstructorDoc cd : m) {
0335: if (hidden(cd))
0336: continue;
0337: stereotype(opt, cd, Align.LEFT);
0338: String cs = visibility(opt, cd) + cd.name();
0339: if (opt.showType) {
0340: cs += "(" + parameter(opt, cd.parameters()) + ")";
0341: } else {
0342: cs += "()";
0343: }
0344: tableLine(Align.LEFT, cs);
0345: printed = true;
0346: tagvalue(opt, cd);
0347: }
0348: return printed;
0349: }
0350:
0351: /** Print the class's operations m */
0352: private boolean operations(Options opt, MethodDoc m[]) {
0353: boolean printed = false;
0354: for (MethodDoc md : m) {
0355: if (hidden(md))
0356: continue;
0357: // Filter-out static initializer method
0358: if (md.name().equals("<clinit>") && md.isStatic()
0359: && md.isPackagePrivate())
0360: continue;
0361: stereotype(opt, md, Align.LEFT);
0362: String op = visibility(opt, md) + md.name();
0363: if (opt.showType) {
0364: op += "(" + parameter(opt, md.parameters()) + ")"
0365: + typeAnnotation(opt, md.returnType());
0366: } else {
0367: op += "()";
0368: }
0369: tableLine(Align.LEFT, op, opt,
0370: md.isAbstract() ? Font.ABSTRACT : Font.NORMAL);
0371: printed = true;
0372:
0373: tagvalue(opt, md);
0374: }
0375: return printed;
0376: }
0377:
0378: /** Print the common class node's properties */
0379: private void nodeProperties(Options opt) {
0380: w.print(", fontname=\"" + opt.nodeFontName + "\"");
0381: w.print(", fontcolor=\"" + opt.nodeFontColor + "\"");
0382: w.print(", fontsize=" + opt.nodeFontSize);
0383: w.print(opt.shape.graphvizAttribute());
0384: w.println("];");
0385: }
0386:
0387: /**
0388: * Return as a string the tagged values associated with c
0389: * @param opt the Options used to guess font names
0390: * @param c the Doc entry to look for @tagvalue
0391: * @param prevterm the termination string for the previous element
0392: * @param term the termination character for each tagged value
0393: */
0394: private void tagvalue(Options opt, Doc c) {
0395: Tag tags[] = c.tags("tagvalue");
0396: if (tags.length == 0)
0397: return;
0398:
0399: for (Tag tag : tags) {
0400: String t[] = StringUtil.tokenize(tag.text());
0401: if (t.length != 2) {
0402: System.err.println("@tagvalue expects two fields: "
0403: + tag.text());
0404: continue;
0405: }
0406: tableLine(Align.RIGHT, "{" + t[0] + " = " + t[1] + "}",
0407: opt, Font.TAG);
0408: }
0409: }
0410:
0411: /**
0412: * Return as a string the stereotypes associated with c
0413: * terminated by the escape character term
0414: */
0415: private void stereotype(Options opt, Doc c, Align align) {
0416: for (Tag tag : c.tags("stereotype")) {
0417: String t[] = StringUtil.tokenize(tag.text());
0418: if (t.length != 1) {
0419: System.err.println("@stereotype expects one field: "
0420: + tag.text());
0421: continue;
0422: }
0423: tableLine(align, guilWrap(opt, t[0]));
0424: }
0425: }
0426:
0427: /** Return true if c has a @hidden tag associated with it */
0428: private boolean hidden(ProgramElementDoc c) {
0429: Tag tags[] = c.tags("hidden");
0430: if (tags.length > 0)
0431: return true;
0432: tags = c.tags("view");
0433: if (tags.length > 0)
0434: return true;
0435: Options opt;
0436: if (c instanceof ClassDoc)
0437: opt = optionProvider.getOptionsFor((ClassDoc) c);
0438: else
0439: opt = optionProvider.getOptionsFor(c.containingClass());
0440: return opt.matchesHideExpression(c.toString());
0441: }
0442:
0443: protected ClassInfo getClassInfo(String className) {
0444: return classnames.get(removeTemplate(className));
0445: }
0446:
0447: private ClassInfo newClassInfo(String className, boolean printed,
0448: boolean hidden) {
0449: ClassInfo ci = new ClassInfo(printed, hidden);
0450: classnames.put(removeTemplate(className), ci);
0451: return ci;
0452: }
0453:
0454: /** Return true if the class name is associated to an hidden class or matches a hide expression */
0455: private boolean hidden(String s) {
0456: ClassInfo ci = getClassInfo(s);
0457: Options opt = optionProvider.getOptionsFor(s);
0458: if (ci != null)
0459: return ci.hidden || opt.matchesHideExpression(s);
0460: else
0461: return opt.matchesHideExpression(s);
0462: }
0463:
0464: /**
0465: * Prints the class if needed.
0466: * <p>
0467: * A class is a rootClass if it's included among the classes returned by
0468: * RootDoc.classes(), this information is used to properly compute
0469: * relative links in diagrams for UMLDoc
0470: */
0471: public String printClass(ClassDoc c, boolean rootClass) {
0472: ClassInfo ci;
0473: boolean toPrint;
0474: Options opt = optionProvider.getOptionsFor(c);
0475:
0476: String className = c.toString();
0477: if ((ci = getClassInfo(className)) != null)
0478: toPrint = !ci.nodePrinted;
0479: else {
0480: toPrint = true;
0481: ci = newClassInfo(className, true, hidden(c));
0482: }
0483: if (toPrint && !hidden(c)
0484: && (!c.isEnum() || opt.showEnumerations)) {
0485: // Associate classname's alias
0486: String r = className;
0487: w.println("\t// " + r);
0488: // Create label
0489: w.print("\t" + ci.name + " [label=");
0490:
0491: boolean showMembers = (opt.showAttributes && c.fields().length > 0)
0492: || (c.isEnum() && opt.showEnumConstants && c
0493: .enumConstants().length > 0)
0494: || (opt.showOperations && c.methods().length > 0)
0495: || (opt.showConstructors && c.constructors().length > 0);
0496:
0497: externalTableStart(opt, c.qualifiedName(), classToUrl(c,
0498: rootClass));
0499:
0500: // Calculate the number of innerTable rows we will emmit
0501: int nRows = 1;
0502: if (showMembers) {
0503: if (opt.showAttributes)
0504: nRows++;
0505: else if (!c.isEnum()
0506: && (opt.showConstructors || opt.showOperations))
0507: nRows++;
0508: if (c.isEnum() && opt.showEnumConstants)
0509: nRows++;
0510: if (!c.isEnum()
0511: && (opt.showConstructors || opt.showOperations))
0512: nRows++;
0513: }
0514:
0515: firstInnerTableStart(opt, nRows);
0516: if (c.isInterface())
0517: tableLine(Align.CENTER, guilWrap(opt, "interface"));
0518: if (c.isEnum())
0519: tableLine(Align.CENTER, guilWrap(opt, "enumeration"));
0520: stereotype(opt, c, Align.CENTER);
0521: Font font = c.isAbstract() && !c.isInterface() ? Font.CLASS_ABSTRACT
0522: : Font.CLASS;
0523: String qualifiedName = qualifiedName(opt, r);
0524: int startTemplate = qualifiedName.indexOf('<');
0525: int idx = 0;
0526: if (startTemplate < 0)
0527: idx = qualifiedName.lastIndexOf('.');
0528: else
0529: idx = qualifiedName.lastIndexOf('.', startTemplate);
0530: if (opt.showComment)
0531: tableLine(Align.LEFT, htmlNewline(escape(c
0532: .commentText())), opt, Font.CLASS);
0533: else if (opt.postfixPackage && idx > 0
0534: && idx < (qualifiedName.length() - 1)) {
0535: String packageName = qualifiedName.substring(0, idx);
0536: String cn = className.substring(idx + 1);
0537: tableLine(Align.CENTER, escape(cn), opt, font);
0538: tableLine(Align.CENTER, packageName, opt, Font.PACKAGE);
0539: } else {
0540: tableLine(Align.CENTER, escape(qualifiedName), opt,
0541: font);
0542: }
0543: tagvalue(opt, c);
0544: firstInnerTableEnd(opt, nRows);
0545:
0546: /*
0547: * Warning: The boolean expressions guarding innerTableStart()
0548: * in this block, should match those in the code block above
0549: * marked: "Calculate the number of innerTable rows we will emmit"
0550: */
0551: if (showMembers) {
0552: if (opt.showAttributes) {
0553: innerTableStart();
0554: FieldDoc[] fields = c.fields();
0555: // if there are no fields, print an empty line to generate proper HTML
0556: if (fields.length == 0)
0557: tableLine(Align.LEFT, "");
0558: else
0559: attributes(opt, c.fields());
0560: innerTableEnd();
0561: } else if (!c.isEnum()
0562: && (opt.showConstructors || opt.showOperations)) {
0563: // show an emtpy box if we don't show attributes but
0564: // we show operations
0565: innerTableStart();
0566: tableLine(Align.LEFT, "");
0567: innerTableEnd();
0568: }
0569: if (c.isEnum() && opt.showEnumConstants) {
0570: innerTableStart();
0571: FieldDoc[] ecs = c.enumConstants();
0572: // if there are no constants, print an empty line to generate proper HTML
0573: if (ecs.length == 0) {
0574: tableLine(Align.LEFT, "");
0575: } else {
0576: for (FieldDoc fd : c.enumConstants()) {
0577: tableLine(Align.LEFT, fd.name());
0578: }
0579: }
0580: innerTableEnd();
0581: }
0582: if (!c.isEnum()
0583: && (opt.showConstructors || opt.showOperations)) {
0584: innerTableStart();
0585: boolean printedLines = false;
0586: if (opt.showConstructors)
0587: printedLines |= operations(opt, c
0588: .constructors());
0589: if (opt.showOperations)
0590: printedLines |= operations(opt, c.methods());
0591:
0592: if (!printedLines)
0593: // if there are no operations nor constructors,
0594: // print an empty line to generate proper HTML
0595: tableLine(Align.LEFT, "");
0596:
0597: innerTableEnd();
0598: }
0599: }
0600: externalTableEnd();
0601: nodeProperties(opt);
0602:
0603: // If needed, add a note for this node
0604: int ni = 0;
0605: for (Tag t : c.tags("note")) {
0606: String noteName = "n" + ni + "c" + ci.name;
0607: w.print("\t// Note annotation\n");
0608: w.print("\t" + noteName + " [label=");
0609: externalTableStart(UmlGraph.getCommentOptions(), c
0610: .qualifiedName(), classToUrl(c, rootClass));
0611: innerTableStart();
0612: tableLine(Align.LEFT, htmlNewline(escape(t.text())),
0613: UmlGraph.getCommentOptions(), Font.CLASS);
0614: innerTableEnd();
0615: externalTableEnd();
0616: nodeProperties(UmlGraph.getCommentOptions());
0617: w.print("\t" + noteName + " -> " + relationNode(c)
0618: + "[arrowhead=none];\n");
0619: ni++;
0620: }
0621: ci.nodePrinted = true;
0622: }
0623: return ci.name;
0624: }
0625:
0626: private String getNodeName(ClassDoc c) {
0627: String className = c.toString();
0628: ClassInfo ci = getClassInfo(className);
0629: if (ci == null)
0630: ci = newClassInfo(className, false, hidden(c));
0631: return ci.name;
0632: }
0633:
0634: /** Return a class's internal name */
0635: private String getNodeName(String c) {
0636: ClassInfo ci = getClassInfo(c);
0637:
0638: if (ci == null)
0639: ci = newClassInfo(c, false, false);
0640: return ci.name;
0641: }
0642:
0643: /**
0644: * Print all relations for a given's class's tag
0645: * @param tagname the tag containing the given relation
0646: * @param from the source class
0647: * @param edgetype the dot edge specification
0648: */
0649: private void allRelation(Options opt, RelationType rt, ClassDoc from) {
0650: String tagname = rt.toString().toLowerCase();
0651: for (Tag tag : from.tags(tagname)) {
0652: String t[] = StringUtil.tokenize(tag.text()); // l-src label l-dst target
0653: if (t.length != 4) {
0654: System.err
0655: .println("Error in "
0656: + from
0657: + "\n"
0658: + tagname
0659: + " expects four fields (l-src label l-dst target): "
0660: + tag.text());
0661: return;
0662: }
0663: ClassDoc to = from.findClass(t[3]);
0664:
0665: if (to != null) {
0666: if (hidden(to))
0667: continue;
0668: relation(opt, rt, from, to, t[0], t[1], t[2]);
0669: } else {
0670: if (hidden(t[3]))
0671: continue;
0672: relation(opt, rt, from, from.toString(), to, t[3],
0673: t[0], t[1], t[2]);
0674: }
0675: }
0676: }
0677:
0678: /**
0679: * Print the specified relation
0680: * @param from the source class (may be null)
0681: * @param fromName the source class's name
0682: * @param to the destination class (may be null)
0683: * @param toName the destination class's name
0684: */
0685: private void relation(Options opt, RelationType rt, ClassDoc from,
0686: String fromName, ClassDoc to, String toName,
0687: String tailLabel, String label, String headLabel) {
0688:
0689: // print relation
0690: String edgetype = associationMap.get(rt);
0691: w.println("\t// " + fromName + " " + rt.toString() + " "
0692: + toName);
0693: w.println("\t" + relationNode(from, fromName) + " -> "
0694: + relationNode(to, toName) + " [" + "taillabel=\""
0695: + tailLabel + "\", " + "label=\""
0696: + guillemize(opt, label) + "\", " + "headlabel=\""
0697: + headLabel + "\", " + "fontname=\"" + opt.edgeFontName
0698: + "\", " + "fontcolor=\"" + opt.edgeFontColor + "\", "
0699: + "fontsize=" + opt.edgeFontSize + ", " + "color=\""
0700: + opt.edgeColor + "\", " + edgetype + "];");
0701:
0702: // update relation info
0703: RelationDirection d = RelationDirection.BOTH;
0704: if (rt == RelationType.NAVASSOC || rt == RelationType.DEPEND)
0705: d = RelationDirection.OUT;
0706: getClassInfo(fromName).addRelation(toName, rt, d);
0707: getClassInfo(toName).addRelation(fromName, rt, d.inverse());
0708: }
0709:
0710: /**
0711: * Print the specified relation
0712: * @param from the source class
0713: * @param to the destination class
0714: */
0715: private void relation(Options opt, RelationType rt, ClassDoc from,
0716: ClassDoc to, String tailLabel, String label,
0717: String headLabel) {
0718: relation(opt, rt, from, from.toString(), to, to.toString(),
0719: tailLabel, label, headLabel);
0720: }
0721:
0722: /** Return the full name of a relation's node.
0723: * This may involve appending the port :p for the standard nodes
0724: * whose outline is rendered through an inner table.
0725: */
0726: private String relationNode(ClassDoc c) {
0727: Options opt = optionProvider.getOptionsFor(c);
0728: String name = getNodeName(c);
0729: return name + opt.shape.landingPort();
0730: }
0731:
0732: /** Return the full name of a relation's node c.
0733: * This may involve appending the port :p for the standard nodes
0734: * whose outline is rendered through an inner table.
0735: * @param c the node's class (may be null)
0736: * @param cName the node's class name
0737: */
0738: private String relationNode(ClassDoc c, String cName) {
0739: Options opt;
0740: if (c == null)
0741: opt = optionProvider.getOptionsFor(cName);
0742: else
0743: opt = optionProvider.getOptionsFor(c);
0744: String name = getNodeName(cName);
0745: return name + opt.shape.landingPort();
0746: }
0747:
0748: /** Print a class's relations */
0749: public void printRelations(ClassDoc c) {
0750: Options opt = optionProvider.getOptionsFor(c);
0751: if (hidden(c) || c.name().equals("")) // avoid phantom classes, they may pop up when the source uses annotations
0752: return;
0753: String className = c.toString();
0754:
0755: // Print generalization (through the Java superclass)
0756: Type s = c.super classType();
0757: if (s != null && !s.toString().equals("java.lang.Object")
0758: && !c.isEnum() && !hidden(s.asClassDoc())) {
0759: ClassDoc sc = s.asClassDoc();
0760: w.println("\t//" + c + " extends " + s + "\n" + "\t"
0761: + relationNode(sc) + " -> " + relationNode(c)
0762: + " [dir=back,arrowtail=empty];");
0763: getClassInfo(className).addRelation(sc.toString(),
0764: RelationType.EXTENDS, RelationDirection.OUT);
0765: getClassInfo(sc.toString()).addRelation(className,
0766: RelationType.EXTENDS, RelationDirection.IN);
0767: }
0768:
0769: // Print generalizations (through @extends tags)
0770: for (Tag tag : c.tags("extends"))
0771: if (!hidden(tag.text())) {
0772: ClassDoc from = c.findClass(tag.text());
0773: w.println("\t//" + c + " extends " + tag.text() + "\n"
0774: + "\t" + relationNode(from, tag.text())
0775: + " -> " + relationNode(c)
0776: + " [dir=back,arrowtail=empty];");
0777: getClassInfo(className).addRelation(tag.text(),
0778: RelationType.EXTENDS, RelationDirection.OUT);
0779: getClassInfo(tag.text()).addRelation(className,
0780: RelationType.EXTENDS, RelationDirection.IN);
0781: }
0782: // Print realizations (Java interfaces)
0783: for (Type iface : c.interfaceTypes()) {
0784: ClassDoc ic = iface.asClassDoc();
0785: if (!hidden(ic)) {
0786: w.println("\t//" + c + " implements " + ic + "\n\t"
0787: + relationNode(ic) + " -> " + relationNode(c)
0788: + " [dir=back,arrowtail=empty,style=dashed];");
0789: getClassInfo(className).addRelation(ic.toString(),
0790: RelationType.IMPLEMENTS, RelationDirection.OUT);
0791: getClassInfo(ic.toString()).addRelation(className,
0792: RelationType.IMPLEMENTS, RelationDirection.IN);
0793: }
0794: }
0795: // Print other associations
0796: allRelation(opt, RelationType.ASSOC, c);
0797: allRelation(opt, RelationType.NAVASSOC, c);
0798: allRelation(opt, RelationType.HAS, c);
0799: allRelation(opt, RelationType.COMPOSED, c);
0800: allRelation(opt, RelationType.DEPEND, c);
0801: }
0802:
0803: /** Print classes that were parts of relationships, but not parsed by javadoc */
0804: public void printExtraClasses(RootDoc root) {
0805: Set<String> names = new HashSet<String>(classnames.keySet());
0806: for (String className : names) {
0807: ClassInfo info = getClassInfo(className);
0808: if (!info.nodePrinted) {
0809: ClassDoc c = root.classNamed(className);
0810: if (c != null) {
0811: printClass(c, false);
0812: } else {
0813: Options opt = optionProvider
0814: .getOptionsFor(className);
0815: if (opt.matchesHideExpression(className))
0816: continue;
0817: w.println("\t// " + className);
0818: w.print("\t" + info.name + "[label=");
0819: externalTableStart(opt, className,
0820: classToUrl(className));
0821: innerTableStart();
0822: int idx = className.lastIndexOf(".");
0823: if (opt.postfixPackage && idx > 0
0824: && idx < (className.length() - 1)) {
0825: String packageName = className
0826: .substring(0, idx);
0827: String cn = className.substring(idx + 1);
0828: tableLine(Align.CENTER, escape(cn), opt,
0829: Font.CLASS);
0830: tableLine(Align.CENTER, packageName, opt,
0831: Font.PACKAGE);
0832: } else {
0833: tableLine(Align.CENTER, escape(className), opt,
0834: Font.CLASS);
0835: }
0836: innerTableEnd();
0837: externalTableEnd();
0838: nodeProperties(opt);
0839: }
0840: }
0841: }
0842: }
0843:
0844: /**
0845: * Prints associations recovered from the fields of a class. An association is inferred only
0846: * if another relation between the two classes is not already in the graph.
0847: * @param classes
0848: */
0849: public void printInferredRelations(ClassDoc[] classes) {
0850: for (ClassDoc c : classes) {
0851: printInferredRelations(c);
0852: }
0853: }
0854:
0855: /**
0856: * Prints associations recovered from the fields of a class. An association is inferred only
0857: * if another relation between the two classes is not already in the graph.
0858: * @param classes
0859: */
0860: public void printInferredRelations(ClassDoc c) {
0861: Options opt = optionProvider.getOptionsFor(c);
0862:
0863: // check if the source is excluded from inference
0864: if (hidden(c))
0865: return;
0866:
0867: for (FieldDoc field : c.fields(false)) {
0868: // skip statics
0869: if (field.isStatic())
0870: continue;
0871:
0872: // skip primitives
0873: FieldRelationInfo fri = getFieldRelationInfo(field);
0874: if (fri == null)
0875: continue;
0876:
0877: // check if the destination is excluded from inference
0878: if (hidden(fri.cd))
0879: continue;
0880:
0881: String destAdornment = fri.multiple ? "*" : "";
0882: relation(opt, opt.inferRelationshipType, c, fri.cd, "", "",
0883: destAdornment);
0884: }
0885: }
0886:
0887: /**
0888: * Prints dependencies recovered from the methods of a class. A
0889: * dependency is inferred only if another relation between the two
0890: * classes is not already in the graph.
0891: * @param classes
0892: */
0893: public void printInferredDependencies(ClassDoc[] classes) {
0894: for (ClassDoc c : classes) {
0895: printInferredDependencies(c);
0896: }
0897: }
0898:
0899: /**
0900: * Prints dependencies recovered from the methods of a class. A
0901: * dependency is inferred only if another relation between the two
0902: * classes is not already in the graph.
0903: * @param classes
0904: */
0905: public void printInferredDependencies(ClassDoc c) {
0906: Options opt = optionProvider.getOptionsFor(c);
0907:
0908: String sourceName = c.toString();
0909: if (hidden(c))
0910: return;
0911:
0912: Set<Type> types = new HashSet<Type>();
0913: // harvest method return and parameter types
0914: for (MethodDoc method : filterByVisibility(c.methods(false),
0915: opt.inferDependencyVisibility)) {
0916: types.add(method.returnType());
0917: for (Parameter parameter : method.parameters()) {
0918: types.add(parameter.type());
0919: }
0920: }
0921: // and the field types
0922: if (!opt.inferRelationships) {
0923: for (FieldDoc field : filterByVisibility(c.fields(false),
0924: opt.inferDependencyVisibility)) {
0925: types.add(field.type());
0926: }
0927: }
0928: // see if there are some type parameters
0929: if (c.asParameterizedType() != null) {
0930: ParameterizedType pt = c.asParameterizedType();
0931: types.addAll(Arrays.asList(pt.typeArguments()));
0932: }
0933: // see if type parameters extend something
0934: for (TypeVariable tv : c.typeParameters()) {
0935: if (tv.bounds().length > 0)
0936: types.addAll(Arrays.asList(tv.bounds()));
0937: }
0938:
0939: // and finally check for explicitly imported classes (this
0940: // assumes there are no unused imports...)
0941: if (opt.useImports)
0942: types.addAll(Arrays.asList(c.importedClasses()));
0943:
0944: // compute dependencies
0945: for (Type type : types) {
0946: // skip primitives and type variables, as well as dependencies
0947: // on the source class
0948: if (type.isPrimitive()
0949: || type instanceof WildcardType
0950: || type instanceof TypeVariable
0951: || c.toString()
0952: .equals(type.asClassDoc().toString()))
0953: continue;
0954:
0955: // check if the destination is excluded from inference
0956: ClassDoc fc = type.asClassDoc();
0957: if (hidden(fc))
0958: continue;
0959:
0960: // check if source and destination are in the same package and if we are allowed
0961: // to infer dependencies between classes in the same package
0962: if (!opt.inferDepInPackage
0963: && c.containingPackage().equals(
0964: fc.containingPackage()))
0965: continue;
0966:
0967: // if source and dest are not already linked, add a dependency
0968: RelationPattern rp = getClassInfo(sourceName).getRelation(
0969: fc.toString());
0970: if (rp == null
0971: || rp.matchesOne(new RelationPattern(
0972: RelationDirection.OUT))) {
0973: relation(opt, RelationType.DEPEND, c, fc, "", "", "");
0974: }
0975:
0976: }
0977: }
0978:
0979: /**
0980: * Returns all program element docs that have a visibility greater or
0981: * equal than the specified level
0982: */
0983: private <T extends ProgramElementDoc> List<T> filterByVisibility(
0984: T[] docs, Visibility visibility) {
0985: if (visibility == Visibility.PRIVATE)
0986: return Arrays.asList(docs);
0987:
0988: List<T> filtered = new ArrayList<T>();
0989: for (T doc : docs) {
0990: if (Visibility.get(doc).compareTo(visibility) > 0)
0991: filtered.add(doc);
0992: }
0993: return filtered;
0994: }
0995:
0996: private FieldRelationInfo getFieldRelationInfo(FieldDoc field) {
0997: Type type = field.type();
0998: if (type.isPrimitive() || type instanceof WildcardType
0999: || type instanceof TypeVariable)
1000: return null;
1001:
1002: if (type.dimension().endsWith("[]")) {
1003: return new FieldRelationInfo(type.asClassDoc(), true);
1004: }
1005:
1006: Options opt = optionProvider.getOptionsFor(type.asClassDoc());
1007: if (opt.matchesCollPackageExpression(type.qualifiedTypeName())) {
1008: Type[] argTypes = getInterfaceTypeArguments(
1009: collectionClassDoc, type);
1010: if (argTypes != null && argTypes.length == 1
1011: && !argTypes[0].isPrimitive())
1012: return new FieldRelationInfo(argTypes[0].asClassDoc(),
1013: true);
1014:
1015: argTypes = getInterfaceTypeArguments(mapClassDoc, type);
1016: if (argTypes != null && argTypes.length == 2
1017: && !argTypes[1].isPrimitive())
1018: return new FieldRelationInfo(argTypes[1].asClassDoc(),
1019: true);
1020: }
1021:
1022: return new FieldRelationInfo(type.asClassDoc(), false);
1023: }
1024:
1025: private Type[] getInterfaceTypeArguments(ClassDoc iface, Type t) {
1026: if (t instanceof ParameterizedType) {
1027: ParameterizedType pt = (ParameterizedType) t;
1028: if (iface.equals(t.asClassDoc())) {
1029: return pt.typeArguments();
1030: } else {
1031: for (Type pti : pt.interfaceTypes()) {
1032: Type[] result = getInterfaceTypeArguments(iface,
1033: pti);
1034: if (result != null)
1035: return result;
1036: }
1037: if (pt.super classType() != null)
1038: return getInterfaceTypeArguments(iface, pt
1039: .super classType());
1040: }
1041: } else if (t instanceof ClassDoc) {
1042: ClassDoc cd = (ClassDoc) t;
1043: for (Type pti : cd.interfaceTypes()) {
1044: Type[] result = getInterfaceTypeArguments(iface, pti);
1045: if (result != null)
1046: return result;
1047: }
1048: if (cd.super classType() != null)
1049: return getInterfaceTypeArguments(iface, cd
1050: .super classType());
1051: }
1052: return null;
1053: }
1054:
1055: /** Removes the template specs from a class name. */
1056: private String removeTemplate(String name) {
1057: int openIdx = name.indexOf('<');
1058: if (openIdx == -1)
1059: return name;
1060: else
1061: return name.substring(0, openIdx);
1062: }
1063:
1064: /** Convert the class name into a corresponding URL */
1065: public String classToUrl(ClassDoc cd, boolean rootClass) {
1066: // building relative path for context and package diagrams
1067: if (contextDoc != null && rootClass) {
1068: // determine the context path, relative to the root
1069: String packageName = null;
1070: if (contextDoc instanceof ClassDoc) {
1071: packageName = ((ClassDoc) contextDoc)
1072: .containingPackage().name();
1073: } else if (contextDoc instanceof PackageDoc) {
1074: packageName = ((PackageDoc) contextDoc).name();
1075: } else {
1076: return classToUrl(cd.qualifiedName());
1077: }
1078: return buildRelativePath(packageName, cd
1079: .containingPackage().name())
1080: + cd.name() + ".html";
1081: } else {
1082: return classToUrl(cd.qualifiedName());
1083: }
1084: }
1085:
1086: protected static String buildRelativePath(
1087: String contextPackageName, String classPackageName) {
1088: // path, relative to the root, of the destination class
1089: String[] contextClassPath = contextPackageName.split("\\.");
1090: String[] currClassPath = classPackageName.split("\\.");
1091:
1092: // compute relative path between the context and the destination
1093: // ... first, compute common part
1094: int i = 0;
1095: while (i < contextClassPath.length && i < currClassPath.length
1096: && contextClassPath[i].equals(currClassPath[i]))
1097: i++;
1098: // ... go up with ".." to reach the common root
1099: StringBuffer buf = new StringBuffer();
1100: if (i == contextClassPath.length) {
1101: buf.append(".").append(FILE_SEPARATOR);
1102: } else {
1103: for (int j = i; j < contextClassPath.length; j++) {
1104: buf.append("..").append(FILE_SEPARATOR);
1105: }
1106: }
1107: // ... go down from the common root to the destination
1108: for (int j = i; j < currClassPath.length; j++) {
1109: buf.append(currClassPath[j]).append(FILE_SEPARATOR);
1110: }
1111: return buf.toString();
1112: }
1113:
1114: private String getPackageName(String className) {
1115: if (this .rootClassdocs.get(className) == null) {
1116: return className.substring(0, className.lastIndexOf('.'));
1117: } else {
1118: return this .rootClassdocs.get(className)
1119: .containingPackage().name();
1120: }
1121: }
1122:
1123: private String getUnqualifiedName(String className) {
1124: if (this .rootClassdocs.get(className) == null) {
1125: return className.substring(className.lastIndexOf('.') + 1);
1126: } else {
1127: return this .rootClassdocs.get(className).name();
1128: }
1129: }
1130:
1131: /** Convert the class name into a corresponding URL */
1132: public String classToUrl(String className) {
1133: String docRoot = mapApiDocRoot(className);
1134: if (docRoot != null) {
1135: StringBuffer buf = new StringBuffer(docRoot);
1136: buf.append(getPackageName(className).replace('.',
1137: FILE_SEPARATOR)
1138: + FILE_SEPARATOR);
1139: buf.append(getUnqualifiedName(className));
1140: buf.append(".html");
1141: return buf.toString();
1142: } else {
1143: return null;
1144: }
1145: }
1146:
1147: /**
1148: * Returns the appropriate URL "root" for a given class name.
1149: * The root will be used as the prefix of the URL used to link the class in
1150: * the final diagram to the associated JavaDoc page.
1151: */
1152: private String mapApiDocRoot(String className) {
1153: String root = null;
1154: /* If no packages are specified, we use apiDocRoot for all of them. */
1155: if (rootClasses.contains(className)) {
1156: root = optionProvider.getGlobalOptions().apiDocRoot;
1157: } else {
1158: Options globalOptions = optionProvider.getGlobalOptions();
1159: root = globalOptions.getApiDocRoot(className);
1160: }
1161: return root;
1162: }
1163:
1164: /** Dot prologue
1165: * @throws IOException */
1166: public void prologue() throws IOException {
1167: Options opt = optionProvider.getGlobalOptions();
1168: OutputStream os = null;
1169:
1170: if (opt.outputFileName.equals("-"))
1171: os = System.out;
1172: else {
1173: // prepare output file. Use the output file name as a full path unless the output
1174: // directory is specified
1175: File file = null;
1176: if (opt.outputDirectory != null)
1177: file = new File(opt.outputDirectory, opt.outputFileName);
1178: else
1179: file = new File(opt.outputFileName);
1180: // make sure the output directory are there, otherwise create them
1181: if (file.getParentFile() != null
1182: && !file.getParentFile().exists())
1183: file.getParentFile().mkdirs();
1184: os = new FileOutputStream(file);
1185: }
1186:
1187: // print plologue
1188: w = new PrintWriter(new OutputStreamWriter(
1189: new BufferedOutputStream(os), opt.outputEncoding));
1190: w.println("#!/usr/local/bin/dot\n" + "#\n"
1191: + "# Class diagram \n"
1192: + "# Generated by UmlGraph version " + Version.VERSION
1193: + " (http://www.spinellis.gr/sw/umlgraph)\n" + "#\n\n"
1194: + "digraph G {\n" + "\tedge [fontname=\""
1195: + opt.edgeFontName + "\",fontsize=10,labelfontname=\""
1196: + opt.edgeFontName + "\",labelfontsize=10];\n"
1197: + "\tnode [fontname=\"" + opt.nodeFontName
1198: + "\",fontsize=10,shape=plaintext];");
1199: if (opt.horizontal)
1200: w.println("\trankdir=LR;\n\tranksep=1;");
1201: if (opt.bgColor != null)
1202: w.println("\tbgcolor=\"" + opt.bgColor + "\";\n");
1203: }
1204:
1205: /** Dot epilogue */
1206: public void epilogue() {
1207: w.println("}\n");
1208: w.flush();
1209: w.close();
1210: }
1211:
1212: private void externalTableStart(Options opt, String name, String url) {
1213: String bgcolor = "";
1214: if (opt.nodeFillColor != null)
1215: bgcolor = " bgcolor=\"" + opt.nodeFillColor + "\"";
1216: String href = "";
1217: if (url != null)
1218: href = " href=\"" + url + "\"";
1219:
1220: w.print("<<table border=\"0\" cellborder=\""
1221: + opt.shape.cellBorder() + "\" cellspacing=\"0\" "
1222: + "cellpadding=\"2\" port=\"p\"" + bgcolor + href + ">"
1223: + linePostfix);
1224: }
1225:
1226: private void externalTableEnd() {
1227: w.print(linePrefix + linePrefix + "</table>>");
1228: }
1229:
1230: private void innerTableStart() {
1231: w.print(linePrefix + linePrefix
1232: + "<tr><td><table border=\"0\" cellspacing=\"0\" "
1233: + "cellpadding=\"1\">" + linePostfix);
1234: }
1235:
1236: /**
1237: * Start the first inner table of a class.
1238: * @param nRows the total number of rows in this table.
1239: */
1240: private void firstInnerTableStart(Options opt, int nRows) {
1241: w.print(linePrefix + linePrefix + "<tr>"
1242: + opt.shape.extraColumn(nRows)
1243: + "<td><table border=\"0\" cellspacing=\"0\" "
1244: + "cellpadding=\"1\">" + linePostfix);
1245: }
1246:
1247: private void innerTableEnd() {
1248: w.print(linePrefix + linePrefix + "</table></td></tr>"
1249: + linePostfix);
1250: }
1251:
1252: /**
1253: * End the first inner table of a class.
1254: * @param nRows the total number of rows in this table.
1255: */
1256: private void firstInnerTableEnd(Options opt, int nRows) {
1257: w.print(linePrefix + linePrefix + "</table></td>"
1258: + opt.shape.extraColumn(nRows) + "</tr>" + linePostfix);
1259: }
1260:
1261: private void tableLine(Align align, String text) {
1262: tableLine(align, text, null, Font.NORMAL);
1263: }
1264:
1265: private void tableLine(Align align, String text, Options opt,
1266: Font font) {
1267: String open;
1268: String close = "</td></tr>";
1269: String prefix = linePrefix + linePrefix + linePrefix;
1270: String alignText;
1271:
1272: if (align == Align.CENTER)
1273: alignText = "center";
1274: else if (align == Align.LEFT)
1275: alignText = "left";
1276: else if (align == Align.RIGHT)
1277: alignText = "right";
1278: else
1279: throw new RuntimeException("Unknown alignement type "
1280: + align);
1281:
1282: text = fontWrap(" " + text + " ", opt, font);
1283: open = "<tr><td align=\"" + alignText + "\" balign=\""
1284: + alignText + "\">";
1285: w.print(open + text + close + linePostfix);
1286: }
1287:
1288: /**
1289: * Wraps the text with the appropriate font according to the specified font type
1290: * @param opt
1291: * @param text
1292: * @param font
1293: * @return
1294: */
1295: private String fontWrap(String text, Options opt, Font font) {
1296: if (font == Font.ABSTRACT) {
1297: return fontWrap(text, opt.nodeFontAbstractName,
1298: opt.nodeFontSize);
1299: } else if (font == Font.CLASS) {
1300: return fontWrap(text, opt.nodeFontClassName,
1301: opt.nodeFontClassSize);
1302: } else if (font == Font.CLASS_ABSTRACT) {
1303: String name;
1304: if (opt.nodeFontClassAbstractName == null)
1305: name = opt.nodeFontAbstractName;
1306: else
1307: name = opt.nodeFontClassAbstractName;
1308: return fontWrap(text, name, opt.nodeFontClassSize);
1309: } else if (font == Font.PACKAGE) {
1310: return fontWrap(text, opt.nodeFontPackageName,
1311: opt.nodeFontPackageSize);
1312: } else if (font == Font.TAG) {
1313: return fontWrap(text, opt.nodeFontTagName,
1314: opt.nodeFontTagSize);
1315: } else {
1316: return text;
1317: }
1318: }
1319:
1320: /**
1321: * Wraps the text with the appropriate font tags when the font name
1322: * and size are not void
1323: * @param text the text to be wrapped
1324: * @param fontName considered void when it's null
1325: * @param fontSize considered void when it's <= 0
1326: */
1327: private String fontWrap(String text, String fontName,
1328: double fontSize) {
1329: if (fontName == null && fontSize == -1)
1330: return text;
1331: else if (fontName == null)
1332: return "<font point-size=\"" + fontSize + "\">" + text
1333: + "</font>";
1334: else if (fontSize <= 0)
1335: return "<font face=\"" + fontName + "\">" + text
1336: + "</font>";
1337: else
1338: return "<font face=\"" + fontName + "\" point-size=\""
1339: + fontSize + "\">" + text + "</font>";
1340: }
1341:
1342: private static class FieldRelationInfo {
1343: ClassDoc cd;
1344: boolean multiple;
1345:
1346: public FieldRelationInfo(ClassDoc cd, boolean multiple) {
1347: this.cd = cd;
1348: this.multiple = multiple;
1349: }
1350: }
1351: }
|