001package edu.pdx.cs.joy.grader; 002 003import com.google.common.annotations.VisibleForTesting; 004import com.sun.source.doctree.DocCommentTree; 005import com.sun.source.doctree.DocTree; 006import com.sun.source.doctree.ParamTree; 007import com.sun.source.doctree.ThrowsTree; 008import com.sun.source.util.DocTrees; 009import jdk.javadoc.doclet.Doclet; 010import jdk.javadoc.doclet.DocletEnvironment; 011import jdk.javadoc.doclet.Reporter; 012import org.jspecify.annotations.NonNull; 013 014import javax.lang.model.SourceVersion; 015import javax.lang.model.element.*; 016import javax.lang.model.type.TypeMirror; 017import javax.lang.model.util.ElementFilter; 018import javax.lang.model.util.Elements; 019import java.io.PrintWriter; 020import java.text.BreakIterator; 021import java.util.*; 022import java.util.stream.Collectors; 023import java.util.stream.Stream; 024 025/** 026 * This <A 027 * href="https://docs.oracle.com/javase/9/docs/api/jdk/javadoc/doclet/package-summary.html">doclet</A> 028 * extracts the API documentation (Javadocs) 029 * from a student's project submission and produces a text summary of 030 * them. It is used for grading a student's Javadocs. 031 * 032 * @author David Whitlock 033 * @since Summer 2004 (rewritten for Java 9 API in Summer 2018) 034 */ 035 036public class APIDocumentationDoclet implements Doclet { 037 038 private Reporter reporter; 039 040 @VisibleForTesting 041 static String removePackageNames(String name) { 042 return name.replaceAll("(\\w+\\.)*(\\w+)", "$2"); 043 } 044 045 @VisibleForTesting 046 static String classHeader(ElementKind kind, String qualifiedName) { 047 return switch (kind) { 048 case ENUM -> "enum " + qualifiedName; 049 case RECORD -> "record " + qualifiedName; 050 default -> "class " + qualifiedName; 051 }; 052 } 053 054 @Override 055 public void init(Locale locale, Reporter reporter) { 056 this.reporter = reporter; 057 } 058 059 @Override 060 public String getName() { 061 return "API Documentation Doclet"; 062 } 063 064 @Override 065 public Set<? extends Option> getSupportedOptions() { 066 HashSet<Option> options = new HashSet<>(); 067 options.add(new Option() { 068 @Override 069 public int getArgumentCount() { 070 return 0; 071 } 072 073 @Override 074 public String getDescription() { 075 return "Ignored option until HTML 5 is the default"; 076 } 077 078 @Override 079 public Kind getKind() { 080 return Kind.OTHER; 081 } 082 083 @Override 084 public List<String> getNames() { 085 return List.of("-html5"); 086 } 087 088 @Override 089 public String getParameters() { 090 return ""; 091 } 092 093 @Override 094 public boolean process(String option, List<String> arguments) { 095 // Nothing to do 096 return true; 097 } 098 }); 099 return options; 100 } 101 102 @Override 103 public SourceVersion getSupportedSourceVersion() { 104 return SourceVersion.RELEASE_9; 105 } 106 107 /** 108 * Indents a block of text a given amount. 109 */ 110 @VisibleForTesting 111 static void indent(String text, final int indent, 112 PrintWriter pw) { 113 StringBuilder sb = new StringBuilder(); 114 for (int i = 0; i < indent; i++) { 115 sb.append(" "); 116 } 117 String spaces = sb.toString(); 118 119 pw.print(spaces); 120 121 int currentLineLength = indent; 122 123 BreakIterator boundary = BreakIterator.getWordInstance(); 124 boundary.setText(text); 125 int start = boundary.first(); 126 for (int end = boundary.next(); end != BreakIterator.DONE; 127 start = end, end = boundary.next()) { 128 129 String word = text.substring(start, end); 130 if (word.length() > 1) { 131 word = word.trim(); 132 if (start > 0 && text.charAt(start - 1) == '.') { 133 pw.print(" "); 134 currentLineLength++; 135 } 136 } 137 138 if (currentLineLength + word.length() > 72) { 139 pw.println(""); 140 pw.print(spaces); 141 currentLineLength = indent; 142 } 143 144 if (word.length() > 0 && word.charAt(word.length() - 1) == '\n') { 145 pw.write(word, 0, word.length() - 1); 146 currentLineLength += word.length() - 1; 147 148 } else { 149 pw.print(word); 150 currentLineLength += word.length(); 151 } 152 } 153 154 pw.println(""); 155 } 156 157 @Override 158 public boolean run(DocletEnvironment environment) { 159 PrintWriter pw = new PrintWriter(System.out, true); 160 161 DocTrees docTrees = environment.getDocTrees(); 162 for (TypeElement aClass : ElementFilter.typesIn(environment.getIncludedElements())) { 163 generateClassDocumentation(environment.getElementUtils(), docTrees, aClass, pw); 164 } 165 return true; 166 } 167 168 private void generateClassDocumentation(Elements elements, DocTrees docTrees, TypeElement aClass, PrintWriter pw) { 169 pw.println(classHeader(aClass.getKind(), aClass.getQualifiedName().toString())); 170 171 indent(getFullBodyComment(docTrees, aClass), 2, pw); 172 pw.println(""); 173 174 Stream<? extends Element> constructors = getConstructors(elements, docTrees, aClass); 175 constructors.forEach(constructor -> { 176 generateMethodDocumentation(docTrees, (ExecutableElement) constructor, pw); 177 }); 178 179 Stream<? extends Element> methods = getMethods(elements, docTrees, aClass); 180 methods.forEach(method -> { 181 generateMethodDocumentation(docTrees, (ExecutableElement) method, pw); 182 }); 183 184 } 185 186 private @NonNull Stream<? extends Element> getMethods(Elements elements, DocTrees docTrees, TypeElement aClass) { 187 return getElementsOfKind(elements, docTrees, aClass, ElementKind.METHOD); 188 } 189 190 private @NonNull Stream<? extends Element> getElementsOfKind(Elements elements, DocTrees docTrees, TypeElement aClass, ElementKind kind) { 191 return aClass.getEnclosedElements().stream() 192 .filter(e -> e.getKind() == kind) 193 .filter(e -> shouldDocument(elements, docTrees, (ExecutableElement) e)); 194 } 195 196 private @NonNull Stream<? extends Element> getConstructors(Elements elements, DocTrees docTrees, TypeElement aClass) { 197 return getElementsOfKind(elements, docTrees, aClass, ElementKind.CONSTRUCTOR); 198 } 199 200 private boolean shouldDocument(Elements elements, DocTrees docTrees, ExecutableElement executable) { 201 if (executable.getKind() == ElementKind.CONSTRUCTOR) { 202 return elements.getOrigin(executable) == Elements.Origin.EXPLICIT; 203 } 204 205 return docTrees.getPath(executable) != null; 206 } 207 208 private void generateMethodDocumentation(DocTrees docTrees, ExecutableElement method, PrintWriter pw) { 209 StringBuilder sb = new StringBuilder(); 210 sb.append(joinObjectsToStrings(method.getModifiers(), " ")).append(" "); 211 appendTypeVariables(method, sb); 212 appendReturnType(method, sb); 213 sb.append(getMethodOrConstructorName(method)); 214 sb.append("("); 215 sb.append(method.getParameters().stream().map(this::getParameterTypeAndName).collect(Collectors.joining(", "))); 216 sb.append(")"); 217 218 indent(sb.toString(), 2, pw); 219 220 String comment = getFullBodyComment(docTrees, method); 221 if (comment != null && !comment.equals("")) { 222 indent(comment, 4, pw); 223 } 224 pw.println(""); 225 226 DocCommentTree methodDocs = docTrees.getDocCommentTree(method); 227 if (methodDocs != null) { 228 List<? extends DocTree> blockTags = methodDocs.getBlockTags(); 229 blockTags.stream().filter(d -> d.getKind() == DocTree.Kind.PARAM).forEach(parameter -> { 230 ParamTree paramTag = (ParamTree) parameter; 231 String paramDoc = paramTag.getName() + " - " + paramTag.getDescription(); 232 indent(paramDoc, 4, pw); 233 pw.println(""); 234 }); 235 236 blockTags.stream().filter(d -> d.getKind() == DocTree.Kind.THROWS).forEach(exception -> { 237 ThrowsTree throwsTag = (ThrowsTree) exception; 238 String paramDoc = "throws " + throwsTag.getExceptionName() + " - " + throwsTag.getDescription(); 239 indent(paramDoc, 4, pw); 240 pw.println(""); 241 }); 242 } 243 244 } 245 246 private String getParameterTypeAndName(VariableElement parameter) { 247 return removePackageNames(parameter.asType()) + " " + parameter.getSimpleName(); 248 } 249 250 private String removePackageNames(TypeMirror type) { 251 return removePackageNames(type.toString()); 252 } 253 254 private Name getMethodOrConstructorName(ExecutableElement method) { 255 if (method.getKind() == ElementKind.CONSTRUCTOR) { 256 return method.getEnclosingElement().getSimpleName(); 257 258 } else { 259 return method.getSimpleName(); 260 } 261 } 262 263 private void appendReturnType(Element element, StringBuilder sb) { 264 if (element instanceof ExecutableElement) { 265 ExecutableElement method = (ExecutableElement) element; 266 appendType(method.getReturnType(), sb).append(" "); 267 } 268 } 269 270 private StringBuilder appendType(TypeMirror type, StringBuilder sb) { 271 sb.append(removePackageNames(type)); 272 return sb; 273 } 274 275 private void appendTypeVariables(Element constructor, StringBuilder sb) { 276 // Not sure how to do this yet 277 } 278 279 private String getFullBodyComment(DocTrees docTrees, Element element) { 280 DocCommentTree commentTree = docTrees.getDocCommentTree(element); 281 if (commentTree == null) { 282 return ""; 283 284 } else { 285 return joinObjectsToStrings(commentTree.getFullBody(), ""); 286 } 287 } 288 289 private String joinObjectsToStrings(Collection<?> objects, String delimiter) { 290 return objects.stream().map(Object::toString).collect(Collectors.joining(delimiter)); 291 } 292}