# @GPL_HEADER_START
# Use of the RAVE and related source code is subject to the terms
# of the following licenses:
# 
# GNU Public License (GPL) Rights pursuant to Version 2, June 1991
# Government Purpose License Rights (GPLR) pursuant to DFARS 252.225-7013
# 
# NO WARRANTY
# 
# ANY INFORMATION, MATERIALS, SERVICES, INTELLECTUAL PROPERTY OR OTHER
# PROPERTY OR RIGHTS GRANTED OR PROVIDED BY CARNEGIE MELLON UNIVERSITY
# PURSUANT TO THIS LICENSE (HEREINAFTER THE "DELIVERABLES") ARE ON AN
# "AS-IS" BASIS. CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY
# KIND, EITHER EXPRESS OR IMPLIED AS TO ANY MATTER INCLUDING, BUT NOT
# LIMITED TO, WARRANTY OF FITNESS FOR A PARTICULAR PURPOSE,
# MERCHANTABILITY, INFORMATIONAL CONTENT, NONINFRINGEMENT, OR ERROR-FREE
# OPERATION. CARNEGIE MELLON UNIVERSITY SHALL NOT BE LIABLE FOR INDIRECT,
# SPECIAL OR CONSEQUENTIAL DAMAGES, SUCH AS LOSS OF PROFITS OR INABILITY
# TO USE SAID INTELLECTUAL PROPERTY, UNDER THIS LICENSE, REGARDLESS OF
# WHETHER SUCH PARTY WAS AWARE OF THE POSSIBILITY OF SUCH DAMAGES.
# LICENSEE AGREES THAT IT WILL NOT MAKE ANY WARRANTY ON BEHALF OF
# CARNEGIE MELLON UNIVERSITY, EXPRESS OR IMPLIED, TO ANY PERSON
# CONCERNING THE APPLICATION OF OR THE RESULTS TO BE OBTAINED WITH THE
# DELIVERABLES UNDER THIS LICENSE.
# 
# Licensee hereby agrees to defend, indemnify, and hold harmless Carnegie
# Mellon University, its trustees, officers, employees, and agents from
# all claims or demands made against them (and any related losses,
# expenses, or attorney's fees) arising out of, or relating to Licensee's
# and/or its sub licensees' negligent use or willful misuse of or
# negligent conduct or willful misconduct regarding the Software,
# facilities, or other rights or assistance granted by Carnegie Mellon
# University under this License, including, but not limited to, any
# claims of product liability, personal injury, death, damage to
# property, or violation of any laws or regulations.
# 
# Carnegie Mellon University Software Engineering Institute authored
# documents are sponsored by the U.S. Department of Defense under
# Contract F19628-00-C-0003. Carnegie Mellon University retains
# copyrights in all material produced under this contract. The U.S.
# Government retains a non-exclusive, royalty-free license to publish or
# reproduce these documents, or allow others to do so, for U.S.
# Government purposes only pursuant to the copyright license under the
# contract clause at 252.227.7013.
# 
# @GPL_HEADER_END
#
# Classes supporting web server frontend
#

import BaseHTTPServer
import threading
import logging
import time
import socket
import SocketServer
import cgi # for parse_qsl

import rave.plugins.names as rnames
import rave.log as rlog
import rave.exceptions as rexcept

get_log = rlog.log_factory("rave.web")

#
# Constant names for headers et al.
#

# Wait for the request to finish before returning
HDR_WAIT= "X-Rave-Wait"
# Generate a new version and refresh the cache, even if the
# cache hasn't expired yet.
HDR_REFRESH = "X-Rave-Force-Refresh"
# Generate a new version of this request, _and_ if the operation
# uses cached results to generate its results, regenerate them, too.
HDR_DEEP_REFRESH = "X-Rave-Force-Deep-Refresh"

OUTPUT_XML=1
OUTPUT_JSON=2

# Additional module-global variables
output_format = OUTPUT_JSON

class BadRequest(Exception):
    "Client made a bad request"
    pass

def parse_action(action):
    first_slash = action.index('/')
    return action[:first_slash], action[first_slash + 1:]

