com.xpn.xwiki.pdf.impl.PdfExportImpl.java Source code

Java tutorial

Introduction

Here is the source code for com.xpn.xwiki.pdf.impl.PdfExportImpl.java

Source

/*
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package com.xpn.xwiki.pdf.impl;

import info.informatica.doc.dom4j.CSSStylableElement;
import info.informatica.doc.dom4j.DOM4JCSSStyleSheet;
import info.informatica.doc.dom4j.XHTMLDocument;
import info.informatica.doc.dom4j.XHTMLDocumentFactory;
import info.informatica.doc.xml.dtd.DefaultEntityResolver;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Properties;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.sax.SAXResult;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.fop.apps.FOUserAgent;
import org.apache.fop.apps.Fop;
import org.apache.fop.apps.FopFactory;
import org.apache.fop.apps.FormattingResults;
import org.apache.fop.apps.PageSequenceResults;
import org.apache.velocity.VelocityContext;
import org.dom4j.Element;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.SAXReader;
import org.dom4j.io.XMLWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.w3c.dom.bootstrap.DOMImplementationRegistry;
import org.w3c.dom.css.CSSStyleDeclaration;
import org.w3c.dom.ls.DOMImplementationLS;
import org.w3c.dom.ls.LSOutput;
import org.w3c.dom.ls.LSSerializer;
import org.w3c.tidy.Tidy;
import org.xml.sax.InputSource;
import org.xwiki.bridge.DocumentAccessBridge;
import org.xwiki.container.ApplicationContext;
import org.xwiki.container.Container;
import org.xwiki.context.Execution;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.model.reference.DocumentReferenceResolver;
import org.xwiki.model.reference.EntityReferenceSerializer;
import org.xwiki.velocity.VelocityEngine;
import org.xwiki.velocity.VelocityManager;
import org.xwiki.velocity.XWikiVelocityException;
import org.xwiki.xml.EntityResolver;

import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.XWikiException;
import com.xpn.xwiki.doc.XWikiDocument;
import com.xpn.xwiki.pdf.api.PdfExport;
import com.xpn.xwiki.web.Utils;
import com.xpn.xwiki.web.XWikiRequest;

/**
 * Default implementation for the PDF Export process, which uses XSLT transformations and Apache FOP to convert a
 * Document into PDF, passing through HTML, valid XHTML, styled XHTML, and XSL-FO.
 * 
 * @version $Id: 9965c11c792c535a7ab2e451b2c1f28b9c79526d $
 */
public class PdfExportImpl implements PdfExport {
    /** The location where fonts to be used during PDF export should be placed. */
    private static final String FONTS_PATH = "/WEB-INF/fonts/";

    /** The name of the default XHTML2FOP transformation file. */
    private static final String DEFAULT_XHTML2FOP_XSLT = "xhtml2fo.xsl";

    /** The name of the default FOP post-processing transformation file. */
    private static final String DEFAULT_CLEANUP_XSLT = "fop.xsl";

    /** Logging helper object. */
    private static final Logger LOGGER = LoggerFactory.getLogger(PdfExportImpl.class);

    /** Tidy configuration. */
    private static final Properties TIDY_CONFIGURATION;

    /** Document name resolver. */
    @SuppressWarnings("unchecked")
    private static DocumentReferenceResolver<String> referenceResolver = Utils
            .getComponent(DocumentReferenceResolver.class, "currentmixed");

    /** Document name serializer. */
    @SuppressWarnings("unchecked")
    private static EntityReferenceSerializer<String> referenceSerializer = Utils
            .getComponent(EntityReferenceSerializer.class);

    /** Provides access to document properties. */
    private static DocumentAccessBridge dab = Utils.getComponent(DocumentAccessBridge.class);

    /** Velocity engine manager, used for interpreting velocity. */
    private static VelocityManager velocityManager = Utils.getComponent(VelocityManager.class);

    /** DOM parser factory. */
    private static DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();

    /** DOM Serializer factory. */
    private static DOMImplementationLS lsImpl;

    /** XSLT transformer factory. */
    private static TransformerFactory transformerFactory = TransformerFactory.newInstance();

    /** The Apache FOP instance used for XSL-FO processing. */
    private static FopFactory fopFactory;

    /** The JTidy instance used for cleaning up HTML documents. */
    private Tidy tidy;

