Ticket #2251: jsonsupport.patch

File jsonsupport.patch, 14.2 kB (added by okamototk, 1 year ago)

Patch to support JSON

  • tracrpc/json.py

    old new  
     1import string 
     2import types 
     3import datetime 
     4import calendar 
     5 
     6##    json.py implements a JSON (http://json.org) reader and writer. 
     7##    Copyright (C) 2005  Patrick D. Logan 
     8##    Contact mailto:patrickdlogan@stardecisions.com 
     9## 
     10##    This library is free software; you can redistribute it and/or 
     11##    modify it under the terms of the GNU Lesser General Public 
     12##    License as published by the Free Software Foundation; either 
     13##    version 2.1 of the License, or (at your option) any later version. 
     14## 
     15##    This library is distributed in the hope that it will be useful, 
     16##    but WITHOUT ANY WARRANTY; without even the implied warranty of 
     17##    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU 
     18##    Lesser General Public License for more details. 
     19## 
     20##    You should have received a copy of the GNU Lesser General Public 
     21##    License along with this library; if not, write to the Free Software 
     22##    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA 
     23 
     24 
     25class _StringGenerator(object): 
     26        def __init__(self, string): 
     27                self.string = string 
     28                self.index = -1 
     29        def peek(self): 
     30                i = self.index + 1 
     31                if i < len(self.string): 
     32                        return self.string[i] 
     33                else: 
     34                        return None 
     35        def next(self): 
     36                self.index += 1 
     37                if self.index < len(self.string): 
     38                        return self.string[self.index] 
     39                else: 
     40                        raise StopIteration 
     41        def all(self): 
     42                return self.string 
     43 
     44class WriteException(Exception): 
     45    pass 
     46 
     47class ReadException(Exception): 
     48    pass 
     49 
     50class JsonReader(object): 
     51    hex_digits = {'A': 10,'B': 11,'C': 12,'D': 13,'E': 14,'F':15} 
     52    escapes = {'t':'\t','n':'\n','f':'\f','r':'\r','b':'\b'} 
     53 
     54    def read(self, s): 
     55        self._generator = _StringGenerator(s) 
     56        result = self._read() 
     57        return result 
     58 
     59    def _read(self): 
     60        self._eatWhitespace() 
     61        peek = self._peek() 
     62        if peek is None: 
     63            raise ReadException, "Nothing to read: '%s'" % self._generator.all() 
     64        if peek == '{': 
     65            return self._readObject() 
     66        elif peek == '[': 
     67            return self._readArray()             
     68        elif peek == '"': 
     69            return self._readString() 
     70        elif peek == '-' or peek.isdigit(): 
     71            return self._readNumber() 
     72        elif peek == 't': 
     73            return self._readTrue() 
     74        elif peek == 'f': 
     75            return self._readFalse() 
     76        elif peek == 'n': 
     77            return self._readNull() 
     78        elif peek == '/': 
     79            self._readComment() 
     80            return self._read() 
     81        else: 
     82            raise ReadException, "Input is not valid JSON: '%s'" % self._generator.all() 
     83 
     84    def _readTrue(self): 
     85        self._assertNext('t', "true") 
     86        self._assertNext('r', "true") 
     87        self._assertNext('u', "true") 
     88        self._assertNext('e', "true") 
     89        return True 
     90 
     91    def _readFalse(self): 
     92        self._assertNext('f', "false") 
     93        self._assertNext('a', "false") 
     94        self._assertNext('l', "false") 
     95        self._assertNext('s', "false") 
     96        self._assertNext('e', "false") 
     97        return False 
     98 
     99    def _readNull(self): 
     100        self._assertNext('n', "null") 
     101        self._assertNext('u', "null") 
     102        self._assertNext('l', "null") 
     103        self._assertNext('l', "null") 
     104        return None 
     105 
     106    def _assertNext(self, ch, target): 
     107        if self._next() != ch: 
     108            raise ReadException, "Trying to read %s: '%s'" % (target, self._generator.all()) 
     109 
     110    def _readNumber(self): 
     111        isfloat = False 
     112        result = self._next() 
     113        peek = self._peek() 
     114        while peek is not None and (peek.isdigit() or peek == "."): 
     115            isfloat = isfloat or peek == "." 
     116            result = result + self._next() 
     117            peek = self._peek() 
     118        try: 
     119            if isfloat: 
     120                return float(result) 
     121            else: 
     122                return int(result) 
     123        except ValueError: 
     124            raise ReadException, "Not a valid JSON number: '%s'" % result 
     125 
     126    def _readString(self): 
     127        result = "" 
     128        assert self._next() == '"' 
     129        try: 
     130            while self._peek() != '"': 
     131                ch = self._next() 
     132                if ch == "\\": 
     133                    ch = self._next() 
     134                    if ch in 'brnft': 
     135                        ch = self.escapes[ch] 
     136                    elif ch == "u": 
     137                        ch4096 = self._next() 
     138                        ch256  = self._next() 
     139                        ch16   = self._next() 
     140                        ch1    = self._next() 
     141                        n = 4096 * self._hexDigitToInt(ch4096) 
     142                        n += 256 * self._hexDigitToInt(ch256) 
     143                        n += 16  * self._hexDigitToInt(ch16) 
     144                        n += self._hexDigitToInt(ch1) 
     145                        ch = unichr(n) 
     146                    elif ch not in '"/\\': 
     147                        raise ReadException, "Not a valid escaped JSON character: '%s' in %s" % (ch, self._generator.all()) 
     148                result = result + ch 
     149        except StopIteration: 
     150            raise ReadException, "Not a valid JSON string: '%s'" % self._generator.all() 
     151        assert self._next() == '"' 
     152        return result 
     153 
     154    def _hexDigitToInt(self, ch): 
     155        try: 
     156            result = self.hex_digits[ch.upper()] 
     157        except KeyError: 
     158            try: 
     159                result = int(ch) 
     160            except ValueError: 
     161                 raise ReadException, "The character %s is not a hex digit." % ch 
     162        return result 
     163 
     164    def _readComment(self): 
     165        assert self._next() == "/" 
     166        second = self._next() 
     167        if second == "/": 
     168            self._readDoubleSolidusComment() 
     169        elif second == '*': 
     170            self._readCStyleComment() 
     171        else: 
     172            raise ReadException, "Not a valid JSON comment: %s" % self._generator.all() 
     173 
     174    def _readCStyleComment(self): 
     175        try: 
     176            done = False 
     177            while not done: 
     178                ch = self._next() 
     179                done = (ch == "*" and self._peek() == "/") 
     180                if not done and ch == "/" and self._peek() == "*": 
     181                    raise ReadException, "Not a valid JSON comment: %s, '/*' cannot be embedded in the comment." % self._generator.all() 
     182            self._next() 
     183        except StopIteration: 
     184            raise ReadException, "Not a valid JSON comment: %s, expected */" % self._generator.all() 
     185 
     186    def _readDoubleSolidusComment(self): 
     187        try: 
     188            ch = self._next() 
     189            while ch != "\r" and ch != "\n": 
     190                ch = self._next() 
     191        except StopIteration: 
     192            pass 
     193 
     194    def _readArray(self): 
     195        result = [] 
     196        assert self._next() == '[' 
     197        done = self._peek() == ']' 
     198        while not done: 
     199            item = self._read() 
     200            result.append(item) 
     201            self._eatWhitespace() 
     202            done = self._peek() == ']' 
     203            if not done: 
     204                ch = self._next() 
     205                if ch != ",": 
     206                    raise ReadException, "Not a valid JSON array: '%s' due to: '%s'" % (self._generator.all(), ch) 
     207        assert ']' == self._next() 
     208        return result 
     209 
     210    def _readObject(self): 
     211        result = {} 
     212        assert self._next() == '{' 
     213        done = self._peek() == '}' 
     214        while not done: 
     215            key = self._read() 
     216            if type(key) is not types.StringType: 
     217                raise ReadException, "Not a valid JSON object key (should be a string): %s" % key 
     218            self._eatWhitespace() 
     219            ch = self._next() 
     220            if ch != ":": 
     221                raise ReadException, "Not a valid JSON object: '%s' due to: '%s'" % (self._generator.all(), ch) 
     222            self._eatWhitespace() 
     223            val = self._read() 
     224            result[key] = val 
     225            self._eatWhitespace() 
     226            done = self._peek() == '}' 
     227            if not done: 
     228                ch = self._next() 
     229                if ch != ",": 
     230                    raise ReadException, "Not a valid JSON array: '%s' due to: '%s'" % (self._generator.all(), ch) 
     231        assert self._next() == "}" 
     232        return result 
     233 
     234    def _eatWhitespace(self): 
     235        p = self._peek() 
     236        while p is not None and p in string.whitespace or p == '/': 
     237            if p == '/': 
     238                self._readComment() 
     239            else: 
     240                self._next() 
     241            p = self._peek() 
     242 
     243    def _peek(self): 
     244        return self._generator.peek() 
     245 
     246    def _next(self): 
     247        return self._generator.next() 
     248 
     249class JsonWriter(object): 
     250         
     251    def _append(self, s): 
     252        self._results.append(s) 
     253 
     254    def write(self, obj, escaped_forward_slash=False): 
     255        self._escaped_forward_slash = escaped_forward_slash 
     256        self._results = [] 
     257        self._write(obj) 
     258        return "".join(self._results) 
     259 
     260    def _write(self, obj): 
     261        ty = type(obj) 
     262        if ty is types.DictType: 
     263            n = len(obj) 
     264            self._append("{") 
     265            for k, v in obj.items(): 
     266                self._write(k) 
     267                self._append(":") 
     268                self._write(v) 
     269                n = n - 1 
     270                if n > 0: 
     271                    self._append(",") 
     272            self._append("}") 
     273        elif ty is types.ListType or ty is types.TupleType: 
     274            n = len(obj) 
     275            self._append("[") 
     276            for item in obj: 
     277                self._write(item) 
     278                n = n - 1 
     279                if n > 0: 
     280                    self._append(",") 
     281            self._append("]") 
     282        elif ty is types.StringType or ty is types.UnicodeType: 
     283            self._append('"') 
     284            obj = obj.replace('\\', r'\\') 
     285            if self._escaped_forward_slash: 
     286                obj = obj.replace('/', r'\/') 
     287            obj = obj.replace('"', r'\"') 
     288            obj = obj.replace('\b', r'\b') 
     289            obj = obj.replace('\f', r'\f') 
     290            obj = obj.replace('\n', r'\n') 
     291            obj = obj.replace('\r', r'\r') 
     292            obj = obj.replace('\t', r'\t') 
     293            self._append(obj) 
     294            self._append('"') 
     295        elif ty is types.IntType or ty is types.LongType: 
     296            self._append(str(obj)) 
     297        elif ty is types.FloatType: 
     298            self._append("%f" % obj) 
     299        elif obj is True: 
     300            self._append("true") 
     301        elif obj is False: 
     302            self._append("false") 
     303        elif obj is None: 
     304            self._append("null") 
     305        elif obj.__class__ == datetime.datetime: 
     306            self._append(str(calendar.timegm(obj.timetuple()))) 
     307        else: 
     308            raise WriteException, "Cannot write in JSON: %s" % repr(obj) 
     309 
     310def write(obj, escaped_forward_slash=False): 
     311    return JsonWriter().write(obj, escaped_forward_slash) 
     312 
     313def read(s): 
     314    return JsonReader().read(s) 
  • tracrpc/web_ui.py

    old new  
    66from trac.web.chrome import ITemplateProvider, add_stylesheet 
    77from tracrpc.api import IXMLRPCHandler, XMLRPCSystem 
    88from trac.wiki.formatter import wiki_to_oneliner 
     9from tracrpc import json 
    910 
    1011class XMLRPCWeb(Component): 
    1112    """ Handle XML-RPC calls from HTTP clients, as well as presenting a list of 
    1213        methods available to the currently logged in user. Browsing to 
    1314        <trac>/xmlrpc or <trac>/login/xmlrpc will display this list. """ 
    1415 
    15     content_type_re = re.compile(r'(text|application)/xml') 
    16  
     16    content_type_re = re.compile(r'(text/xml|application/xml|application/json)') 
     17    jsonrpc_path_re = re.compile(r'/(login/|)json/([\w\.]+)$') 
    1718    implements(IRequestHandler, ITemplateProvider) 
    1819 
    1920    # IRequestHandler methods 
    2021    def match_request(self, req): 
    21         return req.path_info in ('/login/xmlrpc', '/xmlrpc') 
     22        if req.path_info in ('/login/xmlrpc', '/xmlrpc', '/login/json', '/json'): 
     23            self.log.debug("%s matches %s" % (req.path_info, True)) 
     24            return True 
     25        if self.jsonrpc_path_re.match(req.path_info): 
     26            self.log.debug("%s matches %s" % (req.path_info, True)) 
     27            return True 
     28        self.log.debug("%s matches %s" % (req.path_info, False)) 
     29        return False 
    2230 
    2331    def _send_response(self, req, response, content_type='application/xml'): 
    2432        req.send_response(200) 
    2533        req.send_header('Content-Type', content_type) 
    26         req.send_header('Content-Length', len(response)) 
     34        req.send_header('Content-Length', len(response.encode("utf-8"))) 
    2735        req.end_headers() 
    2836        req.write(response) 
    2937 
     
    5563            return ('xmlrpclist.html', {'xmlrpc': {'functions': namespaces}}, None) 
    5664 
    5765        # Handle XML-RPC call 
    58         args, method = xmlrpclib.loads(req.read(int(req.get_header('Content-Length')))) 
     66        isjson = False 
    5967        try: 
    60             result = XMLRPCSystem(self.env).get_method(method)(req, args) 
    61             self._send_response(req, xmlrpclib.dumps(result, methodresponse=True), content_type) 
     68            if content_type.startswith('application/json'): 
     69                isjson = True 
     70                method = self.jsonrpc_path_re.match(req.path_info).group(2) 
     71                args = json.read(req.read(int(req.get_header('Content-Length')))) 
     72                result = XMLRPCSystem(self.env).get_method(method)(req, args) 
     73                self._send_response(req, json.write(result[0]).encode('utf-8'), content_type) 
     74            else: 
     75                args, method = xmlrpclib.loads(req.read(int(req.get_header('Content-Length')))) 
     76                result = XMLRPCSystem(self.env).get_method(method)(req, args) 
     77                self._send_response(req, xmlrpclib.dumps(result, methodresponse=True), content_type) 
    6278        except xmlrpclib.Fault, e: 
    6379            self.log.error(e) 
    6480            self._send_response(req, xmlrpclib.dumps(e), content_type) 
     
    6985            out = StringIO() 
    7086            traceback.print_exc(file = out) 
    7187            self.log.error(out.getvalue()) 
    72             self._send_response(req, xmlrpclib.dumps(xmlrpclib.Fault(2, "'%s' while executing '%s()'" % (str(e), method)))) 
     88            if isjson == True: 
     89                self._send_response(req, json.write("'%s' while executing '%s()'" % (str(e), method))) 
     90            else: 
     91                self._send_response(req, xmlrpclib.dumps(xmlrpclib.Fault(2, "'%s' while executing '%s()'" % (str(e), method)))) 
    7392 
    7493    # ITemplateProvider 
    7594    def get_htdocs_dirs(self):