/**
 * Copyright (C) 2010 Orbeon, Inc.
 *
 * This program 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 program 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.
 *
 * The full text of the license is available at http://www.gnu.org/copyleft/lesser.html
 */
package org.orbeon.oxf.xml;

import org.dom4j.Document;
import org.dom4j.io.DocumentSource;
import org.orbeon.oxf.common.OXFException;
import org.orbeon.oxf.pipeline.api.TransformerXMLReceiver;
import org.orbeon.oxf.processor.transformer.TransformerURIResolver;
import org.orbeon.oxf.resources.URLFactory;
import org.orbeon.oxf.util.StringBuilderWriter;
import org.orbeon.oxf.util.XPath;
import org.orbeon.oxf.xml.dom4j.*;
import org.orbeon.saxon.Configuration;
import org.orbeon.saxon.TransformerFactoryImpl;
import org.orbeon.saxon.om.DocumentInfo;
import org.orbeon.saxon.om.NodeInfo;
import org.orbeon.saxon.tinytree.TinyBuilder;
import org.w3c.dom.Node;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;

import javax.xml.transform.*;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.sax.SAXResult;
import javax.xml.transform.sax.SAXTransformerFactory;
import javax.xml.transform.sax.TransformerHandler;
import javax.xml.transform.stream.StreamResult;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;

/**
 * Utility class for XSLT and other transformations.
 */
public class TransformerUtils {

    public static final String DEFAULT_OUTPUT_ENCODING = "utf-8";

    /**
     * Property name to use for choosing the amount of indentation.
     */
    public static final String INDENT_AMOUNT_PROPERTY = "{http://orbeon.org/oxf/}indent-spaces";

    private static final String SAXON_INDENT_AMOUNT_PROPERTY = "{http://saxon.sf.net/}indent-spaces";

    // Class.forName is expensive, so we cache mappings
    private static Map<String, Class> classNameToHandlerClass = new HashMap<String, Class>();

    private static Class getTransformerClass(String clazz) throws ClassNotFoundException {
        Class transformerClass = classNameToHandlerClass.get(clazz);
        if (transformerClass == null) {
            transformerClass = Class.forName(clazz);
            classNameToHandlerClass.put(clazz, transformerClass);
        }
        return transformerClass;
    }

    /*
     * NOTE: Factories are not thread-safe. So we should not store them into transformerFactories
     * for now, or we should make sure there is one factory per thread, or that we synchronize.
     */
    public static SAXTransformerFactory getFactory(String className, Map attributes, Configuration configuration) {
        try {
            final SAXTransformerFactory factory; {
                if (className.equals(TransformerFactoryImpl.class.getName())) {
                    factory = new TransformerFactoryImpl(configuration);
                } else {
                    factory = (SAXTransformerFactory) getTransformerClass(className).newInstance();
                }
            }

            for (Iterator i = attributes.keySet().iterator(); i.hasNext();) {
                final String key = (String) i.next();
                factory.setAttribute(key, attributes.get(key));
            }

            return factory;
        } catch (Exception e) {
            throw new OXFException(e);
        }
    }

    /*
     * NOTE: Factories are not thread-safe. So we should not store them into transformerFactories
     * for now, or we should make sure there is one factory per thread, or that we synchronize.
     */
    public static SAXTransformerFactory getFactory(String clazz) {
        try {
            return (SAXTransformerFactory) getTransformerClass(clazz).newInstance();
        } catch (Exception e) {
            throw new OXFException(e);
        }
    }

    public static Transformer getXMLIdentityTransformer() {
        try {
            Transformer transformer = getIdentityTransformer();
            transformer.setOutputProperty(OutputKeys.ENCODING, DEFAULT_OUTPUT_ENCODING);
            transformer.setOutputProperty(OutputKeys.METHOD, "xml");
            transformer.setOutputProperty(OutputKeys.VERSION, "1.0");
            transformer.setOutputProperty(OutputKeys.INDENT, "no");
            transformer.setOutputProperty(INDENT_AMOUNT_PROPERTY, "0");
            return transformer;
        } catch (TransformerException e) {
            throw new OXFException(e);
        }
    }