    // Fields initialization
    static {
        // ----------------------------------------------------------------------
        // XML parser/serializer initialization
        // ----------------------------------------------------------------------
        dbFactory.setNamespaceAware(true);
        dbFactory.setValidating(false);

        try {
            lsImpl = (DOMImplementationLS) DOMImplementationRegistry.newInstance().getDOMImplementation("LS 3.0");
        } catch (Exception ex) {
            LOGGER.warn("Cannot initialize the DomLS implementation needed by the PDF export: " + ex.getMessage());
        }

        // ----------------------------------------------------------------------
        // FOP configuration
        // ----------------------------------------------------------------------
        fopFactory = FopFactory.newInstance();
        try {
            ApplicationContext context = Utils.getComponent(Container.class).getApplicationContext();
            String fontsPath = context.getResource(FONTS_PATH).getPath();
            Execution execution = Utils.getComponent(Execution.class);
            XWikiContext xcontext = (XWikiContext) execution.getContext().getProperty("xwikicontext");
            if (xcontext != null) {
                XWikiRequest request = xcontext.getRequest();
                if (request != null && request.getSession() != null) {
                    fontsPath = request.getSession().getServletContext().getRealPath(FONTS_PATH);
                }
            }
            fopFactory.getFontManager().setFontBaseURL(fontsPath);
        } catch (Throwable ex) {
            LOGGER.warn("Starting with 1.5, XWiki uses the WEB-INF/fonts/ directory as the font directory, "
                    + "and it should contain the FreeFont (http://savannah.gnu.org/projects/freefont/) fonts. "
                    + "FOP cannot access this directory. If this is an upgrade from a previous version, "
                    + "make sure you also copy the WEB-INF/fonts directory from the new distribution package.");
        }
        InputStream fopConfigurationFile = PdfExportImpl.class.getResourceAsStream("/fop-config.xml");
        if (fopConfigurationFile != null) {
            try {
                fopFactory.setUserConfig(new DefaultConfigurationBuilder().build(fopConfigurationFile));
            } catch (Exception ex) {
                LOGGER.warn("Wrong FOP configuration: " + ex.getMessage());
            }
        }

        // ----------------------------------------------------------------------
        // Tidy configuration
        // ----------------------------------------------------------------------
        // Setup a default configuration for Tidy
        Properties baseConfiguration = new Properties();
        baseConfiguration.setProperty("quiet", Boolean.TRUE.toString());
        baseConfiguration.setProperty("clean", Boolean.TRUE.toString());
        baseConfiguration.setProperty("tidy-mark", Boolean.FALSE.toString());
        baseConfiguration.setProperty("output-xhtml", Boolean.TRUE.toString());
        baseConfiguration.setProperty("show-warnings", Boolean.FALSE.toString());
        baseConfiguration.setProperty("trim-empty-elements", Boolean.FALSE.toString());
        baseConfiguration.setProperty("numeric-entities", Boolean.TRUE.toString());
        // Don't replace non-ASCII apostrophes/quotes/dashes with plain ASCII ones.
        baseConfiguration.setProperty("ascii-chars", Boolean.FALSE.toString());
        // Don't wrap, as it is not needed, and it triggers JTidy bugs when combined with non-ASCII chars.
        baseConfiguration.setProperty("wrap", "0");

        // Allow Tidy to be configured in the tidy.properties file
        TIDY_CONFIGURATION = new Properties(baseConfiguration);
        try {
            TIDY_CONFIGURATION.load(PdfExportImpl.class.getClassLoader().getResourceAsStream("/tidy.properties"));
        } catch (IOException ex) {
            LOGGER.warn("Tidy configuration file could not be read. Using default configuration.");
        } catch (NullPointerException ex) {
            LOGGER.warn("Tidy configuration file doesn't exist. Using default configuration.");
        }
    }

    /** Default constructor. */
    public PdfExportImpl() {
        this.tidy = new Tidy();
        this.tidy.setConfigurationFromProps(TIDY_CONFIGURATION);
    }

    /**
     * {@inheritDoc}
     * 
     * @see PdfExport#exportToPDF(XWikiDocument, OutputStream, XWikiContext)
     * @since 1.0
     */
    public void exportToPDF(XWikiDocument doc, OutputStream out, XWikiContext context) throws XWikiException {
        export(doc, out, ExportType.PDF, context);
    }

