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

com.sap.cds.adapter.odata.v4.query.ExpressionParser 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.ql.CQL.func;
import static com.sap.cds.ql.CQL.val;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

import org.apache.olingo.commons.api.edm.EdmEnumType;
import org.apache.olingo.commons.api.edm.EdmType;
import org.apache.olingo.server.api.ODataApplicationException;
import org.apache.olingo.server.api.uri.UriInfo;
import org.apache.olingo.server.api.uri.UriResource;
import org.apache.olingo.server.api.uri.UriResourceEntitySet;
import org.apache.olingo.server.api.uri.UriResourceKind;
import org.apache.olingo.server.api.uri.UriResourceLambdaAll;
import org.apache.olingo.server.api.uri.UriResourceLambdaAny;
import org.apache.olingo.server.api.uri.UriResourceNavigation;
import org.apache.olingo.server.api.uri.queryoption.apply.AggregateExpression.StandardMethod;
import org.apache.olingo.server.api.uri.queryoption.expression.BinaryOperatorKind;
import org.apache.olingo.server.api.uri.queryoption.expression.Expression;
import org.apache.olingo.server.api.uri.queryoption.expression.ExpressionVisitException;
import org.apache.olingo.server.api.uri.queryoption.expression.ExpressionVisitor;
import org.apache.olingo.server.api.uri.queryoption.expression.Literal;
import org.apache.olingo.server.api.uri.queryoption.expression.Member;
import org.apache.olingo.server.api.uri.queryoption.expression.MethodKind;
import org.apache.olingo.server.api.uri.queryoption.expression.UnaryOperatorKind;

import com.google.common.annotations.VisibleForTesting;
import com.sap.cds.adapter.odata.v4.query.apply.ElementAggregator;
import com.sap.cds.adapter.odata.v4.utils.TypeConverterUtils;
import com.sap.cds.adapter.odata.v4.utils.mapper.EdmxFlavourMapper;
import com.sap.cds.impl.builder.model.CqnNull;
import com.sap.cds.ql.CQL;
import com.sap.cds.ql.ElementRef;
import com.sap.cds.ql.Predicate;
import com.sap.cds.ql.StructuredType;
import com.sap.cds.ql.Value;
import com.sap.cds.ql.cqn.CqnPredicate;
import com.sap.cds.ql.cqn.CqnReference.Segment;
import com.sap.cds.ql.cqn.CqnStructuredTypeRef;
import com.sap.cds.ql.cqn.CqnToken;
import com.sap.cds.ql.cqn.CqnValue;
import com.sap.cds.reflect.CdsAssociationType;
import com.sap.cds.reflect.CdsElement;
import com.sap.cds.reflect.CdsStructuredType;
import com.sap.cds.reflect.CdsType;
import com.sap.cds.services.ErrorStatuses;
import com.sap.cds.services.utils.CdsErrorStatuses;
import com.sap.cds.services.utils.ErrorStatusException;
import com.sap.cds.util.CdsModelUtils;

public class ExpressionParser {

	private final CdsStructuredType rootType;
	private final EdmxFlavourMapper elementMapper;

	public ExpressionParser(CdsStructuredType rootType, EdmxFlavourMapper elementMapper) {
		this.rootType = rootType;
		this.elementMapper = elementMapper;
	}

	public CqnValue parseValue(Expression expression) {
		try {
			return (CqnValue) expression.accept(new ExpressionToCqnVisitor());
		} catch (ExpressionVisitException | ODataApplicationException e) {
			throw new ErrorStatusException(CdsErrorStatuses.VALUE_PARSING_FAILED, e);
		}
	}

	public CqnToken parseToken(Expression expression) {
		try {
			return (CqnToken) expression.accept(new ExpressionToCqnVisitor());
		} catch (ExpressionVisitException | ODataApplicationException e) {
			throw new ErrorStatusException(CdsErrorStatuses.VALUE_PARSING_FAILED, e);
		}
	}
	
	public CqnStructuredTypeRef parseStructuredTypeRef(Expression expression) {
		try {
			return (CqnStructuredTypeRef) expression.accept(new ExpressionToCqnVisitor());
		} catch (ExpressionVisitException | ODataApplicationException e) {
			throw new ErrorStatusException(CdsErrorStatuses.VALUE_PARSING_FAILED, e);
		}
	}
	
	public CqnPredicate parseFilter(Expression expression) {
		try {
			return (CqnPredicate) expression.accept(new ExpressionToCqnVisitor());
		} catch (ODataApplicationException | ExpressionVisitException e) {
			throw new ErrorStatusException(CdsErrorStatuses.FILTER_PARSING_FAILED, e);
		}
	}

	public List toSegmentList(List uriResourceParts) {
		List segmentList = new ArrayList<>();
		CdsStructuredType type = rootType;
		String prefix = null;
		for (UriResource part : uriResourceParts) {
			if (part.getKind() == UriResourceKind.root) {
				prefix = rootType.getQualifier() + ".";
				continue; // TODO? skip so far
			}
			if (part.getKind() != UriResourceKind.lambdaVariable) {
				if (type == null) {
					// uri contains navigation property or complex type property
					// however the previous property already returned a simple element
					throw new ErrorStatusException(CdsErrorStatuses.UNEXPECTED_URI_RESOURCE, part.getKind());
				}

				String segment = elementMapper.remap(part.getSegmentValue(), type);
				type = structuredType(type.findElement(segment).orElse(null));
				for (String id : segment.split("\\.")) {
					if (prefix != null) {
						id = prefix + id;
						prefix = null;
					}
					segmentList.add(CQL.refSegment(id));
				}
			}
		}
		return segmentList;
	}

	public String remap(String element) {
		return elementMapper.remap(element, rootType);
	}

	private CdsStructuredType structuredType(CdsElement element) {
		if (element != null) {
			CdsType type = element.getType();
			if (type.isStructured()) {
				return type.as(CdsStructuredType.class);
			}
			if (type.isAssociation()) {
				return type.as(CdsAssociationType.class).getTarget();
			}
		}
		return null;
	}

	public CdsStructuredType getRootType() {
		return rootType;
	}

	public CqnStructuredTypeRef getTargetTypeRef(Expression exr) {
		List resources = ((Member) exr).getResourcePath().getUriResourceParts();
		UriResource last = resources.get(resources.size() - 1);
		return switch (last.getKind()) {
			case navigationProperty -> CQL.to(((UriResourceNavigation) last).getType().getName()).asRef();
			case entitySet -> CQL.to(((UriResourceEntitySet) last).getEntityType().getName()).asRef();
			default ->
				throw new ErrorStatusException(CdsErrorStatuses.UNEXPECTED_URI_RESOURCE, last.getKind());
		};
	}

	@VisibleForTesting
	class ExpressionToCqnVisitor implements ExpressionVisitor {

		@Override
		@SuppressWarnings({ "unchecked", "rawtypes" })
		public Object visitBinaryOperator(BinaryOperatorKind operator, Object left, Object right) {
			switch (operator) {
			case AND:
				return ((Predicate) left).and((Predicate) right);
			case OR:
				return ((Predicate) left).or((Predicate) right);

			case EQ:
				if (left instanceof Predicate lhs && right instanceof Predicate rhs) {
					Predicate bothTrue = lhs.and(rhs);
					Predicate bothFalse = lhs.not().and(rhs.not());
					return bothTrue.or(bothFalse);
				}
				return ((Value) left).is((Value) right);
			case NE:
				if (left instanceof Predicate lhs && right instanceof Predicate rhs) {
					Predicate eitherTrue = lhs.or(rhs);
					Predicate eitherFalse = lhs.not().or(rhs.not());
					return eitherTrue.and(eitherFalse);
				}
				return ((Value) left).isNot((Value) right);
			case GE:
				return ((Value) left).ge((Value) right);
			case GT:
				return ((Value) left).gt((Value) right);
			case LE:
				return ((Value) left).le((Value) right);
			case LT:
				return ((Value) left).lt((Value) right);
			case ADD:
				return ((Value) left).plus((Value) right);
			case SUB:
				return ((Value) left).minus((Value) right);
			case MUL:
				return ((Value) left).times((Value) right);
			case DIV:
				return ((Value) left).dividedBy((Value) right);

			case MOD:
			case HAS:
			default:
				throw new ErrorStatusException(CdsErrorStatuses.UNSUPPORTED_OPERATOR, operator);
			}
		}

		@Override
		public Object visitUnaryOperator(UnaryOperatorKind operator, Object operand) {
			switch (operator) {
			case NOT:
				return ((Predicate) operand).not();
			default:
				throw new ErrorStatusException(ErrorStatuses.NOT_IMPLEMENTED);
			}
		}

		@Override
		@SuppressWarnings("unchecked")
		public Object visitMethodCall(MethodKind methodCall, List parameters) {
			Value value = (Value) parameters.get(0);
			String func = methodCall.name().toUpperCase(Locale.US);

			switch (func) {
			case "TOUPPER":
				return value.toUpper();
			case "TOLOWER":
				return value.toLower();
			case "LENGTH":
			case "TRIM":
				// TODO replace with dedicated builder functions once available in CDS4J
				return func(methodCall.name(), value);
			case "SUBSTRING":
				Value start = null;
				if (parameters.size() >= 2) {
					start = (Value) parameters.get(1);
				}
				switch (parameters.size()) {
				case 2:
					return value.substring(start);
				case 3:
					Value length = (Value) parameters.get(2);
					return value.substring(start, length);
				default:
					throw new ErrorStatusException(CdsErrorStatuses.INVALID_SUBSTRING);
				}
			case "CONTAINS":
				Value substring = (Value) parameters.get(1);
				return value.contains(substring);
			case "STARTSWITH":
				Value prefix = (Value) parameters.get(1);
				return value.startsWith(prefix);
			case "ENDSWITH":
				Value suffix = (Value) parameters.get(1);
				return value.endsWith(suffix);
			case "COMPUTE_AGGREGATE", "CURRENT_COLLECTION_AGGREGATE":
				return value;
			case "MATCHES_PATTERN":
				return matchesPattern(value, parameters.subList(1, parameters.size()));
			default:
				throw new ErrorStatusException(CdsErrorStatuses.UNSUPPORTED_METHOD, func);
			}
		}

