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

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

There is a newer version: 3.2.0
Show newest version
/**************************************************************************
 * (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 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 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;

	}

	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;
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy