###########################################################################
# Joshua R. Boverhof, LBNL
# See Copyright for copyright notice!
# $Id: WSresource.py 1439 2008-01-11 00:47:15Z boverhof $
###########################################################################

import sys, warnings

# twisted & related imports
from zope.interface import classProvides, implements, Interface
from twisted.python import log, failure
from twisted.web.error import NoResource
from twisted.web.server import NOT_DONE_YET
import twisted.web.http
import twisted.web.resource

# ZSI imports
from ZSI import _get_element_nsuri_name, EvaluateException, ParseException
from ZSI.parse import ParsedSoap
from ZSI.writer import SoapWriter
from ZSI import fault

# WS-Address related imports
from ZSI.address import Address
from ZSI.ServiceContainer import WSActionException

from interfaces import CheckInputArgs, HandlerChainInterface, CallbackChainInterface,\
    DataHandler


class LoggingHandlerChain:

    @CheckInputArgs(CallbackChainInterface, HandlerChainInterface)
    def __init__(self, cb, *handlers):
        self.handlercb = cb
        self.handlers = handlers
        self.debug = len(log.theLogPublisher.observers) > 0
        
    def processRequest(self, arg, **kw):
        debug = self.debug
        if debug: log.msg('--->PROCESS REQUEST: %s' %arg, debug=1)
        
        for h in self.handlers:
            if debug: log.msg('\t%s handler: %s' %(arg, h), debug=1)
            arg = h.processRequest(arg, **kw)
            
        return self.handlercb.processRequest(arg, **kw)
            
    def processResponse(self, arg, **kw):
        debug = self.debug
        if debug: log.msg('===>PROCESS RESPONSE: %s' %str(arg), debug=1)

        if arg is None: 
            return

        for h in self.handlers:
            if debug: log.msg('\t%s handler: %s' %(arg, h), debug=1)
            arg = h.processResponse(arg, **kw)
            
        s = str(arg)
        if debug: log.msg(s, debug=1)
        
        return s


# 
# Stability: Unstable
#     
class DefaultCallbackHandler:
    classProvides(CallbackChainInterface)

    @classmethod
    def processRequest(cls, ps, **kw):
        """invokes callback that should return a (request,response) tuple.
        representing the SOAP request and response respectively.
        ps -- ParsedSoap instance representing HTTP Body.
        request -- twisted.web.server.Request
        """
        resource = kw['resource']
        request = kw['request']
        method =  getattr(resource, 'soap_%s' %
                           _get_element_nsuri_name(ps.body_root)[-1])
                                              
        try:
            req_pyobj,rsp_pyobj = method(ps, request=request)
        except TypeError, ex:
            log.err(
                'ERROR: service %s is broken, method MUST return request, response'\
                    % cls.__name__
            )
            raise
        except Exception, ex:
            log.err('failure when calling bound method')
            raise
        
        return rsp_pyobj
    

class WSAddressHandler:
    """General WS-Address handler.  This implementation depends on a 
    'wsAction' dictionary in the service stub which contains keys to 
    WS-Action values.  

    Implementation saves state on request response flow, so using this 
    handle is not  reliable if execution is deferred between proceesRequest 
    and processResponse.  

    TODO: sink this up with wsdl2dispatch
    TODO: reduce coupling with WSAddressCallbackHandler.
    """
    implements(HandlerChainInterface)
    
    def processRequest(self, ps, **kw):
        # TODO: Clean this up
        resource = kw['resource']

        d = getattr(resource, 'root', None)
        key = _get_element_nsuri_name(ps.body_root)
        if d is None or d.has_key(key) is False:
            raise RuntimeError,\
                'Error looking for key(%s) in root dictionary(%s)' %(key, str(d))

        self.op_name = d[key]
        self.address = address = Address()
        address.parse(ps)
        action = address.getAction()
        if not action:
            raise WSActionException('No WS-Action specified in Request')

        request = kw['request']
        http_headers = request.getAllHeaders()
        soap_action = http_headers.get('soapaction')
        if soap_action and soap_action.strip('\'"') != action:
            raise WSActionException(\
                'SOAP Action("%s") must match WS-Action("%s") if specified.'\
                %(soap_action,action)
            )
            
        # Save WS-Address in ParsedSoap instance.
        ps.address = address
        return ps
        
    def processResponse(self, sw, **kw):
        if sw is None:
            self.address = None
            return
        
        request, resource = kw['request'], kw['resource']
        if isinstance(request, twisted.web.http.Request) is False:
            raise TypeError, '%s instance expected' %http.Request
                
        d = getattr(resource, 'wsAction', None)
        key = self.op_name
        if d is None or d.has_key(key) is False:
            raise WSActionNotSpecified,\
                'Error looking for key(%s) in wsAction dictionary(%s)' %(key, str(d))

        addressRsp = Address(action=d[key])
        if request.transport.TLS == 0:
            addressRsp.setResponseFromWSAddress(\
                 self.address, 'http://%s:%d%s' %(
                 request.host.host, request.host.port, request.path)
            )
        else:
            addressRsp.setResponseFromWSAddress(\
                 self.address, 'https://%s:%d%s' %(
                 request.host.host, request.host.port, request.path)
            )
            
        addressRsp.serialize(sw, typed=False)
        self.address = None
        return sw
    

