package com.orbeon.struts;

/**
 * This is the meat of the Model 2X. This servlet loads
 * the XSLT styesheet after initialization. On each request,
 * the form beans are serialized in XML, along with other
 * information (see below). Finally, the XSLT processor is
 * invoked.
 *
 * If the XML_DEBUG_PARAM parameter is found on the Request,
 * the raw XML is sent to the client.
 * This is useful for debugging.
 */


import org.apache.struts.action.ActionForm;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Templates;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URLConnection;
import java.util.Collection;
import java.util.Date;
import java.util.Enumeration;
import java.util.Iterator;

public class XSLServlet extends HttpServlet {

    private static final String XSL_FILE_PARAM = "xsl-file";
    private static final String XML_DEBUG_PARAM = "xml";

    private DocumentBuilder documentBuilder;
    private TransformerFactory tFactory;

    private Templates xsl;
    private Date xslLastRead = new Date();
    private ServletConfig servletConfig;
    private String xslFile;

    public void init(ServletConfig config)
            throws ServletException {

        String xslFile = config.getInitParameter(XSL_FILE_PARAM);
        if (xslFile == null || xslFile.equals(""))
            throw new ServletException(XSL_FILE_PARAM + " parameter must be present !");

        try {
            documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
        } catch (ParserConfigurationException e) {
            e.printStackTrace();
            throw new ServletException(e.toString());
        }

        tFactory = TransformerFactory.newInstance();

        this.servletConfig = config;
        this.xslFile = xslFile;

    }

    /**
     * This method reads and parses the XSL stylesheet. For performance
     * reasons, the file is read only if it changes on disk since
     * the last read.
     */
    private void readXSL(ServletConfig config, String xslFile) throws ServletException {
        try {
            URLConnection urlConn = config.getServletContext().getResource(this.xslFile).openConnection();
            urlConn.connect();
            Date d = new Date(urlConn.getLastModified());
            if (this.xslLastRead.after(d)) {
                System.out.println("XSL File has changed: reparsing...");
                StreamSource ss = new StreamSource(urlConn.getInputStream());
                this.xsl = tFactory.newTemplates(ss);
                this.xslLastRead = new Date();
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new ServletException(e.toString());
        }

    }

    /**
     * Main method. We introspect the beans, serialize them and call the XSLT processor
     */
    public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException {

        try {
            long timer = 0;

            readXSL(servletConfig, xslFile);
            Document doc = documentBuilder.newDocument();
            Element page = doc.createElement("page");

            ActionForm form = (ActionForm) request.getAttribute("currentForm");

            String beanName = Introspector.getBeanInfo(form.getClass()).getBeanDescriptor().getName();
            page.setAttribute("name", beanName);
            doc.appendChild(page);

            Element xmlRequest = doc.createElement("request");

            // serialize the request. Note that the form beans are is the request.
            for (Enumeration e = request.getAttributeNames(); e.hasMoreElements();) {
                String name = (String) e.nextElement();
                Object value = request.getAttribute(name);
                System.out.println("request Attr: " + name + " = " + value);
                timer = System.currentTimeMillis();
                Element attr = serializeBean(value, doc);
                System.out.println("XML Serialization time for bean: " + name +
                        " is: " + (System.currentTimeMillis() - timer) + "ms");
                xmlRequest.appendChild(attr);

            }
            page.appendChild(xmlRequest);

            // dump session attributes
            Element xmlSession = doc.createElement("session");
            for (Enumeration e = request.getSession().getAttributeNames(); e.hasMoreElements();) {
                String name = (String) e.nextElement();
                Object value = request.getSession().getAttribute(name);
                System.out.println("session Attr: " + name + " = " + value);
                timer = System.currentTimeMillis();
                Element attr = serializeBean(value, doc);
                System.out.println("XML Serialization time for bean: " + name +
                        " is: " + (System.currentTimeMillis() - timer) + "ms");
                xmlSession.appendChild(attr);
            }
            page.appendChild(xmlSession);

            DOMSource source = new DOMSource(doc);


            Transformer xslt = xsl.newTransformer();
            if (request.getParameter(XML_DEBUG_PARAM) == null) {
                // call the xslt transformer and time it
                timer = System.currentTimeMillis();
                xslt.transform(source, new StreamResult(response.getOutputStream()));
                System.out.println("XSL Transforming time is: " + (System.currentTimeMillis() - timer) + "ms");
            } else {
                // call the identity transformer to serialize the DOM into text XML
                Transformer identity = tFactory.newTransformer();
                identity.transform(source, new StreamResult(response.getOutputStream()));
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new ServletException(e.toString());
        }
    }


    /**
     * Main method to serialize a bean and its content.
     *
     * @param bean a JavaBean to serialize
     * @param document an XML document is needed to create elements
     *
     * @return the resulting DOM tree
     */
    private Element serializeBean(Object bean, Document document) {
        Element element;
        try {
            Element root = inspectBean(bean, document);
            return root;

        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e.toString());
        }
    }

    /**
     * This method looks into a bean for the getters and invoke
     * them to get the property's value.
     *
     * @param bean a JavaBean to introspect
     * @param document an XML document is needed to create elements
     *
     * @return the resulting DOM tree
     */
    private Element inspectBean(Object bean, Document document)
            throws IntrospectionException, IllegalAccessException, InvocationTargetException {

        BeanInfo info = Introspector.getBeanInfo(bean.getClass());
        PropertyDescriptor[] properties = info.getPropertyDescriptors();
        Element root = document.createElement(info.getBeanDescriptor().getName());

        for (int i = 0; i < properties.length; i++) {
            Class type = properties[i].getPropertyType();
            String name = properties[i].getName();
            Method m = properties[i].getReadMethod();
            if (m != null) {
                Object value = m.invoke(bean, null);
                if (value == null)
                    value = new String("");
                root = serializeProperty(name, value, root, document);
            }
        }
        return root;
    }

    /**
     *  This method serializes a JavaBean property. If the property is:
     *  - an instance of Collection, we iterate through it to serialize
     *    all items in the collection. The resulting DOM tree looks like:
     *    <code>   <list><item>One</item><item>Two</item></list>   </code>
     *
     *  - an instance java.lang.*, we simply invoke toString()
     *
     *  - null, NOP
     *
     *  - an instance of another class, we inspect that bean.
     *
     * @param name the name of the property
     * @param value the value of the property
     * @param baseElement where to append to resulting DOM tree
     * @param document an XML document is needed to create elements
     *
     * @return the resulting DOM tree
     *
     */
    private Element serializeProperty(String name, Object value, Element baseElement, Document document) {
        try {
            Class c = value.getClass();
            Package p = c.getPackage();
            if (value instanceof Collection) {
                Collection col = (Collection) value;
                Element list = document.createElement(name);
                for (Iterator i = col.iterator(); i.hasNext();)
                    list = serializeProperty("item", i.next(), list, document);
                baseElement.appendChild(list);
            } else if (p == null) {

            } else if (p != null && p.getName().equals("java.lang")) {
                Element e = document.createElement(name);
                e.appendChild(document.createTextNode(value.toString()));
                baseElement.appendChild(e);
            } else {
                Element e = inspectBean(value, document);
                baseElement.appendChild(e);
            }

        } catch (Exception exc) {
            exc.printStackTrace();
        }
        return baseElement;
    }


}

