/** * 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. */ package org.apache.atlas.gremlin; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.apache.atlas.AtlasException; import org.apache.atlas.groovy.AbstractFunctionExpression; import org.apache.atlas.groovy.CastExpression; import org.apache.atlas.groovy.ClosureExpression; import org.apache.atlas.groovy.ComparisonExpression; import org.apache.atlas.groovy.ComparisonExpression.ComparisonOperator; import org.apache.atlas.groovy.ComparisonOperatorExpression; import org.apache.atlas.groovy.FieldExpression; import org.apache.atlas.groovy.FunctionCallExpression; import org.apache.atlas.groovy.GroovyExpression; import org.apache.atlas.groovy.IdentifierExpression; import org.apache.atlas.groovy.LiteralExpression; import org.apache.atlas.groovy.LogicalExpression; import org.apache.atlas.groovy.LogicalExpression.LogicalOperator; import org.apache.atlas.groovy.TernaryOperatorExpression; import org.apache.atlas.groovy.TraversalStepType; import org.apache.atlas.groovy.TypeCoersionExpression; import org.apache.atlas.query.GraphPersistenceStrategies; import org.apache.atlas.query.TypeUtils.FieldInfo; import org.apache.atlas.repository.graph.AtlasGraphProvider; import org.apache.atlas.repository.graphdb.AtlasGraph; import org.apache.atlas.typesystem.types.AttributeInfo; import org.apache.atlas.typesystem.types.IDataType; /** * Generates gremlin query expressions using Gremlin 3 syntax. * */ public class Gremlin3ExpressionFactory extends GremlinExpressionFactory { private static final String VERTEX_LIST_CLASS = "List<Vertex>"; private static final String VERTEX_ARRAY_CLASS = "Vertex[]"; private static final String OBJECT_ARRAY_CLASS = "Object[]"; private static final String VERTEX_CLASS = "Vertex"; private static final String FUNCTION_CLASS = "Function"; private static final String VALUE_METHOD = "value"; private static final String IS_PRESENT_METHOD = "isPresent"; private static final String MAP_METHOD = "map"; private static final String VALUES_METHOD = "values"; private static final String GET_METHOD = "get"; private static final String OR_ELSE_METHOD = "orElse"; private static final String PROPERTY_METHOD = "property"; private static final String BY_METHOD = "by"; private static final String EQ_METHOD = "eq"; private static final String EMIT_METHOD = "emit"; private static final String TIMES_METHOD = "times"; private static final String REPEAT_METHOD = "repeat"; private static final String RANGE_METHOD = "range"; private static final String LAST_METHOD = "last"; private static final String TO_STRING_METHOD = "toString"; private static final GroovyExpression EMPTY_STRING_EXPRESSION = new LiteralExpression(""); @Override public GroovyExpression generateLogicalExpression(GroovyExpression parent, String operator, List<GroovyExpression> operands) { return new FunctionCallExpression(TraversalStepType.FILTER, parent, operator, operands); } @Override public GroovyExpression generateBackReferenceExpression(GroovyExpression parent, boolean inSelect, String alias) { if (inSelect) { return getFieldInSelect(); } else { return new FunctionCallExpression(TraversalStepType.MAP_TO_ELEMENT, parent, SELECT_METHOD, new LiteralExpression(alias)); } } @Override public GroovyExpression typeTestExpression(GraphPersistenceStrategies s, String typeName, GroovyExpression itRef) { LiteralExpression superTypeAttrExpr = new LiteralExpression(s.superTypeAttributeName()); LiteralExpression typeNameExpr = new LiteralExpression(typeName); LiteralExpression typeAttrExpr = new LiteralExpression(s.typeAttributeName()); FunctionCallExpression result = new FunctionCallExpression(TraversalStepType.FILTER, HAS_METHOD, typeAttrExpr, new FunctionCallExpression(EQ_METHOD, typeNameExpr)); result = new FunctionCallExpression(TraversalStepType.FILTER, result, "or"); result = new FunctionCallExpression(TraversalStepType.FILTER, result, HAS_METHOD, superTypeAttrExpr, new FunctionCallExpression(EQ_METHOD, typeNameExpr)); return result; } @Override public GroovyExpression generateLoopExpression(GroovyExpression parent,GraphPersistenceStrategies s, IDataType dataType, GroovyExpression loopExpr, String alias, Integer times) { GroovyExpression emitExpr = generateLoopEmitExpression(s, dataType); GroovyExpression result = new FunctionCallExpression(TraversalStepType.BRANCH, parent, REPEAT_METHOD, loopExpr); if (times != null) { GroovyExpression timesExpr = new LiteralExpression(times); result = new FunctionCallExpression(TraversalStepType.SIDE_EFFECT, result, TIMES_METHOD, timesExpr); } result = new FunctionCallExpression(TraversalStepType.SIDE_EFFECT, result, EMIT_METHOD, emitExpr); return result; } @Override public GroovyExpression getLoopExpressionParent(GroovyExpression inputQry) { GroovyExpression curTraversal = getAnonymousTraversalStartExpression(); return curTraversal; } private IdentifierExpression getAnonymousTraversalStartExpression() { return new IdentifierExpression(TraversalStepType.START, "__"); } @Override public GroovyExpression generateSelectExpression(GroovyExpression parent, List<LiteralExpression> sourceNames, List<GroovyExpression> srcExprs) { FunctionCallExpression result = new FunctionCallExpression(TraversalStepType.MAP_TO_VALUE, parent, SELECT_METHOD, sourceNames); for (GroovyExpression expr : srcExprs) { GroovyExpression closure = new ClosureExpression(expr); GroovyExpression castClosure = new TypeCoersionExpression(closure, FUNCTION_CLASS); result = new FunctionCallExpression(TraversalStepType.SIDE_EFFECT, result, BY_METHOD, castClosure); } return result; } @Override public GroovyExpression generateFieldExpression(GroovyExpression parent, FieldInfo fInfo, String propertyName, boolean inSelect) { AttributeInfo attrInfo = fInfo.attrInfo(); IDataType attrType = attrInfo.dataType(); GroovyExpression propertyNameExpr = new LiteralExpression(propertyName); //Whether it is the user or shared graph does not matter here, since we're //just getting the conversion expression. Ideally that would be moved someplace else. AtlasGraph graph = AtlasGraphProvider.getGraphInstance(); if (inSelect) { GroovyExpression expr = new FunctionCallExpression(parent, PROPERTY_METHOD, propertyNameExpr); expr = new FunctionCallExpression(expr, OR_ELSE_METHOD, LiteralExpression.NULL); return graph.generatePersisentToLogicalConversionExpression(expr, attrType); } else { GroovyExpression unmapped = new FunctionCallExpression(TraversalStepType.FLAT_MAP_TO_VALUES, parent, VALUES_METHOD, propertyNameExpr); if (graph.isPropertyValueConversionNeeded(attrType)) { GroovyExpression toConvert = new FunctionCallExpression(getItVariable(), GET_METHOD); GroovyExpression conversionFunction = graph.generatePersisentToLogicalConversionExpression(toConvert, attrType); return new FunctionCallExpression(TraversalStepType.MAP_TO_VALUE, unmapped, MAP_METHOD, new ClosureExpression(conversionFunction)); } else { return unmapped; } } } private ComparisonOperator getGroovyOperator(String symbol) throws AtlasException { String toFind = symbol; if (toFind.equals("=")) { toFind = "=="; } return ComparisonOperator.lookup(toFind); } private String getComparisonFunction(String op) throws AtlasException { if (op.equals("=")) { return "eq"; } if (op.equals("!=")) { return "neq"; } if (op.equals(">")) { return "gt"; } if (op.equals(">=")) { return "gte"; } if (op.equals("<")) { return "lt"; } if (op.equals("<=")) { return "lte"; } throw new AtlasException("Comparison operator " + op + " not supported in Gremlin"); } @Override public GroovyExpression generateHasExpression(GraphPersistenceStrategies s, GroovyExpression parent, String propertyName, String symbol, GroovyExpression requiredValue, FieldInfo fInfo) throws AtlasException { AttributeInfo attrInfo = fInfo.attrInfo(); IDataType attrType = attrInfo.dataType(); GroovyExpression propertNameExpr = new LiteralExpression(propertyName); if (s.isPropertyValueConversionNeeded(attrType)) { // for some types, the logical value cannot be stored directly in // the underlying graph, // and conversion logic is needed to convert the persistent form of // the value // to the actual value. In cases like this, we generate a conversion // expression to // do this conversion and use the filter step to perform the // comparsion in the gremlin query GroovyExpression itExpr = getItVariable(); GroovyExpression vertexExpr = new CastExpression(new FunctionCallExpression(itExpr, GET_METHOD), VERTEX_CLASS); GroovyExpression propertyValueExpr = new FunctionCallExpression(vertexExpr, VALUE_METHOD, propertNameExpr); GroovyExpression conversionExpr = s.generatePersisentToLogicalConversionExpression(propertyValueExpr, attrType); GroovyExpression propertyIsPresentExpression = new FunctionCallExpression( new FunctionCallExpression(vertexExpr, PROPERTY_METHOD, propertNameExpr), IS_PRESENT_METHOD); GroovyExpression valueMatchesExpr = new ComparisonExpression(conversionExpr, getGroovyOperator(symbol), requiredValue); GroovyExpression filterCondition = new LogicalExpression(propertyIsPresentExpression, LogicalOperator.AND, valueMatchesExpr); GroovyExpression filterFunction = new ClosureExpression(filterCondition); return new FunctionCallExpression(TraversalStepType.FILTER, parent, FILTER_METHOD, filterFunction); } else { GroovyExpression valueMatches = new FunctionCallExpression(getComparisonFunction(symbol), requiredValue); return new FunctionCallExpression(TraversalStepType.FILTER, parent, HAS_METHOD, propertNameExpr, valueMatches); } } @Override public GroovyExpression generateLikeExpressionUsingFilter(GroovyExpression parent, String propertyName, GroovyExpression propertyValue) throws AtlasException { GroovyExpression itExpr = getItVariable(); GroovyExpression nameExpr = new FieldExpression(itExpr, propertyName); GroovyExpression matchesExpr = new FunctionCallExpression(nameExpr, MATCHES, escapePropertyValue(propertyValue)); GroovyExpression closureExpr = new ClosureExpression(matchesExpr); GroovyExpression filterExpr = new FunctionCallExpression(parent, FILTER_METHOD, closureExpr); return filterExpr; } private GroovyExpression escapePropertyValue(GroovyExpression propertyValue) { GroovyExpression ret = propertyValue; if (propertyValue instanceof LiteralExpression) { LiteralExpression exp = (LiteralExpression) propertyValue; if (exp != null && exp.getValue() instanceof String) { String stringValue = (String) exp.getValue(); // replace '*' with ".*", replace '?' with '.' stringValue = stringValue.replaceAll("\\*", ".*") .replaceAll("\\?", "."); ret = new LiteralExpression(stringValue); } } return ret; } @Override protected GroovyExpression initialExpression(GroovyExpression varExpr, GraphPersistenceStrategies s) { // this bit of groovy magic converts the set of vertices in varName into // a String containing the ids of all the vertices. This becomes the // argument // to g.V(). This is needed because Gremlin 3 does not support // _() // s"g.V(${varName}.collect{it.id()} as String[])" GroovyExpression gExpr = getGraphExpression(); GroovyExpression varRefExpr = new TypeCoersionExpression(varExpr, OBJECT_ARRAY_CLASS); GroovyExpression matchingVerticesExpr = new FunctionCallExpression(TraversalStepType.START, gExpr, V_METHOD, varRefExpr); GroovyExpression isEmpty = new FunctionCallExpression(varExpr, "isEmpty"); GroovyExpression emptyGraph = getEmptyTraversalExpression(); GroovyExpression expr = new TernaryOperatorExpression(isEmpty, emptyGraph, matchingVerticesExpr); return s.addInitialQueryCondition(expr); } private GroovyExpression getEmptyTraversalExpression() { GroovyExpression emptyGraph = new FunctionCallExpression(TraversalStepType.START, getGraphExpression(), V_METHOD, EMPTY_STRING_EXPRESSION); return emptyGraph; } @Override public GroovyExpression generateRangeExpression(GroovyExpression parent, int startIndex, int endIndex) { //treat as barrier step, since limits need to be applied globally (even though it //is technically a filter step) return new FunctionCallExpression(TraversalStepType.BARRIER, parent, RANGE_METHOD, new LiteralExpression(startIndex), new LiteralExpression(endIndex)); } @Override public boolean isRangeExpression(GroovyExpression expr) { return (expr instanceof FunctionCallExpression && ((FunctionCallExpression)expr).getFunctionName().equals(RANGE_METHOD)); } @Override public int[] getRangeParameters(AbstractFunctionExpression expr) { if (isRangeExpression(expr)) { FunctionCallExpression rangeExpression = (FunctionCallExpression) expr; List<GroovyExpression> arguments = rangeExpression.getArguments(); int startIndex = (int)((LiteralExpression)arguments.get(0)).getValue(); int endIndex = (int)((LiteralExpression)arguments.get(1)).getValue(); return new int[]{startIndex, endIndex}; } else { return null; } } @Override public void setRangeParameters(GroovyExpression expr, int startIndex, int endIndex) { if (isRangeExpression(expr)) { FunctionCallExpression rangeExpression = (FunctionCallExpression) expr; rangeExpression.setArgument(0, new LiteralExpression(Integer.valueOf(startIndex))); rangeExpression.setArgument(1, new LiteralExpression(Integer.valueOf(endIndex))); } else { throw new IllegalArgumentException(expr + " is not a valid range expression"); } } @Override public List<GroovyExpression> getOrderFieldParents() { List<GroovyExpression> result = new ArrayList<>(1); result.add(null); return result; } @Override public GroovyExpression generateOrderByExpression(GroovyExpression parent, List<GroovyExpression> translatedOrderBy, boolean isAscending) { GroovyExpression orderByExpr = translatedOrderBy.get(0); GroovyExpression orderByClosure = new ClosureExpression(orderByExpr); GroovyExpression orderByClause = new TypeCoersionExpression(orderByClosure, FUNCTION_CLASS); GroovyExpression aExpr = new IdentifierExpression("a"); GroovyExpression bExpr = new IdentifierExpression("b"); GroovyExpression aCompExpr = new FunctionCallExpression(new FunctionCallExpression(aExpr, TO_STRING_METHOD), TO_LOWER_CASE_METHOD); GroovyExpression bCompExpr = new FunctionCallExpression(new FunctionCallExpression(bExpr, TO_STRING_METHOD), TO_LOWER_CASE_METHOD); GroovyExpression comparisonExpr = null; if (isAscending) { comparisonExpr = new ComparisonOperatorExpression(aCompExpr, bCompExpr); } else { comparisonExpr = new ComparisonOperatorExpression(bCompExpr, aCompExpr); } ClosureExpression comparisonFunction = new ClosureExpression(comparisonExpr, "a", "b"); FunctionCallExpression orderCall = new FunctionCallExpression(TraversalStepType.BARRIER, parent, ORDER_METHOD); return new FunctionCallExpression(TraversalStepType.SIDE_EFFECT, orderCall, BY_METHOD, orderByClause, comparisonFunction); } @Override public GroovyExpression getAnonymousTraversalExpression() { return null; } @Override public GroovyExpression getFieldInSelect() { // this logic is needed to remove extra results from // what is emitted by repeat loops. Technically // for queries that don't have a loop in them we could just use "it" // the reason for this is that in repeat loops with an alias, // although the alias gets set to the right value, for some // reason the select actually includes all vertices that were traversed // through in the loop. In these cases, we only want the last vertex // traversed in the loop to be selected. The logic here handles that // case by converting the result to a list and just selecting the // last item from it. GroovyExpression itExpr = getItVariable(); GroovyExpression expr1 = new TypeCoersionExpression(itExpr, VERTEX_ARRAY_CLASS); GroovyExpression expr2 = new TypeCoersionExpression(expr1, VERTEX_LIST_CLASS); return new FunctionCallExpression(expr2, LAST_METHOD); } @Override public GroovyExpression generateGroupByExpression(GroovyExpression parent, GroovyExpression groupByExpression, GroovyExpression aggregationFunction) { GroovyExpression result = new FunctionCallExpression(TraversalStepType.BARRIER, parent, "group"); GroovyExpression groupByClosureExpr = new TypeCoersionExpression(new ClosureExpression(groupByExpression), "Function"); result = new FunctionCallExpression(TraversalStepType.SIDE_EFFECT, result, "by", groupByClosureExpr); result = new FunctionCallExpression(TraversalStepType.END, result, "toList"); GroovyExpression mapValuesClosure = new ClosureExpression(new FunctionCallExpression(new CastExpression(getItVariable(), "Map"), "values")); result = new FunctionCallExpression(result, "collect", mapValuesClosure); //when we call Map.values(), we end up with an extra list around the result. We remove this by calling toList().get(0). This //leaves us with a list of lists containing the vertices that match each group. We then apply the aggregation functions //specified in the select list to each of these inner lists. result = new FunctionCallExpression(result ,"toList"); result = new FunctionCallExpression(result, "get", new LiteralExpression(0)); GroovyExpression aggregrationFunctionClosure = new ClosureExpression(aggregationFunction); result = new FunctionCallExpression(result, "collect", aggregrationFunctionClosure); return result; } @Override public GroovyExpression generateSeededTraversalExpresssion(boolean isMap, GroovyExpression valueCollection) { GroovyExpression coersedExpression = new TypeCoersionExpression(valueCollection, isMap ? "Map[]" : "Vertex[]"); if(isMap) { return new FunctionCallExpression(TraversalStepType.START, "__", coersedExpression); } else { //We cannot always use an anonymous traversal because that breaks repeat steps return new FunctionCallExpression(TraversalStepType.START, getEmptyTraversalExpression(), "inject", coersedExpression); } } @Override public GroovyExpression getGroupBySelectFieldParent() { return null; } @Override public String getTraversalExpressionClass() { return "GraphTraversal"; } @Override public boolean isSelectGeneratesMap(int aliasCount) { //in Gremlin 3, you only get a map if there is more than 1 alias. return aliasCount > 1; } @Override public GroovyExpression generateMapExpression(GroovyExpression parent, ClosureExpression closureExpression) { return new FunctionCallExpression(TraversalStepType.MAP_TO_ELEMENT, parent, "map", closureExpression); } @Override public GroovyExpression generateGetSelectedValueExpression(LiteralExpression key, GroovyExpression rowMapExpr) { rowMapExpr = new CastExpression(rowMapExpr, "Map"); GroovyExpression getExpr = new FunctionCallExpression(rowMapExpr, "get", key); return getExpr; } @Override public GroovyExpression getCurrentTraverserObject(GroovyExpression traverser) { return new FunctionCallExpression(traverser, "get"); } public List<String> getAliasesRequiredByExpression(GroovyExpression expr) { return Collections.emptyList(); } @Override public boolean isRepeatExpression(GroovyExpression expr) { if(!(expr instanceof FunctionCallExpression)) { return false; } return ((FunctionCallExpression)expr).getFunctionName().equals(REPEAT_METHOD); } }