    /**
     * {@inheritDoc}
     * 
     * @see PdfExport#export(XWikiDocument, OutputStream, com.xpn.xwiki.pdf.api.PdfExport.ExportType, XWikiContext)
     * @since 3.0M2
     */
    public void export(XWikiDocument doc, OutputStream out, ExportType type, XWikiContext context)
            throws XWikiException {
        // Note: The passed document is not used currently since we're calling pdf.vm and that
        // velocity template uses the XWiki Context to get the current doc or its translations.
        // This could be improved by setting a specific context using the passed document but we
        // would also need to get the translations and set them too.

        File dir = context.getWiki().getTempDirectory(context);
        File tempdir = new File(dir, RandomStringUtils.randomAlphanumeric(8));
        this.tidy.setOutputEncoding(context.getWiki().getEncoding());
        this.tidy.setInputEncoding(context.getWiki().getEncoding());
        try {
            tempdir.mkdirs();
            context.put("pdfexportdir", tempdir);
            context.put("pdfexport-file-mapping", new HashMap<String, File>());
            boolean useLocalPlaceholders = !Utils.arePlaceholdersEnabled(context);
            if (useLocalPlaceholders) {
                Utils.enablePlaceholders(context);
            }
            String content = context.getWiki().parseTemplate("pdf.vm", context).trim();
            if (useLocalPlaceholders) {
                content = Utils.replacePlaceholders(content, context);
                Utils.disablePlaceholders(context);
            }
            exportHtml(content, out, type, context);
        } finally {
            try {
                FileUtils.deleteDirectory(tempdir);
            } catch (IOException ex) {
                // Should not happen, but it's nothing serious, just that temporary files are left on the disk.
                LOGGER.warn("Failed to cleanup temporary files after a PDF export", ex);
            }
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @see PdfExport#exportHtml(String, OutputStream, com.xpn.xwiki.pdf.api.PdfExport.ExportType, XWikiContext)
     * @since 3.0M2
     */
    public void exportHtml(String html, OutputStream out, ExportType type, XWikiContext context)
            throws XWikiException {
        exportXHTML(applyCSS(convertToStrictXHtml(html), context), out, type, context);
    }

    /**
     * Cleans up an HTML document, turning it into valid XHTML.
     * 
     * @param input the source HTML to process
     * @return the cleaned up source
     */
    private String convertToStrictXHtml(String input) {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Cleaning HTML: " + input);
        }

        try {
            // First step, Tidy the document
            StringWriter tidyOutput = new StringWriter(input.length());
            this.tidy.parse(new StringReader(input), tidyOutput);

            // Tidy can't solve duplicate IDs, so it needs to be done manually
            DocumentBuilder docBuilder = dbFactory.newDocumentBuilder();
            docBuilder.setEntityResolver(Utils.getComponent(EntityResolver.class));
            String tidied = tidyOutput.toString().trim();
            if (StringUtils.isEmpty(tidied)) {
                tidied = input.trim();
            }
            Document doc = docBuilder.parse(new InputSource(new StringReader(tidied)));
            List<String> seenIDs = new ArrayList<String>();
            this.cleanIDs(doc.getDocumentElement(), seenIDs);

            // Write back the fixed document to a String
            LSOutput output = lsImpl.createLSOutput();
            StringWriter result = new StringWriter();
            output.setCharacterStream(result);
            LSSerializer serializer = lsImpl.createLSSerializer();
            serializer.setNewLine("\n");
            output.setEncoding(doc.getXmlEncoding());
            serializer.write(doc, output);
            return result.toString();
        } catch (Exception ex) {
            LOGGER.warn("Failed to tidy document for export: " + ex.getMessage(), ex);
            return input;
        }
    }

    /**
     * Solve duplicate IDs found in a document. When an already seen ID is encountered, a new ID is created for the
     * current element by suffixing its original ID with a counter.
     * 
     * @param e the current element to process
     * @param seenIDs the list of already encountered IDs so far
     */
    private void cleanIDs(org.w3c.dom.Element e, List<String> seenIDs) {
        String id = e.getAttribute("id");
        if (StringUtils.isNotEmpty(id)) {
            if (seenIDs.contains(id)) {
                int i = 0;
                while (seenIDs.contains(id + i)) {
                    ++i;
                }
                e.setAttribute("id", id + i);
                seenIDs.add(id + i);
            } else {
                seenIDs.add(id);
            }
        }
        NodeList children = e.getChildNodes();
        for (int i = 0; i < children.getLength(); ++i) {
            if (children.item(i) instanceof org.w3c.dom.Element) {
                cleanIDs((org.w3c.dom.Element) children.item(i), seenIDs);
            }
        }
    }

    /**
     * Convert a valid XHTML document into PDF. No further processing of the XHTML occurs.
     * <p>
     * Note: This method is protected just allow other exporters to hook their code and use the PDF export
     * infrastructure. This is just a temporary solution. The PDF export code needs to be redesigned because it has
     * parts than can be reused for other export formats.
     * 
     * @param xhtml the source document to transform
     * @param out where to write the resulting document
     * @param type the type of the output: PDF or RTF
     * @param context the current request context
     * @throws XWikiException if the conversion fails for any reason
     */
    protected void exportXHTML(String xhtml, OutputStream out, ExportType type, XWikiContext context)
            throws XWikiException {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Final XHTML for export: " + xhtml);
        }

        // XSL Transformation to XML-FO
        String xmlfo = convertXHtmlToXMLFO(xhtml, context);

        // Debug output
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("XSL-FO source: " + xmlfo);
        }

