########################################################################
# $Header: /var/local/cvsroot/4Suite/Ft/Server/Server/Http/Soap/Handler.py,v 1.9 2005/04/14 03:21:04 cogbuji Exp $
"""
Exposes the SCore interface over SOAP

Copyright 2005 Fourthought, Inc. (USA).
Detailed license and copyright information: http://4suite.org/COPYRIGHT
Project home, documentation, distributions: http://4suite.org/
"""

__all__ = ['FTSS_SOAP_NS', 'SOAP_NS', 'SOAP_ENCODING_STYLE',
           'XMLSCHEMA_INSTANCE_NS',
           'VersaQueryHandler', 'SoapHandler',
           'GenericResourceMethod', 'RawFileSoapImp',
           'FetchResource', 'CreateResource', 'DeleteResource',
           ]

import string, base64, sys, cStringIO, traceback, sha, time
from xml.dom import Node

from Ft.Rdf._4versa import PrettyPrintResults, BASE_NS_MAP
from Ft.Rdf.Parsers.Versa import VERSA_NS
from Ft.Server import FTSERVER_NAMESPACE
from Ft.Server.Common import Util, ResourceTypes, Schema
from Ft.Server.Common.AclConstants import ANONYMOUS_USER_NAME
from Ft.Server.Server import FtServerServerException
from Ft.Server.Server import Error as ServerError
from Ft.Server.Server.Http import BaseRequestHandler, Status
from Ft.Server.Server.Http import FtServerHttpException, Error
from Ft.Server.Server.SCore import GetRepository
from Ft.Xml import Domlette, XPath, EMPTY_NAMESPACE

FTSS_SOAP_NS = '%s#services' % FTSERVER_NAMESPACE

XMLSCHEMA_INSTANCE_NS = 'http://www.w3.org/2001/XMLSchema-instance'
SOAP_NS = "http://schemas.xmlsoap.org/soap/envelope/"
SOAP_ENCODING_STYLE = "http://schemas.xmlsoap.org/soap/encoding/"

SESSION_HEADER_ID = "sessionId"
SESSION_HEADER_KEY = "sessionKey"
USER_HEADER_KEY     = "authenticatingUser"
PASSWORD_HEADER_KEY = "authenticatingPassword"

def VersaQueryHandler(repo,reqel,respbody,logFile):
    """Handler for Versa Queries"""

    args = {}
    for child in [child for child in reqel.childNodes if child.nodeName != '#text']:
        name = child.localName
        try:
            value = child.firstChild.data
        except:
            value = None

        args[str(name)] = value

    query = args.get('query')
    scope = args.get('scope')
    if scope:
        scope = scope.strip()

    encodeContent = False
    if args.has_key('base64') and args['base64'].lower() in ['true','1']:
        encodeContent = True

    if args.has_key('base64'):
        del args['base64']

    if not query:
        raise FtServerHttpException(Error.BAD_SOAP_QUERY)

    from Ft.Rdf.Parsers import Versa
    from Ft.Rdf.Parsers.Versa.Util import ResultsToXml

    if encodeContent:
        exp = Versa.Compile(base64.decodestring(query))
    else:
        exp = Versa.Compile(query)

    try:
        nsMappings = Util.GetUserNsMappings(repo, BASE_NS_MAP)
        nsMappings = ParseNsMappings(repo.fetchResource('/ftss/dashboard/NsMappings.xml').getContent(), {})
    except:
        pass

    con = Versa.CreateContext(model=repo.getModel(),
                              nsMapping=nsMappings,
                              scope=scope)

    st = cStringIO.StringIO()
    ResultsToXml(exp.evaluate(con),st)
    resultRoot = "<Result xmlns='http://rdfinference.org/versa/0/2/'>%s</Result>"
    doc = Domlette.NonvalidatingReader.parseString(resultRoot%(st.getvalue()), uri='urn:uuid:versaResult')
    respbody.appendChild(doc.documentElement)