def cgi_demunge(cgi_dict):
    """
    Turn crazy CGI dictionary into a real one. Multiple values are ignored;
    only the first is used.
    """
    return dict((k, v[0]) for k, v in cgi_dict.items())
        

class Result(object):
    """
    Results of running a file operation from query().

    Parameters:
        filename: str
            Name of the file that contains (or will contain) the
            results.
        mime_type: str
            MIME type of the results.
        complete: bool
            Did the operation complete yet?
    """
    def __init__(self, filename, mime_type, is_complete):
        self.filename = filename
        self.mime_type = mime_type
        self.is_complete = is_complete
    def __str__(self):
        return (
            "<rave.web.result: filename: %s;"
            "mime_type: %s; complete: %s>" % (
            self.filename, self.mime_type, self.is_complete)
        )

def query(action, params, scheduler,
          timeout=0, refresh=False, deep_refresh=False):
    """
    Obtain the results of the requested action.

    Parameters:
        action: str
            A string of the form "namespace/analysis" specifying an
            analysis to perform.
        params: dict
            Parameters to the analysis. Keys are strings, values are
            a list of all listed values for that parameter (usually
            one, so it's usually a one-element array).
        scheduler: rave.threads.Scheduler
            Work scheduler used to run analysis, if not cached
        timeout: int | None
            Controls whether to run the anlysis synchronously or
            asynchronously, and how long to wait for the analysis to finish.
                0 == asynchronous
                any integer == wait that many seconds
                None == wait forever
        refresh: Boolean
            Whether to unconditionally refresh the cache by rerunning
            the operation
        deep_refresh: Boolean
            Whether to unconditionally refresh this operation's cached data
            and any cached data it might use.
    Returns: Result
        The desired results
    """
    try:
        ns, opname = parse_action(action)
    except ValueError:
        raise BadRequest()

    fileop = rnames.op_from_export(ns, opname)
    get_log().debug("fileop is %s", fileop)

    get_log().debug("timeout is %s", timeout)

    if timeout == 0:
        get_log().debug("Won't wait for file operation to finish.")
        get_log().debug("Calling: %s(rave_params=%s...)",
                        fileop.core_name(), cgi_demunge(params))
        filename = fileop(
              rave_params=cgi_demunge(params)
            , rave_refresh=refresh
            , rave_deep_refresh=deep_refresh
            , rave_relative_path=True
            , rave_scheduler=scheduler
        )
        is_complete = False
    else:
        if timeout is not None:
            get_log().debug(
                "Waiting up to %d seconds for file operation to finish.",
                timeout)
        else:
            get_log().debug(
                "Waiting forever for file operation to finish.")
        job_done = threading.Event()
        demunged = cgi_demunge(params)
        get_log().debug("Calling: %s(rave_params=%s...)",
                        fileop.core_name(), demunged)
        filename = fileop(
              rave_params=demunged
            , rave_refresh=refresh
            , rave_deep_refresh=deep_refresh
            , rave_relative_path=True
            , rave_scheduler=scheduler
            , rave_on_done=job_done
        )
        job_done.wait(timeout)
        is_complete = job_done.isSet()
        
    return Result(filename, fileop.mime_type, is_complete)


class Ticket(object):
    """
    Identifies a job either in queue with the analysis engine or completed.
    """
    def __init__(self, url_base, result):
        """
        Constructor.
        Parameters:
        url_base
            prefix which, combined with filename, forms URL reference
            for completed job product
        result
            rave.plugins.registry.Result object
        """
        self.url_base  = url_base
        self.fname     = result.filename
        self.mime_type = result.mime_type
        self.version   = "1.1"

    def uri(self):
        return self.url_base + "/" + self.fname

    def name(self):
        return self.fname

    def xml(self):
        return """<?xml version="1.0"?>
<rave:ticket xmlns:rave="http://www.cert.org/schema/rave">
    <rave:version>%s</rave:version>
    <rave:uri>%s/%s</rave:uri>
    <rave:content-type>%s</rave:content-type>
</rave:ticket>
        """ % (self.version, self.url_base, self.fname, self.mime_type)

    def json(self):
        return """{
"version": "%s", 
"uri": "%s/%s",
"content-type": "%s"
 }""" % (self.version, self.url_base, self.fname, self.mime_type)

    def output(self):
        global output_format
        if output_format == OUTPUT_XML:
            return self.xml()
        elif output_format == OUTPUT_JSON:
            return self.json()
        else:
            raise RaveException("Unrecognized output format: %s" % 
                                output_format)

    def output_type(self):
        global output_format
        if output_format == OUTPUT_XML:
            return "text/xml"
        elif output_format == OUTPUT_JSON:
            return "text/json"
        else:
            raise RaveException("Unrecognized output format: %s" % 
                                output_format)

