Hackernoon logoUse DocRaptor to Add Output Options to a Full-Stack Application by@johnjvester

Use DocRaptor to Add Output Options to a Full-Stack Application

Author profile picture

@johnjvesterJohn Vester

Information Technology professional with 25+ years expertise in application design and architecture.

Building modern applications has become easier for feature teams, because of wonderful frameworks like Spring Boot, Angular, ReactJS, and Vue.  Services from Amazon, Heroku, Microsoft, and even Google have provided exciting options to further compliment the modern application experience.  However, the need for a rich output format (PDF or Microsoft Excel [XLS] ) seems to remain a few steps behind everything else.  Complex designs in CSS are often the go-to architecture, however, these can become challenging to understand and support.

One would think this is a clear indicator that rich output is becoming less of a need as the application experience evolves.  However, these needs continue to remain on the feature list from applications of all sizes. In fact, the Fitness Application I am currently building needs to produce invoices in a PDF format. Tax-preparation software requires print quality output for either manual filing or for future reference.  XLS extractions are a must for applications that provide a great deal of data.

After realizing that rich output is still a valid need, I investigated a product called DocRaptor.

About DocRaptor

I first looked into several open source solutions for creating my PDFs, but quickly realized they couldn't handle the complexity, nor the (hopeful!) scale that I would need.  Instead I found what seemed like a perfect solution: DocRaptor.  It is based on Prince, an industry-proven, feature-rich engine that performs the conversion; however, Prince does not have a service offering on top to provide easy access to this engine.

DocRaptor is a SaaS solution on top of the engine with APIs that support various languages, including:

  • C#
  • Java
  • Node
  • PHP
  • Python
  • Ruby

There is also a RESTful URI, which can be called using a simple cURL command:

curl http://YOUR_API_KEY_HERE@docraptor.com/docs \
 --fail --silent --show-error \
 --header "Content-Type:application/json" \
 --data '{"test": true,
          "document_url": "http://www.docraptor.com/examples/invoice.html",
          "type": "pdf" }' > docraptor.pdf

In the example above, the template data in the invoice.html file is used to produce a simple PDF called docraptor.pdf.  You can even try the cURL command above, which will produce a PDF similar to below:

The Need

To try out DocRaptor, I wanted to create a full-stack application experience using Spring Boot and Angular.  The Spring Boot RESTful service would introduce a service to provide a list classic rock and roll performers.  The Angular application would display these performing artists in a table format.

On the same display, I added a button to create a PDF version of the list.  The use of this button will make a RESTful call to the Spring Boot API.  In turn, the DocRaptor SaaS solution will generate a PDF version of the table output.  The example PDF will test the capabilities of the convertor engine by duplicating the table across two pages with the following features:

Page #1

The first page will contain the following elements:

  • utilize portrait orientation
  • a watermark (but only available when test-mode is set to false)
  • lower-case roman numeric page numbering
  • page numbering at the bottom of the page (footer)

Page #2

In order to demonstrate the power and flexibility with DocRaptor, the second page will contain::

  • landscape orientation
  • no watermark will appear on the second page
  • two-digit standard page numbering
  • page numbering at the top of the page (header)

The Approach

DocRaptor expects HTML format to produce PDF or XLS documents. In this example, the Spring Boot service will act as the integration point for DocRaptor. As such, Spring Boot must gather the necessary data and format the source as HTML so that DocRaptor can return it as a byte[].

Here are the contents from the https://docraptor.com/examples/invoice.html DocRaptor file:

<!DOCTYPE html>
<html>
  <head>
    <title>Your New Project for Our Best Client</title>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
    <style type="text/css">
      /*resets from YUI*/
      table {border-collapse:collapse; border-spacing:0;}

      /* setup the page */
      @page { margin: 30px; background: #ffffff; }
      /* setup the footer */
      @page { @bottom { content: flow(foot); } }
      #footer { flow: static(foot); }

      /* useful utility */
      .clear { clear:both; }

      /* layout */
      #container { font-family: Omnes Light, Trebuchet MS, Calibri, Futura, Geneva, Tahoma; font-size: 14pt; color: #a7a7a7; position: relative; }

      /* footer shenanigans! */
      #footer { text-align: center; display: block; }

      /* colors */
      .black { color: black }

      /* stylin */

      #quote_name { margin-top: 3.5em; text-align: right; font-weight: bold; font-size: 1.5em }

      #client { font-size: 0.75em; margin-top: 3em; margin-left: 0.5em;}

      #client_header { font-size: 0.5em; }

      #phase_details {
        margin-top: 2em;
        font-size: 0.6em;
        border-width: 1px;
        border-spacing: 0px;
        border-style: solid;
        border-color: gray;
        width: 100%;
      }

      #phase_details th { font-size: 0.8em; padding: 10px !important; border-style: solid !important; }

      #phase_details th, td {
        border-width: 1px;
        padding: 3px 5px;
        border-top-style: none;
        border-bottom-style: none;
        border-left-style: solid;
        border-right-style: solid;
        border-color: gray;
        background-color: white;
      }

      #phase_details tr.first td { padding-top: 10px; padding-bottom: 10px; }

      #phase_details td.price { text-align: left; }
      #phase_details .price_container { float: left; min-width: 30%; }

      #phase_details thead .title { width: 20%; }
      #phase_details thead .description { width: 60%; }
      #phase_details thead .price { width: 20%; }
      #phase_details tr.last { border-bottom: 1px solid gray; }
      #footer #contain { text-align: right; font-size: 0.8em; }
      #total_price { text-align: right; margin-right: 6.75em; margin-top: 0.5em; }
      #total_price h2 { color: black; font-size: 0.6em; font-weight: bold; }
      #total_price .price { margin-left: 0.75em; }
    </style>
  </head>
  <body>
    <div id="container">
      <div id="logo">Your Logo</div>
      <div id="main">
        <div id="header">
          <div id="header_info black">1234 Made Up LN <span class="black">|</span> (555)-555-5555 <span class="black">|</span> example.com</div>
        </div>
        <h1 class="black" id="quote_name">Your New Project</h1>
        <div id="client">
          <div id="client_header">client:</div>
          <p class="address black">
            Our Best Cient
          </p>
        </div>
        <table id="phase_details">
          <thead>
            <tr>
              <th class="title">phase title</th>
              <th class="description">phase description & features</th>
              <th class="price">price</th>
            </tr>
          </thead>
          <tr class="first black">
            <td>When We Do Stuff</td>
            <td>From 10/10/2010 to 11/11/2011</td>
            <td class="price"><div class="price_container">$300</div></td>
            </tr>
          <tr>
            <td></td>
            <td>Doing Stuff</td>
            <td class="price"><div class="price_container">$200</div></td>
          </tr>
          ...
        </table>
      </div>
      <div id="total_price">
        <h2>TOTAL: <span class="price black">$1100</span></h2>
      </div>
    </div>
  </body>
</html>

Everything that DocRaptor needs to convert HTML to a PDF is within this file. Of course, it is possible to link to external styling files to avoid duplicating efforts.

Setting Up the Spring Boot Service With DocRaptor

Adding DocRaptor to a Spring Boot instance is as simple as adding the following dependency:

<dependency>
    <groupId>com.docraptor</groupId>
    <artifactId>docraptor</artifactId>
    <version>2.0.0</version>
</dependency>

The Spring Boot service for this example includes an in-memory H2 database and is populated with a small list of classic rock and roll artists. For this data, the following Entity object exists:

@Data
@Entity
@Table(name = "artists")
public class Artist {
    @Id
    private String name;
    private String yearFormed;
    private boolean active;
    private String imageUrl;
}

When combined with a RESTful URI in the ArtistController, a simple GET command to the \artists path provides data similar to the list below:

[
 ...
 {
   "name": "Yes",
   "yearFormed": "1968",
   "active": true,
   "imageUrl": "https://somehost/Yes_concert.jpg"
 }
]

