Tuesday, December 14, 2010

Using LiveCycle Assembler to Create a PDF Portfolio via Webservices

Introduction

This post is about using Adobe LiveCycle ES2.5 Assembler services to create dynamic PDF Portfolios via Webservices (SOAP).

My actual  implementation is done in 4th Dimension but the technique works from any language/environment that provides access to external SOAP services. In fact, the only reference to 4D is the one above, no 4D code will be shown/covered here.

Additional documentation on LCES2.5 services can be found here:



Problem Description

Now, on to the problem I had to solve. My requirement was to generate a PDF Portfolio from a collection of documents assigned to a Patient record. All those documents (PDFs, Word, Excel, graphs, etc...) were held in a Patient's database and available to the application. So managing the documents was already dealt with, all I had to do was to generate a PDF Portfolio, given a Patient record and a list of documents to add to the Portfolio.

One extra requirement was the ability to add a front page with Patient details, like name, date of birth, gender, etc. (a front PDF inside the Portfolio).

That extra requirement demanded an extra call to Assembler, to create that front PDF, which is then used along the other Patient documents to assemble the Portfolio.

Let's dig into how the above was achieved...

DDX A very powerful tool

Before delving into the Webservices call properly, one needs to get a grip on DDX. DDX stands for "Document Description XML", a tag language that describes how to assemble/build/create PDF documents. That is the language used by LCES Assembler services. It is powerful and complex, and the documentation is a bit hard to follow. The worst part is that there are very few examples, and as usual none of the examples really addressed my needs. Thus creating the DDX for my case was a long trial and error process.

Fortunately, along the process I found a great tool to help creating/testing DDX scripts. It is built into LCES Workbench ES2 and is called Document Builder. You can use it to create and try your DDX before hand.

The PDF Portfolio DDX

The first thing to do is to create a DDX to assemble the Patient's documents. The XML script is dynamically built from the list of documents passed to the routine.

Here is a sample one:


<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<DDX xmlns="http://ns.adobe.com/DDX/1.0/">
<PDF result="ThePackage.pdf">
<Portfolio>
<Navigator source="AdobeRevolve.nav"/>
</Portfolio>
<PackageFiles>
<PDF baseDocument="true" source="cover"/>
</PackageFiles>
<Folder name="Research">
<PackageFiles>
<PDF source="Doc1.pdf"/>
<PDF source="Doc2.pdf"/>
</PackageFiles>
</Folder>
</PDF>
</DDX>

The area marked in red is the dynamic portion of the DDX. The rest is pretty much fixed (for this special usage). Let's analyze each part:

The outmost PDF element describes the output document, or the "result" PDF. All the elements inside will be used to produce that "result" PDF. (Assembler Service allows you to create multiple result PDFs)

What makes the resulting document a PDF Portfolio is the presence of the Portfolio and PackageFiles elements. The Portfolio element informs which Portfolio Navigator to use. (Adobe provides some standard Navigators but you can roll your own, Navigators are Flex apps).

The PackageFiles elements list the documents that go into the PDF Portfolio and Folder elements allows one to group documents into a folder/directory hierarchy, which can then be used to navigate the PDF Portfolio.

The first PackageFiles element lists my Patient cover page, which must go at the front of the PDF Portfolio. After that I dynamically generate the elements that comprise the PDF Portfolio contents, based on the submitted document list. The order of the elements here is important because the PDF Portfolio will be assembled on that input order.

Please note that the PDF elements inside the PackageFiles element are "source" documents. Although the element name is PDF, they can refer to any type of document, PDF or not. The source attribute is not necessarily the document name. That is actually a reference to the input map sent along with the DDX in the Assembler invoke call. (see below).

The Cover Page DDX

The Patient cover page uses a PDF Form with fields for patient name, DOB, gender, etc... Those fields are filled in from XML data. The resulting PDF is then used in the Portfolio.


<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<DDX xmlns="http://ns.adobe.com/DDX/1.0/">
<PDF result="cover.pdf">
<PDF source="cover">
<XFAData source="coverData"/>
</PDF>
</PDF>
</DDX>

The cover page DDX is pretty much static as the variable Patient data comes as the XFA XML data. That is referenced via the XFAData element. Same as above, the source attribute is a reference to the actual XML data sent along in the Assembler invoke call input map.

The XML data looks something like:


<?xml version="1.0" encoding="UTF-8"?>
<topmostSubform>
<PatientName>SMITH, JOE</PatientName>
<PHN>123456789</PHN>
<Gender>M</Gender>
<DOB>Jan 1, 1978</DOB>
<CreatedOn>12/14/2010 at 18:29:48</CreatedOn>
</topmostSubform>


Invoking the Assembler Service

The Assembler service SOAP call is described by its WSDL which is accessible via

http://<your_serverhost>:<your_port>/soap/services/AssemblerService?wsdl


The service you want is the invoke SOAP call and it takes a XML that includes a DDX script, plus an input map listing the documents referenced by the DDX and options that control the assembly process.

Here is the invoke XML used for the sample DDX to create the PDF Portfolio:


<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<invoke xmlns="http://adobe.com/idp/services">
  <inDDXDoc>
    <contentType>text/xml</contentType>
    <binaryData>...base64 encoded DDX...</binaryData>
  </inDDXDoc>
  <inputs>
    <item>
      <key>Doc1.pdf</key>
      <value xsi:type="BLOB">
        <remoteURL>http://...</remoteURL>
      </value>
    </item>
    <item>
      <key>Doc2.pdf</key>
      <value xsi:type="BLOB">
        <remoteURL>http://...</remoteURL>
      </value>
    </item>
    <item>
      <key>cover</key>
      <value xsi:type="BLOB">
        <remoteURL>http://...the link returned from the Create Cover call...</remoteURL>
      </value>
    </item>
  </inputs>
  <environment>
    <failOnError>false</failOnError>
    <logLevel>INFO</logLevel>
  </environment>
</invoke>

So, the DDX goes into the inDDXDoc/binaryData element as a base64 encoded stream. Each of the documents referenced in the DDX via a source attribute are supplied in the inputs element. That is an input map where each item is comprised of the elements: key and value. The key element gets the value of the corresponding source attribute, so there is the link between the documents referenced in the DDX and the actual data.

In the value element, the xsi:type="BLOB" is mandatory, it took me a lot of trial and error to find that out. That is the type of the data referenced by the value element, and not the type of the element contents, duh!

The value element contents can be either a remoteURL or binaryData. If a remoteURL is present, it should contain the URL to access the specific document. That URL will be called from LCES Server during the Assembler Service processing. A binaryData element can be used to send the actual document contents along, and it should be base64 encoded as well. It is up to you to decide how you want to send the documents along, as a URL or as binary data. Either way, the main point is that for each source attribute in the DDX, there must exist an item in inputs with the same corresponding key.

The final environment element provides options for the Assembler Service execution. The possible values are described in the Assembler Service API.

The Assembler Service Result


The Assembler Service invoke SOAP call returns a invokeResponse XML with some information on the Assembler process and if all goes well, a URI pointing to the resulting PDF.


<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<invokeResponse xmlns="http://adobe.com/idp/services">
  <result xmlns:ns1="http://adobe.com/idp/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="ns1:AssemblerResult">
    <documents xmlns:ns2="http://xml.apache.org/xml-soap" xsi:type="ns2:Map">
      <ns2:item>
        <ns2:key xsi:type="xsd:string">ThePackage.pdf</ns2:key>
        <ns2:value xsi:type="ns1:BLOB">
          <contentType>application/pdf</contentType>
          <remoteURL>http://...here is the result you want...</remoteURL>
        </ns2:value>
      </ns2:item>
    </documents>
    <failedBlockNames xsi:type="MyArrayOf_xsd_anyType"/>
    <jobLog xsi:type="ns1:BLOB">
      <contentType>text/xml</contentType>
      <remoteURL>http://... a link to the job log...</remoteURL>
    </jobLog>
    <multipleResultsBlocks xmlns:ns3="http://xml.apache.org/xml-soap" xsi:type="ns3:Map"/>
    <numRequestedBlocks xsi:type="xsd:int">1</numRequestedBlocks>
    <successfulBlockNames xsi:type="MyArrayOf_xsd_anyType">
      <item xsi:type="xsd:string">ThePackage.pdf</item>
    </successfulBlockNames>
    <successfulDocumentNames xsi:type="MyArrayOf_xsd_anyType">
      <item xsi:type="xsd:string">ThePackage.pdf</item>
    </successfulDocumentNames>
    <throwables xmlns:ns4="http://xml.apache.org/xml-soap" xsi:type="ns4:Map"/>
  </result>
</invokeResponse>

The documents element in the response will list all the documents produced from the DDX script. That is again an input Map with key and value elements. In our case we have only one PDF result and the value element is a remoteURL that points to the PDF Portfolio produced.

The PDF Portfolio URI points to a temporary document, which will be kept by LCES Document Manager only for a set time (all that is configurable).

Where does the Cover Page gets into that?

What about the Patient Cover page? Well, that gets created on a separate Assembler Service invoke similar to the above. The Patient Cover invokeReponse will contain a URI pointing to the built cover PDF, and that is then used in the PDF Portfolio invoke XML. That URI goes into the "cover" item's remoteURL.

For illustration here is the invoke XML example for the Patient Cover page creation:

<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<invoke xmlns="http://adobe.com/idp/services">
  <inDDXDoc>
    <contentType>text/xml</contentType>
    <binaryData>...base64 encoded DDX...</binaryData>
  </inDDXDoc>
  <inputs>
    <item>
      <key>cover</key>
      <value xsi:type="BLOB">
        <binaryData>...base64 encoded PDF Form...</binaryData>
      </value>
    </item>
    <item>
      <key>coverData</key>
      <value xsi:type="BLOB">
        <binaryData>...base64 encoded XFA XML data...</binaryData>
      </value>
    </item>
  </inputs>
  <environment>
    <failOnError>false</failOnError>
    <logLevel>INFO</logLevel>
  </environment>
</invoke>

Here I'm passing the PDF Form inline as well as the form's XML Data goes inline too.
The PDF Form was created with LiveCycle Designer, as a XFA Dynamic Form.

The invokeResponse is similar to the one returned in the PDF Portfolio invoke, and I get back a URI pointing to the Patient Cover page. I use it as the remoteURL in the PDF Portfolio invoke XML.

Summary

The code to do all that is pretty straightforward and I'll outline it here. You can implement it in your preferred language/environment:

  1. prepare the Patient Cover XML Data
  2. prepare the Patient Cover DDX
  3. prepare the Patient Cover invoke XML
  4. call AssemblerService to get the Patient Cover URI
  5. build the PDF Portfolio DDX, looping thru the document list and building the folder hierarchy
  6. prepare the PDF Portfolio invoke XML, using the Patient Cover URI from (4) and building the document references from the document list
  7. invoke AssemblerService and get the PDF Portfolio URI
  8. open the browser and point it to the PDF Portfolio URI
What's Next

There are a lot of improvements to the above, like adding password security to the PDF Portfolio, or using a custom Navigator, or using a template PDF Portfolio and adding the new documents to it, adding rights management to the resulting Portfolio, etc, etc.

Most of that can easily be accomplished simply by tweaking the DDX script. Go read the docs to see what is available.

Cheers,
julio


No comments:

Post a Comment