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.DataUtils Maven / Gradle / Ivy
/************************************************************************
* © 2020-2023 SAP SE or an SAP affiliate company. All rights reserved. *
************************************************************************/
package com.sap.cds.util;
import static com.google.common.collect.Streams.stream;
import static com.sap.cds.reflect.CdsAnnotatable.byAnnotation;
import static com.sap.cds.reflect.CdsBaseType.DATETIME;
import static com.sap.cds.reflect.CdsBaseType.STRING;
import static com.sap.cds.reflect.CdsBaseType.TIMESTAMP;
import static com.sap.cds.reflect.CdsBaseType.UUID;
import static com.sap.cds.util.CdsTypeUtils.dateTime;
import static com.sap.cds.util.CdsTypeUtils.instant;
import static com.sap.cds.util.CdsTypeUtils.isStrictUUID;
import static com.sap.cds.util.CdsTypeUtils.parseUuid;
import static com.sap.cds.util.CdsTypeUtils.timestamp;
import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
import static java.util.UUID.randomUUID;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.joining;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.Lists;
import com.sap.cds.CdsData;
import com.sap.cds.CdsDataProcessor;
import com.sap.cds.CdsDataProcessor.Converter;
import com.sap.cds.CdsException;
import com.sap.cds.SessionContext;
import com.sap.cds.impl.CdsDataImpl;
import com.sap.cds.impl.CdsListImpl;
import com.sap.cds.impl.DataProcessor;
import com.sap.cds.impl.localized.LocaleUtils;
import com.sap.cds.impl.util.Pair;
import com.sap.cds.reflect.CdsArrayedType;
import com.sap.cds.reflect.CdsElement;
import com.sap.cds.reflect.CdsEntity;
import com.sap.cds.reflect.CdsStructuredType;
import com.sap.cds.reflect.CdsType;
import com.sap.cds.reflect.impl.DraftAdapter;
import com.sap.cds.reflect.impl.reader.model.CdsConstants;
public class DataUtils {
private static final Logger logger = LoggerFactory.getLogger(DataUtils.class);
private static final String ON_INSERT = "cds.on.insert";
private static final String ON_ODATA_INSERT = "odata.on.insert";
private static final String ON_UPDATE = "cds.on.update";
private static final String ON_ODATA_UPDATE = "odata.on.update";
private static final CdsDataProcessor uuidKeyNormalizer = DataProcessor.create()
.addConverter((p, e, t) -> e.isKey() && isStrictUUID(e, t), (p, e, uuid) -> parseUuid(uuid));
private final Supplier session;
private final int timestampPrecision;
private final CdsDataProcessor updateDataSanitizer = DataProcessor.create().forUpdate() //
.bulkAction((t, entries) -> resolvePaths(entries))
.bulkAction((t, entries) -> removeOpenTypeElements(t, entries))
.addConverter((p, e, t) -> t.isSimpleType(DATETIME), (p, e, dt) -> dateTime(dt))
.addConverter((p, e, t) -> t.isSimpleType(TIMESTAMP), (p, e, ts) -> ts(ts))
.addConverter((p, e, t) -> isStrictUUID(e, t), (p, e, uuid) -> parseUuid(uuid));
private final CdsDataProcessor insertDataSanitizer = DataProcessor.create().forInsert()
.bulkAction((t, entries) -> resolvePaths(entries)) //
.bulkAction((t, entries) -> removeOpenTypeElements(t, entries))
.addGenerator((p, e, t) -> hasDefaultValue(e, t), (p, e, isNull) -> isNull ? null : defaultValue(e))
.addConverter((p, e, t) -> t.isSimpleType(DATETIME), (p, e, dt) -> dateTime(dt))
.addConverter((p, e, t) -> t.isSimpleType(TIMESTAMP), (p, e, ts) -> ts(ts))
.addConverter((p, e, t) -> isStrictUUID(e, t), (p, e, uuid) -> parseUuid(uuid)) //
.addGenerator((p, e, t) -> e.isKey() && t.isSimpleType(UUID), (p, e, isNull) -> randomUUID().toString());
private final CdsDataProcessor onInsertGenerator = DataProcessor.create().forInsert()
.addGenerator((p, e, t) -> hasOnInsertAnnotation(e),
(p, e, isNull) -> managedValue(e, isNull, new String[] {ON_INSERT, ON_ODATA_INSERT}));
private final CdsDataProcessor onUpdateGenerator = DataProcessor.create().forUpdate()
.addGenerator((p, e, t) -> hasOnUpdateAnnotation(e),
(p, e, isNull) -> managedValue(e, isNull, new String[] {ON_UPDATE, ON_ODATA_UPDATE}));
private final CdsDataProcessor virtualDataSanitizer = DataProcessor.create()
.addConverter((p, e, t) -> e.isVirtual(), (p, e, t) -> Converter.REMOVE);
private final AnnotationValueSupplier annotationValueSupplier = new AnnotationValueSupplier()
.addSupplier(
(v) -> v.equals(CdsConstants.$NOW) || v.equals(CdsConstants.NOW),
(session, annotationValue, type) -> session.get().getNow())
.addSupplier(
(v) -> v.equals(CdsConstants.$USER) || v.equals(CdsConstants.USER),
(session, annotationValue, type) -> session.get().getUserContext().getId())
.addSupplier(
(v) -> v.equals(CdsConstants.$USER_LOCALE),
(session, annotationValue, type) -> LocaleUtils.getLocaleString(session.get().getUserContext().getLocale()))
.addSupplier(
(v) -> v.equals(CdsConstants.$USER_TENANT),
(session, annotationValue, type) -> session.get().getUserContext().getTenant())
.addSupplier(
(v) -> v.equals(CdsConstants.$UUID),
(session, annotationValue, type) -> java.util.UUID.randomUUID().toString())
.addSupplier(this::isArbitraryUserAttribute, this::getUserAttributeValue);
private DataUtils(Supplier session, int timestampPrecision) {
this.session = session;
this.timestampPrecision = timestampPrecision;
}
private Object ts(Object o) {
return timestamp(instant("cds.Timestamp", o), timestampPrecision);
}
private static boolean hasOnInsertAnnotation(CdsElement element) {
return element.findAnnotation(ON_INSERT).isPresent() || element.findAnnotation(ON_ODATA_INSERT).isPresent();
}
private static boolean hasOnUpdateAnnotation(CdsElement element) {
return element.findAnnotation(ON_UPDATE).isPresent() || element.findAnnotation(ON_ODATA_UPDATE).isPresent();
}
public static DataUtils create(Supplier session, int timestampPrecision) {
return new DataUtils(session, timestampPrecision);
}
public static boolean isDeep(CdsStructuredType type, Collection> entries) {
return type.associations().map(CdsElement::getName)
.anyMatch(a -> entries.stream().anyMatch(d -> d.containsKey(a)));
}
public static boolean hasNonKeyValues(CdsStructuredType type, Map data) {
if (data.isEmpty()) {
return false;
}
Set keys = CdsModelUtils.keyNames(type);
return data.keySet().stream().anyMatch(e -> !keys.contains(e));
}
public static boolean uniformData(CdsStructuredType type, Collection> entries) {
if (entries.isEmpty()) {
return true;
}
Set elements = entries.iterator().next().keySet();
return entries.stream().allMatch(e -> e.keySet().equals(elements));
}
public void prepareForInsert(CdsStructuredType struct, List extends Map> entries) {
insertDataSanitizer.process(entries, struct);
processOnInsert(struct, entries);
}
public void prepareForUpdate(CdsStructuredType struct, List extends Map> entries) {
updateDataSanitizer.process(entries, struct);
processOnUpdate(struct,entries, false);
}
public void processOnInsert(CdsStructuredType struct, Iterable extends Map> data) {
onInsertGenerator.process(data, struct);
}
public void processOnUpdate(CdsStructuredType struct, Iterable extends Map> data, boolean deep) {
// TODO We don't know yet, whether all composite entities shall be updated
// or not. onUpdateGenerator.process is processing the entire object tree and adds values for
// all managed attributes although that might not be necessary. It needs to be checked how
// this be handled in a symmetric way.
if (deep) {
onUpdateGenerator.process(data, struct);
} else {
Map managed = new HashMap<>();
struct
.concreteElements()
.filter(byAnnotation(ON_UPDATE).or(byAnnotation(ON_ODATA_UPDATE)))
.forEach(e -> managed.put(e.getName(), managedValue(e, new String[] { ON_UPDATE, ON_ODATA_UPDATE })));
stream(data).forEach(e -> managed.forEach((k, v) -> e.putIfAbsent(k, v)));
}
}
public static void resolvePaths(CdsEntity entity, List extends Map> entries) {
DataProcessor.create().bulkAction((t, data) -> resolvePaths(data)).process(entries, entity);
}
public static boolean hasDefaultValue(CdsElement element, CdsType type) {
return type.isSimple() && element.defaultValue().isPresent()
&& !DraftAdapter.isDraftElement(element.getName()); // TODO CDSJAVA-2385
}
public Object defaultValue(CdsElement e) {
Object val = e.defaultValue().orElse(null);
if (val != null && CdsConstants.$NOW.equalsIgnoreCase(val.toString())) {
val = session.get().getNow();
}
return val;
}
private static void resolvePaths(Iterable> entries) {
entries.forEach(entry -> new HashSet<>(entry.keySet()).forEach( //
key -> resolvePathAndAdd(entry, key, entry.get(key))));
}
@SuppressWarnings("unchecked")
public static void resolvePathAndAdd(Map map, String key, Object value) {
int i = key.indexOf('.');
if (i == -1) {
if (value != null) {
map.put(key, value);
}
} else {
map.remove(key);
String seg = key.substring(0, i);
key = key.substring(i + 1);
if (value == null) {
if (!map.containsKey(seg)) {
map.put(seg, null);
}
} else {
map = (Map) map.computeIfAbsent(seg, s -> new HashMap());
resolvePathAndAdd(map, key, value);
}
}
}
public static Object putPath(Map map, String key, Object value) {
return putPath(map, key, value, true);
}
@SuppressWarnings("unchecked")
public static Object putPath(Map map, String key, Object value, boolean createMaps) {
int i = key.indexOf('.');
if (i == -1) {
return map.put(key, value);
} else {
String seg = key.substring(0, i);
key = key.substring(i + 1);
if (createMaps) {
map = (Map) map.computeIfAbsent(seg, s -> new HashMap());
} else {
map = (Map) map.get(seg);
if (map == null) {
return null;
}
}
return putPath(map, key, value, createMaps);
}
}
@SuppressWarnings("unchecked")
public static void createPath(Map map, String path, boolean notNull) {
int i = path.indexOf('.');
if (i > -1) {
String seg = path.substring(0, i);
path = path.substring(i + 1);
if (notNull) {
map = (Map) map.computeIfAbsent(seg, s -> new HashMap());
} else {
if (path.equals("?")) {
map = (Map) map.putIfAbsent(seg, null);
} else {
map = (Map) map.get(seg);
}
if (map == null) {
return;
}
}
createPath(map, path, notNull);
}
}
private Object managedValue(CdsElement e, boolean isNull, String[] annotations) {
if (isNull) {
return null;
}
return managedValue(e, annotations);
}
private Object managedValue(CdsElement e, String[] annotations) {
Object value = getAnnotationValue(e, annotations);
if (value instanceof Map) {
Object object = getConvertedAnnotationValue(value);
String annotationValue = ((String) object).toLowerCase(Locale.ROOT);
Pair result = annotationValueSupplier.supply(e, e.getType(), annotationValue, session);
if (result.left) {
return result.right;
} else {
logger.debug("Unsupported managed value '{}' in element {}", object, e.getQualifiedName());
return object;
}
}
return value;
}
private boolean isArbitraryUserAttribute(String value) {
return value.startsWith(CdsConstants.$USER) && CdsConstants.$USER.length() + 1 < value.length();
}
private Object getUserAttributeValue(Supplier sessionContext, String annotationValue, CdsType type) {
String attributeName = getUserAttributeName(annotationValue);
List attributeValues = sessionContext.get().getUserContext().getAttribute(attributeName);
if (isNull(attributeValues)) {
// If the attribute is unknown, assign null.
return null;
} else if(type.isArrayed() && ((CdsArrayedType) type).getItemsType().isSimpleType(STRING)) {
// If the type of the CDS element is an array of type String, the list of values is assigned.
return new ArrayList<>(attributeValues);
} else if (type.isSimpleType(STRING)) {
// If the type of the CDS element is a simple type String, the size of the list is checked:
if (attributeValues.isEmpty()) {
// Size == 0: Assign empty string.
return "";
} else if (attributeValues.size() == 1) {
// Size == 1: Assign the first list element.
return attributeValues.get(0);
} else {
// Size > 1: Throw a CdsException ('Value list can't be assigned to a non-array type.')
throw new CdsException("Value list can't be assigned to a non-array type.");
}
} else {
// If the base type of the CDS element is not a String, throw a
// CdsException ('Value list can't be assigned to none-String type.')
throw new CdsException("Value list can't be assigned to a non-string type.");
}
}
private String getUserAttributeName(String annotationValue) {
int index = annotationValue.indexOf('.');
if (index > 1 && annotationValue.substring(index + 1).length() > 0) {
return annotationValue.substring(index + 1);
}
return "";
}
private static Object getAnnotationValue(CdsElement e, String[] annotations) {
return Arrays
.stream(annotations)
.map(annotation -> e.getAnnotationValue(annotation, null))
.filter(Objects::nonNull)
.findFirst()
.orElse(null);
}
private static String getStringAnnotationValue(CdsElement e, String[] annotations) {
Object value = getAnnotationValue(e, annotations);
return nonNull(value) ? getConvertedAnnotationValue(value).toString() : null;
}
@SuppressWarnings("unchecked")
private static Object getConvertedAnnotationValue(Object annotationValue) {
Map value = (Map) annotationValue;
Object result = Optional.ofNullable(value.get("=")).orElse(value.get("#"));
return result;
}
public static List dateTimeValues(List instants) {
return instants.stream().map(CdsTypeUtils::dateTime).collect(Collectors.toList());
}
public static List timestampValues(List instants, int precision) {
return instants.stream().map(i -> timestamp(i, precision)).collect(Collectors.toList());
}
public static boolean generateUuidKeys(CdsStructuredType struct, Map data) {
int size = data.size();
generateUuids(
struct.keyElements()
.filter(key -> key.getType().isSimpleType(UUID)
|| (key.getType().isSimpleType(STRING)) && hasManagedUuidValue(key)),
Lists.newArrayList(data));
return data.size() > size;
}
public static boolean hasManagedUuidValue(CdsElement element) {
String[] annotations = new String[] { ON_INSERT, ON_ODATA_INSERT };
return CdsConstants.$UUID.equals(getStringAnnotationValue(element, annotations));
}
private static void generateUuids(Stream elements, Iterable extends Map> entries) {
List listOfElements = elements.collect(Collectors.toList());
entries.forEach(entry -> listOfElements.forEach(e -> entry.compute(e.getName(), DataUtils::normalizedUuid)));
}
private static String normalizedUuid(String k, Object uuid) {
return uuid != null ? CdsTypeUtils.parseUuid(uuid) : randomUUID().toString();
}
public static void normalizedUuidKeys(CdsStructuredType type, Iterable extends Map> entries) {
uuidKeyNormalizer.process(entries, type);
}
public static Map copyMap(Map original) {
return original.entrySet().stream().collect(CdsData::create,
(m, e) -> m.put(e.getKey(), copyIfListOrMap(e.getValue())), CdsData::putAll);
}
@SuppressWarnings({ "unchecked", "rawtypes" })
public static List copyGenericList(List original) {
if (!original.isEmpty()) {
if (Map.class.isAssignableFrom(original.get(0).getClass())) {
return ((List>) original).stream().map(CdsDataImpl::copy)
.collect(Collectors.toList());
}
}
return new ArrayList(original);
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private static Object copyIfListOrMap(Object original) {
if (original instanceof List) {
return CdsListImpl.copy((List) original);
}
if (original instanceof Map) {
return CdsDataImpl.copy((Map) original);
}
return original; // expected to be immutable
}
public void removeVirtualElements(CdsStructuredType struct, List extends Map> data) {
virtualDataSanitizer.process(data, struct);
}
public void removeOpenTypeElements(CdsStructuredType struct, Iterable extends Map> data) {
if (struct.findAnnotation(CdsConstants.OPEN).isPresent()) {
Set elements = struct.concreteElements().map(CdsElement::getName).collect(Collectors.toSet());
data.forEach(row -> row.keySet().retainAll(elements));
}
}
public static void merge(List extends Map> source, List extends Map> target,
String expRefName, Map mapping, String fkPrefix) {
Map>> groupedTarget = target.stream()
.collect(groupingBy(row -> groupId(mapping.keySet(), row)));
source.forEach(row -> {
String groupId = groupId(mapping.values(), row);
List> list = groupedTarget.getOrDefault(groupId, new ArrayList<>(0));
list.forEach(r -> r.keySet().removeIf(k -> k.startsWith(fkPrefix)));
DataUtils.putPath(row, expRefName, list, !list.isEmpty());
});
}
public static Stream deepMapKeys(Map map) {
return prefixedKeys("", map);
}
private static Stream prefixedKeys(String prefix, Map map) {
return map.entrySet().stream().flatMap(e -> prefixedKeys(prefix, e.getKey(), e.getValue()));
}
@SuppressWarnings("unchecked")
private static Stream prefixedKeys(String prefix, String key, Object value) {
if (value instanceof Map) {
return prefixedKeys(prefix + key + ".", (Map) value);
}
return Stream.of(prefix + key);
}
private static String groupId(Collection mapping, Map values) {
return mapping.stream().map(k -> Objects.toString(values.get(k))).collect(joining("#"));
}
/**
* Extracts the key values for the given entity from the data map. If data does
* not contain a value for a key, it is mapped to null. For association keys, fk
* values can be extracted from nested maps in data.
*
* @param entity the entity
* @param data the data map
* @return the key values of the entity
*/
public static Map keyValues(CdsEntity entity, Map data) {
Map keyValues = new HashMap<>();
Set keyNames = CdsModelUtils.keyNames(entity);
keyNames.forEach(k -> {
if (data.containsKey(k)) {
keyValues.put(k, data.get(k));
} else {
String path = k.replace("_", ".");
Object val = getOrDefault(data, path, null);
keyValues.put(k, val);
}
});
return keyValues;
}
/**
* Returns the value to which the specified key is mapped. The key can be a path
* separated by '.' to extract values from nested maps. If no value is found,
* defaultValue is returned.
*
* @param the type to which the value is casted
* @param data the data map
* @param path the path or key whose associated value is to be returned
* @param defaultValue the default mapping of the key
* @return the value to which the specified key is mapped, or defaultValue if
* this map contains no mapping for the key
*/
@SuppressWarnings("unchecked")
public static T getOrDefault(Map data, String path, T defaultValue) {
if (data.containsKey(path)) {
return (T) data.get(path);
}
return getPathOrDefault(data, path, defaultValue);
}
/**
* Returns the value to which the specified path is mapped. If no value is
* found, defaultValue is returned.
*
* @param the type to which the value is casted
* @param data the data map
* @param path the path with dot separator whose associated value is to
* be returned
* @param defaultValue the default mapping of the key
* @return the value to which the specified key is mapped, or defaultValue if
* this map contains no mapping for the key
*/
public static T getPathOrDefault(Map data, String path, T defaultValue) {
if (!data.isEmpty()) {
return getPathOrDefault(defaultValue, data, path.split("\\."));
}
return defaultValue;
}
@SuppressWarnings("unchecked")
public static T getPathOrDefault(T defaultValue, Map d, String[] segment) {
for (int i = 0; i < segment.length; i++) {
String k = segment[i];
if (i + 1 == segment.length) {
return (T) d.getOrDefault(k, defaultValue);
} else {
d = (Map) d.get(k);
if (d == null) {
return defaultValue;
}
}
}
return defaultValue;
}
public static T getPath(Map d, String[] segment) {
return getPathOrDefault(null, d, segment);
}
@SuppressWarnings("unchecked")
public static T removePath(Map data, String path, boolean removeEmptyMaps) {
int lastDot = path.lastIndexOf(".");
if (lastDot == -1) {
return (T) data.remove(path);
}
String key = path.substring(lastDot + 1);
path = path.substring(0, lastDot);
Map map = getPathOrDefault(data, path, null);
if (map != null) {
Object value = map.remove(key);
if (removeEmptyMaps && map.isEmpty()) {
removePath(data, path, removeEmptyMaps);
}
return (T) value;
}
return null;
}
/**
* Returns true if the specified key is present in the map. The key can be a path separated by
* '.' to check keys in nested maps.
*
* @param data the data map
* @param path the path or key to be checked
* @return true if the specified key is present
*/
public static boolean containsKey(Map data, String path) {
return containsKey(data, path, false);
}
/**
* Returns true if the specified key is present in the map. The key can be a path separated by
* '.' to check keys in nested maps.
*
* @param data the data map
* @param path the path or key to be checked
* @param propagateNull true, if explicit null values should be considered propagating, meaning
* all values from that key-level on are considered explicitly set to null
* @return true if the specified key is present
*/
@SuppressWarnings("unchecked")
public static boolean containsKey(Map data, String path,
boolean propagateNull) {
if (data.containsKey(path)) {
return true;
} else if (data.isEmpty()) {
return false;
}
String[] segments = path.split("\\.");
Map d = data;
for (int i = 0; i < segments.length; ++i) {
String segment = segments[i];
boolean contained = d.containsKey(segment);
if (!contained || i + 1 == segments.length) {
return contained;
}
d = (Map) d.get(segment);
if (d == null) {
return propagateNull;
}
}
return false;
}
public static boolean isFkUpdate(CdsElement assoc, Map data, SessionContext session) {
Map refValues = new OnConditionAnalyzer(assoc, false, session).getFkValues(data);
return refValues.size() == data.size() && !refValues.containsValue(null);
}
private static class AnnotationValueSupplier {
@FunctionalInterface
interface TriFunction {
R apply(A a, B b, C c);
}
private final List, TriFunction, String, CdsType, Object>>>
generators = new ArrayList<>();
AnnotationValueSupplier addSupplier(
Function filter, TriFunction, String, CdsType, Object> supplier) {
generators.add(Pair.of(filter, supplier));
return this;
}
public Pair supply(CdsElement e, CdsType type, String annotationValue, Supplier session) {
for(Pair, TriFunction, String, CdsType, Object>> generator : generators) {
if (generator.left.apply(annotationValue)) {
return Pair.of(true, generator.right.apply(session, annotationValue, type));
}
}
return Pair.of(false, null);
}
}
}