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

com.sap.cds.impl.EntityCascader Maven / Gradle / Ivy

There is a newer version: 3.4.0
Show newest version
/************************************************************************
 * © 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() + "]";
		}
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy