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

com.sap.cds.ql.impl.MatchPredicateNormalizer Maven / Gradle / Ivy

The newest version!
/************************************************************************
 * © 2021-2023 SAP SE or an SAP affiliate company. All rights reserved. *
 ************************************************************************/
package com.sap.cds.ql.impl;

import static com.sap.cds.impl.builder.model.Conjunction.and;
import static com.sap.cds.impl.builder.model.StructuredTypeRefImpl.typeRef;
import static com.sap.cds.impl.parser.token.RefSegmentImpl.refSegment;
import static com.sap.cds.ql.CQL.not;
import static com.sap.cds.ql.cqn.CqnMatchPredicate.Quantifier.ALL;

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import com.sap.cds.impl.AssociationAnalyzer;
import com.sap.cds.impl.builder.model.ElementRefImpl;
import com.sap.cds.impl.builder.model.ExistsSubquery;
import com.sap.cds.impl.builder.model.MatchPredicate;
import com.sap.cds.ql.CQL;
import com.sap.cds.ql.Predicate;
import com.sap.cds.ql.Select;
import com.sap.cds.ql.StructuredTypeRef;
import com.sap.cds.ql.cqn.CqnElementRef;
import com.sap.cds.ql.cqn.CqnExistsSubquery;
import com.sap.cds.ql.cqn.CqnExpand;
import com.sap.cds.ql.cqn.CqnMatchPredicate;
import com.sap.cds.ql.cqn.CqnMatchPredicate.Quantifier;
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.CqnSelect;
import com.sap.cds.ql.cqn.CqnSelectListItem;
import com.sap.cds.ql.cqn.CqnStatement;
import com.sap.cds.ql.cqn.CqnStructuredTypeRef;
import com.sap.cds.ql.cqn.CqnValidationException;
import com.sap.cds.ql.cqn.CqnValue;
import com.sap.cds.ql.cqn.CqnVisitor;
import com.sap.cds.ql.cqn.Modifier;
import com.sap.cds.reflect.CdsAssociationType;
import com.sap.cds.reflect.CdsElement;
import com.sap.cds.reflect.CdsElementNotFoundException;
import com.sap.cds.reflect.CdsEntity;
import com.sap.cds.reflect.CdsModel;
import com.sap.cds.reflect.CdsStructuredType;
import com.sap.cds.reflect.CdsType;
import com.sap.cds.util.CdsModelUtils;

public class MatchPredicateNormalizer {

	public static final CqnReference.Segment $OUTER = refSegment(CqnExistsSubquery.OUTER);
	private final AssociationAnalyzer associationAnalyzer = new AssociationAnalyzer();
	private final CdsModel model;
	private final CdsStructuredType target;

	public MatchPredicateNormalizer(CdsModel model, CdsStructuredType target) {
		this.model = model;
		this.target = target;
	}

	public  S normalize(S s) {
		s = CQL.copy(s, new Path2Nested());
		s = CQL.copy(s, new Match2Exists());

		return s;
	}

	public Predicate normalize(CqnPredicate pred) {
		Predicate copy = CQL.copy(pred, new Path2Nested());
		copy = CQL.copy(copy, new Match2Exists());

		return copy;
	}

	private class Path2Nested implements Modifier {

		@Override
		public Predicate match(CqnMatchPredicate match) {
			return convertToNestedIfNecessary(target, match.ref(), match.predicate().orElse(null), match.quantifier());
		}

		private Predicate convertToNestedIfNecessary(CdsStructuredType rowType, CqnStructuredTypeRef ref,
				CqnPredicate pred, Quantifier quantifier) {
			List segments = ref.segments();

			int n = segments.size();
			for (int i = 1; i < n; i++) {
				Segment segment = segments.get(i - 1);
				CdsElement element = rowType.getElement(segment.id());
				CdsType elementType = element.getType();
				if (elementType.isStructured()) {
					rowType = elementType.as(CdsStructuredType.class);
				} else if (elementType.isAssociation()) {
					rowType = elementType.as(CdsAssociationType.class).getTarget();
					if (!CdsModelUtils.isSingleValued(elementType)) {
						// 1:n in the middle
						// author.books.publisher.journals[title='xyz']
						// 0: author, 1: books
						CqnStructuredTypeRef prefix = typeRef(segments.subList(0, i), null);
						CqnStructuredTypeRef suffix = typeRef(segments.subList(i, n), null);
						Predicate nested = convertToNestedIfNecessary(rowType, suffix, pred, quantifier);
						return MatchPredicate.match(prefix, quantifier, nested);
					}
				}
			}

			return MatchPredicate.match(ref, quantifier, pred);
		}

	}

	private class Match2Exists implements Modifier {

		@Override
		public CqnStructuredTypeRef ref(CqnStructuredTypeRef ref) {
			return resolveInfixFilters(model.getStructuredType(ref.firstSegment()), ref);
		}

		private CqnStructuredTypeRef resolveInfixFilters(CdsStructuredType rowType, CqnStructuredTypeRef ref) {
			Iterator iter = ref.segments().iterator();

			List segments = new ArrayList<>(ref.size());
			CqnReference.Segment seg = iter.next();
			segments.add(normalizeSegment(seg, rowType));

			while (iter.hasNext()) {
				seg = iter.next();
				rowType = navigate(rowType, seg.id());
				segments.add(normalizeSegment(seg, rowType));
			}

			return CQL.to(segments).asRef();
		}

		@Override
		public CqnSelectListItem expand(CqnExpand expand) {
			resolveInfixFilters(target.getTargetOf(expand.ref().firstSegment()), expand.ref());

			return expand;
		}

		@Override
		public Predicate match(CqnMatchPredicate match) {
			CqnStructuredTypeRef ref = match.ref();
			CqnPredicate pred = match.predicate().orElse(null);
			Quantifier quantifier = match.quantifier();

			CdsElement association = getAssociation(ref);
			CdsEntity target = association.getType().as(CdsAssociationType.class).getTarget();

			StructuredTypeRef from = absoluteRef(target, ref);
			rejectToManyPathInFilter(target, pred);

			CqnPredicate where = innerToOuter(getPrefix(ref), association);

			if (pred != null) {
				pred = normalize(target, pred);
				if (quantifier == ALL) {
					pred = not(pred);
				}
				where = and(where, pred);
			}

			CqnSelect subquery = Select.from(from).where(where);

			ExistsSubquery exists = new ExistsSubquery(subquery);

			if (quantifier == ALL) {
				return exists.not();
			}
			return exists;
		}

		private void rejectToManyPathInFilter(CdsEntity entity, CqnPredicate pred) {
			if (pred == null) {
				return;
			}
			CqnVisitor visitor = new CqnVisitor() {
				@Override
				public void visit(CqnElementRef ref) {
					CdsStructuredType structType = entity;
					for (Segment segment : ref.segments()) {
						CdsType elType = structType.getElement(segment.id()).getType();
						if (elType.isStructured()) {
							structType = elType.as(CdsStructuredType.class);
						} else if (elType.isAssociation()) {
							CdsAssociationType assoc = elType.as(CdsAssociationType.class);
							structType = assoc.getTarget();
							if (CdsModelUtils.isToMany(assoc)) {
								String message = MessageFormat.format(
										"Association ''{0}'' with to-many target cardinality in anyMatch/allMatch is not supported",
										segment.id());
								throw new CqnValidationException(message);
							}
						}
					}
				}
			};
			pred.accept(visitor);
		}

		private CqnPredicate innerToOuter(List outerPrefix, CdsElement association) {
			CqnPredicate on = associationAnalyzer.getOnCondition(association);
			Modifier m = new Modifier() {
				@Override
				public CqnValue ref(CqnElementRef ref) {
					List segments = new ArrayList<>(ref.segments());
					if (ref.firstSegment().equals(association.getName())) {
						segments.remove(0);
					} else if (!CdsModelUtils.isContextElementRef(ref)) {
						segments.addAll(0, outerPrefix);
					}
					return ElementRefImpl.elementRef(segments, null, null);
				}
			};
			on = ExpressionVisitor.copy(on, m);

			return on;
		}

		private StructuredTypeRef absoluteRef(CdsEntity target, CqnStructuredTypeRef ref) {
			CqnPredicate filter = ref.targetSegment().filter().map(ExpressionVisitor::copy).orElse(null);
			return CQL.entity(target.getQualifiedName()).filter(filter).asRef();
		}

		private CqnReference.Segment normalizeSegment(CqnReference.Segment seg, CdsStructuredType rowType) {
			CqnPredicate filter = seg.filter().map(f -> normalize(rowType, f)).orElse(null);

			return refSegment(seg.id(), filter);
		}

		private Predicate normalize(CdsStructuredType rowType, CqnPredicate pred) {
			return new MatchPredicateNormalizer(model, rowType).normalize(pred);
		}

		private List getPrefix(CqnStructuredTypeRef ref) {
			List all = ref.segments();

			int size = all.size();
			List start = all.subList(0, size - 1);

			List prefix = new ArrayList<>(size);
			prefix.add($OUTER);
			prefix.addAll(start);

			return prefix;
		}

		private CdsElement getAssociation(CqnStructuredTypeRef ref) {
			CdsStructuredType rowType = target;
			String root = rowType.getQualifiedName();
			CdsElement element = CdsModelUtils.element(rowType, ref.segments());
			CdsType elementType = element.getType();
			if (!elementType.isAssociation()) {
				String message = MessageFormat.format(
						"The reference {0}.{1} does not terminate in an association and can''t be used with the anyMatch predicate.",
						root, ref.path());
				throw new CqnValidationException(message);
			}
			return element;
		}

	}

	private static CdsStructuredType navigate(CdsStructuredType rowType, String id) {
		try {
			CdsType type = rowType.getElement(id).getType();
			if (type.isStructured()) {
				return type.as(CdsStructuredType.class);
			}

			if (type.isAssociation()) {
				return type.as(CdsAssociationType.class).getTarget();
			}
		} catch (CdsElementNotFoundException e) {
			throw e;
		}

		String message = MessageFormat.format(
				"The reference {0}.{1} does not terminate in an association and can''t be used with the anyMatch predicate.",
				rowType.getQualifiedName(), id);
		throw new CqnValidationException(message);
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy