com.naryx.tagfusion.cfm.document.cfDOCUMENT.java Source code

Java tutorial

Introduction

Here is the source code for com.naryx.tagfusion.cfm.document.cfDOCUMENT.java

Source

/*
 *
 *  This file is part of Open BlueDragon (OpenBD) CFML Server Engine.
 *
 *  OpenBD is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  Free Software Foundation,version 3.
 *
 *  OpenBD is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with OpenBD.  If not, see http://www.gnu.org/licenses/
 *
 *  Additional permission under GNU GPL version 3 section 7
 *
 *  If you modify this Program, or any covered work, by linking or combining
 *  it with any of the JARS listed in the README.txt (or a modified version of
 *  (that library), containing parts covered by the terms of that JAR, the
 *  licensors of this Program grant you additional permission to convey the
 *  resulting work.
 *  README.txt @ http://www.openbluedragon.org/license/README.txt
 *
 *  http://www.openbluedragon.org/
 */

package com.naryx.tagfusion.cfm.document;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.io.StringReader;
import java.io.StringWriter;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.apache.commons.io.IOUtils;
import org.apache.http.Header;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.conn.params.ConnRoutePNames;
import org.apache.http.impl.client.DefaultHttpClient;
import org.aw20.io.StreamUtil;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.tidy.Tidy;
import org.xhtmlrenderer.pdf.DefaultPDFCreationListener;
import org.xhtmlrenderer.pdf.ITextFontResolver;
import org.xhtmlrenderer.pdf.ITextRenderer;
import org.xhtmlrenderer.pdf.PDFEncryption;
import org.xml.sax.InputSource;

import com.lowagie.text.DocumentException;
import com.lowagie.text.FontFactory;
import com.lowagie.text.pdf.BaseFont;
import com.lowagie.text.pdf.PdfName;
import com.lowagie.text.pdf.PdfString;
import com.lowagie.text.pdf.PdfWriter;
import com.nary.io.FileUtils;
import com.nary.io.multiOutputStream;
import com.naryx.tagfusion.cfm.engine.cfBinaryData;
import com.naryx.tagfusion.cfm.engine.cfData;
import com.naryx.tagfusion.cfm.engine.cfEngine;
import com.naryx.tagfusion.cfm.engine.cfSession;
import com.naryx.tagfusion.cfm.engine.cfmBadFileException;
import com.naryx.tagfusion.cfm.engine.cfmRunTimeException;
import com.naryx.tagfusion.cfm.engine.dataNotSupportedException;
import com.naryx.tagfusion.cfm.tag.cfOptionalBodyTag;
import com.naryx.tagfusion.cfm.tag.cfTag;
import com.naryx.tagfusion.cfm.tag.cfTagReturnType;
import com.naryx.tagfusion.cfm.tag.tagLocator;
import com.naryx.tagfusion.cfm.tag.tagReader;
import com.naryx.tagfusion.cfm.xml.cfXmlData;
import com.naryx.tagfusion.cfm.xml.parse.NoValidationResolver;
import com.naryx.tagfusion.xmlConfig.xmlCFML;

public class cfDOCUMENT extends cfTag implements cfOptionalBodyTag, Serializable {

    private static final long serialVersionUID = 1;

    private static final String TAG_NAME = "CFDOCUMENT";
    private String endMarker = null;

    public static final String CFDOCUMENT_KEY = "CFDOCUMENT";

    private static String[] defaultWindowsFontDirs = { "C:\\Windows\\Fonts", "C:\\WINNT\\Fonts" };
    private static String[] defaultOtherFontDirs = { "/usr/X/lib/X11/fonts/TrueType",
            "/usr/openwin/lib/X11/fonts/TrueType", "/usr/share/fonts/default/TrueType",
            "/usr/X11R6/lib/X11/fonts/ttf", "/usr/X11R6/lib/X11/fonts/truetype", "/usr/X11R6/lib/X11/fonts/TTF" };

    private static String[] defaultFontDirs;

    public static void init(xmlCFML configFile) {
        String fontDirs = cfEngine.getConfig().getString("server.fonts.dirs", "");

        if (fontDirs.length() == 0) { // no fonts configured, set defaults
            StringBuilder defaultFontDirsList = new StringBuilder();
            if (cfEngine.WINDOWS) {
                for (int i = 0; i < defaultWindowsFontDirs.length; i++) {
                    if (FileUtils.exists(defaultWindowsFontDirs[i])) {
                        if (defaultFontDirsList.length() > 0) { // not the first
                            defaultFontDirsList.append(',');
                        }
                        defaultFontDirsList.append(defaultWindowsFontDirs[i]);
                    }
                }
            } else {
                for (int i = 0; i < defaultOtherFontDirs.length; i++) {
                    if (FileUtils.exists(defaultOtherFontDirs[i])) {
                        if (defaultFontDirsList.length() > 0) { // not the first
                            defaultFontDirsList.append(',');
                        }
                        defaultFontDirsList.append(defaultOtherFontDirs[i]);
                    }
                }
            }

            if (defaultFontDirsList.length() > 0) {
                cfEngine.getConfig().setData("server.fonts.dirs", defaultFontDirsList.toString());
            }
            fontDirs = defaultFontDirsList.toString();
        }

        defaultFontDirs = fontDirs.split(",");
        for (int i = 0; i < defaultFontDirs.length; i++) {
            FontFactory.registerDirectory(defaultFontDirs[i].toString());
        }

    }

    public boolean doesTagHaveEmbeddedPoundSigns() {
        return false;
    }

    public String getEndMarker() {
        return endMarker;
    }

    public void setEndTag() {
        endMarker = null;
    }

    public void lookAheadForEndTag(tagReader inFile) {
        endMarker = (new tagLocator(TAG_NAME, inFile)).findEndMarker();
    }

    /*
    TODO: these attributes are still to be supported
    fontEmbed = "yes|no|selective" // use ITextResolver
    bookmark = "yes|no"
    scale = "percentage less than 100"
    localUrl = "yes|no"
    */

    protected void defaultParameters(String _tag) throws cfmBadFileException {
        defaultAttribute("ENCRYPTION", "none");
        defaultAttribute("FORMAT", "PDF");
        defaultAttribute("FONTEMBED", "true");
        defaultAttribute("MIMETYPE", "text/html");
        defaultAttribute("ORIENTATION", "PORTRAIT");
        defaultAttribute("OVERWRITE", "false");
        defaultAttribute("PAGETYPE", "letter");
        defaultAttribute("UNIT", "IN");
        defaultAttribute("USERAGENT", "OpenBD");
        defaultAttribute("BACKGROUNDVISIBLE", "true");

        parseTagHeader(_tag);
    }