    /**
     * Apply output properties on a Transformer.
     *
     * @param transformer           transformer to apply properties on
     * @param method                output method
     * @param version               HTML or XML version
     * @param publicDoctype         public doctype
     * @param systemDoctype         system doctype
     * @param encoding              character encoding
     * @param omitXMLDeclaration    whether XML declaration must be omitted
     * @param standalone            whether a standalone declaration must be set and to what value
     * @param indent                whether the HTML or XML must be indented
     * @param indentAmount          amount of indenting for the markup
     */
    public static void applyOutputProperties(Transformer transformer,
                                             String method,
                                             String version,
                                             String publicDoctype,
                                             String systemDoctype,
                                             String encoding,
                                             boolean omitXMLDeclaration,
                                             Boolean standalone,
                                             boolean indent,
                                             int indentAmount) {
        if (method != null && !"".equals(method))
            transformer.setOutputProperty(OutputKeys.METHOD, method);
        if (version != null && !"".equals(version))
            transformer.setOutputProperty(OutputKeys.VERSION, version);
        if (publicDoctype != null && !"".equals(publicDoctype))
            transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, publicDoctype);
        if (systemDoctype != null && !"".equals(systemDoctype))
            transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, systemDoctype);
        if (encoding != null && !"".equals(encoding))
            transformer.setOutputProperty(OutputKeys.ENCODING, encoding);
        transformer.setOutputProperty(OutputKeys.INDENT, indent ? "yes" : "no");
        if (indent)
            transformer.setOutputProperty(INDENT_AMOUNT_PROPERTY, String.valueOf(indentAmount));
        transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, omitXMLDeclaration ? "yes" : "no");
        if (standalone != null)
            transformer.setOutputProperty(OutputKeys.STANDALONE, standalone ? "yes" : "no");
    }

    /**
     * Return a new identity transformer object.
     *
     * @return  a new identity Transformer object
     * @throws TransformerConfigurationException
     */
    public static Transformer getIdentityTransformer() throws TransformerConfigurationException {
        return getIdentityTransformer(XPath.GlobalConfiguration());
    }

    public static Transformer getIdentityTransformer(Configuration configuration) throws TransformerConfigurationException {
        final Transformer transformer = new TransformerFactoryImpl(configuration).newTransformer();
        // Wrap Transformer for properties
        return new TransformerWrapper(transformer, INDENT_AMOUNT_PROPERTY, SAXON_INDENT_AMOUNT_PROPERTY);
    }

    /**
     * Return a new identity TransformerHandler object.
     *
     * @return  a new identity TransformerHandler object
     */
    public static TransformerXMLReceiver getIdentityTransformerHandler() {
        return getIdentityTransformerHandler(XPath.GlobalConfiguration());
    }

    public static TransformerXMLReceiver getIdentityTransformerHandler(Configuration configuration) {
        try {
            TransformerHandler transformerHandler = new TransformerFactoryImpl(configuration).newTransformerHandler();
            // Wrap TransformerHandler for properties
            return new TransformerHandlerWrapper(transformerHandler, INDENT_AMOUNT_PROPERTY, SAXON_INDENT_AMOUNT_PROPERTY);
        } catch (TransformerException e) {
            throw new OXFException(e);
        }
    }

    public static TransformerHandler getTransformerHandler(Templates templates, String clazz, Map attributes, Configuration configuration) throws TransformerConfigurationException {
        return ((attributes != null) ? getFactory(clazz, attributes, configuration) : getFactory(clazz)).newTransformerHandler(templates);
    }

    public static Templates getTemplates(Source source, String clazz, Map attributes, Configuration configuration, ErrorListener errorListener, URIResolver uriResolver)
            throws TransformerConfigurationException {
        final SAXTransformerFactory factory = (attributes != null) ? getFactory(clazz, attributes, configuration) : getFactory(clazz);
        factory.setErrorListener(errorListener);
        factory.setURIResolver(uriResolver);

        final Templates templates = factory.newTemplates(source);
        // These should only be used during stylesheet compilation. It is dangerous to keep them around when the
        // Templates object is cached especially the URI Resolver which may reference PipelineContext objects.
//        factory.setErrorListener(null);// This causes issues with Xalan
        factory.setURIResolver(null);

        return templates;
    }

    /**
     * Transform a W3C DOM node into a dom4j document
     *
     * @param   node    W3C DOM node
     * @return  dom4j document
     * @throws TransformerException
     */
    public static Document domToDom4jDocument(org.w3c.dom.Node node) throws TransformerException {
        final Transformer identity = getIdentityTransformer();
        final LocationDocumentResult documentResult = new LocationDocumentResult();
        identity.transform(new DOMSource(node), documentResult);
        return documentResult.getDocument();
    }

    /**
     * Transform a SAXStore into a dom4j document
     *
     * @param   saxStore input SAXStore
     * @return  dom4j document
     */
    public static Document saxStoreToDom4jDocument(SAXStore saxStore) {
        final TransformerXMLReceiver identity = getIdentityTransformerHandler();
        final LocationDocumentResult documentResult = new LocationDocumentResult();
        identity.setResult(documentResult);
        try {
            saxStore.replay(identity);
        } catch (SAXException e) {
            throw new OXFException(e);
        }
        return documentResult.getDocument();
    }

    // Transform a SAXStore mark into a dom4j document
    public static Document saxStoreMarkToDom4jDocument(SAXStore.Mark mark) {
        final TransformerXMLReceiver identity = getIdentityTransformerHandler();
        final LocationDocumentResult documentResult = new LocationDocumentResult();
        identity.setResult(documentResult);
        try {
            identity.startDocument();
            mark.replay(identity);
            identity.endDocument();
        } catch (SAXException e) {
            throw new OXFException(e);
        }
        return documentResult.getDocument();
    }

    /**
     * Transform a SAXStore into a TinyTree document
     *
     * @param   saxStore input SAXStore
     * @return  DocumentInfo
     */
    public static DocumentInfo saxStoreToTinyTree(Configuration configuration, SAXStore saxStore) {
        final TinyBuilder treeBuilder = new TinyBuilder();
        try {
            final TransformerXMLReceiver identity = getIdentityTransformerHandler(configuration);
            identity.setResult(treeBuilder);
            saxStore.replay(identity);
        } catch (SAXException e) {
            throw new OXFException(e);
        }
        return (DocumentInfo) treeBuilder.getCurrentRoot();
    }

    /**
     * Transform a SAXStore into a DOM document
     *
     * @param   saxStore input SAXStore
     * @return  DOM document
     */
    public static org.w3c.dom.Document saxStoreToDomDocument(SAXStore saxStore) {
        try {
            // Convert to dom4j and then to DOM
            return TransformerUtils.dom4jToDomDocument(saxStoreToDom4jDocument(saxStore));
        } catch (TransformerException e) {
            throw new OXFException(e);
        }

        // NOTE: The more straight and efficient implementation below doesn't seem to work
//        final TransformerHandler identity = getIdentityTransformerHandler();
//        final DOMResult domResult = new DOMResult();
//        identity.setResult(domResult);
//        try {
//            saxStore.replay(identity);
//        } catch (SAXException e) {
//            throw new OXFException(e);
//        }
//        return domResult.getNode().getOwnerDocument();
    }

    /**
     * Transform a dom4j Document into a TinyTree.
     */
    public static DocumentInfo dom4jToTinyTree(Configuration configuration, Document document, boolean location) {
        final TinyBuilder treeBuilder = new TinyBuilder();
        try {
            final Transformer identity = getIdentityTransformer(configuration);
            identity.transform(location ? new LocationDocumentSource(document) : new DocumentSource(document), treeBuilder);
        } catch (TransformerException e) {
            throw new OXFException(e);
        }
        return (DocumentInfo) treeBuilder.getCurrentRoot();
    }

    /**
     * Transform a dom4j document into a W3C DOM document
     *
     * @param   document dom4j document
     * @return  W3C DOM document
     * @throws TransformerException
     */
    public static org.w3c.dom.Document dom4jToDomDocument(Document document) throws TransformerException {
        final Transformer identity = getIdentityTransformer();
        final DOMResult domResult = new DOMResult();
        identity.transform(new DocumentSource(document), domResult);
        final Node resultNode = domResult.getNode();
        return (resultNode instanceof org.w3c.dom.Document) ? ((org.w3c.dom.Document) resultNode) : resultNode.getOwnerDocument();
    }

    public static Transformer testCreateTransformerWrapper(Transformer transformer, String publicProperty, String privateProperty) {
        return new TransformerWrapper(transformer, publicProperty, privateProperty);
    }

    /**
     * Transform an InputStream to a dom4j Document.
     */
    public static Document readDom4j(InputStream inputStream, String systemId, boolean handleXInclude, boolean handleLexical) {
        final LocationSAXContentHandler dom4jResult = new LocationSAXContentHandler();
        {
            final TransformerURIResolver resolver;
            final XMLReceiver xmlReceiver;
            if (handleXInclude) {
                // Insert XIncludeContentHandler
                resolver = new TransformerURIResolver(XMLParsing.ParserConfiguration.PLAIN);
                xmlReceiver = new XIncludeReceiver(null, dom4jResult, null, resolver);
            } else {
                resolver = null;
                xmlReceiver = dom4jResult;
            }
            try {
                XMLParsing.inputStreamToSAX(inputStream, systemId, xmlReceiver, XMLParsing.ParserConfiguration.PLAIN, handleLexical);
            } finally {
                if (resolver != null)
                    resolver.destroy();
            }
        }
        return dom4jResult.getDocument();

    }

    /**
     * Transform an InputStream to a dom4j Document.
     */
    public static Document readDom4j(Source source, boolean handleXInclude) {
        final LocationSAXContentHandler dom4jResult = new LocationSAXContentHandler();
        {
            final TransformerURIResolver resolver;
            final XMLReceiver xmlReceiver;
            if (handleXInclude) {
                // Insert XIncludeContentHandler
                resolver = new TransformerURIResolver(XMLParsing.ParserConfiguration.PLAIN);
                xmlReceiver = new XIncludeReceiver(null, dom4jResult, null, resolver);
            } else {
                resolver = null;
                xmlReceiver = dom4jResult;
            }
            try {
                sourceToSAX(source, xmlReceiver);
            } finally {
                if (resolver != null)
                    resolver.destroy();
            }
        }
        return dom4jResult.getDocument();

    }

    /**
     * Transform an InputStream to a TinyTree.
     */
    public static DocumentInfo readTinyTree(Configuration configuration, InputStream inputStream, String systemId, boolean handleXInclude, boolean handleLexical) {
        final TinyBuilder treeBuilder = new TinyBuilder();
        {
            final TransformerXMLReceiver identityHandler = getIdentityTransformerHandler(configuration);
            identityHandler.setResult(treeBuilder);
            final TransformerURIResolver resolver;
            final XMLReceiver xmlReceiver;
            if (handleXInclude) {
                // Insert XIncludeContentHandler
                resolver = new TransformerURIResolver(XMLParsing.ParserConfiguration.PLAIN);
                xmlReceiver = new XIncludeReceiver(null, identityHandler, null, resolver);
            } else {
                resolver = null;
                xmlReceiver = identityHandler;
            }
            try {
                XMLParsing.inputStreamToSAX(inputStream, systemId, xmlReceiver, XMLParsing.ParserConfiguration.PLAIN, handleLexical);
            } finally {
                if (resolver != null)
                    resolver.destroy();
            }
        }
        return (DocumentInfo) treeBuilder.getCurrentRoot();
    }

    /**
     * Transform a SAX Source to a TinyTree.
     */
    public static DocumentInfo readTinyTree(Configuration configuration, Source source, boolean handleXInclude) {
        final TinyBuilder treeBuilder = new TinyBuilder();
        try {
            final TransformerURIResolver resolver;
            if (handleXInclude) {
                resolver = new TransformerURIResolver(XMLParsing.ParserConfiguration.PLAIN);
            } else {
                resolver = null;
            }
            try {
                if (handleXInclude) {
                    // Insert XIncludeContentHandler
                    final TransformerXMLReceiver identityHandler = getIdentityTransformerHandler(configuration);
                    identityHandler.setResult(treeBuilder);
                    final XMLReceiver receiver = new XIncludeReceiver(null, identityHandler, null, resolver);
                    TransformerUtils.sourceToSAX(source, receiver);
                } else {
                    final Transformer identity = getIdentityTransformer(configuration);
                    identity.transform(source, treeBuilder);
                }
            } finally {
                if (resolver != null)
                    resolver.destroy();
            }
        } catch (TransformerException e) {
            throw new OXFException(e);
        }
        return (DocumentInfo) treeBuilder.getCurrentRoot();
    }

    /**
     * Transform a TinyTree to a dom4j document.
     *
     * This version uses a temporary string as we are having issues with converting directly from TinyTree to dom4j.
     */
    public static Document tinyTreeToDom4j(NodeInfo nodeInfo) {
        try {
            final String xmlString = tinyTreeToString(nodeInfo);
            return Dom4jUtils.readDom4j(xmlString);
        } catch (Exception e) {
            throw new OXFException(e);
        }
    }

    /**
     * Transform a String to a TinyTree.
     */
    public static DocumentInfo stringToTinyTree(Configuration configuration, String string, boolean handleXInclude, boolean handleLexical) {
        try {
            return readTinyTree(configuration, new ByteArrayInputStream(string.getBytes("utf-8")), null, handleXInclude, handleLexical);
        } catch (UnsupportedEncodingException e) {
            throw new OXFException(e);// should not happen
        }
    }

    public static DocumentInfo urlToTinyTree(String url) {
        try {
            final InputStream inputStream = URLFactory.createURL(url).openStream();
            try {
                return readTinyTree(XPath.GlobalConfiguration(), inputStream, null, true, true);
            } finally {
                inputStream.close();
            }
        } catch (IOException e) {
            throw new OXFException(e);
        }
    }

    /**
     * Transform a TinyTree to SAX events.
     */
    public static void writeTinyTree(NodeInfo nodeInfo, XMLReceiver xmlReceiver) {
        sourceToSAX(nodeInfo, xmlReceiver);
    }

    /**
     * Transform a SAX source to SAX events.
     */
    public static void sourceToSAX(Source source, XMLReceiver xmlReceiver) {
        try {
            final Transformer identity = getIdentityTransformer();
            final SAXResult saxResult = new SAXResult(xmlReceiver);
            saxResult.setLexicalHandler(xmlReceiver);
            identity.transform(source, saxResult);
        } catch (TransformerException e) {
            throw new OXFException(e);
        }
    }

    /**
     * Transform a SAX source to SAX events.
     */
    private static void sourceToSAX(Source source, ContentHandler contentHandler) {
        try {
            final Transformer identity = getIdentityTransformer();
            final SAXResult saxResult = new SAXResult(contentHandler);
            identity.transform(source, saxResult);
        } catch (TransformerException e) {
            throw new OXFException(e);
        }
    }

    /**
     * Transform a dom4j Node to SAX events.
     */
    public static void writeDom4j(org.dom4j.Node node, XMLReceiver xmlReceiver) {

        // NOTE: Use dom4j directly instead of sourceToSAX as performance is better
        final LocationSAXWriter locationSAXWriter = new LocationSAXWriter();
        locationSAXWriter.setContentHandler(xmlReceiver);
        locationSAXWriter.setLexicalHandler(xmlReceiver);

        try {
            locationSAXWriter.write(node);
        } catch (SAXException e) {
            throw new OXFException(e);
        }
    }

    /**
     * Transform a dom4j Node to SAX events.
     */
    public static void writeDom4j(org.dom4j.Node node, ContentHandler contentHandler) {
        sourceToSAX(new LocationDocumentSource(node), contentHandler);
    }

    /**
     * Transform a dom4j document to a String.
     */
    public static String dom4jToString(Document document, boolean location) {
        try {
            final Transformer identity = getXMLIdentityTransformer();
            identity.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
            final StringBuilderWriter writer = new StringBuilderWriter();
            identity.transform(location ? new LocationDocumentSource(document) : new DocumentSource(document), new StreamResult(writer));
            return writer.toString();
        } catch (TransformerException e) {
            throw new OXFException(e);
        }
    }

    /**
     * Transform a TinyTree to a String.
     */
    public static String tinyTreeToString(NodeInfo nodeInfo) {
        try {
            final Transformer identity = getXMLIdentityTransformer();
            identity.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
            final StringBuilderWriter writer = new StringBuilderWriter();
            identity.transform(nodeInfo, new StreamResult(writer));
            return writer.toString();
        } catch (TransformerException e) {
            throw new OXFException(e);
        }
    }

    /**
     * Transform a TinyTree to a SAXStore.
     */
    public static SAXStore tinyTreeToSAXStore(NodeInfo nodeInfo) {
        final SAXStore saxStore = new SAXStore();
        sourceToSAX(nodeInfo, saxStore);
        return saxStore;
    }

    /**
     * Transform a dom4j document to a SAXStore.
     */
    public static SAXStore dom4jToSAXStore(Document document, boolean location) {
        final SAXStore saxStore = new SAXStore();
        sourceToSAX(location ? new LocationDocumentSource(document) : new DocumentSource(document), saxStore);
        return saxStore;
    }

    public static String domToString(Node node) {
        try {
            Transformer transformer = getXMLIdentityTransformer();
            DOMSource source = new DOMSource(node);

            StringBuilderWriter writer = new StringBuilderWriter();
            transformer.transform(source, new StreamResult(writer));
            return writer.toString();
        } catch (TransformerException e) {
            throw new OXFException(e);
        }
    }
}

