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.ql.impl.DeepUpdateSplitter Maven / Gradle / Ivy
/************************************************************************
* © 2021-2023 SAP SE or an SAP affiliate company. All rights reserved. *
************************************************************************/
package com.sap.cds.ql.impl;
import static com.sap.cds.impl.EntityCascader.cascadeDelete;
import static com.sap.cds.impl.EntityCascader.EntityKeys.keys;
import static com.sap.cds.impl.EntityCascader.EntityOperation.insert;
import static com.sap.cds.impl.EntityCascader.EntityOperation.nop;
import static com.sap.cds.impl.EntityCascader.EntityOperation.root;
import static com.sap.cds.impl.EntityCascader.EntityOperation.updateOrInsert;
import static com.sap.cds.impl.EntityCascader.EntityOperation.upsert;
import static com.sap.cds.impl.RowImpl.row;
import static com.sap.cds.reflect.CdsAnnotatable.byAnnotation;
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 com.sap.cds.util.CqnStatementUtils.hasInfixFilter;
import static com.sap.cds.util.DataUtils.generateUuidKeys;
import static com.sap.cds.util.DataUtils.isFkUpdate;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.sap.cds.CdsData;
import com.sap.cds.CdsList;
import com.sap.cds.CdsDataStore;
import com.sap.cds.Result;
import com.sap.cds.Row;
import com.sap.cds.SessionContext;
import com.sap.cds.impl.Cascader;
import com.sap.cds.impl.EntityCascader.EntityKeys;
import com.sap.cds.impl.EntityCascader.EntityOperation;
import com.sap.cds.impl.EntityCascader.EntityOperation.Operation;
import com.sap.cds.impl.EntityCascader.EntityOperations;
import com.sap.cds.impl.parser.token.RefSegmentBuilder;
import com.sap.cds.ql.CQL;
import com.sap.cds.ql.CdsDataException;
import com.sap.cds.ql.Select;
import com.sap.cds.ql.StructuredType;
import com.sap.cds.ql.cqn.CqnSelect;
import com.sap.cds.ql.cqn.CqnSelectListItem;
import com.sap.cds.ql.cqn.CqnUpdate;
import com.sap.cds.ql.cqn.CqnUpsert;
import com.sap.cds.reflect.CdsAssociationType;
import com.sap.cds.reflect.CdsElement;
import com.sap.cds.reflect.CdsEntity;
import com.sap.cds.reflect.impl.DraftAdapter;
import com.sap.cds.util.CdsModelUtils;
import com.sap.cds.util.CdsModelUtils.CascadeType;
import com.sap.cds.util.CqnStatementUtils;
import com.sap.cds.util.DataUtils;
import com.sap.cds.util.OnConditionAnalyzer;
public class DeepUpdateSplitter {
private static final Logger logger = LoggerFactory.getLogger(DeepUpdateSplitter.class);
private final CdsDataStore dataStore;
private final SessionContext session;
private CdsEntity entity;
private EntityOperations operations;
private boolean deepUpsert;
public DeepUpdateSplitter(CdsDataStore dataStore) {
this.dataStore = dataStore;
this.session = dataStore.getSessionContext();
}
public EntityOperations computeOperations(CdsEntity targetEntity, CqnUpdate update,
Map targetKeys) {
StructuredType> path = targetRef(update);
return computeOps(Operation.UPDATE, path, targetEntity, targetKeys, update.entries());
}
public EntityOperations computeOperations(CdsEntity targetEntity, CqnUpsert upsert,
Map targetKeys) {
this.deepUpsert = true;
StructuredType> path = CQL.to(RefSegmentBuilder.copy(upsert.ref().segments()));
return computeOps(Operation.UPSERT, path, targetEntity, targetKeys, upsert.entries());
}
private EntityOperations computeOps(Operation op, StructuredType> path, CdsEntity targetEntity,
Map targetKeys, List> entries) {
entity = targetEntity;
operations = new EntityOperations();
operations.entries(entries);
List> updateEntries = determineEntries(targetKeys, path);
for (Map entry : updateEntries) {
EntityKeys entityId = keys(entity, entry);
operations.add(root(entityId, op, session).update(entry, emptyMap()));
}
if (!updateEntries.isEmpty()) { // compute ops for child entities
entity.associations().forEach(assoc -> cascade(path, assoc, updateEntries));
}
return operations;
}
private List> determineEntries(Map targetKeys, StructuredType> path) {
Set keyElements = keyNames(entity);
if (!hasInfixFilter(path.asRef())) {
operations.entries().forEach(e -> addKeyValues(entity, targetKeys, e));
if (entriesContainValuesFor(keyElements)) {
// key values in data, no restricting filter
return operations.entries();
}
}
Set targetKeyElements = targetKeys.keySet();
Set missingKeys = Sets.filter(keyElements, k -> !targetKeyElements.contains(k));
if (!entriesContainValuesFor(missingKeys)) {
// missing key values (searched update) -> select w/ filter
return selectKeyValues(path, missingKeys);
}
// filter condition may restrict the update
return evaluateFilter(path, targetKeys, keyElements);
}
private boolean entriesContainValuesFor(Set elements) {
return operations.entries().stream().allMatch(e -> e.keySet().containsAll(elements));
}
private List> selectKeyValues(StructuredType> path, Set missingKeys) {
if (operations.entries().size() == 1) {
logger.warn("Update data is missing key values of entity {}. Executing query to determine key values.",
entity.getQualifiedName());
CqnSelect select = Select.from(path).columns(missingKeys.stream().map(CQL::get));
Result result = dataStore.execute(select);
Map singleEntry = operations.entries().get(0);
List> updateEntries = new ArrayList<>();
if (result.rowCount() == 1) {
Row keyValues = result.single();
singleEntry.putAll(keyValues);
updateEntries.add(singleEntry);
} else if (result.rowCount() > 1) { // searched deep update
result.forEach(row -> row.putAll(DataUtils.copyMap(singleEntry)));
updateEntries.addAll(result.list());
}
return updateEntries;
}
throw new CdsDataException("Update data is missing key values " + missingKeys + " of entity " + entity);
}
private List> evaluateFilter(StructuredType> path, Map targetKeys,
Set keyElements) {
logger.debug("Executing query to evaluate update filter condition {}", path);
if (!targetKeys.isEmpty()) {
// add key values from infix filter to each data entry
operations.entries().forEach(e -> addKeyValues(entity, targetKeys, e));
}
List> updateEntries = new ArrayList<>();
Map entityKeys = selectKeysMatchingPathFilter(path, keyElements, operations.entries());
Map> updateData = operations.entries().stream()
.collect(toMap(row -> index(row, keyElements), Function.identity()));
entityKeys.forEach((hash, key) -> {
updateEntries.add(updateData.get(hash));
});
logger.debug("Update filter condition fulfilled by {} entities", entityKeys.size());
return updateEntries;
}
private static void addKeyValues(CdsEntity entity, Map keyValues, Map target) {
keyValues.forEach((key, valInRef) -> {
Object valInData = target.put(key, valInRef);
if (valInData != null && !valInData.equals(valInRef)) {
throw new CdsDataException("Values for key element '" + key
+ "' in update data do not match values in update ref or where clause");
}
});
target.putAll(Maps.filterValues(DataUtils.keyValues(entity, target), v -> !Objects.isNull(v)));
}
private static StructuredType> targetRef(CqnUpdate update) {
if (!CqnStatementUtils.containsPathExpression(update.where())) {
return CqnStatementUtils.targetRef(update);
}
throw new UnsupportedOperationException("Deep updates with path in where clause are not supported");
}
private Map selectKeysMatchingPathFilter(StructuredType> path, Set keys,
Iterable> keyValueSets) {
CqnSelect select = Select.from(path).columns(keys.stream().map(CQL::get)).byParams(keys);
Result entries = dataStore.execute(select, keyValueSets);
return entries.stream().collect(toMap(row -> index(row, keys), Function.identity()));
}
private void cascade(StructuredType> path, CdsElement assoc, List> entries) {
Iterable> updateEntries = Iterables.filter(entries, e -> e.containsKey(assoc.getName()));
if (!Iterables.isEmpty(updateEntries)) {
CdsEntity targetEntity = assoc.getType().as(CdsAssociationType.class).getTarget();
if (isSingleValued(assoc.getType())) {
toOne(path, assoc, targetEntity, updateEntries);
} else {
toMany(path, assoc, targetEntity, updateEntries);
}
}
}
/*
* Computes operations for to-one associations / compositions
* - remove / delete if association is mapped to null
* - insert / update if association is mapped to data map
*/
private void toOne(StructuredType> path, CdsElement assoc, CdsEntity child,
Iterable> parentEntries) {
CdsEntity parent = assoc.getDeclaringType();
boolean forward = !isReverseAssociation(assoc);
boolean composition = assoc.getType().as(CdsAssociationType.class).isComposition();
Set parentKeys = keyNames(parent);
Map expandedParentEntries = deepUpsert ? emptyMap()
// execute batch select to determine the association's target key values
: selectPathExpandToOneTargetKeys(path, assoc, forward, parentKeys, parentEntries);
List> childEntries = new ArrayList<>(Iterables.size(parentEntries));
for (Map parentEntry : parentEntries) {
Map childEntry = getDataFor(assoc, parentEntry);
if (deepUpsert) {
upsertToOne(parent, child, assoc, forward, composition,
parentEntry, childEntry, childEntries);
} else {
updateToOne(parent, child, assoc, forward, composition, parentKeys,
expandedParentEntries, parentEntry, childEntry, childEntries);
}
}
// cascade over associations
child.associations().forEach(a -> cascade(path.to(assoc.getName()), a, childEntries));
}
private void upsertToOne(CdsEntity parent, CdsEntity child, CdsElement assoc, boolean forward,
boolean composition, Map parentEntry, Map childEntry,
List> childEntries) {
if (childEntry == null) {
// clear FKs & delete target when cascading
remove(assoc, forward, parent, singletonList(parentEntry), assoc.getName());
} else if (isCascading(CascadeType.INSERT, assoc) || isCascading(CascadeType.UPDATE, assoc)) {
childEntries.add(childEntry);
// TODO allow to delete old child via annotation
operations.add(xsert(parentEntry, assoc, forward, child, childEntry, false));
} else if (forward && !composition) {
// set ref to existing target
removeNonFkValues(assoc, childEntry);
}
}
private void updateToOne(CdsEntity parentEntity, CdsEntity targetEntity, CdsElement assoc, boolean forward,
boolean composition, Set parentKeys, Map expandedParentEntries,
Map parentUpdateEntry, Map targetUpdateEntry, List> targetUpdateEntries) {
Row parentEntry = expandedParentEntries.getOrDefault(index(parentUpdateEntry, parentKeys), row(emptyMap()));
Map targetEntry = getDataFor(assoc, parentEntry);
if (targetEntry != null && !targetEntry.isEmpty()) {
// assoc points to an existing target entity
if (targetUpdateEntry == null) {
// clear FKs & delete target when cascading
remove(assoc, forward, targetEntity, singletonList(targetEntry), null);
} else if (targetChange(assoc, forward && !composition, targetEntity, targetEntry, targetUpdateEntry)) {
// target key values changed
if (isCascading(CascadeType.INSERT, assoc) || isCascading(CascadeType.UPDATE, assoc)) {
// composition -> insert / assoc -> update or insert
operations.add(
xsert(parentUpdateEntry, assoc, forward, targetEntity, targetUpdateEntry, composition));
targetUpdateEntries.add(targetUpdateEntry);
} else { // update FK (relaxed data)
removeNonFkValues(assoc, targetUpdateEntry);
}
if (composition) { // delete old target
delete(targetEntity, targetEntry, null);
}
} else if (isCascading(CascadeType.UPDATE, assoc)) { // update target
operations.add(update(targetEntity, targetEntry, targetUpdateEntry, emptyMap()));
targetUpdateEntries.add(targetUpdateEntry);
}
// same target, not cascading update -> ignore
} else if (targetUpdateEntry != null) { // assoc is null
if (forward && !composition && !isCascading(CascadeType.INSERT, assoc)) {
// set ref to existing target
removeNonFkValues(assoc, targetUpdateEntry);
} else { // insert or update target
boolean generatedKey = generateUuidKeys(targetEntity, targetUpdateEntry);
if (forward && generatedKey) { // update parent with target ref (FK) values
Map parentKeyValues = keys(parentEntity, parentUpdateEntry);
Map targetRefValues = fkValues(assoc, !forward, targetUpdateEntry);
operations.add(update(parentEntity, parentKeyValues, new HashMap<>(), targetRefValues));
}
boolean insertOnly = composition || generatedKey;
operations
.add(xsert(parentUpdateEntry, assoc, forward, targetEntity, targetUpdateEntry, insertOnly));
targetUpdateEntries.add(targetUpdateEntry);
}
}
}
private static boolean targetChange(CdsElement assoc, boolean forward, CdsEntity targetEntity,
Map oldEntry, Map newEntry) {
Set refElements = forward ? refElements(assoc, forward) : keyNames(targetEntity);
return refElements.stream().anyMatch(k -> {
Object newVal = newEntry.get(k);
return newVal != null && !newVal.equals(oldEntry.get(k));
});
}
private static void removeNonFkValues(CdsElement assoc, Map data) {
Set assocKeys = refElements(assoc, true);
data.keySet().retainAll(assocKeys);
}
private Map selectPathExpandToOneTargetKeys(StructuredType> path, CdsElement assoc,
boolean forwardMapped, Set parentKeys, Iterable> keyValueSets) {
logger.debug("Executing query to determine target entity of {}", assoc.getQualifiedName());
List slis = parentKeys.stream().map(CQL::get).collect(toList());
Set refElements = refElements(assoc, forwardMapped);
CdsModelUtils.targetKeys(assoc).forEach(refElements::add);
slis.add(CQL.to(assoc.getName()).expand(refElements.toArray(new String[refElements.size()])));
CqnSelect select = Select.from(path).columns(slis).byParams(parentKeys);
Result targetEntries = dataStore.execute(select, keyValueSets);
return targetEntries.stream().collect(toMap(row -> index(row, parentKeys), Function.identity()));
}
private Map fkValues(CdsElement assoc, boolean reverseMapped, Map data) {
return new OnConditionAnalyzer(assoc, reverseMapped, session).getFkValues(data);
}
/*
* Computes operations for to-many associations / compositions
* - insert / update entities that are in the update data list
* - remove / delete entities that are not in the update data list
*/
private void toMany(StructuredType> path, CdsElement assoc, CdsEntity targetEntity,
Iterable> parentEntries) {
boolean composition = assoc.getType().as(CdsAssociationType.class).isComposition();
Set parentKeys = keyNames(assoc.getDeclaringType());
Set targetKeys = CdsModelUtils.targetKeys(assoc);
targetKeys.remove(DraftAdapter.IS_ACTIVE_ENTITY);
// execute batch select to determine the association target entities of non-delta entries
Map fullSetTargetEntries = selectTargetEntries(assoc, parentKeys, targetKeys, parentEntries);
List> updateEntries = new ArrayList<>(Iterables.size(parentEntries));
for (Map parentEntry : parentEntries) {
List> targetUpdateEntries = getDataFor(assoc, parentEntry);
if (targetUpdateEntries == null) {
throw new CdsDataException("Value for to-many association '" + assoc.getDeclaringType() + "." + assoc
+ "' must not be null.");
}
Map parentRefValues = new OnConditionAnalyzer(assoc, true, session)
.getFkValues(parentEntry);
if (parentRefValues.containsValue(null)) {
throw new CdsDataException("Values of ref elements " + parentRefValues.keySet() + " for mapping "
+ targetEntity + " to " + assoc.getDeclaringType() + " cannot be determined from update data.");
}
boolean delta = isDelta(targetUpdateEntries);
for (Map updateEntry : targetUpdateEntries) {
Map updateEntryWithFks = new HashMap<>(updateEntry);
updateEntryWithFks.putAll(parentRefValues);
EntityOperation operation;
if (delta) { // DELTA: remove/delete via annotation
if (isRemove(updateEntry)) {
remove(assoc, false, targetEntity, singletonList(updateEntryWithFks), null);
continue;
}
operation = xsert(assoc, targetEntity, updateEntry, parentRefValues, false);
} else { // FULL-SET: remove/delete if not present in update data
boolean targetPresent = fullSetTargetEntries.remove(index(updateEntryWithFks, targetKeys)) != null;
if (deepUpsert) {
// Do DB upsert to deal with concurrent inserts/deletes
operation = xsert(assoc, targetEntity, updateEntry, parentRefValues, false);
} else {
boolean generatedKey = !targetPresent && generateUuidKeys(targetEntity, updateEntry);
if (generatedKey) {
updateEntryWithFks.putAll(updateEntry);
}
EntityKeys targetId = keys(targetEntity, updateEntryWithFks);
if (generatedKey) { // insert
operation = EntityOperation.insert(targetId, updateEntry, parentRefValues, session);
} else if (targetPresent) { // nop or update
operation = nop(targetId, null, session).update(updateEntry, emptyMap());
} else { // assoc: insert or update / composition: insert
operation = xsert(assoc, targetEntity, updateEntry, parentRefValues, composition);
}
}
}
assertCascading(operation, assoc);
operations.add(operation);
updateEntries.add(updateEntryWithFks);
}
}
// full-sets: remove/delete all entities that are not included in the update data list
remove(assoc, false, targetEntity, fullSetTargetEntries.values(), null);
// cascade over associations
targetEntity.associations().forEach(a -> cascade(path.to(assoc.getName()), a, updateEntries));
}
private static boolean isDelta(List> entries) {
if (entries instanceof CdsList) {
return ((CdsList>) entries).isDelta();
}
return false;
}
private static boolean isRemove(Map entry) {
if (entry instanceof CdsData) {
return ((CdsData) entry).isForRemoval();
}
return false;
}
private Map selectTargetEntries(CdsElement assoc, Set parentKeys, Set targetKeys,
Iterable> keyValueSets) {
logger.debug("Executing query to determine target entity of {}", assoc.getQualifiedName());
StructuredType> path = CQL.entity(assoc.getDeclaringType().getQualifiedName()).filterByParams(parentKeys)
.to(assoc.getName());
CqnSelect select = Select.from(path).columns(targetKeys.stream().map(CQL::get));
Result targetEntries = dataStore.execute(select,
Iterables.filter(keyValueSets, entry -> !isDelta(getDataFor(assoc, entry))));
return targetEntries.stream().collect(toMap(row -> index(row, targetKeys), Function.identity()));
}
private EntityOperation update(CdsEntity target, Map entry, Map updateData, Map fkValues) {
EntityOperation op = nop(keys(target, entry), null, entry, session).update(updateData, fkValues);
updateData.putAll(op.targetKeys());
return op;
}
private void remove(CdsElement association, boolean forwardMapped, CdsEntity targetEntity,
Collection extends Map> keyValues, String path) {
if (isCascading(CascadeType.DELETE, association)) {
keyValues.forEach(k -> delete(targetEntity, k, path));
} else if (!forwardMapped) {
assertCascading(CascadeType.UPDATE, association);
Set parentRefElements = refElements(association, forwardMapped);
keyValues.forEach(k -> {
operations.add(nop(keys(targetEntity, k), path, session).updateToNull(parentRefElements));
});
}
}
private void delete(CdsEntity entity, Map data, String path) {
EntityKeys rootKey = keys(entity, data);
CdsEntity target = entity;
if (path != null) {
target = entity.getTargetOf(path);
}
boolean cascaded = Cascader.create(CascadeType.DELETE, target).from(path)
.cascade(p -> operations.add(EntityOperation.delete(rootKey, p.asRef().path(), session)));
if (!cascaded) {
// fallback for cyclic models and subqueries in where
// compute object graph (select key values)
cascadeDelete(dataStore, rootKey, path).forEach(operations::add);
}
operations.add(EntityOperation.delete(rootKey, path, session));
}
private static Set keyNames(CdsEntity entity) {
Set keyElements = CdsModelUtils.keyNames(entity);
keyElements.remove(DraftAdapter.IS_ACTIVE_ENTITY);
return keyElements;
}
private EntityOperation xsert(Map parentData, CdsElement association, boolean forwardMapped, CdsEntity entity,
Map entityData, boolean insertOnly) {
Map fkValues = emptyMap();
if (!forwardMapped) { // set parent ref (FK) in target entity
fkValues = fkValues(association, !forwardMapped, parentData);
}
return xsert(association, entity, entityData, fkValues, insertOnly);
}
private EntityOperation xsert(CdsElement association, CdsEntity entity, Map entityData,
Map fkValues, boolean insertOnly) {
Map data = new HashMap<>(entityData);
data.putAll(fkValues);
EntityKeys targetEntity = keys(entity, data);
boolean insert = isCascading(CascadeType.INSERT, association);
boolean update = !insertOnly && isCascading(CascadeType.UPDATE, association);
if (!deepUpsert && insert && update && containsStream(entityData)) {
// InputStreams can only be consumed once
// Determine if target exists to decide between insert and update
CqnSelect select = Select.from(entity).columns(CQL.plain("1").as("1")).matching(targetEntity);
update = dataStore.execute(select).rowCount() > 0;
insert = !update;
}
if (insert && update) {
if (deepUpsert || !hasDefaultValues(entity, entityData)) {
return upsert(targetEntity, entityData, fkValues, session);
}
return updateOrInsert(targetEntity, entityData, fkValues, session);
}
if (insert) {
return insert(targetEntity, entityData, fkValues, session);
}
if (update) {
return nop(targetEntity, null, session).update(entityData, fkValues);
}
if (CdsModelUtils.managedToOne(association.getType()) && isFkUpdate(association, entityData, session)) {
return nop(targetEntity, null, session);
}
CdsEntity target = association.getType().as(CdsAssociationType.class).getTarget();
throw new CdsDataException(String.format(
"UPSERT entity '%s' via association '%s.%s' is not allowed. The association does not cascade insert or update.",
target, association.getDeclaringType(), association));
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private static boolean containsStream(Map entityData) {
return entityData.values().stream()
.anyMatch(v -> v instanceof InputStream || v instanceof Map && containsStream((Map) v));
}
private static boolean hasDefaultValues(CdsEntity entity, Map entityData) {
boolean defaultValue = entity.concreteNonAssociationElements().filter(e -> e.defaultValue().isPresent())
.anyMatch(e -> !entityData.containsKey(e.getName()));
return defaultValue || entity.concreteNonAssociationElements()
.filter(byAnnotation("cds.on.insert"))
.anyMatch(e -> !entityData.containsKey(e.getName()));
}
private static void assertCascading(EntityOperation op, CdsElement association) {
switch (op.operation()) {
case INSERT:
assertCascading(CascadeType.INSERT, association);
break;
case UPDATE:
assertCascading(CascadeType.UPDATE, association);
break;
case DELETE:
assertCascading(CascadeType.DELETE, association);
break;
case UPDATE_OR_INSERT:
case UPSERT:
assertCascading(CascadeType.UPDATE, association);
assertCascading(CascadeType.INSERT, association);
}
}
private static void assertCascading(CascadeType cascadeType, CdsElement association) {
if (!isCascading(cascadeType, association)) {
CdsEntity target = association.getType().as(CdsAssociationType.class).getTarget();
throw new CdsDataException(String.format(
"%s entity '%s' via association '%s.%s' is not allowed. The association does not cascade %s.",
cascadeType.name(), target, association.getDeclaringType(), association, cascadeType));
}
}
private static Set refElements(CdsElement assoc, boolean forwardMapped) {
HashMap mapping = new HashMap<>();
new OnConditionAnalyzer(assoc, !forwardMapped).getFkMapping().forEach((fk, val) -> {
if (val.isRef() && !val.asRef().firstSegment().startsWith("$")) {
mapping.put(fk, val.asRef().lastSegment());
}
});
if (forwardMapped) {
return new HashSet<>(mapping.values());
}
return new HashSet<>(mapping.keySet());
}
public static T getDataFor(CdsElement assoc, Map data) {
return DataUtils.getOrDefault(data, assoc.getName(), null);
}
public static String index(Map values, Set keys) {
return values.entrySet().stream().filter(e -> keys.contains(e.getKey()))
.map(e -> e.getKey() + ":" + e.getValue()).sorted().collect(joining("-"));
}
}