Sunday, November 4, 2012

Submitting PDF Forms in Java

For a client I had to add some prototyping functionality to an existing web application to allow the client to quickly upload PDF forms and assign them to specific users. Those users should then see the PDF embedded inside the browser window and be able to fill out the form and submit the data to the server.

This functionality would help the client to quickly try out different kinds of questionnaires and different approaches on groups of test users without depending on developers to develop new web pages in the application all the time. Only when they were fully satisfied with the feedback from the test group, developers would convert the PDF forms into actual HTML forms.

Of course, proper embedding inside the web page only works on a subset of platforms and browser and only if a PDF Reader plugin is installed that supports those features. For us this wasn't much of a problem, since the test group was in a controlled environment (using Windows + IE + Adobe Acrobat Reader).

In this blog I will create a simple Java web application that has this basic functionality. The project structure looks like this:


So there are just 2 servlets, 2 JSPs and a web.xml file. I also provide a pom-file so you can build and run the project using Maven.

Let's take a look at the web.xml first:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
         version="2.5">

   <display-name>PDFForms</display-name>

   <!-- This servlet generates a PDF form on the fly. -->
   <servlet>
      <servlet-name>PDFFormServlet</servlet-name>
      <servlet-class>nl.piraya.blog.PDFServlet</servlet-class>
   </servlet>
   <servlet-mapping>
      <servlet-name>PDFFormServlet</servlet-name>
      <url-pattern>/dynamic.pdf</url-pattern>
   </servlet-mapping>

   <!-- This PDF accepts submitted FDF data. -->
   <servlet>
      <servlet-name>FDFServlet</servlet-name>
      <servlet-class>nl.piraya.blog.FDFServlet</servlet-class>
   </servlet>
   <servlet-mapping>
      <servlet-name>FDFServlet</servlet-name>
      <url-pattern>/fdf</url-pattern>
   </servlet-mapping>

   <!-- Make sure the index.jsp file is opened when people access the application. -->
   <welcome-file-list>
      <welcome-file>index.jsp</welcome-file>
   </welcome-file-list>
</web-app>

Here we setup a Servlet that generates the PDF form dynamically and map it to the path /dynamic.pdf. We also setup a Servlet that accepts submitted FDF data and map it to the path /fdf. At the end we also make sure the index.jsp file is opened if the user visits our webapp.

Let's take a look at the index.jsp:
<!DOCTYPE html>
<html lang="en-us">
   <head>
      <meta charset="utf-8">
      <title>PDF Forms Example</title>
   </head>
   <body>
      <h1>Embedded PDF Form</h1>

      <!-- Embed a dynamically generated PDF that can submit its data back to the server. -->
      <object type="application/pdf" width="640" height="480" data="dynamic.pdf">
         <a href="dynamic.pdf">Dynamically Generated PDF</a>
      </object>
   </body>
</html>

Not much happening here. As a matter of fact, there's really no need to make this a JSP. An HTML file would have sufficed. The only mildly interesting part is the embedding code; an object tag is used that points to the PDF servlet. If PDF embedding is not supported, we still show a link so you can download the PDF instead. Although usually when embedding fails, you most likely don't have a PDF reader on your system that supports submitting data from a PDF form either.

Next up, the PDFServlet code:
package nl.piraya.blog;

import com.lowagie.text.*;
import com.lowagie.text.Rectangle;
import com.lowagie.text.pdf.*;
import com.lowagie.text.pdf.TextField;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;

/**
 * Servlet that uses iText to generate a PDF. The PDF that is created will hold a couple of text fields and a submit
 * button that submits the form data to the FDFServlet.
 *
 * @author Onno Scheffers
 */
public class PDFServlet extends javax.servlet.http.HttpServlet {
   /**
    * Handles GET requests to this servlet.
    *
    * @param request An {@link HttpServletRequest} object that contains the request the client has made of the servlet.
    * @param response An {@link HttpServletResponse} object that contains the response the servlet sends to the client.
    * @exception IOException If an input or output error is detected when the servlet handles the GET request.
    * @exception ServletException If the request for the GET could not be handled.
    */
   protected void doGet(
         final HttpServletRequest request,
         final HttpServletResponse response
   ) throws ServletException, IOException {

      // Setup a buffer for the PDF data
      ByteArrayOutputStream buffer = new ByteArrayOutputStream();
      try {
         // Create the new PDF document and add some content
         Document document = new Document(PageSize.A4);
         PdfWriter writer = PdfWriter.getInstance(document, buffer);
         document.open();
         addParagraph(document);
         addFields(writer);
         addSubmitButton(writer);
         document.close();

         // Handle the response
         response.setHeader("Expires", "0");
         response.setHeader("Cache-Control", "must-revalidate, post-check=0, pre-check=0");
         response.setHeader("Pragma", "public");
         response.setContentType("application/pdf");
         response.setContentLength(buffer.size());
         OutputStream os = response.getOutputStream();
         buffer.writeTo(os);
         os.flush();
         os.close();
      } catch (Exception e) {
         throw new IOException("Problem during PDF creation", e);
      }
   }

   /**
    * Adds a paragraph of text to the document.
    *
    * @param document The PDF document.
    * @throws DocumentException If something goes wrong while adding the paragraph to the PDF document.
    */
   private void addParagraph(final Document document) throws DocumentException {
      Paragraph p = new Paragraph("Please fill out the fields below and click the submit button.");
      p.setAlignment(Element.ALIGN_CENTER);
      document.add(p);
   }

