/* ======================================================================
   Parts Copyright 2006 University of Leeds, Oxford University, University of the Highlands and Islands.

   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.bodington.servlet;

import org.apache.log4j.Logger;

import java.io.*;
import java.net.*;
import java.util.Date;

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;
import org.bodington.util.BodingtonURL;

/**
 * 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);
    
    BuildingServletContext buildingServletContext;
    
    private String loginPath = "bs_template_login_oxford_wa.html";
    private String cookieName = "webauth_found";
	
    
    public void init(ServletConfig c) throws ServletException
    {
        super.init(c);
        log("Initialising building servlet");
        buildingServletContext = new BuildingServletContext(getServletContext());
        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.");


	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("/"))
        {
            StringBuffer url = new StringBuffer(req.getRequestURI());
            url.append("/");
            if (req.getQueryString() != null && req.getQueryString().length() > 0) {
                url.append("?");
                url.append(req.getQueryString());
            }
            res.sendRedirect(url.toString());
            log.debug("Redirected broken incomming request(noslash)");
            return;
        }

        try
        {
            
            BuildingContext context = BuildingContext.startContext();
            if ( context == null )
                throw new UnavailableException( "Bodington server isn't ready." );
            
            // wrap request and response in Bodington's own wrappers so that
            // extra functionality can be added.
            Request buildingRequest = new Request(req, buildingServletContext);
            Response buildingResponse = new Response(res);
            
            doProcessing(buildingRequest, buildingResponse);
        }
        catch (RequestSizeException sse)
        {
            res.sendError(HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE, sse.getMessage());
        }
        catch (BuildingServerException e)
        {
            res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
            log.error( e );
        }
        finally
        {
            BuildingContext.endContext();
        }


    }

    public void doProcessing(Request breq, Response res)
        throws ServletException, IOException
    {
        User user = (User) BuildingContext.getContext().getUser();
        if (user == null)
        {
	    res.sendError( HttpServletResponse.SC_NOT_FOUND, "Unrecognised user of this building." );
            BuildingContext.trace("ending,unknown user");
            BuildingContext.dumpTrace();
            return;
        }
        // There is very similar code in sakai(tool-util).
        // Redirect to login page if is doesn't look like we're logged in.w
		if (hasCookie(breq) && breq.getParameter("org_bodington_servlet_entry_path") == null) {
			if (breq.isAnonymous()) {
				// Ok, time to redirect then.
				StringBuffer originalUrl = breq.getRequestURL();
				String queryString = breq.getQueryString();
				if (queryString != null) {
						originalUrl.append("?").append(breq.getQueryString());
				}
				String resource = new BodingtonURL(breq).getResourceUrl(breq.getResource());
				String redirectUrl = new StringBuilder(resource).append(loginPath).append(
						"?org_bodington_servlet_entry_path=").append(
						URLEncoder.encode(originalUrl.toString(), "UTF-8")).toString();
				res.sendRedirect(redirectUrl);
				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( HttpServletResponse.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( HttpServletResponse.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( HttpServletResponse.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( HttpServletResponse.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(Response.SC_FORBIDDEN, "Bodington Page");
                    break;
                default:
                    res.disableCaching();
		    res.sendError( HttpServletResponse.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( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "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( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Technical problem fetching file - couldn't find input stream." );
                        return;
                    }
                }

                UploadedFileSummary uf = req.getUploadedFile();
                String mimeType;

                if (uf  != null)
                {
                    if (ServletUtils.isModified(req, res, uf.getUpdatedTime().getTime(), 1))
                    {
                        res.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                        return;
                    }
                    
                    long size = uf.getSize();
                    if (size >= 0L && size <= (long)Integer.MAX_VALUE)
                        res.setContentLength((int) size);
                    UserFileEvent event = new UserFileEvent(UserFileEvent.EVENT_DOWNLOAD, req.getResource(), uf);
                    event.save();
                    mimeType = uf.getMimeType();
                    if (mimeType != null)
                    {
                        if (mimeType.length() == 0)
                            mimeType = BuildingServer.getInstance().getMimeType(uf.getName());
                        // getMimeType may have set it back to NULL
                        if (mimeType != null)
                            res.setContentType(mimeType);
                    }
                    
                    // 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( mimeType ) )
                    {
                        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( HttpServletResponse.SC_NOT_FOUND, "This document doesn't yet have a home page (index.html)." );
                else
                    res.sendError( HttpServletResponse.SC_NOT_FOUND, "The requested page or graphic doesn't exist in this document." );
            }
        }
        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(HttpServletResponse.SC_NOT_FOUND, "Template file not found.");
                return;
            }
            
            if (ServletUtils.isModified(breq, res,template.getTemplateTimestamp(), 300))
            {
                res.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                return;
            }
            
            to = "/templates" + template.getUrl();
            dispatcher=context.getRequestDispatcher( to );     // to grab file dispatcher
            if (dispatcher == null)
            {
                res.sendError( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "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.";
    }
    
	private boolean hasCookie(HttpServletRequest request) {
		boolean hasCookie = false;
		boolean multipleMatches = false;
		if (request.getCookies() != null) {
			Cookie[] cookies = request.getCookies();
			for (int i = 0; i < cookies.length; i++) {
				if (cookieName.equals(cookies[i].getName())) {
					// If found multiple cookie.
					multipleMatches = hasCookie;
					// If we've already found it once, don't try again.
					if (!hasCookie){
						String value = cookies[i].getValue();
						if (value != null && value.length() > 0) {
							long now = System.currentTimeMillis();
							try {
								long expires = Long.parseLong(value);
								if (log.isDebugEnabled()) {
									log.debug("Webauth session expires "+ new Date(expires).toString()+ " which is in "+ (expires - now)+ "ms"); 
								}
								if (expires > now) {
									hasCookie = true;
								}
							} catch (NumberFormatException nfe) {
								log.warn("Cookie doesn't have a good expiry time: "+ value);
							}
						}
					}
				}
			}
			if (multipleMatches && log.isDebugEnabled()) {
				log.debug("Multiple cookies for '" + cookieName + "' found.");
			}
		}
		return hasCookie;
	}
	
}
