Generating PDFs in your controllers

Generating binary content is a standard procedure in every web application, be it a dynamic generated image such as a CAPTCHA or user-specific document such as an invoice or an order confirmation. Play already supports the renderBinary() command in the controller to send binary data to the browser, however this is quite low level. This recipe shows how to combine the use of Apache FOP – which allows creation of PDF data out of XML-based templates – and the Play built-in templating mechanism to create customized PDF documents in real time.

You can find the source code of this example in the chapter2/pdf directory.

Getting ready

As there is already a PDF module included in Play, you should make sure you disable it in your application in order to avoid clashes. This of course only applies, if it has already been enabled before.

How to do it...

First you should download Apache FOP from http://www.apache.org/dyn/closer.cgi/xmlgraphics/fop and unpack it into your application. Get the ZIP file and unzip it so that there is a fop-1.0 directory in your application depending on your downloaded version.

Now you have to copy the JAR files into the lib/ directory, which is always included in the classpath when your application starts.

cp fop-1.0/build/fop.jar lib/
cp fop-1.0/lib/*.jar lib/
cp fop-1.0/examples/fo/basic/simple.fo app/views/Application/index.fo
rm lib/commons*

Make sure to remove the commons JAR files from the lib directory, as Play already provides them. In case of using Windows, you would have to use copy and del as commands instead of the Unix commands cp and rm. Instead of copying these files manually you could also add the entry to conf/dependencies.yml. However, you would have to exclude many dependencies manually, which can be removed as well.

Create a dummy User model, which is rendered in the PDF:

public class User {

        public String name = "Alexander";
        public String description = "Random: " + 
RandomStringUtils.randomAlphanumeric(20);
}

You should now replace the content of the freshly copied app/views/Application/index.fo file to resemble something from the user data like you would do it in a standard HTML template file in Play:

<fo:block font-size="18pt"
            ...
            padding-top="3pt">
    ${user.name}
</fo:block>

<fo:block font-size="12pt"
            ...
            text-align="justify">
    ${user.description}
</fo:block>

Change the application controller to call renderPDF() instead of render():

import static pdf.RenderPDF.renderPDF;

public class Application extends Controller {

    public static void index() {
        User user = new User();
        renderPDF(user);
    }
}

Now the only class that needs to be implemented is the RenderPDF class in the PDF package:

public class RenderPDF extends Result {

        private static FopFactoryfopFactory = FopFactory.newInstance();
        private static TransformerFactorytFactory = 
         TransformerFactory.newInstance();
        private VirtualFiletemplateFile;

        public static void renderPDF(Object... args) {
            throw new RenderPDF(args);
        }

        public RenderPDF(Object ... args) {
            populateRenderArgs(args);
            templateFile = getTemplateFile(args);
        }

        @Override
        public void apply(Request request, Response response) {
            Template template = TemplateLoader.load(templateFile);

            String header = "inline; filename="" + request.actionMethod + ".pdf"";
            response.setHeader("Content-Disposition", header);

            setContentTypeIfNotSet(response, "application/pdf");

            try {
                   Fop fop = fopFactory.newFop(MimeConstants.MIME_PDF, response.out);
                   Transformer transformer = tFactory.newTransformer();
                   Scope.RenderArgsargs = Scope.RenderArgs.current();
                   String content = template.render(args.data);
                   InputStream is = IOUtils.toInputStream(content);
                   Source src = new StreamSource(is);
                   javax.xml.transform.Result res = new SAXResult(fop.getDefaultHandler());
                   transformer.transform(src, res);
            } catch (FOPException e) {
                   Logger.error(e, "Error creating pdf");
            } catch (TransformerException e) {
                   Logger.error(e, "Error creating pdf");
            }
        }

        private void populateRenderArgs(Object ... args) {
            Scope.RenderArgsrenderArgs = Scope.RenderArgs.current();
            for (Object o : args) {
                   List<String> names = LocalVariablesNamesTracer.getAllLocalVariableNames(o);
                   for (String name : names) {
                       renderArgs.put(name, o);
                   }
            }
            renderArgs.put("session", Scope.Session.current());

            renderArgs.put("request", Http.Request.current());
            renderArgs.put("flash", Scope.Flash.current());
            renderArgs.put("params", Scope.Params.current());
            renderArgs.put("errors", Validation.errors());
        }

        private VirtualFilegetTemplateFile(Object ... args) {
            final Http.Request request = Http.Request.current();
            String templateName = null;
            List<String>renderNames = LocalVariablesNamesTracer.getAllLocalVariableNames(args[0]);
            if (args.length> 0 &&args[0] instanceof String &&renderNames.isEmpty()) {
                templateName = args[0].toString();
            } else {
                templateName = request.action.replace(".", "/") + ".fo";
            }

            if (templateName.startsWith("@")) {
                templateName = templateName.substring(1);
                if (!templateName.contains(".")) {
                    templateName = request.controller + "." + templateName;
                }
                templateName = templateName.replace(".", "/") + ".fo";
            }

            VirtualFile file = VirtualFile.search(Play.templatesPath, templateName);
            return file;
        }

}

How it works...

Before trying to understand how this example works, you could also fire up the included example of this application under examples/chapter2/pdf and open http://localhost:9000/ which will show you a PDF that includes the user data defined in the entity.

When opening the PDF, an XML template is rendered by the Play template engine and later processed by Apache FOP. Then it is streamed to the client. Basically, there is a new renderPDF() method created, which does all this magic. This method is defined in the pdf.RenderPDF class. All you need to hand over is a user to render.

The RenderPDF is only a rendering class, similar to the DigestRequest class in the preceding recipe. It consists of a static renderPDF() method usable in the controller and of three additional methods.

The getTemplateFile() method finds out which template to use. If no template was specified, a template with the name as the called method is searched for. Furthermore it is always assumed that the template file has a .fo suffix. The VirtualFile class is a Play helper class, which makes it possible to use files inside archives (like modules) as well. The LocalVariablesNamesTracer class allows you to get the names and the objects that should be rendered in the template.

The populateRenderArgs() method puts all the standard variables into the list of arguments which are used to render the template, for example, the session or the request.

The heart of this recipe is the apply() method, which sets the response content type to application/pdf and uses the Play built-in template loader to load the .fo template. After initializing all required variables for ApacheFOP, it renders the template and hands the rendered string over to the FOP transformer. The output of the PDF creation has been specified when calling the FopFactory. It goes directly to the output stream of the response object.

There's more...

As you can see, it is pretty simple in Play to write your own renderer. You should do this whenever possible, as it keeps your code clean and allows clean splitting of view and controller logic. You should especially do this to ensure that complex code such as Apache FOP does not sneak in to your controller code and make it less readable.

This special case poses one problem. Creating PDFs might be a long running task. However, the current implementation does not suspend the request. There is a solution to use the await() code from the controller in your own responses as seen in Chapter 1.

More about Apache FOP

Apache FOP is a pretty complex toolkit. You can create really nifty PDFs with it; however, it has quite a steep learning curve. If you intend to work with it, read the documentation under http://xmlgraphics.apache.org/fop/quickstartguide.html and check the examples directory (where the index.fo file used in this recipe has been copied from).

Using other solutions to create PDFs

There are many other solutions, which might fit your needs better than one based on xsl-fo. Libraries such as iText can be used to programmatically create PDFs. Perhaps even the PDF module available in the module repository will absolutely fit your needs.

See also

There is also the recipe Writing your own renderRSS method as controller output for writing your own RSS renderer at the end of this chapter.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.97.170