/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.commons.jelly.tags.xml;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Iterator;
import java.util.List;

import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.URIResolver;
import javax.xml.transform.sax.SAXResult;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.sax.SAXTransformerFactory;
import javax.xml.transform.sax.TransformerHandler;
import javax.xml.transform.stream.StreamSource;

import org.apache.commons.jelly.JellyContext;
import org.apache.commons.jelly.JellyException;
import org.apache.commons.jelly.JellyTagException;
import org.apache.commons.jelly.MissingAttributeException;
import org.apache.commons.jelly.Script;
import org.apache.commons.jelly.Tag;
import org.apache.commons.jelly.XMLOutput;
import org.apache.commons.jelly.impl.ScriptBlock;
import org.apache.commons.jelly.impl.StaticTagScript;
import org.apache.commons.jelly.impl.TagScript;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.dom4j.Document;
import org.dom4j.io.DocumentResult;
import org.dom4j.io.DocumentSource;
import org.xml.sax.ContentHandler;
import org.xml.sax.DTDHandler;
import org.xml.sax.EntityResolver;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXNotRecognizedException;
import org.xml.sax.SAXNotSupportedException;
import org.xml.sax.XMLReader;
import org.xml.sax.ext.LexicalHandler;
import org.xml.sax.helpers.XMLReaderFactory;

/** A tag which parses some XML, applies an xslt transform to it
  * and defines a variable with the transformed Document.
  * The XML can either be specified as its body or can be passed in via the
  * xml property which can be a Reader, InputStream, URL or String URI.
  *
  * The XSL can be passed in via the
  * xslt property which can be a Reader, InputStream, URL or String URI.
  *
  * @author Robert Leftwich
  * @version $Revision: 1808985 $
  */
public class TransformTag extends ParseTag {

    /** The Log to which logging calls will be made. */
    private static final Log log = LogFactory.getLog(TransformTag.class);

    /** Propert name for lexical handler */
    private static final String LEXICAL_HANDLER_PROPERTY =
        "http://xml.org/sax/properties/lexical-handler";

    /** The xslt to parse, either a String URI, a Reader or InputStream */
    private Object xslt;

    /** The xsl transformer factory */
    private SAXTransformerFactory tf;

    /** the transformer handler, doing the real work */
    private TransformerHandler transformerHandler;

    /**
     * Constructor for TransformTag.
     */
    public TransformTag() {
        super();
        this.tf = (SAXTransformerFactory) TransformerFactory.newInstance();
    }

    // Tag interface
    //-------------------------------------------------------------------------

    /**
     * Process this tag instance
     *
     * @param output The pipeline for xml events
     * @throws Exception - when required attributes are missing
     */
    public void doTag(XMLOutput output) throws MissingAttributeException, JellyTagException {

        if (null == this.getXslt()) {
            throw new MissingAttributeException("The xslt attribute cannot be null");
        }

        // set a resolver to locate uri
        this.tf.setURIResolver(createURIResolver());

        try {
            this.transformerHandler =
                this.tf.newTransformerHandler(this.getObjAsSAXSource(this.getXslt()));
        }
        catch (TransformerConfigurationException e) {
            throw new JellyTagException(e);
        }

        // run any nested param tags
        this.doNestedParamTag(output);

        try {
            // get a reader to provide SAX events to transformer
            XMLReader xmlReader = this.createXMLReader();
            xmlReader.setContentHandler(this.transformerHandler);
            xmlReader.setProperty(LEXICAL_HANDLER_PROPERTY, this.transformerHandler);

            // handle result differently, depending on if var is specified
            String varName = this.getVar();
            if (null == varName) {
                // pass the result of the transform out as SAX events
                this.transformerHandler.setResult(this.createSAXResult(output));
                xmlReader.parse(this.getXMLInputSource());
            }
            else {
                // pass the result of the transform out as a document
                DocumentResult result = new DocumentResult();
                this.transformerHandler.setResult(result);
                xmlReader.parse(this.getXMLInputSource());

                // output the result as a variable
                Document transformedDoc = result.getDocument();
                this.context.setVariable(varName, transformedDoc);
            }
        }
        catch (SAXException e) {
            throw new JellyTagException(e);
        }
        catch (IOException e) {
            throw new JellyTagException(e);
        }

    }

