Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.sap.cds.adapter.odata.v4.query.SystemQueryLoader Maven / Gradle / Ivy
/**************************************************************************
* (C) 2019-2024 SAP SE or an SAP affiliate company. All rights reserved. *
**************************************************************************/
package com.sap.cds.adapter.odata.v4.query;
import static com.sap.cds.adapter.odata.v4.utils.ElementUtils.aliasedGet;
import static com.sap.cds.adapter.odata.v4.utils.ElementUtils.aliasedTo;
import static com.sap.cds.ql.CQL.get;
import static com.sap.cds.ql.CQL.to;
import static org.apache.olingo.server.api.uri.queryoption.SystemQueryOptionKind.APPLY;
import static org.apache.olingo.server.api.uri.queryoption.SystemQueryOptionKind.FORMAT;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.lang3.StringUtils;
import org.apache.olingo.server.api.uri.UriInfo;
import org.apache.olingo.server.api.uri.UriResource;
import org.apache.olingo.server.api.uri.UriResourceNavigation;
import org.apache.olingo.server.api.uri.queryoption.ApplyOption;
import org.apache.olingo.server.api.uri.queryoption.ExpandItem;
import org.apache.olingo.server.api.uri.queryoption.ExpandOption;
import org.apache.olingo.server.api.uri.queryoption.SearchOption;
import org.apache.olingo.server.api.uri.queryoption.SelectItem;
import org.apache.olingo.server.api.uri.queryoption.SelectOption;
import org.apache.olingo.server.api.uri.queryoption.SystemQueryOption;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.sap.cds.adapter.odata.v4.query.apply.ApplyHandler;
import com.sap.cds.adapter.odata.v4.query.apply.OrderByTransformation;
import com.sap.cds.adapter.odata.v4.query.apply.SearchTransformation;
import com.sap.cds.adapter.odata.v4.utils.ETagHelper;
import com.sap.cds.adapter.odata.v4.utils.ElementUtils;
import com.sap.cds.adapter.odata.v4.utils.StreamUtils;
import com.sap.cds.adapter.odata.v4.utils.mapper.EdmxFlavourMapper;
import com.sap.cds.adapter.odata.v4.utils.mapper.EdmxFlavourMapper.EdmxFlavour;
import com.sap.cds.impl.builder.model.PassThroughSearchPredicate;
import com.sap.cds.ql.Expand;
import com.sap.cds.ql.Select;
import com.sap.cds.ql.StructuredType;
import com.sap.cds.ql.cqn.CqnPredicate;
import com.sap.cds.ql.cqn.CqnReference;
import com.sap.cds.ql.cqn.CqnReference.Segment;
import com.sap.cds.ql.cqn.CqnSelectListItem;
import com.sap.cds.ql.cqn.CqnSelectListValue;
import com.sap.cds.ql.cqn.CqnStar;
import com.sap.cds.ql.cqn.CqnToken;
import com.sap.cds.ql.cqn.CqnValue;
import com.sap.cds.reflect.CdsEntity;
import com.sap.cds.reflect.CdsStructuredType;
import com.sap.cds.services.environment.CdsProperties;
import com.sap.cds.services.utils.CdsErrorStatuses;
import com.sap.cds.services.utils.ErrorStatusException;
import com.sap.cds.util.CdsModelUtils;
import com.sap.cds.util.transformations.TransformationToSelect;
public class SystemQueryLoader {
private static final Logger logger = LoggerFactory.getLogger(SystemQueryLoader.class);
private static final String SEARCH_ODATA_LENIENT = "odata-lenient";
private static final String SEARCH_PASS_THROUGH = "pass-through";
private final EdmxFlavourMapper elementMapper;
private final EdmxFlavour flavour;
private final boolean transformationsInCqn;
private final CdsProperties cdsProperties;
private SystemQueryLoader(EdmxFlavourMapper elementMapper, EdmxFlavour flavour, CdsProperties cdsProperties, boolean transformationsInCqn) {
this.elementMapper = elementMapper;
this.flavour = flavour;
this.transformationsInCqn = transformationsInCqn;
this.cdsProperties = cdsProperties;
}
public static SystemQueryLoader create(EdmxFlavourMapper elementMapper, EdmxFlavour flavour, CdsProperties cdsProperties) {
return new SystemQueryLoader(elementMapper, flavour, cdsProperties, false);
}
public static SystemQueryLoader create(EdmxFlavourMapper elementMapper, EdmxFlavour flavour, CdsProperties cdsProperties, boolean transformationsInCqn) {
return new SystemQueryLoader(elementMapper, flavour, cdsProperties, transformationsInCqn);
}
public List> updateSelectQuery(Select> select, UriInfo uriInfo, CdsEntity entity) {
List> selects;
ApplyOption apply = uriInfo.getApplyOption();
if (apply == null) {
select.columns(unfold(entity, CqnStar.star()));
addSystemQueryOptions(select, uriInfo, entity);
selects = List.of(select);
} else {
ApplyHandler applyHandler = ApplyHandler.create(apply, new ExpressionParser(entity, elementMapper));
if (applyHandler.hasConcat()) {
rejectOtherSystemQueryOptions(uriInfo);
}
List> unfolded = applyHandler.transform(select);
selects = new ArrayList<>(unfolded.size());
for (Select> s : unfolded) {
addSystemQueryOptions(s, uriInfo, entity);
TransformationToSelect transformer = new TransformationToSelect(s);
if (transformationsInCqn) {
transformer.applyOrderAndLimit();
} else {
transformer.applyTransformations();
}
selects.add(transformer.get());
}
}
return selects;
}
private void rejectOtherSystemQueryOptions(UriInfo uriInfo) {
uriInfo.getSystemQueryOptions()
.stream().map(SystemQueryOption::getKind)
.filter(k -> k != APPLY && k != FORMAT).findAny()
.ifPresent(k -> { throw new ErrorStatusException(CdsErrorStatuses.UNSUPPORTED_CONCAT_SYSTEMQUERY, k.toString());});
}
private void addSystemQueryOptions(Select> select, UriInfo uriInfo, CdsEntity entity) {
ExpressionParser expressionParser = new ExpressionParser(entity, elementMapper);
if (uriInfo.getOrderByOption() != null) {
OrderByTransformation transformation = new OrderByTransformation(uriInfo.getOrderByOption(), expressionParser);
select.orderBy(transformation.orderBy());
}
if (uriInfo.getFilterOption() != null) {
select.where(expressionParser.parseFilter(uriInfo.getFilterOption().getExpression()));
}
SearchOption searchOption = uriInfo.getSearchOption();
if (searchOption != null) {
String searchMode = cdsProperties.getOdataV4().getSearchMode();
if (SEARCH_PASS_THROUGH.equalsIgnoreCase(searchMode)) {
// cds.odataV2.searchMode: pass-through
select.search(new PassThroughSearchPredicate(uriInfo.getSearchOption().getSearchString()));
} else if (searchOption.getSearchExpression() != null) {
// cds.odataV4.searchMode: odata-strict | odata-lenient
CqnPredicate searchPredicate = SearchTransformation.convertSearchOption(uriInfo.getSearchOption());
select.search(searchPredicate);
} else if (SEARCH_ODATA_LENIENT.equalsIgnoreCase(searchMode)) {
// cds.odataV4.searchMode: odata-lenient
logger.debug("Failed to parse search string, fall back to pass-through search");
String searchString = searchOption.getSearchString();
select.search(new PassThroughSearchPredicate(searchString));
} else {
throw new ErrorStatusException(CdsErrorStatuses.SEARCH_PARSING_FAILED);
}
}
if (uriInfo.getCountOption() != null) {
if (uriInfo.getCountOption().getValue()) {
select.inlineCount();
}
}
}
public Select> updateSelectColumns(Select> select, UriInfo uriInfo, CdsEntity entity) {
if (uriInfo.getSelectOption() != null) {
// select limits the items determined from star select (default) or $apply
Stream items = select.items().stream().flatMap(sli -> unfold(entity, sli));
select.columns(reduce(items, getSelectColumns(uriInfo.getSelectOption(), entity)));
}
if (uriInfo.getExpandOption() != null) {
// expands always adds to the available items and overrides matching simple items
select.columns(merge(select.items(), getExpandColumns(uriInfo.getExpandOption(), entity)));
}
return select;
}
public Select> updateSelect(Select> select, UriInfo uriInfo, CdsEntity entity) {
select = updateSelectQuery(select, uriInfo, entity).get(0);
return updateSelectColumns(select, uriInfo, entity);
}
private List getSelectColumns(SelectOption selectOption, CdsEntity entity) {
Set items = new HashSet<>();
entity.keyElements().map(key -> key.getName()).forEach(items::add);
String etagElement = ETagHelper.getETagElementName(entity);
if(etagElement != null) {
items.add(etagElement);
}
for (SelectItem selectItem : selectOption.getSelectItems()) {
if (selectItem.isStar()) {
items.clear();
ElementUtils.recursiveElements(entity).keySet().forEach(items::add);
break;
} else if (selectItem.getResourcePath() != null) {
List uriResourceParts = selectItem.getResourcePath().getUriResourceParts();
if (uriResourceParts.size() > 1) {
throw new ErrorStatusException(CdsErrorStatuses.SELECT_PARSING_FAILED);
}
String element = elementMapper.remap(uriResourceParts.get(0).getSegmentValue(), entity);
items.add(element);
// select media type for selected stream properties
String mediaTypeElement = StreamUtils.getCoreMediaTypeElement(entity, element);
if (!StringUtils.isEmpty(mediaTypeElement)) {
items.add(mediaTypeElement);
}
}
}
return new ArrayList<>(items);
}
private List getExpandColumns(ExpandOption expandOption, CdsEntity entity) {
List expands = new ArrayList<>();
List expandItems = expandOption.getExpandItems();
if (expandItems.size() == 1 && expandItems.get(0).isStar()) {
entity.associations().map(a -> to(a.getName()).expand()).forEach(expands::add);
return expands;
}
for (ExpandItem expandItem : expandItems) {
if (expandItem.isStar()) {
throw new ErrorStatusException(CdsErrorStatuses.INVALID_EXPAND_STAR);
}
for (UriResource uriResource : expandItem.getResourcePath().getUriResourceParts()) {
if (uriResource instanceof UriResourceNavigation) {
String associationName = elementMapper.remap(uriResource.getSegmentValue(), entity);
StructuredType> type = aliasedTo(associationName);
CdsEntity navigationEntity = entity.getTargetOf(associationName);
ExpressionParser expressionParser = new ExpressionParser(navigationEntity, elementMapper);
// select all by default
List items = navigationEntity.nonAssociationElements().map(e -> get(e.getName())).collect(Collectors.toList());
if(expandItem.getSelectOption() != null) {
// select limits the items determined from star select or (future) $apply
Stream slvs = items.stream().flatMap(sli -> unfold(navigationEntity, sli));
items = reduce(slvs, getSelectColumns(expandItem.getSelectOption(), navigationEntity));
}
if(expandItem.getFilterOption() != null) {
type.filter(expressionParser.parseFilter(expandItem.getFilterOption().getExpression()));
}
if (expandItem.getExpandOption() != null) {
// expands always adds to the available items and overrides matching simple items
items = merge(items, getExpandColumns(expandItem.getExpandOption(), navigationEntity));
}
Expand> expand = type.expand(items);
if (expandItem.getOrderByOption() != null) {
OrderByTransformation transformation = new OrderByTransformation(expandItem.getOrderByOption(), expressionParser);
expand.orderBy(transformation.orderBy());
}
int top = Integer.MAX_VALUE;
int skip = 0;
if (expandItem.getTopOption() != null) {
top = expandItem.getTopOption().getValue();
}
if (expandItem.getSkipOption() != null) {
skip = expandItem.getSkipOption().getValue();
}
if (top < Integer.MAX_VALUE || skip > 0) {
expand.limit(top, skip);
}
applyInlineCount(entity, expandItem, associationName, expand);
expands.add(expand);
}
}
}
return expands;
}
private static void applyInlineCount(CdsEntity entity, ExpandItem expandItem, String associationName, Expand> expand) {
// $count at the end of URL should trigger pure count
if (expandItem.hasCountPath() || expandItem.getCountOption() != null) {
if (CdsModelUtils.isSingleValued(entity.getAssociation(associationName).getType())) {
throw new ErrorStatusException(CdsErrorStatuses.INLINE_COUNT_ON_TO_ONE_ASSOC, associationName);
}
if (expandItem.hasCountPath()) {
expand.limit(0).inlineCount();
// Otherwise just pass the count to the statement
} else if (expandItem.getCountOption() != null && expandItem.getCountOption().getValue()) {
expand.inlineCount();
}
}
}
private static List reduce(Stream slvs, List allowedColumns) {
return slvs.filter(slv -> allowedColumns.stream().anyMatch(c -> c.equals(slv.displayName()) || c.startsWith(slv.displayName() + "."))).collect(Collectors.toList());
}
private static List merge(List columns, List additionalColumns) {
if (additionalColumns.isEmpty()) {
return columns;
}
List items = new ArrayList<>(columns);
for (CqnSelectListItem newItem : additionalColumns) {
List extends Segment> newSegments = getSegments(newItem.token());
// remove the potential matching item
items.stream().filter(c -> isMatching(c, newSegments)).findFirst().ifPresent(items::remove);
items.add(newItem);
}
return items;
}
private Stream unfold(CdsStructuredType type, CqnSelectListItem sli) {
if (sli.isValue()) {
return Stream.of(sli.asValue());
}
if (sli.isStar()) {
Stream elements = ElementUtils.recursiveElements(type, e -> !e.getType().isAssociation())
.keySet().stream().map(e -> aliasedGet(e));
if(flavour == EdmxFlavour.V4) {
Stream associations = ElementUtils.recursiveElements(type, e -> e.getType().isAssociation() && CdsModelUtils.managedToOne(e.getType()))
.keySet().stream().map(e -> aliasedGet(e));
elements = Stream.concat(elements, associations);
}
return elements;
}
return Stream.empty();
}
private static boolean isMatching(CqnSelectListItem c, List extends Segment> newSegments) {
List extends Segment> segments = getSegments(c.token());
if (segments == null || newSegments == null || segments.size() != newSegments.size()) {
return false;
}
for (int i = 0; i < segments.size(); ++i) {
Segment segment = segments.get(i);
Segment newSegment = newSegments.get(i);
if (!segment.id().equals(newSegment.id())) {
return false;
}
if (segment.filter().isPresent()) {
if (!segment.toJson().equals(newSegment.toJson())) {
return false;
}
} else {
if (newSegment.filter().isPresent()) {
return false;
}
}
}
return true;
}
private static List extends Segment> getSegments(CqnToken token) {
if (token instanceof CqnReference reference) {
return reference.segments();
} else if (token instanceof Expand> expand) {
return expand.ref().segments();
} else if (token instanceof CqnSelectListValue listValue) {
CqnValue value = listValue.value();
if (value.isRef()) {
return value.asRef().segments();
}
}
return null;
}
}