/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
 *
 * Portions Copyright Apache Software Foundation.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License. You can obtain
 * a copy of the License at https://glassfish.dev.java.net/public/CDDL+GPL.html
 * or glassfish/bootstrap/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at glassfish/bootstrap/legal/LICENSE.txt.
 * Sun designates this particular file as subject to the "Classpath" exception
 * as provided by Sun in the GPL Version 2 section of the License file that
 * accompanied this code.  If applicable, add the following below the License
 * Header, with the fields enclosed by brackets [] replaced by your own
 * identifying information: "Portions Copyrighted [year]
 * [name of copyright owner]"
 *
 * Contributor(s):
 *
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

package org.apache.taglibs.standard.tag.common.xml;

import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;
import java.io.Writer;
import java.util.List;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.JspTagException;
import javax.servlet.jsp.PageContext;
import javax.servlet.jsp.tagext.BodyTagSupport;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.URIResolver;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import org.apache.taglibs.standard.resources.Resources;
import org.apache.taglibs.standard.tag.common.core.ImportSupport;
import org.apache.taglibs.standard.tag.common.core.Util;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;

/**
 * <p>
 * Support for tag handlers for &lt;transform&gt;, the XML transformation tag.
 * </p>
 * @author Shawn Bayern
 */
public abstract class TransformSupport extends BodyTagSupport {

    // *********************************************************************
    // Protected state

    protected Object xml; // attribute

    protected String xmlSystemId; // attribute

    protected Object xslt; // attribute

    protected String xsltSystemId; // attribute

    protected Result result; // attribute

    // *********************************************************************
    // Private state

    private String var; // 'var' attribute

    private int scope; // processed 'scope' attr

    private Transformer t; // actual Transformer

    private TransformerFactory tf; // reusable factory

    private DocumentBuilder db; // reusable factory

    private DocumentBuilderFactory dbf; // reusable factory

    // *********************************************************************
    // Constructor and initialization

    public TransformSupport() {
        super();
        init();
    }

    private void init() {
        xml = xslt = null;
        xmlSystemId = xsltSystemId = null;
        var = null;
        result = null;
        tf = null;
        scope = PageContext.PAGE_SCOPE;
    }

    // *********************************************************************
    // Tag logic

    @Override
    public int doStartTag() throws JspException {
        /*
         * We can set up our Transformer here, so we do so, and we let it
         * receive parameters directly from subtags (instead of caching them.
         */
        try {

            // ************************************
            // Initialize

            // set up our DocumentBuilderFactory if necessary
            if (dbf == null) {
                dbf = DocumentBuilderFactory.newInstance();
                dbf.setNamespaceAware(true);
                dbf.setValidating(false);
            }
            if (db == null) {
                db = dbf.newDocumentBuilder();
            }

            // set up the TransformerFactory if necessary
            if (tf == null) {
                tf = TransformerFactory.newInstance();
            }

            // ************************************
            // Produce transformer

            Source s;
            if (xslt != null) {
                if (!(xslt instanceof String) && !(xslt instanceof Reader) && !(xslt instanceof javax.xml.transform.Source)) {
                    throw new JspTagException(Resources.getMessage("TRANSFORM_XSLT_UNRECOGNIZED"));
                }
                s = getSource(xslt, xsltSystemId);
            } else {
                throw new JspTagException(Resources.getMessage("TRANSFORM_NO_TRANSFORMER"));
            }
            tf.setURIResolver(new JstlUriResolver(pageContext));
            t = tf.newTransformer(s);

            // ###############################################
            // Patch for xalan 2.7.0
            // The method newTransformer(s) should throw a
            // TransformerConfigurationException if the XSLT source is not valid
            // instead of returning an object Transformer null
            if (t == null) {
                throw new TransformerConfigurationException();
            }
            // ###############################################

            return EVAL_BODY_BUFFERED;

        } catch (SAXException ex) {
            throw new JspException(ex);
        } catch (ParserConfigurationException ex) {
            throw new JspException(ex);
        } catch (IOException ex) {
            throw new JspException(ex);
        } catch (TransformerConfigurationException ex) {
            throw new JspException(ex);
        }
    }

    // parse 'xml' or body, transform via our Transformer,
    // and store as 'var' or through 'result'
    @Override
    public int doEndTag() throws JspException {
        try {

            // ************************************
            // Determine source XML

            // if we haven't gotten a source, use the body (which may be empty)
            Object xml = this.xml;
            if (xml == null) {
                if (bodyContent != null && bodyContent.getString() != null) {
                    xml = bodyContent.getString().trim();
                } else {
                    xml = "";
                }
            }

            // let the Source be with you
            Source source = getSource(xml, xmlSystemId);

            // ************************************
            // Conduct the transformation

            // we can assume at most one of 'var' or 'result' is specified
            if (result != null) {
                // we can write directly to the Result
                t.transform(source, result);
            } else if (var != null) {
                // we need a Document
                Document d = db.newDocument();
                Result doc = new DOMResult(d);
                t.transform(source, doc);
                pageContext.setAttribute(var, d, scope);
            } else {
                Result page = new StreamResult(new SafeWriter(pageContext.getOut()));
                t.transform(source, page);
            }

            return EVAL_PAGE;
        } catch (SAXException ex) {
            throw new JspException(ex);
        } catch (ParserConfigurationException ex) {
            throw new JspException(ex);
        } catch (IOException ex) {
            throw new JspException(ex);
        } catch (TransformerException ex) {
            throw new JspException(ex);
        }
    }