class WSAddressCallbackHandler:
    classProvides(CallbackChainInterface)

    @classmethod
    def processRequest(cls, ps, **kw):
        """invokes callback that should return a (request,response) tuple.
        representing the SOAP request and response respectively.
        ps -- ParsedSoap instance representing HTTP Body.
        request -- twisted.web.server.Request
        """
        resource = kw['resource']
        request = kw['request']
        method =  getattr(resource, 'wsa_%s' %
                           _get_element_nsuri_name(ps.body_root)[-1])
                                              
        # TODO: grab ps.address, clean this up.
        try:
            req_pyobj,rsp_pyobj = method(ps, ps.address, request=request)
        except TypeError, ex:
            log.err(
                'ERROR: service %s is broken, method MUST return request, response'\
                    %self.__class__.__name__
            )
            raise
        except Exception, ex:
            log.err('failure when calling bound method')
            raise
        
        return rsp_pyobj
            

class DeferHandlerChain:
    """Each handler is 
    """

    @CheckInputArgs(CallbackChainInterface, HandlerChainInterface)
    def __init__(self, cb, *handlers):
        self.handlercb = cb
        self.handlers = handlers
        self.debug = len(log.theLogPublisher.observers) > 0
        
    def processRequest(self, arg, **kw):
        from twisted.internet import reactor
        from twisted.internet.defer import Deferred
        
        debug = self.debug
        if debug: log.msg('--->DEFER PROCESS REQUEST: %s' %arg, debug=1)
        
        d = Deferred()
        for h in self.handlers:
            if debug: 
                log.msg('\t%s handler: %s' %(arg, h), debug=1)
                log.msg('\thandler callback: %s' %h.processRequest)
            d.addCallback(h.processRequest, **kw)
            
        d.addCallback(self.handlercb.processRequest, **kw)
        reactor.callLater(.0001, d.callback, arg)

        if debug: log.msg('===>DEFER PROCESS RESPONSE: %s' %str(arg), debug=1)


        
        for h in self.handlers:
            if debug: log.msg('\t%s handler: %s' %(arg, h), debug=1)
            d.addCallback(h.processResponse, **kw)

        d.addCallback(str)
        return d
    
    def processResponse(self, arg, **kw):
        return arg


class DefaultHandlerChainFactory:
    protocol = LoggingHandlerChain
    
    @classmethod
    def newInstance(cls):
        return cls.protocol(DefaultCallbackHandler, DataHandler)
    

class DefaultHandlerChain:

    @CheckInputArgs(CallbackChainInterface, HandlerChainInterface)
    def __init__(self, cb, *handlers):
        self.handlercb = cb
        self.handlers = handlers
        self.debug = len(log.theLogPublisher.observers) > 0
        
    def processRequest(self, arg, **kw):
        debug = self.debug
        if debug: log.msg('--->PROCESS REQUEST: %s' %arg, debug=1)
        
        for h in self.handlers:
            if debug: log.msg('\t%s handler: %s' %(arg, h), debug=1)
            arg = h.processRequest(arg, **kw)
            
        return self.handlercb.processRequest(arg, **kw)
            
    def processResponse(self, arg, **kw):
        debug = self.debug
        if debug: log.msg('===>PROCESS RESPONSE: %s' %str(arg), debug=1)

        if arg is None: 
            return

        for h in self.handlers:
            if debug: log.msg('\t%s handler: %s' %(arg, h), debug=1)
            arg = h.processResponse(arg, **kw)
            
        s = str(arg)
        if debug: log.msg(s, debug=1)
        
        return s


class WSAddressHandlerChainFactory:
    protocol = DefaultHandlerChain
    
    @classmethod
    def newInstance(cls):
        return cls.protocol(WSAddressCallbackHandler, DataHandler, 
            WSAddressHandler())


class WSResource(twisted.web.resource.Resource, object):
    """
    class variables:
        encoding  --
        factory -- hander chain, which has a factory method "newInstance"
		that returns a 
    """
    encoding = "UTF-8"
    factory = DefaultHandlerChainFactory

    def __init__(self):
        """
        """
        twisted.web.resource.Resource.__init__(self)

    def _writeResponse(self, response, request, status=200):
        """
        request -- request message
        response --- response message
        status -- HTTP Status
        """
        request.setResponseCode(status)
        if self.encoding is not None:
            mimeType = 'text/xml; charset="%s"' % self.encoding
        else:
            mimeType = "text/xml"

        request.setHeader("Content-Type", mimeType)
        request.setHeader("Content-Length", str(len(response)))
        request.write(response)
        request.finish()

    def _writeFault(self, fail, request):
        """
        fail -- failure
        request -- request message
        ex -- Exception 
        """
        response = fault.FaultFromException(fail.value, False, fail.tb).AsSOAP() 
        self._writeResponse(response, request, status=500)

    def render_POST(self, request):
        """Dispatch Method called by twisted render, creates a 
        request/response handler chain.
        request -- twisted.web.server.Request
        """
        from twisted.internet.defer import maybeDeferred
        
        chain = self.factory.newInstance()
        data = request.content.read()
        
        d = maybeDeferred(chain.processRequest, data, request=request, resource=self)
        d.addCallback(chain.processResponse, request=request, resource=self)
        d.addCallback(self._writeResponse, request)
        d.addErrback(self._writeFault, request)
        
        return NOT_DONE_YET


