/* ======================================================================
The Bodington System Software License, Version 1.0

Copyright (c) 2001 The University of Leeds.  All rights reserved.
 
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
 
1.  Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
 
2.  Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
 
3.  The end-user documentation included with the redistribution, if any,
must include the following acknowledgement:  "This product includes
software developed by the University of Leeds
(http://www.bodington.org/)."  Alternately, this acknowledgement may
appear in the software itself, if and wherever such third-party
acknowledgements normally appear.
 
4.  The names "Bodington", "Nathan Bodington", "Bodington System",
"Bodington Open Source Project", and "The University of Leeds" must not be
used to endorse or promote products derived from this software without
prior written permission. For written permission, please contact
d.gardner@leeds.ac.uk.
 
5.  The name "Bodington" may not appear in the name of products derived
from this software without prior written permission of the University of
Leeds.
 
THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
WARRANTIES, INCLUDING, BUT NOT LIMITED TO,  TITLE,  THE IMPLIED WARRANTIES
OF QUALITY  AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO
EVENT SHALL THE UNIVERSITY OF LEEDS OR ITS CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
=========================================================
 
This software was originally created by the University of Leeds and may contain voluntary
contributions from others.  For more information on the Bodington Open Source Project, please
see http://bodington.org/
 
====================================================================== */

package org.bodington.servlet;

import org.apache.log4j.Logger;

import java.io.*;
import java.net.*;
import javax.servlet.*;
import javax.servlet.http.*;

import org.bodington.server.*;
import org.bodington.servlet.template.*;

import org.bodington.server.events.UserFileEvent;
import org.bodington.server.resources.*;
import org.bodington.server.realm.User;

/**
 * Building servlet. This servlet navigates a virtual building
 * @version 0.1
 * @author Jon Maber
 */
