1 // Copyright 2020 Google LLC
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //      http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 package com.google.api.generator.engine.ast;
16 
17 import com.google.api.generator.engine.escaper.HtmlEscaper;
18 import com.google.api.generator.engine.escaper.MetacharEscaper;
19 import com.google.auto.value.AutoValue;
20 import com.google.common.base.Strings;
21 import java.util.ArrayList;
22 import java.util.Arrays;
23 import java.util.List;
24 import java.util.stream.Collectors;
25 import java.util.stream.Stream;
26 
27 @AutoValue
28 public abstract class JavaDocComment implements Comment {
29   @Override
comment()30   public abstract String comment();
31 
32   // Convenience helper for simple comments.
withComment(String comment)33   public static JavaDocComment withComment(String comment) {
34     return builder().addComment(comment).build();
35   }
36 
builder()37   public static Builder builder() {
38     return new AutoValue_JavaDocComment.Builder();
39   }
40 
41   @Override
accept(AstNodeVisitor visitor)42   public void accept(AstNodeVisitor visitor) {
43     visitor.visit(this);
44   }
45 
46   @AutoValue.Builder
47   public abstract static class Builder {
48     static final String PARAM_INDENT = "       ";
49 
50     // The lack of a getter for these local variables in the external class is WAI.
51     String throwsType = null;
52     String throwsDescription = null;
53     String deprecated = null;
54     String returnDescription = null;
55     List<String> paramsList = new ArrayList<>();
56     List<String> componentsList = new ArrayList<>();
57     // Private accessor, set complete and consolidated comment.
setComment(String comment)58     abstract Builder setComment(String comment);
59 
autoBuild()60     abstract JavaDocComment autoBuild();
61 
setThrows(String type, String description)62     public Builder setThrows(String type, String description) {
63       throwsType = type;
64       throwsDescription = description;
65       return this;
66     }
67 
setDeprecated(String deprecatedText)68     public Builder setDeprecated(String deprecatedText) {
69       deprecated = deprecatedText;
70       return this;
71     }
72 
setReturn(String returnText)73     public Builder setReturn(String returnText) {
74       returnDescription = returnText;
75       return this;
76     }
77 
addParam(String name, String description)78     public Builder addParam(String name, String description) {
79       paramsList.add(String.format("@param %s %s", name, processParamComment(description)));
80       return this;
81     }
82 
addUnescapedComment(String comment)83     public Builder addUnescapedComment(String comment) {
84       componentsList.add(comment);
85       return this;
86     }
87 
addComment(String comment)88     public Builder addComment(String comment) {
89       componentsList.add(HtmlEscaper.process(comment));
90       return this;
91     }
92 
93     // TODO(developer): <pre> and {@code} is not supporting rendering '@' character, it is evaluated
94     // as Javadoc tag. Please handle '@' character if need in the future. More details at
95     // https://reflectoring.io/howto-format-code-snippets-in-javadoc/#pre--code
addSampleCode(String sampleCode)96     public Builder addSampleCode(String sampleCode) {
97       componentsList.add("<pre>{@code");
98       Arrays.stream(sampleCode.split("\\r?\\n"))
99           .forEach(
100               line -> {
101                 componentsList.add(line);
102               });
103       componentsList.add("}</pre>");
104       return this;
105     }
106 
addParagraph(String paragraph)107     public Builder addParagraph(String paragraph) {
108       componentsList.add(String.format("<p> %s", HtmlEscaper.process(paragraph)));
109       return this;
110     }
111 
addOrderedList(List<String> oList)112     public Builder addOrderedList(List<String> oList) {
113       componentsList.add("<ol>");
114       oList.stream()
115           .forEach(
116               s -> {
117                 componentsList.add(String.format("<li> %s", HtmlEscaper.process(s)));
118               });
119       componentsList.add("</ol>");
120       return this;
121     }
122 
addUnorderedList(List<String> uList)123     public Builder addUnorderedList(List<String> uList) {
124       componentsList.add("<ul>");
125       uList.stream()
126           .forEach(
127               s -> {
128                 componentsList.add(String.format("<li> %s", HtmlEscaper.process(s)));
129               });
130       componentsList.add("</ul>");
131       return this;
132     }
133 
emptyComments()134     public boolean emptyComments() {
135       return Strings.isNullOrEmpty(throwsType)
136           && Strings.isNullOrEmpty(throwsDescription)
137           && Strings.isNullOrEmpty(deprecated)
138           && Strings.isNullOrEmpty(returnDescription)
139           && paramsList.isEmpty()
140           && componentsList.isEmpty();
141     }
142 
build()143     public JavaDocComment build() {
144       // @param, @throws, @return, and @deprecated should always get printed at the end.
145       componentsList.addAll(paramsList);
146       if (!Strings.isNullOrEmpty(throwsType)) {
147         componentsList.add(
148             String.format("@throws %s %s", throwsType, HtmlEscaper.process(throwsDescription)));
149       }
150       if (!Strings.isNullOrEmpty(deprecated)) {
151         componentsList.add(String.format("@deprecated %s", deprecated));
152       }
153       if (!Strings.isNullOrEmpty(returnDescription)) {
154         componentsList.add(String.format("@return %s", returnDescription));
155       }
156       // Escape component in list one by one, because we will join the components by `\n`
157       // `\n` will be taken as escape character by the comment escaper.
158       componentsList =
159           componentsList.stream().map(c -> MetacharEscaper.process(c)).collect(Collectors.toList());
160       setComment(String.join("\n", componentsList));
161       return autoBuild();
162     }
163 
164     // TODO(miraleung): Refactor param paragraph parsing to be more robust.
processParamComment(String rawComment)165     private static String processParamComment(String rawComment) {
166       StringBuilder processedCommentBuilder = new StringBuilder();
167       String[] descriptionParagraphs = rawComment.split("\\n\\n");
168       for (int i = 0; i < descriptionParagraphs.length; i++) {
169         boolean startsWithItemizedList = descriptionParagraphs[i].startsWith(" * ");
170         // Split by listed items, then join newlines.
171         List<String> listItems =
172             Stream.of(descriptionParagraphs[i].split("\\n \\*"))
173                 .map(s -> s.replace("\n", ""))
174                 .collect(Collectors.toList());
175         if (startsWithItemizedList) {
176           // Remove the first asterisk.
177           listItems.set(0, listItems.get(0).substring(2));
178         }
179         if (!startsWithItemizedList) {
180           if (i == 0) {
181             processedCommentBuilder.append(
182                 String.format("%s", HtmlEscaper.process(listItems.get(0))));
183           } else {
184             processedCommentBuilder.append(
185                 String.format("%s<p> %s", PARAM_INDENT, HtmlEscaper.process(listItems.get(0))));
186           }
187         }
188         if (listItems.size() > 1 || startsWithItemizedList) {
189           processedCommentBuilder.append(
190               String.format(
191                   "%s<ul>\n%s\n%s</ul>",
192                   PARAM_INDENT,
193                   listItems.subList(startsWithItemizedList ? 0 : 1, listItems.size()).stream()
194                       .map(li -> String.format("%s  <li>%s", PARAM_INDENT, HtmlEscaper.process(li)))
195                       .reduce("", String::concat),
196                   PARAM_INDENT));
197         }
198       }
199       return processedCommentBuilder.toString();
200     }
201   }
202 }
203