Using Controllers in Play Framework

September 20, 2011

Java

«»

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.getAll
			LocalVariableNames(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.

email

«»

Comments

comments