Code Documentation - Reports & JBake

Thomas J Kennedy

Contents:

Make sure you have finished reading Code Documentation & Comments - Looking Back which covers code documentation (e.g., Javadoc comments).

1 There is More to Documentation…

Documentation includes more than just writing inline code comments and using Javadoc (Java), Pydoc (Python), Rustdoc (Rust), or another tool for API documentation. Documentation includes reports, e.g.,

A few of these tools (especially Jacoco, PMD, and Spotbugs) are covered in the next module (Analysis Tools).

2 A Case Study

Recently (during April 2020) I updated one of my CS 330 (Object Oriented Programming and Design) Java examples.

  1. Grab a copy of Review-09-Java-Shapes.zip. Our case study will be Example-6.

  2. Open a terminal and run ./gradlew bake (or .\gradlew.bat bake in Windows Command Prompt or Windows Powershell).

  3. Double click documentation.htm (a quick-and-dirty helper file) or build/jbake/home.html. Your web browser will open.

  4. Take a few minutes to read through the home page and explore the left navigation bar.

Everything was generated using jbake and Gradle. A few items (e.g., Checkstyle and PMD) are covered in the next couple modules. For now, keep in mind that they exist, but do not dwell on them.

2.1 Project Structure

Take a quick look at the project structure. There are quite a few pieces. Take a few moments to look at the “big picture.”

Example 1: Java Shapes Directory Structure
├── build
├── build.gradle
├── config
│   ├── checkstyle
│   │   └── checkstyle.xml
│   └── documentation.config
├── documentation.htm
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
└── src
    ├── integrationTest
    │   └── java
    │       └── edu
    │           └── odu
    │               └── cs
    │                   └── cs330
    │                       └── examples
    │                           └── shapes
    │                               ├── io
    │                               │   └── TestShapeIterator.java
    │                               └── TestShapeFactory.java
    ├── jbake
    │   ├── assets
    │   │   └── css
    │   │       └── base.css
    │   ├── content
    │   │   ├── checkstyle.md
    │   │   ├── coverageIntegration.md
    │   │   ├── coverage.md
    │   │   ├── coverageMerged.md
    │   │   ├── dependencies.md
    │   │   ├── home0.md
    │   │   ├── junitIntegration.md
    │   │   ├── junit.md
    │   │   ├── pmd.md
    │   │   ├── spotbugs.md
    │   │   └── tasks.md
    │   ├── jbake.properties
    │   └── templates
    │       ├── footer.ftl
    │       ├── header.ftl
    │       ├── index.ftl
    │       ├── menu.ftl
    │       ├── page.ftl
    │       ├── reportPage.ftl
    │       └── sitemap.ftl
    ├── main
    │   ├── java
    │   │   └── edu
    │   │       └── odu
    │   │           └── cs
    │   │               ├── cs330
    │   │               │   └── examples
    │   │               │       ├── package-info.java
    │   │               │       ├── RunShapes.java
    │   │               │       └── shapes
    │   │               │           ├── Circle.java
    │   │               │           ├── EquilateralTriangle.java
    │   │               │           ├── io
    │   │               │           │   ├── package-info.java
    │   │               │           │   └── ShapeIterator.java
    │   │               │           ├── package-info.java
    │   │               │           ├── RightTriangle.java
    │   │               │           ├── ShapeFactory.java
    │   │               │           ├── Shape.java
    │   │               │           ├── Square.java
    │   │               │           └── Triangle.java
    │   │               └── tkennedy
    │   │                   └── utilities
    │   │                       ├── package-info.java
    │   │                       └── Utilities.java
    │   └── resources
    │       └── inputShapes.txt
    └── test
        └── java
            └── edu
                └── odu
                    └── cs
                        └── cs330
                            └── examples
                                └── shapes
                                    ├── TestCircle.java
                                    ├── TestEquilateralTriangle.java
                                    ├── TestRightTriangle.java
                                    ├── TestSquare.java
                                    └── TestTriangle.java

You should recognize a few items from Gradle Whirlwind Introduction:

Let us focus on the first two levels.