		@SuppressWarnings("unchecked")
		private Predicate matchesPattern(Value value, List parameters) {
			var options = parameters.size() > 1 ? ((Value)parameters.get(1)).asLiteral().asString().value() : "";
			if (options.isBlank()) {
				return value.matchesPattern((Value) parameters.get(0));
			} else {
				return value.matchesPattern((Value) parameters.get(0), CQL.val(options));
			}
		}

		@Override
		public Object visitLambdaExpression(String lambdaFunction, String lambdaVariable, Expression expression) {
			throw new ErrorStatusException(ErrorStatuses.NOT_IMPLEMENTED);
		}

		@Override
		public Value visitLiteral(Literal literal) {
			if (literal.getText() == null || literal.getType() == null) {
				return CqnNull.getInstance();
			}
			Object literalValue = TypeConverterUtils.convertToType(literal.getType(), literal.getText());

			return val(literalValue);
		}

		@Override
		public CqnToken visitMember(Member member) throws ExpressionVisitException, ODataApplicationException {
			List uriResourceParts = member.getResourcePath().getUriResourceParts();
			UriResource lastPart = uriResourceParts.get(uriResourceParts.size() - 1);
			if (lastPart.getKind() == UriResourceKind.lambdaAny) {
				StructuredType ref = CQL.to(toSegmentList(uriResourceParts.subList(0, uriResourceParts.size() - 1)));
				UriResourceLambdaAny lambdaAny = (UriResourceLambdaAny) lastPart;
				if (lambdaAny.getExpression() != null) {
					CdsStructuredType target = CdsModelUtils.target(rootType, ref.asRef().segments());
					CqnPredicate pred = (CqnPredicate) lambdaAny.getExpression().accept((new ExpressionParser(target, elementMapper)).new ExpressionToCqnVisitor());
					return ref.anyMatch(pred);
				}
				return ref.exists();
			}
			if (lastPart.getKind() == UriResourceKind.lambdaAll) {
				StructuredType ref = CQL.to(toSegmentList(uriResourceParts.subList(0, uriResourceParts.size() - 1)));
				UriResourceLambdaAll lambdaAll = (UriResourceLambdaAll) lastPart;
				CdsStructuredType target = CdsModelUtils.target(rootType, ref.asRef().segments());
				Predicate pred = (Predicate) lambdaAll.getExpression().accept((new ExpressionParser(target, elementMapper)).new ExpressionToCqnVisitor());

				return ref.allMatch(pred);
			}
			if (lastPart.getKind() == UriResourceKind.entitySet) {
				return CQL.to(toSegmentList(uriResourceParts)).asRef();
			}
			return CQL.get(toSegmentList(uriResourceParts));
		}

		@Override
		public Object visitAlias(String aliasName) {
			throw new ErrorStatusException(ErrorStatuses.NOT_IMPLEMENTED);
		}

		@Override
		public Object visitTypeLiteral(EdmType type) {
			throw new ErrorStatusException(ErrorStatuses.NOT_IMPLEMENTED);
		}

		@Override
		public Object visitLambdaReference(String variableName) {
			throw new ErrorStatusException(ErrorStatuses.NOT_IMPLEMENTED);
		}

		@Override
		public Object visitEnum(EdmEnumType type, @SuppressWarnings("rawtypes") List enumValues) {
			throw new ErrorStatusException(ErrorStatuses.NOT_IMPLEMENTED);
		}

		@Override
		@SuppressWarnings({ "rawtypes", "unchecked" })
		public Predicate visitBinaryOperator(BinaryOperatorKind operator, Object left, List right) {

			if (operator == BinaryOperatorKind.IN) {
				Value[] items = right.toArray(new Value[right.size()]);
				return ((Value) left).in(items);
			}

			throw new ErrorStatusException(ErrorStatuses.NOT_IMPLEMENTED);
		}

		@Override
		public CqnValue visitComputeAggregate(StandardMethod standardMethod, UriInfo path, Object expression)
				throws ExpressionVisitException, ODataApplicationException {
			ElementAggregator aggregator = new ElementAggregator(ExpressionParser.this);

			Value func;
			if (standardMethod != null) {
				func = aggregator.toFunctionCall((Value) expression, standardMethod);
			} else {
				List uriResourceParts = path.asUriInfoResource().getUriResourceParts();
				ElementRef ref = CQL.get(toSegmentList(uriResourceParts));
				func = aggregator.customAggregate(ref);
			}

			return func;
		}

	}
}