class SoapHandler(BaseRequestHandler.BaseRequestHandler):
    def __init__(self, connection):
        BaseRequestHandler.BaseRequestHandler.__init__(self, connection)
        config = vars(self.server)
        self.http_post_encoding_var = config.get('http_post_encoding_var', 'http-post-encoding')
        return



    #This dictionary is to be used by overridden implementations to look up the correct actions.
    #The default action is to map the URI to a SOAP Server instance.
    #Then call the correct action on the instance.  Since 90% of all of the parameters are strings,
    #All will be type cast to strings by the "default" handler.
    #The biggest exception is self will be the actual resource that was requested.
    NS_TO_HANDLER_MAPPING = {
        VERSA_NS : {'versa-query' : VersaQueryHandler,}
    }

    def SOAPAuthenticate(self,SOAPdoc):
        self.sessionId = None
        soap_envelope = SOAPdoc.documentElement
        soap_header = filter(lambda x: (x.namespaceURI, x.localName) == (SOAP_NS, "Header"), soap_envelope.childNodes)
        if not soap_header:
            return

        soap_header = soap_header[0]
        soap_authEl = filter(lambda x: (x.namespaceURI, x.localName) == (FTSS_SOAP_NS, "authenticationHeader"), soap_header.childNodes)
        if not soap_authEl:
            return
        soap_authEl = soap_authEl[0]

        self.user       = soap_authEl.xpath('string(*[local-name() = "%s"  ])'%(USER_HEADER_KEY)) or None
        self.password   = sha.new(soap_authEl.xpath('string(*[local-name() = "%s"  ])'%(PASSWORD_HEADER_KEY)) or '').hexdigest()
        self.sessionId  = soap_authEl.xpath('string(*[local-name() = "%s"  ])'%(SESSION_HEADER_ID)) or None
        self.sessionKey = soap_authEl.xpath('string(*[local-name() = "%s"  ])'%(SESSION_HEADER_KEY)) or None

    def getRepository(self, sendError=1):
        """
        Overidden to account for session handshaking
        """
        if self.sessionId:
            from Ft.Server.Server.SCore import RetrieveSession
            repo = RetrieveSession(self.sessionId,
                                   self.sessionKey,
                                   self.server.errorLog,
                                   self.server.properties)

        elif not self.user or self.user == ANONYMOUS_USER_NAME:
            # Anonymous login or no authentication required
            self.server.errorLog.debug("Repository access obtained for anonymous user")
            repo = GetRepository(None,
                                 None,
                                 self.server.errorLog,
                                 self.server.properties)
        else:
            try:
                repo = GetRepository(self.user,
                                     self.password,
                                     self.server.errorLog,
                                     self.server.properties)
                self.server.errorLog.debug("Repository access obtained for user %s" % self.user)
            except FtServerServerException, e:
                if sendError and e.errorCode in [ServerError.INVALID_LOGIN,]:
                    # Generate an HTTP_FORBIDDEN response
                    self.server.errorLog.error("Repository access denied for user %s; will send Forbidden response" % self.user)
                    self.status = Status.HTTP_FORBIDDEN
                    return None
                raise
        return repo

    def do_POST(self):
        """
        Handle a POST request that is a SOAP message
        """

        #Look for the SOAP Action
        for name in self.headers_in.keys():
            if name.lower() == 'soapaction':
                break
        else:
            raise FtServerHttpException(Error.MISSING_SOAPACTION_HEADER)

        self.server.errorLog.info("Beginning processing of SOAP message (%s bytes)" % (len(self.body)))

        #Read in the request document
        start = time.time()
        try:
            doc = Domlette.NonvalidatingReader.parseString(self.body, uri='http://schemas.xmlsoap.org/soap/envelope/')
        except Exception, e:
            print "Unparseable SOAP message: ", self.body
            raise FtServerHttpException(Error.MALFORMED_SOAP_MESSAGE, errmsg=str(e))
        end = time.time()
        self.server.errorLog.info("Successfully parsed SOAP message in %0.3fs"%(end - start))

        #Look four our handler function
        #this will raise an exception if not handled
        handlerfunc,reqbody = self._checkHeader(doc)

        #Get a connection to the repository
        #check if user/passwd was supplied in SOAP header
        self.server.errorLog.info("Processing authentication header ..")
        self.SOAPAuthenticate(doc)
        repo = self.getRepository(sendError = 1)
        if repo is None:
            #An error was sent, just return
            return

        commit = 0
        try:
            #Fall into the main handling
            try:
                respbody = self._prepResponse(reqbody)
                commit = handlerfunc(repo, reqbody, respbody,self.server.errorLog)
            except:
                self.server.errorLog.info("Exception executing SOAP handler function")
                import traceback;
                traceback.print_exc()
                commit = 0
                #Send a SOAP fault
                respbody = self._generateFault(reqbody)
            return self._finalizeResponse(reqbody, respbody)

        finally:
            if commit:
                repo.txCommit()
            else:
                try:
                    repo.txRollback()
                except:
                    pass


    def _checkHeader(self, doc):
        #Read in the SOAP request envelope
        #Make sure it is a valid request for us to handle

        soap_envelope = doc.documentElement
        if (soap_envelope.namespaceURI, soap_envelope.localName) != (SOAP_NS, "Envelope"):
            #FIXME: Use HTTP SoapAction header for screening insead.
            #If it's present but the above condition is false, we send a client fault
            #FIXME: This should return a SOAP client fault
            raise FtServerHttpException(Error.INVALID_SOAP_ENVELOPE)

        soap_body = filter(lambda x: (x.namespaceURI, x.localName) == (SOAP_NS, "Body"), soap_envelope.childNodes)[0]
        request_el = filter(lambda x: x.nodeType == Node.ELEMENT_NODE, soap_body.childNodes)[0]
        request_ns = request_el.namespaceURI
        request_name = request_el.localName

        if not self.body:
            raise FtServerHttpException(Error.MISSING_SOAP_BODY)

        handlerfunc = None
        #Determine our handler
        if request_ns in self.NS_TO_HANDLER_MAPPING:
            handlerfunc1 = self.NS_TO_HANDLER_MAPPING[request_ns]
            if request_name in handlerfunc1:
                handlerfunc = handlerfunc1[request_name]
            else:
                raise FtServerHttpException(Error.NO_SOAP_HANDLER_FOR_NAME,
                    name=request_name, ns=request_ns)
        elif request_ns == FTSS_SOAP_NS:
            handlerfunc = GenericResourceMethod
        else:
            #Otherwise no clue!!!
                raise FtServerHttpException(Error.NO_SOAP_HANDLER_FOR_NS, ns=request_ns)

        return handlerfunc, request_el

    def _prepResponse(self, doc):
        #Create the response document
        try:
            dt = Domlette.implementation.createDocumentType("SOAP-ENV:Envelope", '', '')
        except:
            dt = None
        respdoc = Domlette.implementation.createDocument(SOAP_NS, "SOAP-ENV:Envelope",dt)
        respdocel = respdoc.documentElement
        respdocel.setAttributeNS(SOAP_NS, "SOAP-ENV:encodingStyle", SOAP_ENCODING_STYLE)
        respbody = respdoc.createElementNS(SOAP_NS, "SOAP-ENV:Body")
        respdocel.appendChild(respbody)
        return respbody

    def _finalizeResponse(self, reqbody, respbody):
        #Serialize the results
        self.status = Status.HTTP_OK
        Domlette.PrettyPrint(respbody.ownerDocument, stream=self.wfile)
        self.headers_out['Content-Type'] = 'text/xml'
        return



    def _generateFault(self,reqbody):

        #Create a new respbody
        respbody = self._prepResponse(reqbody)

        eType, ex = sys.exc_info()[:2]

        doc = respbody.ownerDocument

        if 0:
            #FIXME Capture specific exceptions
            pass
        else:
            #Unknown Fault
            faultCode = "SOAP-ENV:Server"
            #faultCode = ""
            faultString = str(ex)
            faultFactor = None
            st = cStringIO.StringIO()
            traceback.print_exc(file=st)
            detailStr = base64.encodestring(st.getvalue())
            detail = doc.createElementNS(EMPTY_NAMESPACE,'detail')
            tb = doc.createElementNS(FTSS_SOAP_NS,'ftsoap:traceback')
            tb.appendChild(doc.createTextNode(detailStr))
            detail.appendChild(tb)

        fault = doc.createElementNS(SOAP_NS,'SOAP-ENV:Fault')
        fc = doc.createElementNS(EMPTY_NAMESPACE,'faultcode')
        fc.appendChild(doc.createTextNode(str(faultCode)))
        fault.appendChild(fc)

        fs = doc.createElementNS(EMPTY_NAMESPACE,'faultstring')
        fs.appendChild(doc.createTextNode(str(faultString)))
        fault.appendChild(fs)

        if faultFactor:
            ff = doc.createElementNS(EMPTY_NAMESPACE,'faultfactor')
            ff.appendChild(doc.createTextNode(str(faultFactor)))
            fault.appendChild(ff)

        if detail:
            fault.appendChild(detail)

        respbody.appendChild(fault)
        return respbody

    def get_form_encoding(self, form):
        if self.form_encoding: return
        import cgi
        encoding = 'ISO-8859-1'
        fields = cgi.parse_qs(form)
        if fields.has_key(self.http_post_encoding_var):
            encoding = fields[self.http_post_encoding_var][0]
        self.form_encoding = encoding



