Backup-Restore support library + tests
[integration/test.git] / csit / libraries / backuprestore / jsonpathl.py
1 """
2 An XPath for JSON
3
4 A port of the Perl, and JavaScript versions of JSONPath
5 see http://goessner.net/articles/JsonPath/
6
7 Based on on JavaScript version by Stefan Goessner at:
8         http://code.google.com/p/jsonpath/
9 and Perl version by Kate Rhodes at:
10         http://github.com/masukomi/jsonpath-perl/tree/master
11 """
12
13 import re
14 import sys
15
16 __author__ = "Phil Budne"
17 __revision__ = "$Revision: 1.13 $"
18 __version__ = '0.54'
19
20 #   Copyright (c) 2007 Stefan Goessner (goessner.net)
21 #       Copyright (c) 2008 Kate Rhodes (masukomi.org)
22 #       Copyright (c) 2008-2012 Philip Budne (ultimate.com)
23 #   Licensed under the MIT licence:
24 #
25 #   Permission is hereby granted, free of charge, to any person
26 #   obtaining a copy of this software and associated documentation
27 #   files (the "Software"), to deal in the Software without
28 #   restriction, including without limitation the rights to use,
29 #   copy, modify, merge, publish, distribute, sublicense, and/or sell
30 #   copies of the Software, and to permit persons to whom the
31 #   Software is furnished to do so, subject to the following
32 #   conditions:
33 #
34 #   The above copyright notice and this permission notice shall be
35 #   included in all copies or substantial portions of the Software.
36 #
37 #   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
38 #   EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
39 #   OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
40 #   NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
41 #   HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
42 #   WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
43 #   FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
44 #   OTHER DEALINGS IN THE SOFTWARE.
45
46 # XXX BUGS:
47 # evalx is generally a crock:
48 #       handle !@.name.name???
49 # there are probably myriad unexpected ways to get an exception:
50 #       wrap initial "trace" call in jsonpath body in a try/except??
51
52 # XXX TODO:
53 # internally keep paths as lists to preserve integer types
54 #       (instead of as ';' delimited strings)
55
56 __all__ = ['jsonpath']
57
58
59 # XXX precompile RE objects on load???
60 # re_1 = re.compile(.....)
61 # re_2 = re.compile(.....)
62
63 def normalize(x):
64     """normalize the path expression; outside jsonpath to allow testing"""
65     subx = []
66
67     # replace index/filter expressions with placeholders
68     # Python anonymous functions (lambdas) are cryptic, hard to debug
69     def f1(m):
70         n = len(subx)  # before append
71         g1 = m.group(1)
72         subx.append(g1)
73         ret = "[#%d]" % n
74         #       print "f1:", g1, ret
75         return ret
76
77     x = re.sub(r"[\['](\??\(.*?\))[\]']", f1, x)
78
79     # added the negative lookbehind -krhodes
80     x = re.sub(r"'?(?<!@)\.'?|\['?", ";", x)
81
82     x = re.sub(r";;;|;;", ";..;", x)
83
84     x = re.sub(r";$|'?\]|'$", "", x)
85
86     # put expressions back
87     def f2(m):
88         g1 = m.group(1)
89         #       print "f2:", g1
90         return subx[int(g1)]
91
92     x = re.sub(r"#([0-9]+)", f2, x)
93
94     return x
95
96
97 def jsonpath(obj, expr, result_type='VALUE', debug=0, use_eval=True):
98     """traverse JSON object using jsonpath expr, returning values or paths"""
99
100     def s(x, y):
101         """concatenate path elements"""
102         return str(x) + ';' + str(y)
103
104     def isint(x):
105         """check if argument represents a decimal integer"""
106         return x.isdigit()
107
108     def as_path(path):
109         """convert internal path representation to
110            "full bracket notation" for PATH output"""
111         p = '$'
112         for piece in path.split(';')[1:]:
113             # make a guess on how to index
114             # XXX need to apply \ quoting on '!!
115             if isint(piece):
116                 p += "[%s]" % piece
117             else:
118                 p += "['%s']" % piece
119         return p
120
121     def store(path, object):
122         if result_type == 'VALUE':
123             result.append(object)
124         elif result_type == 'IPATH':  # Index format path (Python ext)
125             # return list of list of indices -- can be used w/o "eval" or split
126             result.append(path.split(';')[1:])
127         else:  # PATH
128             result.append(as_path(path))
129         return path
130
131     def trace(expr, obj, path):
132         if debug:
133             print "trace", expr, "/", path
134         if expr:
135             x = expr.split(';')
136             loc = x[0]
137             x = ';'.join(x[1:])
138             if debug:
139                 print "\t", loc, type(obj)
140             if loc == "*":
141                 def f03(key, loc, expr, obj, path):
142                     if debug > 1:
143                         print "\tf03", key, loc, expr, path
144                     trace(s(key, expr), obj, path)
145
146                 walk(loc, x, obj, path, f03)
147             elif loc == "..":
148                 trace(x, obj, path)
149
150                 def f04(key, loc, expr, obj, path):
151                     if debug > 1:
152                         print "\tf04", key, loc, expr, path
153                     if isinstance(obj, dict):
154                         if key in obj:
155                             trace(s('..', expr), obj[key], s(path, key))
156                     else:
157                         if key < len(obj):
158                             trace(s('..', expr), obj[key], s(path, key))
159
160                 walk(loc, x, obj, path, f04)
161             elif loc == "!":
162                 # Perl jsonpath extension: return keys
163                 def f06(key, loc, expr, obj, path):
164                     if isinstance(obj, dict):
165                         trace(expr, key, path)
166
167                 walk(loc, x, obj, path, f06)
168             elif isinstance(obj, dict) and loc in obj:
169                 trace(x, obj[loc], s(path, loc))
170             elif isinstance(obj, list) and isint(loc):
171                 iloc = int(loc)
172                 if len(obj) >= iloc:
173                     trace(x, obj[iloc], s(path, loc))
174             else:
175                 # [(index_expression)]
176                 if loc.startswith("(") and loc.endswith(")"):
177                     if debug > 1:
178                         print "index", loc
179                     e = evalx(loc, obj)
180                     trace(s(e, x), obj, path)
181                     return
182
183                 # ?(filter_expression)
184                 if loc.startswith("?(") and loc.endswith(")"):
185                     if debug > 1:
186                         print "filter", loc
187
188                     def f05(key, loc, expr, obj, path):
189                         if debug > 1:
190                             print "f05", key, loc, expr, path
191                         if isinstance(obj, dict):
192                             eval_result = evalx(loc, obj[key])
193                         else:
194                             eval_result = evalx(loc, obj[int(key)])
195                         if eval_result:
196                             trace(s(key, expr), obj, path)
197
198                     loc = loc[2:-1]
199                     walk(loc, x, obj, path, f05)
200                     return
201
202                 m = re.match(r'(-?[0-9]*):(-?[0-9]*):?(-?[0-9]*)$', loc)
203                 if m:
204                     if isinstance(obj, (dict, list)):
205                         def max(x, y):
206                             if x > y:
207                                 return x
208                             return y
209
210                         def min(x, y):
211                             if x < y:
212                                 return x
213                             return y
214
215                         objlen = len(obj)
216                         s0 = m.group(1)
217                         s1 = m.group(2)
218                         s2 = m.group(3)
219
220                         # XXX int("badstr") raises exception
221                         start = int(s0) if s0 else 0
222                         end = int(s1) if s1 else objlen
223                         step = int(s2) if s2 else 1
224
225                         if start < 0:
226                             start = max(0, start + objlen)
227                         else:
228                             start = min(objlen, start)
229                         if end < 0:
230                             end = max(0, end + objlen)
231                         else:
232                             end = min(objlen, end)
233
234                         for i in xrange(start, end, step):
235                             trace(s(i, x), obj, path)
236                     return
237
238                 # after (expr) & ?(expr)
239                 if loc.find(",") >= 0:
240                     # [index,index....]
241                     for piece in re.split(r"'?,'?", loc):
242                         if debug > 1:
243                             print "piece", piece
244                         trace(s(piece, x), obj, path)
245         else:
246             store(path, obj)
247
248     def walk(loc, expr, obj, path, funct):
249         if isinstance(obj, list):
250             for i in xrange(0, len(obj)):
251                 funct(i, loc, expr, obj, path)
252         elif isinstance(obj, dict):
253             for key in obj:
254                 funct(key, loc, expr, obj, path)
255
256     def evalx(loc, obj):
257         """eval expression"""
258
259         if debug:
260             print "evalx", loc
261
262         # a nod to JavaScript. doesn't work for @.name.name.length
263         # Write len(@.name.name) instead!!!
264         loc = loc.replace("@.length", "len(__obj)")
265
266         loc = loc.replace("&&", " and ").replace("||", " or ")
267
268         # replace !@.name with 'name' not in obj
269         # XXX handle !@.name.name.name....
270         def notvar(m):
271             return "'%s' not in __obj" % m.group(1)
272
273         loc = re.sub("!@\.([a-zA-Z@_]+)", notvar, loc)
274
275         # replace @.name.... with __obj['name']....
276         # handle @.name[.name...].length
277         def varmatch(m):
278             def brackets(elts):
279                 ret = "__obj"
280                 for e in elts:
281                     if isint(e):
282                         ret += "[%s]" % e  # ain't necessarily so
283                     else:
284                         ret += "['%s']" % e  # XXX beware quotes!!!!
285                 return ret
286
287             g1 = m.group(1)
288             elts = g1.split('.')
289             if elts[-1] == "length":
290                 return "len(%s)" % brackets(elts[1:-1])
291             return brackets(elts[1:])
292
293         loc = re.sub(r'(?<!\\)(@\.[a-zA-Z@_.]+)', varmatch, loc)
294
295         # removed = -> == translation
296         # causes problems if a string contains =
297
298         # replace @  w/ "__obj", but \@ means a literal @
299         loc = re.sub(r'(?<!\\)@', "__obj", loc).replace(r'\@', '@')
300         if not use_eval:
301             if debug:
302                 print "eval disabled"
303             raise Exception("eval disabled")
304         if debug:
305             print "eval", loc
306         try:
307             # eval w/ caller globals, w/ local "__obj"!
308             v = eval(loc, caller_globals, {'__obj': obj})
309         except Exception, e:
310             if debug:
311                 print e
312             return False
313
314         if debug:
315             print "->", v
316         return v
317
318     # body of jsonpath()
319
320     # Get caller globals so eval can pick up user functions!!!
321     caller_globals = sys._getframe(1).f_globals
322     result = []
323     if expr and obj:
324         cleaned_expr = normalize(expr)
325         if cleaned_expr.startswith("$;"):
326             cleaned_expr = cleaned_expr[2:]
327
328         # XXX wrap this in a try??
329         trace(cleaned_expr, obj, '$')
330
331         if len(result) > 0:
332             return result
333     return False
334
335
336 if __name__ == '__main__':
337     try:
338         import json  # v2.6
339     except ImportError:
340         import simplejson as json
341
342     import sys
343
344     # XXX take options for output format, output file, debug level
345
346     if len(sys.argv) < 3 or len(sys.argv) > 4:
347         sys.stdout.write("Usage: jsonpath.py FILE PATH [OUTPUT_TYPE]\n")
348         sys.exit(1)
349
350     object = json.load(file(sys.argv[1]))
351     path = sys.argv[2]
352     format = 'VALUE'
353
354     if len(sys.argv) > 3:
355         # XXX verify?
356         format = sys.argv[3]
357
358     value = jsonpath(object, path, format)
359
360     if not value:
361         sys.exit(1)
362
363     f = sys.stdout
364     json.dump(value, f, sort_keys=True, indent=1)
365     f.write("\n")
366
367     sys.exit(0)