class TransformerWrapper extends Transformer {

    private Transformer transformer;
    private String publicProperty;
    private String privateProperty;

    public TransformerWrapper(Transformer transformer, String publicProperty, String privateProperty) {
        this.transformer = transformer;
        this.publicProperty = publicProperty;
        this.privateProperty = privateProperty;
    }

    public void clearParameters() {
        transformer.clearParameters();
    }

    public ErrorListener getErrorListener() {
        return transformer.getErrorListener();
    }

    public Properties getOutputProperties() {
        Properties properties = transformer.getOutputProperties();

        if (properties.get(privateProperty) == null) {
            // Optimize case where we don't need to map
            return properties;
        } else {
            // Switch property
            properties.put(publicProperty, properties.get(privateProperty));
            properties.remove(privateProperty);

            return properties;
        }
    }

    public String getOutputProperty(String name) throws IllegalArgumentException {
        if (publicProperty.equals(name))
            return transformer.getOutputProperty(privateProperty);
        else
            return transformer.getOutputProperty(name);
    }

    public Object getParameter(String name) {
        return transformer.getParameter(name);
    }

    public URIResolver getURIResolver() {
        return transformer.getURIResolver();
    }

    public void setErrorListener(ErrorListener listener) throws IllegalArgumentException {
        transformer.setErrorListener(listener);
    }

    public void setOutputProperties(Properties oformat) throws IllegalArgumentException {

        if (oformat.get(publicProperty) == null) {
            // Optimize case where we don't need to map
            transformer.setOutputProperties(oformat);
        } else {
            Properties newProperties = (Properties) oformat.clone();

            newProperties.put(privateProperty, oformat.get(publicProperty));
            newProperties.remove(publicProperty);

            transformer.setOutputProperties(newProperties);
        }
    }

    public void setOutputProperty(String name, String value) throws IllegalArgumentException {
        if (publicProperty.equals(name))
            transformer.setOutputProperty(privateProperty, value);
        else
            transformer.setOutputProperty(name, value);
    }

    public void setParameter(String name, Object value) {
        transformer.setParameter(name, value);
    }

    public void setURIResolver(URIResolver resolver) {
        transformer.setURIResolver(resolver);
    }

    public void transform(Source xmlSource, Result outputTarget) throws TransformerException {
        transformer.transform(xmlSource, outputTarget);
    }
}

class TransformerHandlerWrapper extends ForwardingXMLReceiver implements TransformerXMLReceiver {

    private TransformerHandler transformerHandler;
    private String publicProperty;
    private String privateProperty;

    public TransformerHandlerWrapper(TransformerHandler transformerHandler, String publicProperty, String privateProperty) {
        super(transformerHandler, transformerHandler);
        this.transformerHandler = transformerHandler;
        this.publicProperty = publicProperty;
        this.privateProperty = privateProperty;
    }