Configuring DocRaptor Inside Spring Boot

The following attributes are required to interact with the DocRaptor service:

  • ${DOCRAPTOR_API_KEY}¬†- API key provided for a new account¬†[doc-raptor.api-key]
  • ${DOCRAPTOR_TEST_MODE_ENABLED}¬†- determines if test mode is enabled (see below)¬†[doc-raptor.test-mode]
  • ${DOCRAPTOR_USE_JAVASCRIPT}¬†- determines if Javascript is enabled¬†[doc-raptor.use-javascript]
  • ${PORT}¬†- port for Spring Boot service¬†[server.port]¬†(optional, default is 8080)

About doc-raptor.test-mode

By enabling test mode via the ${DOCRAPTOR_TEST_MODE_ENABLED} variable, which maps to [doc-raptor.test-mode], no charges will be incurred against the DocRaptor account.  However, enabling test mode replaces any custom watermarks with a generic DocRaptor watermark.

Keep in mind, it is possible to use a [doc-raptor.api-key] equal to YOUR_API_KEY_HERE. However, in doing so [doc-raptor.test-mode] will always be set to true within the DocRaptor service.

Starting Spring Boot With DocRaptor

With the DocRaptor configuration in place, starting the Spring Boot server with the sample project should yield a start-up screen similar to below:

After  loading, log entries similar to those below should appear:

2020-10-30 10:42:05.100  INFO 95816 --- [           main] DeferredRepositoryInitializationListener : Spring Data repositories initialized!
2020-10-30 10:42:05.108  INFO 95816 --- [           main] c.g.j.d.DocraptorServiceApplication      : Started DocraptorServiceApplication in 2.615 seconds (JVM running for 3.042)
2020-10-30 10:42:15.634  INFO 95816 --- [nio-8017-exec-2] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2020-10-30 10:42:15.634  INFO 95816 --- [nio-8017-exec-2] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2020-10-30 10:42:15.639  INFO 95816 --- [nio-8017-exec-2] o.s.web.servlet.DispatcherServlet        : Completed initialization in 5 ms

Creating a Dynamic Template for DocRaptor

In order to create the template for DocRaptor to use, I decided to use j2html. j2html is an HTML builder that allows  Java programs to produce HTML text.  I added the library to Spring Boot via the following dependency:

<dependency>
   <groupId>com.j2html</groupId>
   <artifactId>j2html</artifactId>
   <version>1.4.0</version>
</dependency>

Next, I created a very simple static utility class for this example:

public final class HtmlBuilder {
    private HtmlBuilder() { }

    public static String createHtml(List<Artist> artists, String title) {
        ContainerTag containerTag = html(
                        head(
                                title(title),
                                style(createStyleObjects()).attr("type", "text/css")
                        ),
                        body(
                                div(
                                    div("DRAFT").attr("id", "watermark"),
                                    h1(title),
                                    createBasicTable(artists.stream())
                                ).withClass("named_page_one"),
                                div(
                                    h1(title),
                                    createBasicTable(artists.stream())
                                ).withClass("named_page_two page_break")
                        )
        );

        return containerTag.render();
    }

    private static ContainerTag createBasicTable(Stream<Artist> artists) {
        return table(
                thead(
                        tr(
                            th(text("Photo")),
                            th(text("Name")),
                            th(text("Founded")),
                            th(text("Active"))
                        )
                ),
                tbody(
                        artists.map(artist ->
                                tr(
                                    td("").attr("width", "5%").attr("background", artist.getImageUrl()).withStyle("background-size: contain; background-repeat: no-repeat;"),
                                    td(text(artist.getName())),
                                    td(text(artist.getYearFormed())),
                                    td(text(artist.isActive() ? "Yes" : "No"))
                                )).toArray(ContainerTag[]::new)
                )
        );
    }