    // Properties
    //-------------------------------------------------------------------------

    /**
     * Gets the source of the XSL which is either a String URI, Reader or
     * InputStream
     *
     * @returns xslt    The source of the xslt
     */
    public Object getXslt() {
        return this.xslt;
    }

    /**
     * Sets the source of the XSL which is either a String URI, Reader or
     * InputStream
     *
     * @param xslt    The source of the xslt
     */
    public void setXslt(Object xslt) {
        this.xslt = xslt;
    }

    public void setParameterValue(String name, Object value) {
        this.transformerHandler.getTransformer().setParameter(name, value);
    }

    // Implementation methods
    //-------------------------------------------------------------------------

    /**
     * Creates a new URI Resolver so that URIs inside the XSLT document can be
     * resolved using the JellyContext
     *
     * @return a URI Resolver for the JellyContext
     */
    protected URIResolver createURIResolver() {
        return new URIResolver() {
            public Source resolve(String href, String base)
                throws TransformerException {

                if (log.isDebugEnabled() ) {
                    log.debug( "base: " + base + " href: " + href );
                }

                // pass if we don't have a systemId
                if (null == href)
                    return null;

                // @todo
                // #### this is a pretty simplistic implementation.
                // #### we should really handle this better such that if
                // #### base is specified as an absolute URL
                // #### we trim the end off it and append href
                return new StreamSource(context.getResourceAsStream(href));
            }
        };
    }

    /**
     * Factory method to create a new SAXResult for the given
     * XMLOutput so that the output of an XSLT transform will go
     * directly into the XMLOutput that we are given.
     *
     * @param output The destination of the transform output
     * @return A SAXResult for the transfrom output
     */
    protected Result createSAXResult(XMLOutput output) {
        SAXResult result = new SAXResult(output);
        result.setLexicalHandler(output);
        return result;
    }

    /**
     * Factory method to create a new XMLReader for this tag
     * so that the input of the XSLT transform comes from
     * either the xml var, the nested tag or the tag body.
     *
     * @return XMLReader for the transform input
     * @throws SAXException
     *             If the value of the "org.xml.sax.driver" system property
     *             is null, or if the class cannot be loaded and instantiated.
     */
    protected XMLReader createXMLReader() throws SAXException {
        XMLReader xmlReader = null;
        Object xmlReaderSourceObj = this.getXml();
        // if no xml source specified then get from body
        // otherwise convert it to a SAX source
        if (null == xmlReaderSourceObj) {
            xmlReader = new TagBodyXMLReader(this);
        }
        else {
            xmlReader = XMLReaderFactory.createXMLReader();
        }

        return xmlReader;
    }

    /**
     * Helper method to get the appropriate xml input source
     * so that the input of the XSLT transform comes from
     * either the xml var, the nested tag or the tag body.
     *
     * @return InputSource for the transform input
     */
    protected InputSource getXMLInputSource() {
        InputSource xmlInputSource = null;
        Object xmlInputSourceObj = this.getXml();
        // if no xml source specified then get from tag body
        // otherwise convert it to an input source
        if (null == xmlInputSourceObj) {
            xmlInputSource = new TagBodyInputSource();
        } else {
            xmlInputSource = this.getInputSourceFromObj(xmlInputSourceObj);
        }
        return xmlInputSource;
    }

