Generating AsciiDoc using a Java annotation processor
In the past I have helped a number of customers with their journey of migrating their Java 8 based applications to Java 17 (and Java 21 later on). I came up with the idea of creating a slide deck covering the major Java language and API improvements to teach my customers and give some motivation to move on to the latest Java releases.
The essential part has been a suite of examples of new and updated features of each Java release. I wanted the code to be tested with the JUnit framework and I wanted the slide deck to always be in sync with the code. This was the point where I launched the docbuilder project.
The docbuilder annotation processor is able to process a series of annotations for directing the processor to generate an AsciiDoc document. It uses information extracted from the Java code using reflection and information explicitly provided by annotation values.
With that approach it is possible to have fully functional code that is tested and is the source for automatically generated documentation.
Generating a document from a class
Let’s have a look at the following Java source file that has been annotated by docbuilder annotations.
nine/lang/TryWithResource.java
package nine.lang;
import io.codebreeze.docbuilder.api.annotation.Paragraph;
import io.codebreeze.docbuilder.api.annotation.Slide;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.FileReader;
import java.io.IOException;
import static io.codebreeze.docbuilder.api.annotation.ParagraphPosition.BOTTOM;
//tag::TryWithResource[]
@Slide
public class TryWithResource {
@Paragraph(position = BOTTOM, value= """
Before Java 9 a variable referencing a resource had to be redeclared (and assigned) in the `try` block in order to be automatically closed.
""")
@Test
@DisplayName("Try with resource before Java 9")
public void beforeJava9() throws IOException {
// tag::beforeJava9[]
FileReader fileReader = new FileReader("Try.txt");
try (FileReader reader = fileReader) {
reader.read();
}
// end::beforeJava9[]
}
@Paragraph(value="""
Variables that are effectively final
can be used in a try with resource block (and don't have to be redeclared).
""", position = BOTTOM)
@Test
@DisplayName("Try with (effectively final) resource with Java 9 ")
public void withJava9() throws IOException {
// tag::withJava9[]
// reader is an effectively final variable
// as it is not assigned after the initial assignment!
FileReader reader = new FileReader("Try.txt");
try (reader) { (1)
reader.read();
}
// end::withJava9[]
}
}
//end::TryWithResource[]
The above class’s name is TryWithResource
. The @Slide
annotation directs the annotation processor to create a new AsciiDoc document named nine/lang/TryWithResource.adoc
.
It derives the section header from the class name by turning the camel cased class name into a section title.
nine/lang/TryWithResource.adoc
= Try With Resource
.Before Java9
[source,java,linenums,indent=0]
----
FileReader fileReader = new FileReader("Try.txt");
try (FileReader reader = fileReader) {
reader.read();
}
----
Before Java 9 a variable referencing a resource had to be redeclared (and assigned) in the `try` block in order to be automatically closed.
.With Java9
[source,java,linenums,indent=0]
----
// reader is an effectively final variable
// as it is not assigned after the initial assignment!
FileReader reader = new FileReader("Try.txt");
try (reader) { (1)
reader.read();
}
----
Variables that are effectively final
can be used in a try with resource block (and don't have to be redeclared).
Each method produces a titled block that includes a tagged region from the source file.
The tag is derived from the method name but can be customized by the @Snippet
annotation (e.g. @Snippet(tag = "withJava9")
).
Its title is derived from the camel cased method name but can be customized by the @Snippet
annotation (e.g. @Snippet(caption="With Java 9"
).
Custom AsciiDoc content is provided by adding @Paragraph
annotations which can be applied to any class and any method.
Generating a chapter from a package
Now we have a set of AsciiDoc files for each processed Java source file.
Using a package-info.java
file we can create a document to include all of these documents into a single document.
A package can be annotated using the @Chapter
annotation
nine/lang/package-info.java
@Chapter(caption = "Java 9 Language improvements")
@Paragraph("""
The following describes selected improvements of the Java language.
See the https://www.oracle.com/java/technologies/javase/9-relnotes.html[Java 9 release notes].
""")
package nine.lang;
import io.codebreeze.docbuilder.api.annotation.Chapter;
import io.codebreeze.docbuilder.api.annotation.Paragraph;
nine/lang/package-info.adoc
= Java 9 Language improvements
The following describes selected improvements of the Java language.
See the https://www.oracle.com/java/technologies/javase/9-relnotes.html[Java 9 release notes].
:leveloffset: +1
= Try With Resource
.Before Java9
[source,java,linenums,indent=0]
----
FileReader fileReader = new FileReader("Try.txt");
try (FileReader reader = fileReader) {
reader.read();
}
----
Before Java 9 a variable referencing a resource had to be redeclared (and assigned) in the `try` block in order to be automatically closed.
.With Java9
[source,java,linenums,indent=0]
----
// reader is an effectively final variable
// as it is not assigned after the initial assignment!
FileReader reader = new FileReader("Try.txt");
try (reader) { (1)
reader.read();
}
----
Variables that are effectively final
can be used in a try with resource block (and don't have to be redeclared).
:leveloffset!:
:leveloffset: +1
= Private Interface Method
An interface can now have private methods.
These can simplify implementing default methods.
.Say Hello
[source,java,linenums,indent=0]
----
interface Hello {
default String sayHello(String firstName, String lastName) {
return formatGreeting(firstName, lastName);
}
default String sayGoodMorning(String firstName, String lastName) {
return formatGreeting(firstName, lastName);
}
private String formatGreeting(String firstName, String lastName) {
return String.format("Hello %s %s", firstName, lastName);
}
}
----
:leveloffset!:
Summary
The docbuilder project is currently under development.
It was started to automatically create reveal.js slide decks which is still visible from annotations like @Slide
.
The plan is to gradually convert it into a more general purpose tool for generating any kind of markup.
Currently, there is only a single implementation for producing AsciiDoc but everything is in place for plugging in other markup formats.
It already does a good job generating AsciiDoc as shown by the first post in a series of posts covering the latest additions and improvements introduced starting with the Java 9 release and onward.