    private static String createStyleObjects() {
        return "table, th, td {" +
                "border: 1px solid black; " +
                "padding: 7px; " +
                "} " +
                "table {" +
                "  border-collapse: collapse;" +
                "  width: 100%;" +
                "} " +

                "#watermark {" +
                "flow: static(watermarkflow);" +
                "font-size: 120px;" +
                "opacity: 0.5;" +
                "transform: rotate(-30deg);" +
                "text-align: center;" +
                "} " +

                "@page namedPage1 {" +
                "size: letter portrait;" +
                "@bottom {" +
                "content: counter(page, lower-roman);" +
                "} " +
                "@prince-overlay {" +
                "content: flow(watermarkflow)" +
                "} " +
                "} " +

                "@page namedPage2 {" +
                "size: letter landscape;" +
                "margin-top: 70px; " +
                "@top {" +
                "content: counter(page, decimal-leading-zero);" +
                "} " +
                "} " +

                ".named_page_one {" +
                "page: namedPage1;" +
                "} " +

                ".named_page_two {" +
                "page: namedPage2;" +
                "} " +

                ".page_break {" +
                "page-break-before: always;" +
                "} ";
    }
}

While very light on cascading style sheets (CSS) features, this HtmlBuilder class will create the template required by DocRaptor and even allow Artist data to be part of the artifact that will be created.

Creating a DocRaptor Service

You can process the DocRaptor request via a simple public method within the DocRaptorService:

public byte[] process(DocRaptorRequest docRaptorRequest) throws Exception {
        log.info("process(docRaptorRequest={}, testMode={})", docRaptorRequest, docRaptorProperties.isTestMode());

        DocApi docApi = new DocApi();
        ApiClient apiClient = docApi.getApiClient();
        apiClient.setUsername(docRaptorProperties.getApiKey());

        validateRequest(docRaptorRequest);
        return docApi.createDoc(buildDocFromRequest(docRaptorRequest));
    }

Once the validateRequest verifies the docRaptorRequest it calls the buildDocFromRequest() method:

private Doc buildDocFromRequest(DocRaptorRequest docRaptorRequest) {
        Doc doc = new Doc();
        doc.setTest(docRaptorProperties.isTestMode());
        doc.setJavascript(docRaptorProperties.isUseJavascript());

        if (docRaptorRequest.getContentType().equals(DocRaptorContentType.STRING)) {
            doc.setDocumentContent(docRaptorRequest.getDocumentContent());
        } else {
            doc.setDocumentUrl(docRaptorRequest.getDocumentUrl());
        }

        doc.setDocumentType(docRaptorRequest.getDocumentType());
        doc.setName(docRaptorRequest.getName());

        if (docRaptorRequest.usePrinceXml()) {
            PrinceOptions princeOptions = new PrinceOptions();
            princeOptions.setBaseurl(docRaptorRequest.getBaseUrl());
            doc.setPrinceOptions(princeOptions);
        }

        log.debug("doc={}", doc.toString());
        return doc;
    }

Within the ArtistService, the following method interfaces with the DocRaptor service:

public byte[] createPdf(String name) throws Exception {
        DocRaptorRequest docRaptorRequest = new DocRaptorRequest();
        docRaptorRequest.setDocumentType(Doc.DocumentTypeEnum.PDF);
        docRaptorRequest.setContentType(DocRaptorContentType.STRING);
        docRaptorRequest.setDocumentContent(HtmlBuilder.createHtml(getArtists(), "Classic Rock Artists"));
        docRaptorRequest.setName(name);
        return docRaptorService.process(docRaptorRequest);
    }

Then, the ArtistController and the /artists/pdf URI call the service:

