Friday, March 5, 2010

Easy PDF generation with Seam

Seam is a fantastic integration framework that can do many things. It is mostly known for its integration with JSF 1.x making it actually usable and adding a lot of features that have now become standard in JSF 2.0 i.e. page actions, factory methods, better navigation, etc. 
But there are many more features worth mentioning: JPA lazy loading, BPM support in web applications, integration with quartz, iText integration and many more.

iText integration
The iText library is a widely used open source Java library for generating PDF documents. However, using its programatic API to construct a PDF document is time consuming (think building a XML document using DOM or construct a UI using Swing). Seam neatly ties iText, JSF, and Facelets together and allows developers to declaratively write PDF pages with dynamic content, just as you would do for JSF web pages. Furthermore, you can now use templates in PDF pages.

The key to do this the special XHTML tag library for PDF elements that transparently calls iText when the page is rendered. Let's see a simple example of how we to use the iText integration to send a number of PDF documents attached in an email.
@Name("pdfSender")
@AutoCreate
public class PdfSender {

    @In
    private Renderer renderer;

    @In
    private PdfAssembler pdfAssembler;

    @In
    private FtpHandler ftpHandler;

    public void sendPdf() throws IOException, ParseException {

        Map<String, String> files = ftpHandler.getData();
        if (files.isEmpty()) {
            throw new IllegalArgumentException("No data found on FTP server");
        }

        List<Pdf> pdfList = processFTPData(files);
        Contexts.getEventContext().set("pdfList", pdfList);
        renderer.render("/pdf/sendPdf.xhtml");  //sends mail
        ftpHandler.deleteFiles();
    }

    private List<Pdf> processFTPData(Map<String, String> files) throws ParseException {

        List<Pdf> pdfList = new ArrayList<Pdf>();
        for (final String location : files.keySet()) {
            final String data = files.get(location);
            if (StringUtils.isEmpty(data)) {
                throw new IllegalArgumentException("Empty data file for: " + location);
            }
            pdfList.add(pdfAssembler.assemblePdf(location, data));
        }

        return pdfList;
    }
}

We just gather a bunch of data files via FTP, process them into a collection of DTOs representing the PDF structure and then add it to Seam's event context.  The DTOs are used to transfer data to the view (xhtml templates). Next we just "render" the mail with the attachments.

<?xml version="1.0" encoding="UTF-8"?>
<m:message xmlns="http://www.w3.org/1999/xhtml"
           xmlns:ui="http://java.sun.com/jsf/facelets"
           xmlns:h="http://java.sun.com/jsf/html"
           xmlns:p="http://jboss.com/products/seam/pdf"
           xmlns:m="http://jboss.com/products/seam/mail">

    <m:header name="X-Sent-From" value="PDFGenerator"/>
    <m:from name="PDF Generator" address="#{mailAddress}"/>
    <m:to name="Manuel Palacio">#{mailAddress}</m:to>
    <m:subject>PDF</m:subject>
    <m:body>
        <ui:repeat value="#{pdfList}" var="pdf">
            <m:attachment fileName="#{pdf.location}.pdf">
                <ui:include src="/pdf/pdf.xhtml"/>
            </m:attachment>
        </ui:repeat>
    </m:body>

</m:message> 
 
Pdf.xhtml generates the sections of the document

<p:document xmlns:ui="http://java.sun.com/jsf/facelets"
            xmlns:f="http://java.sun.com/jsf/core"
            xmlns:c="http://java.sun.com/jsp/jstl/core"
            xmlns:p="http://jboss.com/products/seam/pdf"
            title="Report"
            keywords="Report #{pdf.artNr}"
            subject="Report #{pdf.artNr}"
            author="MP"
            creator="MP">


   
     <ui:repeat value="#{pdf.sections}" var="section" varStatus="status">
        <p:chapter number="#{status.index + 1}">
            <p:title><p:font size="18"><p:paragraph spacingAfter="12"
                                                    alignment="center">#{section.title}</p:paragraph></p:font></p:title>

            <ui:repeat value="#{section.areas}" var="area">
                <p:section>
                    <p:title>
                        <p:font size="8" style="bold"><p:paragraph
                                spacingBefore="10">#{area.name}</p:paragraph></p:font>
                    </p:title>
                    <p:paragraph spacingBefore="1" spacingAfter="0" leading="10" keepTogether="true">
                        <ui:include src="/pdf/displayObject.xhtml"/>
                    </p:paragraph>
                </p:section>
            </ui:repeat>
        </p:chapter>
    </ui:repeat>