    // Releases any resources we may have (or inherit)
    @Override
    public void release() {
        init();
    }

    // *********************************************************************
    // Public methods for subtags

    /** Sets (adds) a transformation parameter on our transformer. */
    public void addParameter(final String name, final Object value) {
        t.setParameter(name, value);
    }

    // *********************************************************************
    // Utility methods

    /**
     * Wraps systemId with a "jstl:" prefix to prevent the parser from thinking
     * that the URI is truly relative and resolving it against the current
     * directory in the filesystem.
     */
    private static String wrapSystemId(final String systemId) {
        if (systemId == null) {
            return "jstl:";
        } else if (ImportSupport.isAbsoluteUrl(systemId)) {
            return systemId;
        } else {
            return ("jstl:" + systemId);
        }
    }

    /**
     * Retrieves a Source from the given Object, whether it be a String, Reader,
     * Node, or other supported types (even a Source already). If 'url' is true,
     * then we must be passed a String and will interpret it as a URL. A null
     * input always results in a null output.
     */
    private Source getSource(final Object o, final String systemId) throws SAXException, ParserConfigurationException,
            IOException {
        if (o == null) {
            return null;
        } else if (o instanceof Source) {
            return (Source) o;
        } else if (o instanceof String) {
            // if we've got a string, chain to Reader below
            return getSource(new StringReader((String) o), systemId);
        } else if (o instanceof Reader) {
            // explicitly go through SAX to maintain control
            // over how relative external entities resolve
            XMLReader xr = XMLReaderFactory.createXMLReader();
            xr.setEntityResolver(new ParseSupport.JstlEntityResolver(pageContext));
            InputSource s = new InputSource((Reader) o);
            s.setSystemId(wrapSystemId(systemId));
            Source result = new SAXSource(xr, s);
            result.setSystemId(wrapSystemId(systemId));
            return result;
        } else if (o instanceof Node) {
            return new DOMSource((Node) o);
        } else if (o instanceof List) {
            // support 1-item List because our XPath processor outputs them
            List l = (List) o;
            if (l.size() == 1) {
                return getSource(l.get(0), systemId); // unwrap List
            } else {
                throw new IllegalArgumentException(Resources.getMessage("TRANSFORM_SOURCE_INVALID_LIST"));
            }
        } else {
            throw new IllegalArgumentException(Resources.getMessage("TRANSFORM_SOURCE_UNRECOGNIZED") + o.getClass());
        }
    }

    // *********************************************************************
    // Tag attributes

    public void setVar(final String var) {
        this.var = var;
    }

    public void setScope(final String scope) {
        this.scope = Util.getScope(scope);
    }

    // *********************************************************************
    // Private utility classes

    /**
     * A Writer based on a wrapped Writer but ignoring requests to close() and
     * flush() it. (Someone must have wrapped the toilet in my office
     * similarly...)
     */
    private static class SafeWriter extends Writer {
        private Writer w;

        public SafeWriter(final Writer w) {
            this.w = w;
        }

        @Override
        public void close() {
        }

        @Override
        public void flush() {
        }

        @Override
        public void write(final char[] cbuf, final int off, final int len) throws IOException {
            w.write(cbuf, off, len);
        }
    }

    // *********************************************************************
    // JSTL-specific URIResolver class

    /** Lets us resolve relative external entities. */
    private static class JstlUriResolver implements URIResolver {
        private final PageContext ctx;

        public JstlUriResolver(final PageContext ctx) {
            this.ctx = ctx;
        }

        public Source resolve(final String href, String base) throws TransformerException {

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

            // remove "jstl" marker from 'base'
            // NOTE: how 'base' is determined varies among different Xalan
            // xsltc implementations
            int index;
            if (base != null && (index = base.indexOf("jstl:")) != -1) {
                base = base.substring(index + 5);
            }

            // we're only concerned with relative URLs
            if (ImportSupport.isAbsoluteUrl(href) || (base != null && ImportSupport.isAbsoluteUrl(base))) {
                return null;
            }

            // base is relative; remove everything after trailing '/'
            if (base == null || base.lastIndexOf("/") == -1) {
                base = "";
            } else {
                base = base.substring(0, base.lastIndexOf("/") + 1);
            }

            // concatenate to produce the real URL we're interested in
            String target = base + href;

            // for relative URLs, load and wrap the resource.
            // don't bother checking for 'null' since we specifically want
            // the parser to fail if the resource doesn't exist
            InputStream s;
            if (target.startsWith("/")) {
                s = ctx.getServletContext().getResourceAsStream(target);
                if (s == null) {
                    throw new TransformerException(Resources.getMessage("UNABLE_TO_RESOLVE_ENTITY", href));
                }
            } else {
                String pagePath = ((HttpServletRequest) ctx.getRequest()).getServletPath();
                String basePath = pagePath.substring(0, pagePath.lastIndexOf("/"));
                s = ctx.getServletContext().getResourceAsStream(basePath + "/" + target);
                if (s == null) {
                    throw new TransformerException(Resources.getMessage("UNABLE_TO_RESOLVE_ENTITY", href));
                }
            }
            return new StreamSource(s);
        }
    }

}
