1 /*
2  * Copyright (c) 1997, 2018, Oracle and/or its affiliates. All rights reserved.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.  Oracle designates this
8  * particular file as subject to the "Classpath" exception as provided
9  * by Oracle in the LICENSE file that accompanied this code.
10  *
11  * This code is distributed in the hope that it will be useful, but WITHOUT
12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14  * version 2 for more details (a copy is included in the LICENSE file that
15  * accompanied this code).
16  *
17  * You should have received a copy of the GNU General Public License version
18  * 2 along with this work; if not, write to the Free Software Foundation,
19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20  *
21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22  * or visit www.oracle.com if you need additional information or have any
23  * questions.
24  */
25 
26 package com.sun.tools.javadoc.main;
27 
28 import java.util.regex.Matcher;
29 import java.util.regex.Pattern;
30 import com.sun.javadoc.*;
31 import com.sun.tools.javac.util.ListBuffer;
32 
33 /**
34  * Comment contains all information in comment part.
35  *      It allows users to get first sentence of this comment, get
36  *      comment for different tags...
37  *
38  *  <p><b>This is NOT part of any supported API.
39  *  If you write code that depends on this, you do so at your own risk.
40  *  This code and its internal interfaces are subject to change or
41  *  deletion without notice.</b>
42  *
43  * @author Kaiyang Liu (original)
44  * @author Robert Field (rewrite)
45  * @author Atul M Dambalkar
46  * @author Neal Gafter (rewrite)
47  */
48 @Deprecated(since="9", forRemoval=true)
49 @SuppressWarnings("removal")
50 class Comment {
51 
52     /**
53      * sorted comments with different tags.
54      */
55     private final ListBuffer<Tag> tagList = new ListBuffer<>();
56 
57     /**
58      * text minus any tags.
59      */
60     private String text;
61 
62     /**
63      * Doc environment
64      */
65     private final DocEnv docenv;
66 
67     /**
68      * constructor of Comment.
69      */
Comment(final DocImpl holder, final String commentString)70     Comment(final DocImpl holder, final String commentString) {
71         this.docenv = holder.env;
72 
73         /**
74          * Separate the comment into the text part and zero to N tags.
75          * Simple state machine is in one of three states:
76          * <pre>
77          * IN_TEXT: parsing the comment text or tag text.
78          * TAG_NAME: parsing the name of a tag.
79          * TAG_GAP: skipping through the gap between the tag name and
80          * the tag text.
81          * </pre>
82          */
83         @SuppressWarnings("fallthrough")
84         class CommentStringParser {
85             /**
86              * The entry point to the comment string parser
87              */
88             void parseCommentStateMachine() {
89                 final int IN_TEXT = 1;
90                 final int TAG_GAP = 2;
91                 final int TAG_NAME = 3;
92                 int state = TAG_GAP;
93                 boolean newLine = true;
94                 String tagName = null;
95                 int tagStart = 0;
96                 int textStart = 0;
97                 int lastNonWhite = -1;
98                 int len = commentString.length();
99                 for (int inx = 0; inx < len; ++inx) {
100                     char ch = commentString.charAt(inx);
101                     boolean isWhite = Character.isWhitespace(ch);
102                     switch (state)  {
103                         case TAG_NAME:
104                             if (isWhite) {
105                                 tagName = commentString.substring(tagStart, inx);
106                                 state = TAG_GAP;
107                             }
108                             break;
109                         case TAG_GAP:
110                             if (isWhite) {
111                                 break;
112                             }
113                             textStart = inx;
114                             state = IN_TEXT;
115                             /* fall thru */
116                         case IN_TEXT:
117                             if (newLine && ch == '@') {
118                                 parseCommentComponent(tagName, textStart,
119                                                       lastNonWhite+1);
120                                 tagStart = inx;
121                                 state = TAG_NAME;
122                             }
123                             break;
124                     }
125                     if (ch == '\n') {
126                         newLine = true;
127                     } else if (!isWhite) {
128                         lastNonWhite = inx;
129                         newLine = false;
130                     }
131                 }
132                 // Finish what's currently being processed
133                 switch (state)  {
134                     case TAG_NAME:
135                         tagName = commentString.substring(tagStart, len);
136                         /* fall thru */
137                     case TAG_GAP:
138                         textStart = len;
139                         /* fall thru */
140                     case IN_TEXT:
141                         parseCommentComponent(tagName, textStart, lastNonWhite+1);
142                         break;
143                 }
144             }
145 
146             /**
147              * Save away the last parsed item.
148              */
149             void parseCommentComponent(String tagName,
150                                        int from, int upto) {
151                 String tx = upto <= from ? "" : commentString.substring(from, upto);
152                 if (tagName == null) {
153                     text = tx;
154                 } else {
155                     TagImpl tag;
156                     switch (tagName) {
157                         case "@exception":
158                         case "@throws":
159                             warnIfEmpty(tagName, tx);
160                             tag = new ThrowsTagImpl(holder, tagName, tx);
161                             break;
162                         case "@param":
163                             warnIfEmpty(tagName, tx);
164                             tag = new ParamTagImpl(holder, tagName, tx);
165                             break;
166                         case "@see":
167                             warnIfEmpty(tagName, tx);
168                             tag = new SeeTagImpl(holder, tagName, tx);
169                             break;
170                         case "@serialField":
171                             warnIfEmpty(tagName, tx);
172                             tag = new SerialFieldTagImpl(holder, tagName, tx);
173                             break;
174                         case "@return":
175                             warnIfEmpty(tagName, tx);
176                             tag = new TagImpl(holder, tagName, tx);
177                             break;
178                         case "@author":
179                             warnIfEmpty(tagName, tx);
180                             tag = new TagImpl(holder, tagName, tx);
181                             break;
182                         case "@version":
183                             warnIfEmpty(tagName, tx);
184                             tag = new TagImpl(holder, tagName, tx);
185                             break;
186                         default:
187                             tag = new TagImpl(holder, tagName, tx);
188                             break;
189                     }
190                     tagList.append(tag);
191                 }
192             }
193 
194             void warnIfEmpty(String tagName, String tx) {
195                 if (tx.length() == 0) {
196                     docenv.warning(holder, "tag.tag_has_no_arguments", tagName);
197                 }
198             }
199 
200         }
201 
202         new CommentStringParser().parseCommentStateMachine();
203     }
204 
205     /**
206      * Return the text of the comment.
207      */
commentText()208     String commentText() {
209         return text;
210     }
211 
212     /**
213      * Return all tags in this comment.
214      */
tags()215     Tag[] tags() {
216         return tagList.toArray(new Tag[tagList.length()]);
217     }
218 
219     /**
220      * Return tags of the specified kind in this comment.
221      */
tags(String tagname)222     Tag[] tags(String tagname) {
223         ListBuffer<Tag> found = new ListBuffer<>();
224         String target = tagname;
225         if (target.charAt(0) != '@') {
226             target = "@" + target;
227         }
228         for (Tag tag : tagList) {
229             if (tag.kind().equals(target)) {
230                 found.append(tag);
231             }
232         }
233         return found.toArray(new Tag[found.length()]);
234     }
235 
236     /**
237      * Return throws tags in this comment.
238      */
throwsTags()239     ThrowsTag[] throwsTags() {
240         ListBuffer<ThrowsTag> found = new ListBuffer<>();
241         for (Tag next : tagList) {
242             if (next instanceof ThrowsTag) {
243                 found.append((ThrowsTag)next);
244             }
245         }
246         return found.toArray(new ThrowsTag[found.length()]);
247     }
248 
249     /**
250      * Return param tags (excluding type param tags) in this comment.
251      */
paramTags()252     ParamTag[] paramTags() {
253         return paramTags(false);
254     }
255 
256     /**
257      * Return type param tags in this comment.
258      */
typeParamTags()259     ParamTag[] typeParamTags() {
260         return paramTags(true);
261     }
262 
263     /**
264      * Return param tags in this comment.  If typeParams is true
265      * include only type param tags, otherwise include only ordinary
266      * param tags.
267      */
paramTags(boolean typeParams)268     private ParamTag[] paramTags(boolean typeParams) {
269         ListBuffer<ParamTag> found = new ListBuffer<>();
270         for (Tag next : tagList) {
271             if (next instanceof ParamTag) {
272                 ParamTag p = (ParamTag)next;
273                 if (typeParams == p.isTypeParameter()) {
274                     found.append(p);
275                 }
276             }
277         }
278         return found.toArray(new ParamTag[found.length()]);
279     }
280 
281     /**
282      * Return see also tags in this comment.
283      */
seeTags()284     SeeTag[] seeTags() {
285         ListBuffer<SeeTag> found = new ListBuffer<>();
286         for (Tag next : tagList) {
287             if (next instanceof SeeTag) {
288                 found.append((SeeTag)next);
289             }
290         }
291         return found.toArray(new SeeTag[found.length()]);
292     }
293 
294     /**
295      * Return serialField tags in this comment.
296      */
serialFieldTags()297     SerialFieldTag[] serialFieldTags() {
298         ListBuffer<SerialFieldTag> found = new ListBuffer<>();
299         for (Tag next : tagList) {
300             if (next instanceof SerialFieldTag) {
301                 found.append((SerialFieldTag)next);
302             }
303         }
304         return found.toArray(new SerialFieldTag[found.length()]);
305     }
306 
307     /**
308      * Return array of tags with text and inline See Tags for a Doc comment.
309      */
getInlineTags(DocImpl holder, String inlinetext)310     static Tag[] getInlineTags(DocImpl holder, String inlinetext) {
311         ListBuffer<Tag> taglist = new ListBuffer<>();
312         int delimend = 0, textstart = 0, len = inlinetext.length();
313         boolean inPre = false;
314         DocEnv docenv = holder.env;
315 
316         if (len == 0) {
317             return taglist.toArray(new Tag[taglist.length()]);
318         }
319         while (true) {
320             int linkstart;
321             if ((linkstart = inlineTagFound(holder, inlinetext,
322                                             textstart)) == -1) {
323                 taglist.append(new TagImpl(holder, "Text",
324                                            inlinetext.substring(textstart)));
325                 break;
326             } else {
327                 inPre = scanForPre(inlinetext, textstart, linkstart, inPre);
328                 int seetextstart = linkstart;
329                 for (int i = linkstart; i < inlinetext.length(); i++) {
330                     char c = inlinetext.charAt(i);
331                     if (Character.isWhitespace(c) ||
332                         c == '}') {
333                         seetextstart = i;
334                         break;
335                      }
336                 }
337                 String linkName = inlinetext.substring(linkstart+2, seetextstart);
338                 if (!(inPre && (linkName.equals("code") || linkName.equals("literal")))) {
339                     //Move past the white space after the inline tag name.
340                     while (Character.isWhitespace(inlinetext.
341                                                       charAt(seetextstart))) {
342                         if (inlinetext.length() <= seetextstart) {
343                             taglist.append(new TagImpl(holder, "Text",
344                                                        inlinetext.substring(textstart, seetextstart)));
345                             docenv.warning(holder,
346                                            "tag.Improper_Use_Of_Link_Tag",
347                                            inlinetext);
348                             return taglist.toArray(new Tag[taglist.length()]);
349                         } else {
350                             seetextstart++;
351                         }
352                     }
353                 }
354                 taglist.append(new TagImpl(holder, "Text",
355                                            inlinetext.substring(textstart, linkstart)));
356                 textstart = seetextstart;   // this text is actually seetag
357                 if ((delimend = findInlineTagDelim(inlinetext, textstart)) == -1) {
358                     //Missing closing '}' character.
359                     // store the text as it is with the {@link.
360                     taglist.append(new TagImpl(holder, "Text",
361                                                inlinetext.substring(textstart)));
362                     docenv.warning(holder,
363                                    "tag.End_delimiter_missing_for_possible_SeeTag",
364                                    inlinetext);
365                     return taglist.toArray(new Tag[taglist.length()]);
366                 } else {
367                     //Found closing '}' character.
368                     if (linkName.equals("see")
369                            || linkName.equals("link")
370                            || linkName.equals("linkplain")) {
371                         taglist.append( new SeeTagImpl(holder, "@" + linkName,
372                               inlinetext.substring(textstart, delimend)));
373                     } else {
374                         taglist.append( new TagImpl(holder, "@" + linkName,
375                               inlinetext.substring(textstart, delimend)));
376                     }
377                     textstart = delimend + 1;
378                 }
379             }
380             if (textstart == inlinetext.length()) {
381                 break;
382             }
383         }
384         return taglist.toArray(new Tag[taglist.length()]);
385     }
386 
387     /** regex for case-insensitive match for {@literal <pre> } and  {@literal </pre> }. */
388     private static final Pattern prePat = Pattern.compile("(?i)<(/?)pre>");
389 
scanForPre(String inlinetext, int start, int end, boolean inPre)390     private static boolean scanForPre(String inlinetext, int start, int end, boolean inPre) {
391         Matcher m = prePat.matcher(inlinetext).region(start, end);
392         while (m.find()) {
393             inPre = m.group(1).isEmpty();
394         }
395         return inPre;
396     }
397 
398     /**
399      * Recursively find the index of the closing '}' character for an inline tag
400      * and return it.  If it can't be found, return -1.
401      * @param inlineText the text to search in.
402      * @param searchStart the index of the place to start searching at.
403      * @return the index of the closing '}' character for an inline tag.
404      * If it can't be found, return -1.
405      */
findInlineTagDelim(String inlineText, int searchStart)406     private static int findInlineTagDelim(String inlineText, int searchStart) {
407         int delimEnd, nestedOpenBrace;
408         if ((delimEnd = inlineText.indexOf("}", searchStart)) == -1) {
409             return -1;
410         } else if (((nestedOpenBrace = inlineText.indexOf("{", searchStart)) != -1) &&
411             nestedOpenBrace < delimEnd){
412             //Found a nested open brace.
413             int nestedCloseBrace = findInlineTagDelim(inlineText, nestedOpenBrace + 1);
414             return (nestedCloseBrace != -1) ?
415                 findInlineTagDelim(inlineText, nestedCloseBrace + 1) :
416                 -1;
417         } else {
418             return delimEnd;
419         }
420     }
421 
422     /**
423      * Recursively search for the characters '{', '@', followed by
424      * name of inline tag and white space,
425      * if found
426      *    return the index of the text following the white space.
427      * else
428      *    return -1.
429      */
inlineTagFound(DocImpl holder, String inlinetext, int start)430     private static int inlineTagFound(DocImpl holder, String inlinetext, int start) {
431         DocEnv docenv = holder.env;
432         int linkstart = inlinetext.indexOf("{@", start);
433         if (start == inlinetext.length() || linkstart == -1) {
434             return -1;
435         } else if (inlinetext.indexOf('}', linkstart) == -1) {
436             //Missing '}'.
437             docenv.warning(holder, "tag.Improper_Use_Of_Link_Tag",
438                     inlinetext.substring(linkstart, inlinetext.length()));
439             return -1;
440         } else {
441             return linkstart;
442         }
443     }
444 
445 
446     /**
447      * Return array of tags for the locale specific first sentence in the text.
448      */
firstSentenceTags(DocImpl holder, String text)449     static Tag[] firstSentenceTags(DocImpl holder, String text) {
450         DocLocale doclocale = holder.env.doclocale;
451         return getInlineTags(holder,
452                              doclocale.localeSpecificFirstSentence(holder, text));
453     }
454 
455     /**
456      * Return text for this Doc comment.
457      */
458     @Override
toString()459     public String toString() {
460         return text;
461     }
462 }
463