/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.atlas.repository.store.graph.v1; import org.apache.atlas.AtlasErrorCode; import org.apache.atlas.AtlasException; import org.apache.atlas.RequestContextV1; import org.apache.atlas.exception.AtlasBaseException; import org.apache.atlas.model.TypeCategory; import org.apache.atlas.model.instance.AtlasEntity; import org.apache.atlas.model.instance.AtlasObjectId; import org.apache.atlas.model.typedef.AtlasStructDef.AtlasAttributeDef; import org.apache.atlas.repository.Constants; import org.apache.atlas.repository.graph.AtlasEdgeLabel; import org.apache.atlas.repository.graph.GraphHelper; import org.apache.atlas.repository.graphdb.AtlasEdge; import org.apache.atlas.repository.graphdb.AtlasEdgeDirection; import org.apache.atlas.repository.graphdb.AtlasVertex; import org.apache.atlas.type.AtlasArrayType; import org.apache.atlas.type.AtlasEntityType; import org.apache.atlas.type.AtlasMapType; import org.apache.atlas.type.AtlasStructType; import org.apache.atlas.type.AtlasStructType.AtlasAttribute; import org.apache.atlas.type.AtlasType; import org.apache.atlas.type.AtlasTypeRegistry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.Stack; import static org.apache.atlas.repository.graph.GraphHelper.EDGE_LABEL_PREFIX; import static org.apache.atlas.repository.graph.GraphHelper.string; public abstract class DeleteHandlerV1 { public static final Logger LOG = LoggerFactory.getLogger(DeleteHandlerV1.class); private AtlasTypeRegistry typeRegistry; private boolean shouldUpdateInverseReferences; private boolean softDelete; protected static final GraphHelper graphHelper = GraphHelper.getInstance(); public DeleteHandlerV1(AtlasTypeRegistry typeRegistry, boolean shouldUpdateInverseReference, boolean softDelete) { this.typeRegistry = typeRegistry; this.shouldUpdateInverseReferences = shouldUpdateInverseReference; this.softDelete = softDelete; } /** * Deletes the specified entity vertices. * Deletes any traits, composite entities, and structs owned by each entity. * Also deletes all the references from/to the entity. * * @param instanceVertices * @throws AtlasException */ public void deleteEntities(Collection<AtlasVertex> instanceVertices) throws AtlasBaseException { RequestContextV1 requestContext = RequestContextV1.get(); Set<AtlasVertex> deletionCandidateVertices = new HashSet<>(); for (AtlasVertex instanceVertex : instanceVertices) { String guid = AtlasGraphUtilsV1.getIdFromVertex(instanceVertex); AtlasEntity.Status state = AtlasGraphUtilsV1.getState(instanceVertex); if (state == AtlasEntity.Status.DELETED) { LOG.debug("Skipping deletion of {} as it is already deleted", guid); continue; } String typeName = AtlasGraphUtilsV1.getTypeName(instanceVertex); AtlasObjectId objId = new AtlasObjectId(guid, typeName); if (requestContext.getDeletedEntityIds().contains(objId)) { LOG.debug("Skipping deletion of {} as it is already deleted", guid); continue; } // Get GUIDs and vertices for all deletion candidates. Set<GraphHelper.VertexInfo> compositeVertices = getOwnedVertices(instanceVertex); // Record all deletion candidate GUIDs in RequestContext // and gather deletion candidate vertices. for (GraphHelper.VertexInfo vertexInfo : compositeVertices) { requestContext.recordEntityDelete(new AtlasObjectId(vertexInfo.getGuid(), vertexInfo.getTypeName())); deletionCandidateVertices.add(vertexInfo.getVertex()); } } // Delete traits and vertices. for (AtlasVertex deletionCandidateVertex : deletionCandidateVertices) { deleteAllTraits(deletionCandidateVertex); deleteTypeVertex(deletionCandidateVertex, false); } } /** * Get the GUIDs and vertices for all composite entities owned/contained by the specified root entity AtlasVertex. * The graph is traversed from the root entity through to the leaf nodes of the containment graph. * * @param entityVertex the root entity vertex * @return set of VertexInfo for all composite entities * @throws AtlasException */ public Set<GraphHelper.VertexInfo> getOwnedVertices(AtlasVertex entityVertex) throws AtlasBaseException { Set<GraphHelper.VertexInfo> result = new LinkedHashSet<>(); Stack<AtlasVertex> vertices = new Stack<>(); vertices.push(entityVertex); while (vertices.size() > 0) { AtlasVertex vertex = vertices.pop(); AtlasEntity.Status state = AtlasGraphUtilsV1.getState(vertex); if (state == AtlasEntity.Status.DELETED) { //If the reference vertex is marked for deletion, skip it continue; } String typeName = GraphHelper.getTypeName(vertex); String guid = GraphHelper.getGuid(vertex); result.add(new GraphHelper.VertexInfo(guid, vertex, typeName)); AtlasEntityType entityType = typeRegistry.getEntityTypeByName(typeName); if (entityType == null) { throw new AtlasBaseException(AtlasErrorCode.TYPE_NAME_INVALID, TypeCategory.ENTITY.name(), typeName); } for (AtlasStructType.AtlasAttribute attributeInfo : entityType.getAllAttributes().values()) { if (! attributeInfo.isOwnedRef()) { continue; } String edgeLabel = AtlasGraphUtilsV1.getAttributeEdgeLabel(entityType, attributeInfo.getName()); AtlasType attrType = attributeInfo.getAttributeType(); switch (attrType.getTypeCategory()) { case OBJECT_ID_TYPE: AtlasEdge edge = graphHelper.getEdgeForLabel(vertex, edgeLabel); if (edge != null && AtlasGraphUtilsV1.getState(edge) == AtlasEntity.Status.ACTIVE) { AtlasVertex compositeVertex = edge.getInVertex(); vertices.push(compositeVertex); } break; case ARRAY: AtlasArrayType arrType = (AtlasArrayType) attrType; if (arrType.getElementType().getTypeCategory() != TypeCategory.OBJECT_ID_TYPE) { continue; } Iterator<AtlasEdge> edges = graphHelper.getOutGoingEdgesByLabel(vertex, edgeLabel); if (edges != null) { while (edges.hasNext()) { edge = edges.next(); if (edge != null && AtlasGraphUtilsV1.getState(edge) == AtlasEntity.Status.ACTIVE) { AtlasVertex compositeVertex = edge.getInVertex(); vertices.push(compositeVertex); } } } break; case MAP: AtlasMapType mapType = (AtlasMapType) attrType; TypeCategory valueTypeCategory = mapType.getValueType().getTypeCategory(); if (valueTypeCategory != TypeCategory.OBJECT_ID_TYPE) { continue; } String propertyName = AtlasGraphUtilsV1.getQualifiedAttributePropertyKey(entityType, attributeInfo.getName()); List<String> keys = vertex.getProperty(propertyName, List.class); if (keys != null) { for (String key : keys) { String mapEdgeLabel = GraphHelper.getQualifiedNameForMapKey(edgeLabel, key); edge = graphHelper.getEdgeForLabel(vertex, mapEdgeLabel); if (edge != null && AtlasGraphUtilsV1.getState(edge) == AtlasEntity.Status.ACTIVE) { AtlasVertex compositeVertex = edge.getInVertex(); vertices.push(compositeVertex); } } } break; default: } } } return result; } /** * Force delete is used to remove struct/trait in case of entity updates * @param edge * @param typeCategory * @param isOwned * @param forceDeleteStructTrait * @return returns true if the edge reference is hard deleted * @throws AtlasException */ public boolean deleteEdgeReference(AtlasEdge edge, TypeCategory typeCategory, boolean isOwned, boolean forceDeleteStructTrait) throws AtlasBaseException { LOG.debug("Deleting {}", string(edge)); boolean forceDelete = (typeCategory == TypeCategory.STRUCT || typeCategory == TypeCategory.CLASSIFICATION) && forceDeleteStructTrait; if (typeCategory == TypeCategory.STRUCT || typeCategory == TypeCategory.CLASSIFICATION || (typeCategory == TypeCategory.OBJECT_ID_TYPE && isOwned)) { //If the vertex is of type struct/trait, delete the edge and then the reference vertex as the vertex is not shared by any other entities. //If the vertex is of type class, and its composite attribute, this reference vertex' lifecycle is controlled //through this delete, hence delete the edge and the reference vertex. AtlasVertex vertexForDelete = edge.getInVertex(); //If deleting the edge and then the in vertex, reverse attribute shouldn't be updated deleteEdge(edge, false, forceDelete); deleteTypeVertex(vertexForDelete, typeCategory, forceDelete); } else { //If the vertex is of type class, and its not a composite attributes, the reference AtlasVertex' lifecycle is not controlled //through this delete. Hence just remove the reference edge. Leave the reference AtlasVertex as is //If deleting just the edge, reverse attribute should be updated for any references //For example, for the department type system, if the person's manager edge is deleted, subordinates of manager should be updated deleteEdge(edge, true, false); } return !softDelete || forceDelete; } protected void deleteEdge(AtlasEdge edge, boolean updateInverseAttribute, boolean force) throws AtlasBaseException { //update inverse attribute if (updateInverseAttribute) { AtlasEdgeLabel atlasEdgeLabel = new AtlasEdgeLabel(edge.getLabel()); AtlasType parentType = typeRegistry.getType(atlasEdgeLabel.getTypeName()); if (parentType instanceof AtlasEntityType) { AtlasEntityType parentEntityType = (AtlasEntityType) parentType; AtlasStructType.AtlasAttribute attribute = parentEntityType.getAttribute(atlasEdgeLabel.getAttributeName()); if (attribute.getInverseRefAttribute() != null) { deleteEdgeBetweenVertices(edge.getInVertex(), edge.getOutVertex(), attribute.getInverseRefAttribute()); } } } deleteEdge(edge, force); } protected void deleteTypeVertex(AtlasVertex instanceVertex, TypeCategory typeCategory, boolean force) throws AtlasBaseException { switch (typeCategory) { case STRUCT: case CLASSIFICATION: deleteTypeVertex(instanceVertex, force); break; case ENTITY: case OBJECT_ID_TYPE: deleteEntities(Collections.singletonList(instanceVertex)); break; default: throw new IllegalStateException("Type category " + typeCategory + " not handled"); } } /** * Deleting any type vertex. Goes over the complex attributes and removes the references * @param instanceVertex * @throws AtlasException */ protected void deleteTypeVertex(AtlasVertex instanceVertex, boolean force) throws AtlasBaseException { LOG.debug("Deleting {}", string(instanceVertex)); String typeName = GraphHelper.getTypeName(instanceVertex); AtlasType parentType = typeRegistry.getType(typeName); if (parentType instanceof AtlasStructType) { AtlasStructType structType = (AtlasStructType) parentType; boolean isEntityType = (parentType instanceof AtlasEntityType); for (AtlasStructType.AtlasAttribute attributeInfo : structType.getAllAttributes().values()) { LOG.debug("Deleting attribute {} for {}", attributeInfo.getName(), string(instanceVertex)); boolean isOwned = isEntityType && attributeInfo.isOwnedRef(); AtlasType attrType = attributeInfo.getAttributeType(); String edgeLabel = AtlasGraphUtilsV1.getAttributeEdgeLabel(structType, attributeInfo.getName()); switch (attrType.getTypeCategory()) { case OBJECT_ID_TYPE: //If its class attribute, delete the reference deleteEdgeReference(instanceVertex, edgeLabel, attrType.getTypeCategory(), isOwned); break; case STRUCT: //If its struct attribute, delete the reference deleteEdgeReference(instanceVertex, edgeLabel, attrType.getTypeCategory(), false); break; case ARRAY: //For array attribute, if the element is struct/class, delete all the references AtlasArrayType arrType = (AtlasArrayType) attrType; AtlasType elemType = arrType.getElementType(); if (AtlasGraphUtilsV1.isReference(elemType.getTypeCategory())) { Iterator<AtlasEdge> edges = graphHelper.getOutGoingEdgesByLabel(instanceVertex, edgeLabel); if (edges != null) { while (edges.hasNext()) { AtlasEdge edge = edges.next(); deleteEdgeReference(edge, elemType.getTypeCategory(), isOwned, false); } } } break; case MAP: //For map attribute, if the value type is struct/class, delete all the references AtlasMapType mapType = (AtlasMapType) attrType; AtlasType keyType = mapType.getKeyType(); TypeCategory valueTypeCategory = mapType.getValueType().getTypeCategory(); String propertyName = AtlasGraphUtilsV1.getQualifiedAttributePropertyKey(structType, attributeInfo.getName()); if (AtlasGraphUtilsV1.isReference(valueTypeCategory)) { List<Object> keys = EntityGraphMapper.getArrayElementsProperty(keyType, instanceVertex, propertyName); if (keys != null) { for (Object key : keys) { String mapEdgeLabel = GraphHelper.getQualifiedNameForMapKey(edgeLabel, (String) key); deleteEdgeReference(instanceVertex, mapEdgeLabel, valueTypeCategory, isOwned); } } } } } } deleteVertex(instanceVertex, force); } public void deleteEdgeReference(AtlasVertex outVertex, String edgeLabel, TypeCategory typeCategory, boolean isOwned) throws AtlasBaseException { AtlasEdge edge = graphHelper.getEdgeForLabel(outVertex, edgeLabel); if (edge != null) { deleteEdgeReference(edge, typeCategory, isOwned, false); } } /** * Delete all traits from the specified vertex. * @param instanceVertex * @throws AtlasException */ private void deleteAllTraits(AtlasVertex instanceVertex) throws AtlasBaseException { List<String> traitNames = GraphHelper.getTraitNames(instanceVertex); LOG.debug("Deleting traits {} for {}", traitNames, string(instanceVertex)); String typeName = GraphHelper.getTypeName(instanceVertex); for (String traitNameToBeDeleted : traitNames) { String relationshipLabel = GraphHelper.getTraitLabel(typeName, traitNameToBeDeleted); deleteEdgeReference(instanceVertex, relationshipLabel, TypeCategory.CLASSIFICATION, false); } } protected AtlasAttribute getAttributeForEdge(String edgeLabel) throws AtlasBaseException { AtlasEdgeLabel atlasEdgeLabel = new AtlasEdgeLabel(edgeLabel); AtlasType parentType = typeRegistry.getType(atlasEdgeLabel.getTypeName()); AtlasStructType parentStructType = (AtlasStructType) parentType; return parentStructType.getAttribute(atlasEdgeLabel.getAttributeName()); } protected abstract void _deleteVertex(AtlasVertex instanceVertex, boolean force); protected abstract void deleteEdge(AtlasEdge edge, boolean force) throws AtlasBaseException; /** * Deletes the edge between outvertex and inVertex. The edge is for attribute attributeName of outVertex * @param outVertex * @param inVertex * @param attribute * @throws AtlasException */ protected void deleteEdgeBetweenVertices(AtlasVertex outVertex, AtlasVertex inVertex, AtlasAttribute attribute) throws AtlasBaseException { LOG.debug("Removing edge from {} to {} with attribute name {}", string(outVertex), string(inVertex), attribute.getName()); String typeName = GraphHelper.getTypeName(outVertex); String outId = GraphHelper.getGuid(outVertex); AtlasObjectId objId = new AtlasObjectId(outId, typeName); AtlasEntity.Status state = AtlasGraphUtilsV1.getState(outVertex); if (state == AtlasEntity.Status.DELETED || (outId != null && RequestContextV1.get().isDeletedEntity(objId))) { //If the reference vertex is marked for deletion, skip updating the reference return; } AtlasStructType parentType = (AtlasStructType) typeRegistry.getType(typeName); String propertyName = AtlasGraphUtilsV1.getQualifiedAttributePropertyKey(parentType, attribute.getName()); String edgeLabel = EDGE_LABEL_PREFIX + propertyName; AtlasEdge edge = null; AtlasAttributeDef attrDef = attribute.getAttributeDef(); AtlasType attrType = attribute.getAttributeType(); switch (attrType.getTypeCategory()) { case OBJECT_ID_TYPE: //If its class attribute, its the only edge between two vertices if (attrDef.getIsOptional()) { edge = graphHelper.getEdgeForLabel(outVertex, edgeLabel); if (shouldUpdateInverseReferences) { GraphHelper.setProperty(outVertex, propertyName, null); } } else { // Cannot unset a required attribute. throw new AtlasBaseException("Cannot unset required attribute " + propertyName + " on " + GraphHelper.vertexString(outVertex) + " edge = " + edgeLabel); } break; case ARRAY: //If its array attribute, find the right edge between the two vertices and update array property List<String> elements = GraphHelper.getListProperty(outVertex, propertyName); if (elements != null) { elements = new ArrayList<>(elements); //Make a copy, else list.remove reflects on titan.getProperty() for (String elementEdgeId : elements) { AtlasEdge elementEdge = graphHelper.getEdgeByEdgeId(outVertex, edgeLabel, elementEdgeId); if (elementEdge == null) { continue; } AtlasVertex elementVertex = elementEdge.getInVertex(); if (elementVertex.equals(inVertex)) { edge = elementEdge; //TODO element.size includes deleted items as well. should exclude if (!attrDef.getIsOptional() && elements.size() <= attrDef.getValuesMinCount()) { // Deleting this edge would violate the attribute's lower bound. throw new AtlasBaseException( "Cannot remove array element from required attribute " + propertyName + " on " + GraphHelper.getVertexDetails(outVertex) + " " + GraphHelper.getEdgeDetails(elementEdge)); } if (shouldUpdateInverseReferences) { //if composite attribute, remove the reference as well. else, just remove the edge //for example, when table is deleted, process still references the table //but when column is deleted, table will not reference the deleted column LOG.debug("Removing edge {} from the array attribute {}", string(elementEdge), attribute.getName()); // Remove all occurrences of the edge ID from the list. // This prevents dangling edge IDs (i.e. edge IDs for deleted edges) // from the remaining in the list if there are duplicates. elements.removeAll(Collections.singletonList(elementEdge.getId().toString())); GraphHelper.setProperty(outVertex, propertyName, elements); break; } } } } break; case MAP: //If its map attribute, find the right edge between two vertices and update map property List<String> keys = GraphHelper.getListProperty(outVertex, propertyName); if (keys != null) { keys = new ArrayList<>(keys); //Make a copy, else list.remove reflects on titan.getProperty() for (String key : keys) { String keyPropertyName = GraphHelper.getQualifiedNameForMapKey(propertyName, key); String mapEdgeId = GraphHelper.getSingleValuedProperty(outVertex, keyPropertyName, String.class); AtlasEdge mapEdge = graphHelper.getEdgeByEdgeId(outVertex, keyPropertyName, mapEdgeId); if(mapEdge != null) { AtlasVertex mapVertex = mapEdge.getInVertex(); if (mapVertex.getId().toString().equals(inVertex.getId().toString())) { //TODO keys.size includes deleted items as well. should exclude if (attrDef.getIsOptional() || keys.size() > attrDef.getValuesMinCount()) { edge = mapEdge; } else { // Deleting this entry would violate the attribute's lower bound. throw new AtlasBaseException( "Cannot remove map entry " + keyPropertyName + " from required attribute " + propertyName + " on " + GraphHelper.getVertexDetails(outVertex) + " " + GraphHelper.getEdgeDetails(mapEdge)); } if (shouldUpdateInverseReferences) { //remove this key LOG.debug("Removing edge {}, key {} from the map attribute {}", string(mapEdge), key, attribute.getName()); keys.remove(key); GraphHelper.setProperty(outVertex, propertyName, keys); GraphHelper.setProperty(outVertex, keyPropertyName, null); } break; } } } } break; case STRUCT: case CLASSIFICATION: break; default: throw new IllegalStateException("There can't be an edge from " + GraphHelper.getVertexDetails(outVertex) + " to " + GraphHelper.getVertexDetails(inVertex) + " with attribute name " + attribute.getName() + " which is not class/array/map attribute. found " + attrType.getTypeCategory().name()); } if (edge != null) { deleteEdge(edge, false); RequestContextV1 requestContext = RequestContextV1.get(); GraphHelper.setProperty(outVertex, Constants.MODIFICATION_TIMESTAMP_PROPERTY_KEY, requestContext.getRequestTime()); GraphHelper.setProperty(outVertex, Constants.MODIFIED_BY_KEY, requestContext.getUser()); requestContext.recordEntityUpdate(new AtlasObjectId(outId, typeName)); } } protected void deleteVertex(AtlasVertex instanceVertex, boolean force) throws AtlasBaseException { //Update external references(incoming edges) to this vertex LOG.debug("Setting the external references to {} to null(removing edges)", string(instanceVertex)); for (AtlasEdge edge : (Iterable<AtlasEdge>) instanceVertex.getEdges(AtlasEdgeDirection.IN)) { AtlasEntity.Status edgeState = AtlasGraphUtilsV1.getState(edge); if (edgeState == AtlasEntity.Status.ACTIVE) { //Delete only the active edge references AtlasAttribute attribute = getAttributeForEdge(edge.getLabel()); //TODO use delete edge instead?? deleteEdgeBetweenVertices(edge.getOutVertex(), edge.getInVertex(), attribute); } } _deleteVertex(instanceVertex, force); } }