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

com.sap.cds.adapter.odata.v2.query.SystemQueryLoader Maven / Gradle / Ivy

There is a newer version: 3.6.0
Show newest version
/**************************************************************************
 * (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 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 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 newSegments) {
		List 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