    /**
     * Helper method to convert the specified object to a SAX source
     *
     * @return SAXSource from the source object or null
     */
    protected SAXSource getObjAsSAXSource(Object saxSourceObj) {
        SAXSource saxSource = null;
        if (null != saxSourceObj) {
            if (saxSourceObj instanceof Document) {
                saxSource =  new DocumentSource((Document) saxSourceObj);
            } else {
                InputSource xmlInputSource =
                    this.getInputSourceFromObj(saxSourceObj);
                saxSource = new SAXSource(xmlInputSource);
            }
        }

        return saxSource;
    }

    /**
     * Helper method to get an xml input source for the supplied object
     *
     * @return InputSource for the object or null
     */
    protected InputSource getInputSourceFromObj(Object sourceObj ) {
        InputSource xmlInputSource = null;
        if (sourceObj instanceof Document) {
            SAXSource saxSource = new DocumentSource((Document) sourceObj);
            xmlInputSource = saxSource.getInputSource();
        } else {
            if (sourceObj instanceof String) {
                String uri = (String) sourceObj;
                xmlInputSource = new InputSource(context.getResourceAsStream(uri));
            }
            else if (sourceObj instanceof Reader) {
                xmlInputSource = new InputSource((Reader) sourceObj);
            }
            else if (sourceObj instanceof InputStream) {
                xmlInputSource = new InputSource((InputStream) sourceObj);
            }
            else if (sourceObj instanceof URL) {
                String uri = ((URL) sourceObj).toString();
                xmlInputSource = new InputSource(context.getResourceAsStream(uri));
            }
            else if (sourceObj instanceof File) {
                try {
                    String uri = ((File) sourceObj).toURL().toString();
                    xmlInputSource = new InputSource(context.getResourceAsStream(uri));
                }
                catch (MalformedURLException e) {
                    throw new IllegalArgumentException(
                        "This should never occur. We should always be able to convert a File to a URL" + e );
                }
            }
            else {
                throw new IllegalArgumentException(
                    "Invalid source argument. Must be a String, Reader, InputStream or URL."
                        + " Was type; "
                        + sourceObj.getClass().getName()
                        + " with value: "
                        + sourceObj);
            }
        }

        return xmlInputSource;
    }

    /**
     * Helper method to run any nested param tags
     *
    * @param output The destination for any SAX output (not actually used)
     */
    private void doNestedParamTag(XMLOutput output) throws JellyTagException {
        // find any nested param tags and run them
        Script bodyScript = this.getBody();
        
        if (bodyScript instanceof ScriptBlock) {
            ScriptBlock scriptBlock = (ScriptBlock) bodyScript;
            List scriptList = scriptBlock.getScriptList();
            for (Iterator iter = scriptList.iterator(); iter.hasNext(); ) {
                Script script = (Script) iter.next();
                if (script instanceof TagScript) {

                    Tag tag = null;
                    try {
                        tag = ((TagScript) script).getTag(getContext());
                    } catch (JellyException e) {
                        throw new JellyTagException(e);
                    }

                    if (tag instanceof ParamTag) {
                        script.run(context, output);
                    }


                }
            }
        }
    }

    
    /** A helper class that converts a transform tag body to an XMLReader
      * to hide the details of where the input for the transform is obtained
      *
      * @author <a href="mailto:robert@leftwich.info">Robert Leftwich</a>
      * @version $Revision: 1808985 $
      */
    private class TagBodyXMLReader implements XMLReader {

        /** The tag whose body is to be read. */
        private Tag tag;

        /** The destination for the sax events generated by the reader. */
        private XMLOutput xmlOutput;

        /** Storage for a DTDHandler if set by the user of the reader. */
        private DTDHandler dtdHandler;

        /** Storage for a ErrorHandler if set by the user of the reader. */
        private ErrorHandler errorHandler;

        /** Storage for a EntityResolver if set by the user of the reader. */
        private EntityResolver entityResolver;

        /**
         * Construct an XMLReader for the specified Tag
         *
         * @param tag    The Tag to convert to an XMLReader
         */
        public TagBodyXMLReader(Tag tag)
        {
            this.tag = tag;
            this.xmlOutput = new XMLOutput();
        }

