// $Id: PDFProxyServlet.java 10482 2009-07-10 09:53:38Z chris $
//
package org.faceless.report;

import java.util.*;
import java.io.*;
import java.net.*;
import javax.servlet.*;
import javax.servlet.http.*;
import org.faceless.pdf2.*;
import org.faceless.util.*;
import org.faceless.report.*;
import org.xml.sax.*;

/**
 * <p>
 * The PDFProxyServlet is a basic servlet which accepts a request,
 * reads an XML report from another URL and turns it into a PDF to be
 * returned to the client. Those using the Servlet 2.3 architecture may
 * want to consider using the {@link PDFFilter} class instead.
 * </p>
 * <p>
 * Most overriders should be able to just override the <tt>getProxyURL()</tt>
 * method, to alter where the report XML is kept.
 * </p>
 * <p>
 * Meta tags that aren't already known to the Report Generator and that
 * begin with "<tt>HTTP-</tt>" are added to the response header (minus the
 * "HTTP-" prefix). An example would be to place
 * <tt>&lt;meta name="HTTP-Expires" value="Mon, 01 Jan 1999 12:00:00 GMT"&gt;</tt>
 * in the head of the XML, which would set the "Expires" header in the HTTP
 * response.
 * </p>
 * <p>
 * The following custom Meta-Tags may also be used to control the behaviour of
 * the servlet.
 * <ul>
 * <li><tt>Servlet-Cache</tt> - set the length of time to cache the generated
 * PDF. The cache is actually implemented in the ProxyServlet, and is completely
 * independent of any external caches between the server and the client. Examples
 * of valid values are "1h30m", to cache the document for 1.5 hours, "45s" to cache
 * for 45 seconds or "1d" to cache for 1 day. Any combination of days, hours, minutes
 * and seconds is valid. There is no way to flush this document from the client browser.</li>
 * <li><tt>Servlet-FileName</tt> - ask the client browser to save the file as the
 * specified filename instead of displaying it inline. This uses the <tt>Content-Disposition</tt>
 * header, which <i>in theory</i> is accepted by NS4+ and IE5+. On our test system
 * at least however, this didn't work, and in IE5.5 actually prevented us from viewing
 * the document at all! Use with caution.</li>
 * </ul>
 * Some initialization parameters can be set in the <tt>web.xml</tt> file to further
 * control various internal aspects of the servlet:
 * <ul>
 * <li><tt>org.xml.sax.driver</tt> - may be set to the base class of your SAX parsers
 * <tt>XMLReader</tt> implementation, if the generator is unable to locate it.</li>
 * <li><tt>org.faceless.util.proxytimeout</tt> - may be set to the number of 
 * milliseconds to wait for a response from the proxy request. Defaults to 15000,
 * or 15 seconds.</li>
 * </ul>
 * </p>
 * <p>
 * This class also implements <tt>org.xml.sax.ErrorHandler</tt>, to deal with
 * any errors found during the XML parsing process. Currently all warnings and
 * errors are fatal, and logged to <tt>System.err</tt>. 
 * </p>
 */
public abstract class PDFProxyServlet extends HttpProxyServlet implements ErrorHandler
{
    public void init() throws ServletException {
        super.init();
	String license = (String)getServletConfig().getInitParameter("license");
	if (license!=null) {
	    org.faceless.report.ReportParser.setLicenseKey(license);
	}
    }

    /**
     * Implements the <tt>getRequest</tt> method of the generic <tt>HttpProxyServlet</tt>.
     * Subclasses should be able to leave this method untouched.
     */
    public HttpRequestWriter getRequest(HttpRequestReader request, HttpServletResponse response)
        throws ServletException, IOException
    {
        HttpRequestWriter writer = null;
	String url = getProxyURL(request.getUnderlyingRequest(), response);
	if (url!=null) {
	    writer = new HttpRequestWriter(request);
	    writer.setURL(url);
	    writer.clearHeader("Accept-Encoding");
	}
	return writer;
    }

    /**
     * Implements the <tt>getResponse</tt> method of the generic <tt>HttpProxyServlet</tt>.
     * Subclasses should be able to leave this method untouched.
     */
    public HttpResponseWriter getResponse(HttpResponseReader in)
        throws ServletException, IOException
    {
	HttpResponseWriter out = null;

	// At this point we have the response from the proxy, after any 300
	// level responses (redirections) have been processed. Check it's
	// a valid response, if it is send the PDF, if not send the error
	//
	if (in.getStatus()>=200 && in.getStatus()<=299 && in.getStatus()!=204) {
	    out = new HttpResponseWriter();

	    // I don't think I've ever seen a 2xx response other than 200 and
	    // 204, so I'm not sure if this behavious is correct for the others
	    // (or even if it matters).
	    //
	    out.setStatus(in.getStatus(), in.getStatusMessage());
	    out.setHeader("Content-Type", "application/pdf");

	    // Create the input source. Be sure to set the System ID, or
	    // every relative URL in the document will be wrong
	    //
	    InputSource xmlin = new InputSource();
	    xmlin.setByteStream(in.getInputStream());
	    xmlin.setSystemId(in.getURL());

	    // Here we go. Watch closely!
	    //
	    PDF pdf=null;
	    try {
		// Create the parser, using the "org.xml.sax.driver"
		// initialization parameter if specified, or null (meaning
		// "find it yourself") if not.
		//
		ReportParser parser = ReportParser.getInstance((String)getServletConfig().getInitParameter("org.xml.sax.driver"));
		initParser(parser);

		// Set the unknown meta tag callback to our helper class.
		//
		parser.setMetaHandler(new MetaCallback(in, out, this));

		// Go go go!
		//
		pdf = parser.parse(xmlin);
	    } catch (SAXException e) {
		if (e.getException()!=null) {
		    throw new ServletException(e.getException());
		} else {
		    throw new ServletException(e);
		}
	    }

            modifyPDF(pdf);

	    // That's 95% of the job done. Now we need to render to the
	    // OutputStream.
	    //
	    pdf.render(out.getOutputStream());
	} else {
	    // Something went wrong. Pass the error back to the original
	    // requester. This may include "401 Authorization Required"
	    // responses (haven't tested this yet), or the more common
	    // "500 Internal Error", which we test hundreds of times a day.
	    //
	    out = new HttpResponseWriter(in);
	}
	return out;
    }

    // A class to pass the unknown Meta tags back to a context where
    // we have a HttpServletResponse to use them
    //
    private class MetaCallback implements MetaHandler {
	private HttpResponseReader reader;
	private HttpResponseWriter writer;
	private PDFProxyServlet prox;

        public MetaCallback(HttpResponseReader reader, HttpResponseWriter writer, PDFProxyServlet prox) {
	    this.reader=reader;
	    this.writer=writer;
	    this.prox=prox;
	}

	// Whenever this is called, pass the request back to the servlet
	// to handle it (so the method can be overridden).
	//
	public void handleMetaTag(String key, String val) throws SAXException {
	    try {
		prox.metaTag(reader, writer, key, val);
	    } catch (Exception e) {
	        throw new SAXException(e);
	    }
	}
    }


    //-------------------------------------------------------------------------
    //
    // If there are any methods to override, they're after here
    //
    //-------------------------------------------------------------------------

    /**
     * Handle any meta tags that aren't recognised by the core Report Generator.
     * This method recognises tags begining with <tt>HTTP-</tt>, as well as
     * <tt>Servlet-FileName</tt> and <tt>Servlet-Cache</tt>.
     * @param request the Servlet request
     * @param response the Servlet request
     * @param name the "name" attribute from the meta tag
     * @param value the "value" attribute from the meta tag
     */
    public void metaTag(HttpResponseReader reader, HttpResponseWriter writer, String name, String value)
        throws ServletException, IOException
    {
        if (name.toUpperCase().startsWith("HTTP-")) {
	    writer.setHeader(name.substring(5), value);
	} else if (name.equalsIgnoreCase("Servlet-FileName")) {
	    writer.setHeader("Content-Disposition", "attachment; filename=\""+value+"\"");
	} else if (name.equalsIgnoreCase("Servlet-Cache")) {
	    int len=0,s=0,e=0;
	    int[] mul = { 86400, 3600, 60, 1 };
	    if (Character.isDigit(value.charAt(value.length()-1))) value=value+"s";
	    try {
		for (;e<value.length();e++) {
		    int p = "dhms".indexOf(value.charAt(e));
		    if (p>=0) {
			len += Integer.parseInt(value.substring(s,e)) * mul[p];
			s=e+1;
		    }
		}
	    } catch (NumberFormatException ex) {
	        len=0;
	    }
	    writer.setHeader("X-Proxy-Cache-Control", "max-age="+len);
	    writer.setHeader("Cache-Control", "max-age="+len);
	}
    }


    // SAX error handlers from here on

    public void warning(SAXParseException exception) throws SAXException {
	System.err.println("PDF WARNING"+(exception.getLineNumber()>=0 ? " AT "+exception.getSystemId()+" line "+exception.getLineNumber() : "")+": "+exception.getMessage());
        throw exception;
    }

    public void error(SAXParseException exception) throws SAXException {
	System.err.println("PDF ERROR"+(exception.getLineNumber()>=0 ? " AT "+exception.getSystemId()+" line "+exception.getLineNumber() : "")+": "+exception.getMessage());
	throw exception;
    }

    public void fatalError(SAXParseException exception) throws SAXException {
	System.err.println("PDF FATAL ERROR"+(exception.getLineNumber()>=0 ? " AT "+exception.getSystemId()+" line "+exception.getLineNumber() : "")+": "+exception.getMessage());
	throw exception;
    }

    /**
     * Set any initialization options for the parser, if necessary.
     * Overriders may optionally use this method to set parser flags
     * (by default, this method does nothing).
     * @since 1.0.4
     */
    public void initParser(ReportParser parser) {
    }

    /**
     * Adjust the PDF after it's been created but before it's rendered
     * back to the browser. Subclassers who need to alter the PDF in
     * some way should override this method and make any changes they
     * need to make to the PDF there.
     * @since 1.1.27
     */
    public void modifyPDF(PDF pdf) {
    }

    /**
     * <p>
     * Returns the <i>absolute</i> URL to send the proxy request to. This method
     * is the only method that needs to be overriden by concrete subclasses of
     * this class.
     * </p><p>
     * If no valid URL can be constructed due to an incorrect request, this method
     * should return <tt>null</tt> and an appropriate error written to the
     * <tt>response</tt>.
     * </p>
     * @param request the incoming request from the client
     * @param response the outgoing response to the client - should only be written
     * to if an error has occurred and the method is returning null
     * @return the absolute URL to proxy the request to, or <tt>null</tt> if an
     * error has occurred and no request should be made.
     */
    public abstract String getProxyURL(HttpServletRequest request, HttpServletResponse response)
        throws IOException, ServletException;
}
