Support OVSDB entity name format in Phosphorus
[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
64 def normalize(x):
65     """normalize the path expression; outside jsonpath to allow testing"""
66     subx = []
67
68     # replace index/filter expressions with placeholders
69     # Python anonymous functions (lambdas) are cryptic, hard to debug
70     def f1(m):
71         n = len(subx)  # before append
72         g1 = m.group(1)
73         subx.append(g1)
74         ret = "[#%d]" % n
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         return subx[int(g1)]
90
91     x = re.sub(r"#([0-9]+)", f2, x)
92
93     return x
94
95
96 def jsonpath(obj, expr, result_type="VALUE", debug=0, use_eval=True):
97     """traverse JSON object using jsonpath expr, returning values or paths"""
98
99     def s(x, y):
100         """concatenate path elements"""
101         return str(x) + ";" + str(y)
102
103     def isint(x):
104         """check if argument represents a decimal integer"""
105         return x.isdigit()
106
107     def as_path(path):
108         """convert internal path representation to
109         "full bracket notation" for PATH output"""
110         p = "$"
111         for piece in path.split(";")[1:]:
112             # make a guess on how to index
113             # XXX need to apply \ quoting on '!!
114             if isint(piece):
115                 p += "[%s]" % piece
116             else:
117                 p += "['%s']" % piece
118         return p
119
120     def store(path, object):
121         if result_type == "VALUE":
122             result.append(object)
123         elif result_type == "IPATH":  # Index format path (Python ext)
124             # return list of list of indices -- can be used w/o "eval" or split
125             result.append(path.split(";")[1:])
126         else:  # PATH
127             result.append(as_path(path))
128         return path
129
130     def trace(expr, obj, path):
131         if debug:
132             print("trace", expr, "/", path)
133         if expr:
134             x = expr.split(";")
135             loc = x[0]
136             x = ";".join(x[1:])
137             if debug:
138                 print("\t", loc, type(obj))
139             if loc == "*":
140
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
206                         def max(x, y):
207                             if x > y:
208                                 return x
209                             return y
210
211                         def min(x, y):
212                             if x < y:
213                                 return x
214                             return y
215
216                         objlen = len(obj)
217                         s0 = m.group(1)
218                         s1 = m.group(2)
219                         s2 = m.group(3)
220
221                         # XXX int("badstr") raises exception
222                         start = int(s0) if s0 else 0
223                         end = int(s1) if s1 else objlen
224                         step = int(s2) if s2 else 1
225
226                         if start < 0:
227                             start = max(0, start + objlen)
228                         else:
229                             start = min(objlen, start)
230                         if end < 0:
231                             end = max(0, end + objlen)
232                         else:
233                             end = min(objlen, end)
234
235                         for i in xrange(start, end, step):
236                             trace(s(i, x), obj, path)
237                     return
238
239                 # after (expr) & ?(expr)
240                 if loc.find(",") >= 0:
241                     # [index,index....]
242                     for piece in re.split(r"'?,'?", loc):
243                         if debug > 1:
244                             print("piece", piece)
245                         trace(s(piece, x), obj, path)
246         else:
247             store(path, obj)
248
249     def walk(loc, expr, obj, path, funct):
250         if isinstance(obj, list):
251             for i in xrange(0, len(obj)):
252                 funct(i, loc, expr, obj, path)
253         elif isinstance(obj, dict):
254             for key in obj:
255                 funct(key, loc, expr, obj, path)
256
257     def evalx(loc, obj):
258         """eval expression"""
259
260         if debug:
261             print("evalx", loc)
262
263         # a nod to JavaScript. doesn't work for @.name.name.length
264         # Write len(@.name.name) instead!!!
265         loc = loc.replace("@.length", "len(__obj)")
266
267         loc = loc.replace("&&", " and ").replace("||", " or ")
268
269         # replace !@.name with 'name' not in obj
270         # XXX handle !@.name.name.name....
271         def notvar(m):
272             return "'%s' not in __obj" % m.group(1)
273
274         loc = re.sub("!@\.([a-zA-Z@_]+)", notvar, loc)
275
276         # replace @.name.... with __obj['name']....
277         # handle @.name[.name...].length
278         def varmatch(m):
279             def brackets(elts):
280                 ret = "__obj"
281                 for e in elts:
282                     if isint(e):
283                         ret += "[%s]" % e  # ain't necessarily so
284                     else:
285                         ret += "['%s']" % e  # XXX beware quotes!!!!
286                 return ret
287
288             g1 = m.group(1)
289             elts = g1.split(".")
290             if elts[-1] == "length":
291                 return "len(%s)" % brackets(elts[1:-1])
292             return brackets(elts[1:])
293
294         loc = re.sub(r"(?<!\\)(@\.[a-zA-Z@_.]+)", varmatch, loc)
295
296         # removed = -> == translation
297         # causes problems if a string contains =
298
299         # replace @  w/ "__obj", but \@ means a literal @
300         loc = re.sub(r"(?<!\\)@", "__obj", loc).replace(r"\@", "@")
301         if not use_eval:
302             if debug:
303                 print("eval disabled")
304             raise Exception("eval disabled")
305         if debug:
306             print("eval", loc)
307         try:
308             # eval w/ caller globals, w/ local "__obj"!
309             v = eval(loc, caller_globals, {"__obj": obj})
310         except Exception as e:
311             if debug:
312                 print(e)
313         return False
314
315         if debug:
316             print("->", v)
317         return v
318
319     # body of jsonpath()
320
321     # Get caller globals so eval can pick up user functions!!!
322     caller_globals = sys._getframe(1).f_globals
323     result = []
324     if expr and obj:
325         cleaned_expr = normalize(expr)
326         if cleaned_expr.startswith("$;"):
327             cleaned_expr = cleaned_expr[2:]
328
329         # XXX wrap this in a try??
330         trace(cleaned_expr, obj, "$")
331
332         if len(result) > 0:
333             return result
334     return False
335
336
337 if __name__ == "__main__":
338     try:
339         import json  # v2.6
340     except ImportError:
341         import simplejson as json
342
343     import sys
344
345     # XXX take options for output format, output file, debug level
346
347     if len(sys.argv) < 3 or len(sys.argv) > 4:
348         sys.stdout.write("Usage: jsonpath.py FILE PATH [OUTPUT_TYPE]\n")
349         sys.exit(1)
350
351     object = json.load(file(sys.argv[1]))
352     path = sys.argv[2]
353     format = "VALUE"
354
355     if len(sys.argv) > 3:
356         # XXX verify?
357         format = sys.argv[3]
358
359     value = jsonpath(object, path, format)
360
361     if not value:
362         sys.exit(1)
363
364     f = sys.stdout
365     json.dump(value, f, sort_keys=True, indent=1)
366     f.write("\n")
367
368     sys.exit(0)