/* gnu.classpath.tools.gjdoc.DocImpl Copyright (C) 2001 Free Software Foundation, Inc. This file is part of GNU Classpath. GNU Classpath is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2, or (at your option) any later version. GNU Classpath is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with GNU Classpath; see the file COPYING. If not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. */ package gnu.classpath.tools.gjdoc; import com.sun.javadoc.*; import java.util.*; import java.text.*; import java.io.File; import javax.swing.text.Segment; /** * Represents the least common denominator of all Javadoc * comment classes. */ public abstract class DocImpl implements Doc, TagContainer { protected static Tag[] seeTagEmptyArr = new SeeTagImpl[0]; protected static Tag[] linkTagEmptyArr = new LinkTagImpl[0]; protected static Tag[] paramTagEmptyArr = new ParamTagImpl[0]; protected static Tag[] throwsTagEmptyArr = new ThrowsTagImpl[0]; protected SourcePosition position; private String boilerplateComment; // Return the text of the comment for this doc item. public String commentText() { StringBuffer rc=new StringBuffer(); Tag[] textTags=(Tag[])tagMap.get("text"); if (textTags!=null) { for (int i=0; i<textTags.length; ++i) { rc.append(textTags[i].text()); } } return rc.toString(); } // Compares this Object with the specified Object for order. public int compareTo(java.lang.Object o) { return Main.getInstance().getCollator().compare(name(), ((Doc)o).name()); } // Return the first sentence of the comment as tags. public Tag[] firstSentenceTags() { Tag[] rc=(Tag[])tagMap.get("first"); if (rc==null) rc=new Tag[0]; return rc; } // Return the full unprocessed text of the comment. public String getRawCommentText() { if (rawDocumentation!=null) return rawDocumentation; else if (rawDocOffset>=0) return Main.getRootDoc().readRawComment(rawDocOffset); else return null; } // Return comment as tags. public Tag[] inlineTags() { Tag[] rc=(Tag[])tagMap.get("inline"); if (rc==null) rc=new Tag[0]; return rc; } // Is this Doc item a class. public boolean isClass() { return false; } // Is this Doc item a constructor? False until overridden. public boolean isConstructor() { return false; } // Is this Doc item a error class? False until overridden. public boolean isError() { return false; } // Is this Doc item a exception class? False until overridden. public boolean isException() { return false; } // Is this Doc item a field? False until overridden. public boolean isField() { return false; } // return true if this Doc is include in the active set. public boolean isIncluded() { return false; } // Is this Doc item a interface? False until overridden. public boolean isInterface() { return false; } // Is this Doc item a simple method (i.e. public boolean isMethod() { return false; } public boolean isPackage() { return false; } // Is this Doc item a ordinary class (i.e. public boolean isOrdinaryClass() { return false; } // Return the see also tags in this Doc item. public SeeTag[] seeTags() { return (SeeTag[])getTagArr("see", seeTagEmptyArr); } protected Tag[] getTagArr(String kindOfTag, Tag[] defaultRc) { Tag[] rc=(Tag[])tagMap.get(kindOfTag); if (rc==null) rc=defaultRc; return rc; } // Set the full unprocessed text of the comment. public void setRawCommentText(String rawDocumentation) { this.rawDocumentation=rawDocumentation; } public void resolveComments() { if (rawDocumentation!=null && tagMap.isEmpty()) { char[] charArray = rawDocumentation.toCharArray(); int length = rawDocumentation.length(); int startOffset = 0; int endOffset = 0; if (charArray[0] == '/' && charArray[1] == '*' && charArray[2] == '*' && charArray[length - 2] == '*' && charArray[length - 1] == '/') { startOffset = 3; endOffset = 2; } this.tagMap=parseCommentTags(charArray, startOffset, length - endOffset, getContextClass(), getContextMember(), null, boilerplateComment); if (Main.getInstance().isCacheRawComments()) { rawDocOffset=Main.getRootDoc().writeRawComment(rawDocumentation); rawDocumentation=null; } resolveTags(); } else if (tagMap.isEmpty() && null != boilerplateComment) { tagMap.put("all", new Tag[] { new TagImpl("@boilerplate", boilerplateComment,getContextClass(),null) }); tagMap.put("@boilerplate", new Tag[] { new TagImpl("@boilerplate", boilerplateComment,getContextClass(),null) }); } } public static int skipHtmlWhitespace(char[] buffer, int startIndex) { while (startIndex < buffer.length) { char c=buffer[startIndex]; if (!Parser.isWhitespace(c)) { break; } else { ++ startIndex; } } return startIndex; } /** * Looks for an end-of-sentence marker in <code>text</code>, * starting at <code>startIndex</code> and stopping at * <code>endIndex</code>. * * @param text the text to be searched * @param startIndex index in <code>text</code> at which to start * @param endIndex index in <code>text</code> at which to stop * * @return the index of the character following the end-of-sentence * marker, <code>endIndex</code> if no end-of-sentence * marker could be found, or -1 if not implemented. */ private static int findEndOfSentence(char[] text, int startIndex, int endIndex) { if (Main.getInstance().isUseBreakIterator()) { Segment segment = new Segment(text, startIndex, endIndex - startIndex); BreakIterator breakIterator = BreakIterator.getSentenceInstance(Main.getInstance().getLocale()); breakIterator.setText(segment); int result = breakIterator.next(); if (BreakIterator.DONE == result) { return endIndex; } else { return result; } } else { while (startIndex < endIndex) { if (text[startIndex] == '.' && (startIndex+1 == endIndex || Character.isWhitespace(text[startIndex+1]) || isHTMLBreakTag(text, startIndex+1, endIndex) )) { return startIndex; } startIndex++; } return endIndex; } } /** * Returns true is the text from start to end begins with a 'p' or 'br' tag. */ private static boolean isHTMLBreakTag(char[] text, int start, int end) { String[] breakTags = { "p>", "/p>", "h1>", "h2>", "h3>", "h4>", "h5>", "h6>", "hr>", "pre>", "/pre>" }; if (text[start] == '<') { outer: for (int i=0; i<breakTags.length; ++i) { String tag = breakTags[i]; int len = tag.length(); if (start + len < end) { for (int j=0; j<len; ++j) { char c = tag.charAt(j); if (Character.toLowerCase(text[start + 1 + j]) != c) { continue outer; } } return true; } } } return false; } //private static final StringBuffer buf=new StringBuffer(32768); private static final StringBuffer whitespaceBuf=new StringBuffer(); private static char[] charBuf = new char[60000]; private static int bufPos = 0; private static void appendToBuf(char c) { if (bufPos < charBuf.length) { charBuf[bufPos++] = c; } else { // } } private static void appendToBuf(StringBuffer s) { if (bufPos + s.length() <= charBuf.length) { s.getChars(0, s.length(), charBuf, bufPos); bufPos += s.length(); } else { // } } private static void setBufLength(int length) { bufPos = 0; } private static String bufToString() { return new String(charBuf, 0, bufPos); } private static int bufLength() { return bufPos; } public static Map parseCommentTags(char[] comment, int startIndex, int endIndex, ClassDocImpl contextClass, MemberDocImpl contextMember, AbstractTagImpl contextTag, String boilerplateComment) { int rawDocStart=skipHtmlWhitespace(comment, startIndex); int firstSentenceEnd = 0; if (comment.length>rawDocStart) { firstSentenceEnd = findEndOfSentence(comment, rawDocStart, comment.length); if (firstSentenceEnd < 0) { BreakIterator boundary = BreakIterator.getSentenceInstance(Locale.ENGLISH); boundary.setText(new ArrayCharacterIterator(comment, rawDocStart)); boundary.first(); boundary.next(); firstSentenceEnd = boundary.current(); } // Always include period at end of sentence if there is one. if (firstSentenceEnd < comment.length && '.' == comment[firstSentenceEnd]) { ++ firstSentenceEnd; } } final int STATE_BEGOFLINE = 1; final int STATE_TEXT = 2; final int STATE_PARAM = 3; final int STATE_PARAMVALUE = 4; final int STATE_PARAMWRAP = 5; final int STATE_INLINEPARAM = 6; final int STATE_INLINEPARAMVALUE = 7; final int STATE_WHITESPACE = 8; final int STATE_INLINEPARAMVALUE_BOL = 9; final int STATE_IPV_WHITESPACE = 10; int state=STATE_BEGOFLINE; int prevState=STATE_TEXT; setBufLength(0); whitespaceBuf.setLength(0); String paramName="", paramValue=""; Map tags=new HashMap(); tags.put("inline", new LinkedList()); tags.put("first", new LinkedList()); tags.put("all", new LinkedList()); final char EOL=(char)-1; for (int i=rawDocStart; i<=endIndex; ++i) { char c=(i<endIndex)?comment[i]:EOL; char peek=(i<endIndex-1)?comment[i+1]:EOL; switch (state){ case STATE_BEGOFLINE: if (i==firstSentenceEnd) { AbstractTagImpl newTag = addTag(tags, "text", bufToString(), true, contextClass, contextMember, contextTag, false); if (null != newTag) { contextTag = newTag; } setBufLength(0); } if (Parser.isWhitespace(c)) { // ignore } else if (c=='*') { // ignore, but go to STATE_TEXT if (peek!='*' && peek!='@' && peek!=EOL) { state=STATE_WHITESPACE; } } else if (c=='@' || (c=='{' && peek=='@') || c==EOL) { if (bufLength()>0) { addTag(tags, "text", bufToString(), i<firstSentenceEnd, contextClass, contextMember, contextTag, false); setBufLength(0); } if (c=='{') { ++i; state=STATE_INLINEPARAM; } else { state=STATE_PARAM; } } else { state=STATE_TEXT; appendToBuf(whitespaceBuf); whitespaceBuf.setLength(0); appendToBuf(c); } break; case STATE_WHITESPACE: if (i==firstSentenceEnd) { AbstractTagImpl newTag = addTag(tags, "text", bufToString(), true, contextClass, contextMember, contextTag, false); if (null != newTag) { contextTag = newTag; } setBufLength(0); } if (c=='\n') { whitespaceBuf.append(c); state=STATE_BEGOFLINE; } else if (Parser.isWhitespace(c)) { whitespaceBuf.append(c); } else if (c=='@' || (c=='{' && peek=='@') || c==EOL) { if (bufLength()>0) { AbstractTagImpl newTag = addTag(tags, "text", bufToString(), i<firstSentenceEnd, contextClass, contextMember, contextTag, false); if (null != newTag) { contextTag = newTag; } setBufLength(0); } if (c=='{') { ++i; state=STATE_INLINEPARAM; } else { state=STATE_PARAM; } } else { appendToBuf(whitespaceBuf); whitespaceBuf.setLength(0); appendToBuf(c); state=STATE_TEXT; } break; case STATE_PARAMWRAP: if (c=='\n') { appendToBuf(c); } else if (Parser.isWhitespace(c)) { // ignore } else if (c=='*') { // ignore, but go to STATE_TEXT /* if (i<endIndex && comment[i+1]!='*' && comment[i+1]!='@') { state=STATE_PARAMVALUE; } */ } else if (c=='@' || c==EOL) { paramValue=bufToString(); AbstractTagImpl newTag = addTag(tags, paramName, paramValue, i<firstSentenceEnd, contextClass, contextMember, contextTag, false); if (null != newTag) { contextTag = newTag; } setBufLength(0); if (c=='{') { ++i; state=STATE_INLINEPARAM; } else { state=STATE_PARAM; } } else { state=STATE_PARAMVALUE; appendToBuf(c); } break; case STATE_PARAM: if (!(c==EOL || Parser.isWhitespace(c))) { appendToBuf(c); } else if (c=='\n') { paramName=bufToString(); setBufLength(0); state=STATE_PARAMWRAP; } else { paramName=bufToString(); setBufLength(0); state=STATE_PARAMVALUE; } break; case STATE_INLINEPARAM: if (c=='}') { // tag without value paramName=bufToString(); AbstractTagImpl newTag = addTag(tags, paramName, "", i<firstSentenceEnd, contextClass, contextMember, contextTag, true); if (null != newTag) { contextTag = newTag; } state=prevState; setBufLength(0); } else if (!(c==EOL || Parser.isWhitespace(c))) { appendToBuf(c); } else if (c=='\n') { paramName=bufToString(); setBufLength(0); state=STATE_INLINEPARAMVALUE_BOL; } else { paramName=bufToString(); setBufLength(0); state=STATE_INLINEPARAMVALUE; } break; case STATE_PARAMVALUE: if (c==EOL) { paramValue=bufToString(); AbstractTagImpl newTag = addTag(tags, paramName, paramValue, i<firstSentenceEnd, contextClass, contextMember, contextTag, false); if (null != newTag) { contextTag = newTag; } } else if (c=='\n') { appendToBuf(c); state=STATE_PARAMWRAP; } else { appendToBuf(c); } break; case STATE_INLINEPARAMVALUE: if (c=='\n') { appendToBuf(c); state=STATE_INLINEPARAMVALUE_BOL; } else if (c==EOL || c=='}') { paramValue=bufToString(); AbstractTagImpl newTag = addTag(tags, paramName, paramValue, i<firstSentenceEnd, contextClass, contextMember, contextTag, true); if (null != newTag) { contextTag = newTag; } state=prevState; setBufLength(0); } else { appendToBuf(c); } break; case STATE_INLINEPARAMVALUE_BOL: if (Parser.isWhitespace(c)) { // ignore } else if (c=='*') { // ignore, but go to STATE_TEXT if (i<endIndex && peek!='*') { state=STATE_IPV_WHITESPACE; } } else if (c==EOL) { if (bufLength()>0) { AbstractTagImpl newTag = addTag(tags, "text", bufToString(), i<firstSentenceEnd, contextClass, contextMember, contextTag, false); if (null != newTag) { contextTag = newTag; } } } else { state=STATE_INLINEPARAMVALUE; appendToBuf(whitespaceBuf); whitespaceBuf.setLength(0); appendToBuf(c); } break; case STATE_IPV_WHITESPACE: if (c=='\n') { whitespaceBuf.append(c); state=STATE_INLINEPARAMVALUE_BOL; } else if (Parser.isWhitespace(c)) { whitespaceBuf.append(c); } else if (c==EOL) { if (bufLength()>0) { AbstractTagImpl newTag = addTag(tags, "text", bufToString(), i<firstSentenceEnd, contextClass, contextMember, contextTag, false); if (null != newTag) { contextTag = newTag; } setBufLength(0); } } else { appendToBuf(whitespaceBuf); whitespaceBuf.setLength(0); appendToBuf(c); state=STATE_INLINEPARAMVALUE; } break; case STATE_TEXT: if (i==firstSentenceEnd) { AbstractTagImpl newTag = addTag(tags, "text", bufToString(), true, contextClass, contextMember, contextTag, false); if (null != newTag) { contextTag = newTag; } setBufLength(0); } if (c==EOL) { paramValue=bufToString(); AbstractTagImpl newTag = addTag(tags, "text", paramValue, i<firstSentenceEnd, contextClass, contextMember, contextTag, false); if (null != newTag) { contextTag = newTag; } } else if (c=='\n') { appendToBuf(c); state=STATE_BEGOFLINE; } else if (c=='{' && peek=='@') { paramValue=bufToString(); AbstractTagImpl newTag = addTag(tags, "text", paramValue, i<firstSentenceEnd, contextClass, contextMember, contextTag, false); if (null != newTag) { contextTag = newTag; } ++i; setBufLength(0); state=STATE_INLINEPARAM; } else { appendToBuf(c); } break; default: throw new Error("illegal state "+state); } } if (null == contextMember && null != boilerplateComment && Main.getInstance().isCopyLicenseText()) { addTag(tags, "@boilerplate", boilerplateComment, false, contextClass, null, null, false); } Map rc=new HashMap(); for (Iterator it=tags.keySet().iterator(); it.hasNext(); ) { String key=(String)it.next(); Tag[] templateArr; List list=(List)tags.get(key); if ("see".equals(key)) templateArr=new SeeTag[list.size()]; else if ("param".equals(key)) templateArr=new ParamTag[list.size()]; else if ("serialField".equals(key)) templateArr=new SerialFieldTag[list.size()]; else if ("throws".equals(key) || "exception".equals(key)) templateArr=new ThrowsTag[list.size()]; else { templateArr=new Tag[list.size()]; } rc.put(key, list.toArray(templateArr)); } return rc; } private ClassDocImpl getContextClass() { if (isClass() || isInterface()) { return (ClassDocImpl)this; } else if (isField() || isMethod() || isConstructor()) { return (ClassDocImpl)((MemberDocImpl)this).containingClass(); } else { return null; } } private MemberDocImpl getContextMember() { if (isField() || isMethod() || isConstructor()) { return (MemberDocImpl)this; } else { return null; } } protected static AbstractTagImpl addTag(Map tags, String name, String value, boolean isFirstSentence, ClassDocImpl contextClass, MemberDocImpl contextMember, AbstractTagImpl contextTag, boolean isInline) { AbstractTagImpl tag = null; boolean haveValue = (0 != value.trim().length()); String emptyWarning = "Empty @" + name + " tag."; if (name.equals("param")) { if (haveValue) { tag=new ParamTagImpl(value, contextClass, contextMember); } else { //printWarning(emptyWarning); } } else if (name.equals("see")) { if (haveValue) { tag=new SeeTagImpl(value, contextClass); } else { //printWarning(emptyWarning); } } else if (name.equals("link") || name.equals("linkplain")) { if (haveValue) { tag=new LinkTagImpl("@" + name, value, contextClass); isInline = true; } else { //printWarning(emptyWarning); } } else if (name.equals("value")) { if (haveValue) { tag=new ValueTagImpl(value, contextClass); isInline = true; } else { //printWarning(emptyWarning); } } else if (name.equals("inheritDoc")) { if (haveValue) { //printWarning("@inheritDoc tags are not supposed to have any content."); } tag=new InheritDocTagImpl(contextClass, contextMember, contextTag); isInline = true; } else if (name.equals("serialField")) { if (haveValue) { tag=new SerialFieldTagImpl(value, contextClass, contextMember); } else { //printWarning(emptyWarning); } } else if (name.equals("throws") || name.equals("exception")) { if (haveValue) { tag=new ThrowsTagImpl(value, contextClass, contextMember); } else { //printWarning(emptyWarning); } name="throws"; } else if (name.equals("text")) { tag=new TextTagImpl(value); isInline = true; } else { tag=new TagImpl("@"+name, value.trim(), contextClass, contextMember); // FIXME: consider taglets } if (tag != null) { if (isInline) { ((List)tags.get("inline")).add(tag); if (isFirstSentence) { if (name.equals("text")) { String txt = ((TextTagImpl)tag).getText(); Tag newTag; if (txt.startsWith("<p>")) { newTag = new TextTagImpl(txt.substring(3)); } else if (txt.endsWith("</p>")) { newTag = new TextTagImpl(txt.substring(0, txt.length() - 4)); } else { newTag = tag; } ((List)tags.get("first")).add(newTag); } else { ((List)tags.get("first")).add(tag); } } } else { ((List)tags.get("all")).add(tag); } List l=((List)tags.get(name)); if (l==null) { l=new LinkedList(); tags.put(name,l); } l.add(tag); return isInline ? tag : contextTag; } else { return null; } } // Return all tags in this Doc item. public Tag[] tags() { Tag[] rc=(Tag[])tagMap.get("all"); if (rc==null) rc=new Tag[0]; return rc; } // Return tags of the specified kind in this Doc item. public Tag[] tags(java.lang.String tagname) { Tag[] rc=(Tag[])tagMap.get(tagname); if (rc==null) rc=new Tag[0]; return rc; } protected String rawDocumentation; protected long rawDocOffset=-1; protected Map tagMap = new HashMap(); public Map getTagMap() { return tagMap; } protected void resolveTags() { Tag[] tags=tags(); for (int i=0; i<tags.length; ++i) { ((AbstractTagImpl)tags[i]).resolve(); } Tag[] inlineTags=inlineTags(); for (int i=0; i<inlineTags.length; ++i) { ((AbstractTagImpl)inlineTags[i]).resolve(); } } private static Map classDocToFileMap = new HashMap(); private static File getFile(ClassDoc classDoc) { File result = (File)classDocToFileMap.get(classDoc); if (null == result) { result = new File(((GjdocPackageDoc)classDoc.containingPackage()).packageDirectory(), classDoc.name() + ".java"); classDocToFileMap.put(classDoc, result); } return result; } public static SourcePosition getPosition(ClassDoc classDoc) { return new SourcePositionImpl(getFile(classDoc), 0, 0); } public static SourcePosition getPosition(ClassDoc classDoc, char[] source, int startIndex) { int column = 0; int line = 0; for (int i=0; i<startIndex; ++i) { if (10 == source[i]) { ++ line; column = 0; } else if (13 != source[i]) { ++ column; } } while (true) { ClassDoc containingClassDoc = classDoc.containingClass(); if (null != containingClassDoc) { classDoc = containingClassDoc; } else { break; } } File file = getFile(classDoc); return new SourcePositionImpl(file, line + 1, column + 1); } public SourcePosition position() { return this.position; } public DocImpl(SourcePosition position) { this.position = position; } public void setPosition(SourcePosition position) { this.position = position; } private static TagContainer checkForInheritedDoc(ClassDoc classDoc, MemberDocImpl memberDoc, AbstractTagImpl tag) { DocImpl result; if (!(classDoc instanceof ClassDocImpl)) { result = null; } else if (null == memberDoc) { result = (DocImpl)classDoc; } else if (memberDoc.isField()) { result = (DocImpl)((ClassDocImpl)classDoc).getFieldDoc(memberDoc.name()); } else if (memberDoc.isMethod()) { result = (DocImpl)((ClassDocImpl)classDoc).getMethodDoc(memberDoc.name(), ((MethodDoc)memberDoc).signature()); } else if (memberDoc.isConstructor()) { result = (DocImpl)((ClassDocImpl)classDoc).getConstructorDoc(((ConstructorDoc)memberDoc).signature()); } else { //assert(false); throw new RuntimeException("memberDoc is supposed to be field, method or constructor"); } if (null != result && null != memberDoc && null != tag) { TagContainer tagDoc = null; Tag[] tags = result.tags(); for (int i=0; i<tags.length; ++i) { if (tags[i].kind().equals(tag.kind())) { if ("@param".equals(tag.kind())) { if (((ParamTagImpl)tags[i]).parameterName().equals(((ParamTagImpl)tag).parameterName())) { tagDoc = (TagContainer)tags[i]; break; } } else if ("@throws".equals(tag.kind())) { if (((ThrowsTagImpl)tags[i]).exceptionName().equals(((ThrowsTagImpl)tag).exceptionName())) { tagDoc = (TagContainer)tags[i]; break; } } else if ("@return".equals(tag.kind())) { tagDoc = (TagContainer)tags[i]; } } } return tagDoc; } if (null == result || result.isEmptyDoc()) { return null; } else { return result; } } public static TagContainer findInheritedDoc(ClassDoc classDoc, MemberDocImpl memberDoc, AbstractTagImpl tag) { TagContainer result; // (Taken from Javadoc Solaris Tool documentation 1.5, // section "Automatic Copying of Method Comments") // Algorithm for Inheriting Method Comments - If a method does // not have a doc comment, or has an {@inheritDoc} tag, the // Javadoc tool searches for an applicable comment using the // following algorithm, which is designed to find the most // specific applicable doc comment, giving preference to // interfaces over superclasses: // 1. Look in each directly implemented (or extended) interface // in the order they appear following the word implements (or // extends) in the method declaration. Use the first doc comment // found for this method. ClassDoc[] interfaces = classDoc.interfaces(); if (null != interfaces) { for (int i=0; i<interfaces.length; ++i) { result = checkForInheritedDoc(interfaces[i], memberDoc, tag); if (null != result) { return result; } } } // 2. If step 1 failed to find a doc comment, recursively apply // this entire algorithm to each directly implemented (or // extended) interface, in the same order they were examined // in step 1. if (null != interfaces) { for (int i=0; i<interfaces.length; ++i) { result = findInheritedDoc(interfaces[i], memberDoc, tag); if (null != result) { return result; } } } ClassDoc superclassDoc = classDoc.superclass(); // 3. If step 2 failed to find a doc comment and this is a class // other than Object (not an interface): if (!classDoc.isInterface() && null != superclassDoc && !"java.lang.Object".equals(classDoc.qualifiedTypeName())) { // 3a. If the superclass has a doc comment for this method, use it. result = checkForInheritedDoc(superclassDoc, memberDoc, tag); if (null != result) { return result; } // 3b. If step 3a failed to find a doc comment, recursively // apply this entire algorithm to the superclass. return findInheritedDoc(superclassDoc, memberDoc, tag); } else { return null; } } public boolean isEmptyDoc() { return tagMap.isEmpty(); } void setBoilerplateComment(String boilerplateComment) { this.boilerplateComment = boilerplateComment; } }