com.sap.cds.reflect.impl.DraftAdapter Maven / Gradle / Ivy
package com.sap.cds.reflect.impl;
import static com.sap.cds.reflect.impl.reader.model.CdsConstants.$SELF;
import static com.sap.cds.reflect.impl.reader.model.CdsConstants.ANNOTATION_CASCADE_ALL;
import static com.sap.cds.reflect.impl.reader.model.CdsConstants.ANNOTATION_CASCADE_UPDATE;
import static com.sap.cds.reflect.impl.reader.model.CdsConstants.ANNOTATION_CDS_AUTOEXPOSED;
import static com.sap.cds.reflect.impl.reader.model.CdsConstants.ANNOTATION_DRAFT_ENABLED;
import static com.sap.cds.reflect.impl.reader.model.CdsConstants.ANNOTATION_DRAFT_ENCLOSED;
import static com.sap.cds.reflect.impl.reader.model.CdsConstants.ANNOTATION_DRAFT_PREPARE_ACTION;
import static com.sap.cds.reflect.impl.reader.model.CdsConstants.ANNOTATION_PERSISTENCE_NAME;
import static com.sap.cds.reflect.impl.reader.model.CdsConstants.ANNOTATION_READONLY;
import static com.sap.cds.reflect.impl.reader.model.CdsConstants.ASSOCIATION;
import static com.sap.cds.reflect.impl.reader.model.CdsConstants.COMPOSITION;
import static com.sap.cds.reflect.impl.reader.model.CdsConstants.ELEMENTS;
import static com.sap.cds.reflect.impl.reader.model.CdsConstants.KEY;
import static com.sap.cds.reflect.impl.reader.model.CdsConstants.LOCALIZEDCSN;
import static com.sap.cds.reflect.impl.reader.model.CdsConstants.ON;
import static com.sap.cds.reflect.impl.reader.model.CdsConstants.PROJECTION;
import static com.sap.cds.reflect.impl.reader.model.CdsConstants.QUERY;
import static com.sap.cds.reflect.impl.reader.model.CdsConstants.TARGET;
import static com.sap.cds.reflect.impl.reader.model.CdsConstants.TYPE;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import com.google.common.collect.Maps;
import com.sap.cds.reflect.impl.reader.model.CdsConstants;
/**
* This class can be used to adapt a CDS model that was generated for OData in a
* way that it reflects the actual database objects and relations between them.
*
*/
public class DraftAdapter {
// suffixes
private static final String DRAFTS_SUFFIX_ENTITY = "_drafts";
private static final String DRAFTS_SUFFIX_TABLE = "_DRAFTS";
// field names of draft entities
public static final String IS_ACTIVE_ENTITY = "IsActiveEntity";
public static final String HAS_ACTIVE_ENTITY = "HasActiveEntity";
public static final String HAS_DRAFT_ENTITY = "HasDraftEntity";
private static final String SIBLING_ENTITY = "SiblingEntity";
private static final String SIBLING_ENTITY_UNSECURED = SIBLING_ENTITY + "_unsecured";
private static final String DRAFT_ADMINISTRATIVE_DATA = "DraftAdministrativeData";
private static final String ADMINSTRATIVE_DATA_TABLE = "DRAFT." + DRAFT_ADMINISTRATIVE_DATA;
public static final String DRAFT_UUID = DRAFT_ADMINISTRATIVE_DATA + "_DraftUUID";
private static final ObjectMapper mapper = new ObjectMapper();
/**
* Flag to indicate if the model should really be adapted
*/
private final boolean adaptDrafts;
/**
* A map of the entities in the model
*/
private final Map entityObjects;
/**
* A map of the services in the model
*/
private final Map serviceObjects;
/**
* The draft entities that were found in the model
*/
private final Map draftEntities = Maps.newLinkedHashMap();
/**
* Constructor
*
* @param adaptDrafts if {@code true}, the model should be adapted
* @param entityObjects a map of the entities in the model
* @param serviceObjects a map of the services in the model
*/
public DraftAdapter(boolean adaptDrafts, Map entityObjects,
Map serviceObjects) {
this.adaptDrafts = adaptDrafts;
this.entityObjects = entityObjects;
this.serviceObjects = serviceObjects;
}
/**
* Checks if the entity is a draft entity
*
* @param name the name of the entity
* @param object the JSON node
*/
public void processEntity(String name, JsonNode object) {
if (adaptDrafts) {
if (isDraftEnabled(object)) {
draftEntities.put(name, object);
}
}
}
/**
* Adapts the model, if required
*/
public void adaptDraftEntities() {
if (adaptDrafts) {
entityObjects.forEach((name, entityNode) -> {
addAssociationsToDraftEntities(entityNode, name);
if (isDraftAdminDataEntity(name)) {
addAccessControlAnnotations(entityNode);
}
});
for (Map.Entry entry : draftEntities.entrySet()) {
JsonNode object = entry.getValue();
String name = entry.getKey();
ObjectNode copy = object.deepCopy();
changeCompositionTargets(copy);
changeAssociations(copy, name);
if (copy.has(ANNOTATION_PERSISTENCE_NAME)) {
copy.put(ANNOTATION_PERSISTENCE_NAME,
copy.get(ANNOTATION_PERSISTENCE_NAME).asText() + DRAFTS_SUFFIX_TABLE);
}
copy.remove(QUERY);
copy.remove(PROJECTION); // required for cds-compiler v2 compatibility
removeSiblingActiveEntityCondition(copy);
removeActiveEntityKeyQualifier(copy);
adaptDraftAdministrativeData(copy);
entityObjects.put(getDraftsEntity(name), copy);
removeDraftFieldsAndChangeSibling(object);
removeLocalizedFromElements(copy);
removeCascadeAnnotationsFromAssociations(copy);
}
}
}
private void removeCascadeAnnotationsFromAssociations(ObjectNode object) {
if (object.has(ELEMENTS)) {
ObjectNode elements = (ObjectNode) object.findValue(ELEMENTS);
elements.fields().forEachRemaining(e -> {
if (!DRAFT_ADMINISTRATIVE_DATA.equals(e.getKey()) && e.getValue().has(TYPE)
&& CdsConstants.ASSOCIATION.equals(e.getValue().get(TYPE).asText())) {
((ObjectNode) e.getValue()).remove(CdsConstants.ANNOTATION_CASCADE_ALL);
((ObjectNode) e.getValue()).remove(CdsConstants.ANNOTATION_CASCADE_UPDATE);
((ObjectNode) e.getValue()).remove(CdsConstants.ANNOTATION_CASCADE_INSERT);
((ObjectNode) e.getValue()).remove(CdsConstants.ANNOTATION_CASCADE_DELETE);
}
});
}
}
private boolean isDraftAdminDataEntity(String name) {
int draftAdminDataIndex = name.lastIndexOf("." + DRAFT_ADMINISTRATIVE_DATA);
if (draftAdminDataIndex > 0) {
String serviceName = name.substring(0, draftAdminDataIndex);
return serviceObjects.get(serviceName) != null;
}
return false;
}
private void addAccessControlAnnotations(JsonNode entityNode) {
if (entityNode.isObject()) {
ObjectNode entityObject = (ObjectNode) entityNode;
// DraftAdministrativeData may only be accessed indirectly (navigation link)
// with 'READ'.
// Draft handler implementation works on persistence service layer and hence
// should not be affected.
// Note: @cds.autoexposed is not fully correct, but this is to indicate the
// runtime that this table has been added by cds tools (compiler or umbrella).
// The annotation will only be visible internally.
entityObject.put(ANNOTATION_CDS_AUTOEXPOSED, true);
entityObject.put(ANNOTATION_READONLY, true);
}
}
private void removeLocalizedFromElements(JsonNode object) {
if (object.has(ELEMENTS)) {
ObjectNode elements = (ObjectNode) object.findValue(ELEMENTS);
elements.forEach(e -> {
if (e.has(LOCALIZEDCSN)) {
ObjectNode o = (ObjectNode) e;
o.remove(LOCALIZEDCSN);
}
});
}
}
private static boolean isDraftEnabled(JsonNode entity) {
return (entity.has(ANNOTATION_DRAFT_ENABLED) && entity.get(ANNOTATION_DRAFT_ENABLED).asBoolean(false))
|| entity.has(ANNOTATION_DRAFT_PREPARE_ACTION);
}
/**
* For every association to a draft enabled entity a second association is added
* which has the draft table as target
*
* @param entity
*/
private void addAssociationsToDraftEntities(JsonNode entity, String name) {
if (entity.has(ELEMENTS)) {
List> tmpList = new ArrayList<>(entity.get(ELEMENTS).size());
entity.get(ELEMENTS).fields().forEachRemaining(tmpList::add);
tmpList.forEach(e -> addDraftAssociation(entity, e.getKey(), e.getValue(), name));
}
}
// draft-association present == not draft-enabled association
// draft-association not present == draft-enabled association
private void addDraftAssociation(JsonNode entity, String elementName, JsonNode element, String entityName) {
if (isAssociation(element) && !SIBLING_ENTITY.equals(elementName)
&& !isDraftEnabledAssociation(entity, entityName, element, elementName)) {
String targetName = element.get(TARGET).textValue();
JsonNode target = entityObjects.get(targetName);
if (isDraftEnabled(target)) {
// if the target of the association is draft enabled a second association
// with the changed target is added
ObjectNode copy = element.deepCopy();
copy.put(TARGET, getDraftsEntity(copy.get(TARGET).textValue()));
String draftAssociationName = getDraftsEntity(elementName);
if (copy.has(ON) && copy.get(ON).isArray()) {
// change the references in the on condition
changeOnCondition(copy, elementName, draftAssociationName);
} else {
// TODO the ON condition relies on foreignKey fields that will not be present
// with universal CSN anymore
// we should model the ON condition using the associations themselves, e.g.
// draftAssociationName = fieldName or draftAssociationName. =
// fieldName..
copy.set("on", onCondition(draftAssociationName, elementName, copy.get("keys")));
}
((ObjectNode) entity.get(ELEMENTS)).set(draftAssociationName, copy);
}
}
}
static ArrayNode onCondition(String draftAssociationName, String fieldName, JsonNode keys) {
Iterator elements = keys.elements();
ArrayNode onCondition = mapper.createArrayNode();
int index = 0;
while (elements.hasNext()) {
if (index > 0) {
onCondition.insert(index++, "and");
}
Iterator refSegments = elements.next().get("ref").elements(); // refs can have more than 1
// segments
List segments = StreamSupport
.stream(Spliterators.spliteratorUnknownSize(refSegments, Spliterator.ORDERED), false)
.map(JsonNode::asText).collect(Collectors.toList());
ObjectNode lhs = refNode(draftAssociationName, segments);
ObjectNode rhs = refNode(fieldName + "_" + segments.get(segments.size() - 1));
onCondition.insert(index++, lhs);
onCondition.insert(index++, "=");
onCondition.insert(index++, rhs);
}
return onCondition;
}
static ObjectNode refNode(String firstSeg, List additionalSegs) {
String[] segments = new String[additionalSegs.size() + 1];
segments[0] = firstSeg;
for (int i = 0; i < additionalSegs.size(); i++) {
segments[i + 1] = additionalSegs.get(i);
}
return refNode(segments);
}
static ObjectNode refNode(String... segments) {
ObjectNode refNode = mapper.createObjectNode();
ArrayNode refArray = mapper.createArrayNode();
for (int i = 0; i < segments.length; i++) {
refArray.insert(i, segments[i]);
}
refNode.set("ref", refArray);
return refNode;
}
// not draft-enabled association => both active and inactive (only if
// associationsToInactiveEntities == true) data is read for the active and
// inactive entities
// draft-enabled association => active data is read for active entities and
// inactive data for inactive entities
private boolean isDraftEnabledAssociation(JsonNode entity, String entityName, JsonNode association,
String associationName) {
if (association.has(ANNOTATION_DRAFT_ENCLOSED)) {
return association.get(ANNOTATION_DRAFT_ENCLOSED).asBoolean(false);
}
// TODO remove again with 2.0
if (association.has(ANNOTATION_DRAFT_ENABLED)) {
return association.get(ANNOTATION_DRAFT_ENABLED).asBoolean(false);
}
return isBacklinkAssociationInDraftDocument(association, associationName, entityName)
|| isForwardAssociationInDraftDocument(entity, association);
}
// Determines associations that are pointing to the same target as a composition
// in that entity
private boolean isForwardAssociationInDraftDocument(JsonNode entity, JsonNode element) {
String targetName = element.get(TARGET).textValue();
AtomicBoolean compositionWithSameTarget = new AtomicBoolean(false);
forCompositions(entity, (composition, compositionName) -> {
String compositionTarget = composition.get(TARGET).asText();
compositionWithSameTarget
.set(compositionTarget.equals(targetName) || compositionTarget.equals(getDraftsEntity(targetName)));
});
return compositionWithSameTarget.get();
}
private boolean isAssociation(JsonNode node) {
return node.has(TYPE) && ASSOCIATION.equals(node.get(TYPE).textValue());
}
private void changeOnCondition(ObjectNode copy, String fieldName, String draftAssociationName) {
ArrayNode onCopy = (ArrayNode) copy.get(ON);
for (int j = 0; j < onCopy.size(); ++j) {
if (onCopy.get(j).has("ref") && onCopy.get(j).get("ref").isArray()) {
ArrayNode refCopy = (ArrayNode) onCopy.get(j).get("ref");
for (int i = 0; i < refCopy.size(); ++i) {
if (fieldName.equals(refCopy.get(i).asText())) {
refCopy.remove(i);
refCopy.insert(i, draftAssociationName);
}
}
}
}
}
/**
* change the composition targets to the drafts tables
*
* @param entity the JSON node
*/
private void changeCompositionTargets(JsonNode entity) {
forCompositions(entity, (composition, compositionName) -> {
String compositionTarget = composition.get(TARGET).asText();
if (draftEntities.containsKey(compositionTarget)) {
composition.put(TARGET, getDraftsEntity(compositionTarget));
}
});
}
/**
* Checks if an association is part of a composition relationship and adapt it
* in this case
*
* @param entity the JSON node
* @param name the name of the entity
*/
private void changeAssociations(JsonNode entity, String name) {
if (entity.has(ELEMENTS)) {
entity.get(ELEMENTS).fields().forEachRemaining(entry -> {
String elementName = entry.getKey();
JsonNode element = entry.getValue();
if (element.has(TYPE)) {
String type = element.get(TYPE).asText();
if (ASSOCIATION.equals(type) && isDraftEnabledAssociation(entity, name, element, elementName)) {
String targetEntityName = element.get(TARGET).asText();
JsonNode targetEntity = entityObjects.get(targetEntityName);
if (isDraftEnabled(targetEntity)) {
((ObjectNode) element).put(TARGET, getDraftsEntity(targetEntityName));
}
}
}
});
}
}
// Determines associations that are pointing back to their draft enabled parent
private boolean isBacklinkAssociationInDraftDocument(JsonNode association, String associationName,
String entityName) {
AtomicBoolean result = new AtomicBoolean(false);
if (association.has(TARGET)) {
String target = association.get(TARGET).asText();
if (draftEntities.containsKey(target)) {
JsonNode parent = draftEntities.get(target);
forCompositions(parent, (composition, compositionName) -> {
String compositionTarget = composition.get(TARGET).asText();
if (compositionTarget.equals(entityName)
&& (hasBacklinkOnCondition(composition, compositionName, associationName)
|| hasBacklinkOnCondition(association, associationName, compositionName))) {
result.set(true);
}
});
}
}
return result.get();
}
// association is the backlink of an association if the ON
// condition of is . = $self
private static boolean hasBacklinkOnCondition(JsonNode partner, String partnerName, String assocName) {
JsonNode onCondition = partner.get(ON);
if (onCondition != null && onCondition.size() == 3) {
JsonNode left = onCondition.get(0).get("ref");
JsonNode operator = onCondition.get(1);
JsonNode right = onCondition.get(2).get("ref");
if (left != null && left.size() == 2 && left.get(0).asText().equals(partnerName)
&& left.get(1).asText().equals(assocName) && operator.asText().equals("=") && right != null
&& right.size() == 1 && right.get(0).asText().equals($SELF)) {
return true;
}
}
return false;
}
private static String getDraftsEntity(String entity) {
return entity + DRAFTS_SUFFIX_ENTITY;
}
/**
* Adapts the target of the DraftAdministrativeData association to the actual
* table because insertions do not work otherwise on SQLite
*
* @param entity the JSON node
*/
private void adaptDraftAdministrativeData(ObjectNode entity) {
ObjectNode adminData = (ObjectNode) entity.findValue(DRAFT_ADMINISTRATIVE_DATA);
if (adminData != null && adminData.has(TARGET)) {
adminData.put(TARGET, ADMINSTRATIVE_DATA_TABLE);
if (entity.has(ANNOTATION_DRAFT_ENABLED) && entity.get(ANNOTATION_DRAFT_ENABLED).asBoolean()) {
adminData.put(ANNOTATION_CASCADE_ALL, true);
} else {
adminData.put(ANNOTATION_CASCADE_UPDATE, true);
}
}
}
/**
* Removes the "key" qualifier for the IsActiveEntity field because it is no key
* on the database
*
* @param entity the JSON node
*/
private void removeActiveEntityKeyQualifier(ObjectNode entity) {
ObjectNode activeEntity = (ObjectNode) entity.findValue(IS_ACTIVE_ENTITY);
if (activeEntity != null) {
activeEntity.remove(KEY);
}
}
/**
* Iterates over all composition types and executes the
* {@link CompositionProcessor}.
*
* @param entity the JSON node
* @param action the consumer
*/
private void forCompositions(JsonNode entity, CompositionProcessor processor) {
JsonNode elements = entity.get(ELEMENTS);
if (elements != null) {
elements.fields().forEachRemaining(entry -> {
String elementName = entry.getKey();
JsonNode element = entry.getValue();
JsonNode type = element.get(TYPE);
if (type != null && COMPOSITION.equals(type.asText())) {
processor.process((ObjectNode) element, elementName);
}
});
}
}
@FunctionalInterface
private interface CompositionProcessor {
/**
* Performs an action for a given composition and it's name
*
* @param composition the composition element node
* @param compositionName the name of the composition element
*/
public void process(ObjectNode composition, String compositionName);
}
/**
* Removes the active entity condition for the sibling since we reference a
* different table for the association
*
* @param entity the JSON node
*/
private void removeSiblingActiveEntityCondition(ObjectNode entity) {
JsonNode sibling = entity.findValue(SIBLING_ENTITY);
if (sibling != null) {
JsonNode on = sibling.findValue(ON);
if (on != null) {
removeActiveEntityCondition(on);
// remove the last textual node
Iterator iter = on.elements();
JsonNode node = null;
while (iter.hasNext()) {
node = iter.next();
}
if (node != null && node.isTextual()) {
iter.remove();
}
}
}
}
private void removeActiveEntityCondition(JsonNode on) {
Iterator iter = on.elements();
while (iter.hasNext()) {
JsonNode ref = iter.next().findValue("ref");
if (ref != null && contains(ref, IS_ACTIVE_ENTITY)) {
iter.remove();
if (iter.hasNext()) {
iter.next();
iter.remove();
}
}
}
}
private static boolean contains(JsonNode ref, String str) {
Iterator iter = ref.elements();
JsonNode current;
while (iter.hasNext()) {
current = iter.next();
if (current.isTextual() && str.equals(current.asText())) {
return true;
}
}
return false;
}
/**
* Removes the draft fields because the original table does not have the draft
* fields. Also adapts the SiblingEntity association
*
* @param object the JSON node
*/
private void removeDraftFieldsAndChangeSibling(JsonNode object) {
if (object.has(ELEMENTS)) {
ObjectNode elements = (ObjectNode) object.findValue(ELEMENTS);
// change sibling entity
if (elements.has(SIBLING_ENTITY)) {
ObjectNode sibling = (ObjectNode) elements.get(SIBLING_ENTITY);
if (sibling.has(TARGET) && sibling.get(TARGET).isTextual()) {
String target = sibling.get(TARGET).asText();
sibling.put(TARGET, getDraftsEntity(target));
removeSiblingActiveEntityCondition(elements);
}
ObjectNode unsecuredSibling = sibling.deepCopy();
addSecurityConstraint(sibling);
changeReferences(unsecuredSibling);
elements.set(SIBLING_ENTITY_UNSECURED, unsecuredSibling);
}
}
}
private void changeReferences(ObjectNode unsecuredSibling) {
if (unsecuredSibling.findValue(ON) != null && unsecuredSibling.findValue(ON).isArray()) {
ArrayNode on = (ArrayNode) unsecuredSibling.findValue(ON);
on.forEach(node -> {
if (node.has("ref") && node.get("ref").isArray()) {
ArrayNode ref = (ArrayNode) node.get("ref");
for (int i = 0; i < ref.size(); ++i) {
if (ref.get(i).isTextual() && ((TextNode) ref.get(i)).asText().equals(SIBLING_ENTITY)) {
ref.remove(i);
ref.insert(i, SIBLING_ENTITY_UNSECURED);
}
}
}
});
}
}
private void addSecurityConstraint(ObjectNode sibling) {
if (sibling.findValue(ON) != null && sibling.findValue(ON).isArray()) {
ArrayNode on = (ArrayNode) sibling.findValue(ON);
on.add("and");
ArrayNode refUuid = mapper.createArrayNode();
refUuid.add(SIBLING_ENTITY);
refUuid.add(DRAFT_UUID);
on.add(createRefNode(refUuid));
on.add("IN (SELECT DraftUUID FROM DRAFT_DraftAdministrativeData where CreatedByUser is null or");
ArrayNode userIdRef = mapper.createArrayNode();
userIdRef.add("$user");
userIdRef.add("id");
on.add(createRefNode(userIdRef));
on.add("= CreatedByUser)");
}
}
private ObjectNode createRefNode(ArrayNode ref) {
ObjectNode node = mapper.createObjectNode();
node.set("ref", ref);
return node;
}
public static boolean isDraftElement(String element) {
return IS_ACTIVE_ENTITY.equals(element) || HAS_ACTIVE_ENTITY.equals(element)
|| HAS_DRAFT_ENTITY.equals(element);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy