# Web Services controllers
"""Handles the Controller part of MVC for web services. This provides the
WebServicesController class which is the main entry point for people wanting
to use web services."""

import logging
import types
import re
import sys
import traceback

import cherrypy
import dispatch
import cElementTree as et
from genshi.builder import tag, Element

from turbogears import controllers, expose as tgexpose
from turbogears import config
from turbogears.decorator import weak_signature_decorator
from turbogears import validators

from tgwebservices import soap, iconv
from tgwebservices.runtime import register, primitives, \
                                  FunctionInfo, register_complex_type, \
                                  ctvalues

config.update({"tg.empty_flash" : False})

log = logging.getLogger("tgwebservices.controller")

def dict_wrapped_result(func):
    """Wraps a call to a function so that the return value is put into a
    single item dictionary, with the key 'result'.
    
    @param func Function to wrap
    @return a new function
    """
    def d_w_r(*args, **kw):
        return dict(result=func(*args, **kw))
    return d_w_r

_wrong_parameters = re.compile(r".*\(\) takes .* \(\d+ given\)")
_unexpected_parameters = re.compile(r".*\(\) got an unexpected keyword "
                                    "argument .*")

def wsexpose(return_type=basestring):
    """Exposes a method for access via web services. Only one value
    can be returned because many languages only support a single
    return value from methods.
    
    @param return_type the class that will be returned. This is used to
                       help statically typed clients."""
    def entangle(func):
        fi = register(func)
        fi.return_type = return_type
        
        # we'll use the TurboGears version of expose to handle JSON
        # output.
        tgfunc = tgexpose("wsautoxml", accept_format="text/xml",
                          allow_json=True)(dict_wrapped_result(func))
        
        def newfunc(self, **kw):
            if "tg_format" in kw:
                if kw["tg_format"] == "json":
                    cherrypy.request.headers["Accept"] = "text/javascript"
                else:
                    cherrypy.request.headers["Accept"] = "text/xml"
                del kw["tg_format"]
            
            # TurboJson doesn't support application/json at present, but we want
            # to accept that.
            if cherrypy.request.headers.get("Accept", None) == "application/json":
                cherrypy.request.headers["Accept"] = "text/javascript"
            
            request = cherrypy.request
            input_types = fi.input_types
            
            # the _tgws keyword is used when a web service system, such as
            # SOAP, is calling the function rather than a call that is being
            # made via direct URL traversal.
            if "_tgws" in kw:
                webservice = True
                del kw["_tgws"]
            else:
                webservice = False
            
            if "xml_body" in kw:
                kw = iconv.handle_xml_params(kw["xml_body"], input_types)
            elif request.headers.get("Content-Type", "") \
                 .startswith("text/xml"):
                try:
                    clen = int(request.headers.get('Content-Length')) or 0
                    data = request.body.read(clen)
                    body = et.fromstring(data)
                except SyntaxError:
                    raise validators.Invalid("Request XML is invalid", "", 
                                             None)
                kw = iconv.handle_xml_params(body, input_types)
            elif request.headers.get("Content-Type", "") \
                .startswith("application/json"):
                clen = int(request.headers.get('Content-Length')) or 0
                data = request.body.read(clen)
                kw = iconv.handle_json_params(data, input_types)
            else:
                iconv.handle_keyword_params(kw, input_types)
            
            try:
                if webservice:
                    return func(self, **kw)
                else:
                    # when direct traversal is used, call the standard
                    # TurboGears expose mechanism.
                    return tgfunc(self, **kw)
            except TypeError:
                # handle the case where a bogus parameter was sent in to the
                # function
                msg = str(sys.exc_info()[1])
                if (_unexpected_parameters.match(msg) or 
                    _wrong_parameters.match(msg)) and \
                    msg.startswith(func.__name__):
                    raise validators.Invalid(
                        "Unexpected parameter in function call (%s)" % msg, 
                        None, None)
                else:
                    raise
                
        newfunc.exposed = True
        newfunc.__name__ = func.__name__
        newfunc.__doc__ = func.__doc__
        newfunc._ws_func_info = fi
        # a list denotes that the user is returning an array of the type
        # given by the first element in the array
        # we need to keep track of the complex types so that they can be
        # defined as appropriate for the given protocol
        if isinstance(return_type, list):
            rt = return_type[0]
            if rt not in primitives:
                register_complex_type(fi, rt)
        elif return_type not in primitives:
            register_complex_type(fi, return_type)

        return newfunc
    return entangle

def wsvalidate(*args, **kw):
    """Validates and converts incoming parameters. Also registers the 
    parameters used by the method for use by statically typed languages.
    Method parameters can be specified via positional or keyword arguments.
    You should pass in the class used for each parameter."""
    def entangle(func):
        fi = register(func)
        input_types = dict()
        
        # match up the validators with the function parameters
        
        # the validators list doesn't include self, but the parameters list
        # does. So, the parameters list is offset by one higher.
        for i in range(0, len(args)):
            argtype = args[i]
            input_types[fi.params[i]] = argtype
        
        input_types.update(kw)
        fi.input_types = input_types
        return func
    return entangle

class WebServicesController(object):
    """A controller that implements a piece of a web services API."""
    
    def _ws_gather_functions_and_types(self, prefix):
        """Collects all of the functions and types on this controller and the
        controllers beneath it."""
        funcs = dict()
        complex_types = set()
        for key in dir(self):
            if key.startswith("_"):
                continue
            item = getattr(self, key)
            
            # functions are globally registered with Java-style
            # camelCase names
            if prefix:
                newname = prefix+key[0].upper()+key[1:]
            else:
                newname = key
            
            # check for functions and sub-controllers
            if isinstance(item, types.MethodType) and hasattr(item, "_ws_func_info"):
                funcs[newname] = item
                complex_types.update(item._ws_func_info.complex_types)
            elif hasattr(item, "_ws_gather_functions_and_types"):
                morefuncs, moretypes = \
                            item._ws_gather_functions_and_types(newname)
                funcs.update(morefuncs)
                complex_types.update(moretypes)
        return funcs, complex_types
        
    
class WebServicesRoot(WebServicesController):
    """The root of a multi-protocol web service."""
    
    def __init__(self, baseURL, tns=None, typenamespace=None):
        """Constructor for a WebServicesRoot controller.
        
        @param baseURL Sets the URL path for this controller. Some protocols
            need to know this.
        @param tns The SOAP target namespace (defaults to baseURL+soap/)
        @param typenamespace the namespace for the SOAP types (defaults to
            baseURL+soap/types)
        """
        
        if not tns:
            tns = baseURL + "soap/"
        if not typenamespace:
            typenamespace = tns + "types"
        self._ws_baseURL = baseURL
        self._ws_parent = None
        self.soap = soap.SoapController(self, tns, typenamespace)
        self._ws_funcs, self._ws_complex_types = \
            self._ws_gather_functions_and_types("")
        
    def _cp_on_error(self):
        excinfo = sys.exc_info()
        cherrypy.response.body = self._formatexception(excinfo)
    
    @tgexpose("wsautoxml", allow_json=True, accept_format="text/xml")
    def _formatexception(self, excinfo):
        if isinstance(excinfo[1], validators.Invalid):
            return dict(faultcode="Client",
                        faultstring=str(excinfo[1]))
        else:
            return dict(faultcode="Server",
                        faultstring=str(excinfo[1]),
                        debuginfo="\nTraceback:\n%s\n" % 
                            "\n".join(traceback.format_exception(
                                                    *excinfo)))
    
