/** * 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_METHOD 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.HashMap; import java.util.List; import java.util.Map; import org.apache.atlas.AtlasException; import org.apache.atlas.groovy.AbstractFunctionExpression; import org.apache.atlas.groovy.ArithmeticExpression; import org.apache.atlas.groovy.ArithmeticExpression.ArithmeticOperator; import org.apache.atlas.groovy.CastExpression; import org.apache.atlas.groovy.ClosureExpression; 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.ListExpression; import org.apache.atlas.groovy.LiteralExpression; import org.apache.atlas.groovy.TraversalStepType; import org.apache.atlas.groovy.TypeCoersionExpression; import org.apache.atlas.groovy.VariableAssignmentExpression; import org.apache.atlas.query.GraphPersistenceStrategies; import org.apache.atlas.query.IntSequence; import org.apache.atlas.query.TypeUtils.FieldInfo; import org.apache.atlas.repository.graph.AtlasGraphProvider; import org.apache.atlas.repository.graphdb.AtlasEdgeDirection; import org.apache.atlas.repository.graphdb.GremlinVersion; import org.apache.atlas.typesystem.types.IDataType; import org.apache.atlas.typesystem.types.TypeSystem; import org.apache.atlas.typesystem.types.cache.TypeCache.TYPE_FILTER; import org.apache.atlas.util.AtlasRepositoryConfiguration; import com.google.common.collect.ImmutableList; /** * Factory to generate Groovy expressions representing Gremlin syntax that that * are independent of the specific version of Gremlin that is being used. * */ public abstract class GremlinExpressionFactory { private static final String G_VARIABLE = "g"; private static final String IT_VARIABLE = "it"; protected static final String SET_CLASS = "Set"; private static final String OBJECT_FIELD = "object"; protected static final String V_METHOD = "V"; protected static final String FILTER_METHOD = "filter"; private static final String PATH_METHOD = "path"; private static final String AS_METHOD = "as"; private static final String IN_OPERATOR = "in"; protected static final String HAS_METHOD = "has"; protected static final String TO_LOWER_CASE_METHOD = "toLowerCase"; protected static final String SELECT_METHOD = "select"; protected static final String ORDER_METHOD = "order"; protected static final String FILL_METHOD = "fill"; protected static final String MATCHES = "matches"; public static final GremlinExpressionFactory INSTANCE = AtlasGraphProvider.getGraphInstance() .getSupportedGremlinVersion() == GremlinVersion.THREE ? new Gremlin3ExpressionFactory() : new Gremlin2ExpressionFactory(); /** * Returns the unqualified name of the class used in this version of gremlin to * represent Gremlin queries as they are being generated. * @return */ public abstract String getTraversalExpressionClass(); /** * Gets the expression to use as the parent when translating the loop * expression in a loop * * @param inputQry * the * @return */ public abstract GroovyExpression getLoopExpressionParent(GroovyExpression inputQry); /** * Generates a loop expression. * * @param parent * the parent of the loop expression * @param emitExpr * Expression with the value that should be emitted by the loop * expression. * @param loopExpr * the query expression that is being executed repeatedly * executed in a loop * @param alias * The alias of the expression being looped over * @param times * the number of times to repeat, or null if a times condition * should not be used. * @return */ public abstract GroovyExpression generateLoopExpression(GroovyExpression parent, GraphPersistenceStrategies s, IDataType dataType, GroovyExpression loopExpr, String alias, Integer times); /** * Generates a logical (and/or) expression with the given operands. * @param parent * @param operator * @param operands * @return */ public abstract GroovyExpression generateLogicalExpression(GroovyExpression parent, String operator, List<GroovyExpression> operands); /** * Generates a back reference expression that refers to the given alias. * * @param parent * @param inSelect * @param alias * @return */ public abstract GroovyExpression generateBackReferenceExpression(GroovyExpression parent, boolean inSelect, String alias); /** * Generates a select expression * * @param parent * @param sourceNames * the names of the select fields * @param srcExprs * the corresponding values to return * @return */ public abstract GroovyExpression generateSelectExpression(GroovyExpression parent, List<LiteralExpression> sourceNames, List<GroovyExpression> srcExprs); /** * Generates a an expression that gets the value of the given property from the * vertex presented by the parent. * * @param parent * @param fInfo * @param propertyName * @param inSelect * @return */ public abstract GroovyExpression generateFieldExpression(GroovyExpression parent, FieldInfo fInfo, String propertyName, boolean inSelect); /** * Generates a has expression that checks whether the vertices match a specific condition * * @param s * @param parent the object that we should call apply the "has" condition to. * @param propertyName the name of the property whose value we are comparing * @param symbol comparsion operator symbol ('=','<', etc.) * @param requiredValue the value to compare against * @param fInfo info about the field whose value we are checking * @return * @throws AtlasException */ public abstract GroovyExpression generateHasExpression(GraphPersistenceStrategies s, GroovyExpression parent, String propertyName, String symbol, GroovyExpression requiredValue, FieldInfo fInfo) throws AtlasException; public abstract GroovyExpression generateLikeExpressionUsingFilter(GroovyExpression parent, String propertyName, GroovyExpression propertyValue) throws AtlasException; /** * Generates a range expression * * @param parent * @param startIndex * @param endIndex * @return */ public abstract GroovyExpression generateRangeExpression(GroovyExpression parent, int startIndex, int endIndex); /** * Determines if the specified expression is a range method call. * * @param expr * @return */ public abstract boolean isRangeExpression(GroovyExpression expr); /** * Set the start index and end index of a range expression * * @param expr * @param startIndex * @param endIndex */ public abstract void setRangeParameters(GroovyExpression expr, int startIndex, int endIndex); /** * If the specified function expression is a range expression, returns the start and end index parameters * otherwise returns null. * * @param expr * @return int array with two elements - element 0 is start index, element 1 is end index */ public abstract int[] getRangeParameters(AbstractFunctionExpression expr); /** * Generates an order by expression * * @param parent * @param translatedOrderBy * @param isAscending * @return */ public abstract GroovyExpression generateOrderByExpression(GroovyExpression parent, List<GroovyExpression> translatedOrderBy, boolean isAscending); /** * Determines if specified expression is an order method call * * @param expr * @return */ public boolean isOrderExpression(GroovyExpression expr) { if (expr instanceof FunctionCallExpression) { FunctionCallExpression functionCallExpression = (FunctionCallExpression) expr; if (functionCallExpression.getFunctionName().equals(ORDER_METHOD)) { return true; } } return false; } /** * Returns the Groovy expressions that should be used as the parents when * translating an order by expression. This is needed because Gremlin 2 and * 3 handle order by expressions very differently. * */ public abstract List<GroovyExpression> getOrderFieldParents(); /** * Returns the expression that represents an anonymous graph traversal. * * @return */ public abstract GroovyExpression getAnonymousTraversalExpression(); public boolean isLeafAnonymousTraversalExpression(GroovyExpression expr) { if(!(expr instanceof FunctionCallExpression)) { return false; } FunctionCallExpression functionCallExpr = (FunctionCallExpression)expr; if(functionCallExpr.getCaller() != null) { return false; } return functionCallExpr.getFunctionName().equals("_") & functionCallExpr.getArguments().size() == 0; } /** * Returns an expression representing * * @return */ public abstract GroovyExpression getFieldInSelect(); /** * Generates the expression the serves as the root of the Gremlin query. * @param varExpr variable containing the vertices to traverse * @return */ protected abstract GroovyExpression initialExpression(GroovyExpression varExpr, GraphPersistenceStrategies s); /** * Generates an expression that tests whether the vertex represented by the 'toTest' * expression represents an instance of the specified type, checking both the type * and super type names. * * @param s * @param typeName * @param itRef * @return */ protected abstract GroovyExpression typeTestExpression(GraphPersistenceStrategies s, String typeName, GroovyExpression vertexExpr); /** /** * Generates a sequence of groovy expressions that filter the vertices to only * those that match the specified type. If GraphPersistenceStrategies.collectTypeInstancesIntoVar() * is set and the gremlin optimizer is disabled, the vertices are put into a variable whose name is generated * from the specified IntSequence. The last item in the result will be a graph traversal restricted to only * the matching vertices. */ public List<GroovyExpression> generateTypeTestExpression(GraphPersistenceStrategies s, GroovyExpression parent, String typeName, IntSequence intSeq) throws AtlasException { if(AtlasRepositoryConfiguration.isGremlinOptimizerEnabled()) { GroovyExpression superTypeAttributeNameExpr = new LiteralExpression(s.superTypeAttributeName()); GroovyExpression typeNameExpr = new LiteralExpression(typeName); GroovyExpression superTypeMatchesExpr = new FunctionCallExpression(TraversalStepType.FILTER, HAS_METHOD, superTypeAttributeNameExpr, typeNameExpr); GroovyExpression typeAttributeNameExpr = new LiteralExpression(s.typeAttributeName()); GroovyExpression typeMatchesExpr = new FunctionCallExpression(TraversalStepType.FILTER, HAS_METHOD, typeAttributeNameExpr, typeNameExpr); GroovyExpression result = new FunctionCallExpression(TraversalStepType.FILTER, parent, "or", typeMatchesExpr, superTypeMatchesExpr); return Collections.singletonList(result); } else { if (s.filterBySubTypes()) { return typeTestExpressionUsingInFilter(s, parent, typeName); } else if (s.collectTypeInstancesIntoVar()) { return typeTestExpressionMultiStep(s, typeName, intSeq); } else { return typeTestExpressionUsingFilter(s, parent, typeName); } } } private List<GroovyExpression> typeTestExpressionUsingInFilter(GraphPersistenceStrategies s, GroovyExpression parent, final String typeName) throws AtlasException { List<GroovyExpression> typeNames = new ArrayList<>(); typeNames.add(new LiteralExpression(typeName)); Map<TYPE_FILTER, String> filters = new HashMap<TYPE_FILTER, String>() {{ put(TYPE_FILTER.SUPERTYPE, typeName); }}; ImmutableList<String> subTypes = TypeSystem.getInstance().getTypeNames(filters); if (!subTypes.isEmpty()) { for (String subType : subTypes) { typeNames.add(new LiteralExpression(subType)); } } GroovyExpression inFilterExpr = generateHasExpression(s, parent, s.typeAttributeName(), IN_OPERATOR, new ListExpression(typeNames), null); return Collections.singletonList(inFilterExpr); } private List<GroovyExpression> typeTestExpressionMultiStep(GraphPersistenceStrategies s, String typeName, IntSequence intSeq) { String varName = "_var_" + intSeq.next(); GroovyExpression varExpr = new IdentifierExpression(varName); List<GroovyExpression> result = new ArrayList<>(); result.add(newSetVar(varName)); result.add(fillVarWithTypeInstances(s, typeName, varName)); result.add(fillVarWithSubTypeInstances(s, typeName, varName)); result.add(initialExpression(varExpr, s)); return result; } private GroovyExpression newSetVar(String varName) { GroovyExpression castExpr = new TypeCoersionExpression(new ListExpression(), SET_CLASS); return new VariableAssignmentExpression(varName, castExpr); } private GroovyExpression fillVarWithTypeInstances(GraphPersistenceStrategies s, String typeName, String fillVar) { GroovyExpression graphExpr = getAllVerticesExpr(); GroovyExpression typeAttributeNameExpr = new LiteralExpression(s.typeAttributeName()); GroovyExpression typeNameExpr = new LiteralExpression(typeName); GroovyExpression hasExpr = new FunctionCallExpression(graphExpr, HAS_METHOD, typeAttributeNameExpr, typeNameExpr); GroovyExpression fillExpr = new FunctionCallExpression(hasExpr, FILL_METHOD, new IdentifierExpression(fillVar)); return fillExpr; } private GroovyExpression fillVarWithSubTypeInstances(GraphPersistenceStrategies s, String typeName, String fillVar) { GroovyExpression graphExpr = getAllVerticesExpr(); GroovyExpression superTypeAttributeNameExpr = new LiteralExpression(s.superTypeAttributeName()); GroovyExpression typeNameExpr = new LiteralExpression(typeName); GroovyExpression hasExpr = new FunctionCallExpression(graphExpr, HAS_METHOD, superTypeAttributeNameExpr, typeNameExpr); GroovyExpression fillExpr = new FunctionCallExpression(hasExpr, FILL_METHOD, new IdentifierExpression(fillVar)); return fillExpr; } private List<GroovyExpression> typeTestExpressionUsingFilter(GraphPersistenceStrategies s, GroovyExpression parent, String typeName) { GroovyExpression itExpr = getItVariable(); GroovyExpression typeTestExpr = typeTestExpression(s, typeName, itExpr); GroovyExpression closureExpr = new ClosureExpression(typeTestExpr); GroovyExpression filterExpr = new FunctionCallExpression(parent, FILTER_METHOD, closureExpr); return Collections.singletonList(filterExpr); } /** * Generates an expression which checks whether the vertices in the query have * a field with the given name. * * @param parent * @param fieldName * @return */ public GroovyExpression generateUnaryHasExpression(GroovyExpression parent, String fieldName) { return new FunctionCallExpression(TraversalStepType.FILTER, parent, HAS_METHOD, new LiteralExpression(fieldName)); } /** * Generates a path expression * * @param parent * @return */ public GroovyExpression generatePathExpression(GroovyExpression parent) { return new FunctionCallExpression(TraversalStepType.MAP_TO_VALUE, parent, PATH_METHOD); } /** * Generates the emit expression used in loop expressions. * @param s * @param dataType * @return */ protected GroovyExpression generateLoopEmitExpression(GraphPersistenceStrategies s, IDataType dataType) { return typeTestExpression(s, dataType.getName(), getCurrentObjectExpression()); } /** * Generates an alias expression * * @param parent * @param alias * @return */ public GroovyExpression generateAliasExpression(GroovyExpression parent, String alias) { return new FunctionCallExpression(TraversalStepType.SIDE_EFFECT, parent, AS_METHOD, new LiteralExpression(alias)); } /** * Generates an expression that gets the vertices adjacent to the vertex in 'parent' * in the specified direction. * * @param parent * @param dir * @return */ public GroovyExpression generateAdjacentVerticesExpression(GroovyExpression parent, AtlasEdgeDirection dir) { return new FunctionCallExpression(TraversalStepType.FLAT_MAP_TO_ELEMENTS, parent, getGremlinFunctionName(dir)); } private String getGremlinFunctionName(AtlasEdgeDirection dir) { switch(dir) { case IN: return "in"; case OUT: return "out"; case BOTH: return "both"; default: throw new RuntimeException("Unknown Atlas Edge Direction: " + dir); } } /** * Generates an expression that gets the vertices adjacent to the vertex in 'parent' * in the specified direction, following only edges with the given label. * * @param parent * @param dir * @return */ public GroovyExpression generateAdjacentVerticesExpression(GroovyExpression parent, AtlasEdgeDirection dir, String label) { return new FunctionCallExpression(TraversalStepType.FLAT_MAP_TO_ELEMENTS, parent, getGremlinFunctionName(dir), new LiteralExpression(label)); } /** * Generates an arithmetic expression, e.g. a + b * */ public GroovyExpression generateArithmeticExpression(GroovyExpression left, String operator, GroovyExpression right) throws AtlasException { ArithmeticOperator op = ArithmeticOperator.lookup(operator); return new ArithmeticExpression(left, op, right); } public abstract GroovyExpression generateGroupByExpression(GroovyExpression parent, GroovyExpression groupByExpression, GroovyExpression aggregationFunction); protected GroovyExpression getItVariable() { return new IdentifierExpression(IT_VARIABLE); } protected GroovyExpression getAllVerticesExpr() { GroovyExpression gExpr = getGraphExpression(); return new FunctionCallExpression(TraversalStepType.START, gExpr, V_METHOD); } protected IdentifierExpression getGraphExpression() { return new IdentifierExpression(TraversalStepType.SOURCE, G_VARIABLE); } protected GroovyExpression getCurrentObjectExpression() { return new FieldExpression(getItVariable(), OBJECT_FIELD); } //assumes cast already performed public GroovyExpression generateCountExpression(GroovyExpression itExpr) { GroovyExpression collectionExpr = new CastExpression(itExpr,"Collection"); return new FunctionCallExpression(collectionExpr, "size"); } public GroovyExpression generateMinExpression(GroovyExpression itExpr, GroovyExpression mapFunction) { return getAggregrationExpression(itExpr, mapFunction, "min"); } public GroovyExpression generateMaxExpression(GroovyExpression itExpr, GroovyExpression mapFunction) { return getAggregrationExpression(itExpr, mapFunction, "max"); } public GroovyExpression generateSumExpression(GroovyExpression itExpr, GroovyExpression mapFunction) { return getAggregrationExpression(itExpr, mapFunction, "sum"); } private GroovyExpression getAggregrationExpression(GroovyExpression itExpr, GroovyExpression mapFunction, String functionName) { GroovyExpression collectionExpr = new CastExpression(itExpr,"Collection"); ClosureExpression collectFunction = new ClosureExpression(mapFunction); GroovyExpression transformedList = new FunctionCallExpression(collectionExpr, "collect", collectFunction); return new FunctionCallExpression(transformedList, functionName); } public GroovyExpression getClosureArgumentValue() { return getItVariable(); } /** * Specifies the parent to use when translating the select list in * a group by statement. * * @return */ public abstract GroovyExpression getGroupBySelectFieldParent(); public GroovyExpression generateFillExpression(GroovyExpression parent, GroovyExpression variable) { return new FunctionCallExpression(TraversalStepType.END,parent , "fill", variable); } /** * Generates an anonymous graph traversal initialized with the specified value. In Gremlin 3, we need * to use a different syntax for this when the object is a map, so that information needs to be provided * to this method so that the correct syntax is used. * * @param isMap true if the value contains Map instances, false if it contains Vertex instances * @param valueCollection the source objects to start the traversal from. */ public abstract GroovyExpression generateSeededTraversalExpresssion(boolean isMap, GroovyExpression valueCollection); /** * Returns the current value of the traverser. This is used when generating closure expressions that * need to operate on the current value in the graph graversal. * * @param traverser * @return */ public abstract GroovyExpression getCurrentTraverserObject(GroovyExpression traverser); /** * Generates an expression that transforms the current value of the traverser by * applying the function specified * * @param parent * @param closureExpression * @return */ public abstract GroovyExpression generateMapExpression(GroovyExpression parent, ClosureExpression closureExpression); /** * Returns whether a select statement generates a map (or Gremlin 2 "Row") when it contains the specified * number of aliases. * */ public abstract boolean isSelectGeneratesMap(int aliasCount); /** * Generates an expression to get the value of the value from the row map * generated by select() with the specified key. * */ public abstract GroovyExpression generateGetSelectedValueExpression(LiteralExpression key, GroovyExpression rowMapExpr); public GroovyExpression removeExtraMapFromPathInResult(GroovyExpression parent) { GroovyExpression listItem = getItVariable(); GroovyExpression tailExpr = new FunctionCallExpression(listItem, "tail"); return new FunctionCallExpression(parent, "collect", new ClosureExpression(tailExpr)); } /** * Generates a toList expression to execute the gremlin query and * store the result in a new list. * * @param expr * @return */ public GroovyExpression generateToListExpression(GroovyExpression expr) { return new FunctionCallExpression(TraversalStepType.END, expr, "toList"); } /** * Finds aliases that absolutely must be brought along with this expression into * the output expression and cannot just be recreated there. For example, in the * Gremlin 2 loop expression, the loop semantics break of the alias is simply recreated * in the output expression. * @param expr * @return */ public abstract List<String> getAliasesRequiredByExpression(GroovyExpression expr); /** * Checks if the given expression is an alias expression, and if so * returns the alias from the expression. Otherwise, null is * returned. */ public String getAliasNameIfRelevant(GroovyExpression expr) { if(!(expr instanceof FunctionCallExpression)) { return null; } FunctionCallExpression fc = (FunctionCallExpression)expr; if(! fc.getFunctionName().equals(AS_METHOD)) { return null; } LiteralExpression aliasName = (LiteralExpression)fc.getArguments().get(0); return aliasName.getValue().toString(); } public abstract boolean isRepeatExpression(GroovyExpression expr); }