blob: 60480a10ee78e2e6f80eb62190a9e976e0f114b3 [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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.query;
import com.google.common.annotations.VisibleForTesting;
import org.apache.atlas.exception.AtlasBaseException;
import org.apache.atlas.model.TypeCategory;
import org.apache.atlas.model.discovery.SearchParameters;
import org.apache.atlas.query.Expressions.Expression;
import org.apache.atlas.type.AtlasArrayType;
import org.apache.atlas.type.AtlasBuiltInTypes;
import org.apache.atlas.type.AtlasEntityType;
import org.apache.atlas.type.AtlasStructType;
import org.apache.atlas.type.AtlasType;
import org.apache.atlas.type.AtlasTypeRegistry;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class QueryProcessor {
private static final Logger LOG = LoggerFactory.getLogger(QueryProcessor.class);
private final int DEFAULT_QUERY_RESULT_LIMIT = 25;
private final Pattern SINGLE_QUOTED_IDENTIFIER = Pattern.compile("'(\\w[\\w\\d\\.\\s]*)'");
private final Pattern DOUBLE_QUOTED_IDENTIFIER = Pattern.compile("\"(\\w[\\w\\d\\.\\s]*)\"");
private final Pattern BACKTICK_QUOTED_IDENTIFIER = Pattern.compile("`(\\w[\\w\\d\\.\\s]*)`");
private final List<String> errorList = new ArrayList<>();
private final GremlinClauseList queryClauses = new GremlinClauseList(errorList);
private int currentStep;
private final TypeRegistryLookup registryLookup;
@Inject
public QueryProcessor(AtlasTypeRegistry typeRegistry) {
registryLookup = new TypeRegistryLookup(errorList, typeRegistry);
init();
}
@VisibleForTesting
public QueryProcessor(TypeRegistryLookup lookup) {
registryLookup = lookup;
init();
}
private void init() {
add(GremlinClause.G);
add(GremlinClause.V);
}
public Expression validate(Expression expression) {
return expression.isReady();
}
public void addFrom(String typeName) {
if (LOG.isDebugEnabled()) {
LOG.debug("addFrom(typeName={})", typeName);
}
String actualTypeName = extractIdentifier(typeName);
if(registryLookup.isTypeTrait(actualTypeName)) {
addTraitAndRegister(actualTypeName);
} else if (!registryLookup.hasActiveType()) {
registryLookup.registerActive(actualTypeName);
if(registryLookup.doesActiveTypeHaveSubTypes()) {
add(GremlinClause.HAS_TYPE_WITHIN, registryLookup.getActiveTypeAndSubTypes());
} else {
add(GremlinClause.HAS_TYPE, actualTypeName);
}
} else {
add(GremlinClause.OUT, registryLookup.getRelationshipEdgeLabelForActiveType(actualTypeName));
registryLookup.registerActive(registryLookup.getTypeFromEdge(actualTypeName));
}
}
private void addTraitAndRegister(String typeName) {
if (LOG.isDebugEnabled()) {
LOG.debug("addTraitAndRegister(typeName={})", typeName);
}
add(GremlinClause.TRAIT, typeName);
registryLookup.registerActive(typeName);
}
public void addFromIsA(String typeName, String trait) {
if (LOG.isDebugEnabled()) {
LOG.debug("addFromIsA(typeName={}, trait={})", typeName, trait);
}
if(!registryLookup.hasActiveType()) {
addFrom(typeName);
}
add(GremlinClause.TRAIT, trait);
}
public void addFromProperty(String typeName, String attribute) {
if (LOG.isDebugEnabled()) {
LOG.debug("addFromIsA(typeName={}, attribute={})", typeName, attribute);
}
if(registryLookup.isSameAsActive(typeName) == false) {
addFrom(typeName);
}
add(GremlinClause.HAS_PROPERTY, registryLookup.getQualifiedAttributeName(attribute));
}
public void addFromAlias(String typeName, String alias) {
if (LOG.isDebugEnabled()) {
LOG.debug("addFromAlias(typeName={}, alias={})", typeName, alias);
}
addFrom(typeName);
addAsClause(alias);
}
public void addWhere(String lhs, String operator, String rhs) {
if (LOG.isDebugEnabled()) {
LOG.debug("addWhere(lhs={}, operator={}, rhs={})", lhs, operator, rhs);
}
lhs = registryLookup.getQualifiedAttributeName(lhs);
SearchParameters.Operator op = SearchParameters.Operator.fromString(operator);
switch (op) {
case LT:
add(GremlinClause.HAS_OPERATOR, lhs, "lt", rhs);
break;
case GT:
add(GremlinClause.HAS_OPERATOR, lhs, "gt", rhs);
break;
case LTE:
add(GremlinClause.HAS_OPERATOR, lhs, "lte", rhs);
break;
case GTE:
add(GremlinClause.HAS_OPERATOR, lhs, "gte", rhs);
break;
case EQ:
add(GremlinClause.HAS_OPERATOR, lhs, "eq", rhs);
break;
case NEQ:
add(GremlinClause.HAS_OPERATOR, lhs, "neq", rhs);
break;
case IN:
// TODO: Handle multiple RHS values
add(GremlinClause.HAS_OPERATOR, lhs, "within", rhs);
break;
case LIKE:
add(GremlinClause.TEXT_CONTAINS, lhs, rhs.replace("*", ".*").replace('?', '.'));
break;
}
}
public void addSelect(String[] items) {
if (LOG.isDebugEnabled()) {
LOG.debug("addSelect(items.length={})", items != null ? items.length : -1);
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < items.length; i++) {
String s = registryLookup.getQualifiedAttributeName(items[i]);
if (items[i].contains(".") || registryLookup.isAttributePrimitiveTypeForActiveType(items[i])) {
sb.append(String.format("'%s'", s));
if (i != items.length - 1) {
sb.append(", ");
}
} else {
add(GremlinClause.OUT, registryLookup.getRelationshipEdgeLabelForActiveType(items[i]));
add(GremlinClause.AS, getCurrentStep());
addSelectClause(getCurrentStep());
incrementCurrentStep();
}
}
if (!StringUtils.isEmpty(sb.toString())) {
addValueMapClause(sb.toString());
}
}
public void addLimit(String limit, String offset) {
if (LOG.isDebugEnabled()) {
LOG.debug("addLimit(limit={}, offset={})", limit, offset);
}
add(GremlinClause.ORDER);
if (offset.equalsIgnoreCase("0")) {
add(GremlinClause.LIMIT, limit);
} else {
addRangeClause(offset, limit);
}
}
public void addGroupBy(String item) {
if (LOG.isDebugEnabled()) {
LOG.debug("addGroupBy(item={})", item);
}
add(GremlinClause.GROUP);
addByClause(item, false);
}
private void addRangeClause(String startIndex, String endIndex) {
if (LOG.isDebugEnabled()) {
LOG.debug("addRangeClause(startIndex={}, endIndex={})", startIndex, endIndex);
}
add(GremlinClause.RANGE, startIndex, startIndex, endIndex);
}
public String getText() {
String[] items = new String[queryClauses.size()];
for (int i = 0; i < queryClauses.size(); i++) {
items[i] = queryClauses.getValue(i);
}
String ret = StringUtils.join(items, ".");
if (LOG.isDebugEnabled()) {
LOG.debug("getText() => {}", ret);
}
return ret;
}
public void close() {
if(queryClauses.hasClause(GremlinClause.LIMIT) == -1) {
add(GremlinClause.LIMIT, "" + DEFAULT_QUERY_RESULT_LIMIT);
}
add(GremlinClause.TO_LIST);
}
public boolean hasSelect() {
return (queryClauses.hasClause(GremlinClause.VALUE_MAP) != -1);
}
public void addAsClause(String stepName) {
if (LOG.isDebugEnabled()) {
LOG.debug("addAsClause(stepName={})", stepName);
}
add(GremlinClause.AS, stepName);
registryLookup.registerStepType(stepName);
}
public void addOrderBy(String name, boolean isDesc) {
if (LOG.isDebugEnabled()) {
LOG.debug("addOrderBy(name={}, isDesc={})", name, isDesc);
}
add(GremlinClause.ORDER);
addByClause(registryLookup.getQualifiedAttributeName(name), isDesc);
}
private void addValueMapClause(String s) {
if (LOG.isDebugEnabled()) {
LOG.debug("addValueMapClause(s={})", s);
}
add(GremlinClause.VALUE_MAP, s);
}
private void addSelectClause(String s) {
if (LOG.isDebugEnabled()) {
LOG.debug("addSelectClause(s={})", s);
}
add(GremlinClause.SELECT, s);
}
private void addByClause(String name, boolean descr) {
if (LOG.isDebugEnabled()) {
LOG.debug("addByClause(name={})", name, descr);
}
add((!descr) ? GremlinClause.BY : GremlinClause.BY_DESC,
registryLookup.getQualifiedAttributeName(name));
}
private String getCurrentStep() {
return String.format("s%d", currentStep);
}
private void incrementCurrentStep() {
currentStep++;
}
private void add(GremlinClause clause, String... args) {
queryClauses.add(new GremlinClauseValue(clause, clause.get(args)));
}
private String extractIdentifier(String quotedIdentifier) {
String ret;
if (quotedIdentifier.charAt(0) == '`') {
ret = extract(BACKTICK_QUOTED_IDENTIFIER, quotedIdentifier);
} else if (quotedIdentifier.charAt(0) == '\'') {
ret = extract(SINGLE_QUOTED_IDENTIFIER, quotedIdentifier);
} else if (quotedIdentifier.charAt(0) == '"') {
ret = extract(DOUBLE_QUOTED_IDENTIFIER, quotedIdentifier);
} else {
ret = quotedIdentifier;
}
return ret;
}
private String extract(Pattern p, String s) {
Matcher m = p.matcher(s);
return m.find() ? m.group(1) : s;
}
private enum GremlinClause {
AS("as('%s')"),
BY("by('%s')"),
BY_DESC("by('%s', decr)"),
G("g"),
GROUP("group()"),
HAS("has('%s', %s)"),
HAS_OPERATOR("has('%s', %s(%s))"),
HAS_PROPERTY("has('%s')"),
HAS_NOT_PROPERTY("hasNot('%s')"),
HAS_TYPE("has('__typeName', '%s')"),
HAS_TYPE_WITHIN("has('__typeName', within(%s))"),
HAS_WITHIN("has('%s', within(%s))"),
IN("in()"),
LIMIT("limit(%s)"),
ORDER("order()"),
OUT("out('%s')"),
RANGE("range(%s, %s + %s)"),
SELECT("select('%s')"),
TO_LIST("toList()"),
TEXT_CONTAINS("has('%s', org.janusgraph.core.attribute.Text.textRegex(%s))"),
TEXT_PREFIX("has('%s', org.janusgraph.core.attribute.Text.textPrefix(%s))"),
TEXT_SUFFIX("has('%s', org.janusgraph.core.attribute.Text.textRegex(\".*\" + %s))"),
TRAIT("has('__traitNames', within('%s'))"),
V("V()"),
VALUE_MAP("valueMap(%s)");
private final String format;
GremlinClause(String format) {
this.format = format;
}
String get(String... args) {
return (args == null || args.length == 0) ?
format :
String.format(format, args);
}
}
private static class GremlinClauseValue {
private final GremlinClause clause;
private final String value;
public GremlinClauseValue(GremlinClause clause, String value) {
this.clause = clause;
this.value = value;
}
public GremlinClause getClause() {
return clause;
}
public String getValue() {
return value;
}
}
private static class GremlinClauseList {
private final List<String> errorList;
private AtlasEntityType activeType;
private final List<GremlinClauseValue> list;
private GremlinClauseList(List<String> errorList) {
this.errorList = errorList;
this.list = new LinkedList<>();
}
public void add(GremlinClauseValue g) {
list.add(g);
}
public void add(GremlinClauseValue g, AtlasEntityType t) {
add(g);
activeType = t;
}
public void add(GremlinClause clause, String... args) {
list.add(new GremlinClauseValue(clause, clause.get(args)));
}
public String getValue(int i) {
return list.get(i).value;
}
public int size() {
return list.size();
}
public int hasClause(GremlinClause clause) {
for (int i = 0; i < list.size(); i++) {
if (list.get(i).getClause() == clause)
return i;
}
return -1;
}
}
@VisibleForTesting
static class TypeRegistryLookup {
private final List<String> errorList;
private final AtlasTypeRegistry typeRegistry;
private AtlasEntityType activeType;
private final Map<String, AtlasEntityType> asClauseContext = new HashMap<>();
public TypeRegistryLookup(List<String> errorList, AtlasTypeRegistry typeRegistry) {
this.errorList = errorList;
this.typeRegistry = typeRegistry;
}
public void registerActive(String typeName) {
activeType = typeRegistry.getEntityTypeByName(typeName);
}
public boolean hasActiveType() {
return (activeType != null);
}
public void registerStepType(String stepName) {
if (!asClauseContext.containsKey(stepName)) {
asClauseContext.put(stepName, activeType);
} else {
addError(String.format("Multiple steps with same name detected: %s", stepName));
}
}
protected void addError(String s) {
errorList.add(s);
}
public String getRelationshipEdgeLabelForActiveType(String item) {
return getRelationshipEdgeLabel(activeType, item);
}
private String getRelationshipEdgeLabel(AtlasEntityType t, String item) {
if(t == null) {
return "";
}
AtlasStructType.AtlasAttribute attr = t.getAttribute(item);
return (attr != null) ? attr.getRelationshipEdgeLabel() : "";
}
protected boolean isAttributePrimitiveTypeForActiveType(String name) {
return isAttributePrimitiveType(activeType, name);
}
private boolean isAttributePrimitiveType(AtlasEntityType t, String name) {
if (activeType == null) {
return false;
}
AtlasType attrType = t.getAttributeType(name);
TypeCategory attrTypeCategory = attrType.getTypeCategory();
return (attrTypeCategory == TypeCategory.PRIMITIVE || attrTypeCategory == TypeCategory.ENUM);
}
public boolean isTypeTrait(String name) {
return (typeRegistry.getClassificationTypeByName(name) != null);
}
public String getQualifiedAttributeName(String item) {
if (item.contains(".")) {
String[] keyValue = StringUtils.split(item, ".");
if (!asClauseContext.containsKey(keyValue[0])) {
return item;
} else {
String s = getStitchedString(keyValue, 1, keyValue.length - 1);
return getQualifiedAttributeNameFromType(
asClauseContext.get(keyValue[0]), s);
}
}
return getQualifiedAttributeNameFromType(activeType, item);
}
protected String getStitchedString(String[] keyValue, int startIndex, int endIndex) {
if(startIndex == endIndex) {
return keyValue[startIndex];
}
return StringUtils.join(keyValue, ".", startIndex, endIndex);
}
private String getQualifiedAttributeNameFromType(AtlasEntityType t, String item) {
try {
return (t != null) ? t.getQualifiedAttributeName(item) : item;
} catch (AtlasBaseException e) {
addError(e.getMessage());
}
return item;
}
public String getTypeFromEdge(String item) {
AtlasType at = activeType.getAttribute(item).getAttributeType();
if(at.getTypeCategory() == TypeCategory.ARRAY) {
AtlasArrayType arrType = ((AtlasArrayType)at);
return ((AtlasBuiltInTypes.AtlasObjectIdType) arrType.getElementType()).getObjectType();
}
return activeType.getAttribute(item).getTypeName();
}
public boolean doesActiveTypeHaveSubTypes() {
return (activeType.getAllSubTypes().size() != 0);
}
public String getActiveTypeAndSubTypes() {
Set<String> set = activeType.getTypeAndAllSubTypes();
String[] str = set.toArray(new String[]{});
for (int i = 0; i < str.length; i++) {
str[i] = String.format("'%s'", str[i]);
}
return StringUtils.join(str, ",");
}
public boolean isSameAsActive(String typeName) {
return (activeType != null) && activeType.getTypeName().equalsIgnoreCase(typeName);
}
}
}