import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.io.Reader;
import java.net.URI;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Properties;
import java.util.zip.GZIPOutputStream;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.stream.StreamResult;
import net.sf.saxon.Configuration;
import net.sf.saxon.Controller;
import net.sf.saxon.TransformerFactoryImpl;
import net.sf.saxon.lib.FeatureKeys;
import net.sf.saxon.lib.StandardEntityResolver;
import net.sf.saxon.lib.UnparsedTextURIResolver;
import net.sf.saxon.lib.Validation;
import net.sf.saxon.trace.XSLTTraceListener;
import net.sf.saxon.trans.XPathException;
import net.sf.saxon.value.StringValue;
import org.w3c.app.util.BlacklistChecker;
import org.w3c.app.xsl.BaseAuthURIResolver;
import org.w3c.app.xsl.ServletAuthReader;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;

// you can grab the following org.w3c files individually under
//     http://dev.w3.org/cvsweb/java/classes/org/w3c/

/**
 * SaxonSavvyServlet. Transforms a supplied input document using a
 * supplied stylesheet
 * Try it online and see options available
 * http://www.w3.org/2005/08/online_xslt/
 * <p/>
 * Author: Ted Guild <ted@w3.org>
 * (c) COPYRIGHT W3C http://www.w3.org/Consortium/Legal/copyright-software
 * $Id: SaxonSavvyServlet.java,v 1.47 2013/11/16 21:17:46 ylafon Exp $
 */

public class SaxonSavvyServlet extends HttpServlet implements EntityResolver {
    private static final boolean debug = false;

    protected static final HashMap<String, String> saxParsers;
    private static final long serialVersionUID = -5980347723984067588L;
    private static String servletPath = null;

    static {
        saxParsers = new HashMap<String, String>();

        System.setProperty("javax.xml.transform.TransformerFactory",
                "net.sf.saxon.TransformerFactoryImpl");
        //otherwise it defaults to one in jvm
        saxParsers.put("default", "org.apache.xerces.parsers.SAXParser");
        saxParsers.put("xerces", "org.apache.xerces.parsers.SAXParser");
        saxParsers.put("javax", "javax.xml.parsers.SAXParser");
        saxParsers.put("aelfred", "net.sf.saxon.aelfred.SAXDriver");
        saxParsers.put("xp", "com.jclark.xml.sax.Driver");
        //add others as installed and sanction instead of allowing user
        //specified arbitrary classes in classpath
    }

    public void init() throws ServletException {
        // load once so blacklist, whitelist and two-level-tld stays in
        // memory and not read from filesystem for each request
        BlacklistChecker blc  = new BlacklistChecker();
        super.init();
    }


    // hack to cache dtds
    public InputSource resolveEntity(String publicId, String systemId) {
        InputSource is = null;
        // first, use Saxon's cache.
        try {
            is = StandardEntityResolver.getInstance().resolveEntity(publicId, systemId);
        } catch (Exception ex) {
            is = null;
            ex.printStackTrace(System.err);
        }
        // if nothing is found, check the BL
        if (is == null && systemId != null) {
            String bl = BlacklistChecker.checkURIs(systemId);
            if (bl != null) {
                throw new RuntimeException("Resolve Entity error: " + bl);
            }
        }
        return is;
    }

    /**
     * doGet() - accept request and produce response<BR>
     * URL parameters: <UL>
     * <li>xmlfile - URL of source document</li>
     * <li>xslfile - URL of stylesheet</li>
     * </UL>
     *
     * @param req The HTTP request
     * @param res The HTTP response
     */

    public void doGet(HttpServletRequest req, HttpServletResponse res)
            throws ServletException, IOException {
        if (servletPath == null) {
            String servPath = req.getServletPath();
            servletPath = req.getRealPath(servPath.substring(0,
                    servPath.lastIndexOf('/')));
        }
        String source = req.getParameter("xmlfile");
        String style = req.getParameter("xslfile");
        apply(style, source, req, res);
    }

    /**
     * getServletInfo<BR>
     * Required by Servlet interface
     */

