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}