All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sap.cds.impl.SearchResolver Maven / Gradle / Ivy

There is a newer version: 3.8.0
Show newest version
/*******************************************************************
 * © 2020 SAP SE or an SAP affiliate company. All rights reserved. *
 *******************************************************************/
package com.sap.cds.impl;

import static com.sap.cds.impl.builder.model.Conjunction.and;
import static com.sap.cds.impl.builder.model.LiteralImpl.literal;
import static com.sap.cds.impl.parser.token.CqnBoolLiteral.FALSE;
import static java.util.Comparator.comparing;

import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

import com.sap.cds.impl.builder.model.ComparisonPredicate;
import com.sap.cds.impl.builder.model.Connective;
import com.sap.cds.impl.builder.model.Disjunction;
import com.sap.cds.impl.builder.model.ElementRefImpl;
import com.sap.cds.impl.builder.model.ExistsSubQuery;
import com.sap.cds.impl.builder.model.Negation;
import com.sap.cds.impl.builder.model.SubQuery;
import com.sap.cds.impl.util.Stack;
import com.sap.cds.ql.ElementRef;
import com.sap.cds.ql.RefSegment;
import com.sap.cds.ql.Select;
import com.sap.cds.ql.cqn.CqnConnectivePredicate;
import com.sap.cds.ql.cqn.CqnNegation;
import com.sap.cds.ql.cqn.CqnPredicate;
import com.sap.cds.ql.cqn.CqnSearchPredicate;
import com.sap.cds.ql.cqn.CqnSelect;
import com.sap.cds.ql.cqn.CqnVisitor;
import com.sap.cds.ql.impl.SelectBuilder;
import com.sap.cds.reflect.CdsElement;
import com.sap.cds.reflect.CdsEntity;
import com.sap.cds.reflect.CdsModel;
import com.sap.cds.reflect.CdsStructuredType;
import com.sap.cds.util.CdsSearchUtils;
import com.sap.cds.util.CqnStatementUtils;

public class SearchResolver {

	private final CdsModel model;

	public SearchResolver(CdsModel model) {
		this.model = model;
	}

	public CqnSelect resolve(CqnSelect select) {
		select.search().ifPresent(expression -> {
			CdsStructuredType targetType = CqnStatementUtils.targetType(model, select);
			Collection> searchableRefs = getSearchableElements(select, targetType);
			CqnPredicate filter = searchToContains(searchableRefs, expression);
			if (anyRefViaCollectionAssociation(targetType, searchableRefs)) {
				if (!(targetType instanceof CdsEntity)) {
					throw new UnsupportedOperationException(
							"A path expression used in search must originate from an entity");
				}
				CdsEntity targetEntity = (CdsEntity) targetType;
				filter = wrapIntoExistsSubquery(targetEntity, filter);
			}
			moveSearchToWhere(select, filter);
		});

		return select;
	}

	public CqnSelect resolveWithPath(CqnSelect select) {
		select.search().ifPresent(expression -> {
			CdsStructuredType targetType = CqnStatementUtils.targetType(model, select);
			Collection> searchableRefs = getSearchableElements(select, targetType);
			CqnPredicate filter = searchToContains(searchableRefs, expression);
			moveSearchToWhere(select, filter);
		});

		return select;
	}

	private static boolean anyRefViaCollectionAssociation(CdsStructuredType root, Collection> refs) {
		for (ElementRef ref : refs) {
			List segments = ref.segments();
			if (segments.size() > 1) {
				List prefix = segments.subList(0, segments.size() - 1);
				if (!CqnStatementUtils.isToOnePath(root, prefix)) {
					return true;
				}
			}
		}

		return false;
	}

	private static void moveSearchToWhere(CqnSelect select, CqnPredicate filter) {
		SelectBuilder selectBuilder = (SelectBuilder) select;
		selectBuilder.where(select.where().map(w -> and(w, filter)).orElse(filter));
		selectBuilder.search((CqnPredicate) null);
	}

	private CqnPredicate wrapIntoExistsSubquery(CdsEntity target, CqnPredicate search) {
		CqnPredicate toOuter = target.concreteNonAssociationElements().filter(e -> e.isKey())
				.sorted(comparing(CdsElement::getName)).map(e -> outerToInner(e.getName())).collect(and());

		CqnSelect subquery = Select.from(target).where(and(toOuter, search));

		return new ExistsSubQuery(subquery);
	}

	private static CqnPredicate outerToInner(String name) {
		ElementRef outer = ElementRefImpl.element(SubQuery.OUTER, name);
		ElementRef inner = ElementRefImpl.element(name);
		return ComparisonPredicate.eq(outer, inner);
	}

	private static CqnPredicate searchToContains(Collection> elements, CqnPredicate expression) {
		Stack stack = new Stack<>();
		CqnVisitor visitor = new CqnVisitor() {

			@Override
			public void visit(CqnSearchPredicate search) {
				stack.push(anyElementContains(search.searchTerm()));
			}

			@Override
			public void visit(CqnConnectivePredicate connective) {
				int n = connective.predicates().size();
				stack.push(Connective.create(connective.operator(), stack.pop(n)));
			}

			@Override
			public void visit(CqnNegation cqnNegation) {
				stack.push(Negation.not(stack.pop()));
			}

			private CqnPredicate anyElementContains(String searchTerm) {
				return elements.stream().map(e -> containsCaseInsensitive(e, searchTerm)).collect(Disjunction.or());
			}

		};
		expression.accept(visitor);

		if (stack.isEmpty()) {
			return FALSE;
		}
		return stack.pop();
	}

	private static Collection> getSearchableElements(CqnSelect select, CdsStructuredType targetType) {
		Collection searchableElements = ((SelectBuilder) select).searchableElements();
		if (!searchableElements.isEmpty()) {
			// are searchable elements set manually?
			return searchableElements.stream().map(ElementRefImpl::parse).collect(Collectors.toList());
		}
		return CdsSearchUtils.searchableElementRefs(targetType);
	}

	private static CqnPredicate containsCaseInsensitive(ElementRef element, String searchTerm) {
		return element.isNotNull().and(element.contains(literal(searchTerm), true));
	}

}