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