    public cfTagReturnType render(cfSession _Session) throws cfmRunTimeException {

        if (!getDynamic(_Session, "FORMAT").getString().equalsIgnoreCase("PDF")) {
            throw newRunTimeException("Invalid FORMAT value. Only \"PDF\" is supported.");
        }

        if (containsAttribute("SRC") && containsAttribute("SRCFILE")) {
            throw newRunTimeException(
                    "Invalid attribute combination. Either the SRC or SRCFILE attribute must be specified but not both");
        }

        ITextRenderer renderer = new ITextRenderer();
        CreationListener listener = new CreationListener(getDynamic(_Session, "AUTHOR"),
                getDynamic(_Session, "TITLE"), getDynamic(_Session, "SUBJECT"), getDynamic(_Session, "KEYWORDS"));
        renderer.setListener(listener);
        resolveFonts(_Session, renderer);

        if (_Session.getDataBin(CFDOCUMENT_KEY) != null) {
            throw newRunTimeException("CFDOCUMENT cannot be embedded within another CFDOCUMENT tag");
        }

        _Session.setDataBin(CFDOCUMENT_KEY, new DocumentContainer());

        String renderedBody = renderToString(_Session).getOutput();
        try {
            DocumentContainer container = (DocumentContainer) _Session.getDataBin(CFDOCUMENT_KEY);

            List<DocumentSection> sections = container.getSections();
            if (sections.size() == 0) {
                // if no sections are specified then construct one from this tag
                DocumentSection section = new DocumentSection();
                section.setHeader(container.getMainHeader(), container.getMainHeaderAlign());
                section.setFooter(container.getMainFooter(), container.getMainFooterAlign());

                if (renderedBody.length() == 0 && !(containsAttribute("SRC") || containsAttribute("SRCFILE"))) {
                    throw newRunTimeException("Cannot create a PDF from an empty document!");
                }

                String src = containsAttribute("SRC") ? getDynamic(_Session, "SRC").getString() : null;
                String srcFile = containsAttribute("SRCFILE") ? getDynamic(_Session, "SRCFILE").getString() : null;

                section.setSources(src, srcFile, renderedBody);
                appendSectionAttributes(_Session, section);

                sections.add(section);
            }

            DocumentSettings settings = getDocumentSettings(_Session, container);

            // If there is more than 1 section and page counters are used that need special
            // processing then we need to do an initial conversion of the HTML to PDF to
            // determine how many pages are created per section and how many pages are created total.
            if ((sections.size() > 1) && (container.usesTotalPageCounters()))
                preparePageCounters(_Session, renderer, sections, settings);

            preparePDF(_Session, renderer, sections, settings);

            return cfTagReturnType.NORMAL;
        } finally {
            _Session.deleteDataBin(CFDOCUMENT_KEY);
        }

    }

    private void resolveFonts(cfSession _Session, ITextRenderer _renderer)
            throws dataNotSupportedException, cfmRunTimeException {
        ITextFontResolver resolver = _renderer.getFontResolver();

        boolean embed = getDynamic(_Session, "FONTEMBED").getBoolean();
        for (int i = 0; i < defaultFontDirs.length; i++) {
            File nextFontDir = new File(defaultFontDirs[i]);
            File[] fontFiles = nextFontDir.listFiles(new FilenameFilter() {
                public boolean accept(File _dir, String _name) {
                    String name = _name.toLowerCase();
                    return name.endsWith(".otf") || name.endsWith(".ttf");
                }
            });
            if (fontFiles != null) {
                for (int f = 0; f < fontFiles.length; f++) {
                    try {
                        resolver.addFont(fontFiles[f].getAbsolutePath(), BaseFont.IDENTITY_H, embed);
                    } catch (Exception ignored) {
                    } // ignore fonts that can't be added
                }
            }
        }
    }

    /*
     * preparePageCounters
     * 
     * This method is expensive because it requires us to convert the HTML to PDF to
     * determine the total page counters. This method should only be called if there is more
     * than one section and one of the following page counter variables is being used:
     * 
     *   1. TotalPageCount
     *   2. TotalSectionPageCount
     */
    private void preparePageCounters(cfSession _Session, ITextRenderer _renderer, List<DocumentSection> _sections,
            DocumentSettings _settings) throws cfmRunTimeException {
        OutputStream pdfOut = null;
        try {
            pdfOut = new NullOutputStream();

            DocumentSection nextSection = _sections.get(0);
            if (nextSection.pageCounterConflict())
                throw newRunTimeException(
                        "OpenBD doesn't support currentpagenumber and currentsectionpagenumber in same section.");
            String renderedBody = getRenderedBody(_Session, nextSection, _settings, _sections.size());
            _renderer.setDocument(getDocument(renderedBody), nextSection.getBaseUrl(_Session));
            _renderer.layout();
            _renderer.createPDF(pdfOut, false);

            int currentPageNumber = _renderer.getWriter().getCurrentPageNumber();
            nextSection.setTotalSectionPageCount(currentPageNumber);
            int totalPageCount = currentPageNumber;

            for (int i = 1; i < _sections.size(); i++) {
                nextSection = _sections.get(i);
                if (nextSection.pageCounterConflict())
                    throw newRunTimeException(
                            "OpenBD doesn't support currentpagenumber and currentsectionpagenumber in same section.");
                renderedBody = getRenderedBody(_Session, nextSection, _settings, _sections.size());
                _renderer.setDocument(getDocument(renderedBody), nextSection.getBaseUrl(_Session));
                _renderer.layout();
                _renderer.writeNextDocument(_renderer.getWriter().getCurrentPageNumber() + 1);
                currentPageNumber = _renderer.getWriter().getCurrentPageNumber();
                nextSection.setTotalSectionPageCount(currentPageNumber - totalPageCount);
                totalPageCount = currentPageNumber;
            }

            for (int i = 0; i < _sections.size(); i++) {
                nextSection = _sections.get(i);
                nextSection.setTotalPageCount(totalPageCount);
            }

        } catch (DocumentException e) {
            throw newRunTimeException("Failed to create PDF due to DocumentException: " + e.getMessage());
        } finally {
            if (pdfOut != null)
                try {
                    pdfOut.close();
                } catch (IOException ignored) {
                }
        }
    }

