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