public class
BuildingServlet extends HttpServlet
{
    
    private static Logger log = Logger.getLogger(BuildingServlet.class);
    
    public void init(ServletConfig c) throws ServletException
    {
        super.init(c);
        log("Initialising building servlet");

        int bs_status = BuildingServer.getStatus();
        if (bs_status != BuildingServer.STATUS_READY)
            throw new BuildingServerUnavailableException(
                "Can't start servlet - BuildingServer isn't available.");

        if (BuildingContext.startContext() == null)
            throw new BuildingServerUnavailableException(
                "Can't start servlet - BuildingServer isn't ready.");

        ServletContext servlet_context = c.getServletContext();


	Template.setClassDirectory( BuildingContext.getProperty( "buildingservlet.templates.classes" ) );
	Template.setTemplateDirectory( BuildingContext.getProperty( "buildingservlet.templatedirectory" ) );

    // iPlanet screw up - doesn't use sub class of URLClassLoader so we don't get the jar files and
    // class directories from the web application. So provide option to manually add these from the
        // bodington.properties file.
    String additional_class_path = BuildingContext.getProperty( "buildingservlet.templates.additional_classpath" );

        // work out class path for compiling templates....
        URLClassLoader url_class_loader;
        ClassLoader parent_class_loader;
        URL[] urls;
        StringBuffer class_path, full_class_path;
        String sep = System.getProperty("path.separator");
        int i;

        parent_class_loader = Thread.currentThread().getContextClassLoader();
        if (parent_class_loader == null)
            parent_class_loader = this.getClass().getClassLoader();

        full_class_path = new StringBuffer();
        while (parent_class_loader != null)
        {
            if (parent_class_loader instanceof URLClassLoader)
            {
                class_path = new StringBuffer();
                url_class_loader = (URLClassLoader) parent_class_loader;
                urls = url_class_loader.getURLs();

                for (i = 0; i < urls.length; i++)
                {
                    if (urls[i].getProtocol().equals("file"))
                    {
                        class_path.append((String) urls[i].getFile());
                        class_path.append(sep);
                    }
                }
                full_class_path.insert(0, class_path);
            }
            parent_class_loader = parent_class_loader.getParent();
        }

        if (additional_class_path != null)
            full_class_path.append(additional_class_path);

        Template.setClassPath(full_class_path.toString());

        BuildingContext.endContext();
            }


    public void doPost(HttpServletRequest req, HttpServletResponse res)
        throws ServletException, IOException
    {
        doGet(req, res);
    }

    public void doGet(HttpServletRequest req, HttpServletResponse res)
        throws ServletException, IOException
    {
        if (BuildingServer.getStatus() != BuildingServer.STATUS_READY)
            throw new BuildingServerUnavailableException(
                "Can't provide page - BuildingServer isn't available.");
        
        // This is a hack so that if someone doesn't append a "/" after
        // the servlet (ie /site instead of /site/) we redirect them
        if (req.getPathInfo() == null || !req.getPathInfo().startsWith("/"))
        {
            res.sendRedirect(req.getRequestURI() + "/");
        	log.debug("Redirected broken incomming request(noslash)");
        	return;
        }

            doProcessing((Request)req, (Response)res);
    }

    public void doProcessing(Request breq, Response res)
        throws ServletException, IOException
    {
        User user = (User) BuildingContext.getContext().getUser();
        if (user == null)
        {
	    res.sendError( Response.SC_NOT_FOUND, "Unrecognised user of this building." );
            BuildingContext.trace("ending,unknown user");
            BuildingContext.dumpTrace();
            return;
        }

        // We always send out the cookie no matter what.
        HttpSession session = (HttpSession)breq.getSession( true );
        Cookie session_cookie = new Cookie( Request.SESSION_ID_COOKIE_NAME, session.getId() );
        String cookie_path = breq.getContextPath();
        // Mozilla and derived browsers don't like empty cookie path and will assume
        // wrongly that cookie path matches request path.  So, fix path if the Bodington
        // web app is at the root of the server
        if ( cookie_path.length() == 0 )
            cookie_path = "/";
        session_cookie.setPath( cookie_path );
        res.addCookie( session_cookie );
        
        int l, i;
        String html_out = null;
        Template tplate = null;

        try
        {
            switch (breq.getRequestType())
            {
                case Request.REQUEST_TYPE_NOT_FOUND:
                    res.disableCaching();
                	res.sendError( Response.SC_NOT_FOUND, "The requested web address doesn't exist." );
                    break;
                case Request.REQUEST_TYPE_LOGIN_TEMPLATE:
                // drop through and proccess like other templates
                case Request.REQUEST_TYPE_TEMPLATE:
                    tplate = breq.getTemplate();
                    if (tplate == null)
                    {
                        res.sendError( Response.SC_NOT_FOUND, "The request page doesn't exist within the specified resource." );
                        BuildingContext.trace("ending,no template");
                        BuildingContext.dumpTrace();
                        return;
                    }
                    if (tplate.isRedirected())
                    {
                        log.debug( "Template to redirect to" );
                        redirectToTemplate(breq, res, tplate);
                    }
                    else
                    {
                        log.debug( "Template to output" );
                        XmlTemplateProcessor xml_template_processor = tplate.getXmlTemplateProcessor();
                        if (xml_template_processor == null)
                        {
                            res.sendError( Response.SC_NOT_FOUND, "The request page can't be processed." );
                            return;
                        }
                        else
                            xml_template_processor.process(breq, res);
                    }
                    break;

                 case Request.REQUEST_TYPE_SPRING:
                     RequestDispatcher dispatcher = getServletContext().getNamedDispatcher("spring");
                     dispatcher.forward(breq, res);
                     break;
                case Request.REQUEST_TYPE_GENERATED:
                    res.disableCaching();
                    if (!breq.getFacility().generateFile(breq, res))
                    {
                        res.sendError( Response.SC_NOT_FOUND, "The requested generated page doesn't exist within the specified resource." );
                        BuildingContext.trace("ending,no template");
                        BuildingContext.dumpTrace();
                        return;
                    }
                //drop through to send the generated file....

                case Request.REQUEST_TYPE_DOWNLOAD:
                    // caching isn't disabled for uploaded files
                    outputUploadedFile(breq, res);
                    break;

                case Request.REQUEST_TYPE_VIRTUAL:
                    //res.disableCaching();
                    // It's up to the individual methods that output virtual
                    // files to decide whether to disable caching
                    breq.getFacility().sendVirtualFile(breq, res);
                    break;
                case Request.REQUEST_TYPE_NOT_ALLOWED:
			// during the checking we found that the user shouldn't be able to access this.
                    res.disableCaching();
                    res.sendError(HttpServletResponse.SC_FORBIDDEN, "Bodington Page");
                    break;
                default:
                    res.disableCaching();
		    res.sendError( Response.SC_INTERNAL_SERVER_ERROR, "There was an unexpected technical problem interpreting your page request." );
                    BuildingContext.trace("ending,request misunderstood");
                    BuildingContext.dumpTrace();
                    return;
            }
        }
        catch (IOException ex)
        {
            log.error( ex.getMessage(), ex );
            //rethrow it
            throw ex;
        }

        BuildingContext.trace("ending,normal");
    }

    private void outputUploadedFile(Request req, HttpServletResponse res)
        throws ServletException, IOException
    {
        String webapp_relative = null;
	if ( !forwardOK( req, res ) )
	    return;

        //   non-JWS version needs this object
        RequestDispatcher dispatcher = null;
        InputStream in = null;
        boolean use_dispatcher;
        OutputStream out = null;
        
     

        try
        {
            if (req.getForwardedFileURL() != null)
            {
                log.debug( "Forwarding to [" + req.getForwardedFileURL() + "]");
                String contextPath = req.getContextPath();
                if (req.getForwardedFileURL().startsWith(contextPath+"/"))
                    webapp_relative = req.getForwardedFileURL().substring( contextPath.length(), req.getForwardedFileURL().length() );
                else
                    webapp_relative = req.getForwardedFileURL();
                log.debug( "Within context that means [" + webapp_relative + "]");
                //ought to ask session to record event.....
                
                ServletContext context;

                // non-JWS independent method...
                context = getServletContext();
                log.debug(context.toString());
                
                // using the dispatcher will be better performance when the web server uses
                // native code and caching etc.  However, the dispatcher may use mappings
                // onto servlets outside of Bodington that we would want to block, e.g.
                // JSP, CGI, SHTML.  THe alterative to using the dispatcher is to read
                // the file as a binary stream and send it to the user from here.
                use_dispatcher = 
                    !webapp_relative.toLowerCase().endsWith( ".jsp" ) && 
                    !webapp_relative.toLowerCase().endsWith( ".exe" ) &&
                    !webapp_relative.toLowerCase().endsWith( ".cgi" ) &&
                    !webapp_relative.toLowerCase().endsWith( ".shtml" );
                // this does not guard against other silly mappings that the sys admin
                // may have put in - best safeguard is to disable all server side
                // scripting at the level of the web server.

                if (use_dispatcher)
                {
                    log.debug("Using dispatcher.");
                    dispatcher=context.getRequestDispatcher( webapp_relative  );     // to grab file dispatcher
                    if (dispatcher == null)
                    {
                        res.sendError( 500, "Technical problem fetching file - couldn't find dispatcher." );
                        return;
                    }
                }
                else
                {
                    log.debug( "Not using dispatcher.");
                    in = context.getResourceAsStream(webapp_relative);
                    if (in == null)
                    {
                        res.sendError( 500, "Technical problem fetching file - couldn't find input stream." );
                        return;
                    }
                }

                UploadedFileSummary uf = req.getUploadedFile();

                if (req.getUploadedFile() != null)
                {
                    if (ServletUtils.isModified(req, res, uf.getUpdatedTime().getTime(), 1))
                    {
                        res.setStatus(Response.SC_NOT_MODIFIED);
                        return;
                    }

                    long size = uf.getSize();
                    if (size > 0L && size < 0x80000000)
                        res.setContentLength((int) size);
                    UserFileEvent event = new UserFileEvent(UserFileEvent.EVENT_DOWNLOAD, req.getResource(), uf);
                    event.save();
                }

                if (req.getUploadedFileMimeType() != null)
                {
                    res.setContentType(req.getUploadedFileMimeType());
                }

                // attempted workaround to bug in powerpoint
                // see MS KB articles Q311928 and Q314535
                // This header (Content-disposition) is not an accepted HTTP
                // header but more properly belongs to Email applications.
                // It seems that MS IE takes notice of this header and the
                // keyword attachment will cause it to override the default
                // behaviour for the link, which is to display inline and
                // the file will be passed to powerpoint as a viewer instead
                // of the URL being passed to powerpoint as an active X
                // plugin.

                if ( "application/vnd.ms-powerpoint".equals( req.getUploadedFileMimeType() ) )
                {
                    res.setHeader("Content-disposition", "attachment");
                }

                // This page request may have claimed a database connection from
                // the pool. Since the include call that follows may take a long
                // time to complete it is a good idea to free up the connection
                // now. The thread can reclaim a database connection later if
                // necessary.

                BuildingContext.getContext().freeConnection();

                try
                {
                    if (dispatcher != null)
                        dispatcher.include(req, res);
                    else
                    {
                        out = res.getOutputStream();
                        byte[] buffer = new byte[1024];
                        int n;
                        while ((n = in.read(buffer)) >= 0)
                            out.write(buffer, 0, n);
                    }
                }
                catch (ServletException sex)
                {
                    String message = "BuildingServlet.outputUploadedFile() call to include failed: " + sex.getMessage() + " url=" + webapp_relative;
                    log.error( message, sex );
                    
                    Throwable root_cause = sex.getRootCause();
                    if (root_cause != null)
                    {
                        message = "Root cause: " + root_cause.getMessage();
                        log.debug( message, root_cause);
                    }
                }
                finally
                {
                    if ( in!=null )
                        in.close();
                    if ( out!=null )
                        out.close();
                    if ( dispatcher!=null )
                        res.getOutputStream().close();
                }
            }
            else
            {
                if (req.getPageName().equals("index.html"))
                    res.sendError( Response.SC_NOT_FOUND, "<H3>This document doesn't yet have a home page (index.html).</H3>" );
                else
                    res.sendError( Response.SC_NOT_FOUND, "<H3>The requested page or graphic doesn't exist in this document.</H3>" );
            }
        }
        catch (IOException ioe)
        {
            if (log.isInfoEnabled())
            {
                log.info("Problem sending file: "+ ioe.toString());
            }
        }
        catch (Exception ex)
        {
            String message = ex.getMessage() + " url=" + webapp_relative;
            log.error( message, ex );
            
            if (!res.isCommitted())
            {
                res.setContentType("text/html");
                PrintWriter writer = res.getWriter();
                writer.print("<HTML><BODY><H3>Technical Problem.</H3><P>");
                writer.print(message);
                writer.print("</P></BODY></HTML>");
            }

        }

        return;
    }

    
    private void redirectToTemplate( Request breq, HttpServletResponse res, Template template )
    throws ServletException, IOException
    {
        String to = null;

	if ( !forwardOK( breq, res ) )
	    return;

        //   non-JWS version needs this object
        RequestDispatcher dispatcher;

        try
        {
            log.debug("redirection to ");
            log.debug( "/templates" + template.getUrl() );
            ServletContext context = getServletContext();
            File file = template.getFile();
            if (!file.exists() || !file.isFile())
            {
                res.sendError(404, "Template file not found.");
                return;
            }
            
            if (ServletUtils.isModified(breq, res,template.getTemplateTimestamp(), 300))
            {
                res.setStatus(Response.SC_NOT_MODIFIED);
                return;
            }
            
            to = "/templates" + template.getUrl();
            dispatcher=context.getRequestDispatcher( to );     // to grab file dispatcher
            if (dispatcher == null)
            {
                res.sendError( 500, "Technical problem fetching file - couldn't find dispatcher." );
                return;
            }
            
            try
            {
                dispatcher.include(breq, res);
            }
            catch (ServletException sex)
            {
                String message = "BuildingServlet.redirectToTemplate() call to include failed: " + sex.getMessage() + " url=" + to;
                log.error(message);
                
                log.debug( message, sex );
                
                Throwable root_cause = sex.getRootCause();
                if (root_cause != null)
                {
                    message = "Root cause: " + root_cause.getMessage();
                    log.debug( message, root_cause);
                }
            }
            finally
            {
                res.getOutputStream().close();
            }
        }
        catch (Exception ex)
        {
            String message = ex.getMessage() + " url=" + to;
            log.error( message, ex );
            if (!res.isCommitted())
            {
                res.setContentType("text/html");
                PrintWriter out = res.getWriter();
                out.println( "<HTML><BODY BACKGROUND=/tiles/default.gif><H3>Technical Problem.</H3><P>" + message + "</P></BODY></HTML>" );
                BuildingContext.dumpTrace();
            }
        }

        return;
    }


    public static Boolean disallow_http11=null;
    public static boolean forwardOK(Request req, HttpServletResponse res)
        throws ServletException, IOException
    {
        if (disallow_http11 == null)
        {
	    String disallow = BuildingContext.getProperty( "webpublish.disallowhttp11" );
	    disallow_http11 = new Boolean( disallow!=null &&
	    (disallow.equalsIgnoreCase( "true" ) ||
	    disallow.equalsIgnoreCase( "yes" )    )  );
        }

	if ( !disallow_http11.booleanValue() )
	    return true;

	if ( !req.getProtocol().equalsIgnoreCase( "HTTP/1.1" ) )
	    return true;

        res.setContentType("text/html");
        PrintWriter out = res.getWriter();
        out.println("<HTML><HEAD><TITLE>Technical Problem</TITLE></HEAD>");
        out.println("<BODY><H1>Technical Problem</H1>");
	out.println( "<P>Sorry, it is not possible to deliver the requested file to you " );
	out.println( "because this server is currently configured to use only HTTP 1.0 " );
	out.println( "and your browser used HTTP 1.1 to make its page request. " );
	out.println( "Please configure your web browser to use HTTP 1.0 and not HTTP 1.1.</p>" );

        out.println("</BODY></HTML>");
        out.flush();
        out.close();
        return false;
    }

    public String getServletInfo()
    {
        return "A servlet that implements a virtual building.";
    }
}