    private void preparePDF(cfSession _Session, ITextRenderer _renderer, List<DocumentSection> _sections,
            DocumentSettings _settings) throws cfmRunTimeException {
        OutputStream pdfOut = null;
        try {
            ByteArrayOutputStream bos = null;

            if (containsAttribute("FILENAME")) {
                File pdfFile = new File(getDynamic(_Session, "FILENAME").getString());
                if (pdfFile.exists() && !getDynamic(_Session, "OVERWRITE").getBoolean()) {
                    throw newRunTimeException("PDF file already exists and overwrite is disabled.");
                }
                pdfOut = cfEngine.thisPlatform.getFileIO().getFileOutputStream(pdfFile);

                if (containsAttribute("NAME")) {
                    bos = new ByteArrayOutputStream();
                    pdfOut = new multiOutputStream(pdfOut, bos);
                }
            } else if (containsAttribute("NAME")) {
                pdfOut = new ByteArrayOutputStream();

            } else {
                _Session.resetBuffer();

                // The SAVEASNAME attribute as been tested with the following:
                //
                //    IE8 - Page/Save As : FAILS
                //    IE8 - PDF plugin save image : FAILS
                //    Firefox 3.5.8 - File/Save Page As : WORKS
                //    Firefox 3.5.8 - PDF plugin save image : FAILS
                //
                String saveAsName;
                if (containsAttribute("SAVEASNAME")) {
                    saveAsName = getDynamic(_Session, "SAVEASNAME").toString();
                } else {
                    // Extract the filename from the path and use it as the save as name
                    saveAsName = _Session.REQ.getServletPath();
                    int slash = saveAsName.lastIndexOf('/');
                    if (slash != -1)
                        saveAsName = saveAsName.substring(slash + 1);
                    int dot = saveAsName.lastIndexOf('.');
                    if (dot != -1)
                        saveAsName = saveAsName.substring(0, dot) + ".pdf";
                }

                pdfOut = new SessionOutputStream(_Session, saveAsName);
            }

            // handle encryption/password if attributes are set
            if (containsAttribute("OWNERPASSWORD") || containsAttribute("USERPASSWORD")
                    || containsAttribute("PERMISSIONS")
                    || !getDynamic(_Session, "ENCRYPTION").getString().equalsIgnoreCase("none")) {
                PDFEncryption mEnc = new PDFEncryption();
                setPermissions(_Session, mEnc);
                _renderer.setPDFEncryption(mEnc);
            }

            DocumentSection nextSection = _sections.get(0);
            String renderedBody = getRenderedBody(_Session, nextSection, _settings, _sections.size());
            _renderer.setDocument(getDocument(renderedBody), nextSection.getBaseUrl(_Session));
            _renderer.layout();
            _renderer.createPDF(pdfOut, false);

            for (int i = 1; i < _sections.size(); i++) {
                nextSection = _sections.get(i);
                renderedBody = getRenderedBody(_Session, nextSection, _settings, _sections.size());
                _renderer.setDocument(getDocument(renderedBody), nextSection.getBaseUrl(_Session));
                _renderer.layout();
                if (nextSection.usesCurrentPageNumber()) {
                    // uses currentpagenumber so start page numbering with current page number + 1
                    _renderer.writeNextDocument(_renderer.getWriter().getCurrentPageNumber() + 1);
                } else {
                    // uses currentsectionpagenumber so start page numbering with 1
                    _renderer.writeNextDocument(1);
                }
            }

            _renderer.finishPDF();

            if (pdfOut instanceof SessionOutputStream) {
                if (((SessionOutputStream) pdfOut).getException() != null) {
                    throw ((SessionOutputStream) pdfOut).getException();
                }
                _Session.pageFlush();
                _Session.abortPageProcessing();
            }

            // Add the data to our session
            if (containsAttribute("NAME")) {
                if (bos != null)
                    _Session.setData(getDynamic(_Session, "NAME").toString(), new cfBinaryData(bos.toByteArray()));
                else
                    _Session.setData(getDynamic(_Session, "NAME").toString(),
                            new cfBinaryData(((ByteArrayOutputStream) pdfOut).toByteArray()));
            }

        } catch (DocumentException e) {
            throw newRunTimeException("Failed to create PDF due to DocumentException: " + e.getMessage());
        } catch (IOException e) {
            throw newRunTimeException("Error writing PDF to file. Check the file exists and can be written to.");
        } finally {
            if (pdfOut != null)
                try {
                    pdfOut.close();
                } catch (IOException ignored) {
                }
        }
    }

    private String getRenderedBody(cfSession _Session, DocumentSection _section, DocumentSettings _settings,
            int _numSections) throws cfmRunTimeException {
        String renderedBody = _section.getBody();
        if (renderedBody.length() != 0) {
            // If the section had a mimetype specified and it's text/plain or if it didn't have a mimetype
            // specified and the document mimetype was set to text/plain then treat the body as plain text.
            if (((_section.getMimeType() != null) && (_section.getMimeType().equalsIgnoreCase("text/plain")))
                    || ((_section.getMimeType() == null)
                            && (getDynamic(_Session, "MIMETYPE").getString().equalsIgnoreCase("text/plain"))))
                renderedBody = getXHTML("<pre>" + escapeHtmlChars(renderedBody) + "</pre>");
            else
                renderedBody = getXHTML(renderedBody);
        } else {
            renderedBody = getXHTML(retrieveDocument(_Session, _section, _settings));
        }
        renderedBody = insertStyles(_Session, renderedBody, _section, _settings,
                getDynamic(_Session, "BACKGROUNDVISIBLE").getBoolean(), _numSections);

        return renderedBody;
    }

    public Document getDocument(String _renderedBody) throws cfmRunTimeException {
        try {
            DocumentBuilder builder;
            InputSource is = new InputSource(new StringReader(_renderedBody));
            Document doc;
            DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
            builderFactory.setValidating(false);
            builder = builderFactory.newDocumentBuilder();
            builder.setEntityResolver(new NoValidationResolver());
            doc = builder.parse(is);
            return doc;
        } catch (Exception e) {
            throw newRunTimeException("Failed to create valid xhtml document due to " + e.getClass().getName()
                    + ": " + e.getMessage());
        }
    }