def GenericResourceMethod(repo,reqel,respbody,logFile):

    #If, we have fallen to this then we want to perform a generic request on the
    #On a resource.  Use the path attribute to get the resource
    path = reqel.xpath('string(./*[local-name() = "srcpath"])')

    if not path:
        raise ValueError("Missing (or illegally specified) required 'srcpath' parameter")

    res = repo.fetchResource(path)

    #Now, mapp the resource type to the local implementation
    #l = g_resourceTypeMapping[res.resourceType](res)
    l = RawFileSoapImp(res)
    return l.execute(reqel,respbody,logFile)



class RawFileSoapImp:

    integerArguments = [
        'createDocument.forcedType',
        'createContainer.createParents'
    ]

    def __init__(self,res):
        self.resource = res

    def execute(self,reqel,respbody,logFile):
        """
        See if we define the requested action, ifso call that, otherwise, just call it on the resource
        """
        mname = reqel.localName[0].lower() + reqel.localName[1:]
        #print mname
        if hasattr(self,mname):
            #Call it special
            return getattr(self,mname)(res,reqel,respbody)

        #If not, just call them all assuming that ever attribute mapps to a string
        args = {}

        for child in [child for child in reqel.childNodes if child.nodeName != '#text']:
            name = child.localName
            try:
                value = child.firstChild.data
            except:
                value = None

            if name == 'srcpath':
                srcpath = value
                continue

            if string.join([mname,str(name)],'.') in self.integerArguments:
                args[str(name)] = int(value)
            else:
                args[str(name)] = value

        #Special case, if method is fetchResource, then assume src-path referes to the resource we wish
        #to retrieve.  Also check for base64 attribute which determines whether or not to encode the returned
        #content (encodes the content by default)
        encodeContent = True
        if mname == 'fetchResource':
            args['path'] = srcpath

        if args.has_key('base64') and args['base64'].lower() in ['false','0']:
            encodeContent = False

        if args.has_key('base64'):
            del args['base64']

        #See if there is a src or updateSrc argument.  If so check for a 'base64' attribute (assume encoded if)
        #doesn't exist
        if encodeContent:
            if args.has_key('src'):
                start = time.time()
                args['src'] = base64.decodestring(args['src'])
                end = time.time()
                logFile.info("Time to decode Base64 'src' argument: %s seconds" % (end - start))

            if args.has_key('updateSrc'):
                start = time.time()
                args['updateSrc'] = base64.decodestring(args['updateSrc'])
                end = time.time()
                logFile.info("Time to decode Base64 'updateSrc' argument: %s seconds" % (end - start))

        meth = getattr(self.resource, mname)
        msg = "%s(" % mname
        for name, value in args.items():
            if name in ['src','updateSrc']:
                value = "..."
            msg += "%s='%s'," % (name, value)
        msg += ')'
        logFile.info("Executing SOAP command: %s on %s" % (msg, self.resource.getAbsolutePath()))

        start = time.time()
        if mname == 'xUpdate':
            print "Special command (xUpdate).  Processed explicitely"
            from Ft.Xml.Domlette import Print, NonvalidatingReader
            from Ft.Xml import XUpdate
            from Ft.Rdf.Triclops import ParseNsMappings
            resDom = self.resource.asDom()
            userMappings = Util.GetUserNsMappings(repo, BASE_NS_MAP)
            XUpdate.Processor().execute(resDom,
                                        NonvalidatingReader.parseString(args['updateSrc'],
                                                                        uri="urn:uuid:XUpdate"),
                                        processorNss=userMappings)
            st = cStringIO.StringIO()
            Print(resDom,stream=st)
            res = self.resource.setContent(st.getvalue())
        else:
            res = meth(**args)
        end = time.time()

        logFile.info("Time to execute command: %s seconds" % (end - start))
        logFile.info("result of command: %s"%(res))

        if mname == 'applyXslt':
            #FIXME Other special cases: applyXslt (return results only)
            res = res[0]

        if hasattr(res, 'resourceType'):
            self._makeResourceResponse(res,respbody,encodeContent)
        elif res is not None:
            self._makeValueResponse(res,respbody)
        else:
            self._makeSuccessResponse(respbody)
        return 1

    def _makeSuccessResponse(self, respbody):
        doc = respbody.ownerDocument
        succRespNode = doc.createElementNS(FTSS_SOAP_NS,'ftsoap:successResponse')
        respbody.appendChild(succRespNode)

    def _makeValueResponse(self, res,respbody):
        doc = respbody.ownerDocument
        valRespNode = doc.createElementNS(FTSS_SOAP_NS,'ftsoap:valueResponse')
        valRespNode.appendChild(doc.createTextNode(str(res)))
        respbody.appendChild(valRespNode)

    def _makeResourceResponse(self, res,respbody,encodeContent):
        doc = respbody.ownerDocument
        cont = res.getContent()
        imt = res.getImt()        
        resNode = doc.createElementNS(FTSERVER_NAMESPACE,'ftss:Resource')        
        md=res.getMetaDataResource().getContent()
        mdDom = Domlette.NonvalidatingReader.parseString(md,uri=res.getAbsolutePath())
        resNode.appendChild(mdDom.documentElement)        

        if res.resourceType == ResourceTypes.ResourceType.CONTAINER:
            #print self.resource.values()
            containerNode = doc.createElementNS(FTSERVER_NAMESPACE,'ftss:Children')
            for index in xrange(len(self.resource)):
                try:
                    child = self.resource[index]
                    childRefNode = doc.createElementNS(FTSERVER_NAMESPACE,'ftss:ChildReference')
                    childMD=child.getMetaDataResource().getContent()
                    Domlette.NonvalidatingReader.parseString(childMD,uri=child.getAbsolutePath())
                    childRefNode.appendChild(
                        Domlette.NonvalidatingReader.parseString(childMD,uri=child.getAbsolutePath()).documentElement
                    )
                    containerNode.appendChild(childRefNode)
                except:
                    raise
            resNode.appendChild(containerNode)
        else:
            contentNode = doc.createElementNS(FTSERVER_NAMESPACE,'ftss:Data')
            if encodeContent:
                contentNode.appendChild(doc.createTextNode(base64.encodestring(cont)))
                contentNode.setAttributeNS(XMLSCHEMA_INSTANCE_NS,'xsi:type','xsd:base64Binary')
            else:
                contentNode.appendChild(doc.createTextNode(cont))
                contentNode.setAttributeNS(XMLSCHEMA_INSTANCE_NS,'xsi:type','xsd:string')
            resNode.appendChild(contentNode)
            
        respbody.appendChild(resNode)

    resourceNameMapping = {ResourceTypes.ResourceType.RAW_FILE:'RawFile',
                           ResourceTypes.ResourceType.XML_DOCUMENT:'XmlDocument',
                           ResourceTypes.ResourceType.CONTAINER:'Container',
                           }

g_resourceTypeMapping = {ResourceTypes.ResourceType.RAW_FILE:RawFileSoapImp,
                         ResourceTypes.ResourceType.XML_DOCUMENT:RawFileSoapImp,
                         ResourceTypes.ResourceType.CONTAINER:RawFileSoapImp,
                         }




def FetchResource(repo, reqel, respbody):
    doc = respbody.ownerDocument
    context = XPath.Context.Context(reqel)
    #context = XPath.Context.Context(reqel, processorNss={'ft': XMLSERVER_NS})
    uri = XPath.Evaluate('string(@uri)', context=context)
    try:
        res = repo.fetchResource(uri)
    except:
        GenerateFault(respbody)
        return 0
    _MakeResourceResponse(res,respbody)
    return 0


def CreateResource(repo, reqel, respbody):
    doc = respbody.ownerDocument
    context = XPath.Context.Context(reqel, processorNss={'ft': FTSS_SOAP_NS})
    cont = XPath.Evaluate('ft:Container', context=context)
    ddName = XPath.Evaluate('string(@doc-def)', context=context)

    if cont:
        context = XPath.Context.Context(cont[0], processorNss={'ft': FTSS_SOAP_NS})
        uri = XPath.Evaluate('string(@uri)', context=context)
        cp = XPath.Evaluate('number(@create-parents)', context=context)
        try:
            res = repo.createContainer(uri, createParents=cp)
        except:
            GenerateFault(respbody)
            return 0
    elif ddName:
        uri = XPath.Evaluate('string(@uri)', context=context)
        source = base64.decodestring(XPath.Evaluate('string(node())',
                                                    context=context))
        res = repo.createDocument(ddName, uri, source)
    else:
        GenerateFault(respbody)
        return 0
    _MakeResourceResponse(res,respbody)
    return 1


def DeleteResource(repo, reqel, respbody):
    doc = respbody.ownerDocument
    context = XPath.Context.Context(reqel)
    #context = XPath.Context.Context(reqel, processorNss={'ft': XMLSERVER_NS})
    uri = XPath.Evaluate('string(@uri)', context=context)
    try:
        repo.deleteResource(uri)
    except:
        GenerateFault(respbody)
        return 0

    return 1
