blob: 750cd84f7d002a442cb2972230a3b41e0d4a8ed2 [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.
*/
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);
return new FunctionCallExpression(TraversalStepType.FILTER, parent, FILTER_METHOD, closureExpr);
}
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);
}
}