@GetMapping(value = "/artists/pdf", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
    public ResponseEntity<byte[]> getArtistsPdf(@RequestParam(required = false) String filename) {
        try {

            if (StringUtils.isEmpty(filename)) {
                filename = "Artists.pdf";
            }

            HttpHeaders headers = new HttpHeaders();
            headers.setCacheControl(CacheControl.noCache().getHeaderValue());
            headers.add("content-disposition", "inline;filename=" + filename);

            return new ResponseEntity<>(artistService.createPdf(filename), headers, HttpStatus.OK);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
        }
    }

Then, the ArtistController and the /artists/pdf URI call the service:

    @GetMapping(value = "/artists/pdf", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
    public ResponseEntity<byte[]> getArtistsPdf(@RequestParam(required = false) String filename) {
        try {

            if (StringUtils.isEmpty(filename)) {
                filename = "Artists.pdf";
            }

            HttpHeaders headers = new HttpHeaders();
            headers.setCacheControl(CacheControl.noCache().getHeaderValue());
            headers.add("content-disposition", "inline;filename=" + filename);

            return new ResponseEntity<>(artistService.createPdf(filename), headers, HttpStatus.OK);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
        }
    }

Adding a Simple Angular Client

At this point, you can use a simple cURL or Postman request to retrieve the PDF file from DocRaptor via the following command:

curl --location --request GET 'http://localhost:8080/artists/pdf'

However, I wanted to create a very basic Angular application to provide some presentation around the experience. After building a simple list-artists component, the following screen appears when performing an ng serve from the Angular repository:

This view is a simple list of data from the in-memory H2 database of the Spring Boot service. The Create PDF located in the top right-hand corner of the application calls DocRaptor and the artists/pdf URI. 

Requesting a PDF

In order for DocRaptor to make the PDF file, the Create PDF button is wired to the following method:

createPdf() {
    this.artistsService.createPdf().subscribe((response)=> {
      let file = new Blob([response], {type: 'application/pdf'});
      let fileURL = URL.createObjectURL(file);
      window.open(fileURL);
    });
  }

This method calls the artistsService in Angular:

createPdf() {
    const httpOptions = {
      'responseType'  : 'arraybuffer' as 'json'
    };

    return this.http.get<any>(this.baseUrl + '/pdf', httpOptions);
  }

This, in turn, calls Spring Boot, which contacts DocRaptor and returns a PDF in a new window:

Success! In the example above, we see a PDF file. 

As you will see, the first page contains the following elements:

  • portrait orientation
  • a watermark (but only available when test-mode is set to false)
  • lower-case roman numeric page numbering
  • page numbering at the bottom of the page (footer)

The second page contains the following elements: 

  • landscape orientation
  • no watermark appears on the second page
  • two-digit standard page numbering
  • page numbering at the top of the page (header)

Conclusion

In this article, we explored how to create a simple PDF of data contained within a full-stack application using DocRaptor. If you are interested in the data for this example, check out the following repository:

https://gitlab.com/johnjvester/doc-raptor

Initially, I was concerned that I wouldn't be able to provide a path to an Angular template for  DocRaptor to build the PDF using HTML data. However, I quickly realized that is not a DocRaptor use case.  While I can provide invoice information to my client as part of the application, I don't need a great deal of the printed invoice content inside the view within an application. Content such as customer and provider information are required when providing a print-quality version of the data.

This rich template data needs to be stored somewhere. Furthermore, the ability to dynamically create these templates fits nicely with the niche that DocRaptor meets.  Of course, the same use case also exists for XLS files.

From a pricing perspective, getting started is free by using the API_KEY=YOUR_API_KEY_HERE in the header of each request. This will generate a PDF or XML file with all the necessary features but will add a "TEST DOCUMENT" watermark in the output.

Getting started with a free seven-day trial allows the testMode=false functionality to remove such watermarks and allow a custom watermark similar to the one provided in this example. After the free trial, the Basic plan costs $15 (USD) a month for 125 documents per month.  The price-per-document reduces significantly at the Silver level, which allows for 40,000 documents at a rate of $1,000 (USD) per month. There are some additional pricing options, including an unlimited version.

In today's modern application development world, SaaS options provide a mechanism to do something really well at an attractive price. This allows feature teams to focus on refining business rules and enhancing intellectual property logic within their application.

Have a really great day!

Also published at https://dzone.com/articles/adding-rich-output-options-to-a-full-stack-applica

Tags

Join Hacker Noon

Create your free account to unlock your custom reading experience.