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

com.sap.cds.ql.impl.DeepInsertSplitter Maven / Gradle / Ivy

There is a newer version: 3.4.0
Show newest version
/************************************************************************
 * © 2019-2022 SAP SE or an SAP affiliate company. All rights reserved. *
 ************************************************************************/
package com.sap.cds.ql.impl;

import static com.google.common.collect.Lists.reverse;
import static com.sap.cds.util.CdsModelUtils.isCascading;
import static com.sap.cds.util.CdsModelUtils.isReverseAssociation;
import static com.sap.cds.util.CdsModelUtils.CascadeType.INSERT;
import static com.sap.cds.util.DataUtils.isFkUpdate;
import static java.util.Collections.singletonList;
import static java.util.stream.Collectors.toList;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.Map.Entry;

import com.sap.cds.SessionContext;
import com.sap.cds.ql.CdsDataException;
import com.sap.cds.ql.Insert;
import com.sap.cds.reflect.CdsAssociationType;
import com.sap.cds.reflect.CdsElement;
import com.sap.cds.reflect.CdsEntity;
import com.sap.cds.reflect.CdsModel;
import com.sap.cds.reflect.CdsStructuredType;
import com.sap.cds.util.CdsModelUtils;
import com.sap.cds.util.OnConditionAnalyzer;

public class DeepInsertSplitter {

	private final CdsEntity entity;
	private final Map>> insertEntries = new LinkedHashMap<>();
	private final SessionContext sessionContext;

	public DeepInsertSplitter(CdsModel model, String entityName, SessionContext sessionContext) {
		this(model.getEntity(entityName), sessionContext);
	}

	public DeepInsertSplitter(CdsEntity entity, SessionContext sessionContext) {
		this.entity = entity;
		this.sessionContext = sessionContext;
	}

	public List split(List> entries) {
		entries.forEach(entry -> flattenEntry(entity, entry));

		return computeInserts();
	}

	public List split(Map entry) {
		flattenEntry(entity, entry);

		return computeInserts();
	}

	private List computeInserts() {
		List inserts = insertEntries.entrySet().stream().map(e -> {
			String entity = e.getKey();
			return Insert.into(entity).entries(e.getValue());
		}).collect(toList());

		return reverse(inserts);
	}

	private void flattenEntry(CdsEntity entity, Map entry) {
		Map flatEntry = new HashMap<>();
		String entityName = entity.getName();

		Iterator> iter = entry.entrySet().iterator();
		while (iter.hasNext()) {
			Entry e = iter.next();
			String elementName = e.getKey();
			Object value = e.getValue();
			// TODO: Re-factor: Use find element instead of association after deprecation of
			// V1 (non-OData)
			Optional assoc = entity.findAssociation(elementName);
			if (assoc.isPresent()) {
				if (value != null) {
					CdsElement association = assoc.get();
					if (isReverseAssociation(association)) {
						handleReverseAssociation(entity, association, entry, value);
					} else {
						handleForwardAssociation(flatEntry, entityName, value, association);
					}
				}
			} else {
				Optional element = entity.findElement(elementName);
				if (element.isPresent() && element.get().getType().isStructured()) {
					CdsElement structElement = element.get();
					if (value != null) {
						flatEntry.put(structElement.getName(), collectStructElement(value, structElement));
					}
				} else if (flatEntry.containsKey(elementName)) {
					// override flat FK with struct value
					e.setValue(flatEntry.get(elementName));
				} else {
					flatEntry.put(elementName, value);
				}
			}
		}
		insertEntries.computeIfAbsent(entity.getQualifiedName(), k -> new ArrayList<>()).add(flatEntry);
	}

	private void handleForwardAssociation(Map flatEntry, String entityName, Object value,
			CdsElement association) {
		Map targetValues = asMap(entityName, association.getName(), value);
		flatEntry.putAll(computeFkValues(association, targetValues));

		if (cascadeInsert(association)) {
			assertInputDataContainsKeys(association, targetValues);
			CdsAssociationType assocType = association.getType();
			flattenEntry(assocType.getTarget(), targetValues);
		} else { // remove non-FK values from result
			Set assocKeys = CdsModelUtils.assocKeys(association);
			targetValues.keySet().retainAll(assocKeys);
		}
	}

	@SuppressWarnings("unchecked")
	private Map collectStructElement(Object value, CdsElement element) {
		Map flatEntry = new HashMap<>();
		Map valueMap = (Map) value;
		CdsStructuredType structElement = element.getType().as(CdsStructuredType.class);
		structElement.associations().filter(f -> valueMap.keySet().contains(f.getName())).forEach(assoc -> {
			handleForwardAssociation(flatEntry, element.getDeclaringType().getName(), valueMap.get(assoc.getName()),
					assoc);
		});
		structElement.nonAssociationElements().filter(f -> valueMap.keySet().contains(f.getName())).forEach(e -> {
			if (e.getType().isStructured()) {
				flatEntry.put(e.getName(), collectStructElement(valueMap.get(e.getName()), e));
			} else {
				flatEntry.put(e.getName(), valueMap.get(e.getName()));
			}
		});
		return flatEntry;
	}

	private void handleReverseAssociation(CdsEntity entity, CdsElement association, Map entry,
			Object targetVal) {
		if (cascadeInsert(association)) {
			CdsAssociationType assoc = association.getType();
			Map fkValues = new OnConditionAnalyzer(association, true, sessionContext)
					.getFkValues(entry);
			List> targetValues = asList(entity.getName(), association.getName(), targetVal);
			for (Map child : targetValues) {
				child.putAll(fkValues);
				flattenEntry(assoc.getTarget(), child);
			}
		} else {
			// TODO set reference from association target to entity
			throw new UnsupportedOperationException("Cannot set reference " + entity.getQualifiedName() + "."
					+ association.getName() + ". Reverse associations are not supported.");
		}
	}

	private static boolean cascadeInsert(CdsElement element) {
		return isCascading(INSERT, element);
	}

	private Map computeFkValues(CdsElement assoc, Map targetValues) {
		return new OnConditionAnalyzer(assoc, false, sessionContext).getFkValues(targetValues);
	}

	private void assertInputDataContainsKeys(CdsElement association, Map targetValues) {
		CdsAssociationType assoc = association.getType();
		Set keyNames = CdsModelUtils.keyNames(assoc.getTarget());
		if (!targetValues.keySet().containsAll(keyNames)) {
			if (isFkUpdate(association, targetValues, sessionContext)) {
				return; // update of FK targeting non-key element
			}
			throw new CdsDataException(
					"Data set " + targetValues.keySet() + " for association " + association.getQualifiedName()
							+ " does not contain values for all target entity keys " + keyNames + ".");
		}
	}

	@SuppressWarnings("unchecked")
	private static Map asMap(String entityName, String associationName, Object value) {
		try {
			return (Map) value;
		} catch (ClassCastException ex) {
			throw badValue(entityName, associationName, ex);
		}
	}

	@SuppressWarnings("unchecked")
	private static List> asList(String entityName, String associationName, Object value) {
		if (value instanceof List) {
			return (List>) value;
		}
		if (value instanceof Map) {
			return singletonList((Map) value);
		}
		throw badValue(entityName, associationName, null);
	}

	private static RuntimeException badValue(String entityName, String associationName, RuntimeException cause) {
		return new CdsDataException("Unexpected value: Entity '" + entityName
				+ "' contains unexpected value for the association '" + associationName + "'. ", cause);
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy