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

com.sap.cds.util.ProjectionUtils Maven / Gradle / Ivy

There is a newer version: 3.6.1
Show newest version
/*******************************************************************
 * © 2020 SAP SE or an SAP affiliate company. All rights reserved. *
 *******************************************************************/
package com.sap.cds.util;

import static com.sap.cds.impl.builder.model.ElementRefImpl.parse;
import static com.sap.cds.ql.CQL.copy;
import static com.sap.cds.util.CqnStatementUtils.getEntries;
import static com.sap.cds.util.CqnStatementUtils.setEntries;

import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

import com.sap.cds.ql.CQL;
import com.sap.cds.ql.ElementRef;
import com.sap.cds.ql.StructuredTypeRef;
import com.sap.cds.ql.Value;
import com.sap.cds.ql.cqn.AnalysisResult;
import com.sap.cds.ql.cqn.CqnAnalyzer;
import com.sap.cds.ql.cqn.CqnModifier;
import com.sap.cds.ql.cqn.CqnPredicate;
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.CqnValue;
import com.sap.cds.ql.cqn.ResolvedSegment;
import com.sap.cds.reflect.CdsEntity;
import com.sap.cds.reflect.CdsModel;

public class ProjectionUtils {

	private ProjectionUtils() {
		// empty
	}

	private static boolean isSupportedProjection(CqnSelect projection) {
		return !projection.having().isPresent() && projection.groupBy().isEmpty() && !projection.where().isPresent()
				&& !(projection.from().isJoin()) && !projection.isDistinct();
	}

	/**
	 * Resolves the statement against all underlying views, as long as they are
	 * views supported by projection resolving
	 *
	 * @param        the CQN statement type
	 * @param statement the CQN statement
	 * @param model     the CDS model
	 * @return the resolved statement (as far as supported)
	 */
	public static  T resolveAll(T statement, CdsModel model) {
		T previous;
		do {
			previous = statement;
			statement = resolve(previous, model);
		} while (previous != statement);
		return statement;
	}

	/**
	 * Resolves the statement against the views defined by the statements entity
	 * path. The statement is resolved if all views along the entity path are
	 * supported by projection resolving.
	 *
	 * If the statement can't be resolved, the original statement is returned.
	 *
	 * @param        the CQN statement type
	 * @param statement the CQN statement
	 * @param model     the CDS model
	 * @return the resolved statement, or the original statement, if the projection
	 *         could not be resolved.
	 */
	public static  T resolve(T statement, CdsModel model) {
		if (statement.isSelect()) {
			return statement;
		}
		CqnAnalyzer analyzer = CqnAnalyzer.create(model);
		CqnStructuredTypeRef ref = statement.ref();
		AnalysisResult analysis = analyzer.analyze(ref);
		Iterator segmentIterator = analysis.iterator();

		if (ref.segments().isEmpty()) {
			throw new IllegalStateException("statement has no path at all");
		}

		T resolved = statement;
		// resolve projection of root entity
		CdsEntity root = segmentIterator.next().entity();
		Optional query = root.query();
		if (query.isPresent()) {
			CqnSelect projection = query.get();
			if (!isSupportedProjection(projection) || hasContextVariable(root, statement)) {
				return statement;
			}

			String projectionTarget = projection.ref().firstSegment();
			// TODO this also needs to adapt potential filters
			resolved = changeTarget(statement, projectionTarget);
		}

		// process remaining entity path
		CdsEntity previous = root;
		while (segmentIterator.hasNext()) {
			CdsEntity entity = segmentIterator.next().entity();
			query = entity.query();
			if (query.isPresent()) {
				CqnSelect projection = query.get();
				// TODO this also needs to adapt element names and potential filters
				if (!isSupportedProjection(projection)) {
					return statement;
				}
			} else if (previous.query().isPresent()) {
				// can't consistently resolve a single layer
				return statement;
			}
			previous = entity;
		}

		// resolve projection of target
		if (query.isPresent()) {
			CqnSelect projection = query.get();
			CdsEntity entity = analyzer.analyze(projection).targetEntity();
			List items = projection.items();
			ProjectionResolver p = new ProjectionResolver(entity, items);
			if (items.isEmpty() || items.size() == 1 && items.get(0).isStar()) {
				return resolved;
			} else if (!p.isSupportedProjection()) {
				return statement;
			}
			// process aliases in select list and where
			resolved = p.resolve(resolved);
			// TODO look at columns, where, orderby, etc. based on the projection of the
			// target based on that we need to handle renames
		}

		return resolved;
	}

