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.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_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.concurrent.atomic.AtomicBoolean;
import java.util.function.BiConsumer;
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";
private static final String HAS_ACTIVE_ENTITY = "HasActiveEntity";
private 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;
private 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));
}
}
private void addDraftAssociation(JsonNode entity, String fieldName, JsonNode element, String entityName) {
if (isAssociation(element) && !SIBLING_ENTITY.equals(fieldName) && !isParent(element, entityName)
&& !isAssociationToCompositionTarget(entity, element)) {
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(fieldName);
if (copy.has(ON) && copy.get(ON).isArray()) {
// change the references in the on condition
changeOnCondition(copy, fieldName, draftAssociationName);
} else {
// TODO remove this workaround once $generatedFieldName is supported
// currently we need this because cds4j uses the wrong field name
ArrayNode on = mapper.createArrayNode();
ObjectNode obj1 = mapper.createObjectNode();
ArrayNode ref1 = mapper.createArrayNode();
ref1.insert(0, draftAssociationName);
ref1.insert(1, copy.get("keys").get(0).get("ref").get(0).asText());
obj1.set("ref", ref1);
on.insert(0, obj1);
on.insert(1, "=");
ObjectNode obj2 = mapper.createObjectNode();
ArrayNode ref2 = mapper.createArrayNode();
ref2.insert(0, fieldName + "_ID");
obj2.set("ref", ref2);
on.insert(2, obj2);
copy.set("on", on);
}
((ObjectNode) entity.get(ELEMENTS)).set(draftAssociationName, copy);
}
}
}
private boolean isAssociationToCompositionTarget(JsonNode entity, JsonNode element) {
String targetName = element.get(TARGET).textValue();
AtomicBoolean compositionWithSameTarget = new AtomicBoolean(false);
forCompositions(entity, (elem, compositionTarget) -> {
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, (node, target) -> {
if (draftEntities.containsKey(target)) {
node.put(TARGET, getDraftsEntity(target));
}
});
}
/**
* 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) {
for (JsonNode node : entity.findParents(TYPE)) {
String type = node.get(TYPE).asText();
if (ASSOCIATION.equals(type)) {
if (isAssociationToCompositionTarget(entity, node)) {
String targetEntityName = node.get(TARGET).asText();
JsonNode targetEntity = entityObjects.get(targetEntityName);
if (isDraftEnabled(targetEntity)) {
((ObjectNode) node).put(TARGET, getDraftsEntity(targetEntityName));
}
} else {
checkTarget(node, name);
}
}
}
}
/**
* Checks if the target of an association is draft enabled and has this entity
* as composition entity
*
* @param node the JSON node
* @param name the name of the entity
*/
private void checkTarget(JsonNode node, String name) {
if (node.has(TARGET)) {
String target = node.get(TARGET).asText();
if (draftEntities.containsKey(target)) {
JsonNode parent = draftEntities.get(target);
forCompositions(parent, (n, tParent) -> {
if (tParent.equals(name)) {
((ObjectNode) node).put(TARGET, getDraftsEntity(target));
}
});
}
}
}
private boolean isParent(JsonNode node, String name) {
AtomicBoolean result = new AtomicBoolean(false);
if (node.has(TARGET)) {
String target = node.get(TARGET).asText();
if (draftEntities.containsKey(target)) {
JsonNode parent = draftEntities.get(target);
forCompositions(parent, (n, tParent) -> {
if (tParent.equals(name)) {
result.set(true);
}
});
}
}
return result.get();
}
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 {@code action}.
*
* @param entity the JSON node
* @param action the consumer
*/
private void forCompositions(JsonNode entity, BiConsumer action) {
for (JsonNode node : entity.findParents(TYPE)) {
String type = node.get(TYPE).asText();
if (COMPOSITION.equals(type) && node.has(TARGET)) {
String target = node.get(TARGET).asText();
action.accept((ObjectNode) node, target);
}
}
}
/**
* 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