JSONRPCServerMiddleware: jsonrpcserver.py

File jsonrpcserver.py, 9.2 KB (added by alx3, 15 years ago)
Line 
1"""
2This Django middleware suits for building JSON-RPC servers.
3Configuration:
4    1) put class 'jsonrpcserver.JSONRPCServerMiddleware' to Django middleware list in settings.py
5    2) add 'jsonrpc_urlpatterns' to 'urls.py' with 'urlpatterns' syntax
6        example:
7            jsonrpc_urlpatterns = patterns('',
8                (r'^myapp/json_rpc_service/$', 'myapp.newapp.json_rpc_views'),
9                (r'^myapp/json_rpc_geoservice/$', 'myapp.geo.json_rpc_views'),
10            )
11        note: 'myapp.newapp.json_rpc_views' IS NOT A FUNCTION, BUT A MODULE, that
12        contains exposed JSON-RPC functions.
13    3) optional Django settings:
14        1. JSONRPC_URLPATTERNS_NAME:
15            Default value: 'jsonrpc_urlpatterns'.
16            Name of the variable in urls.py,that contains JSON-RPC URL patterns.
17            URL pattern must have Django syntax, but they must contain
18            MODULES NAMES, NOT FUNCTION NAMES as second element in tuple for patterns function.
19        2. JSONRPC_SERIALIZER:
20            Default value: JSONSerializer class - thin wrapper over django.utils.simplejson.
21            Name of the JSON serializer class, that must have methods
22            'serialize' and 'deserialize'
23        3. JSONRPC_JSONFY_AT_ALL_COSTS:
24            Default value: False.
25            Boolean flag, that determines whether JSON encoder should throwing an error or
26            returning a str() representation of unsupportable python object.
27            See implementation of AugmentedJSONEncoder class.
28Use:
29    1) write exposed functions:
30        from jsonrpcserver import jsonrpc_function
31        @jsonrpc_function
32        def my_python_method(par1, par2):
33            return par1 + par2
34        ...
35    2) use from JS client (e.g. dojo):
36        var  service = new dojo.rpc.JsonService({
37            "serviceType": "JSON-RPC",
38            "serviceURL": "/myapp/json_rpc_service/"
39        });
40        service.callRemote("my_python_method", ["string1", "string2"])
41            .addCallback(...);
42Specifications (json-rpc.org):
43    input: '{"method":"my_python_method", "params": ["hello ", "world"], "id": 1}'
44    output: '{"error": null, "result": "hello world", "id": 1}'
45
46Written by alx3 (alx3apps(at)gmail(dot)com).
47Inspired by Java Struts2 JSON-plugin by Musachy Barroso and
48SimpleJSONRPCServer by David McNab.
49You can use this code under the terms of BSD licence (like Django project).
50"""
51
52import sys
53import traceback
54import django
55from django.http import HttpResponse
56import django.utils.simplejson as json
57
58
59_dispatchers_dict = {}
60
61def _import_module(mod_name):
62    """
63    Importing module through __import__ function
64    """
65    module =  __import__(mod_name)
66    components = mod_name.split('.')
67    for comp in components[1:]:
68        module = getattr(module, comp)
69    return module
70
71def _import_class(class_path):
72    """
73    Importing class through __import__ function
74    """
75    path_list = class_path.split(".")
76    module_path = ".".join(path_list[:-1])
77    class_name = path_list[-1]
78    module = _import_module(module_path)
79    return getattr(module, class_name)
80
81class AugmentedJSONEncoder(json.JSONEncoder):
82    """
83    Augmentation for simplejson encoder.
84    Now additionally encodes arbitrary iterables, class instances and decimals.
85    """
86#   switch this flag to True in production   
87    if hasattr(django.conf.settings, "JSONRPC_JSONFY_AT_ALL_COSTS"):
88        _jsonfy_at_all_costs_flag = django.conf.settings.JSONRPC_JSONFY_AT_ALL_COSTS;
89    else:
90        _jsonfy_at_all_costs_flag = False;
91
92    def default(self, o):
93        if(hasattr(o, "__iter__")):
94            iterable = iter(o)
95            return list(iterable)
96        elif(hasattr(o, "__add__") and hasattr(o, "__sub__") and hasattr(o, "__mul__")):
97            return float(o)
98        elif(hasattr(o, "__class__")):
99            return o.__dict__
100        else:
101            if _jsonfy_at_all_costs_flag:
102                return str(o)
103            else:
104#               JSON exception raised here               
105                return json.JSONEncoder.default(self, o)
106
107class JSONSerializer:
108    """
109    JSON encoder/decoder wrapper
110    """
111    def serialize(self, obj):
112        return AugmentedJSONEncoder().encode(obj)
113    def deserialize(self, string):
114        return json.JSONDecoder().decode(string)
115
116
117class FunctionDispatcher(object):
118    """
119    Simple function dispatcher.
120    Dispatch syntax:
121    result = disp.dispatch("my_fun", *["param1", param2])
122    """
123    def __init__(self):
124        self.func_dict = {}
125
126    def register_function(self, func, func_name=None):
127        if not func_name:
128            func_name = func.func_name
129        #prevent overriding existing function
130        if(self.func_dict.has_key(func_name)):
131            raise Exception("Function '%s' already registered" % func_name)
132        self.func_dict[func_name] = func
133
134    def has_function(self, func_name):
135        return func_name in self.func_dict
136
137    def dispatch(self, func_name, *args, **kwargs):
138        if func_name in self.func_dict:
139            sought_func = self.func_dict[func_name]
140        else:
141            raise Exception("Function '%s' doesn't exist" % func_name)
142        response = sought_func(*args, **kwargs)
143        return response
144
145def jsonrpc_function(func):
146    """
147    Decorator for JSON-RPC method.
148    Server use:
149        @jsonrpc_function
150        def my_python_method(s1, s2):
151            return s1 + s2;
152    Client use (dojo):
153        rpcService.callRemote("my_python_method", ["string1", "string2"])
154            .addCallback(...);
155    """
156    if not _dispatchers_dict.has_key(func.__module__):
157        _dispatchers_dict[func.__module__] = FunctionDispatcher()
158    dispatcher = _dispatchers_dict[func.__module__]
159    if not dispatcher.has_function(func.func_name):
160        dispatcher.register_function(func)
161    return func
162
163class JSONRPCServerMiddleware(object):
164    """
165    Django middleware class. Put it Django middleware list in settings.py.
166    """
167    #searching for JSON-RPC urlpatterns
168    urls_mod_name = django.conf.settings.ROOT_URLCONF
169    urls_module = _import_module(urls_mod_name)
170    if hasattr(django.conf.settings, "JSONRPC_URLPATTERNS_NAME"):
171        urlpatterns_name = django.conf.settings.JSONRPC_URLPATTERNS_NAME
172    else:
173        urlpatterns_name = "jsonrpc_urlpatterns" 
174    jsonrpc_urlpatterns = getattr(urls_module, urlpatterns_name)
175
176    def __init__(self):
177        """
178        Importing all modules, listed in jsonrpc_urlpatterns in urls.py.
179        Initialization needs for fill dispatchers with funcitons.
180        """
181        for pattern in self.jsonrpc_urlpatterns:
182            #django hack here, see django.core.urlresolvers.RegexUrlPattern class
183            module_name = pattern._callback_str
184            __import__(module_name)
185        #initializing serializer
186        if hasattr(django.conf.settings, "JSONRPC_SERIALIZER"):
187            serializer_class = _import_class(django.conf.settings.JSONRPC_SERIALIZER)
188            self.serializer = serializer_class()
189        else:
190            self.serializer = JSONSerializer()
191
192
193    def process_request(self, request):
194        """
195        Preprocesses all POST request to find out whether its remote call.
196        Processes remote call and returns HttpResponse as result
197        """
198        if(request.method == "GET"):
199            return
200        for pattern in self.jsonrpc_urlpatterns:
201            match = pattern.regex.search(request.path[1:])
202            if match:
203                # If there are any named groups, use those as kwargs, ignoring
204                # non-named groups. Otherwise, pass all non-named arguments as
205                # positional arguments.
206                kwargs = match.groupdict()
207                if kwargs:
208                    args = ()
209                else:
210                    args = match.groups()
211                # In both cases, pass any extra_kwargs as **kwargs.
212                kwargs.update(pattern.default_args)
213                result_str = self._dispatch_rpc_call(pattern._callback_str, request.raw_post_data, args, kwargs)
214                return HttpResponse(result_str)
215
216    def _dispatch_rpc_call(self, module_name, raw_post_data, args, kwargs):
217        response_dict = {}
218        try:
219            if module_name in _dispatchers_dict:
220                dispatcher = _dispatchers_dict[module_name]
221            else:
222                raise Exception("Module '%s' doesn't have JSON-RPC functions" % module_name)
223
224            call_data = self.serializer.deserialize(raw_post_data)
225            call_id = call_data.get("id", None)
226            if call_id:
227                response_dict["id"] = call_id
228            else:
229                #following JSON-RPC spec, it's a notification, not a request
230                return ""
231            func_name = str(call_data["method"])
232            func_params = list(call_data["params"])
233
234            args_list = list(args)
235            args_list.extend(func_params)
236            result = dispatcher.dispatch(func_name, *args_list, **kwargs)
237            response_dict['result'] = result
238            response_dict['error'] = None
239        except Exception, e:
240            error_dict = {
241                    "name": str(sys.exc_info()[0]),
242                    "message": str(e),
243                    "stack": traceback.format_exc()
244            }
245            response_dict['error'] = error_dict
246            response_dict['result'] = None
247        return self.serializer.serialize(response_dict)
248
249
Back to Top