Skip to Content
h2. Introduction
In this article, we will look at a way of generating PDF documents from WebDynpro Java without the use of Adobe Document Services. We will be using Apache FOP (XSL:FO) java libraries to perform this task. Apache FOP can generate a wide variety of outputs that include PostScript, RTF, PCL and XML. We are primarily concerned with PDF output in our case. In this example we transform a Java Object (a WebDynpro Node in our case) into a PDF. We generate SAX events from the node that is fed into an  XSL transformation that generates a PDF. You can also use this to generate XML events directly from a Node. !http://xmlgraphics.apache.org/fop/0.95/images/EmbeddingExampleObj2PDF.png|height=30|alt=Object to PDF sequence|align=middle|width=359|src=http://xmlgraphics.apache.org/fop/0.95/images/EmbeddingExampleObj2PDF.png! h6. This image has been shamelessly sourced from the Apache fop site. I was too lazy to make my own graphic.  I had a look at an Apache FOP example where a Java Object is converted directly into PDF (see the links section below on how to get to this example). All I had to do was import the FOP libraries into WebDynpro and replace the Java Objects with WD nodes, a few tweaks later viola! I had a working Node PDF example. Make sure you at least glance at the example before attempting the WebDynpro version.  We have successfully pushed this method into production for some reports. The performance is very good (as you would expect from Apache). The XSL FO syntax itself is simple and  powerful. There are plenty of examples on the web from simple to extremely complex layouts for the PDF. At the end of the article, I have provided a list of sites that proved extremely helpful to me during my design and code phase. This is a fairly long article because of the code and verbose instructions. Most of the steps are pretty simple. h2. Sample Application In this application We will follow these steps to create a simple XSL:FO-WebDynpro application. * Create a new DC that holds jar files from the Apache FOP library. * Create a dependency between our WD Java App and the FOP library DC. * Create an Adapter Class (provided in this example) to convert the node to SAX events. * Create a simple XSLT file that defines the layout and content of the PDF. * Write some code to trigger the creation of the  PDF from the data in the Node  * Test 🙂 h3. Step 1 The base app is a very simple WebDynpro Java App that  displays some data from a node in a WebDynpro table. (I won’t go into the details of creating this app). The Node data is populated from an array that is initialized a few fields (all of type string). The node attached to the controller, is a simple value node called materials with a few fields (all of the node elements are of type string). Here is a screen shot of the controller’s context.  I added some code to the controller to populate the node with some data. You can see the controller code {code:html}here{code}. (this is the code without the FOP modifications).  Here is a screen shot of the apps output.  We will now modify this application to generate a PDF from the data we have in the node. h3. Step 2 You will need to download the latest version (0.95 at the time of writing) of the Apache XSL:FO jar files. You can download the zip file (fop-0.95-bin.zip) from: http://archive.apache.org/dist/xmlgraphics/fop/binaries/ We now need to create a Library DC that contains the jar files from Apache FOP. If you are not sure how to do this follow the instruction in this article. [How to use external JAR files in Web Dynpro development components | How to use external JAR files in Web Dynpro development components] h3. Step 3 We now need to do some set up so that our application can use the jar files from the library DC. In the Used DC section, add the Library DC.  h3. Step 4 We now need to create a few supporting classes to convert the data contained in the node to SAX events. We have a total of 4 new Java Classes and 1 XML file. The java files AbstractObjectReader and EasyGenerationContentHandlerProxy have been copied straight from the example provided by the Apache XSL:FO team. I only adjusted for the package name. Switch to the navigator view and navigate to the packages folder. Create a folder underneath the packages folder for holding your classes. (I created com/cmc/foputil). Follow your naming conventions to create your package structure.   h4. Source File: MaterialsInputSource.java /* 1. Created on Jan 10, 2009 * */ package com.cmc.foputil; import org.xml.sax.InputSource; import com.sap.tc.webdynpro.progmodel.api.IWDNode; /** 1. @author RaghaS */ public class MaterialsInputSource extends InputSource { private IWDNode materials; public MaterialsInputSource(IWDNode materials) { this.materials = materials; } public void setMaterials(IWDNode materials) { this.materials = materials; } public IWDNode getMaterials() { return materials; } }  h4. Source File: MaterialsXMLReader.java /* 1. Created on Jan 10, 2009 * */ package com.cmc.foputil; import java.io.IOException; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import com.cmc.foputil.AbstractObjectReader; import com.sap.tc.webdynpro.progmodel.api.IWDNode; import com.sap.tc.webdynpro.progmodel.api.IWDNodeElement; /** 1. @author RaghaS */ public class MaterialsXMLReader extends AbstractObjectReader { private static final com.sap.tc.logging.Location logger = com.sap.tc.logging.Location.getLocation(MaterialsXMLReader.class); private IWDNode materialsNode; public void parse(InputSource input) throws IOException, SAXException { if (input instanceof MaterialsInputSource) { this.materialsNode = ((MaterialsInputSource) input).getMaterials(); this.parse(); } else { throw new SAXException(“Unsupported Input Source”); } } public void parse() throws SAXException { handler.startDocument(); generateFor(materialsNode); handler.endDocument(); } public void generateFor(IWDNode materialsNode) throws SAXException { if (materialsNode == null) { throw new NullPointerException(“materialsNode is null”); } if (handler == null) { throw new IllegalStateException(“ContentHandler not set”); } handler.startElement(“materials”); for (int i = 0; i < materialsNode.size(); i++) { handler.startElement(“material_items”); IWDNodeElement element = materialsNode.getElementAt(i); handler.element(“id”,(String) element.getAttributeValue(“id”)); handler.element(“name”,(String) element.getAttributeValue(“name”)); handler.element(“description”,(String) element.getAttributeValue(“description”)); handler.element(“qtyinstock”,(String) element.getAttributeValue(“qtyinstock”)); handler.element(“imgurl”,(String) element.getAttributeValue(“imgurl”)); handler.element(“producturl”,(String) element.getAttributeValue(“producturl”)); handler.endElement(“material_items”); } handler.endElement(“materials”); } } The next two source files have been directly taken from the examples provided by Apache FOP team. I have not modified them in anyway. I just paste these 2 files wherever I need them. You can do the same. h4. Source File: AbstractObjectReader.java /* 1. Licensed to the Apache Software Foundation (ASF) under one or more 1. contributor license agreements. See the NOTICE file distributed with 1. this work for additional information regarding copyright ownership. 1. The ASF licenses this file to You under the Apache License, Version 2.0 1. (the “License”); you may not use this file except in compliance with 1. the License. You may obtain a copy of the License at 1. 1. http://www.apache.org/licenses/LICENSE-2.0 1. 1. Unless required by applicable law or agreed to in writing, software 1. distributed under the License is distributed on an “AS IS” BASIS, 1. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 1. See the License for the specific language governing permissions and 1. limitations under the License. */ /* $Id: AbstractObjectReader.java 426576 2006-07-28 15:44:37Z jeremias $ */ package com.cmc.foputil; //Java import java.io.IOException; import java.util.Map; //SAX import org.xml.sax.SAXException; import org.xml.sax.InputSource; import org.xml.sax.XMLReader; import org.xml.sax.ContentHandler; import org.xml.sax.DTDHandler; import org.xml.sax.ErrorHandler; import org.xml.sax.EntityResolver; /** 1. This class can be used as base class for XMLReaders that generate SAX 1. events from Java objects. */ public abstract class AbstractObjectReader implements XMLReader { private static final String NAMESPACES = “http://xml.org/sax/features/namespaces“; private static final String NS_PREFIXES = “http://xml.org/sax/features/namespace-prefixes“; private Map features = new java.util.HashMap(); private ContentHandler orgHandler; /** Proxy for easy SAX event generation */ protected EasyGenerationContentHandlerProxy handler; /** Error handler */ protected ErrorHandler errorHandler; /** 1. Constructor for the AbstractObjectReader object */ public AbstractObjectReader() { setFeature(NAMESPACES, false); setFeature(NS_PREFIXES, false); } /* ============ XMLReader interface ============ */ /** 1. @see org.xml.sax.XMLReader#getContentHandler() */ public ContentHandler getContentHandler() { return this.orgHandler; } /** 1. @see org.xml.sax.XMLReader#setContentHandler(ContentHandler) */ public void setContentHandler(ContentHandler handler) { this.orgHandler = handler; this.handler = new EasyGenerationContentHandlerProxy(handler); } /** 1. @see org.xml.sax.XMLReader#getErrorHandler() */ public ErrorHandler getErrorHandler() { return this.errorHandler; } /** 1. @see org.xml.sax.XMLReader#setErrorHandler(ErrorHandler) */ public void setErrorHandler(ErrorHandler handler) { this.errorHandler = handler; } /** 1. @see org.xml.sax.XMLReader#getDTDHandler() */ public DTDHandler getDTDHandler() { return null; } /** 1. @see org.xml.sax.XMLReader#setDTDHandler(DTDHandler) */ public void setDTDHandler(DTDHandler handler) { } /** 1. @see org.xml.sax.XMLReader#getEntityResolver() */ public EntityResolver getEntityResolver() { return null; } /** 1. @see org.xml.sax.XMLReader#setEntityResolver(EntityResolver) */ public void setEntityResolver(EntityResolver resolver) { } /** 1. @see org.xml.sax.XMLReader#getProperty(String) */ public Object getProperty(java.lang.String name) { return null; } /** 1. @see org.xml.sax.XMLReader#setProperty(String, Object) */ public void setProperty(java.lang.String name, java.lang.Object value) { } /** 1. @see org.xml.sax.XMLReader#getFeature(String) */ public boolean getFeature(java.lang.String name) { return ((Boolean) features.get(name)).booleanValue(); } /** 1. Returns true if the NAMESPACES feature is enabled. 1. @return boolean true if enabled */ protected boolean isNamespaces() { return getFeature(NAMESPACES); } /** 1. Returns true if the MS_PREFIXES feature is enabled. 1. @return boolean true if enabled */ protected boolean isNamespacePrefixes() { return getFeature(NS_PREFIXES); } /** 1. @see org.xml.sax.XMLReader#setFeature(String, boolean) */ public void setFeature(java.lang.String name, boolean value) { this.features.put(name, new Boolean(value)); } /** 1. @see org.xml.sax.XMLReader#parse(String) */ public void parse(String systemId) throws IOException, SAXException { throw new SAXException( this.getClass().getName() + ” cannot be used with system identifiers (URIs)”); } /** 1. @see org.xml.sax.XMLReader#parse(InputSource) */ public abstract void parse(InputSource input) throws IOException, SAXException; } h4. Source File: EasyGenerationContentHandlerProxy.java /* 1. Licensed to the Apache Software Foundation (ASF) under one or more 1. contributor license agreements. See the NOTICE file distributed with 1. this work for additional information regarding copyright ownership. 1. The ASF licenses this file to You under the Apache License, Version 2.0 1. (the “License”); you may not use this file except in compliance with 1. the License. You may obtain a copy of the License at 1. 1. http://www.apache.org/licenses/LICENSE-2.0 1. 1. Unless required by applicable law or agreed to in writing, software 1. distributed under the License is distributed on an “AS IS” BASIS, 1. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 1. See the License for the specific language governing permissions and 1. limitations under the License. */ /* $Id: EasyGenerationContentHandlerProxy.java 426576 2006-07-28 15:44:37Z jeremias $ */ package com.cmc.foputil; //SAX import org.xml.sax.ContentHandler; import org.xml.sax.Locator; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.AttributesImpl; /** 1. This class is an implementation of ContentHandler which acts as a proxy to 1. another ContentHandler and has the purpose to provide a few handy methods 1. that make life easier when generating SAX events. 1.
1. Note: This class is only useful for simple cases with no namespaces. */ public class EasyGenerationContentHandlerProxy implements ContentHandler { /** An empty Attributes object used when no attributes are needed. */ public static final Attributes EMPTY_ATTS = new AttributesImpl(); private ContentHandler target; /** 1. Main constructor. 1. @param forwardTo ContentHandler to forward the SAX event to. */ public EasyGenerationContentHandlerProxy(ContentHandler forwardTo) { this.target = forwardTo; } /** 1. Sends the notification of the beginning of an element. 1. @param name Name for the element. 1. @throws SAXException Any SAX exception, possibly wrapping another exception. */ public void startElement(String name) throws SAXException { startElement(name, EMPTY_ATTS); } /** 1. Sends the notification of the beginning of an element. 1. @param name Name for the element. 1. @param atts The attributes attached to the element. If there are no 1. attributes, it shall be an empty Attributes object. 1. @throws SAXException Any SAX exception, possibly wrapping another exception. */ public void startElement(String name, Attributes atts) throws SAXException { startElement(null, name, name, atts); } /** 1. Send a String of character data. 1. @param s The content String 1. @throws SAXException Any SAX exception, possibly wrapping another exception. */ public void characters(String s) throws SAXException { target.characters(s.toCharArray(), 0, s.length()); } /** 1. Send the notification of the end of an element. 1. @param name Name for the element. 1. @throws SAXException Any SAX exception, possibly wrapping another exception. */ public void endElement(String name) throws SAXException { endElement(null, name, name); } /** 1. Sends notifications for a whole element with some String content. 1. @param name Name for the element. 1. @param value Content of the element. 1. @throws SAXException Any SAX exception, possibly wrapping another exception. */ public void element(String name, String value) throws SAXException { element(name, value, EMPTY_ATTS); } /** 1. Sends notifications for a whole element with some String content. 1. @param name Name for the element. 1. @param value Content of the element. 1. @param atts The attributes attached to the element. If there are no 1. attributes, it shall be an empty Attributes object. 1. @throws SAXException Any SAX exception, possibly wrapping another exception. */ public void element(String name, String value, Attributes atts) throws SAXException { startElement(name, atts); if (value != null) { characters(value.toCharArray(), 0, value.length()); } endElement(name); } /* =========== ContentHandler interface =========== */ /** 1. @see org.xml.sax.ContentHandler#setDocumentLocator(Locator) */ public void setDocumentLocator(Locator locator) { target.setDocumentLocator(locator); } /** 1. @see org.xml.sax.ContentHandler#startDocument() */ public void startDocument() throws SAXException { target.startDocument(); } /** 1. @see org.xml.sax.ContentHandler#endDocument() */ public void endDocument() throws SAXException { target.endDocument(); } /** 1. @see org.xml.sax.ContentHandler#startPrefixMapping(String, String) */ public void startPrefixMapping(String prefix, String uri) throws SAXException { target.startPrefixMapping(prefix, uri); } /** 1. @see org.xml.sax.ContentHandler#endPrefixMapping(String) */ public void endPrefixMapping(String prefix) throws SAXException { target.endPrefixMapping(prefix); } /** 1. @see org.xml.sax.ContentHandler#startElement(String, String, String, Attributes) */ public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException { target.startElement(namespaceURI, localName, qName, atts); } /** 1. @see org.xml.sax.ContentHandler#endElement(String, String, String) */ public void endElement(String namespaceURI, String localName, String qName) throws SAXException { target.endElement(namespaceURI, localName, qName); } /** 1. @see org.xml.sax.ContentHandler#characters(char[], int, int) */ public void characters(char[] ch, int start, int length) throws SAXException { target.characters(ch, start, length); } /** 1. @see org.xml.sax.ContentHandler#ignorableWhitespace(char[], int, int) */ public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException { target.ignorableWhitespace(ch, start, length); } /** 1. @see org.xml.sax.ContentHandler#processingInstruction(String, String) */ public void processingInstruction(String target, String data) throws SAXException { this.target.processingInstruction(target, data); } /** 1. @see org.xml.sax.ContentHandler#skippedEntity(String) */ public void skippedEntity(String name) throws SAXException { target.skippedEntity(name); } } h3. Step 5 We are now done with the base setup for the application. All the libraries and supporting classes are in place for Apache FOP to do its magic. We will now create a sample XSLT file that will define the layout of the PDF file. There are some excellent sources on the web that you can use to modify this file to produce a sophisticated output. In the packages directory, create a file called materials.xml. (My NWDS for some reason did not like the extension .XSLT). So I stuck with .xml. h4. File : materials.xml h3. Step 6 We will now add a few methods in the controller that does the actual transformation and returns an object of type WebResource. In case of the controller and view methods, use the methods tab (or action tab in case of the view) to create the method so that NWDS creates the method template for you. h4. Controller Method : getTransformSource() //@@begin javadoc:getTransformSource()/** Declared method. *///@@endpublic javax.xml.transform.Source getTransformSource( com.sap.tc.webdynpro.progmodel.api.IWDNode node ){ //@@begin getTransformSource() return new SAXSource( new MaterialsXMLReader(), new MaterialsInputSource(wdContext.nodeMaterials())); //@@end } h4. Controller Method : generatePDF() //@@begin javadoc:generatePDF()/** Declared method. *///@@endpublic com.sap.tc.webdynpro.services.sal.url.api.IWDCachedWebResource generatePDF( ){ //@@begin generatePDF() File pdfFile = generatePDFFromNode(wdContext.nodeMaterials()); IWDCachedWebResource resource = null; if (pdfFile.length() != 0) { try { resource = WDWebResource.getPublicCachedWebResource( getBytesFromFile(pdfFile), WDWebResourceType.PDF, WDScopeType.CLIENTSESSION_SCOPE, wdThis .wdGetAPI() .getComponent() .getDeployableObjectPart(), pdfFile.getName()); } catch (Exception e) { logMessage(getStackTrace(e)); } } logMessage(“Finished PDF Build”); return resource; //@@end h4. Controller Method : generatePDFFormNode() //@@begin javadoc:generatePDFFromNode()/** Declared method. *///@@endpublic java.io.File generatePDFFromNode( com.sap.tc.webdynpro.progmodel.api.IWDNode dataNode ){ //@@begin generatePDFFromNode() File pdfFile = new File(“materials.pdf”); if (pdfFile.exists()) { pdfFile.delete(); } try { OutputStream out = new java.io.FileOutputStream(pdfFile); out = new BufferedOutputStream(out); /* do some FOP magic */ FOUserAgent foUserAgent = fopFactory.newFOUserAgent(); Fop fop = fopFactory.newFop(MimeConstants.MIME_PDF, foUserAgent, out); TransformerFactory factory = TransformerFactory.newInstance(); /* get the stream source for materials.xml */ InputStream is = null; String url = this .getClass() .getClassLoader() .getResource(“materials.xml”) .toExternalForm(); URL entryURL = new URL(url); is = entryURL.openStream(); /* do the XSLT transform magic */ Transformer transformer = factory.newTransformer(new StreamSource(is)); Source src = getTransformSource(dataNode); Result res = new SAXResult(fop.getDefaultHandler()); transformer.transform(src, res); out.close(); is.close(); } catch (IOException ioe) { logMessage(getStackTrace(ioe)); } catch (TransformerConfigurationException tce) { logMessage(getStackTrace(tce)); } catch (TransformerException te) { logMessage(getStackTrace(te)); } catch (Exception e){ logMessage(getStackTrace(e)); } return pdfFile; //@@end } h4. Controller Utilty Method(s) //@@begin javadoc:logMessage()/** Declared method. *///@@endpublic void logMessage( java.lang.String message ){ //@@begin logMessage() logger.debugT(message); //msgMgr.reportSuccess(message); //@@end}//@@begin javadoc:getStackTrace()/** Declared method. *///@@endpublic java.lang.String getStackTrace( java.lang.Exception e ){ //@@begin getStackTrace() final Writer result = new StringWriter(); final PrintWriter printWriter = new PrintWriter(result); e.printStackTrace(printWriter); return result.toString(); //@@end} Paste the following utility method and the line following it (fopfactory.newInstance()) in the others section (after //@begin others) at the end of the controller code. //@@begin others/* decompose a File into a byte array */public static byte[] getBytesFromFile(File file) throws IOException{ InputStream is = new FileInputStream(file); /* Get the size of the file */ long length = file.length(); if (length > Integer.MAX_VALUE) { throw new IOException( “Generated file size limit exceeded for : ” + file.getName()); } /* Create the byte array to hold the data */ byte[] bytes = new byte[(int) length]; /* Read in the bytes */ int offset = 0; int numRead = 0; while (offset < bytes.length && (numRead = is.read(bytes, offset, bytes.length – offset)) >= 0) { offset += numRead; } /* Ensure all the bytes have been read in */ if (offset < bytes.length) { throw new IOException( “Could not completely read file ” + file.getName()); } /* Close the input stream and return bytes */ is.close(); return bytes; } private FopFactory fopFactory = FopFactory.newInstance(); See the complete Controller Code with the FOP modifications {code:html}here{code}. Now add a button below the table and associate an action with the button (or use the apply template function which will create a button and an action for you) I called my action genPDF. Copy the following code into genPDF method. //@@begin javadoc:onActiongenPDF(ServerEvent)/** Declared validating event handler. *///@@endpublic void onActiongenPDF(com.sap.tc.webdynpro.progmodel.api.IWDCustomEvent wdEvent ){ //@@begin onActiongenPDF(ServerEvent) IWDCachedWebResource resource = wdThis.wdGetApacheFopController().generatePDF(); try { wdComponentAPI .getWindowManager() .createExternalWindow(resource.getURL(), “Open Orders”, false) .open(); } catch (WDURLException e) { // TODO Auto-generated catch block e.printStackTrace(); } //@@end} That’s it we are done 🙂 Now build (hopefully you have no errors at this point) and deploy the application.  h3. Test Launch the application and you should see something similar to the screen below. Click on the generate PDF button and you should see a PDF pop up which contains data from the node.
To report this post you need to login first.

2 Comments

You must be Logged on to comment or reply to a post.

    1. Santosh Raghavan Post author
      Hey Sergio,

      Thanks for the comment. There are several reasons that I chose to go with FOP for certain apps.

      1) I needed good PDF bookmarking support. (the bookmarks are generated on the fly based on the data)
      2) Dynamic (and conditional) formatting is easy with FOP. XSLT syntax is easy to learn.
      3) Good control over the document layout.
      4) ADS has a lot more layers to support interactive forms. I just wanted a quick and easy way to merger data with a layout to generate  a PDF doc.

      I would love to hear your thoughts. Any helpful suggestions or direction is welcome.

      Santosh

      (0) 

Leave a Reply