    private String getXHTML(String _html) {
        Tidy tidy = new Tidy();
        tidy.setQuiet(true);
        tidy.setNumEntities(true);
        tidy.setShowWarnings(false);
        StringWriter result = new StringWriter();
        tidy.setMakeClean(true);
        tidy.setXHTML(true);
        tidy.parse(new StringReader(_html), result);

        return result.toString();
    }

    private String retrieveDocument(cfSession _Session, DocumentSection _section, DocumentSettings _settings)
            throws dataNotSupportedException, cfmRunTimeException {
        String src = _section.getSrc();
        if (src != null) {
            // if this is a file:// url then just handle it as a SRCFILE rather
            // than pass it through HttpClient
            if (src.startsWith("file://")) {
                return retrieveLocalFile(_Session, src.substring(8));
            } else if (src.startsWith("http://") || src.startsWith("https://")) {
                return retrieveHttp(_Session, src, _section, _settings);
            } else {
                return retrieveHttp(_Session, makeAbsoluteUrl(src, _Session), _section, _settings);
            }
        } else {
            return retrieveLocalFile(_Session, _section.getSrcFile());
        }
    }

    private static String makeAbsoluteUrl(String relativeUrl, cfSession _Session) {
        boolean serverRelative = false;
        if (relativeUrl.startsWith("/"))
            serverRelative = true;
        StringBuffer base = new StringBuffer(_Session.REQ.getScheme());
        base.append("://");
        base.append(_Session.REQ.getServerName());
        if (_Session.REQ.getServerPort() != 80)
            base.append(":" + _Session.REQ.getServerPort());
        if (serverRelative) {
            base.append(relativeUrl);
        } else {
            if (_Session.REQ.getContextPath().equals(""))
                base.append("/");
            base.append(_Session.REQ.getContextPath());
            base.append(_Session.REQ.getServletPath().substring(0, _Session.REQ.getServletPath().lastIndexOf("/")));
            base.append("/" + relativeUrl);
        }
        return base.toString();
    }

    private String retrieveLocalFile(cfSession _Session, String _filepath) throws cfmRunTimeException {
        File file = new File(_filepath);
        if (!file.exists())
            file = new File(_Session.getPresentDirectory(), _filepath);
        FileInputStream fin = null;
        try {
            fin = new FileInputStream(file);
            return handleDocument(_Session, fin, null);
        } catch (FileNotFoundException e) {
            throw newRunTimeException("Invalid file specified. " + _filepath + " could not be found");
        } catch (IOException e) {
            throw newRunTimeException("Failed to read specified file " + _filepath
                    + ". Check the sufficient permissions have been set to permit reading of this file.");
        } finally {
            if (fin != null)
                try {
                    fin.close();
                } catch (Exception ignored) {
                }
        }
    }

