blob: 7fd3e88f722604082e40caf4191efac74ebbff15 [file] [log] [blame]
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
"""
Class that can turn traversals back into Gremlin Groovy format text queries.
Those queries can then be run in the Gremlin console or using the GLV submit(<String>) API or
sent to any TinkerPop compliant HTTP endpoint.
"""
__author__ = 'Kelvin R. Lawrence (gfxman)'
import math
import numbers
import re
from gremlin_python.process.traversal import *
from gremlin_python.process.strategies import *
from gremlin_python.structure.graph import Vertex, Edge, VertexProperty
from datetime import datetime
class Translator:
"""
Turn a bytecode object back into a textual query (Gremlin Groovy script).
"""
# Dictionary used to reverse-map token IDs to strings
options = {
WithOptions.tokens: 'tokens',
WithOptions.none: 'none',
WithOptions.ids: 'ids',
WithOptions.labels: 'labels',
WithOptions.keys: 'keys',
WithOptions.values: 'values',
WithOptions.all: 'all',
WithOptions.indexer: 'indexer',
WithOptions.list: 'list',
WithOptions.map: 'map'
}
conn_p = ['and', 'or']
def __init__(self, traversal_source=None):
self.traversal_source = traversal_source
def get_traversal_source(self):
return self.traversal_source
def get_target_language(self):
return "gremlin-groovy"
def of(self, traversal_source):
self.traversal_source = traversal_source
return self
# Do any needed special processing for the representation
# of strings and dates and boolean.
def fixup(self, v):
if isinstance(v, str):
return f'{v!r}' # use repr() format for canonical string rep
elif type(v) == datetime:
return self.process_date(v)
elif type(v) == bool:
return 'true' if v else 'false'
elif isinstance(v, numbers.Number):
return self.process_number(v)
elif isinstance(v, set):
return f'[{str(v)[1:-1]}]'
elif isinstance(v, P):
return self.process_predicate(v)
elif type(v) == Vertex:
return self.process_vertex(v)
elif type(v) == Edge:
return self.process_edge(v)
elif type(v) in [Merge]: # on_create on_match out_v in_v
tmp = str(v)
return f'{tmp.split("_")[0]}{tmp.split("_")[1].capitalize()}' if tmp.find('_') else tmp
elif v is None:
return 'null'
else:
return str(v)
# Turn a Python datetime into the equivalent new Date(...)
def process_date(self, date):
y = date.year - 1900
mo = date.month
d = date.day
h = date.hour
mi = date.minute
s = date.second
return f'new Date({y},{mo},{d},{h},{mi},{s})'
# Do special processing needed to format predicates that come in
# such as "gt(a)" correctly.
def process_predicate(self, p):
res = ''
if p.operator in self.conn_p:
res += f'{self.process_predicate(p.value)}.{p.operator}({self.process_predicate(p.other)})'
else:
res += f'{self.process_p_value(p)}'
return res
# process the value of the predicates
def process_p_value(self, p):
res = str(p).split('(')[0] + '('
if type(p.value) == list:
res += '['
for v in p.value:
res += self.fixup(v) + ','
res = (res[0:-1] + ']') if len(p.value) > 0 else (res + ']')
else:
res += self.fixup(p.value)
if p.other is not None:
res += f',{self.fixup(p.other)}'
res += ')'
return res
# Special processing to handle strategies
def process_strategy(self, s):
c = 0
res = ''
# if parameter is empty, only pass class name (referenced GroovyTranslator.java)
if not s.configuration:
res += s.strategy_name
else:
res = f'new {str(s)}('
for key in s.configuration:
res += ',' if c > 0 else ''
res += key + ':'
val = s.configuration[key]
if isinstance(val, Traversal):
res += self.translate(val.bytecode, child=True)
else:
res += self.fixup(val)
c += 1
res += ')'
return res
pass
# Special processing to handle vertices
def process_vertex(self, vertex):
return f'new ReferenceVertex({self.fixup(vertex.id)},\'{vertex.label}\')'
# Special processing to handle edges
def process_edge(self, edge):
return f'new ReferenceEdge({str(edge.id)},\'{edge.label}\',' \
f'new ReferenceVertex({str(edge.inV.id)},\'{edge.inV.label}\'),' \
f'new ReferenceVertex({str(edge.outV.id)},\'{edge.outV.label}\'))'
# Special processing to handle vertex property
def process_vertex_property(self, vp):
return f'new ReferenceVertexProperty({str(vp.id)},\'{vp.label}\',{self.fixup(vp.value)})'
# Special processing to handle lambda
def process_lambda(self, lam):
lambda_result = lam()
script = lambda_result if isinstance(lambda_result, str) else lambda_result[0]
return f'{script}' if re.match(r"^\{.*\}$", script, flags=re.DOTALL) else f'{{{script}}}'
def process_dict(self, d):
c = 0
res = '['
if len(d) == 0:
res += ':'
else:
for k, v in d.items():
wrap = not isinstance(k, str)
res += ',' if c > 0 else ''
res += '(' if wrap else ''
res += self.fixup(k)
res += ')' if wrap else ''
res += f':{self.fixup(v)}'
c += 1
res += ']'
return res
def process_number(self, n):
if isinstance(n, float):
# converting floats into doubles for script since python doesn't distinguish and java defaults to double
if math.isnan(n):
return "Double.NaN"
elif math.isinf(n) and n > 0:
return "Double.POSITIVE_INFINITY"
elif math.isinf(n) and n < 0:
return "Double.NEGATIVE_INFINITY"
else:
return f'{n}d'
return f'{n}'
def process_list(self, l):
c = 0
res = '['
for i in l:
res += ',' if c > 0 else ''
res += self.fixup(i)
c += 1
res += ']'
return res
# Special processing to handle bindings inside of traversals
def process_binding(self, binding):
return f'Bindings.instance().of(\'{binding.key}\', {str(binding.value)})'
# Main driver of the translation. Different parts of
# a Traversal are handled appropriately.
def do_translation(self, step):
script = ''
params = step[1:]
script += '.' + step[0] + '('
if len(params) > 0:
c = 0
with_opts = False
is_merge_op = (step[0] == 'mergeV') or (step[0] == 'mergeE')
for p in params:
script += ',' if c > 0 else ''
if with_opts:
script += f'WithOptions.{self.options[p]}'
elif type(p) == Bytecode:
script += self.translate(p, True)
elif isinstance(p, P):
script += self.process_predicate(p)
elif type(p) == Vertex:
script += self.process_vertex(p)
elif type(p) == Edge:
script += self.process_edge(p)
elif type(p) == VertexProperty:
script += self.process_vertex_property(p)
elif type(p) in [Cardinality, Pop, Operator, Scope, T]:
tmp = str(p)
script += tmp[0:-1] if tmp.endswith('_') else tmp
elif type(p) in [Merge]: # on_create on_match out_v in_v
is_merge_op = True
tmp = str(p)
script += f'{tmp.split("_")[0]}{tmp.split("_")[1].capitalize()}' if tmp.find('_') else tmp
elif isinstance(p, TraversalStrategy): # this will capture all strategies
script += self.process_strategy(p)
elif type(p) == datetime:
script += self.process_date(p)
elif p == WithOptions.tokens:
script += 'WithOptions.tokens'
with_opts = True
elif isinstance(p, str):
script += f'{p!r}' # use repr() format for canonical string rep
elif type(p) == bool:
script += 'true' if p else 'false'
elif isinstance(p, type(lambda: None)) and p.__name__ == (lambda: None).__name__:
script += self.process_lambda(p)
elif type(p) == Binding:
script += self.process_binding(p)
elif p is None:
script += '(Traversal) null' if is_merge_op else 'null'
elif isinstance(p, type):
script += p.__name__
elif isinstance(p, dict):
script += self.process_dict(p)
elif isinstance(p, numbers.Number):
script += self.process_number(p)
elif isinstance(p, set):
script += f'[{str(p)[1:-1]}] as Set' if len(p) > 0 else '[] as Set'
elif isinstance(p, list):
script += self.process_list(p)
else:
script += str(p)
c += 1
script += ')'
return script
# Translation starts here. There are two main parts to a
# traversal. Source instructions such as "withSideEffect"
# and "withStrategies", and step instructions such as
# "addV" and "repeat". If child is True we will generate
# anonymous traversal style syntax.
def translate(self, bytecode, child=False):
script = '__' if child else self.traversal_source
for step in bytecode.source_instructions:
script += self.do_translation(step)
for step in bytecode.step_instructions:
script += self.do_translation(step)
return script