Example 2: Directory Structure: First Two Levels
├── build
├── build.gradle
├── config
│   ├── checkstyle
│   │   └── checkstyle.xml
│   └── documentation.config
├── documentation.htm
├── gradle
├── gradle.properties
├── gradlew
├── gradlew.bat
└── src
    ├── integrationTest
    ├── jbake
    ├── main
    └── test

Let us focus on 5 items:

2.2 JBake Directory Structure

We are interested in src/jbake.

Example 3: JBake src Files
└── src
    ├── jbake
    │   ├── assets
    │   │   └── css
    │   │       └── base.css
    │   ├── content
    │   │   ├── checkstyle.md
    │   │   ├── coverageIntegration.md
    │   │   ├── coverage.md
    │   │   ├── coverageMerged.md
    │   │   ├── dependencies.md
    │   │   ├── home0.md
    │   │   ├── junitIntegration.md
    │   │   ├── junit.md
    │   │   ├── pmd.md
    │   │   ├── spotbugs.md
    │   │   └── tasks.md
    │   ├── jbake.properties
    │   └── templates
    │       ├── footer.ftl
    │       ├── header.ftl
    │       ├── index.ftl
    │       ├── menu.ftl
    │       ├── page.ftl
    │       ├── reportPage.ftl
    │       └── sitemap.ftl

JBake has a few pieces:

2.2.1 jbake.propertes

JBake uses the ... before and after the settings to mark the beginning and end. The ... must be present.

...
render.index=true
render.archive=false
render.feed=false
render.tags=false
render.sitemap=false
template.reportPage.file=reportPage.ftl
markdown.extensions=-HARDWRAPS
...

The render. settings are built-in to JBake. Each such settings is set to true to enable generation of a page or false to disable generation of a page.

The template.reportPage setting specifies which layout file corresponds to my custom report page template.

The markdown.extensions setting allows markdown settings to be changes. In the case -=HARDWRAPS tells JBake to ignore any hardcoded line breaks in content files.

2.2.2 JBake Templates

In software engineering there exist a few mantras. One is D.R.Y (Don’t repeat yourself). Each report page (e.g., checkstyle.md) shares the same basic structure. After a little trial and error, I came up with the current version of reportPage.ftl.

Example 4: reportPage.ftl

<#include "header.ftl"> <div class="row h-100"> <div class="col-sm-3 col-md-2 bg-light hidden-md-down" id="main-nav"> <#include "menu.ftl"> </div> <div class="col"> <div class="mh-90"> <div class="pageHeader"> <h1><#escape x as x?xml>${content.title}</#escape></h1> </div> <p><em>${content.date?string("dd MMMM yyyy")}</em></p> ${content.body} <div class="embed-responsive ${content.report_iframe_aspectratio}"> <iframe class="embed-responsive-item" src="${content.report_file}" allowfullscreen> </iframe> </div> </div> </div> </div> <#include "footer.ftl">

Most of the CSS classes (e.g., col-sm-3) come from Bootstrap helpers. Pay particular attention to pieces in the form ${...}. These are variables. They are replaced based on metadata located in a content file. Consider src/content/junit.md.

Example 5: Content Page
title=Java Shapes Example - Unit Tests
type=reportPage
status=published
report_file=tests/test/index.html
report_iframe_aspectratio=embed-responsive-4by3
~~~~~~

In this case, junit.md only contains metadata. I could add additional content under the ~~~~~~ (yes, the number of tildas is important).

2.3 Gradle Updates

So far… we have only looked at the JBake directory and file structure. How do we update build.gradle? Take a quick look at the complete build.gradle. There is quite a bit going on, including:

build.gradle.listing
buildscript {
    repositories {
        jcenter()
    }
}

plugins {
    id "java"
    id "application"
    id "eclipse"

    id "checkstyle"
    id "com.github.spotbugs" version "2.0.0"
    id "project-report"
    id "jacoco"
    id "pmd"

    id "org.ysb33r.doxygen" version "0.5"
    id "org.jbake.site" version "5.0.0"

    // Split Integration Tests from Unit Tests
    id "org.unbroken-dome.test-sets" version "3.0.1"
}

sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
    jcenter()
}

dependencies {
    testImplementation "junit:junit:4.12"
    testImplementation "org.hamcrest:hamcrest-library:1.3"
}

jar {
    archiveBaseName = "RunShapes"

    manifest {
        attributes(
            "Main-Class": "edu.odu.cs.cs330.examples.RunShapes"
        )
    }
}

run {
    main = "edu.odu.cs.cs330.examples.RunShapes"
    args = ["src/main/resources/inputShapes.txt", "2"]
}

application {
    mainClassName = "edu.odu.cs.cs330.examples.RunShapes"
}

test {
    reports {
        html.enabled = true
    }
    ignoreFailures = true

    testLogging {
        events "passed", "skipped", "failed", "standardOut", "standardError"
    }
}

// Set up Integration Tests
testSets {
    integrationTest
}

check.dependsOn integrationTest

//------------------------------------------------------------------------------
// Documentation Tools
//------------------------------------------------------------------------------

javadoc {
    failOnError false
}

doxygen {
    generate_html true

    template "config/documentation.config"

    source new File(projectDir, "src/main/java")
    //source new File(projectDir, "src/test/java")
    outputDir new File(buildDir, "doxygen")
}

//------------------------------------------------------------------------------
// Analysis Tools
//------------------------------------------------------------------------------

// SpotBugs
spotbugsMain {
    ignoreFailures = true
    effort = "max"
    reportLevel = "medium"
    reports {
       xml.enabled = false
       html.enabled = true
    }
}

spotbugsTest.enabled = false
spotbugsIntegrationTest.enabled = false
// End SpotBugs config

jacoco {
    toolVersion = "0.8.5"
}

jacocoTestReport {
    reports {
        html.enabled true
        xml.enabled true
        csv.enabled true
    }
}

task mergeCoverageReports(type: JacocoMerge) {
    executionData = layout.files(["build/jacoco/test.exec",
                                  "build/jacoco/integrationTest.exec"])

    destinationFile = "build/jacoco/merged.exec" as File


}

/*
 * This task is based on HenrikBaerbak's example at
 * <https://discuss.gradle.org/t/merge-jacoco-coverage-reports-for-multiproject-setups/12100/10>
 */
task mergedJacocoReportHTML (type: JacocoReport) {
    dependsOn(mergeCoverageReports)

    additionalSourceDirs.from = files(sourceSets.main.allSource.srcDirs)
    sourceDirectories.from = files(sourceSets.main.allSource.srcDirs)
    classDirectories.from = files(sourceSets.main.output)

    executionData.from = files("build/jacoco/merged.exec" as File)

    reports {
        xml.enabled false
        csv.enabled false
        html.enabled true
    }
}

// Check Style Config
checkstyle {
    toolVersion "8.2"
    ignoreFailures = true
    showViolations = false
}

tasks.withType(Checkstyle) {
    reports {
        html.destination project.file("build/reports/checkstyle/main.html")
    }
}
checkstyleTest.enabled = false
checkstyleIntegrationTest.enabled = false
// End Checkstyle config

pmd {
    toolVersion = "6.21.0"
    ignoreFailures = true
    ruleSets = [
        "category/java/bestpractices.xml",
        "category/java/codestyle.xml",
        "category/java/design.xml",
        "category/java/errorprone.xml",
        "category/java/performance.xml"
    ]
}

pmdTest.enabled=false
pmdIntegrationTest.enabled=false

//------------------------------------------------------------------------------
// JBake Configuration
//------------------------------------------------------------------------------
task reports (dependsOn: ["javadoc", "doxygen",
                          "check",
                          "jacocoTestReport",
                          "jacocoIntegrationTestReport",
                          "mergedJacocoReportHTML",
                          "projectReport"]) {

    description "Generate all reports and documentation for this project"
}

task copyJDocs (type: Copy) {
    from "build/docs"
    into "build/tmp/website/assets"
    dependsOn "javadoc"
}

task copyDoxygen (type: Copy) {
    from "build/doxygen"
    into "build/tmp/website/assets/doxygen"
    dependsOn "doxygen"
}

task copyReports (type: Copy) {
    from "build/reports"
    into "build/tmp/website/assets"
    dependsOn "reports"
}

task copyJbakeTemplates (type: Copy) {
    from "src/jbake"
    into "build/tmp/website"
}

// Combine home0.md and the project README.md into a single homepage
task buildHomePage (dependsOn: copyJbakeTemplates) {
    inputs.files ( "build/tmp/website/content/home0.md", "../README.md")
    outputs.file ("build/tmp/website/content/home.md")
    doLast  {
        outputs.files.singleFile.withOutputStream { out ->
            for (file in inputs.files) file.withInputStream {
                out << it << '\n'
            }
        }
    }
}

jbake {
     srcDirName = "build/tmp/website"
}

// Ensure all Copy and JBake build tasks run
task setupWebsite (dependsOn: ["buildHomePage", "copyReports", "copyJDocs", "copyDoxygen"]){
}

bake.dependsOn "setupWebsite"

Let us focus on the Gradle plugins block and everything below

//------------------------------------------------------------------------------
// JBake Configuration
//------------------------------------------------------------------------------

The rest of build.gradle deals with tool configuration (which is a future discussion topic).

buildscript {
    repositories {
        jcenter()
    }
}

plugins {
    id "java"
    id "application"
    id "eclipse"

    id "checkstyle"
    id "com.github.spotbugs" version "2.0.0"
    id "project-report"
    id "jacoco"
    id "pmd"

    id "org.ysb33r.doxygen" version "0.5"
    id "org.jbake.site" version "5.0.0"

    // Split Integration Tests from Unit Tests
    id "org.unbroken-dome.test-sets" version "3.0.1"
}

Most of the plugin blocks deal with analysis tools, testing, or general project setup. We are interested in one line in particular… the line that adds the JBake plugin.

    id "org.jbake.site" version "5.0.0"

The first task (i.e., reports) is for convenience.

task reports (dependsOn: ["javadoc", "doxygen",
                          "check",
                          "jacocoTestReport",
                          "jacocoIntegrationTestReport",
                          "mergedJacocoReportHTML",
                          "projectReport"]) {

    description "Generate all reports and documentation for this project"
}

Defining reports allows us to use ./gradlew reports to generate every report. The reports task triggers everything listed after dependsOn:.

The next few tasks copy each report into a temporary website build directory.

task copyJDocs (type: Copy) {
    from "build/docs"
    into "build/tmp/website/assets"
    dependsOn "javadoc"
}

task copyDoxygen (type: Copy) {
    from "build/doxygen"
    into "build/tmp/website/assets/doxygen"
    dependsOn "doxygen"
}

task copyReports (type: Copy) {
    from "build/reports"
    into "build/tmp/website/assets"
    dependsOn "reports"
}

The copyJBakeTemplates task copies everything from src/jbake into the temporary website build directory.

task copyJbakeTemplates (type: Copy) {
    from "src/jbake"
    into "build/tmp/website"
}

The buildHomePage task combines home0.md with the project README.md. Note that you will probably need to change ../README.md to ./README.md in your own Gradle projects.

// Combine home0.md and the project README.md into a single homepage
task buildHomePage (dependsOn: copyJbakeTemplates) {
    inputs.files ( "build/tmp/website/content/home0.md", "../README.md")
    outputs.file ("build/tmp/website/content/home.md")
    doLast  {
        outputs.files.singleFile.withOutputStream { out ->
            for (file in inputs.files) file.withInputStream {
                out << it << '\n'
            }
        }
    }
}

The next task tells JBake to use the build/tmp/website directory during the build.

jbake {
     srcDirName = "build/tmp/website"
}

The setUpWebsite task is another helper task. It ensures that all the preliminary setup steps happen before the bake task runs.

// Ensure all Copy and JBake build tasks run
task setupWebsite (dependsOn: ["buildHomePage", "copyReports", "copyJDocs", "copyDoxygen"]){
}

The last line guarantees that setupWebsite runs before the bake task.

bake.dependsOn "setupWebsite"

The final step is to… run ./gradlew bake wait for the result!