    private String retrieveHttp(cfSession _Session, String _src, DocumentSection _section,
            DocumentSettings _defaultSettings) throws dataNotSupportedException, cfmRunTimeException {
        DefaultHttpClient httpClient = new DefaultHttpClient();
        HttpGet method = new HttpGet();

        try {
            method.setURI(new URI(_src));
            if (_section.getUserAgent() != null) {
                method.setHeader("User-Agent", _section.getUserAgent());
            } else {
                method.setHeader("User-Agent", _defaultSettings.getUserAgent());
            }

            // HTTP basic authentication
            if (_section.getAuthPassword() != null) {
                httpClient.getCredentialsProvider().setCredentials(AuthScope.ANY,
                        new UsernamePasswordCredentials(_section.getAuthUser(), _section.getAuthPassword()));
            }

            // proxy support
            if (_defaultSettings.getProxyHost() != null) {
                HttpHost proxy = new HttpHost(_defaultSettings.getProxyHost(), _defaultSettings.getProxyPort());
                httpClient.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy);

                if (_defaultSettings.getProxyUser() != null) {
                    httpClient.getCredentialsProvider().setCredentials(
                            new AuthScope(_defaultSettings.getProxyHost(), _defaultSettings.getProxyPort()),
                            new UsernamePasswordCredentials(_defaultSettings.getProxyUser(),
                                    _defaultSettings.getProxyPassword()));
                }
            }

            HttpResponse response;
            response = httpClient.execute(method);

            if (response.getStatusLine().getStatusCode() == 200) {
                String charset = null;
                Header contentType = response.getFirstHeader("Content-type");
                if (contentType != null) {
                    String value = contentType.getValue();
                    int indx = value.indexOf("charset=");
                    if (indx > 0) {
                        charset = value.substring(indx + 8).trim();
                    }
                }
                return handleDocument(_Session, response.getEntity().getContent(), charset);
            } else {
                throw newRunTimeException("Failed to retrieve document from source. HTTP status code "
                        + response.getStatusLine().getStatusCode() + " was returned");
            }

            //   throw newRunTimeException( "Failed to retrieve document from " + _src + " due to HttpException: " + e.getMessage() );
        } catch (URISyntaxException e) {
            throw newRunTimeException("Error retrieving document via http: " + e.getMessage());

        } catch (IOException e) {
            throw newRunTimeException("Error retrieving document via http: " + e.getMessage());
        }

    }

    private DocumentSettings getDocumentSettings(cfSession _Session, DocumentContainer _container)
            throws dataNotSupportedException, cfmRunTimeException {
        DocumentSettings settings = new DocumentSettings();

        // get UNIT value 
        String unit = getDynamic(_Session, "UNIT").getString().toLowerCase();
        if (!unit.equals("in") && !unit.equals("cm")) {
            throw newRunTimeException("Invalid UNIT value. Valid values include \"IN\" and \"CM\".");
        }
        settings.setUnit(unit);

        // set margins
        if (containsAttribute("MARGINTOP")) {
            settings.setMarginTop(getDynamic(_Session, "MARGINTOP").getString());
        }
        if (containsAttribute("MARGINLEFT")) {
            settings.setMarginLeft(getDynamic(_Session, "MARGINLEFT").getString());
        }
        if (containsAttribute("MARGINRIGHT")) {
            settings.setMarginRight(getDynamic(_Session, "MARGINRIGHT").getString());
        }
        if (containsAttribute("MARGINBOTTOM")) {
            settings.setMarginBottom(getDynamic(_Session, "MARGINBOTTOM").getString());
        }

        // page type and size
        String pageType = getDynamic(_Session, "PAGETYPE").getString().toLowerCase();
        String pageSize = "a4";

        boolean landscape = false; // default to portrait
        if (containsAttribute("ORIENTATION")) {
            String orientStr = getDynamic(_Session, "ORIENTATION").getString();
            if (orientStr.equalsIgnoreCase("LANDSCAPE")) {
                landscape = true;
            } else if (!orientStr.equalsIgnoreCase("PORTRAIT")) {
                throw newRunTimeException(
                        "Invalid ORIENTATION value. Valid values include \"PORTRAIT\" and \"LANDSCAPE\".");
            }
        }

        if (pageType.equals("a4") || pageType.equals("a5") || pageType.equals("b4") || pageType.equals("legal")
                || pageType.endsWith("letter")) {
            pageSize = pageType;
            if (landscape) {
                pageSize += " landscape";
            }
        } else {
            String width = null;
            String height = null;
            if (pageType.equals("b5")) {
                width = "7in";
                height = "9.88in";
            } else if (pageType.equals("b5-jis")) {
                width = "7.19in";
                height = "10.13in";
            } else if (pageType.equals("b4-jis")) {
                width = "10.13in";
                height = "14.31in";
            } else if (pageType.equals("custom")) {
                if (!containsAttribute("PAGEHEIGHT") || !containsAttribute("PAGEWIDTH")) {
                    throw newRunTimeException(
                            "Missing PAGEHEIGHT/PAGEWIDTH attribute(s). Both must be specified when specifying a CUSTOM page size");
                }

                width = getDynamic(_Session, "PAGEWIDTH").getString() + unit;
                height = getDynamic(_Session, "PAGEHEIGHT").getString() + unit;

            } else {
                throw newRunTimeException("Invalid PAGETYPE value.");
            }

            if (landscape) {
                String tmp = height;
                height = width;
                width = tmp;
            }

            pageSize = width + " " + height;
        }

        settings.setPageSize(pageSize);

        // proxy details
        if (containsAttribute("PROXYHOST")) {
            String proxyHost = getDynamic(_Session, "PROXYHOST").getString();
            int proxyPort = 80;
            if (containsAttribute("PROXYPORT")) {
                proxyPort = getDynamic(_Session, "PROXYPORT").getInt();
            }
            String proxyUser = null;
            String proxyPassword = null;
            if (containsAttribute("PROXYUSER")) {
                proxyUser = getDynamic(_Session, "PROXYUSER").getString();
                proxyPassword = getDynamic(_Session, "PROXYPASSWORD").getString();
            }

            settings.setProxyDetails(proxyHost, proxyPort, proxyUser, proxyPassword);
        }

        return settings;
    }

    @SuppressWarnings("deprecation")
    private String handleDocument(cfSession _Session, InputStream _in, String _charset)
            throws IOException, dataNotSupportedException, cfmRunTimeException {
        String mimeType = getDynamic(_Session, "MIMETYPE").getString().toLowerCase();
        String charset = _charset;
        if (charset == null) {
            charset = "ISO-8859-1";
        }

        if (mimeType.equals("text/html")) {
            return IOUtils.toString(_in, charset);
        } else if (mimeType.equals("text/plain")) {
            String plainTxt = IOUtils.toString(_in, charset);
            return "<pre>" + escapeHtmlChars(plainTxt) + "</pre>";

        } else if (mimeType.startsWith("image/")) {
            File tmpFile = File.createTempFile("cfdoc", '.' + mimeType.substring(mimeType.indexOf('/') + 1));
            OutputStream fout = cfEngine.thisPlatform.getFileIO().getFileOutputStream(tmpFile);
            StreamUtil.copyTo(_in, fout);
            return "<img src=\"" + tmpFile.toURL() + "\"/>";
        } else {
            throw newRunTimeException(
                    "Invalid MIMETYPE value. Supported values include text/html, text/plain, image/jpg, image/gif, image/png and image/bmp");
        }

    }

    /*
     * escapeHTMLChars
     * 
     * This is used to escape the HTML chars in a plain text file so
     * that JTidy and flying saucer ignore them.
     */
    private static String escapeHtmlChars(String content) {
        char[] old = new char[] { '&', '<', '>', '\"' };
        String[] replacements = new String[] { "&amp;", "&lt;", "&gt;", "&quot;" };

        int strLen = content.length();
        int charsLen = old.length;
        StringBuilder buffer = new StringBuilder(content);
        StringBuilder writer = new StringBuilder(strLen);
        char nextChar;
        boolean foundCh;

        for (int i = 0; i < strLen; i++) {
            nextChar = buffer.charAt(i);
            foundCh = false;

            for (int j = 0; j < charsLen; j++) {
                if (nextChar == old[j]) {
                    writer.append(replacements[j]);
                    foundCh = true;
                }
            }
            if (!foundCh) {
                writer.append(nextChar);
            }
        }

        return writer.toString();//.Replace("\n", "<br>");
    }

    private void setPermissions(cfSession _Session, PDFEncryption _pdfEnc) throws cfmRunTimeException {

        // apply encryption 
        String encryption = getDynamic(_Session, "ENCRYPTION").getString().toLowerCase();
        if (encryption.equals("40") || encryption.equals("40-bit")) {
            _pdfEnc.setEncryptionType(PdfWriter.STANDARD_ENCRYPTION_40);
        } else if (encryption.equals("128") || encryption.equals("128-bit")) {
            _pdfEnc.setEncryptionType(PdfWriter.STANDARD_ENCRYPTION_128);
        } else if (encryption.equals("aes")) {
            _pdfEnc.setEncryptionType(PdfWriter.ENCRYPTION_AES_128);
        } else if (!encryption.equals("none")) {
            throw newRunTimeException(
                    "Invalid ENCRYPTION value. Supported values include \"40-bit\", \"128-bit\", \"AES\" and \"none\"");
        }

        // Default to no permissions
        int permissionsMask = 0;

        if (containsAttribute("PERMISSIONS")) {

            String[] permissions = getDynamic(_Session, "PERMISSIONS").getString().toLowerCase().split(",");
            if (permissions.length > 0) {

                for (int i = 0; i < permissions.length; i++) {
                    String nextPermission = permissions[i];
                    if (nextPermission.equals("allowprinting")) {
                        permissionsMask |= PdfWriter.ALLOW_PRINTING;
                    } else if (nextPermission.equals("allowmodifycontents")) {
                        permissionsMask |= PdfWriter.ALLOW_MODIFY_CONTENTS;
                    } else if (nextPermission.equals("allowcopy")) {
                        permissionsMask |= PdfWriter.ALLOW_COPY;
                    } else if (nextPermission.equals("allowmodifyannotations")) {
                        permissionsMask |= PdfWriter.ALLOW_MODIFY_ANNOTATIONS;
                    } else if (nextPermission.equals("allowscreenreaders")) {
                        if (_pdfEnc.getEncryptionType() == PdfWriter.STANDARD_ENCRYPTION_40)
                            throw newRunTimeException("AllowScreenReaders is not valid with 40-bit encryption");
                        permissionsMask |= PdfWriter.ALLOW_SCREENREADERS;
                    } else if (nextPermission.equals("allowassembly")) {
                        if (_pdfEnc.getEncryptionType() == PdfWriter.STANDARD_ENCRYPTION_40)
                            throw newRunTimeException("AllowAssembly is not valid with 40-bit encryption");
                        permissionsMask |= PdfWriter.ALLOW_ASSEMBLY;
                    } else if (nextPermission.equals("allowdegradedprinting")) {
                        if (_pdfEnc.getEncryptionType() == PdfWriter.STANDARD_ENCRYPTION_40)
                            throw newRunTimeException("AllowDegradedPrinting is not valid with 40-bit encryption");
                        permissionsMask |= PdfWriter.ALLOW_DEGRADED_PRINTING;
                    } else if (nextPermission.equals("allowfillin")) {
                        if (_pdfEnc.getEncryptionType() == PdfWriter.STANDARD_ENCRYPTION_40)
                            throw newRunTimeException("AllowFillIn is not valid with 40-bit encryption");
                        permissionsMask |= PdfWriter.ALLOW_FILL_IN;
                    } else {
                        throw newRunTimeException("Invalid permissions value: " + nextPermission);
                    }
                }
            }
        }

        // Set the allowed permissions
        _pdfEnc.setAllowedPrivileges(permissionsMask);

        if (containsAttribute("OWNERPASSWORD"))
            _pdfEnc.setOwnerPassword(getDynamic(_Session, "OWNERPASSWORD").getString().getBytes());

        if (containsAttribute("USERPASSWORD"))
            _pdfEnc.setUserPassword(getDynamic(_Session, "USERPASSWORD").getString().getBytes());

    }

    /*
     * removeBackground
     * 
     * Removes all of the background items like the 'bgcolor' attribute from
     * the XHTML body.
     */
    private String removeBackground(String _body) {
        try {
            // Parse the XHTML body into an XML object
            cfXmlData content = cfXmlData.parseXml(_body, true, null);

            // Remove the background items from the top node and all of its children
            Node node = content.getXMLNode();
            removeBackground(node);

            // Convert the XML object back into a string
            _body = content.toString();
        } catch (Exception e) {
        }

        return _body;
    }

    /*
     * removeBackground
     * 
     * Removes all of the background items like the 'bgcolor' attribute from
     * the XML node and recursively calls itself to remove them from all child
     * nodes too.
     */
    private void removeBackground(Node _node) {
        // Remove any background items from the node.
        // For now we only remove the 'bgcolor' attribute.
        NamedNodeMap attributes = _node.getAttributes();
        if ((attributes != null) && (attributes.getNamedItem("bgcolor") != null))
            attributes.removeNamedItem("bgcolor");

        // If the node has children then make recursive calls to remove the
        // background items from the children too.
        if (_node.hasChildNodes()) {
            NodeList children = _node.getChildNodes();
            for (int i = 0; i < children.getLength(); i++)
                removeBackground(children.item(i));
        }
    }

    /*
     * extractTagAttributes
     * 
     * Extracts the attributes for the tag with the specified name.
     */
    private HashMap<String, String> extractTagAttributes(String _tagName, String _xhtml) {
        HashMap<String, String> attributes = new HashMap<String, String>();

        try {
            // Parse the XHTML body into an XML object
            cfXmlData content = cfXmlData.parseXml(_xhtml, true, null);

            // Find the node for the specified tag
            Node node = findNode(content.getXMLNode(), _tagName);
            if (node != null) {
                // Copy the node's attributes (if any) into the HashMap
                NamedNodeMap nodeAttributes = node.getAttributes();
                if (nodeAttributes != null) {
                    for (int i = 0; i < nodeAttributes.getLength(); i++)
                        attributes.put(nodeAttributes.item(i).getNodeName(), nodeAttributes.item(i).getNodeValue());
                }
            }
        } catch (Exception e) {
        }

        return attributes;
    }

    /*
     * findNode
     * 
     * Search the node and all child nodes for the node with the specified name.
     */
    private Node findNode(Node _node, String _tagName) {
        String name = _node.getNodeName();
        if ((name != null) && name.equals(_tagName))
            return _node;

        if (_node.hasChildNodes()) {
            NodeList children = _node.getChildNodes();
            for (int i = 0; i < children.getLength(); i++) {
                Node n = findNode(children.item(i), _tagName);
                if (n != null)
                    return n;
            }
        }

        return null;
    }

    private String insertStyles(cfSession _Session, String _body, DocumentSection _section,
            DocumentSettings _settings, boolean _backgroundVisible, int _numSections) throws cfmRunTimeException {

        int headStart = _body.indexOf("<head>");
        if (headStart < 0) {
            return _body;
        }

        HashMap<String, String> bodyTagAttributes = null;
        if (!_backgroundVisible) {
            // The background is set to not visible so remove all
            // the background items like the 'bgcolor' attribute.
            _body = removeBackground(_body);
            headStart = _body.indexOf("<head>");
        } else {
            // For some reason the BODY bgcolor attribute is being ignored so let's extract
            // it and set the background color using CSS. We'll extract all the body tag's
            // attributes since we might need them in the future.
            bodyTagAttributes = extractTagAttributes("body", _body);
        }

        StringBuilder styleBlock = new StringBuilder();
        styleBlock.append("<style>\n");
        styleBlock.append("@page{\n");
        styleBlock.append(getMargins(_Session, _section, _settings));
        String header = replaceHeaderFooterVariables(_section, _section.getHeader(), _numSections);
        String footer = replaceHeaderFooterVariables(_section, _section.getFooter(), _numSections);
        insertHeaderFooter(_Session, styleBlock, "top", _section.getHeaderAlign(), header, _backgroundVisible,
                _section.getMimeType());
        insertHeaderFooter(_Session, styleBlock, "bottom", _section.getFooterAlign(), footer, _backgroundVisible,
                _section.getMimeType());
        styleBlock.append("size: ");
        styleBlock.append(_settings.getPageSize());
        styleBlock.append(";\n");
        styleBlock.append("}\n");

        if ((bodyTagAttributes != null) && (bodyTagAttributes.containsKey("bgcolor"))) {
            styleBlock.append("body { background-color: ");
            styleBlock.append(bodyTagAttributes.get("bgcolor"));
            styleBlock.append("; }\n");
        }

        styleBlock.append("</style>\n");

        return _body.substring(0, headStart + 6) + styleBlock + _body.substring(headStart + 6);
    }

    /*
     * replaceHeaderFooterVariables
     * 
     * Replace the header/footer page count variables with the appropriate values.
     */
    private String replaceHeaderFooterVariables(DocumentSection _section, String _content, int _numSections) {
        if (_content == null)
            return null;

        _content = _content.replaceAll("BD:CURRENTPAGENUMBER", "\" counter(page) \"");
        _content = _content.replaceAll("BD:CURRENTSECTIONPAGENUMBER", "\" counter(page) \"");

        if (_numSections == 1) {
            // There's only 1 section so we can use the page and pages counters.
            _content = _content.replaceAll("BD:TOTALPAGECOUNT", "\" counter(pages) \"");
            _content = _content.replaceAll("BD:TOTALSECTIONPAGECOUNT", "\" counter(pages) \"");
        } else {
            // There's more than 1 section so we need to use the values calculated by preparePageCounters().
            _content = _content.replaceAll("BD:TOTALPAGECOUNT", Integer.toString(_section.getTotalPageCount()));
            _content = _content.replaceAll("BD:TOTALSECTIONPAGECOUNT",
                    Integer.toString(_section.getTotalSectionPageCount()));
        }

        return _content;
    }

    private void insertHeaderFooter(cfSession _Session, StringBuilder _sb, String _position, String _align,
            String _content, boolean _backgroundVisible, String _sectionMimeType) throws cfmRunTimeException {
        if (_content != null) {
            _sb.append("@");
            _sb.append(_position);
            _sb.append("-");
            _sb.append(_align);
            _sb.append("{\nwhite-space: pre;\n"); // this is necessary so escaped newlines in the content are displayed properly

            // If the section had a mimetype specified and it's text/plain or if it didn't have a mimetype
            // specified and the document mimetype was set to text/plain then treat the body as plain text.
            if (((_sectionMimeType != null) && (_sectionMimeType.equalsIgnoreCase("text/plain")))
                    || ((_sectionMimeType == null)
                            && (getDynamic(_Session, "MIMETYPE").getString().equalsIgnoreCase("text/plain")))) {
                _sb.append("content: ");
                _sb.append(convertPlainTextToCSSContent(_content));
            } else {
                HashMap<String, String> properties = new HashMap<String, String>();
                String xhtml = getHeaderFooterXHTML(_content, properties);
                if (_backgroundVisible && properties.containsKey("background-color")) {
                    _sb.append("background-color: ");
                    _sb.append(properties.get("background-color"));
                    _sb.append(";\n");
                }
                _sb.append("content: ");
                _sb.append(xhtml);
            }
            _sb.append(";\n}\n");
        }
    }

    private String getHeaderFooterXHTML(String _content, HashMap<String, String> _properties) {
        // Convert to XHTML
        String xhtml = getXHTML(_content);

        // Find the beginning of the body tag and body text (starts with double quotes)
        int beginBody = xhtml.indexOf("<body");
        if (beginBody >= 0) {
            beginBody = xhtml.indexOf('>', beginBody);
            while (xhtml.charAt(beginBody) != '"')
                beginBody++;
        } else {
            beginBody = 0;
        }

        // Find the end of the body text (ends with double quotes) and extract the body
        String body;
        int endPos = xhtml.indexOf("</body>");
        if (endPos > 0) {
            while (xhtml.charAt(endPos) != '"')
                endPos--;
            body = xhtml.substring(beginBody, endPos + 1);
        } else {
            body = xhtml.substring(beginBody);
        }

        // Convert the body to an appropriate format for CSS content
        String cssContent = convertHTMLToCSSContent(body);

        // Now see if there are any HTML attributes that need to be returned
        // as CSS properties.
        HashMap<String, String> attributes = extractTagAttributes("body", xhtml);
        if (attributes.containsKey("bgcolor"))
            _properties.put("background-color", attributes.get("bgcolor"));

        return cssContent;
    }

    /*
     * convertPlainTextToCSSContent
     */
    private static String convertPlainTextToCSSContent(String _text) {
        StringBuilder sb = new StringBuilder();

        // First escape all HTML special characters except for the surrounding double quotes
        _text = '"' + escapeHtmlChars(_text.substring(1, _text.length() - 1)) + '"';

        // Now escape all newline characters and HTML escaped double quotes
        for (int i = 0; i < _text.length(); i++) {
            char ch = _text.charAt(i);
            switch (ch) {
            case '\r':
            case '\n':
                sb.append("\\A "); // escape sequence for newline
                if ((ch == '\r') && (i + 1 < _text.length()) && (_text.charAt(i + 1) == '\n'))
                    i++;
                break;

            case '&':
                if (_text.startsWith("&quot;", i)) {
                    if (_text.startsWith("&quot; counter(page) &quot;", i)) {
                        sb.append("\" counter(page) \""); // put back as unescaped
                        i += "&quot; counter(page) &quot;".length() - 1; // move to the end
                    } else if (_text.startsWith("&quot; counter(pages) &quot;", i)) {
                        sb.append("\" counter(pages) \""); // put back as unescaped
                        i += "&quot; counter(pages) &quot;".length() - 1; // move to the end
                    } else {
                        sb.append("\\22 "); // escape sequence for double quote
                        i += "&quot;".length() - 1; // move to the end
                    }
                } else {
                    sb.append(ch);
                }
                break;

            default:
                sb.append(ch);
                break;
            }
        }

        return sb.toString();
    }

    /*
     * convertHTMLToCSSContent
     */
    private static String convertHTMLToCSSContent(String _html) {
        StringBuilder sb = new StringBuilder();

        for (int i = 0; i < _html.length(); i++) {
            char ch = _html.charAt(i);
            switch (ch) {
            case '\r':
            case '\n':
                // Do nothing so these characters are removed.
                break;

            case '<':
                int endTag = _html.indexOf('>', i);
                if (endTag > i) {
                    String htmlTag = _html.substring(i, endTag + 1);

                    // Currently we only handle the <br> tag.
                    if (htmlTag.equals("<br />"))
                        sb.append("\\A "); // escape sequence for newline

                    i = endTag;
                } else {
                    sb.append(ch);
                }
                break;

            default:
                sb.append(ch);
                break;
            }
        }

        return sb.toString();
    }

    private String getMargins(cfSession _Session, DocumentSection _section, DocumentSettings _settings)
            throws dataNotSupportedException, cfmRunTimeException {
        StringBuilder sb = new StringBuilder();

        String marginTop = _section.getMarginTop();
        if (_section.getMarginTop() == null) {
            marginTop = _settings.getMarginTop();
        }
        if (marginTop != null) {
            sb.append("margin-top: ");
            sb.append(marginTop);
            sb.append(_settings.getUnit());
            sb.append(";\n");
        }

        String marginBottom = _section.getMarginBottom();
        if (_section.getMarginBottom() == null) {
            marginBottom = _settings.getMarginBottom();
        }
        if (marginBottom != null) {
            sb.append("margin-bottom: ");
            sb.append(marginBottom);
            sb.append(_settings.getUnit());
            sb.append(";\n");
        }

        String marginLeft = _section.getMarginLeft();
        if (_section.getMarginLeft() == null) {
            marginLeft = _settings.getMarginLeft();
        }
        if (marginLeft != null) {
            sb.append("margin-left: ");
            sb.append(marginLeft);
            sb.append(_settings.getUnit());
            sb.append(";\n");
        }

        String marginRight = _section.getMarginRight();
        if (_section.getMarginRight() == null) {
            marginRight = _settings.getMarginRight();
        }
        if (marginRight != null) {
            sb.append("margin-right: ");
            sb.append(marginRight);
            sb.append(_settings.getUnit());
            sb.append(";\n");
        }

        return sb.toString();
    }

    private void appendSectionAttributes(cfSession _Session, DocumentSection _section) throws cfmRunTimeException {
        if (containsAttribute("MIMETYPE"))
            _section.setMimeType(getDynamic(_Session, "MIMETYPE").toString());

        if (containsAttribute("NAME"))
            _section.setName(getDynamic(_Session, "NAME").toString());

        //TODO: validate values?
        if (containsAttribute("MARGINTOP"))
            _section.setMarginTop(getDynamic(_Session, "MARGINTOP").toString());
        if (containsAttribute("MARGINBOTTOM"))
            _section.setMarginBottom(getDynamic(_Session, "MARGINBOTTOM").toString());
        if (containsAttribute("MARGINLEFT"))
            _section.setMarginLeft(getDynamic(_Session, "MARGINLEFT").toString());
        if (containsAttribute("MARGINRIGHT"))
            _section.setMarginRight(getDynamic(_Session, "MARGINRIGHT").toString());

        _section.setUserAgent(getDynamic(_Session, "USERAGENT").getString());

        if (containsAttribute("AUTHPASSWORD") && containsAttribute("AUTHUSER")) {
            _section.setAuthentication(getDynamic(_Session, "AUTHUSER").getString(),
                    getDynamic(_Session, "AUTHPASSWORD").getString());
        }

    }

    private class SessionOutputStream extends java.io.OutputStream {

        private cfSession session;
        private cfmRunTimeException exception;
        private boolean firstWrite = true;
        private String saveAsName;

        SessionOutputStream(cfSession _session, String _saveAsName) {
            session = _session;
            saveAsName = _saveAsName;
        }

        public cfmRunTimeException getException() {
            return exception;
        }

        @Override
        public void write(byte[] b, int off, int len) throws IOException {
            if (exception == null) { // only attempt to write further if exception hasn't occurred
                try {
                    // If this is the first write then set the content type and appropriate headers.
                    // We wait to set these here so that any exceptions that occur before this will
                    // be displayed properly in the browser.
                    if (firstWrite) {
                        firstWrite = false;
                        session.setContentType("application/pdf");
                        session.setHeader("Content-Disposition", "inline; filename=" + saveAsName);
                    }
                    session.write(b, off, len);
                } catch (cfmRunTimeException e) {
                    exception = e;
                }
            }
        }

        @Override
        public void write(byte[] b) throws IOException {
            this.write(b, 0, b.length);
        }

        @Override
        public void write(int arg0) throws IOException {
            this.write(new byte[] { (byte) arg0 }, 0, 1);
        }

    }

    private class NullOutputStream extends java.io.OutputStream {

        NullOutputStream() {
        }

        @Override
        public void write(byte[] b, int off, int len) throws IOException {
        }

        @Override
        public void write(byte[] b) throws IOException {
        }

        @Override
        public void write(int arg0) throws IOException {
        }
    }

    private class CreationListener extends DefaultPDFCreationListener {
        private PdfString author;
        private PdfString title;
        private PdfString subject;
        private PdfString keywords;

        public CreationListener(cfData _author, cfData _title, cfData _subject, cfData _keywords) {
            if (_author != null)
                author = new PdfString(_author.toString());
            if (_title != null)
                title = new PdfString(_title.toString());
            if (_subject != null)
                subject = new PdfString(_subject.toString());
            if (_keywords != null)
                keywords = new PdfString(_keywords.toString());
        }

        //public void preOpen(ITextRenderer renderer) 
        public void onClose(ITextRenderer renderer) {
            PdfString creator = new PdfString(
                    "OpenBD " + cfEngine.PRODUCT_VERSION + " (" + cfEngine.BUILD_ISSUE + ")");
            renderer.getOutputDevice().getWriter().getInfo().put(PdfName.CREATOR, creator);

            if (author != null)
                renderer.getOutputDevice().getWriter().getInfo().put(PdfName.AUTHOR, author);
            if (title != null)
                renderer.getOutputDevice().getWriter().getInfo().put(PdfName.TITLE, title);
            if (subject != null)
                renderer.getOutputDevice().getWriter().getInfo().put(PdfName.SUBJECT, subject);
            if (keywords != null)
                renderer.getOutputDevice().getWriter().getInfo().put(PdfName.KEYWORDS, keywords);
        }
    }

}