Enterprise Java

Using Asciidoctor with Spring: Rendering Asciidoc Documents with Spring MVC

Asciidoc is a text based document format, and that is why it is very useful if we want to commit our documents into a version control system and track the changes between different versions. This makes Asciidoc a perfect tool for writing books, technical documents, FAQs, or user’s manuals.

After we have created an Asciidoc document, the odds are that we want to publish it, and one way to do this is to publish that document on our website. Today we will learn how we can transform Asciidoc documents into HTML by using AsciidoctorJ and render the created HTML with Spring MVC.

The requirements of our application are:

  • It must support Asciidoc documents that are found from the classpath.
  • It must support Asciidoc markup that is given as a String object.
  • It must transform the Asciidoc documents into HTML and render the created HTML.
  • It must “embed” the created HTML to the layout of our application.

Let’s start by getting the required dependencies with Maven.

Getting the Required Dependencies with Maven

We can get the required dependencies with Maven by following these steps:

  1. Enable the Spring IO platform.
  2. Configure the required dependencies.

First, we can enable the Spring IO platform by adding the following snippet to our POM file:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>io.spring.platform</groupId>
            <artifactId>platform-bom</artifactId>
            <version>1.0.2.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

Second, we can configure the required dependencies by following these steps:

  1. Configure the logging dependencies in the pom.xml file.
  2. Add the spring-webmvc dependency to the pom.xml file.
  3. Add the Servlet API dependency to the POM file.
  4. Configure the the Sitemesh (version 3.0.0) dependency in the POM file. Sitemesh ensures that every page of our application uses a consistent look and feel.
  5. Add asciidoctorj dependency (version 1.5.0) to the pom.xml file. AsciidoctorJ is a Java API for Asciidoctor and we use it to transform Asciidoc documents into HTML.

The relevant part of our pom.xml file looks as follows:

<dependencies>
    <!-- Logging -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
    </dependency>
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
    </dependency>
    <!-- Spring -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
    </dependency>
    <!-- Java EE -->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <scope>provided</scope>
    </dependency>
    <!-- Sitemesh -->
    <dependency>
        <groupId>org.sitemesh</groupId>
        <artifactId>sitemesh</artifactId>
        <version>3.0.0</version>
    </dependency>
    <!-- AsciidoctorJ -->
    <dependency>
        <groupId>org.asciidoctor</groupId>
        <artifactId>asciidoctorj</artifactId>
        <version>1.5.0</version>
    </dependency>
</dependencies>

Because we use the Spring IO Platform, we don’t have to specify the dependency versions of the artifacts that are part of the Spring IO Platform.

Let’s move on and start implementing our application.

Rendering Asciidoc Documents with Spring MVC

We can fulfil the requirements of our application by following these steps:

  1. Configure our web application and the Sitemesh filter.
  2. Implement the view classes that are responsible of transforming Asciidoc documents into HTML and rendering the created HTML.
  3. Implement the controller methods that use the created view classes.

Let’s get started.

Configuring Sitemesh

The first thing that we have to do is to configure Sitemesh. We can configure Sitemesh by following these three steps:

  1. Configure the Sitemesh filter in the web application configuration.
  2. Create the decorator that is used to create consistent look and feel for our application.
  3. Configure the decorator that is used to by the Sitemesh filter.

