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

com.sap.cds.util.DataUtils Maven / Gradle / Ivy

There is a newer version: 3.8.0
Show newest version
/************************************************************************
 * © 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> entries) {
		insertDataSanitizer.process(entries, struct);
		processOnInsert(struct, entries);
	}

	public void prepareForUpdate(CdsStructuredType struct, List> entries) {
		updateDataSanitizer.process(entries, struct);
		processOnUpdate(struct,entries, false);
	}

	public void processOnInsert(CdsStructuredType struct, Iterable> data) {
		onInsertGenerator.process(data, struct);
	}

	public void processOnUpdate(CdsStructuredType struct, Iterable> 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> 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> 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> 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> data) {
		virtualDataSanitizer.process(data, struct);
	}

	public void removeOpenTypeElements(CdsStructuredType struct, Iterable> 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> source, List> 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);
		}
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy