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

com.sap.cds.jdbc.generic.AbstractSearchResolver Maven / Gradle / Ivy

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

import static com.sap.cds.impl.builder.model.Conjunction.and;
import static com.sap.cds.reflect.impl.reader.model.CdsConstants.ANNOTATION_CDS_SEARCH_MODE;
import static com.sap.cds.util.CdsSearchUtils.getSearchableElements;
import static com.sap.cds.util.CdsSearchUtils.moveSearchToWhere;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.Set;

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

import com.sap.cds.impl.builder.model.ExistsSubquery;
import com.sap.cds.impl.parser.token.CqnBoolLiteral;
import com.sap.cds.jdbc.spi.SearchResolver;
import com.sap.cds.ql.CQL;
import com.sap.cds.ql.Select;
import com.sap.cds.ql.cqn.CqnElementRef;
import com.sap.cds.ql.cqn.CqnPredicate;
import com.sap.cds.ql.cqn.CqnReference.Segment;
import com.sap.cds.ql.cqn.CqnSelect;
import com.sap.cds.ql.cqn.CqnSelectListItem;
import com.sap.cds.ql.impl.SelectBuilder;
import com.sap.cds.reflect.CdsAssociationType;
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.CdsModelUtils;
import com.sap.cds.util.CdsSearchUtils;
import com.sap.cds.util.CqnStatementUtils;

/**
 * Holding common methods used by other search resolvers
 */
public abstract class AbstractSearchResolver implements SearchResolver {

	private static final Logger logger = LoggerFactory.getLogger(AbstractSearchResolver.class);
	private static final String LOCALIZED = "localized";
	protected final CdsModel model;
	protected final Locale locale;

	protected AbstractSearchResolver(CdsModel cdsModel, Locale locale) {
		this.model = cdsModel;
		this.locale = locale;
	}

	public static String getSearchModeAnnotation(CdsStructuredType targetType) {
		return targetType.getAnnotationValue(ANNOTATION_CDS_SEARCH_MODE, "auto");
	}

	@Override
	public CqnSelect resolve(CqnSelect select) {
		select.search().ifPresent(search -> {
			if (logger.isDebugEnabled()) {
				logger.debug("Starting resolution of the following search operation: {}", select.toJson());
			}

			CdsStructuredType targetType = CqnStatementUtils.targetType(model, select);
			Collection searchableRefs = getSearchableElements(select, targetType);

			resolve(select, search, targetType, searchableRefs);

			logger.debug("Finished resolution of search with this result: {}", select);
		});
		return select;
	}

	protected abstract void resolve(CqnSelect select, CqnPredicate search, CdsStructuredType targetType,
			Collection searchableRefs);

	protected static CqnPredicate wrapIntoExistsSubquery(CdsEntity target, CqnPredicate search,
			boolean ignoreLocalizedViews) {
		CqnPredicate toOuter = CqnStatementUtils.linkKeysToOuterQuery(target);
		Select subquery = Select.from(target).where(and(toOuter, search));

		/*
		 * we don't want to have this in the API. Thus, casting to the implementation is
		 * the only option unless some different mechanism to activate this mode is
		 * decided. Discussions in that area are ongoing at the moment.
		 */
		if (ignoreLocalizedViews) {
			subquery.hint(SelectBuilder.IGNORE_LOCALIZED_VIEWS, true);
		}
		subquery.hint(SelectBuilder.IGNORE_DRAFT_SUBQUERIES, true);

		return new ExistsSubquery(subquery);
	}

	protected static boolean anyRefViaCollectionAssociation(CdsStructuredType root, Collection refs) {
		for (CqnElementRef 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;
	}

	protected static boolean navigatesToManyAssoc(CdsStructuredType root, CqnElementRef ref) {
		List segments = ref.segments();
		List prefix = segments.subList(0, segments.size() - 1);

		return !CqnStatementUtils.isToOnePath(root, prefix);
	}

	protected static CqnPredicate pushDownToExistsSubquery(CdsStructuredType targetType, CqnPredicate filter,
			boolean ignoreLocalizedViews) {
		if (filter == CqnBoolLiteral.FALSE) {
			return CqnBoolLiteral.FALSE;
		}
		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, ignoreLocalizedViews);
		return filter;
	}

	protected boolean allLocalizedElementsAreReachableViaLocalizedAssociation(CdsStructuredType targetType,
			Collection searchableRefs, Collection badRefs) {
		Collection notReachableViaLocalizedRefs = searchableRefs.stream()
				.filter(ref -> localizedButNotReachableViaLocalizedRef(targetType, ref)).collect(toSet());

		if (!notReachableViaLocalizedRefs.isEmpty()) {
			logger.warn(
					"Detected one or more localized elements in entity/view {} that is not reachable corresponding 'localized' association: {}. "
							+ "Search will fall back to LIKE and localized views.",
					targetType, notReachableViaLocalizedRefs);
			badRefs.addAll(notReachableViaLocalizedRefs);
			return false;
		}

		return true;
	}

