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

com.sap.cds.jdbc.hana.search.HanaSearchResolver Maven / Gradle / Ivy

There is a newer version: 3.4.0
Show newest version
/************************************************************************
 * © 2020-2024 SAP SE or an SAP affiliate company. All rights reserved. *
 ************************************************************************/
package com.sap.cds.jdbc.hana.search;

import static com.sap.cds.DataStoreConfiguration.SEARCH_HANA_FUZZINESS;
import static com.sap.cds.DataStoreConfiguration.SEARCH_HANA_FUZZINESS_DEFAULT;
import static com.sap.cds.DataStoreConfiguration.SEARCH_HANA_FUZZY;
import static com.sap.cds.reflect.CdsBaseType.HANA_CLOB;
import static com.sap.cds.reflect.CdsBaseType.LARGE_STRING;
import static com.sap.cds.util.CdsSearchUtils.moveSearchToWhere;

import java.math.BigDecimal;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.sap.cds.DataStoreConfiguration;
import com.sap.cds.impl.parser.token.CqnBoolLiteral;
import com.sap.cds.impl.util.Stack;
import com.sap.cds.jdbc.generic.AbstractSearchResolver;
import com.sap.cds.jdbc.spi.SearchResolver;
import com.sap.cds.ql.CQL;
import com.sap.cds.ql.cqn.CqnConnectivePredicate;
import com.sap.cds.ql.cqn.CqnElementRef;
import com.sap.cds.ql.cqn.CqnNegation;
import com.sap.cds.ql.cqn.CqnPredicate;
import com.sap.cds.ql.cqn.CqnReference.Segment;
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.reflect.CdsElement;
import com.sap.cds.reflect.CdsModel;
import com.sap.cds.reflect.CdsStructuredType;
import com.sap.cds.reflect.CdsType;
import com.sap.cds.util.CdsModelUtils;
import com.sap.cds.util.CdsSearchUtils;

/**
 * A {@link SearchResolver} implementation that renders a CONTAINS|SCORE
 * function in an EXISTS subquery.
 */
public abstract class HanaSearchResolver extends AbstractSearchResolver {

	private static final Logger logger = LoggerFactory.getLogger(HanaSearchResolver.class);
	private static Pattern wildcardPattern = Pattern.compile("(? searchableRefs) {

		Set like = new TreeSet<>(ElementRefComparator.INSTANCE);
		Map scoreOrContains = new TreeMap<>(ElementRefComparator.INSTANCE);

		/*
		 * Locale resolution only needs to be performed if a language/locale has been
		 * given in the session context
		 */
		boolean languageGiven = locale != null;

		/*
		 * On HANA (prior to HANA CLoud Q1/2021) CONTAINS cannot search over columns
		 * originating from a subquery. The current draft implementation depends on a
		 * subquery added late in the SQL generation cycle. Thus, the CONTAINS
		 * expression must not operate on the main branch of the statement which would
		 * access the draft subquery's columns but needs to be pushed down to an EXISTS
		 * subquery where the CONTAINS is not affected by other subqueries.
		 */
		boolean pushToSubquery = false;

		/*
		 * if any ref is navigating any 1:n association enforce pushed down to subquery
		 */
		boolean navigatesToManyAssoc = false;

		for (CqnElementRef ref : searchableRefs) {
			CdsElement element = CdsModelUtils.element(targetType, ref);
			CdsType type = element.getType();
			if (type.isSimpleType(LARGE_STRING) || type.isSimpleType(HANA_CLOB)) {
				handleLargeStringElement(like, scoreOrContains, targetType, ref, element);
			} else if (element.isLocalized()) {
				pushToSubquery = handleLocalizedElement(targetType, like, scoreOrContains, languageGiven, ref, element);
			} else {
				handleRegularElement(targetType, like, scoreOrContains, ref, element);
			}
			pushToSubquery = pushToSubquery || needsPushToSubquery(element);
			navigatesToManyAssoc = navigatesToManyAssoc || navigatesToManyAssoc(targetType, ref);
		}

		attachSearchExpressionsToStatement(select, search, targetType, scoreOrContains, pushToSubquery, like,
				navigatesToManyAssoc);
	}

	protected abstract void handleLargeStringElement(Set like,
			Map scoreOrContains, CdsStructuredType targetType, CqnElementRef ref, CdsElement element);

	protected abstract boolean handleLocalizedElement(CdsStructuredType targetType, Set like,
			Map scoreOrContains, boolean languageGiven,
			CqnElementRef ref, CdsElement element);

	protected abstract void handleRegularElement(CdsStructuredType targetType, Set like,
			Map scoreOrContains, CqnElementRef ref, CdsElement element);

	private void attachSearchExpressionsToStatement(CqnSelect select, CqnPredicate search, CdsStructuredType targetType,
			Map scoreOrContains, boolean pushToSubquery, Set like, boolean navigatesToManyAssoc) {
		CqnPredicate filter = CqnBoolLiteral.FALSE;

		if (!scoreOrContains.isEmpty()) {
			CqnPredicate searchPredicate = searchToHana(scoreOrContains, search);
			if (navigatesToManyAssoc || pushToSubquery || !like.isEmpty()) {
				searchPredicate = pushDownToExistsSubquery(targetType, searchPredicate, true);
			}
			filter = CQL.or(filter, searchPredicate);
		}

		if (!like.isEmpty()) {
			if (logger.isDebugEnabled()) {
				String names = refNames(like);
				logger.debug("The following searchable element refs of {} that can be searched with LIKE: {}.",
						targetType, names);
			}
			CqnPredicate likePredicate = CdsSearchUtils.searchToLikeExpression(like, search);
			if (navigatesToManyAssoc) {
				likePredicate = pushDownToExistsSubquery(targetType, likePredicate, true);
			}
			filter = CQL.or(filter, likePredicate);
		}

		moveSearchToWhere(select, filter);
	}

	private String refNames(Collection refs) {
		return refs.stream().map(ref -> ref.asValue().displayName()).collect(Collectors.joining(","));
	}

	public record SearchString(String searchString, boolean containsWildcards) {
	}

	protected static SearchString toSearchString(CqnPredicate expression, boolean fuzzy) {
		Stack stack = new Stack<>();
		boolean[] wildcards = new boolean[] { false };

		CqnVisitor visitor = new CqnVisitor() {

			@Override
			public void visit(CqnSearchPredicate search) {
				String searchTerm = search.searchTerm();
				boolean phrase = searchTerm.trim().contains(" ");
				boolean wildcardInSearchTerm = wildcardPattern.matcher(searchTerm).find();
				wildcards[0] |= wildcardInSearchTerm;
				if (!wildcardInSearchTerm && !fuzzy) {
					searchTerm = "*" + searchTerm + "*";
				}
				if (phrase) {
					searchTerm = "\"" + searchTerm + "\"";
				}
				stack.push(searchTerm);
			}

			@Override
			public void visit(CqnConnectivePredicate connective) {
				int n = connective.predicates().size();
				String delimiter = connective.operator() == CqnConnectivePredicate.Operator.AND ? " " : " OR ";

				stack.push(String.join(delimiter, stack.pop(n)));
			}

			@Override
			public void visit(CqnNegation cqnNegation) {
				stack.push("-" + stack.pop());
			}

		};

		expression.accept(visitor);

		if (stack.isEmpty()) {
			throw new IllegalStateException("the search term stack must not be empty!");
		}
		return new SearchString(stack.pop(), wildcards[0]);
	}

	private static class ElementRefComparator implements Comparator {
		static final Comparator INSTANCE = new ElementRefComparator();

		@Override
		public int compare(CqnElementRef ref1, CqnElementRef ref2) {
			List segs1 = ref1.segments();
			List segs2 = ref2.segments();
			int n1 = segs1.size();
			int n2 = segs2.size();
			if (n1 > n2) {
				return 1;
			}
			if (n1 < n2) {
				return -1;
			}
			for (int i = 0; i < n1; i++) {
				String id1 = segs1.get(i).id();
				String id2 = segs2.get(i).id();
				int cmp = id1.compareTo(id2);
				if (cmp != 0) {
					return cmp;
				}
			}
			return 0;
		}
	}

	protected abstract CqnPredicate searchToHana(Map searchableRefs, CqnPredicate expression);

	protected abstract boolean needsPushToSubquery(CdsElement element);
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy