1/*
2 * Copyright (C) 2010 Google Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.google.doclava;
18
19import java.util.regex.Pattern;
20import java.util.regex.Matcher;
21import java.util.ArrayList;
22import java.util.Arrays;
23import java.util.HashSet;
24import java.util.Set;
25
26public class Comment {
27  static final Pattern FIRST_SENTENCE =
28      Pattern.compile("((.*?)\\.)[ \t\r\n\\<](.*)", Pattern.DOTALL);
29
30  private static final Set<String> KNOWN_TAGS = new HashSet<String>(Arrays.asList(new String[] {
31          "@apiNote",
32          "@author",
33          "@since",
34          "@version",
35          "@deprecated",
36          "@undeprecate",
37          "@docRoot",
38          "@sdkCurrent",
39          "@inheritDoc",
40          "@more",
41          "@samplecode",
42          "@sample",
43          "@include",
44          "@serial",
45          "@implNote",
46          "@implSpec",
47          "@usesMathJax",
48      }));
49
50  public Comment(String text, ContainerInfo base, SourcePositionInfo sp) {
51    mText = text;
52    mBase = base;
53    // sp now points to the end of the text, not the beginning!
54    mPosition = SourcePositionInfo.findBeginning(sp, text);
55  }
56
57  private void parseCommentTags(String text) {
58      int i = 0;
59      int length = text.length();
60      while (i < length  && isWhitespaceChar(text.charAt(i++))) {}
61
62      if (i <=  0) {
63          return;
64      }
65
66      text = text.substring(i-1);
67      length = text.length();
68
69      if ("".equals(text)) {
70          return;
71      }
72
73      int start = 0;
74      int end = findStartOfBlock(text, start);
75
76
77      // possible scenarios
78      //    main and block(s)
79      //    main only (end == -1)
80      //    block(s) only (end == 0)
81
82      switch (end) {
83          case -1: // main only
84              parseMainDescription(text, start, length);
85              return;
86          case 0: // block(s) only
87              break;
88          default: // main and block
89
90              // find end of main because end is really the beginning of @
91              parseMainDescription(text, start, findEndOfMainOrBlock(text, start, end));
92              break;
93      }
94
95      // parse blocks
96      for (start = end; start < length; start = end) {
97          end = findStartOfBlock(text, start+1);
98
99          if (end == -1) {
100              parseBlock(text, start, length);
101              break;
102          } else {
103              parseBlock(text, start, findEndOfMainOrBlock(text, start, end));
104          }
105      }
106
107      // for each block
108      //    make block parts
109      //        end is either next @ at beginning of line or end of text
110  }
111
112  private int findEndOfMainOrBlock(String text, int start, int end) {
113      for (int i = end-1; i >= start; i--) {
114          if (!isWhitespaceChar(text.charAt(i))) {
115              end = i+1;
116              break;
117          }
118      }
119      return end;
120  }
121
122  private void parseMainDescription(String mainDescription, int start, int end) {
123      if (mainDescription == null) {
124          return;
125      }
126
127      SourcePositionInfo pos = SourcePositionInfo.add(mPosition, mText, 0);
128      while (start < end) {
129          int startOfInlineTag = findStartIndexOfInlineTag(mainDescription, start, end);
130
131          // if there are no more tags
132          if (startOfInlineTag == -1) {
133              tag(null, mainDescription.substring(start, end), true, pos);
134              return;
135          }
136
137          //int endOfInlineTag = mainDescription.indexOf('}', startOfInlineTag);
138          int endOfInlineTag = findEndIndexOfInlineTag(mainDescription, startOfInlineTag, end);
139
140          // if there was only beginning tag
141          if (endOfInlineTag == -1) {
142              // parse all of main as one tag
143              tag(null, mainDescription.substring(start, end), true, pos);
144              return;
145          }
146
147          endOfInlineTag++; // add one to make it a proper ending index
148
149          // do first part without an inline tag - ie, just plaintext
150          tag(null, mainDescription.substring(start, startOfInlineTag), true, pos);
151
152          // parse the rest of this section, the inline tag
153          parseInlineTag(mainDescription, startOfInlineTag, endOfInlineTag, pos);
154
155          // keep going
156          start = endOfInlineTag;
157      }
158  }
159
160  private int findStartIndexOfInlineTag(String text, int fromIndex, int toIndex) {
161      for (int i = fromIndex; i < (toIndex-3); i++) {
162          if (text.charAt(i) == '{' && text.charAt(i+1) == '@' && !isWhitespaceChar(text.charAt(i+2))) {
163              return i;
164          }
165      }
166
167      return -1;
168  }
169
170  private int findEndIndexOfInlineTag(String text, int fromIndex, int toIndex) {
171      int braceDepth = 0;
172      for (int i = fromIndex; i < toIndex; i++) {
173          if (text.charAt(i) == '{') {
174              braceDepth++;
175          } else if (text.charAt(i) == '}') {
176              braceDepth--;
177              if (braceDepth == 0) {
178                  return i;
179              }
180          }
181      }
182
183      return -1;
184  }
185
186  private void parseInlineTag(String text, int start, int end, SourcePositionInfo pos) {
187      int index = start+1;
188      //int len = text.length();
189      char c = text.charAt(index);
190      // find the end of the tag name "@something"
191      // need to do something special if we have '}'
192      while (index < end && !isWhitespaceChar(c)) {
193
194          // if this tag has no value, just return with tag name only
195          if (c == '}') {
196              // TODO - should value be "" or null?
197              tag(text.substring(start+1, end), null, true, pos);
198              return;
199          }
200          c = text.charAt(index++);
201      }
202
203      // don't parse things that don't have at least one extra character after @
204      // probably should be plus 3
205      // TODO - remove this - think it's fixed by change in parseMainDescription
206      if (index == start+3) {
207          return;
208      }
209
210      int endOfFirstPart = index-1;
211
212      // get to beginning of tag value
213      while (index < end && isWhitespaceChar(text.charAt(index++))) {}
214      int startOfSecondPart = index-1;
215
216      // +1 to get rid of opening brace and -1 to get rid of closing brace
217      // maybe i wanna make this more elegant
218      String tagName = text.substring(start+1, endOfFirstPart);
219      String tagText = text.substring(startOfSecondPart, end-1);
220      tag(tagName, tagText, true, pos);
221  }
222
223
224  /**
225   * Finds the index of the start of a new block comment or -1 if there are
226   * no more starts.
227   * @param text The String to search
228   * @param start the index of the String to start searching
229   * @return The index of the start of a new block comment or -1 if there are
230   * no more starts.
231   */
232  private int findStartOfBlock(String text, int start) {
233      // how to detect we're at a new @
234      //       if the chars to the left of it are \r or \n, we're at one
235      //       if the chars to the left of it are ' ' or \t, keep looking
236      //       otherwise, we're in the middle of a block, keep looking
237      int index = text.indexOf('@', start);
238
239      // no @ in text or index at first position
240      if (index == -1 ||
241              (index == 0 && text.length() > 1 && !isWhitespaceChar(text.charAt(index+1)))) {
242          return index;
243      }
244
245      index = getPossibleStartOfBlock(text, index);
246
247      int i = index-1; // start at the character immediately to the left of @
248      char c;
249      while (i >= 0) {
250          c = text.charAt(i--);
251
252          // found a new block comment because we're at the beginning of a line
253          if (c == '\r' || c == '\n') {
254              return index;
255          }
256
257          // there is a non whitespace character to the left of the @
258          // before finding a new line, keep searching
259          if (c != ' ' && c != '\t') {
260              index = getPossibleStartOfBlock(text, index+1);
261              i = index-1;
262          }
263
264          // some whitespace character, so keep looking, we might be at a new block comment
265      }
266
267      return -1;
268  }
269
270  private int getPossibleStartOfBlock(String text, int index) {
271      while (isWhitespaceChar(text.charAt(index+1)) || !isWhitespaceChar(text.charAt(index-1))) {
272          index = text.indexOf('@', index+1);
273
274          if (index == -1 || index == text.length()-1) {
275              return -1;
276          }
277      }
278
279      return index;
280  }
281
282  private void parseBlock(String text, int startOfBlock, int endOfBlock) {
283      SourcePositionInfo pos = SourcePositionInfo.add(mPosition, mText, startOfBlock);
284      int index = startOfBlock;
285
286      for (char c = text.charAt(index);
287              index < endOfBlock && !isWhitespaceChar(c); c = text.charAt(index++)) {}
288
289      //
290      if (index == startOfBlock+1) {
291          return;
292      }
293
294      int endOfFirstPart = index-1;
295      if (index == endOfBlock) {
296          // TODO - should value be null or ""
297          tag(text.substring(startOfBlock,
298                  findEndOfMainOrBlock(text, startOfBlock, index)), "", false, pos);
299          return;
300      }
301
302
303      // get to beginning of tag value
304      while (index < endOfBlock && isWhitespaceChar(text.charAt(index++))) {}
305      int startOfSecondPart = index-1;
306
307      tag(text.substring(startOfBlock, endOfFirstPart),
308              text.substring(startOfSecondPart, endOfBlock), false, pos);
309  }
310
311  private boolean isWhitespaceChar(char c) {
312      switch (c) {
313          case ' ':
314          case '\r':
315          case '\t':
316          case '\n':
317              return true;
318      }
319      return false;
320  }
321
322  private void tag(String name, String text, boolean isInline, SourcePositionInfo pos) {
323    /*
324     * String s = isInline ? "inline" : "outofline"; System.out.println("---> " + s + " name=[" +
325     * name + "] text=[" + text + "]");
326     */
327    if (name == null) {
328      mInlineTagsList.add(new TextTagInfo("Text", "Text", text, pos));
329    } else if (name.equals("@param")) {
330      mParamTagsList.add(new ParamTagInfo("@param", "@param", text, mBase, pos));
331    } else if (name.equals("@see")) {
332      mSeeTagsList.add(new SeeTagInfo("@see", "@see", text, mBase, pos));
333    } else if (name.equals("@link")) {
334      if (Doclava.DEVSITE_IGNORE_JDLINKS) {
335        TagInfo linkTag = new TextTagInfo(name, name, text, pos);
336        mInlineTagsList.add(linkTag);
337      } else {
338        mInlineTagsList.add(new SeeTagInfo(name, "@see", text, mBase, pos));
339      }
340    } else if (name.equals("@linkplain")) {
341      mInlineTagsList.add(new SeeTagInfo(name, "@linkplain", text, mBase, pos));
342    } else if (name.equals("@value")) {
343      mInlineTagsList.add(new SeeTagInfo(name, "@value", text, mBase, pos));
344    } else if (name.equals("@throws") || name.equals("@exception")) {
345      mThrowsTagsList.add(new ThrowsTagInfo("@throws", "@throws", text, mBase, pos));
346    } else if (name.equals("@return")) {
347      mReturnTagsList.add(new ParsedTagInfo("@return", "@return", text, mBase, pos));
348    } else if (name.equals("@deprecated")) {
349      if (text.length() == 0) {
350        Errors.error(Errors.MISSING_COMMENT, pos, "@deprecated tag with no explanatory comment");
351        text = "No replacement.";
352      }
353      mDeprecatedTagsList.add(new ParsedTagInfo("@deprecated", "@deprecated", text, mBase, pos));
354    } else if (name.equals("@literal")) {
355      mInlineTagsList.add(new LiteralTagInfo(text, pos));
356    } else if (name.equals("@code")) {
357      mInlineTagsList.add(new CodeTagInfo(text, pos));
358    } else if (name.equals("@hide") || name.equals("@removed")
359            || name.equals("@pending") || name.equals("@doconly")) {
360      // nothing
361    } else if (name.equals("@attr")) {
362      AttrTagInfo tag = new AttrTagInfo("@attr", "@attr", text, mBase, pos);
363      mAttrTagsList.add(tag);
364      Comment c = tag.description();
365      if (c != null) {
366        for (TagInfo t : c.tags()) {
367          mInlineTagsList.add(t);
368        }
369      }
370    } else if (name.equals("@undeprecate")) {
371      mUndeprecateTagsList.add(new TextTagInfo("@undeprecate", "@undeprecate", text, pos));
372    } else if (name.equals("@include") || name.equals("@sample")) {
373      mInlineTagsList.add(new SampleTagInfo(name, "@include", text, mBase, pos));
374    } else if (name.equals("@apiNote") || name.equals("@implSpec") || name.equals("@implNote")) {
375      mTagsList.add(new ParsedTagInfo(name, name, text, mBase, pos));
376    } else if (name.equals("@memberDoc")) {
377      mMemberDocTagsList.add(new ParsedTagInfo("@memberDoc", "@memberDoc", text, mBase, pos));
378    } else if (name.equals("@paramDoc")) {
379      mParamDocTagsList.add(new ParsedTagInfo("@paramDoc", "@paramDoc", text, mBase, pos));
380    } else if (name.equals("@returnDoc")) {
381      mReturnDocTagsList.add(new ParsedTagInfo("@returnDoc", "@returnDoc", text, mBase, pos));
382    } else {
383      boolean known = KNOWN_TAGS.contains(name);
384      if (!known) {
385        known = Doclava.knownTags.contains(name);
386      }
387      if (!known) {
388        Errors.error(Errors.UNKNOWN_TAG, pos == null ? null : new SourcePositionInfo(pos),
389            "Unknown tag: " + name);
390      }
391      TagInfo t = new TextTagInfo(name, name, text, pos);
392      if (isInline) {
393        mInlineTagsList.add(t);
394      } else {
395        mTagsList.add(t);
396      }
397    }
398  }
399
400  private void parseBriefTags() {
401    int N = mInlineTagsList.size();
402
403    // look for "@more" tag, which means that we might go past the first sentence.
404    int more = -1;
405    for (int i = 0; i < N; i++) {
406      if (mInlineTagsList.get(i).name().equals("@more")) {
407        more = i;
408      }
409    }
410    if (more >= 0) {
411      for (int i = 0; i < more; i++) {
412        mBriefTagsList.add(mInlineTagsList.get(i));
413      }
414    } else {
415      for (int i = 0; i < N; i++) {
416        TagInfo t = mInlineTagsList.get(i);
417        if (t.name().equals("Text")) {
418          Matcher m = FIRST_SENTENCE.matcher(t.text());
419          if (m.matches()) {
420            String text = m.group(1);
421            TagInfo firstSentenceTag = new TagInfo(t.name(), t.kind(), text, t.position());
422            mBriefTagsList.add(firstSentenceTag);
423            break;
424          }
425        }
426        mBriefTagsList.add(t);
427
428      }
429    }
430  }
431
432  public TagInfo[] tags() {
433    init();
434    return mInlineTags;
435  }
436
437  public TagInfo[] tags(String name) {
438    init();
439    ArrayList<TagInfo> results = new ArrayList<TagInfo>();
440    int N = mInlineTagsList.size();
441    for (int i = 0; i < N; i++) {
442      TagInfo t = mInlineTagsList.get(i);
443      if (t.name().equals(name)) {
444        results.add(t);
445      }
446    }
447    return results.toArray(TagInfo.getArray(results.size()));
448  }
449
450  public TagInfo[] blockTags() {
451    init();
452    return mTags;
453  }
454
455  public ParamTagInfo[] paramTags() {
456    init();
457    return mParamTags;
458  }
459
460  public SeeTagInfo[] seeTags() {
461    init();
462    return mSeeTags;
463  }
464
465  public ThrowsTagInfo[] throwsTags() {
466    init();
467    return mThrowsTags;
468  }
469
470  public TagInfo[] returnTags() {
471    init();
472    return mReturnTags;
473  }
474
475  public TagInfo[] deprecatedTags() {
476    init();
477    return mDeprecatedTags;
478  }
479
480  public TagInfo[] undeprecateTags() {
481    init();
482    return mUndeprecateTags;
483  }
484
485  public AttrTagInfo[] attrTags() {
486    init();
487    return mAttrTags;
488  }
489
490  public TagInfo[] briefTags() {
491    init();
492    return mBriefTags;
493  }
494
495  public ParsedTagInfo[] memberDocTags() {
496    init();
497    return mMemberDocTags;
498  }
499
500  public ParsedTagInfo[] paramDocTags() {
501    init();
502    return mParamDocTags;
503  }
504
505  public ParsedTagInfo[] returnDocTags() {
506    init();
507    return mReturnDocTags;
508  }
509
510  public boolean isHidden() {
511    if (mHidden == null) {
512      mHidden = !Doclava.checkLevel(Doclava.SHOW_HIDDEN) &&
513          (mText != null) && (mText.indexOf("@hide") >= 0 || mText.indexOf("@pending") >= 0);
514    }
515    return mHidden;
516  }
517
518  public boolean isRemoved() {
519    if (mRemoved == null) {
520        mRemoved = !Doclava.checkLevel(Doclava.SHOW_HIDDEN) &&
521            (mText != null) && (mText.indexOf("@removed") >= 0);
522    }
523
524    return mRemoved;
525  }
526
527  public boolean isDocOnly() {
528    if (mDocOnly == null) {
529      mDocOnly = (mText != null) && (mText.indexOf("@doconly") >= 0);
530    }
531    return mDocOnly;
532  }
533
534  public boolean isDeprecated() {
535    if (mDeprecated == null) {
536      mDeprecated = (mText != null) && (mText.indexOf("@deprecated") >= 0);
537    }
538
539    return mDeprecated;
540  }
541
542  private void init() {
543    if (!mInitialized) {
544      initImpl();
545    }
546  }
547
548  private void initImpl() {
549    isHidden();
550    isRemoved();
551    isDocOnly();
552    isDeprecated();
553
554    // Don't bother parsing text if we aren't generating documentation.
555    if (Doclava.parseComments()) {
556        parseCommentTags(mText);
557        parseBriefTags();
558    } else {
559      // Forces methods to be recognized by findOverriddenMethods in MethodInfo.
560      mInlineTagsList.add(new TextTagInfo("Text", "Text", mText,
561          SourcePositionInfo.add(mPosition, mText, 0)));
562    }
563
564    mText = null;
565    mInitialized = true;
566
567    mInlineTags = mInlineTagsList.toArray(TagInfo.getArray(mInlineTagsList.size()));
568    mTags = mTagsList.toArray(TagInfo.getArray(mTagsList.size()));
569    mParamTags = mParamTagsList.toArray(ParamTagInfo.getArray(mParamTagsList.size()));
570    mSeeTags = mSeeTagsList.toArray(SeeTagInfo.getArray(mSeeTagsList.size()));
571    mThrowsTags = mThrowsTagsList.toArray(ThrowsTagInfo.getArray(mThrowsTagsList.size()));
572    mReturnTags = ParsedTagInfo.joinTags(
573        mReturnTagsList.toArray(ParsedTagInfo.getArray(mReturnTagsList.size())));
574    mDeprecatedTags = ParsedTagInfo.joinTags(
575        mDeprecatedTagsList.toArray(ParsedTagInfo.getArray(mDeprecatedTagsList.size())));
576    mUndeprecateTags = mUndeprecateTagsList.toArray(TagInfo.getArray(mUndeprecateTagsList.size()));
577    mAttrTags = mAttrTagsList.toArray(AttrTagInfo.getArray(mAttrTagsList.size()));
578    mBriefTags = mBriefTagsList.toArray(TagInfo.getArray(mBriefTagsList.size()));
579    mMemberDocTags = mMemberDocTagsList.toArray(ParsedTagInfo.getArray(mMemberDocTagsList.size()));
580    mParamDocTags = mParamDocTagsList.toArray(ParsedTagInfo.getArray(mParamDocTagsList.size()));
581    mReturnDocTags = mReturnDocTagsList.toArray(ParsedTagInfo.getArray(mReturnDocTagsList.size()));
582
583    mTagsList = null;
584    mParamTagsList = null;
585    mSeeTagsList = null;
586    mThrowsTagsList = null;
587    mReturnTagsList = null;
588    mDeprecatedTagsList = null;
589    mUndeprecateTagsList = null;
590    mAttrTagsList = null;
591    mBriefTagsList = null;
592    mMemberDocTagsList = null;
593    mParamDocTagsList = null;
594    mReturnDocTagsList = null;
595  }
596
597  boolean mInitialized;
598  Boolean mHidden = null;
599  Boolean mRemoved = null;
600  Boolean mDocOnly = null;
601  Boolean mDeprecated = null;
602  String mText;
603  ContainerInfo mBase;
604  SourcePositionInfo mPosition;
605  int mLine = 1;
606
607  TagInfo[] mInlineTags;
608  TagInfo[] mTags;
609  ParamTagInfo[] mParamTags;
610  SeeTagInfo[] mSeeTags;
611  ThrowsTagInfo[] mThrowsTags;
612  TagInfo[] mBriefTags;
613  TagInfo[] mReturnTags;
614  TagInfo[] mDeprecatedTags;
615  TagInfo[] mUndeprecateTags;
616  AttrTagInfo[] mAttrTags;
617  ParsedTagInfo[] mMemberDocTags;
618  ParsedTagInfo[] mParamDocTags;
619  ParsedTagInfo[] mReturnDocTags;
620
621  ArrayList<TagInfo> mInlineTagsList = new ArrayList<TagInfo>();
622  ArrayList<TagInfo> mTagsList = new ArrayList<TagInfo>();
623  ArrayList<ParamTagInfo> mParamTagsList = new ArrayList<ParamTagInfo>();
624  ArrayList<SeeTagInfo> mSeeTagsList = new ArrayList<SeeTagInfo>();
625  ArrayList<ThrowsTagInfo> mThrowsTagsList = new ArrayList<ThrowsTagInfo>();
626  ArrayList<TagInfo> mBriefTagsList = new ArrayList<TagInfo>();
627  ArrayList<ParsedTagInfo> mReturnTagsList = new ArrayList<ParsedTagInfo>();
628  ArrayList<ParsedTagInfo> mDeprecatedTagsList = new ArrayList<ParsedTagInfo>();
629  ArrayList<TagInfo> mUndeprecateTagsList = new ArrayList<TagInfo>();
630  ArrayList<AttrTagInfo> mAttrTagsList = new ArrayList<AttrTagInfo>();
631  ArrayList<ParsedTagInfo> mMemberDocTagsList = new ArrayList<ParsedTagInfo>();
632  ArrayList<ParsedTagInfo> mParamDocTagsList = new ArrayList<ParsedTagInfo>();
633  ArrayList<ParsedTagInfo> mReturnDocTagsList = new ArrayList<ParsedTagInfo>();
634
635}
636