class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
    """
    Handles individual requests from rave.app.WebServer.

    The implementation is HTTP, but this is not a full-featured web server.
    In particular, sending query parameters on the URL via a POST is not
    supported.
    """
    def normalize_path(self):
    #   Strip leading slash(es)
        while self.path.startswith("/"):
            self.path = self.path[1:]

        if self.path.find("?") == -1:
            path = self.path
            params = {}
        else:
            try:
                (path, params_str) = self.path.split("?")
            except ValueError:
            #   Malformed query string -- two or more ?'s
                return None, None
            params = cgi.parse_qs(params_str, keep_blank_values=True)

    #   Strip trailing slash(es)
        while path.endswith("/"):
            path = path[:-1]

        return path, params

    def do_GET(self):
        """Serve all requests."""
        global output_format
        get_log().debug("RequestHandler.do_GET() called")
        try:
            path, params = self.normalize_path()
            if not path:
                self.send_error(400, "Bad request")
                return
            timeout_hdr = self.headers.getheader(HDR_WAIT)
        #   For the header:
        #   None      == unsupplied (default is async)
        #   0         == asynchronous
        #   n         == synchronous ( n == int timeout in seconds)
        #   "forever" == synchronous (wait forever)
        #
        #   We need to map this onto the semantics for a timeout
        #   in query, which adhere more closely to the semantics
        #   for threading timeouts:
        #   0    == async, non-blocking (wait() returns immediately)
        #   None == sync, block forever
        #   n    == sync, block n seconds
            if timeout_hdr is None or timeout_hdr == 0:
                timeout = 0    # Async
            elif timeout_hdr == "forever":
                timeout = None # Wait forever
            else:
                timeout = int(timeout_hdr)
                
            get_log().debug("timeout is %s", timeout)

            result = query(
                  path
                , params
                , self.server.scheduler
                , timeout
                , self.headers.has_key(HDR_REFRESH)
                , self.headers.has_key(HDR_DEEP_REFRESH)
            )
            get_log().debug("result is %s", result)
            ticket = Ticket(self.server.url_base, result)
            data = ticket.output()
            if timeout == 0:
                # Async -- we've put the job in queue, but it's not
                # done yet.
                #self.send_response(200, "Accepted")
                self.send_response(202, "Accepted")
            elif timeout is None:
                # Wait forever -- if we've returned, there's data
                #self.send_response(200, "Created")
                self.send_response(201, "Created")
                self.send_header("Location", ticket.uri())
            else:
                # Wait for a finite period of time. When we're done,
                # either the operation has completed successfully (or
                # was cached) and there's something to fetch, or it
                # timed out (errors should raise an exception)
                if result.is_complete:
                    #self.send_response(200, "Created")
                    # TODO:
                    self.send_response(201, "Created")
                    self.send_header("Location", ticket.uri())
                else:
                    #self.send_response(200, "Accepted")
                    self.send_response(202, "Accepted")
            self.send_header("Content-Type", ticket.output_type())
            self.send_header("Content-Length", len(data))
            self.end_headers()
            print >>self.wfile, data, # Final comma suppresses trailing newline
        except socket.error:
            get_log().error("Lost connection")
            return
        except rexcept.NoSuchOperation:
            self.send_error(
                404, "%s not found" % path
            )
            return
        except BadRequest:
            self.send_error(400, "Bad request")
        except:
            get_log().exception("Error processing request")
            self.send_error(500, "Server Error")
            return

    def log_message(self, format, *args):
        get_log('request').info("%s - - [%s] %s",
                                self.address_string(),
                                self.log_date_time_string(),
                                format%args)