    public String getServletInfo() {
        return "Calls SAXON to apply a stylesheet to a source document";
    }

    private boolean doGzip(HttpServletRequest req) {
        Enumeration<String> ae = req.getHeaders("Accept-Encoding");
        if (ae != null) {
            while (ae.hasMoreElements()) {
                for (String s : ae.nextElement().split(",")) {
                    if (s.trim().equalsIgnoreCase("gzip")) {
                        return true;
                    }
                }
            }
        } else {
            // some containers might forbid getHeaders, so move to getHeader
            String aev = req.getHeader("Accept-Encoding");
            if (aev != null) {
                for (String s : aev.split(",")) {
                    if (s.trim().equalsIgnoreCase("gzip")) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    /**
     * Apply stylesheet to source document
     */

    private void apply(String style, String source,
                       HttpServletRequest req, HttpServletResponse res)
            throws java.io.IOException {

        StringBuilder debuff = new StringBuilder();
        PrintStream out;
        if ((req.getParameter("gzout") != null)/* || doGzip(req) */) {
            out = new PrintStream(new GZIPOutputStream(res.getOutputStream()));
            res.setHeader("Content-Encoding", "gzip");
        } else {
            out = new PrintStream(res.getOutputStream());
        }
        boolean debug = false;
        boolean trace = false;
        boolean validate = false;
        boolean troff = false;
        String saxParser;
        ServletAuthReader sar = new ServletAuthReader(req, res, debuff);

        if (req.getParameter("saxparser") != null &&
                saxParsers.get(req.getParameter("saxparser")) != null) {
            saxParser = saxParsers.get(req.getParameter("saxparser"));
        } else {
            saxParser = saxParsers.get("default");
        }
        if (req.getParameter("debug") != null) {
            debug = true;
            //validate=true;
            //being uber careful with out
            //System.setErr(new PrintStream(out));
            res.setContentType("text/plain");
        }
        if (req.getParameter("trace") != null) {
            trace = true;
        }
        if (req.getParameter("troff") != null) {
            troff = true;
        }
        if (req.getParameter("validate") != null) {
            validate = !req.getParameter("validate").equals("0") && !req.getParameter("validate").equals("false");
        }
        if (style == null) {
            out.println("No xslfile parameter supplied");
            return;
        }
        if (source == null) {
            out.println("No xmlfile parameter supplied");
            return;
        }

        try {
            InputSource isXSL = new InputSource(style);
            InputSource isXML = new InputSource(source);
            isXSL.setCharacterStream(sar.initAuthInputStreamReader(style));
            isXML.setCharacterStream(sar.initAuthInputStreamReaderWithStyle(source, style));
            XMLReader xslReader = XMLReaderFactory.createXMLReader(saxParser);
            XMLReader xmlReader = XMLReaderFactory.createXMLReader(saxParser);
            xslReader.setEntityResolver(this);
            xmlReader.setEntityResolver(this);
            SAXSource xslSource = new SAXSource(xslReader, isXSL);
            SAXSource xmlSource = new SAXSource(xmlReader, isXML);
            //TransformerFactoryImpl factory = new TransformerFactoryImpl();
            TransformerFactory factory = TransformerFactory.newInstance();
            //set our choice of parser

            Configuration factoryConfig;

            factoryConfig = ((TransformerFactoryImpl) factory).getConfiguration();
            factory.setAttribute(FeatureKeys.SOURCE_PARSER_CLASS, saxParser);
            factory.setAttribute(FeatureKeys.STYLE_PARSER_CLASS, saxParser);

            //parse dtds and give warnings if asked
            factoryConfig.setValidation(validate);
            factoryConfig.setValidationWarnings(validate);
            if (!validate) {
                factoryConfig.setSchemaValidationMode(Validation.STRIP);
            }

            BaseAuthURIResolver BA = new BaseAuthURIResolver(factoryConfig, sar, (req.getParameter("recurse_auth") != null));
            factoryConfig.setURIResolver(BA);

            if (!debug) {
                factory.setAttribute(FeatureKeys.VERSION_WARNING, false);
            }

            StreamResult destination;
            if (trace) {
                XSLTTraceListener traceListener = new XSLTTraceListener();
                traceListener.setOutputDestination(out);
                factory.setAttribute(FeatureKeys.TRACE_LISTENER, traceListener);
                if (!debug) {
                    res.setContentType("application/xml");
                }

            }
            //we want trace not transform
            if (troff) {
                destination = new StreamResult(new File("/dev/null"));
            } else {
                destination = new StreamResult(out);
            }
            //disable extensions
            factory.setAttribute(FeatureKeys.ALLOW_EXTERNAL_FUNCTIONS, Boolean.FALSE);
            //factory.setAttribute(FeatureKeys.FEATURE_SECURE_PROCESSING,
            //                   new Boolean(true));
            Transformer transformer = factory.newTransformer(xslSource);
            Properties outputProperties = transformer.getOutputProperties();
            String templatesType = outputProperties.getProperty("media-type");
            String templatesCharset = outputProperties.getProperty("encoding");
            String mimeType = "text/html";
            if (req.getParameter("content-type") != null) {
                mimeType = req.getParameter("content-type");
            } else if (templatesType != null) {
                if (!templatesType.contains("charset") && templatesCharset != null) {
                    templatesType += ";charset=" + templatesCharset;
                }
                mimeType = templatesType;
            }
            //mime type can't be set if output already sent, eg from debug
            //in auth function
            if (debug) {
                if (req.getParameter("saxparser") != null) {
                    debuff.append("parsers (xml and xsl) set to: " +
                            factoryConfig.getSourceParserClass() + " and " +
                            factoryConfig.getStyleParserClass() + "\n");
                }
                debuff.append("-- request headers --\n");
                Enumeration<String> p = req.getHeaderNames();
                while (p.hasMoreElements()) {
                    String header = p.nextElement();
                    debuff.append(header + ": " + req.getHeader(header) + "\n");
                }
                //turn off for now
                //outputProperties.list(out);
                debuff.append("-- parameters --\n");
            } else {
                res.setContentType(mimeType);
            }
            Enumeration<String> p = req.getParameterNames();
            while (p.hasMoreElements()) {
                String name = p.nextElement();
                if (name.length() > 0 &&
                        !(name.equals("style") || name.equals("source"))) {
                    String value = req.getParameter(name);
                    if (debug) {
                        debuff.append(name + " :: " + value + "\n");
                    }
                    //transformer.setParameter(name, new StringValue(value));
                    transformer.setParameter(name,
                            new StringValue(new String(value.getBytes(),
                                    "UTF-8")));
                }
            }

            // define the resolver
            Controller controller = (Controller) transformer;
            controller.setUnparsedTextURIResolver(new UnparsedTextURIResolver() {
                private static final long serialVersionUID = -509487018996689672L;
                private ServletAuthReader authReader;

                // we filter out file:// URIs.    for unparsed-text
                public Reader resolve(URI absoluteURI, String encoding, Configuration config)
                        throws XPathException {
                    try {
                        return authReader.initAuthInputStreamReader(absoluteURI.toString(), encoding);
                    } catch (IOException e) {
                        throw new XPathException("URL Syntax error");
                    }
                }

                UnparsedTextURIResolver initialize(ServletAuthReader origsar) {
                    this.authReader = origsar;
                    return this;
                }
            }.initialize(sar));

            transformer.transform(xmlSource, destination);
            if (debug) {
                out.println("\ndebug info:");
                out.println("validation set to " + validate +
                        " Configuration.isValidation() " +
                        factoryConfig.isValidation());
                out.println(debuff);
            }
        } catch (Exception err) {
            res.setContentType("text/plain");
            out.println(debuff);
            out.println("Using " + saxParser);
            out.println("Exception " + err + " \n" + err.getMessage());
            if (debug) {
                err.printStackTrace(out);
            }
        } finally {
            out.flush();
            out.close();
        }
    }
}