    public String getSystemId() {
        return transformerHandler.getSystemId();
    }

    public Transformer getTransformer() {
        return new TransformerWrapper(transformerHandler.getTransformer(), publicProperty, privateProperty);
    }

    public void setResult(Result result) throws IllegalArgumentException {
        transformerHandler.setResult(result);
    }

    public void setSystemId(String systemID) {
        transformerHandler.setSystemId(systemID);
    }

    public void comment(char ch[], int start, int length) throws SAXException {
        transformerHandler.comment(ch, start, length);
    }

    public void endCDATA() throws SAXException {
        transformerHandler.endCDATA();
    }

    public void endDTD() throws SAXException {
        transformerHandler.endDTD();
    }

    public void endEntity(String name) throws SAXException {
        transformerHandler.endEntity(name);
    }

    public void startCDATA() throws SAXException {
        transformerHandler.startCDATA();
    }

    public void startDTD(String name, String publicId, String systemId) throws SAXException {
        transformerHandler.startDTD(name, publicId, systemId);
    }

    public void startEntity(String name) throws SAXException {
        transformerHandler.startEntity(name);
    }

    public void notationDecl(String name, String publicId, String systemId) throws SAXException {
        transformerHandler.notationDecl(name, publicId, systemId);
    }

    public void unparsedEntityDecl(String name, String publicId, String systemId, String notationName) throws SAXException {
        transformerHandler.unparsedEntityDecl(name, publicId, systemId, notationName);
    }
}