#   Assign do_GET as handler for POST (TODO: fix POST)
    do_HEAD = do_POST = do_GET



class WebServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
    """
    The web server used to file tickets with the RAVE analysis
    engines.

    This webserver is thread-safe; in particular, a call to serve() will
    block until another thread calls shutdown().
    """

#   Change defaults in SocketServer
    allow_reuse_address = True

    def __init__(
          self
        , scheduler
        , url_base
        , listen_port=None
        , listen_addr="localhost"
     ):
        """
        Constructor.
        Parameters:
        scheduler
            Work scheduler object used to scheduler analysis queries
        url_base
            Root URL to report to clients to pick up visualizations
        listen_port (required)
            Port on which to listen for connections.
        listen_addr
            Address on which to listen for connections. Defaults to localhost.
        """
        if not listen_port:
            raise rexcept.ParameterError, "Listening port is required parameter"
        else:
            self.listen_port = listen_port
        if listen_addr == 'ALL':
            self.listen_addr = ''
        else:
            self.listen_addr = listen_addr
        #self.registry = registry
        self.scheduler = scheduler
        self.url_base = url_base

        self.should_run   = True
        self.evt_shutdown = threading.Event()
        self.evt_shutdown.set() # Start off in "shut down" state

    #   Doing __init__ to superclass last, so RequestHandler has access
    #   to variables on this class
        BaseHTTPServer.HTTPServer.__init__(
              self
            , (self.listen_addr, self.listen_port)
            , RequestHandler
        )
        get_log().info("Server bound to %s:%s" % (
            self.listen_addr, self.listen_port))

    def serve(self):
        """
        Serve until told to shutdown.

        serve() is a middle ground between serve_forever() (serve forever)
        handle_request() (serve one request). The caller must use serve()
        to service connections if shutdown() is expected to work.
        """
        get_log().debug("Server starting")
        self.evt_shutdown.clear()
        while self.should_run:
            self.handle_request()
        self.server_close()
        get_log().debug("Server stopped")
        self.evt_shutdown.set()

    def shutdown(self, timeout=None):
        """
        Stop serving connections and free up listening socket.
        @param timeout number of seconds to block until server has shut 
               down. By default, wait forever.
        """
        self.should_run = False
    #   The thread that ran serve() is probably blocking in handle_request()
    #   right now, waiting for a connection. Give it what it wants.
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        try:
            sock.connect((self.listen_addr, self.listen_port))
            sock.close()
        except socket.error: 
        #   The connection will occasionally shut down while the connect()
        #   is in the middle of connecting (usu. because a connection came
        #   between setting shouldRun to False and making the connect()
        #   call. In this case, a "connection reset by peer" will ensue.
        #
        #   This is harmless, and usually just means that things are working
        #   right without any help from connect().
            pass
        self.evt_shutdown.wait(timeout)

    def is_shutdown(self):
        """
        Is the webserver accepting connections?

        After calling shutdown() with a timeout, call this method to be sure
        that the server actually shut down and didn't just time out.

        @return True if the listening server has been shut down.
        """
        return self.evt_shutdown.isSet()



class WebServerThread(threading.Thread):
    """Wrap a WebServer in a thread, with facilities for control 
    (startup, shutdown).
    """
    def __init__(self, www):
        super(WebServerThread, self).__init__()
        self.www = www

    def shutdown(self):
    #   Block no more than 5 seconds waiting for shutdown
        self.www.shutdown(5)
        if not self.www.is_shutdown():
            get_log().warn("Webserver did not shut down normally. "
                           "Expect errors.")

    #   Give the server socket time to actually shut down, as we'll
    #   most likely be using it again in just a millisecond.
        time.sleep(.1)

    def run(self):
        self.www.serve()

