Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.sap.cds.util.ProjectionResolver Maven / Gradle / Ivy
/************************************************************************
* © 2021-2023 SAP SE or an SAP affiliate company. All rights reserved. *
************************************************************************/
package com.sap.cds.util;
import static com.sap.cds.impl.builder.model.ElementRefImpl.elementRef;
import static com.sap.cds.ql.cqn.CqnExistsSubquery.OUTER;
import static java.util.Collections.singleton;
import static java.util.stream.Collectors.toList;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.sap.cds.Result;
import com.sap.cds.impl.ResultImpl;
import com.sap.cds.ql.CQL;
import com.sap.cds.ql.ElementRef;
import com.sap.cds.ql.Predicate;
import com.sap.cds.ql.Select;
import com.sap.cds.ql.StructuredType;
import com.sap.cds.ql.StructuredTypeRef;
import com.sap.cds.ql.Value;
import com.sap.cds.ql.cqn.CqnAnalyzer;
import com.sap.cds.ql.cqn.CqnElementRef;
import com.sap.cds.ql.cqn.CqnExpand;
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.CqnSelectListValue;
import com.sap.cds.ql.cqn.CqnSortSpecification;
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.ql.impl.ExpressionVisitor;
import com.sap.cds.ql.impl.LeanModifier;
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;
/**
* Stateful implementation of projection resolver. Keeps track of statement that
* was attempted to be resolved.
*/
public class ProjectionResolver {
private static final String UNKNOWN = "$unknown$";
private static final BiPredicate DEFAULT_STOP_CONDITION = (prev, stmnt) -> prev == stmnt;
private final CdsModel model;
private final CqnAnalyzer analyzer;
private final Set stopConditions = new HashSet<>(singleton(DEFAULT_STOP_CONDITION));
private final Deque projectionStack = new ArrayDeque<>();
private T current;
private T previous;
private ProjectionResolver(CdsModel model, T statement) {
this.model = model;
this.analyzer = CqnAnalyzer.create(model);
this.current = statement;
}
public static ProjectionResolver create(CdsModel model, T statement) {
return new ProjectionResolver<>(model, statement);
}
/**
* Adds the {@code Condition} on which the projection is considered resolved
*
* @param resolvedCondition the {@code Condition} on which resolvement stops
* @return the {@code ProjectionResolver instance}
*/
public ProjectionResolver condition(BiPredicate resolvedCondition) {
stopConditions.add(resolvedCondition);
return this;
}
/**
* Adds the {@code Condition} on which the projection is considered resolved
*
* @param resolvedCondition the {@code Condition} on which resolvement stops
* @return the {@code ProjectionResolver instance}
*/
public ProjectionResolver condition(TriPredicate resolvedCondition) {
stopConditions.add(resolvedCondition);
return this;
}
/**
* Resolves the statement against the views defined by the statements entity
* path and returns the {@code ProjectionResolver} object containing it. The
* statement is resolved until the {@code Condition} is not met.
*
* If the statement can't be resolved, the original statement is returned.
*
* @return resolver object, containing the resolved statement, or the original
* one, if the projection could not be resolved.
*/
public ProjectionResolver resolveAll() {
while (!stopResolvement()) {
resolve();
}
return this;
}
private boolean stopResolvement() {
for (Condition condition : stopConditions) {
boolean test = false;
if (condition.testNext()) {
CqnStatement next = ProjectionResolver.create(model, current).resolve().getResolvedStatement();
test = ((TriPredicate) condition).test(previous, current, next);
} else {
test = ((BiPredicate) condition).test(previous, current);
}
if (test) {
return true;
}
}
return false;
}
/**
* Resolves any aliases from the statement to their original name. If aliases
* are resolved the original projection can be restored by using
* {@link #transform(List)} or {@link #transform(Result)}.
*
* @return resolver object
*/
public ProjectionResolver resolveAliases() {
T resolved = removeAliases(current);
// only set previous if alias removal wasn't a noop
if (resolved != current) {
finishResolvementStep(resolved);
}
return this;
}
/**
* Prepare select statement for further transformations.
*
* @param statement to be preprocessed
*/
private T removeAliases(T statement) {
// project aliases in original statement
if (statement.isSelect()) {
// remove alias in select
CdsEntity target = analyzer.analyze(statement.ref()).targetEntity();
T selectWithoutAlias = CQL.copy(statement, new AliasRemover(target, null));
// resolve aliases used in select
Map aliasMap = calculateAliasMap(statement.asSelect());
projectionStack.push(new Projection(null, statement.asSelect()));
return CQL.copy(selectWithoutAlias, new AliasModifier(null, aliasMap, null));
}
return statement;
}
/**
* Resolves the current statement against its next projection layer.
*
* It resolves projections in all refs contained in the statement.
*
* If the statement can't be resolved, the original statement is returned.
*
* @return {@code ProjectionResolver} containing the resolved statement, or the
* original one, if the projection could not be resolved.
*/
public ProjectionResolver resolve() {
try {
T resolved = previous == null ? removeAliases(current) : current;
CdsEntity root = analyzer.analyze(resolved.ref()).rootEntity();
if (root.query().isPresent() && isSupportedProjection(root, root.query().get())) {
RefModifier refModifier = new RefModifier();
int oldNumProjections = projectionStack.size();
resolved = CQL.copy(resolved, refModifier);
// we need to resolve the newly added projections
Iterator iter = projectionStack.descendingIterator();
for (int i = 0; i < oldNumProjections; ++i)
iter.next();
while (iter.hasNext()) {
Projection projection = iter.next();
// resolve selection set based on projection
if (resolved.isSelect()) {
resolved = CQL.copy(resolved,
new SelectionModifier(projection.getEntity(), projection.getQuery()));
}
// resolve aliases in data
// we need to adapt data also in case of a select star to remove draft fields
if (resolved.isInsert() || resolved.isUpsert() || resolved.isUpdate()) {
new DataAliasResolver(true).resolve(projection.getEntity(),
CqnStatementUtils.getEntries(resolved),
new Projection(projection.getEntity(), projection.getQuery()));
}
// if projection is selecting all elements no need to adapt
if (CqnStatementUtils.isSelectStar(projection.getQuery().items())) {
continue;
}
// adapt all aliases in references
resolved = CQL.copy(resolved, new ExtendedAliasModifier(projection.getEntity(),
calculateAliasMap(projection.getQuery()), projection));
}
finishResolvementStep(resolved);
return this;
}
} catch (UnsupportedProjectionException e) {
// nothing to do, finish with below return
}
if (previous == null) {
projectionStack.clear();
}
finishResolvementStep(current);
return this;
}
private void finishResolvementStep(T resolved) {
this.previous = this.current;
this.current = resolved;
}
/**
* Evaluates if the given projection is supported for resolvement.
*
* @param target the projection entity, which is created / defined by the
* projection
* @param query the projection query
* @return true, if the projection is supported for resolvement, false
* otherwise.
*/
private boolean isSupportedProjection(CdsEntity target, CqnSelect query) {
List items = query.items();
return (items.isEmpty() || containsStar(items) || hasConcreteElement(target, items))
&& query.groupBy().isEmpty() && !query.having().isPresent() && !query.where().isPresent()
&& !query.from().isJoin() && !query.isDistinct() && !isTemporal(target);
}
private boolean containsStar(List items) {
return items.stream().anyMatch(CqnSelectListItem::isStar);
}
private boolean hasConcreteElement(CdsEntity target, List items) {
return slvs(items).anyMatch(slv -> slv.value().isRef() && isConcreteElementOf(target, slv));
}
private Stream slvs(List items) {
return items.stream().flatMap(CqnSelectListItem::ofValue);
}
private boolean isConcreteElementOf(final CdsEntity entity, CqnSelectListValue slv) {
return entity.findElement(slv.displayName()).map(e -> !e.isVirtual()).orElse(true);
}
private boolean isTemporal(CdsEntity entity) {
// TODO Support cds.valid.from & cds.valid.to annotation conditions in a view
return !current.isInsert() && entity.elements().anyMatch(
e -> e.findAnnotation("cds.valid.from").isPresent() || e.findAnnotation("cds.valid.to").isPresent());
}
/**
* Resolves a StructuredTypeRef against its next projection layer of the root
* segment. The other segments are resolved as long as the association target of
* the prior segment does not match the actual target of the segment.
*
* @param ref the ref
* @param currentProjection the projection of the current layer or {@code null}
* if the statement ref is resolved
* @return the resolved ref and the corresponding alias map
*
* @throws UnsupportedProjectionException if the ref can not be resolved
*/
private RefAndAliases resolveRefAndAliases(CqnStructuredTypeRef ref, Projection currentProjection) {
StructuredType> resolvedRef = null;
CdsEntity currentRefTarget = null;
Map previousAliasMap = new HashMap<>();
Iterator iterator = analyzer.analyze(ref).iterator();
while (iterator.hasNext()) {
ResolvedSegment segment = iterator.next();
CdsEntity segmentTarget = segment.entity();
if (resolvedRef == null) {
// first segment
Optional queryOptional = segmentTarget.query();
if (queryOptional.isPresent()) {
currentRefTarget = analyzer.analyze(queryOptional.get()).targetEntity();
resolvedRef = CQL.entity(currentRefTarget.getQualifiedName());
} else {
// first segment has no projection -> nothing to resolve
return new RefAndAliases(ref, new HashMap<>());
}
} else {
// transform association segment based on alias map from previous projection
String id = segment.segment().id();
String resolvedId = previousAliasMap.getOrDefault(id, id);
resolvedRef = resolvedRef.to(resolvedId);
try {
currentRefTarget = currentRefTarget.getTargetOf(resolvedId);
} catch (CdsElementNotFoundException e) {
// mixed in association, that doesn't exist on lower projection level
throw new UnsupportedProjectionException();
}
}
CqnPredicate filter = segment.segment().filter().orElse(null);
previousAliasMap.clear();
// resolve all projections to get to the currentRefTarget
Deque projectionsToResolve = getProjectionStack(segmentTarget, currentRefTarget);
Iterator projectionIterator = projectionsToResolve.descendingIterator();
while (projectionIterator.hasNext()) {
Projection projection = projectionIterator.next();
Map aliasMap = calculateAliasMap(projection.getQuery());
if (filter != null) {
// transform filter based on current projection
CqnPredicate resolvedFilter = CQL.copy(filter,
new AliasModifier(projection.getEntity(), aliasMap, currentProjection));
resolvedRef = resolvedRef.filter(resolvedFilter);
filter = resolvedFilter;
}
if (!iterator.hasNext()) {
// only add new projections for the last ref segment because only
// these are necessary to resolve the data
if (currentProjection == null) {
projectionStack.push(projection);
} else {
Deque projections = currentProjection.refs.get(ref);
if (projections == null) {
projections = new ArrayDeque<>();
currentProjection.refs.put(ref, projections);
}
projections.push(projection);
}
}
// replace the alias of the previous projection layer with the alias from this
// projection layer
previousAliasMap.replaceAll((k, v) -> aliasMap.getOrDefault(v, v));
// add all new aliases
aliasMap.entrySet().stream().filter(e -> !previousAliasMap.containsValue(e.getKey()))
.forEach(e -> previousAliasMap.put(e.getKey(), e.getValue()));
}
}
if (currentProjection != null) {
currentProjection.aliases.put(ref, resolvedRef.asRef());
}
return new RefAndAliases(resolvedRef.asRef(), previousAliasMap);
}
private static class RefAndAliases {
private final CqnStructuredTypeRef ref;
private final Map aliases;
public RefAndAliases(CqnStructuredTypeRef ref, Map aliases) {
this.ref = ref;
this.aliases = aliases;
}
}
/**
* Resolves a rootless ElementRef, by prefixing it with the given root entity.
*
* @param root the root from which the ref branches off.
* @param ref the ref
* @return the resolved ref
*
* @see #resolveRef(StructuredTypeRef)
*/
private ElementRef> resolveRef(CdsEntity root, CqnElementRef ref, Projection currentProjection) {
// prefix ref with root entity to enable resolvement
StructuredTypeRef structuredType = prefix(root, skipLast(ref));
RefAndAliases resolvedRefAndAliases = resolveRefAndAliases(structuredType, currentProjection);
CqnStructuredTypeRef resolvedStructuredType = resolvedRefAndAliases.ref;
Map aliases = resolvedRefAndAliases.aliases;
// restore original ref type
List extends CqnReference.Segment> resolvedSegments = skipFirst(resolvedStructuredType);
ElementRef result = CQL.to(resolvedSegments)
.get(aliases.getOrDefault(ref.lastSegment(), ref.lastSegment()));
currentProjection.aliases.put(ref, result);
return result;
}
/**
* Resolves a rootless ElementRef or StructuredTypeRef, by prefixing it with the
* given root entity.
*
* @param root the root from which the ref branches off.
* @param ref the ref
* @return the resolved ref
*
* @see #resolveRef(StructuredTypeRef)
*/
private StructuredType> resolveType(CdsEntity root, CqnStructuredTypeRef ref, Projection currentProjection) {
StructuredTypeRef structuredType = prefix(root, ref.segments());
RefAndAliases resolvedRefAndAliases = resolveRefAndAliases(structuredType, currentProjection);
CqnStructuredTypeRef resolvedStructuredType = resolvedRefAndAliases.ref;
// restore original ref type
List extends CqnReference.Segment> resolvedSegments = skipFirst(resolvedStructuredType);
return CQL.to(resolvedSegments);
}
private static StructuredTypeRef prefix(CdsEntity root, List extends Segment> list) {
List segments = new ArrayList<>();
segments.add(CQL.refSegment(root.getQualifiedName()));
segments.addAll(list);
return CQL.to(segments).asRef();
}
/**
* Calculates an alias map based on a projection query.
*
* @param query the projection query
* @return the alias map, containing only aliased ElementRefs as strings. The
* alias name is mapped to the original name from the projection queries
* target entity.
*
* @see #calculateAliasMap(CdsEntity, List)
*/
private Map calculateAliasMap(CqnSelect query) {
CdsEntity queryTarget = analyzer.analyze(query).targetEntity();
return calculateAliasMap(queryTarget, query.items());
}
/**
* Calculates an alias map based on a list of SLIs.
*
* For example: entity Foo as projection on Bar { b as f, a } This will return a
* map that contains a single entry which maps f to b. The alias map therefore
* follows projections in the direction towards the base entity.
*
* @param queryTarget the target entity of the projection query (here: Bar).
* This entity is the root from which the unaliased refs in
* the SLI list branch off.
*
* @param items the list of aliased and unaliased SLIs
*
* @return the alias map, containing only aliased ElementRefs as strings. The
* alias name is mapped to the original name from the projection queries
* target entity.
*/
private Map calculateAliasMap(CdsEntity queryTarget, List items) {
Map aliasMap = new HashMap<>();
items.stream().flatMap(CqnSelectListItem::ofValue).forEach(slv -> {
String alias = slv.alias().orElse(null);
if (alias != null) {
CqnValue val = slv.value();
if (val.isRef()) {
CqnElementRef ref = val.asRef();
String original = ref.path();
aliasMap.put(alias, original);
// add mapping for flattened (4odata) structured elements & associated foreign
// key elements
boolean singleValuedAssociation = CdsModelUtils.findElement(queryTarget, ref)
.map(e -> e.getType().isAssociation() && CdsModelUtils.isSingleValued(e.getType()))
.orElse(true);
if (singleValuedAssociation) {
String prefix = original + "_";
queryTarget.elements().map(e -> e.getName()).filter(e -> e.startsWith(prefix)).forEach(e -> {
String elementInStruct = e.substring(prefix.length() - 1);
aliasMap.put(alias + elementInStruct, e);
});
}
} else {
aliasMap.put(alias, UNKNOWN);
}
}
});
return aliasMap;
}
/**
* Removes aliases in SLI and SLIs of expands by replacing the aliased
* ElementRef with its alias name. This prepares the statement for processing by
* the AliasModifier.
*
* This is required in incoming statements, as when resolving projections we can
* only guarantee a unique set of alias names on a single projection level. In
* case we keep aliases present this can create ambiguous statements, for
* example in case the alias is referred to in an orderby clause.
*/
private class AliasRemover implements LeanModifier {
private final CdsEntity target;
private final Projection currentProjection;
/**
* @param target the statement or expand target. It is the root entity from
* which refs of the processed SLIs branch off.
*/
public AliasRemover(CdsEntity target, Projection currentProjection) {
this.target = target;
this.currentProjection = currentProjection;
}
@Override
public CqnSelectListItem selectListItem(Value> value, String alias) {
if (value.isRef() && alias != null) {
// prepare for alias modifier
return CQL.get(alias);
}
return LeanModifier.super.selectListValue(value, alias);
}
@Override
public CqnSelectListItem expand(CqnExpand expand) {
List items = expand.items();
CqnStructuredTypeRef ref = expand.ref();
// no aliases in expand star
if (ref.firstSegment().equals("*")) {
return expand;
}
CdsEntity expandTarget = CdsModelUtils.target(target, ref.segments()).as(CdsEntity.class);
// recursively apply alias remover to expands
AliasRemover aliasRemover = new AliasRemover(expandTarget, currentProjection);
List unaliasedItems = items.stream().map(i -> ExpressionVisitor.copy(i, aliasRemover))
.collect(toList());
// post process items with alias modifier
Map expandAliasMap = calculateAliasMap(expandTarget, items);
AliasModifier aliasModifier = new AliasModifier(null, expandAliasMap, currentProjection);
List resolvedItems = unaliasedItems.stream()
.map(i -> ExpressionVisitor.copy(i, aliasModifier)).collect(toList());
List resolvedOrderBy = expand.orderBy().stream()
.map(o -> ExpressionVisitor.copy(o, aliasModifier)).collect(toList());
return CQL.to(ref.segments()).expand(resolvedItems).orderBy(resolvedOrderBy).limit(expand.top(),
expand.skip());
}
}
/**
* Given the following projection: entity Foo as projection on Bar { b as f }
* Given the following statement: SELECT Foo { f } orderby { f }
*
* The AliasModifier replaces ElementRefs from a projection entity (Foo) with
* their original ElementRefs from the projected entity (Bar). It achieves this
* based on the given aliasMap, which can be calculated from a projection query
* using the {@link ProjectionResolver#calculateAliasMap(CqnSelect)} method.
*
* The AliasModifier assumes that the statement that is modified does not
* contain any explicit aliases anymore. The statement should solely rely on
* names that can be obtained from the projection definition. To achieve this
* the {@link AliasRemover} can be used.
*/
private class AliasModifier implements LeanModifier {
protected final CdsEntity target;
protected final Map aliasMap;
protected final Projection currentProjection;
/**
* @param target the projection entity (Foo, in above example). Can be null,
* in case it is guranteed that there are only single-segment
* element refs with aliases in the statement. This is for
* example the case after processing a statement with the
* {@link AliasRemover}.
*
* @param aliasMap the alias map calculated from the projection query, which
* defines the projection entity.
*/
public AliasModifier(CdsEntity target, Map aliasMap, Projection currentProjection) {
this.target = target;
this.aliasMap = aliasMap;
this.currentProjection = currentProjection;
}
@Override
public CqnValue ref(CqnElementRef ref) {
// resolve single segment refs directly with alias map
if (ref.size() == 1) {
String id = ref.firstSegment();
return CQL.get(aliasMap.getOrDefault(id, id));
} else if (target != null) {
return resolveRef(target, ref, currentProjection);
} else {
return ref;
}
}
@Override
public Predicate exists(Select> subQuery) {
subQuery.where().map(w -> CQL.copy(w, new LeanModifier() {
@Override
public CqnValue ref(CqnElementRef ref) {
// resolve outer ref with single element suffix
String id = ref.lastSegment();
if (ref.size() == 2 && OUTER.equals(ref.firstSegment()) && aliasMap.containsKey(id)) {
return elementRef(OUTER, aliasMap.get(id));
}
return ref;
}
})).ifPresent(subQuery::where);
return CQL.exists(subQuery);
}
}
/**
* The SelectionModifier modifies the SLIs of a statement based on the SLIs
* selected in a projection query.
*
* Its main task is to resolve a star select to all non-association elements
* selected in a projection query. It can also merge this with explicitly
* mentioned elements in the statement. In case the projection itself is a star
* select it does not resolve the star select of the statement.
*
* In addition it takes over all excluded elements from the projection into the
* statement.
*/
private class SelectionModifier implements LeanModifier {
private final CdsEntity target;
private final CqnSelect query;
private final Map aliasMap;
private final CdsEntity projectionTarget;
/**
* @param target the projection entity, which is created / defined by the
* projection
* @param query the projection query
*/
public SelectionModifier(CdsEntity target, CqnSelect query) {
this.target = target;
this.query = query;
this.aliasMap = calculateAliasMap(query);
this.projectionTarget = analyzer.analyze(query).targetEntity();
}
@Override
public List items(List items) {
// if projection is selecting all elements no need to adapt
if (CqnStatementUtils.isSelectStar(query.items())) {
return items;
}
boolean hasStar = false;
boolean hasExpandStar = false;
Set selected = new HashSet<>();
List slis = new ArrayList<>();
for (CqnSelectListItem item : items) {
if (item.isStar()) {
hasStar = true;
continue;
} else if (item.isExpand() && item.asExpand().ref().firstSegment().equals("*")) {
hasExpandStar = true;
continue;
}
String displayName = null;
if (item.isValue()) {
displayName = item.asValue().displayName();
} else if (item.isExpand()) {
displayName = item.asExpand().displayName();
}
if (displayName == null || isAllowedSelection(displayName)) {
if (displayName != null) {
selected.add(displayName);
}
slis.add(item);
}
}
// resolve stars and merge with explicitly mentioned elements
if (hasStar || items.isEmpty()) {
target.concreteNonAssociationElements().filter(e -> isAllowedSelection(e.getName()))
.filter(e -> !selected.contains(e.getName())).map(e -> CQL.get(e.getName())).forEach(slis::add);
}
if (hasExpandStar) {
target.associations().filter(a -> !a.isVirtual()).filter(e -> isAllowedSelection(e.getName()))
.filter(e -> !selected.contains(e.getName())).map(e -> CQL.to(e.getName()).expand())
.forEach(slis::add);
}
return slis;
}
@Override
public Set excluding(Set excluding) {
Set all = new HashSet<>(excluding);
all.addAll(query.excluding());
return all;
}
private boolean isAllowedSelection(String element) {
String mapped = aliasMap.getOrDefault(element, element);
boolean existsOnThisLevel = CdsModelUtils.findElement(target, CQL.get(element)).isPresent();
boolean existsOnNextLevel = CdsModelUtils.findElement(projectionTarget, CQL.get(mapped)).isPresent();
return existsOnThisLevel == existsOnNextLevel;
}
}
/**
* Resolves the root of the statement ref to the next projection layer. All
* other path segments are resolved as long as the association target of the
* prior resolved segment does not match the actual segment target.
*/
private class RefModifier implements LeanModifier {
@Override
public CqnStructuredTypeRef ref(CqnStructuredTypeRef ref) {
return resolveRefAndAliases(ref, null).ref;
}
}
/**
* This modifier is an extended {@link AliasModifier}, which traverses expands
* and handles additional aspects besides ElementRefs.
*
* - It takes care of resolving aliases used in excluding element names. - It
* recursively traverses into expands to resolve refs while also applying the
* {@link SelectionModifier} there.
*/
private class ExtendedAliasModifier extends AliasModifier {
/**
* @param target the projection entity
* @param aliasMap the alias map calculated from the projection query
* @param currentProjection the projection of the current layer
*
* @see AliasModifier#AliasModifier(CdsEntity, Map)
*/
public ExtendedAliasModifier(CdsEntity target, Map aliasMap, Projection currentProjection) {
super(target, aliasMap, currentProjection);
}
@Override
public CqnSelectListItem expand(CqnExpand expand) {
CqnStructuredTypeRef ref = expand.ref();
List items = expand.items();
List orderBy = expand.orderBy();
StructuredType> resolvedType = resolveType(target, ref, currentProjection);
// find matching projection for ref
Optional> projections = currentProjection.refs.entrySet().stream()
.filter(e -> skipFirst(e.getKey()).equals(ref.segments())).map(e -> e.getValue()).findAny();
if (!projections.isPresent()) {
return resolvedType.expand(items).orderBy(orderBy).limit(expand.top(), expand.skip());
}
CdsEntity expandTarget = CdsModelUtils.target(target, ref.asRef().segments()).as(CdsEntity.class);
List resolvedItems = items;
List resolvedOrderBy = orderBy;
// resolve all projection layers of the ref target
Iterator descendingIterator = projections.get().descendingIterator();
while (descendingIterator.hasNext()) {
Projection currentProjection = descendingIterator.next();
CqnSelect expandQuery = currentProjection.getQuery();
// resolve selection set based on projection
List expandedItems = new SelectionModifier(expandTarget, expandQuery)
.items(resolvedItems);
// adapt all aliases in references
ExtendedAliasModifier aliasModifier = new ExtendedAliasModifier(expandTarget,
calculateAliasMap(expandQuery), currentProjection);
resolvedItems = expandedItems.stream().map(i -> ExpressionVisitor.copy(i, aliasModifier))
.collect(toList());
resolvedOrderBy = resolvedOrderBy.stream().map(o -> ExpressionVisitor.copy(o, aliasModifier))
.collect(toList());
expandTarget = CdsModelUtils.entity(model, expandQuery.ref());
}
return resolvedType.expand(resolvedItems).orderBy(resolvedOrderBy).limit(expand.top(), expand.skip());
}
@Override
public Set excluding(Set excluding) {
return excluding.stream().map(e -> aliasMap.getOrDefault(e, e)).collect(Collectors.toSet());
}
}
/**
* The DataAliasResolver maps nested entity data based on a projection. It can
* map in both directions, either following the projection towards the base
* entity, or the opposite direction. It also handles expanded data by
* traversing into the projections defined for these expanded entities.
*/
private class DataAliasResolver {
private final boolean forwardMapping;
/**
* @param forwardMapping true, in case mapping should occur towards the base
* entity
*/
public DataAliasResolver(boolean forwardMapping) {
this.forwardMapping = forwardMapping;
}
/**
* Resolves the entries against the given projection.
*
* @param target the type of entries
* @param entries the entries
* @param projection the projection
*/
public void resolve(CdsEntity target, List extends Map> entries, Projection projection) {
if (projection.refs.isEmpty()) {
// traverse into associations
target.associations().forEach(a -> resolveAssociation(entries, projection, a));
} else {
// traverse into expanded entities
projection.refs.entrySet().forEach(refEntry -> resolveExpandedEntities(entries, projection, refEntry));
}
Map aliasMap = calculateAliasMap(projection.getQuery());
// add all aliases that are introduced through references with more than one
// segment
projection.aliases.entrySet().forEach(
entry -> aliasMap.putIfAbsent(entry.getKey().lastSegment(), entry.getValue().lastSegment()));
// resolve this layer
CdsEntity projectedType = forwardMapping ? analyzer.analyze(projection.getQuery()).targetEntity()
: projection.getEntity();
entries.forEach(entry -> {
Map renamedEntry = new HashMap<>();
entry.forEach((key, value) -> {
String mapping = getMapping(key, aliasMap);
if (projectedType == null
|| (CdsModelUtils.findElement(target, CQL.get(key)).isPresent() == CdsModelUtils
.findElement(projectedType, CQL.get(mapping)).isPresent())
|| (aliasMap.containsKey(mapping) && aliasMap.get(mapping).split("\\.").length > 1)) {
renamedEntry.put(mapping, value);
}
});
entry.clear();
entry.putAll(renamedEntry);
});
}
private void resolveExpandedEntities(List extends Map> entries, Projection projection,
Entry> refEntry) {
CqnReference ref = projection.aliases.get(refEntry.getKey());
String name = ref.lastSegment();
List> associationEntries = getAssociationEntries(entries, name);
if (associationEntries.isEmpty()) {
return;
}
for (Projection childProjection : refEntry.getValue()) {
CdsEntity childProjectionTarget = CqnAnalyzer.create(model).analyze(childProjection.getQuery())
.targetEntity();
resolve(childProjectionTarget, associationEntries, childProjection);
}
}
private void resolveAssociation(List extends Map> entries, Projection projection,
CdsElement a) {
String associationName = a.getName();
CdsEntity associationTarget = a.getType().as(CdsAssociationType.class).getTarget();
List> associationEntries = getAssociationEntries(entries, associationName);
if (associationEntries.isEmpty()) {
return;
}
String resolvedAssociationName = getMapping(associationName, projection);
Deque associationProjections = new LinkedList<>();
CdsEntity resolvedTarget = getResolvedTarget(projection, forwardMapping);
if (resolvedTarget != null && hasAssociation(resolvedTarget, resolvedAssociationName)) {
// we need to resolve all projection layers of the association target until it
// matches the resolvedAssociationTarget
CdsEntity resolvedAssociationTarget = resolvedTarget.getTargetOf(resolvedAssociationName);
associationProjections = getProjectionStack(
forwardMapping ? associationTarget : resolvedAssociationTarget,
forwardMapping ? resolvedAssociationTarget : associationTarget);
} else {
// aliases in expands in projection
for (CqnSelectListItem item : projection.getQuery().items()) {
if (item.isExpand() && item.asExpand().displayName().equals(resolvedAssociationName)) {
associationProjections.add(
new Projection(null, Select.from(associationTarget).columns(item.asExpand().items())));
break;
}
}
}
Iterator iter = forwardMapping ? associationProjections.descendingIterator()
: associationProjections.iterator();
Projection associationProjection;
while (iter.hasNext()) {
associationProjection = iter.next();
resolve(getResolvedTarget(associationProjection, !forwardMapping), associationEntries,
associationProjection);
}
}
private boolean hasAssociation(CdsEntity target, String associationName) {
return !UNKNOWN.equals(associationName)
&& target.findElement(associationName).filter(e -> e.getType().isAssociation()).isPresent();
}
@SuppressWarnings("unchecked")
private List> getAssociationEntries(List extends Map> entries,
String name) {
List> associationEntries = new ArrayList<>();
for (Map entry : entries) {
Object value = entry.get(name);
if (value instanceof Map) {
associationEntries.add((Map) value);
} else if (value instanceof List) {
associationEntries.addAll((List>) value);
}
}
return associationEntries;
}
private String getMapping(String key, Projection projection) {
return getMapping(key, calculateAliasMap(projection.getQuery()));
}
private String getMapping(String key, Map aliasMap) {
if (forwardMapping) {
return aliasMap.getOrDefault(key, key);
}
return aliasMap.entrySet().stream().filter(e -> e.getValue().equals(key)).map(e -> e.getKey()).findFirst()
.orElse(aliasMap.entrySet().stream().filter(e -> getLastSegment(e.getValue()).equals(key))
.map(e -> e.getKey()).findFirst().orElse(key));
}
private CdsEntity getResolvedTarget(Projection projection, boolean forward) {
if (forward) {
return analyzer.analyze(projection.getQuery()).targetEntity();
}
return projection.getEntity();
}
private String getLastSegment(String segments) {
if (segments == null || segments.isEmpty()) {
return segments;
}
String[] segmentArray = segments.split("\\.");
return segmentArray[segmentArray.length - 1];
}
}
/**
* Returns the resolved statement.
*
* @return the resolved statement
*/
public T getResolvedStatement() {
return current;
}
/**
* Transforms the list of entries to structurally match the original statement.
*
* @param entries the execution result for the resolved statement to be
* transformed into the original representation
* @return the transformed entries
*/
public List extends Map> transform(List extends Map> entries) {
if (projectionStack.isEmpty()) {
return entries;
}
List extends Map> copy = DataUtils.copyGenericList(entries);
DataAliasResolver dataAliasResolver = new DataAliasResolver(false);
CdsEntity target = analyzer.analyze(current.ref()).targetEntity();
for (Projection projection : projectionStack) {
dataAliasResolver.resolve(target, copy, projection);
target = projection.getEntity();
}
return copy;
}
/**
* Transforms the {@link Result} to structurally match the original statement.
* It also redetermines the row type of the {@link Result}
*
* @param result the execution result for the resolved statement to be
* transformed into the original representation
* @return the transformed {@link Result}
*/
public Result transform(Result result) {
if (projectionStack.isEmpty()) {
return result;
}
List extends Map> transformedResult = transform(result.list());
ResultImpl builder = ResultImpl.from(result).rows(transformedResult);
if (current.isSelect()) {
builder.rowType(CqnStatementUtils.rowType(model, projectionStack.getLast().getQuery()));
}
return builder.result();
}
private interface Condition {
default boolean testNext() {
return false;
}
}
@FunctionalInterface
public interface BiPredicate extends Condition, java.util.function.BiPredicate {
}
@FunctionalInterface
public interface TriPredicate extends Condition {
boolean test(CqnStatement previous, CqnStatement resolved, CqnStatement next);
@Override
default boolean testNext() {
return true;
}
}
/**
* Represents a projection definition. For example: entity Foo as projection on
* Bar { b as f }
*
* It consists of the projection entity (the entity created / defined by the
* projection, here: Foo) and the projection query (here: SELECT Bar { b as f
* }), which is based on the projected entity (here: Bar)
*/
private static class Projection {
private final CdsEntity entity;
private final CqnSelect query;
/**
* The references resolved in this projection layer and their projection stack
*/
private Map> refs = new HashMap<>();
/**
* The aliases of {@link CqnReference}s resolved on this projection layer
*/
private Map aliases = new HashMap<>();
/**
* @param entity the projection entity, which is created / defined by the
* projection. May be null, if there is no such entity definition
* in the model. This is the case when aliases are used in a
* Select statement, creating a "virtual" projection entity.
*
* @param query the projection query, defining the projection entity based on
* the projected entity.
*/
public Projection(CdsEntity entity, CqnSelect query) {
this.entity = entity;
this.query = query;
}
public CdsEntity getEntity() {
return entity;
}
public CqnSelect getQuery() {
return query;
}
}
/**
* Only visible internally. Used to ease aborting of projection resolvement.
* Can't use a checked Exception however due to Modifier interface constraints.
*/
private static class UnsupportedProjectionException extends RuntimeException {
private static final long serialVersionUID = 1L;
}
private static List extends CqnReference.Segment> skipFirst(CqnReference ref) {
List extends Segment> segments = ref.segments();
return segments.subList(1, segments.size());
}
private static List extends CqnReference.Segment> skipLast(CqnReference ref) {
List extends Segment> segments = ref.segments();
return segments.subList(0, segments.size() - 1);
}
/**
* @param top the high projection layer
* @param bottom the low projection layer
* @return the projection stack to get from {@code top} to {@bottom}
*/
private Deque getProjectionStack(CdsEntity top, CdsEntity bottom) {
Deque stack = new LinkedList<>();
CdsEntity currentEntity = top;
while (!currentEntity.getQualifiedName().equals(bottom.getQualifiedName())) {
Optional optQuery = currentEntity.query();
if (optQuery.isPresent()) {
if (!isSupportedProjection(currentEntity, optQuery.get())) {
throw new UnsupportedProjectionException();
}
stack.push(new Projection(currentEntity, optQuery.get()));
currentEntity = analyzer.analyze(optQuery.get()).targetEntity();
} else {
// entity is not projected
break;
}
}
return stack;
}
}