Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Generate Java Documentation With QDox, EJS, Nashorn, and Asciidoctor

DZone's Guide to

Generate Java Documentation With QDox, EJS, Nashorn, and Asciidoctor

This tactic takes a ''bits and pieces'' approach to creating Java documentation, using QDox, EJS, and Gradle to create partial documentations that can be combined.

· Java Zone ·
Free Resource

Get the Edge with a Professional Java IDE. 30-day free trial.

Javadoc comments are a nice approach to documenting code. By following a specific comment structure, you can describe what classes are responsible for and what methods do. Many developers use the standard Javadoc tool to generate documentation.

But what if your client asks for custom documentation? In this post, I want to show how QDox and Asciidoctor make it a straightforward task.

Problem Definition

Together with every build, we need to generate a document that lists all the important classes and their methods, and how these classes are connected with each other. This may seem like a toy problem, but it gives us enough context to learn what tools to look for and what to expect from them.

Solution

In our solution, there are going to be two modules:

  • docs: implementation of all the analysis tools.
  • sample-app: the target application that we want to document.

It’s important to introduce this separation because docs is a library that only needs to be built once and then can be reused in different applications. sample-app is a toy application we’re going to document.

Document Production Process

Asciidoctor is a great choice when you want to produce a nice-looking document (in my other post, I’ve explained why). Asciidoctor is easy to integrate with Gradle using the asciidoctor-gradle-plugin which means that we can just put our documentation sources together with Java sources and run ./gradlew asciidoctor to produce the documentation.

It would be that easy if our documentation had to be entirely written by a human, but in our case, we want to generate pieces of the document from code, so we’ll have a document like this (docs.adoc):

= Sample App docs

== Classes
Sample App has a few classes:
include::{snippetsDir}/classes.adoc[]

== Class diagram
Here's how Sample App's classes connected with each other:
include::{snippetsDir}/class-diagram.adoc[]

The end.


classes.adoc and class-diagram.adoc are the snippets that we’ll generate during the test run. We make Gradle’s asciidoctor task depend on the test task – this guarantees that test run artifacts will be there when Asciidoctor starts working. So, the build scenario looks like this:

  • Step one: run JUnit tests and produce the classes.adoc and class-diagram.adoc snippets.
  • Step two: run Asciidoctor and let it use content from previously generated classes.adoc and class-diagram.adoc snippets to render the “main” document – docs.adoc.

Producing the Snippets

Here’s what our JUnit tests should look like (DocTest.java):

public class DocTest {
    private final SnippetGenerator snippetGenerator =
        new SnippetGenerator(new File(System.getProperty("sourceDir")));

    private final SnippetWriter snippetWriter =
        new SnippetWriter(Paths.get(System.getProperty("snippetsDir")));

    @Test
    public void documentClasses() {
        String snippet = snippetGenerator.generateClassesSnippet();
        snippetWriter.writeSnippet("classes.adoc", snippet);
    }

    @Test
    public void documentClassDiagram() {
        String snippet = snippetGenerator.generateClassDiagramSnippet();
        snippetWriter.writeSnippet("class-diagram.adoc", snippet);
    }
}


SnippetGenerator is a service that reads the code and produces the snippet content. Its constructor has a single parameter – path to source code directory.

SnippetWriter is a service that takes the content and writes it to the file. Its constructor has a single parameter – path to the directory where to write snippet files.

By making these paths configurable, we achieve nice integration with Gradle (build.gradle):

ext {
  sourceDir = file('src/main')
  snippetsDir = file('build/generated-snippets')
}

test {
  systemProperty 'sourceDir', sourceDir
  systemProperty 'snippetsDir', snippetsDir
}


How SnippetGenerator Works

The big idea behind SnippetGenerator consists of these two parts:

  • Use QDox to read the code. QDox makes it easy to get all the codebase details we need: classes, methods, Javadoc comments, everything. If you’re not familiar with QDox, take a look at my other post where I show how to analyze Java code using QDox.
  • Use EJS and Nashorn to generate snippet contents based on QDox models. EJS is a good choice here because it allows you to mix templates with raw JavaScript. Nashorn’s runtime environment allows JavaScript to work with Java objects.

Let me illustrate it with pseudocode. Here’s a dummy EJS template (dummy.ejs):

We have <%= qdox.getClasses().size() %> classes!


And here’s the code to render this template:

QDox qdox = new QDox("src/main/java");
nashorn.put("template", readAsText("dummy.ejs"));
nashorn.put("qdoxObject", qdox);
String snippet = nashorn.eval("ejs.render(template, {qdox: qdoxObject})");


Assuming that we had 10 classes in our src/main/java, the snippet will have a value of:

We have 10 classes!


How SnippetGenerator Actually Runs EJS With Nashorn

While the pseudocode above explains what happens, let’s take a closer look at how to actually run EJS on Nashorn. First, EJS needs a global window object to initialize properly. Second, it’s important to make a proxy object for original model object. Here’s the “minimal” EJS runner that does all the heavy lifting (SnippetGenerator.java):

private static String renderEjs(String templateString, Object model) {
    ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
    ScriptEngine engine = scriptEngineManager.getEngineByName("nashorn");
    engine.put("template", templateString);
    engine.put("model", model);
    engine.eval("var modelProxy = Object.bindProperties({}, model)");
    engine.eval("window = {}");
    engine.eval("load('classpath:" +
        "META-INF/resources/webjars/ejs/2.4.1/ejs-v2.4.1/ejs.js')");
    return (String) engine.eval("window.ejs.render(template, modelProxy)");
}


Our only use-case assumes that model is always the same – a JavaProjectBuilder object, so here is the convenient render() method that loads the template by name and passes the JavaProjectBuilder object to it (SnippetGenerator.java):

private File sourceRoot;
...
private String render(String templateResourcePath) {
    String templateString = getResourceAsString(templateResourcePath);

    JavaProjectBuilder javaProjectBuilder = new JavaProjectBuilder();
    javaProjectBuilder.addSourceTree(sourceRoot);

    return renderEjs(templateString, javaProjectBuilder);
}


This allows us to provide these two convenience methods to the end user (SnippetGenerator.java):

public String generateClassesSnippet() {
    return render("classes.ejs");
}

public String generateClassDiagramSnippet() {
    return render("class-diagram.ejs");
}


Generating the “List of Classes” Snippet

Here’s how the template for “list of classes” looks like (classes.ejs):

<% load('classpath:utils.js');
var classes = getClasses();
for each (var clazz in classes) {
    if(shouldSkip(clazz)) continue; %>

* `<%= clazz.getName() %>` -- <%= clazz.getComment() %>

    <% for each (var method in clazz.getMethods()) { %>
** `<%= method.getName() %>()` -- <%= method.getComment() %>
    <% } %>
<% } %>


Because our model is the JavaProjectBuilder object itself, we call getClasses() as if it was a global function. In the real world, I would consider moving the querying away from templates – I would build more template-specific models in Java and then just let EJS do the final formatting. I don’t follow this approach in this post to keep it as short and clear as possible.

The shouldSkip() function comes from utils.js. It checks if the class is annotated with Javadoc's @undocumented tag, and if so, it returns true (utils.js):

function shouldSkip(clazz) {
    return clazz.getTagByName('undocumented') != null;
}


When we render the classes.ejs template, the result is an Asciidoctor markup of a two-level unordered list (classes.adoc):

* `CalculatorService` -- Implements &#34;add&#34; and &#34;subtract&#34; operations
** `addNumbers()` -- Adds 2 numbers
** `subtractNumbers()` -- Subtracts 2 numbers
* `AdderService` -- Provides addition functionality
** `add()` -- Adds 2 numbers
* `CalculatorController` -- Calculator REST API facade
** `addNumbers()` -- Handler for &#34;add numbers&#34; request
* `SubtractorService` -- Provides subtraction functionality
** `subtract()` -- Subtracts 2 numbers


When it gets included into the main document and rendered, the final picture looks like this:

 


Generating the “Class Diagram” Snippet

We do a very similar thing to make a class diagram. This time, we’re using Asciidoctor’s diagramming support and, namely, PlantUML syntax for class diagrams (class-diagram.ejs):

[plantuml, class-diagram, svg]
----
<% load('classpath:utils.js');
var classes = getClasses();
for each (var clazz in classes) {
    if(shouldSkip(clazz)) continue; %>
class <%= clazz.getName() %>
<% } %>

<%
for each (var clazz in classes) {
    if(shouldSkip(clazz)) continue;

    for each (var field in clazz.getFields()) {
        if(shouldSkip(field.getType())) continue; %>
<%= clazz.getName() %> --> <%= field.getType().getName() %>
    <% }
} %>
----


This template generates a result like this (class-diagram.adoc):

[plantuml, class-diagram, svg]
----
class CalculatorService
class AdderService
class CalculatorController
class SubtractorService

CalculatorService --> AdderService
CalculatorService --> SubtractorService
CalculatorController --> CalculatorService
----


Which becomes a nice class diagram when finally rendered:

 

Conclusion

Building custom documentation is not the most popular task, but when it appears, make sure to come up with a reproducible solution. EJS, Asciidoctor, and Gradle make it surprisingly easy to produce pieces of the document during the build. While in this post we were using QDox as a source of data, the approach won’t change significantly if, instead of Java code, you’ll need to analyze anything else.

See a self-sufficient sample project in this GitHub repository.

Get the Java IDE that understands code & makes developing enjoyable. Level up your code with IntelliJ IDEA. Download the free trial.

Topics:
java ,documentation generation ,qdox ,asciidoctor ,tutorial

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}