        // Methods
        //-------------------------------------------------------------------------

        /**
         * Parse an XML source.
         *
         * @param input  The source of the xml
         * @throws SAXException -
         *             Any SAX exception, possibly wrapping another exception.
         * @throws IOException -
         *             An IO exception from the parser, possibly from a byte
                       stream or character stream supplied by the application.
         */
        public void parse(InputSource input)
        throws IOException, SAXException
        {
            // safety check that we are being used correctly
            if (input instanceof TagBodyInputSource) {
                this.doInvokeBody();
            } else {
                throw new SAXException("Invalid input source");
            }
        }

        /**
         * Parse an XML source specified by a system id
         *
         * @param input  The system identifier (URI)
         * @throws SAXException -
         *             Any SAX exception, possibly wrapping another exception.
         * @throws IOException -
         *             An IO exception from the parser, possibly from a byte
                       stream or character stream supplied by the application.
         */
        public void parse(String systemId)
        throws IOException, SAXException
        {
            this.doInvokeBody();
        }

        // Helper methods
        //-------------------------------------------------------------------------

        /**
         * Actually invoke the tag body to generate the SAX events
         *
         * @throws SAXException -
         *             Any SAX exception, possibly wrapping another exception.
         */
        private void doInvokeBody() throws SAXException {
            try {
                if (this.shouldParseBody()) {
                    XMLReader anXMLReader = XMLReaderFactory.createXMLReader();
                    anXMLReader.setContentHandler(this.xmlOutput);
                    anXMLReader.setProperty(LEXICAL_HANDLER_PROPERTY,this.xmlOutput);
                    StringWriter writer = new StringWriter();
                    this.tag.invokeBody(XMLOutput.createXMLOutput(writer));
                    Reader reader = new StringReader(writer.toString());
                    anXMLReader.parse(new InputSource(reader));
                } else {
                    this.tag.invokeBody(this.xmlOutput);
                }
            } catch (Exception ex) {
                throw new SAXException(ex);
            }
        }

        /**
         * Helper method to determin if nested body needs to be parsed by (an
         * xml parser, i.e. its only text) to generate SAX events or not
         *
         * @return True if tag body should be parsed or false if invoked only
         * @throws JellyTagException
         */
        private boolean shouldParseBody() throws JellyTagException {
            boolean result = false;
            // check to see if we need to parse the body or just invoke it
            Script bodyScript = this.tag.getBody();
            
            if (bodyScript instanceof ScriptBlock) {
                ScriptBlock scriptBlock = (ScriptBlock) bodyScript;
                List scriptList = scriptBlock.getScriptList();
                for (Iterator iter = scriptList.iterator(); iter.hasNext(); ) {
                    Script script = (Script) iter.next();
                    if (script instanceof StaticTagScript) {
                        result = true;
                         break;
                    }
                }
            }
            return result;
        }

        // Properties
        //-------------------------------------------------------------------------

        /**
         * Gets the SAX ContentHandler to feed SAX events into
         *
         * @return the SAX ContentHandler to use to feed SAX events into
         */
        public ContentHandler getContentHandler() {
            return this.xmlOutput.getContentHandler();
        }

        /**
         * Sets the SAX ContentHandler to feed SAX events into
         *
         * @param contentHandler is the ContentHandler to use.
         *      This value cannot be null.
         */
        public void setContentHandler(ContentHandler contentHandler) {
            this.xmlOutput.setContentHandler(contentHandler);
            // often classes will implement LexicalHandler as well
            if (contentHandler instanceof LexicalHandler) {
                this.xmlOutput.setLexicalHandler((LexicalHandler) contentHandler);
            }
        }

        /**
         * Gets the DTD Handler to feed SAX events into
         *
         * @return the DTD Handler to use to feed SAX events into
         */
        public DTDHandler getDTDHandler() {
            return this.dtdHandler;
        }

