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