</p:document>


DisplayObject.xhtml generates the data in each section

<ui:repeat value="#{area.displayObjects}" var="displayObject" xmlns:ui="http://java.sun.com/jsf/facelets"
           xmlns:p="http://jboss.com/products/seam/pdf">
    <p:paragraph>
        <p:font size="8" style="bold">#{displayObject.name}</p:font>
        <p:font size="8">&#160;(#{displayObject.formattedId})</p:font>
        <p:font size="8" rendered="#{not empty displayObject.address}">&#160;#{displayObject.address}</p:font>
    </p:paragraph>
    <ui:repeat value="#{displayObject.events}" var="event">
        <p:paragraph>         
            <p:font size="7">#{event.text}&#160;#{event.date}</p:font>
         </p:paragraph>
    </ui:repeat>
    <p:paragraph spacingAfter="5"/>
</ui:repeat>

It takes time to learn how to tweak all the parameters to make the PDF look the way you want but it's definitely much easier and intuitive than using the iText API directly. For very advances formatting you may want to look at jasperreports.

Now we could define a cron job that runs the sendPdf task at defined intervals.

@Name("schedulerController")
public class SchedulerController {

    @In
    ScheduleProcessor processor;
    
    /**
    Method called when framework is initialized
    */ 
    public void startScheduler(String cronExpression) throws IOException, ParseException, ExecutionException, InterruptedException {
        processor.createQuartzTimer(new Date(), cronExpression);
    }
}

/**
 * Timer class. The methods annotated as asynchronous will be called by Quartz or other implementations
 * as specified by @IntervalCron
 */
@Name("processor")
@AutoCreate
@Scope(ScopeType.APPLICATION)
public class ScheduleProcessor {


    @Logger
    private Log log;

    @In
    private PdfSender pdfSender;

    @Asynchronous
    public QuartzTriggerHandle createQuartzTimer(@Expiration Date when, @IntervalCron String interval) throws IOException, ParseException, ExecutionException, InterruptedException {
        
        log.info("[#0] Processing pdf document with interval #1", when, interval);
        pdfSender.sendPdf();

        return null;
    }
}

In components.xml we make sure the cron job is started when Seam is initialized

<?xml version="1.0" encoding="UTF-8"?>
<components xmlns="http://jboss.com/products/seam/components"
            xmlns:core="http://jboss.com/products/seam/core"
            xmlns:async="http://jboss.com/products/seam/async"
            xmlns:transaction="http://jboss.com/products/seam/transaction"
            xmlns:mail="http://jboss.com/products/seam/mail"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:schemaLocation=
                "http://jboss.com/products/seam/core http://jboss.com/products/seam/core-2.2.xsd
                 http://jboss.com/products/seam/async http://jboss.com/products/seam/async-2.2.xsd
                 http://jboss.com/products/seam/transaction http://jboss.com/products/seam/transaction-2.2.xsd
                 http://jboss.com/products/seam/mail http://jboss.com/products/seam/mail-2.2.xsd
                 http://jboss.com/products/seam/components http://jboss.com/products/seam/components-2.2.xsd">


    <core:init transaction-management-enabled="false"/>

    <transaction:no-transaction/>

    <mail:mail-session session-jndi-name="mail/pdfGeneratorSession"/>


    <component name="ftpHandler">
        <property name="ftpHost">localhost</property>
        <property name="ftpPort">2121</property>
        <property name="ftpUser">admin</property>
        <property name="ftpPassw">admin</property>
    </component>

    <async:quartz-dispatcher/>

    <factory name="mailAddress" scope="STATELESS" value="xxx@xxx.xx" auto-create="true"/>

    <event type="org.jboss.seam.postInitialization">
        <action execute="#{schedulerController.startScheduler('0 0/1 * * * ?')}"/>
    </event>


</components>

Seam has definitely made simple tasks like sending mail and generating PDFs even simpler and more elegant.

1 comment:

  1. Hi i have used template to generate pdf using seam. problem is the table copied into richTextEditor from word file doesn't display properly in pdf. I think because of mso classes. It has been raised as a major issue and release has been stopped. Please post the solution asap

    ReplyDelete