First, we have to configure the Sitemesh filter in our web application configuration. We can configure our web application by following these steps:

  1. Create a WebAppConfig class that implements the WebApplicationInitializer interface.
  2. Implement the onStartup() method of the WebApplicationInitializer interface by following these steps:
    1. Create an AnnotationConfigWebApplicationContext object and configure it to process our application context configuration class.
    2. Configure the dispatcher servlet.
    3. Configure the Sitemesh filter to process the HTML returned by the JSP pages of our application and all controller methods that use the url pattern ‘/asciidoctor/*’
    4. Add a new ContextLoaderListener object to the ServletContext. A ContextLoaderListener is responsible of starting and shutting down the Spring WebApplicationContext.

The source code of the WebAppConfig class looks as follows (Sitemesh configuration is highlighted):

import org.sitemesh.config.ConfigurableSiteMeshFilter;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;

import javax.servlet.DispatcherType;
import javax.servlet.FilterRegistration;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration;
import java.util.EnumSet;

public class WebAppConfig implements WebApplicationInitializer {

    private static final String DISPATCHER_SERVLET_NAME = "dispatcher";

    private static final String SITEMESH3_FILTER_NAME = "sitemesh";
    private static final String[] SITEMESH3_FILTER_URL_PATTERNS = {"*.jsp", "/asciidoctor/*"};

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
        rootContext.register(WebAppContext.class);

        configureDispatcherServlet(servletContext, rootContext);
        configureSitemesh3Filter(servletContext);

        servletContext.addListener(new ContextLoaderListener(rootContext));
    }

    private void configureDispatcherServlet(ServletContext servletContext, WebApplicationContext rootContext) {
        ServletRegistration.Dynamic dispatcher = servletContext.addServlet(
                DISPATCHER_SERVLET_NAME,
                new DispatcherServlet(rootContext)
        );
        dispatcher.setLoadOnStartup(1);
        dispatcher.addMapping("/");
    }

    private void configureSitemesh3Filter(ServletContext servletContext) {
        FilterRegistration.Dynamic sitemesh = servletContext.addFilter(SITEMESH3_FILTER_NAME, 
                new ConfigurableSiteMeshFilter()
        );
        EnumSet<DispatcherType> dispatcherTypes = EnumSet.of(DispatcherType.REQUEST, 
                DispatcherType.FORWARD
        );
        sitemesh.addMappingForUrlPatterns(dispatcherTypes, true, SITEMESH3_FILTER_URL_PATTERNS);
    }
}
  • If you want to take a look at the application context configuration class of the example application, you can get it from Github.

Second, we have to create the decorator that provides consistent look and feel for our application. We can do this by following these steps:

  1. Create the decorator file to the src/main/webapp/WEB-INF directory. The decorator file of our example application is called layout.jsp.
  2. Add the HTML that provides the consistent look and feel to the created decorator file.
  3. Ensure that Sitemesh adds the title found from the returned HTML to the HTML that is rendered by the web browser.
  4. Configure Sitemesh to add the HTML elements found from the head of the returned HTML to the head of the rendered HTML.
  5. Ensure that Sitemesh adds the body found from the returned HTML to the HTML that is shown to the user.

The source code of our decorator file (layout.jsp) looks as follows (the parts that are related to Sitemesh are highlighted):

<!doctype html>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title><sitemesh:write property="title"/></title>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" type="text/css" href="${contextPath}/static/css/bootstrap.css"/>
    <link rel="stylesheet" type="text/css" href="${contextPath}/static/css/bootstrap-theme.css"/>
    <script type="text/javascript" src="${contextPath}/static/js/jquery-2.1.1.js"></script>
    <script type="text/javascript" src="${contextPath}/static/js/bootstrap.js"></script>
    <sitemesh:write property="head"/>
</head>
<body>
<nav class="navbar navbar-inverse" role="navigation">
    <div class="container-fluid">
        <!-- Brand and toggle get grouped for better mobile display -->
        <div class="navbar-header">
            <button type="button" class="navbar-toggle" data-toggle="collapse"
                    data-target="#bs-example-navbar-collapse-1">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
        </div>
        <div class="collapse navbar-collapse">
            <ul class="nav navbar-nav">
                <li><a href="${contextPath}/">Document list</a></li>
            </ul>
        </div>
    </div>
</nav>
<div class="container-fluid">
    <sitemesh:write property="body"/>
</div>
</body>
</html>

Third, we have to configure Sitemesh to use the decorator file that we created in the second step. We can do this by following these steps:

  1. Create a sitemesh3.xml file to the src/main/webapp/WEB-INF directory.
  2. Configure Sitemesh to use our decorator for all requests that are processed by the Sitemesh filter.

The sitemesh3.xml file looks as follows:

<sitemesh>
    <mapping path="/*" decorator="/WEB-INF/layout/layout.jsp"/>
</sitemesh>

That is it. We have now configured Sitemesh to provide consistent look and feel for our application. Let’s move on and find out how we can implement the view classes that transform Asciidoc markup into HTML and render the created HTML.

Implementing the View Classes

Before we can start implementing the view classes that transform Asciidoc markup into HTML and render the created HTML, we have to take a quick look at our requirements. The requirements that are relevant for this step are:

  • Our solution must support Asciidoc documents that are found from the classpath.
  • Our solution must support Asciidoc markup that is given as a String object.
  • Our solution must transform the Asciidoc documents into HTML and render the created HTML.

These requirements suggest that we should create three view classes. These view classes are described in the following:

  • We should create an abstract base class that contains the logic that transforms Asciidoc markup into HTML and renders the created HTML.
  • We should create a view class that can read the Asciidoc markup from a file that is found from the classpath.
  • We should create a view class that can read the Asciidoc markup from a String object.

In other words, we have to create the following class structure:

asciidoctor-html-views

First, we have to implement the AbstractAsciidoctorHtmlView class. This class is an abstract base class that transforms Asciidoc markup into HTML and renders the created HTML. We can implement this class by following these steps:

  1. Create the AbstractAsciidoctorHtmlView class and extend the AbstractView class.
  2. Add a constructor to the created class and set the content type of the view to ‘text/html’.
  3. Add a protected abstract method getAsciidocMarkupReader() to created class and set its return type to Reader. The subclasses of this abstract class must implement this method, and the implementation of this method must return a Reader object that can be used to read the rendered Asciidoc markup.
  4. Add a private getAsciidoctorOptions() method to the created class and implement it by returning the configuration options of Asciidoctor.
  5. Override the renderMergedOutputModel() method of the AbstractView class, and implement it by transforming the Asciidoc document into HTML and rendering the created HTML.

The source code of the AbstractAsciidoctorHtmlView class looks as follows:

import org.asciidoctor.Asciidoctor;
import org.asciidoctor.Options;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.view.AbstractView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.Reader;
import java.io.Writer;
import java.util.Map;

public abstract class AbstractAsciidoctorHtmlView extends AbstractView {

    public AbstractAsciidoctorHtmlView() {
        super.setContentType(MediaType.TEXT_HTML_VALUE);
    }

    protected abstract Reader getAsciidocMarkupReader();

    @Override
    protected void renderMergedOutputModel(Map<String, Object> model,
                                           HttpServletRequest request,
                                           HttpServletResponse response) throws Exception {
        //Set the content type of the response to 'text/html'
        response.setContentType(super.getContentType());

        Asciidoctor asciidoctor = Asciidoctor.Factory.create();
        Options asciidoctorOptions = getAsciidoctorOptions();

        try (
                //Get the reader that reads the rendered Asciidoc document
                //and the writer that writes the HTML markup to the request body
                Reader asciidoctorMarkupReader = getAsciidocMarkupReader();
                Writer responseWriter = response.getWriter();
        ) {
            //Transform Asciidoc markup into HTML and write the created HTML 
            //to the response body
            asciidoctor.render(asciidoctorMarkupReader, responseWriter, asciidoctorOptions);
        }
    }

    private Options getAsciidoctorOptions() {
        Options asciiDoctorOptions = new Options();
        //Ensure that Asciidoctor includes both the header and the footer of the Asciidoc 
        //document when it is transformed into HTML.
        asciiDoctorOptions.setHeaderFooter(true);
        return asciiDoctorOptions;
    }
}

Second, we have to implement the ClasspathFileAsciidoctorHtmlView class. This class can read the Asciidoc markup from a file that is found from the classpath. We can implement this class by following these steps:

  1. Create the ClasspathFileAsciidoctorHtmlView class and extend the AbstractAsciidoctorHtmlView class.
  2. Add a private String field called asciidocFileLocation to the created class. This field contains the location of the Asciidoc file that is transformed into HTML. This location must be given in a format that is understood by the getResourceAsStream() method of the Class class.
  3. Create a constructor that takes the location the location of the rendered Asciidoc file as a constructor argument. Implement the constructor by calling the constructor of the superclass and storing the location of the rendered Asciidoc file to the asciidocFileLocation field.
  4. Override the getAsciidocMarkupReader() method and implement it by returning a new InputStreamReader object that is used to read the Asciidoc file found from the classpath.

The source code of the ClasspathFileAsciidoctorHtmlView class looks as follows:

import java.io.InputStreamReader;
import java.io.Reader;

public class ClasspathFileAsciidoctorHtmlView extends AbstractAsciidoctorHtmlView {

    private final String asciidocFileLocation;

    public ClasspathFileAsciidoctorHtmlView(String asciidocFileLocation) {
        super();
        this.asciidocFileLocation = asciidocFileLocation;
    }

    @Override
    protected Reader getAsciidocMarkupReader() {
        return new InputStreamReader(this.getClass().getResourceAsStream(asciidocFileLocation));
    }
}

Third, we have to implement the StringAsciidoctorHtmlView class that can read the Asciidoc markup from a String object. We can implement this class by following these steps:

  1. Create the StringAsciidoctorHtmlView class and extend the AbstractAsciidoctorHtmlView class.
  2. Add a private String field called asciidocMarkup to the created class. This field contains the Asciidoc markup that is transformed into HTML.
  3. Create a constructor that takes the rendered Asciidoc markup as a constructor argument. Implement this constructor by calling the constructor of the superclass and setting the rendered Asciidoc markup to the asciidocMarkup field.
  4. Override the getAsciidocMarkupReader() method and implement it by returning a new StringReader object that is used to read the Asciidoc markup stored to the asciidocMarkup field.

The source code of the StringAsciidoctorHtmlView looks as follows:

import java.io.Reader;
import java.io.StringReader;

public class StringAsciidoctorHtmlView extends AbstractAsciidoctorHtmlView {

    private final String asciidocMarkup;

    public StringAsciidoctorHtmlView(String asciidocMarkup) {
        super();
        this.asciidocMarkup = asciidocMarkup;
    }

    @Override
    protected Reader getAsciidocMarkupReader() {
        return new StringReader(asciidocMarkup);
    }
}

We have now created the required view classes. Let’s move on and find out how we can use these classes in a Spring MVC web application.

Using the Created View Classes

Our last step is to create the controller methods that use the created view classes.

We have to implement two controllers methods that are described in the following:

  • The renderAsciidocDocument() method processes GET requests send to the url ‘/asciidoctor/document’, and it transforms an Asciidoc document into HTML and renders the created HTML.
  • The renderAsciidocString() method processes GET get requests send to the url ‘/asciidoctor/string’, and it transforms an Asciidoc String into HTML and renders the created HTML.

The source code of the AsciidoctorController class looks as follows:

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;
 
@Controller
public class AsciidoctorController {
 
    private static final String ASCIIDOC_FILE_LOCATION = "/asciidoctor/document.adoc";
 
    private static final String ASCIIDOC_STRING = "= Hello, AsciiDoc (String)!\n" +
            "Doc Writer <doc@example.com>\n" +
            "\n" +
            "An introduction to http://asciidoc.org[AsciiDoc].\n" +
            "\n" +
            "== First Section\n" +
            "\n" +
            "* item 1\n" +
            "* item 2\n" +
            "\n" +
            "1\n" +
            "puts \"Hello, World!\"";
 
    @RequestMapping(value = "/asciidoctor/document", method = RequestMethod.GET)
    public ModelAndView renderAsciidocDocument() {
        //Create the view that transforms an Asciidoc document into HTML and
        //renders the created HTML.
        ClasspathFileAsciidoctorHtmlView docView = new ClasspathFileAsciidoctorHtmlView(ASCIIDOC_FILE_LOCATION);
        return new ModelAndView(docView);
    }
 
    @RequestMapping(value = "/asciidoctor/string", method = RequestMethod.GET)
    public ModelAndView renderAsciidocString() {
        //Create the view that transforms an Asciidoc String into HTML and
        //renders the created HTML.
        StringAsciidoctorHtmlView stringView = new StringAsciidoctorHtmlView(ASCIIDOC_STRING);
        return new ModelAndView(stringView);
    }
}

Additional Information:

We have now created the controller methods that use our view classes. When the user of our application invokes a GET request to the url ‘/asciidoctor/document’, the source code of rendered HTML page looks as follows (the parts created by Asciidoctor are highlighted):

<!doctype html>

<html>
<head>
    <title>Hello, AsciiDoc (File)!</title>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" type="text/css" href="/static/css/bootstrap.css"/>
    <link rel="stylesheet" type="text/css" href="/static/css/bootstrap-theme.css"/>
    <script type="text/javascript" src="/static/js/jquery-2.1.1.js"></script>
    <script type="text/javascript" src="/static/js/bootstrap.js"></script>
    
<meta charset="UTF-8">
<!--[if IE]><meta http-equiv="X-UA-Compatible" content="IE=edge"><![endif]-->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="generator" content="Asciidoctor 1.5.0">
<meta name="author" content="Doc Writer">

<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Open+Sans:300,300italic,400,400italic,600,600italic|Noto+Serif:400,400italic,700,700italic|Droid+Sans+Mono:400">
<link rel="stylesheet" href="./asciidoctor.css">

</head>
<body>
<nav class="navbar navbar-inverse" role="navigation">
    <div class="container-fluid">
        <!-- Brand and toggle get grouped for better mobile display -->
        <div class="navbar-header">
            <button type="button" class="navbar-toggle" data-toggle="collapse"
                    data-target="#bs-example-navbar-collapse-1">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
        </div>
        <div class="collapse navbar-collapse">
            <ul class="nav navbar-nav">
                <li><a href="/">Document list</a></li>
            </ul>
        </div>
    </div>
</nav>
<div class="container-fluid">
    
<div id="header">
<h1>Hello, AsciiDoc (File)!</h1>
<div class="details">
<span id="author" class="author">Doc Writer</span><br>
<span id="email" class="email"><a href="mailto:doc@example.com">doc@example.com</a></span><br>
</div>
</div>
<div id="content">
<div id="preamble">
<div class="sectionbody">
<div class="paragraph">
<p>An introduction to <a href="http://asciidoc.org">AsciiDoc</a>.</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_first_section">First Section</h2>
<div class="sectionbody">
<div class="ulist">
<ul>
<li>
<p>item 1</p>
</li>
<li>
<p>item 2</p>
</li>
</ul>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-ruby" data-lang="ruby">puts "Hello, World!"</code></pre>
</div>
</div>
</div>
</div>
</div>
<div id="footer">
<div id="footer-text">
Last updated 2014-09-21 14:21:59 EEST
</div>
</div>

</div>
</body>
</html>

As we can see, the HTML created by Asciidoctor is embedded into our layout which provides a consistent user experience to the users of our application.

Let’s move on and evaluate the pros and cons of this solution.

Pros and Cons

The pros of our solution are:

  • The rendered HTML documents share the same look and feel than the other pages of our application. This means that we can provide a consistent user experience to the users of our application.
  • We can render both static files and strings that can be loaded from a database.

The cons of our solution are:

  • The war file of our simple application is huge (51.9 MB). The reason for this is that even though Asciidoctor has a Java API, it is written in Ruby. Thus, our application needs two big jar files:
    • The size of the asciidoctorj-1.5.0.jar file is 27.5MB.
    • The size of the jruby-complete-1.7.9.jar file is 21.7MB.
  • Our application transforms Asciidoc documents into HTML when the user requests them. This has a negative impact to the response time of our controller methods because the bigger the document, the longer it takes to process it.
  • The first request that renders an Asciidoc document as HTML is 4-5 times slower than the next requests. I didn’t profile the application but I assume that JRuby has got something to do with this.
  • At the moment it is not possible to use this technique if we want to transform Asciidoc documents into PDF documents.

Let’s move on and summarize what we have learned from this blog post.

Summary

This blog post has taught us three things:

  • We learned how we can configure Sitemesh to provide a consistent look and feel for our application.
  • We learned how we can create the view classes that transform Asciidoc documents into HTML and render the created HTML.
  • Even though our solution works, it has a lot of downsides that can make it unusable in real life applications.

The next part of this tutorial describes how we can solve the performance problems of this solution.

P.S. If you want play around with the example application of this blog post, you can get it from Github.

Petri Kainulainen

Petri is passionate about software development and continuous improvement. He is specialized in software development with the Spring Framework and is the author of Spring Data book.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button