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.impl.EntityCascader Maven / Gradle / Ivy
/************************************************************************
* © 2019-2023 SAP SE or an SAP affiliate company. All rights reserved. *
************************************************************************/
package com.sap.cds.impl;
import static com.sap.cds.impl.AssociationAnalyzer.refElements;
import static com.sap.cds.impl.EntityCascader.EntityKeys.keys;
import static com.sap.cds.impl.builder.model.ExpressionImpl.matching;
import static com.sap.cds.util.CdsModelUtils.isCascading;
import static com.sap.cds.util.CdsModelUtils.isReverseAssociation;
import static com.sap.cds.util.CdsModelUtils.isSingleValued;
import static java.util.Collections.emptyMap;
import static java.util.Collections.unmodifiableSet;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiPredicate;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Maps;
import com.sap.cds.CdsDataStore;
import com.sap.cds.Result;
import com.sap.cds.SessionContext;
import com.sap.cds.impl.EntityCascader.EntityOperation.Operation;
import com.sap.cds.impl.builder.model.ElementRefImpl;
import com.sap.cds.ql.CQL;
import com.sap.cds.ql.CdsDataException;
import com.sap.cds.ql.ElementRef;
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.CqnSelect;
import com.sap.cds.ql.cqn.CqnSelectListItem;
import com.sap.cds.reflect.CdsAssociationType;
import com.sap.cds.reflect.CdsElement;
import com.sap.cds.reflect.CdsEntity;
import com.sap.cds.util.CdsModelUtils.CascadeType;
import com.sap.cds.util.DataUtils;
import com.sap.cds.util.OccUtils;
import com.sap.cds.util.OnConditionAnalyzer;
public class EntityCascader {
private final CdsDataStore dataStore;
private final CdsEntity rootEntity;
private final Map paramValues = new HashMap<>();
private final Set visited = new HashSet<>();
private CqnPredicate rootFilter;
private EntityCascader(CdsDataStore dataStore, CdsEntity rootEntity) {
this.dataStore = dataStore;
this.rootEntity = rootEntity;
}
public static EntityCascader from(CdsDataStore dataStore, CdsEntity entity) {
return new EntityCascader(dataStore, entity);
}
public EntityCascader where(Optional pred) {
return where(pred.orElse(null));
}
public EntityCascader where(CqnPredicate pred) {
this.rootFilter = pred;
return this;
}
public EntityCascader with(Map paramValues) {
this.paramValues.clear();
this.paramValues.putAll(paramValues);
return this;
}
public Set cascade(CascadeType cascadeType) {
return cascade(cascadeType, null);
}
public Set cascade(CascadeType cascadeType, String path) {
cascadeRoot(path, (d, a) -> isCascading(cascadeType, a), emptyMap());
return unmodifiableSet(visited);
}
@VisibleForTesting
public Set cascade(Predicate assocFilter) {
cascadeRoot(null, (d, a) -> assocFilter.test(a.getType()), emptyMap());
return unmodifiableSet(visited);
}
public static Stream cascadeDelete(CdsDataStore dataStore, EntityKeys targetEntity, String path) {
return EntityCascader.from(dataStore, targetEntity.entity).where(matching(targetEntity))
.cascade(CascadeType.DELETE, path).stream()
.map(k -> EntityOperation.delete(k, null, dataStore.getSessionContext()));
}
private void cascadeRoot(String path, BiPredicate, CdsElement> assocFilter, Map data) {
cascade(rootEntity, rootFilter, paramValues, path, assocFilter, data);
}
private void cascade(CdsEntity entity, CqnPredicate filter, Map paramValues, String path,
BiPredicate, CdsElement> assocFilter, Map data) {
if (path != null) {
CdsElement assoc = entity.getAssociation(path);
cascade(entity, filter, paramValues, assocFilter, assoc, data);
} else {
entity.associations().filter(a -> assocFilter.test(data, a))
.forEach(assoc -> cascade(entity, filter, paramValues, assocFilter, assoc, data));
}
}
private void cascade(CdsEntity entity, CqnPredicate filter, Map paramValues,
BiPredicate, CdsElement> assocFilter, CdsElement association,
Map data) {
String assocName = association.getName();
CdsAssociationType assocType = association.getType();
StructuredType> path = CQL.entity(entity.getQualifiedName()).filter(filter).to(assocName);
CdsEntity targetEntity = assocType.getTarget();
if (isSingleValued(assocType)) {
cascadeToOne(path, targetEntity, association, paramValues, assocFilter, data);
} else {
cascadeToMany(path, targetEntity, association, paramValues, assocFilter);
}
}
@SuppressWarnings("unchecked")
private static Map getMap(Map data, String key) {
return (Map) data.getOrDefault(key, emptyMap());
}
private void cascadeToOne(StructuredType> path, CdsEntity target, CdsElement association,
Map paramValues, BiPredicate, CdsElement> assocFilter,
Map parentData) {
CqnSelect selectTargetKeys = selectTargetKeys(association, path);
Result targetData = dataStore.execute(selectTargetKeys, paramValues);
List targetKeys = new ArrayList<>();
Map newData = getMap(parentData, association.getName());
targetData.stream().map(data -> keys(target, data)).forEach(targetKeys::add);
targetKeys.stream().filter(this::notVisited)
.forEach(keys -> cascade(target, matching(keys), emptyMap(), null, assocFilter, newData));
}
private void cascadeToMany(StructuredType> path, CdsEntity target, CdsElement association,
Map paramValues, BiPredicate, CdsElement> assocFilter) {
CqnSelect selectTargetKeys = selectTargetKeys(association, path);
Set targetKeys = dataStore.execute(selectTargetKeys, paramValues).stream()
.map(row -> keys(target, row)).collect(Collectors.toSet());
targetKeys.stream().filter(this::notVisited)
.forEach(k -> cascade(target, matching(k), emptyMap(), null, assocFilter, emptyMap()));
}
private Select> selectTargetKeys(CdsElement association, StructuredType> path) {
CdsAssociationType assoc = association.getType();
return Select.from(path).columns(assoc.getTarget().keyElements().flatMap(EntityCascader::slis));
}
private static Stream slis(CdsElement keyElement, String... association) {
String[] path = Arrays.copyOf(association, association.length + 1);
path[path.length - 1] = keyElement.getName();
if (keyElement.getType().isAssociation()) {
return refElements(keyElement).flatMap(k -> slis(k, path));
}
return Stream.of(sli(path));
}
private static CqnSelectListItem sli(String... path) {
ElementRef> element = ElementRefImpl.elementRef(path);
if (path.length > 1) {
String alias = Arrays.stream(path).collect(Collectors.joining("."));
element = element.as(alias);
}
return element;
}
private boolean notVisited(EntityKeys keys) {
return visited.add(keys);
}
public static class EntityOperations {
private final List operations = new ArrayList<>();
private final List> entries = new ArrayList<>();
public void entries(List> entries) {
this.entries.addAll(entries);
}
public List> entries() {
return entries;
}
public boolean add(EntityOperation op) {
return operations.add(op);
}
public Stream rootOps() {
return operations.stream().filter(EntityOperation::isRootOp);
}
public Stream filter(Operation opType) {
return operations.stream().filter(d -> d.operation() == opType);
}
public long[] updateCount() {
long[] updateCount = rootOps().mapToLong(EntityOperation::updateCount).toArray();
if (updateCount.length == 0) {
return new long[] { 0 };
}
return updateCount;
}
}
public static class EntityOperation extends com.google.common.collect.ForwardingMap {
private final EntityKeys targetKeys;
private final Map data = new HashMap<>();
private final Set updated = new HashSet<>();
private final boolean root;
private final String path;
private Map updateData;
private SessionContext sessionContext;
private long updateCount = 0;
private Operation operation;
private EntityOperation(EntityKeys targetKeys, String path, Operation operation, SessionContext sessionContext, boolean root) {
this.targetKeys = targetKeys;
this.path = path;
this.data.putAll(targetKeys);
this.operation = operation;
this.sessionContext = sessionContext;
this.root = root;
}
public static EntityOperation root(EntityKeys entity, SessionContext sessionContext) {
return root(entity, Operation.UPDATE, sessionContext);
}
public static EntityOperation root(EntityKeys entity, Operation op, SessionContext sessionContext) {
return new EntityOperation(entity, null, op, sessionContext, true);
}
public static EntityOperation nop(EntityKeys entity, String path, SessionContext sessionContext) {
return new EntityOperation(entity, path, Operation.NOP, sessionContext, false);
}
public static EntityOperation nop(EntityKeys entity, String path, Map data, SessionContext sessionContext) {
return nop(entity, path, sessionContext).data(data, Collections.emptyMap());
}
public static EntityOperation upsert(EntityKeys entity, Map data, Map fkValues,
SessionContext sessionContext) {
return new EntityOperation(entity, null, Operation.UPSERT, sessionContext, false).update(data, fkValues);
}
public static EntityOperation updateOrInsert(EntityKeys entity, Map data, Map fkValues,
SessionContext sessionContext) {
return new EntityOperation(entity, null, Operation.UPDATE_OR_INSERT, sessionContext, false).update(data, fkValues);
}
public static EntityOperation insert(EntityKeys entity, Map data, Map fkValues,
SessionContext sessionContext) {
return new EntityOperation(entity, null, Operation.INSERT, sessionContext, false).data(data, fkValues);
}
public static EntityOperation delete(EntityKeys targetKeys, String path, SessionContext sessionContext) {
return new EntityOperation(targetKeys, path, Operation.DELETE, sessionContext, false);
}
public CdsEntity targetEntity() {
return targetKeys.entity;
}
public String path() {
return path;
}
public Operation operation() {
return operation;
}
public boolean isRootOp() {
return root;
}
public long updateCount() {
return updateCount;
}
private EntityOperation data(Map data, Map fkValues) {
this.updateData = data;
this.data.putAll(flattenData(targetKeys.entity, data));
this.data.putAll(fkValues);
return this;
}
public boolean inserted(Map data) {
if (!data.isEmpty()) {
updateData.putAll(Maps.filterKeys(data, k -> !this.data.keySet().contains(k)));
this.updateCount = 1;
return true;
}
return false;
}
public boolean updated(Map data, long updateCount) {
this.updateCount = updateCount;
if (!data.isEmpty()) {
updateData.putAll(Maps.filterKeys(data, k -> !this.data.keySet().contains(k)));
updateVersion(data);
return true;
}
return false;
}
/*
* Syncronize version element, as it may have been re-generated/increased after
* successful update
*/
private void updateVersion(Map data) {
if (isRootOp()) {
OccUtils.getVersionElement(targetKeys.entity())
.ifPresent(vers -> updateData.computeIfPresent(vers.getName(), (k, v) -> data.get(k)));
}
}
public EntityOperation deleted() {
return this;
}
public EntityOperation updateToNull(Set keys) {
for (String key : keys) {
data.put(key, null);
updated.add(key);
operation = Operation.UPDATE;
}
return this;
}
public EntityOperation update(Map updateData, Map fkValues) {
mergeData(updateData);
Map flattenedData = flattenData(targetEntity(), updateData);
flattenedData.putAll(fkValues);
flattenedData.forEach((key, newValue) -> {
boolean valuePresent = data.containsKey(key);
Object oldVal = data.put(key, newValue);
if (!valuePresent || !Objects.equals(oldVal, newValue)) {
updated.add(key);
if (operation == Operation.NOP) {
operation = Operation.UPDATE;
}
Object keyVal = targetKeys.get(key);
if (keyVal != null && !keyVal.equals(newValue)) {
throw new CdsDataException("Key values cannot be changed");
}
}
});
return this;
}
private void mergeData(Map data) {
if (updateData == null) {
updateData = data;
} else {
updateData.putAll(data);
}
}
private Map flattenData(CdsEntity entity, Map updateData) {
Map flatEntry = new HashMap<>(updateData);
associationsInData(entity, updateData).forEach(a -> flattenData(a, flatEntry, updateData));
return flatEntry;
}
private static Stream associationsInData(CdsEntity e, Map d) {
return e.associations().filter(a -> d.containsKey(a.getName()));
}
private void flattenData(CdsElement association, Map flatEntry, Map original) {
if (isReverseAssociation(association)) {
flatEntry.remove(association.getName());
} else {
Map fkValues;
OnConditionAnalyzer onConAnalyzer = new OnConditionAnalyzer(association, false, sessionContext);
@SuppressWarnings("unchecked")
Map targetValues = (Map) flatEntry.remove(association.getName());
if (targetValues == null) {
// set FKs to null
fkValues = onConAnalyzer.getFkValues(Collections.emptyMap());
// never set keys to null
Set entityKeys = targetKeys.keys.keySet();
fkValues.entrySet().removeIf(entry -> entityKeys.contains(entry.getKey()));
} else {
CdsAssociationType assocType = association.getType();
targetValues = flattenData(assocType.getTarget(), targetValues);
fkValues = onConAnalyzer.getFkValues(targetValues, false);
}
// override flat FK with struct value
flatEntry.putAll(fkValues);
// for result
fkValues.entrySet().stream()
.filter(e -> original.containsKey(e.getKey()))
.forEach(e -> original.put(e.getKey(), e.getValue()));
}
}
public EntityKeys targetKeys() {
return targetKeys;
}
public Map updateValues() {
Map values = new HashMap<>(Maps.filterKeys(data, updated::contains));
values.putAll(targetKeys);
return values;
}
@Override
protected Map delegate() {
return data;
}
@Override
public int hashCode() {
return Objects.hash(targetKeys, data);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null) {
return false;
}
if (obj.getClass() != this.getClass())
return false;
EntityOperation other = (EntityOperation) obj;
if (!targetKeys.equals(other.targetKeys))
return false;
return data.equals(other.data);
}
@Override
public String toString() {
return operation + " " + targetKeys + ": " + data.toString();
}
public enum Operation {
NOP, INSERT, UPDATE, UPDATE_OR_INSERT, UPSERT, DELETE
}
public enum State {
UNCHANGED, INSERTED, UPDATED, DELETED
}
}
public static class EntityKeys extends com.google.common.collect.ForwardingMap {
private final CdsEntity entity;
private final Map keys;
private EntityKeys(CdsEntity entity, Map keys) {
this.entity = entity;
this.keys = keys;
}
public static EntityKeys keys(CdsEntity entity, Map data) {
Map keyValues = DataUtils.keyValues(entity, data);
if (keyValues.values().contains(null)) {
throw new CdsDataException("Key values of entity " + entity + " must not be null");
}
return new EntityKeys(entity, keyValues);
}
public Map keys() {
return Collections.unmodifiableMap(keys);
}
public CdsEntity entity() {
return entity;
}
@Override
protected Map delegate() {
return keys;
}
@Override
public int hashCode() {
return Objects.hash(entity.getQualifiedName(), keys);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null) {
return false;
}
if (obj.getClass() != this.getClass())
return false;
EntityKeys other = (EntityKeys) obj;
if (!entity.getQualifiedName().equals(other.entity.getQualifiedName()))
return false;
return keys.equals(other.keys);
}
@Override
public String toString() {
return entity.getQualifiedName() + "[" + keys.toString() + "]";
}
}
}