com.sap.cds.adapter.odata.v2.query.SystemQueryLoader Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of cds-adapter-odata-v2 Show documentation
Show all versions of cds-adapter-odata-v2 Show documentation
OData V2 adapter for CDS Services Java
/**************************************************************************
* (C) 2019-2024 SAP SE or an SAP affiliate company. All rights reserved. *
**************************************************************************/
package com.sap.cds.adapter.odata.v2.query;
import static com.sap.cds.ql.CQL.get;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.olingo.odata2.api.commons.InlineCount;
import org.apache.olingo.odata2.api.edm.EdmException;
import org.apache.olingo.odata2.api.exception.ODataApplicationException;
import org.apache.olingo.odata2.api.uri.NavigationPropertySegment;
import org.apache.olingo.odata2.api.uri.SelectItem;
import org.apache.olingo.odata2.api.uri.UriInfo;
import org.apache.olingo.odata2.api.uri.expression.ExceptionVisitExpression;
import org.apache.olingo.odata2.api.uri.expression.FilterExpression;
import org.apache.olingo.odata2.api.uri.expression.OrderByExpression;
import org.apache.olingo.odata2.api.uri.expression.OrderExpression;
import org.apache.olingo.odata2.api.uri.expression.SortOrder;
import com.sap.cds.adapter.odata.v2.query.expand.ExpandItem;
import com.sap.cds.adapter.odata.v2.utils.AggregateTransformation;
import com.sap.cds.adapter.odata.v2.utils.ETagHelper;
import com.sap.cds.adapter.odata.v2.utils.ExpandItemTreeBuilder;
import com.sap.cds.ql.CQL;
import com.sap.cds.ql.Expand;
import com.sap.cds.ql.Orderable;
import com.sap.cds.ql.Predicate;
import com.sap.cds.ql.Select;
import com.sap.cds.ql.StructuredType;
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.CqnSortSpecification;
import com.sap.cds.ql.cqn.CqnToken;
import com.sap.cds.ql.cqn.CqnValue;
import com.sap.cds.reflect.CdsElement;
import com.sap.cds.reflect.CdsEntity;
import com.sap.cds.reflect.CdsModel;
import com.sap.cds.reflect.CdsService;
import com.sap.cds.services.environment.CdsProperties;
import com.sap.cds.services.environment.CdsProperties.Query.Limit;
import com.sap.cds.services.request.RequestContext;
import com.sap.cds.services.runtime.CdsRuntime;
import com.sap.cds.services.utils.CdsErrorStatuses;
import com.sap.cds.services.utils.ErrorStatusException;
import com.sap.cds.services.utils.TenantAwareCache;
public class SystemQueryLoader {
private static TenantAwareCache limitLookup;
public static void initialize(CdsRuntime runtime) {
Limit config = runtime.getEnvironment().getCdsProperties().getQuery().getLimit();
limitLookup = TenantAwareCache.create(
() -> RequestContext.getCurrent(runtime).getUserInfo().getTenant(),
() -> new LimitLookup(config),
() -> RequestContext.getCurrent(runtime).getModel());
}
private SystemQueryLoader() {
}
/**
* Add system query options such as $top, $skip, $inlinecount to the select
* statement.
*
* @param select Select statement
* @param uriInfo request
* @param limit boolean value
* @param service CdsService
* @param entity CdsEntity
* @return NextLinkInfo
*/
public static NextLinkInfo applySystemQueryOptions(Select> select, UriInfo uriInfo, boolean limit,
CdsService service, CdsEntity entity, CdsProperties properties) {
NextLinkInfo nextLinkInfo = null;
// select all by default
select.columns(entity.nonAssociationElements().map(e -> get(e.getName())));
if (limit) {
int top = Integer.MAX_VALUE;
int skip = 0;
if (uriInfo.getTop() != null) {
top = uriInfo.getTop();
}
if (uriInfo.getSkipToken() != null) {
// we just treat the skip token as the direct skip value
skip = Integer.parseInt(uriInfo.getSkipToken());
if (top != Integer.MAX_VALUE) {
int diff = skip - (uriInfo.getSkip() != null ? uriInfo.getSkip() : 0);
top = Math.max(top - diff, 0);
}
} else if (uriInfo.getSkip() != null) {
// skip is only evaluated if there is no skiptoken
skip = uriInfo.getSkip();
}
boolean serverDrivenPaging = false;
int defaultTop = limitLookup.findOrCreate().getDefaultValue(service, entity);
if (defaultTop > 0 && top == Integer.MAX_VALUE) {
top = defaultTop;
// top was set server-side
serverDrivenPaging = true;
}
int maxTop = limitLookup.findOrCreate().getMaxValue(service, entity);
if (maxTop > 0 && top > maxTop) {
top = maxTop;
// top was limited server-side
serverDrivenPaging = true;
}
if (top < Integer.MAX_VALUE || skip > 0) {
// Set top & skip
select.limit(top, skip);
if (serverDrivenPaging) {
nextLinkInfo = new NextLinkInfo(top, skip);
}
}
}
// Set inlinecount
InlineCount inlineCount = uriInfo.getInlineCount();
if (inlineCount == InlineCount.ALLPAGES) {
select.inlineCount();
}
boolean caseInsensitive = !properties.getOdataV2().isCaseSensitiveFilter();
// Order by
if (uriInfo.getOrderBy() != null) {
select.orderBy(convertOrderByOption(uriInfo.getOrderBy(), caseInsensitive));
}
// Filter
if (uriInfo.getFilter() != null) {
select.where(convertFilterOption(uriInfo.getFilter(), caseInsensitive));
}
return nextLinkInfo;
}
private static List convertOrderByOption(OrderByExpression orderByOption, boolean caseInsensitive) {
List sortList = new ArrayList<>();
for (OrderExpression orderByItem : orderByOption.getOrders()) {
try {
Orderable orderByElement = (Orderable) orderByItem.getExpression().accept(new ExpressionToCqnVisitor(caseInsensitive));
SortOrder sortOrder = orderByItem.getSortOrder();
sortList.add(sortOrder == SortOrder.desc ? orderByElement.desc() : orderByElement.asc());
} catch (ODataApplicationException | ExceptionVisitExpression e) {
throw new ErrorStatusException(CdsErrorStatuses.ORDERBY_PARSING_FAILED, e);
}
}
return sortList;
}
private static Predicate convertFilterOption(FilterExpression filter, boolean caseInsensitive) {
try {
return (Predicate) filter.getExpression().accept(new ExpressionToCqnVisitor(caseInsensitive));
} catch (ODataApplicationException | ExceptionVisitExpression e) {
throw new ErrorStatusException(CdsErrorStatuses.FILTER_PARSING_FAILED, e);
}
}
public static void updateSelectColumns(Select> select, UriInfo uriInfo, CdsEntity entity) throws EdmException {
if (uriInfo.getSelect() != null && !uriInfo.getSelect().isEmpty()) {
// select limits the items determined from star select (default)
select.columns(reduce(select.items(), getSelectColumns(uriInfo.getSelect(), entity)));
}
if (uriInfo.getExpand() != null && !uriInfo.getExpand().isEmpty()) {
// expands always adds to the available items and overrides matching simple items
ExpandItem expandItem = ExpandItemTreeBuilder.buildTree(uriInfo.getExpand(),
SystemQueryLoader::getNavigationPropertyName);
select.columns(merge(select.items(), getExpands(expandItem, entity)));
}
}
private static String getNavigationPropertyName(NavigationPropertySegment nps) {
try {
return nps.getNavigationProperty().getName();
} catch (EdmException e) {
// This exception is never thrown by Olingo, as the corresponding EdmNamedImplProv.getName()
// simply returns a string.
throw new IllegalStateException(e);
}
}
private static List reduce(List columns, List allowedColumns) {
return columns.stream().filter(i -> i.isValue() && allowedColumns.contains(i.asValue().displayName()))
.collect(Collectors.toList());
}
private static List getSelectColumns(List selectItems, CdsEntity entity) throws EdmException {
Set items = new HashSet<>();
entity.keyElements().map(key -> key.getName()).forEach(items::add);
Optional etagElement = ETagHelper.getETagElement(entity);
if (etagElement.isPresent()) {
items.add(etagElement.get().getName());
}
for (SelectItem selectItem : selectItems) {
if (selectItem.isStar()) {
items.clear();
entity.nonAssociationElements().map(CdsElement::getName).forEach(items::add);
break;
} else {
if (selectItem.getProperty() != null) {
String name = selectItem.getProperty().getName();
// exclude technical ID__ from select list as it is not in CDS Model
if (!AggregateTransformation.isAggregateID(name, entity)) {
items.add(name);
}
}
if (selectItem.getNavigationPropertySegments() != null) {
List navItems = selectItem.getNavigationPropertySegments();
for (NavigationPropertySegment segment : navItems) {
if (segment.getNavigationProperty() != null) {
items.add(segment.getNavigationProperty().getName());
}
}
}
}
}
return new ArrayList<>(items);
}
private static List getExpands(ExpandItem expandItems, CdsEntity entity) {
List expands = new ArrayList<>();
expandItems.forEach((npName, item) -> {
StructuredType> type = CQL.to(npName);
CdsEntity navigationTarget = entity.getTargetOf(npName);
Expand> expand;
if (item.isEmpty()) {
// if there is no further navigation, use *, as it instructs the CDS4J to return also nulls
expand = type.expand(CQL.star());
} else {
List items = navigationTarget.nonAssociationElements().map(e -> get(e.getName()))
.collect(Collectors.toList());
items = merge(items, getExpands(item, navigationTarget));
expand = type.expand(items);
}
expands.add(expand);
});
return expands;
}
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 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;
}
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;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy