| /** |
| * 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.repository.graphdb.janus; |
| |
| import org.apache.atlas.exception.AtlasBaseException; |
| import org.apache.atlas.model.discovery.SearchParameters.FilterCriteria; |
| import org.apache.atlas.model.discovery.SearchParameters.Operator; |
| import org.apache.atlas.model.instance.AtlasEntity; |
| import org.apache.atlas.repository.Constants; |
| import org.apache.atlas.type.AtlasEntityType; |
| import org.apache.atlas.type.AtlasStructType.AtlasAttribute; |
| import org.apache.commons.collections.CollectionUtils; |
| import org.apache.commons.lang.StringUtils; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import java.util.*; |
| |
| import static org.apache.atlas.repository.Constants.CUSTOM_ATTRIBUTES_PROPERTY_KEY; |
| |
| public class AtlasSolrQueryBuilder { |
| private static final Logger LOG = LoggerFactory.getLogger(AtlasSolrQueryBuilder.class); |
| |
| private Set<AtlasEntityType> entityTypes; |
| private String queryString; |
| private FilterCriteria criteria; |
| private boolean excludeDeletedEntities; |
| private boolean includeSubtypes; |
| private Map<String, String> indexFieldNameCache; |
| public static final char CUSTOM_ATTR_SEPARATOR = '='; |
| public static final String CUSTOM_ATTR_SEARCH_FORMAT = "\"\\\"%s\\\":\\\"%s\\\"\""; |
| |
| |
| public AtlasSolrQueryBuilder() { |
| } |
| |
| public AtlasSolrQueryBuilder withEntityTypes(Set<AtlasEntityType> searchForEntityTypes) { |
| this.entityTypes = searchForEntityTypes; |
| |
| return this; |
| } |
| |
| public AtlasSolrQueryBuilder withQueryString(String queryString) { |
| this.queryString = queryString; |
| |
| return this; |
| } |
| |
| public AtlasSolrQueryBuilder withCriteria(FilterCriteria criteria) { |
| this.criteria = criteria; |
| |
| return this; |
| } |
| |
| public AtlasSolrQueryBuilder withExcludedDeletedEntities(boolean excludeDeletedEntities) { |
| this.excludeDeletedEntities = excludeDeletedEntities; |
| |
| return this; |
| } |
| |
| public AtlasSolrQueryBuilder withIncludeSubTypes(boolean includeSubTypes) { |
| this.includeSubtypes = includeSubTypes; |
| |
| return this; |
| } |
| |
| public AtlasSolrQueryBuilder withCommonIndexFieldNames(Map<String, String> indexFieldNameCache) { |
| this.indexFieldNameCache = indexFieldNameCache; |
| |
| return this; |
| } |
| |
| public String build() throws AtlasBaseException { |
| StringBuilder queryBuilder = new StringBuilder(); |
| boolean isAndNeeded = false; |
| |
| if (StringUtils.isNotEmpty(queryString)) { |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("Initial query string is {}.", queryString); |
| } |
| |
| queryBuilder.append("+").append(queryString.trim()).append(" "); |
| |
| isAndNeeded = true; |
| } |
| |
| if (excludeDeletedEntities) { |
| if (isAndNeeded) { |
| queryBuilder.append(" AND "); |
| } |
| |
| dropDeletedEntities(queryBuilder); |
| |
| isAndNeeded = true; |
| } |
| |
| if (CollectionUtils.isNotEmpty(entityTypes)) { |
| if (isAndNeeded) { |
| queryBuilder.append(" AND "); |
| } |
| |
| buildForEntityType(queryBuilder); |
| |
| isAndNeeded = true; |
| } |
| |
| if (criteria != null) { |
| StringBuilder attrFilterQueryBuilder = new StringBuilder(); |
| |
| withCriteria(attrFilterQueryBuilder, criteria); |
| |
| if (attrFilterQueryBuilder.length() != 0) { |
| if (isAndNeeded) { |
| queryBuilder.append(" AND "); |
| } |
| |
| queryBuilder.append(" ").append(attrFilterQueryBuilder.toString()); |
| } |
| } |
| |
| return queryBuilder.toString(); |
| } |
| |
| private void buildForEntityType(StringBuilder queryBuilder) { |
| |
| String typeIndexFieldName = indexFieldNameCache.get(Constants.ENTITY_TYPE_PROPERTY_KEY); |
| |
| queryBuilder.append(" +") |
| .append(typeIndexFieldName) |
| .append(":("); |
| |
| Set<String> typesToSearch = new HashSet<>(); |
| for (AtlasEntityType type : entityTypes) { |
| |
| if (includeSubtypes) { |
| typesToSearch.addAll(type.getTypeAndAllSubTypes()); |
| } else { |
| typesToSearch.add(type.getTypeName()); |
| } |
| } |
| |
| queryBuilder.append(StringUtils.join(typesToSearch, " ")).append(" ) "); |
| |
| } |
| |
| private void dropDeletedEntities(StringBuilder queryBuilder) throws AtlasBaseException { |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("excluding the deleted entities."); |
| } |
| |
| String indexFieldName = indexFieldNameCache.get(Constants.STATE_PROPERTY_KEY); |
| |
| if (indexFieldName == null) { |
| String msg = String.format("There is no index field name defined for attribute '%s'", |
| Constants.STATE_PROPERTY_KEY); |
| |
| LOG.error(msg); |
| |
| throw new AtlasBaseException(msg); |
| } |
| |
| queryBuilder.append(" -").append(indexFieldName).append(":").append(AtlasEntity.Status.DELETED.name()); |
| } |
| |
| private AtlasSolrQueryBuilder withCriteria(StringBuilder queryBuilder, FilterCriteria criteria) throws AtlasBaseException { |
| List<FilterCriteria> criterion = criteria.getCriterion(); |
| Set<String> indexAttributes = new HashSet<>(); |
| if (StringUtils.isNotEmpty(criteria.getAttributeName()) && CollectionUtils.isEmpty(criterion)) { // no child criterion |
| |
| String attributeName = criteria.getAttributeName(); |
| String attributeValue = criteria.getAttributeValue(); |
| Operator operator = criteria.getOperator(); |
| |
| ArrayList<StringBuilder> orExpQuery = new ArrayList<>(); |
| |
| for (AtlasEntityType type : entityTypes) { |
| String indexAttributeName = getIndexAttributeName(type, attributeName); |
| |
| //check to remove duplicate attribute query (for eg. name) |
| if (!indexAttributes.contains(indexAttributeName)) { |
| StringBuilder sb = new StringBuilder(); |
| |
| if (attributeName.equals(CUSTOM_ATTRIBUTES_PROPERTY_KEY)) { |
| // CustomAttributes stores key value pairs in String format, so ideally it should be 'contains' operator to search for one pair, |
| // for use-case, E1 having key1=value1 and E2 having key1=value2, searching key1=value1 results both E1,E2 |
| // surrounding inverted commas to attributeValue works |
| if (operator.equals(Operator.CONTAINS)) { |
| operator = Operator.EQ; |
| } else if (operator.equals(Operator.NOT_CONTAINS)) { |
| operator = Operator.NEQ; |
| } |
| attributeValue = getIndexQueryAttributeValue(attributeValue); |
| } |
| |
| withPropertyCondition(sb, indexAttributeName, operator, attributeValue); |
| indexAttributes.add(indexAttributeName); |
| orExpQuery.add(sb); |
| } |
| } |
| |
| if (CollectionUtils.isNotEmpty(orExpQuery)) { |
| if (orExpQuery.size() > 1) { |
| String orExpStr = StringUtils.join(orExpQuery, FilterCriteria.Condition.OR.name()); |
| queryBuilder.append(" ( ").append(orExpStr).append(" ) "); |
| } else { |
| queryBuilder.append(orExpQuery.iterator().next()); |
| } |
| } |
| |
| } else if (CollectionUtils.isNotEmpty(criterion)) { |
| beginCriteria(queryBuilder); |
| |
| for (Iterator<FilterCriteria> iterator = criterion.iterator(); iterator.hasNext(); ) { |
| FilterCriteria childCriteria = iterator.next(); |
| |
| withCriteria(queryBuilder, childCriteria); |
| |
| if (iterator.hasNext()) { |
| withCondition(queryBuilder, criteria.getCondition().name()); |
| } |
| } |
| |
| endCriteria(queryBuilder); |
| } |
| |
| return this; |
| } |
| |
| private void withPropertyCondition(StringBuilder queryBuilder, String indexFieldName, Operator operator, String attributeValue) throws AtlasBaseException { |
| if (StringUtils.isNotEmpty(indexFieldName) && operator != null) { |
| if (attributeValue != null) { |
| attributeValue = attributeValue.trim(); |
| } |
| |
| beginCriteria(queryBuilder); |
| |
| switch (operator) { |
| case EQ: |
| withEqual(queryBuilder, indexFieldName, attributeValue); |
| break; |
| case NEQ: |
| withNotEqual(queryBuilder, indexFieldName, attributeValue); |
| break; |
| case STARTS_WITH: |
| withStartsWith(queryBuilder, indexFieldName, attributeValue); |
| break; |
| case ENDS_WITH: |
| withEndsWith(queryBuilder, indexFieldName, attributeValue); |
| break; |
| case CONTAINS: |
| withContains(queryBuilder, indexFieldName, attributeValue); |
| break; |
| case NOT_CONTAINS: |
| withNotContains(queryBuilder, indexFieldName, attributeValue); |
| break; |
| case IS_NULL: |
| withIsNull(queryBuilder, indexFieldName); |
| break; |
| case NOT_NULL: |
| withIsNotNull(queryBuilder, indexFieldName); |
| break; |
| case LT: |
| withLessthan(queryBuilder, indexFieldName, attributeValue); |
| break; |
| case GT: |
| withGreaterThan(queryBuilder, indexFieldName, attributeValue); |
| break; |
| case LTE: |
| withLessthanOrEqual(queryBuilder, indexFieldName, attributeValue); |
| break; |
| case GTE: |
| withGreaterThanOrEqual(queryBuilder, indexFieldName, attributeValue); |
| break; |
| case IN: |
| case LIKE: |
| case CONTAINS_ANY: |
| case CONTAINS_ALL: |
| default: |
| String msg = String.format("%s is not supported operation.", operator.getSymbol()); |
| LOG.error(msg); |
| throw new AtlasBaseException(msg); |
| } |
| |
| endCriteria(queryBuilder); |
| } |
| } |
| |
| private String getIndexQueryAttributeValue(String attributeValue) { |
| |
| if (StringUtils.isNotEmpty(attributeValue)) { |
| int separatorIdx = attributeValue.indexOf(CUSTOM_ATTR_SEPARATOR); |
| String key = separatorIdx != -1 ? attributeValue.substring(0, separatorIdx) : null; |
| String value = key != null ? attributeValue.substring(separatorIdx + 1) : null; |
| |
| if (key != null && value != null) { |
| return String.format(CUSTOM_ATTR_SEARCH_FORMAT, key, value); |
| } |
| } |
| |
| return attributeValue; |
| } |
| |
| private String getIndexAttributeName(AtlasEntityType type, String attrName) throws AtlasBaseException { |
| AtlasAttribute ret = type.getAttribute(attrName); |
| |
| if (ret == null) { |
| String msg = String.format("Received unknown attribute '%s' for type '%s'.", attrName, type.getTypeName()); |
| |
| LOG.error(msg); |
| |
| throw new AtlasBaseException(msg); |
| } |
| |
| String indexFieldName = ret.getIndexFieldName(); |
| |
| if (indexFieldName == null) { |
| String msg = String.format("Received non-index attribute %s for type %s.", attrName, type.getTypeName()); |
| |
| LOG.error(msg); |
| |
| throw new AtlasBaseException(msg); |
| } |
| |
| return indexFieldName; |
| } |
| |
| private void beginCriteria(StringBuilder queryBuilder) { |
| queryBuilder.append("( "); |
| } |
| |
| private void endCriteria(StringBuilder queryBuilder) { |
| queryBuilder.append(" )"); |
| } |
| |
| private void withEndsWith(StringBuilder queryBuilder, String indexFieldName, String attributeValue) { |
| queryBuilder.append("+").append(indexFieldName).append(":*").append(attributeValue).append(" "); |
| } |
| |
| private void withStartsWith(StringBuilder queryBuilder, String indexFieldName, String attributeValue) { |
| queryBuilder.append("+").append(indexFieldName).append(":").append(attributeValue).append("* "); |
| } |
| |
| private void withNotEqual(StringBuilder queryBuilder, String indexFieldName, String attributeValue) { |
| queryBuilder.append("*:* -").append(indexFieldName).append(":").append(attributeValue).append(" "); |
| } |
| |
| private void withEqual(StringBuilder queryBuilder, String indexFieldName, String attributeValue) { |
| queryBuilder.append("+").append(indexFieldName).append(":").append(attributeValue).append(" "); |
| } |
| |
| private void withGreaterThan(StringBuilder queryBuilder, String indexFieldName, String attributeValue) { |
| //{ == exclusive |
| //] == inclusive |
| //+__timestamp_l:{<attributeValue> TO *] |
| queryBuilder.append("+").append(indexFieldName).append(":{ ").append(attributeValue).append(" TO * ] "); |
| } |
| |
| private void withGreaterThanOrEqual(StringBuilder queryBuilder, String indexFieldName, String attributeValue) { |
| //[ == inclusive |
| //] == inclusive |
| //+__timestamp_l:[<attributeValue> TO *] |
| queryBuilder.append("+").append(indexFieldName).append(":[ ").append(attributeValue).append(" TO * ] "); |
| } |
| |
| private void withLessthan(StringBuilder queryBuilder, String indexFieldName, String attributeValue) { |
| //[ == inclusive |
| //} == exclusive |
| //+__timestamp_l:[* TO <attributeValue>} |
| queryBuilder.append("+").append(indexFieldName).append(":[ * TO").append(attributeValue).append("} "); |
| } |
| |
| private void withLessthanOrEqual(StringBuilder queryBuilder, String indexFieldName, String attributeValue) { |
| //[ == inclusive |
| //[ == inclusive |
| //+__timestamp_l:[* TO <attributeValue>] |
| queryBuilder.append("+").append(indexFieldName).append(":[ * TO ").append(attributeValue).append(" ] "); |
| } |
| |
| private void withContains(StringBuilder queryBuilder, String indexFieldName, String attributeValue) { |
| queryBuilder.append("+").append(indexFieldName).append(":*").append(attributeValue).append("* "); |
| } |
| |
| private void withNotContains(StringBuilder queryBuilder, String indexFieldName, String attributeValue) { |
| queryBuilder.append("*:* -").append(indexFieldName).append(":*").append(attributeValue).append("* "); |
| } |
| |
| private void withIsNull(StringBuilder queryBuilder, String indexFieldName) { |
| queryBuilder.append("-").append(indexFieldName).append(":*").append(" "); |
| } |
| |
| private void withIsNotNull(StringBuilder queryBuilder, String indexFieldName) { |
| queryBuilder.append("+").append(indexFieldName).append(":*").append(" "); |
| } |
| |
| private void withCondition(StringBuilder queryBuilder, String condition) { |
| queryBuilder.append(" ").append(condition).append(" "); |
| } |
| } |