        /**
         * Sets the DTD Handler to feed SAX events into
         *
         * @param the DTD Handler to use to feed SAX events into
         */
        public void setDTDHandler(DTDHandler dtdHandler) {
            this.dtdHandler = dtdHandler;
        }

        /**
         * Gets the Error Handler to feed SAX events into
         *
         * @return the Error Handler to use to feed SAX events into
         */
        public ErrorHandler getErrorHandler() {
            return this.errorHandler;
        }

        /**
         * Sets the Error Handler to feed SAX events into
         *
         * @param the Error Handler to use to feed SAX events into
         */
        public void setErrorHandler(ErrorHandler errorHandler) {
            // save the error handler
            this.errorHandler = errorHandler;
        }

        /**
         * Gets the Entity Resolver to feed SAX events into
         *
         * @return the Entity Resolver to use to feed SAX events into
         */
        public EntityResolver getEntityResolver() {
            return this.entityResolver;
        }

        /**
         * Sets the Entity Resolver to feed SAX events into
         *
         * @param the Entity Resolver to use to feed SAX events into
         */
        public void setEntityResolver(EntityResolver entityResolver) {
            this.entityResolver = entityResolver;
        }

        /**
         * Lookup the value of a property
         *
         * @param name - The property name, which is a fully-qualified URI.
         * @return - The current value of the property.
         * @throws SAXNotRecognizedException -
         *            When the XMLReader does not recognize the property name.
         * @throws SAXNotSupportedException -
         *            When the XMLReader recognizes the property name but
         *            cannot determine its value at this time.
         */
        public Object getProperty(String name)
        throws SAXNotRecognizedException, SAXNotSupportedException
        {
            // respond to the lexical handler request
            if (name.equalsIgnoreCase(LEXICAL_HANDLER_PROPERTY)) {
                return this.xmlOutput.getLexicalHandler();
            } else {
                // do nothing
                return null;
            }
        }

        /**
         * Set the value of a property
         *
         * @param name - The property name, which is a fully-qualified URI.
         * @param value - The property value
         * @throws SAXNotRecognizedException -
         *            When the XMLReader does not recognize the property name.
         * @throws SAXNotSupportedException -
         *            When the XMLReader recognizes the property name but
         *            cannot determine its value at this time.
         */
        public void setProperty(String name, Object value)
        throws SAXNotRecognizedException, SAXNotSupportedException
        {
            // respond to the lexical handler setting
            if (name.equalsIgnoreCase(LEXICAL_HANDLER_PROPERTY)) {
                this.xmlOutput.setLexicalHandler((LexicalHandler) value);
            }
        }

        /**
         * Lookup the value of a feature
         *
         * @param name - The feature name, which is a fully-qualified URI.
         * @return - The current state of the feature (true or false)
         * @throws SAXNotRecognizedException -
         *            When the XMLReader does not recognize the feature name.
         * @throws SAXNotSupportedException -
         *            When the XMLReader recognizes the feature name but
         *            cannot determine its value at this time.
         */
        public boolean getFeature(String name)
        throws SAXNotRecognizedException, SAXNotSupportedException
        {
            // do nothing
            return false;
        }

        /**
         * Set the value of a feature
         *
         * @param name - The feature name, which is a fully-qualified URI.
         * @param value - The current state of the feature (true or false)
         * @throws SAXNotRecognizedException -
         *            When the XMLReader does not recognize the feature name.
         * @throws SAXNotSupportedException -
         *            When the XMLReader recognizes the feature name but
         *            cannot determine its value at this time.
         */
        public void setFeature(String name, boolean value)
        throws SAXNotRecognizedException, SAXNotSupportedException
        {
            // do nothing
        }
    }

    /** A marker class used by the TagBodyXMLReader as a sanity check
      * (i.e. The source is not actually used)
      *
      */
    private class TagBodyInputSource extends InputSource {

        /**
         * Construct an instance of this marker class
         */
        public TagBodyInputSource() {
        }
    }

}