   /**
    * Adds a couple of text fields to the PDF document.
    *
    * @param writer The PdfWriter to use for adding text fields to the PDF document.
    * @throws IOException If something goes wrong while adding the text fields to the document.
    * @throws DocumentException If something goes wrong while creating the required PDF elements.
    */
   private void addFields(final PdfWriter writer) throws IOException, DocumentException {
      // Add some labels
      PdfContentByte cb = writer.getDirectContent();
      cb.beginText();
      cb.setFontAndSize(BaseFont.createFont(), 12);
      cb.showTextAligned(PdfContentByte.ALIGN_RIGHT, "First Name:", 164, 687, 0);
      cb.showTextAligned(PdfContentByte.ALIGN_RIGHT, "Last Name:", 164, 630, 0);
      cb.endText();

      // Add the text fields
      addTextField(writer, "firstName", 679);
      addTextField(writer, "lastName", 622);
   }

   /**
    * Adds a single text field to the PDF document.
    *
    * @param writer The PdfWriter to use for adding text fields to the PDF document.
    * @param fieldName The name of the field, which will be name to use for submitting the data in this text field.
    * @param y The y-position of the bottom of the text field in points, relative to the left bottom border of the page.
    * @throws IOException If something goes wrong while adding the text fields to the document.
    * @throws DocumentException If something goes wrong while creating the required PDF elements.
    */
   private void addTextField(
         final PdfWriter writer,
         final String fieldName,
         final float y) throws IOException, DocumentException {
      TextField textField = new TextField(writer, new Rectangle(170, y, 425, y + 21), fieldName);
      textField.setBorderColor(Color.BLACK);
      textField.setBackgroundColor(new GrayColor(0.9f));
      textField.setBorderWidth(1);
      textField.setBorderStyle(PdfBorderDictionary.STYLE_BEVELED);
      textField.setAlignment(Element.ALIGN_LEFT);
      textField.setOptions(TextField.REQUIRED);
      writer.addAnnotation(textField.getTextField());
   }

   /**
    * Adds a single submit button to the PDF document.
    *
    * @param writer The PdfWriter to use for adding text fields to the PDF document.
    * @throws IOException If something goes wrong while adding the text fields to the document.
    * @throws DocumentException If something goes wrong while creating the required PDF elements.
    */
   private void addSubmitButton(final PdfWriter writer) throws IOException, DocumentException {
      PushbuttonField button = new PushbuttonField(writer, new Rectangle(170, 558, 255, 586), "submit");
      button.setText("Submit");
      button.setBackgroundColor(new GrayColor(0.7f));
      button.setVisibility(PushbuttonField.VISIBLE_BUT_DOES_NOT_PRINT);
      PdfFormField submit = button.getField();
      submit.setAction(PdfAction.createSubmitForm("fdf", null, 0));
      writer.addAnnotation(submit);
   }
}

Most of this code is used for generating the PDF document with some text and some form elements. If you ignore all PDF creation code, there is really not much going on. We just generate the document and return it. The most important part here is the submit button that is added to the document in the addSubmitButton method. We configure it to post to the FDFServlet, which was mapped to the fdf path.

Let's take a look at that FDFServlet now:
package nl.piraya.blog;

import com.lowagie.text.pdf.FdfReader;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Set;

/**
 * Servlet that accepts POSTing binary FDF data. The form data will be parsed and the results outputted to System.out.
 *
 * @author Onno Scheffers
 */
public class FDFServlet extends HttpServlet {
   /**
    * Handles the POST to this servlet.
    *
    * @param request An {@link HttpServletRequest} object that contains the request the client has made of the servlet.
    * @param response An {@link HttpServletResponse} object that contains the response the servlet sends to the client.
    * @exception IOException If an input or output error is detected when the servlet handles the POST request.
    * @exception ServletException If the request for the POST could not be handled.
    */
   protected void doPost(
         final HttpServletRequest request,
         final HttpServletResponse response
   ) throws ServletException, IOException {

      // Check if we are getting FDF data
      String contentType = request.getContentType();
      if("application/vnd.fdf".equalsIgnoreCase(contentType)) {
         try {
            // Parse the FDF data using iText
            FdfReader reader = new FdfReader(request.getInputStream());
            HashMap map = reader.getFields();
            Set keys = map.keySet();
            for(Object key : keys) {
               // Simply output the keys and their values to System.out
               String value = reader.getFieldValue(key.toString());
               System.out.println(key + " = " + value);
            }
            reader.close();

            // Forward the user to the next page in the application
            // If you want to keep the user in the same PDF instead, you can simply return the FDF data you received
            response.sendRedirect("done.jsp");

            return;
         } catch (Exception e) {
            // Ignore
         }
      }
      throw new IOException("Unable to read FDF data from request");
   }
}

We implement the doPost method here to accept binary data. By default PDF will submit its form data as FDF, although it also supports submitting as HTML, XDF and as a full PDF (if the PDF reader supports it, that is).

We use iText here again. This time we use it to parse the incoming FDF data and extract all form fields. We simply output the field names and their values to System.out and then we redirect the user to the done.jsp page, which simply thanks the user for submitting his or her data:

<!DOCTYPE html>
<html lang="en-us">
   <head>
      <meta charset="utf-8">
      <title>PDF Forms Example</title>
   </head>
   <body>
      <h1>PDF Form was sent</h1>

      <p>
         Thanks for sending your PDF Form.
      </p>
      <a href="index.jsp">back</a>
   </body>
</html>


That's it! A simple web application that creates a PDF form, embeds it inside a web page, allows the user to submit the form data and then parses the submitted data.

I hope you enjoyed this article.

Get the source code