/*
 * Copyright (c) 2022 Contributors to the Eclipse Foundation
 * Copyright (c) 1997-2020 Oracle and/or its affiliates. All rights reserved.
 * Copyright 2004 The Apache Software Foundation
 *
 * Licensed 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.taglibs.standard.tag.common.xml;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.jsp.JspException;
import jakarta.servlet.jsp.JspTagException;
import jakarta.servlet.jsp.PageContext;
import jakarta.servlet.jsp.tagext.BodyTagSupport;

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.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
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


    //*********************************************************************
    // 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

    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
        if (db == null) {
            db = DocumentBuilderProvider.createSecureDocumentBuilder();
        }
	// set up the TransformerFactory if necessary
        if (tf == null) {
            tf = TransformerFactory.newInstance();
            try {
                tf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
            } catch (TransformerConfigurationException e) {
                throw new AssertionError(
                        "TransformerFactory does not support secure processing");
            }
        }

	//************************************
	// 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);

	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'
    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)				// still equal
	    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)
    public void release() {
	init();
    }


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

    /** Sets (adds) a transformation parameter on our transformer. */
    public void addParameter(String name, 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(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(Object o, 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(String var) {
	this.var = var;
    }

    public void setScope(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(Writer w) { this.w = w; }
	public void close() { }
	public void flush() { }
	public void write(char[] cbuf, int off, 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(PageContext ctx) {
            this.ctx = ctx;
        }
        public Source resolve(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);
        }
    }

}