        renderXSLFO(xmlfo, out, type);
    }

    /**
     * Convert a valid XHTML document into an XSL-FO document through XSLT transformations. Two transformations are
     * involved:
     * <ol>
     * <li>A base transformation which converts the XHTML into a temporary XSL-FO; it uses the <tt>xhtml2fo.xsl</tt>
     * file, or the <tt>xhtmlxsl</tt> property of the applied PDFTemplate.</li>
     * <li>An eventual post-processing transformation which cleans up the temporary XSL-FO in order to avoid FOP bugs;
     * it uses the <tt>fop.xsl</tt> file, or the <tt>fopxsl</tt> property of the applied PDFTemplate.</li>
     * </ol>
     * 
     * @param xhtml the XHTML document to convert
     * @param context the current request context
     * @return the resulting XML-FO document
     * @throws XWikiException if the conversion fails for any reason
     */
    private String convertXHtmlToXMLFO(String xhtml, XWikiContext context) throws XWikiException {
        String xmlfo = applyXSLT(xhtml, getXhtml2FopXslt(context));
        return applyXSLT(xmlfo, getFopCleanupXslt(context));
    }

    /**
     * Convert an XSL-FO document into PDF.
     * 
     * @param xmlfo the source FO to render
     * @param out where to write the resulting document
     * @param type the type of the output: PDF or RTF
     * @throws XWikiException if the conversion fails for any reason
     */
    private void renderXSLFO(String xmlfo, OutputStream out, ExportType type) throws XWikiException {
        try {
            FOUserAgent foUserAgent = fopFactory.newFOUserAgent();

            // Construct fop with desired output format
            Fop fop = fopFactory.newFop(type.getMimeType(), foUserAgent, out);

            // Identity transformer
            Transformer transformer = transformerFactory.newTransformer();

            // Setup input stream
            Source source = new StreamSource(new StringReader(xmlfo));

            // Resulting SAX events (the generated FO) must be piped through to FOP
            Result res = new SAXResult(fop.getDefaultHandler());

            // Start XSLT transformation and FOP processing
            transformer.transform(source, res);

            // Result processing
            FormattingResults foResults = fop.getResults();
            if (foResults != null && LOGGER.isDebugEnabled()) {
                @SuppressWarnings("unchecked")
                java.util.List<PageSequenceResults> pageSequences = foResults.getPageSequences();
                for (PageSequenceResults pageSequenceResults : pageSequences) {
                    LOGGER.debug(
                            "PageSequence " + StringUtils.defaultIfEmpty(pageSequenceResults.getID(), "<no id>")
                                    + " generated " + pageSequenceResults.getPageCount() + " pages.");
                }
                LOGGER.debug("Generated " + foResults.getPageCount() + " pages in total.");
            }
        } catch (IllegalStateException e) {
            throw createException(e, type, XWikiException.ERROR_XWIKI_APP_SEND_RESPONSE_EXCEPTION);
        } catch (Exception e) {
            throw createException(e, type, XWikiException.ERROR_XWIKI_EXPORT_PDF_FOP_FAILED);
        }
    }

    /**
     * Applies an XSLT transformation to an XML document.
     * 
     * @param xml the XML document to convert
     * @param xslt the XSLT to apply
     * @return the converted document
     * @throws XWikiException if the transformation fails for any reason
     */
    protected String applyXSLT(String xml, InputStream xslt) throws XWikiException {
        StringWriter output = new StringWriter(xml.length());

        try {
            DocumentBuilder docBuilder = dbFactory.newDocumentBuilder();
            docBuilder.setEntityResolver(Utils.getComponent(EntityResolver.class));
            Document xsltDocument = docBuilder.parse(new InputSource(xslt));
            Document xmlDocument = docBuilder.parse(new InputSource(new StringReader(xml)));
            Transformer transformer = transformerFactory.newTransformer(new DOMSource(xsltDocument));
            transformer.transform(new DOMSource(xmlDocument), new StreamResult(output));
        } catch (Exception e) {
            throw new XWikiException(XWikiException.MODULE_XWIKI_EXPORT,
                    XWikiException.ERROR_XWIKI_EXPORT_XSL_FAILED, "XSL Transformation Failed", e);
        }

        return output.toString();
    }

    /**
     * Apply CSS styling to an XHTML document. The style to apply is taken from:
     * <ol>
     * <li>the <tt>pdf.css</tt> skin file</li>
     * <li>and the <tt>style</tt> property of the applied PDFTemplate</li>
     * </ol>
     * The content found in these locations is concatenated. The CSS rules are applied on the document, and the
     * resulting style properties are embedded in the document, inside <tt>style</tt> attributes. The resulting XHTML
     * document with the inlined style is then serialized and returned.
     * 
     * @param html the valid XHTML document to style
     * @param context the current request context
     * @return the document with inlined style
     * @throws XWikiException if any exception occurs
     */
    private String applyCSS(String html, XWikiContext context) throws XWikiException {
        String css = (context == null || context.getWiki() == null) ? ""
                : context.getWiki().parseTemplate("pdf.css", context);
        String style = getPDFTemplateProperty("style", context);
        if (style != null) {
            css += style;
        }
        return applyCSS(html, css, context);
    }

    /**
     * Apply a CSS style sheet to an XHTML document and return the document with the resulting style properties inlined
     * in <tt>style</tt> attributes.
     * 
     * @param html the valid XHTML document to style
     * @param css the style sheet to apply
     * @param context the current request context
     * @return the document with inlined style
     */
    private String applyCSS(String html, String css, XWikiContext context) {
        try {
            // Prepare the input
            Reader re = new StringReader(html);
            InputSource source = new InputSource(re);
            SAXReader reader = new SAXReader(XHTMLDocumentFactory.getInstance());
            reader.setEntityResolver(new DefaultEntityResolver());
            XHTMLDocument document = (XHTMLDocument) reader.read(source);

            // Apply the style sheet
            document.setDefaultStyleSheet(new DOM4JCSSStyleSheet(null, null, null));
            document.addStyleSheet(new org.w3c.css.sac.InputSource(new StringReader(css)));
            applyInlineStyle(document.getRootElement());
            OutputFormat outputFormat = new OutputFormat("", false);
            if ((context == null) || (context.getWiki() == null)) {
                outputFormat.setEncoding("UTF-8");
            } else {
                outputFormat.setEncoding(context.getWiki().getEncoding());
            }
            StringWriter out = new StringWriter();
            XMLWriter writer = new XMLWriter(out, outputFormat);
            writer.write(document);
            String result = out.toString();
            // Debug output
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("HTML with CSS applied: " + result);
            }
            return result;
        } catch (Exception ex) {
            LOGGER.warn("Failed to apply CSS: " + ex.getMessage(), ex);
            return html;
        }
    }

    /**
     * Recursively inline the computed style that applies to a DOM Element into the {@code style} attribute of that
     * Element.
     * 
     * @param element the Element whose style should be inlined
     */
    private void applyInlineStyle(Element element) {
        for (int i = 0; i < element.nodeCount(); i++) {
            org.dom4j.Node node = element.node(i);
            if (node instanceof CSSStylableElement) {
                CSSStylableElement styleElement = (CSSStylableElement) node;
                CSSStyleDeclaration style = styleElement.getComputedStyle();
                if (style != null && StringUtils.isNotEmpty(style.getCssText())) {
                    styleElement.addAttribute("style", styleElement.getComputedStyle().getCssText());
                }
            }
            if (node instanceof Element) {
                applyInlineStyle((Element) node);
            }
        }
    }

    /**
     * Get the XSLT for converting (valid) XHTML to XSL-FO. The content is searched in:
     * <ol>
     * <li>the <tt>xhtmlxsl</tt> property of the current PDFTemplate</li>
     * <li>the <tt>xhtml2fo.xsl</tt> resource (usually a file inside xwiki-core-*.jar)</li>
     * </ol>
     * 
     * @param context the current request context
     * @return the content of the XSLT as a byte stream
     */
    private InputStream getXhtml2FopXslt(XWikiContext context) {
        return getXslt("xhtmlxsl", DEFAULT_XHTML2FOP_XSLT, context);
    }

    /**
     * Get the XSLT for post-processing the XSL-FO file. The content is searched in:
     * <ol>
     * <li>the <tt>fopxsl</tt> property of the current PDFTemplate</li>
     * <li>the <tt>fop.xsl</tt> resource (usually a file inside xwiki-core-*.jar)</li>
     * </ol>
     * 
     * @param context the current request context
     * @return the content of the XSLT as a byte stream
     */
    private InputStream getFopCleanupXslt(XWikiContext context) {
        return getXslt("fopxsl", DEFAULT_CLEANUP_XSLT, context);
    }

    /**
     * Get an XSLT file. The content is searched in:
     * <ol>
     * <li>the <tt>fopxsl</tt> property of the current <tt>PDFTemplate</tt></li>
     * <li>the <tt>fop.xsl</tt> resource (usually a file inside <tt>xwiki-core-*.jar</tt>)</li>
     * </ol>
     * 
     * @param propertyName the name of the <tt>XWiki.PDFClass</tt> property to read from the current PDFTemplate
     *            document
     * @param fallbackFile the name of a resource file to use when the PDFTemplate does not contain an override
     * @param context the current request context
     * @return the content of the XSLT as a byte stream
     */
    protected InputStream getXslt(String propertyName, String fallbackFile, XWikiContext context) {
        String xsl = getPDFTemplateProperty(propertyName, context);
        if (!StringUtils.isBlank(xsl)) {
            try {
                return IOUtils.toInputStream(xsl, context.getWiki().getEncoding());
            } catch (IOException ex) {
                // This really shouldn't happen
            }
        }
        return getClass().getClassLoader().getResourceAsStream(fallbackFile);
    }

    /**
     * Read a property from the current PDFTemplate document, and pass it through the Velocity engine.
     * 
     * @param propertyName the property to read
     * @param context the current request context
     * @return the content of the property, velocity-parsed, or an empty string if there's no such property in the
     *         current PDFTemplate
     */
    private String getPDFTemplateProperty(String propertyName, XWikiContext context) {
        String pdftemplate = context.getRequest().getParameter("pdftemplate");
        String currentWiki = dab.getCurrentDocumentReference().getRoot().getName();
        DocumentReference templateReference = dab.getCurrentDocumentReference();
        DocumentReference classReference = new DocumentReference(currentWiki, "XWiki", "PDFClass");

        if (StringUtils.isNotEmpty(pdftemplate)) {
            templateReference = referenceResolver.resolve(pdftemplate);
        }

        String result = (String) dab.getProperty(templateReference, classReference, propertyName);
        if (StringUtils.isBlank(result)) {
            return "";
        }
        String templateName = referenceSerializer.serialize(templateReference);
        try {
            StringWriter writer = new StringWriter();
            VelocityEngine engine = velocityManager.getVelocityEngine();
            try {
                VelocityContext vcontext = velocityManager.getVelocityContext();
                engine.startedUsingMacroNamespace(templateName);
                velocityManager.getVelocityEngine().evaluate(vcontext, writer, templateName, result);
                result = writer.toString();
            } finally {
                engine.stoppedUsingMacroNamespace(templateName);
            }
        } catch (XWikiVelocityException ex) {
            LOGGER.warn("Velocity errors while parsing pdf export extension [" + templateName + "]: "
                    + ex.getMessage());
        }
        return result;
    }

    /**
     * Create an XWikiException object with the given source, export type and error type.
     * 
     * @param source the source exception that is forwarded
     * @param exportType the type of the export performed while the exception occurred, {@link #PDF} or {@link #RTF}
     * @param errorType the type of error that occurred, one of the constants in {@link XWikiException}
     * @return a new XWikiException object
     */
    private XWikiException createException(Throwable source, ExportType exportType, int errorType) {
        return new XWikiException(XWikiException.MODULE_XWIKI_EXPORT, errorType,
                "Exception while exporting " + exportType.getExtension(), source);
    }
}