	private static boolean localizedButNotReachableViaLocalizedRef(CdsStructuredType targetType, CqnElementRef ref) {
		CdsElement element = CdsModelUtils.element(targetType, ref);
		if (!element.isLocalized()) {
			return false;
		}

		return !isReachableViaLocalizedAssoc(element);
	}

	protected static boolean isReachableViaLocalizedAssoc(CdsElement element) {
		Optional localizedAssoc = element.getDeclaringType().as(CdsStructuredType.class)
				.findAssociation(LOCALIZED);
		if (!localizedAssoc.isPresent()) {
			return false;
		}
		CdsEntity texts = localizedAssoc.get().getType().as(CdsAssociationType.class).getTarget();
		return texts.findElement(element.getName()).isPresent();
	}

	public List addRefsViaLocalizedAssociation(CdsStructuredType targetType,
			Collection searchableRefs) {
		List allSearchableRefs = new ArrayList<>(searchableRefs);

		searchableRefs.stream().filter(ref -> CdsModelUtils.element(targetType, ref).isLocalized())
				.map(this::localizedRef).forEach(allSearchableRefs::add);

		return deduplicate(allSearchableRefs);
	}

	protected CqnElementRef localizedRef(CqnElementRef ref) {
		List searchSegments = new LinkedList<>(ref.segments());
		searchSegments.add(searchSegments.size() - 1, CQL.refSegment(LOCALIZED));
		return CQL.get(searchSegments);
	}

	protected boolean hasAliasedLocalizedElementsInView(CdsStructuredType targetType,
			Collection searchableRefs, Collection badRefs) {
		if (!(targetType instanceof CdsEntity)) {
			return false; // no view
		}

		Optional query = ((CdsEntity) targetType).query();
		if (query.isPresent()) {
			Set aliasedLocalizedElement = query.get().items().stream()
					// is and has alias
					.filter(i -> i.isRef() && i.asValue().alias().isPresent())
					.filter(i -> isSearchable(i, searchableRefs)) // is in searchableRefs
					// exists in model
					.filter(i -> CdsModelUtils.findElement(targetType, i.asValue().value().asRef()).isPresent())
					.filter(i -> isElementBehindRefLocalized(i, targetType)).map(i -> i.asValue().value().asRef())
					.peek(i -> logger.debug("found aliased localized element {} in {} that"
							+ " consequently cannot be rendered with CONTAINS.", i, targetType))
					.collect(toSet());
			if (!aliasedLocalizedElement.isEmpty()) {
				badRefs.addAll(aliasedLocalizedElement);
				return true;
			}

		}

		return false;
	}

	private boolean isElementBehindRefLocalized(CqnSelectListItem sli, CdsStructuredType targetType) {
		CdsElement element = CdsModelUtils.element(targetType, sli.asRef());
		return element.isLocalized();
	}

	protected boolean isSearchable(CqnSelectListItem sli, Collection searchableRefs) {
		return searchableRefs.stream().anyMatch(s -> s.asValue().displayName().equals(sli.asValue().displayName()));
	}

	private List deduplicate(List allSearchableRefs) {
		Set paths = new HashSet<>(allSearchableRefs.size());
		return allSearchableRefs.stream().filter(r -> paths.add(r.path())).collect(toList());
	}

	protected void resolveUsingLocalizedViewWithLike(CqnSelect select, CqnPredicate expression,
			CdsStructuredType targetType, Collection searchableRefs, boolean pushToExistsSubQuery) {
		CqnPredicate filter = CdsSearchUtils.searchToLikeExpression(searchableRefs, expression);
		if (anyRefViaCollectionAssociation(targetType, searchableRefs) || pushToExistsSubQuery) {
			filter = pushDownToExistsSubquery(targetType, filter, false);
		}
		moveSearchToWhere(select, filter);
	}

	protected void resolveUsingLocalizedAssociationWithLike(CqnSelect select, CqnPredicate expression,
			CdsStructuredType targetType, Collection searchableRefs) {
		List allSearchableRefs = addRefsViaLocalizedAssociation(targetType, searchableRefs);

		CqnPredicate filter = CdsSearchUtils.searchToLikeExpression(allSearchableRefs, expression);
		filter = pushDownToExistsSubquery(targetType, filter, true);

		moveSearchToWhere(select, filter);
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy