com.sap.cds.jdbc.generic.AbstractSearchResolver Maven / Gradle / Ivy
/************************************************************************
* © 2022-2024 SAP SE or an SAP affiliate company. All rights reserved. *
************************************************************************/
package com.sap.cds.jdbc.generic;
import static com.sap.cds.DataStoreConfiguration.SEARCH_MODE;
import static com.sap.cds.DataStoreConfiguration.SEARCH_MODE_GENERIC;
import static com.sap.cds.reflect.impl.reader.model.CdsConstants.ANNOTATION_CDS_SEARCH_MODE;
import static com.sap.cds.util.CdsModelUtils.concreteKeyNames;
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.DataStoreConfiguration;
import com.sap.cds.impl.builder.model.InSubquery;
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.ElementRef;
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 DataStoreConfiguration config;
protected final CdsModel model;
protected final Locale locale;
protected AbstractSearchResolver(DataStoreConfiguration config, CdsModel cdsModel, Locale locale) {
this.config = config;
this.model = cdsModel;
this.locale = locale;
}
protected abstract String defaultSearchMode();
protected String configuredSearchMode() {
return config.getProperty(SEARCH_MODE, defaultSearchMode());
}
protected String searchMode(CdsStructuredType targetType) {
return targetType.getAnnotationValue(ANNOTATION_CDS_SEARCH_MODE, configuredSearchMode());
}
@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);
if (SEARCH_MODE_GENERIC.equalsIgnoreCase(searchMode(targetType))) {
resolveUsingLocalizedViewWithLike(select, search, targetType, searchableRefs);
} else {
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 wrapIntoInSubquery(CdsEntity target, CqnPredicate search,
boolean ignoreLocalizedViews) {
List> keys = concreteKeyNames(target).stream().sorted().map(CQL::get).toList();
Select> subquery = Select.from(target).columns(keys).where(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 InSubquery.in(CQL.list(keys), subquery);
}
protected static boolean anyRefViaCollectionAssociation(CdsStructuredType root, Collection refs) {
for (CqnElementRef ref : refs) {
List extends Segment> segments = ref.segments();
if (segments.size() > 1) {
List extends Segment> 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 extends Segment> segments = ref.segments();
List extends Segment> 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 = wrapIntoInSubquery(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.isEmpty()) {
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( // NOSONAR
"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) {
CqnPredicate filter = CdsSearchUtils.searchToLikeExpression(searchableRefs, expression);
if (anyRefViaCollectionAssociation(targetType, searchableRefs)) {
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