	private static  boolean hasContextVariable(CdsEntity entity, T statement) {
		// TODO: Support cds.valid.from & cds.valid.to annotation conditions in a view.
		return !(((CqnStatement) statement).isInsert()) && entity.elements().anyMatch(
				e -> e.findAnnotation("cds.valid.from").isPresent() || e.findAnnotation("cds.valid.to").isPresent());
	}

	/**
	 * Returns a new statement with the target changed to {@code newTarget}. In a
	 * path expression the first segment is changed.
	 * 
	 * @param s         the statement
	 * @param newTarget the new target
	 * @return the modified statement
	 */
	public static  T changeTarget(T s, String newTarget) {
		return copy(s, new CqnModifier() {
			@Override
			public CqnStructuredTypeRef ref(StructuredTypeRef ref) {
				ref.rootSegment().id(newTarget);
				return ref;
			}
		});
	}

	private static class ProjectionResolver {
		private Map displayNameToRef = new HashMap<>();

		public ProjectionResolver(CdsEntity entity, List slis) {
			slis.stream().filter(CqnSelectListItem::isValue).map(CqnSelectListItem::asValue).forEach(slv -> {
				CqnValue val = slv.value();
				if (val.isRef() && CdsModelUtils.findElement(entity, val.asRef())
						.map(e -> e.getType().isAssociation() && CdsModelUtils.isSingleValued(e.getType()))
						.orElse(true)) {
					// add mapping for flattened (4odata) structured elements & associated foreign
					// key elements
					String prefix = val.asRef().displayName() + "_";
					entity.elements().filter(e -> e.getName().startsWith(prefix)).forEach(e -> {
						String elementInStruct = e.getName().substring(prefix.length() - 1);
						displayNameToRef.put(slv.displayName() + elementInStruct, CQL.get(e.getName()));
					});
				} else {
					displayNameToRef.put(slv.displayName(), val);
				}
			});
		}

		public  T resolve(T statement) {
			setEntries(statement, resolveEntries(getEntries(statement)));
			return resolveRefs(statement);
		}

		public List> resolveEntries(List> entries) {
			return entries.stream().map(this::renameKeys).collect(Collectors.toList());
		}

		private boolean isSupportedProjection() {
			return displayNameToRef.values().parallelStream().allMatch(CqnValue::isRef);
		}

		private Map renameKeys(Map e) {
			Map renamedEntry = new HashMap<>();
			e.forEach((k, v) -> {
				String resolvedName = getNameByAlias(k);
				renamedEntry.put(resolvedName, v);
			});

			return renamedEntry;
		}

		private String getNameByAlias(String alias) {
			CqnValue resolvedKey = displayNameToRef.get(alias);
			if (resolvedKey != null) {
				return resolvedKey.asRef().segments().stream().map(Segment::id).collect(Collectors.joining("."));
			}
			return alias;
		}

		public  T resolveRefs(T s) {
			return copy(s, new CqnModifier() {
				@Override
				public Value ref(ElementRef ref) {
					return parse(getNameByAlias(ref.displayName()));
				}

				@Override
				public CqnStructuredTypeRef ref(StructuredTypeRef ref) {
					ref.targetSegment().filter().ifPresent(f -> {
						CqnPredicate filter = copy(f, this);
						ref.rootSegment().filter(filter);
					});
					return ref;
				}
			});
		}
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy