You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@atlas.apache.org by sa...@apache.org on 2019/10/02 19:09:42 UTC

[atlas] branch branch-2.0 updated: ATLAS-3431: Ability to create and store user defined key-value pairs in Atlas entity instances

This is an automated email from the ASF dual-hosted git repository.

sarath pushed a commit to branch branch-2.0
in repository https://gitbox.apache.org/repos/asf/atlas.git


The following commit(s) were added to refs/heads/branch-2.0 by this push:
     new ccce4f0  ATLAS-3431: Ability to create and store user defined key-value pairs in Atlas entity instances
ccce4f0 is described below

commit ccce4f0a56a27b0a0a9950eb41c1bc49dbc78a95
Author: Sarath Subramanian <sa...@apache.org>
AuthorDate: Wed Oct 2 09:51:06 2019 -0700

    ATLAS-3431: Ability to create and store user defined key-value pairs in Atlas entity instances
    
    (cherry picked from commit bebe746bab27fb53ac2785720965ce897f6ecb62)
---
 .../org/apache/atlas/repository/Constants.java     |   1 +
 .../java/org/apache/atlas/AtlasConfiguration.java  |   2 +
 .../main/java/org/apache/atlas/AtlasErrorCode.java |   5 +-
 .../apache/atlas/model/instance/AtlasEntity.java   |  17 ++-
 .../repository/graph/GraphBackedSearchIndexer.java |   1 +
 .../apache/atlas/repository/graph/GraphHelper.java |  11 ++
 .../graph/v2/AtlasEntityGraphDiscoveryV2.java      |   9 +-
 .../store/graph/v2/AtlasEntityStoreV2.java         |  12 ++
 .../store/graph/v2/EntityGraphMapper.java          |  69 +++++++++--
 .../store/graph/v2/EntityGraphRetriever.java       |   1 +
 .../store/graph/v2/AtlasEntityStoreV2Test.java     | 126 +++++++++++++++++++++
 11 files changed, 243 insertions(+), 11 deletions(-)

diff --git a/common/src/main/java/org/apache/atlas/repository/Constants.java b/common/src/main/java/org/apache/atlas/repository/Constants.java
index 2f08efc..357affd 100644
--- a/common/src/main/java/org/apache/atlas/repository/Constants.java
+++ b/common/src/main/java/org/apache/atlas/repository/Constants.java
@@ -91,6 +91,7 @@ public final class Constants {
     public static final String CLASSIFICATION_TEXT_KEY              = encodePropertyKey(INTERNAL_PROPERTY_KEY_PREFIX + "classificationsText");
     public static final String CLASSIFICATION_NAMES_KEY             = encodePropertyKey(INTERNAL_PROPERTY_KEY_PREFIX + "classificationNames");
     public static final String PROPAGATED_CLASSIFICATION_NAMES_KEY  = encodePropertyKey(INTERNAL_PROPERTY_KEY_PREFIX + "propagatedClassificationNames");
+    public static final String CUSTOM_ATTRIBUTES_PROPERTY_KEY       = encodePropertyKey(INTERNAL_PROPERTY_KEY_PREFIX + "customAttributes");
 
     public static final String MODIFIED_BY_KEY                      = encodePropertyKey(INTERNAL_PROPERTY_KEY_PREFIX + "modifiedBy");
 
diff --git a/intg/src/main/java/org/apache/atlas/AtlasConfiguration.java b/intg/src/main/java/org/apache/atlas/AtlasConfiguration.java
index 9160524..f9d5fe4 100644
--- a/intg/src/main/java/org/apache/atlas/AtlasConfiguration.java
+++ b/intg/src/main/java/org/apache/atlas/AtlasConfiguration.java
@@ -59,6 +59,8 @@ public enum AtlasConfiguration {
     SEARCH_MAX_LIMIT("atlas.search.maxlimit", 10000),
     SEARCH_DEFAULT_LIMIT("atlas.search.defaultlimit", 100),
 
+    CUSTOM_ATTRIBUTE_KEY_MAX_LENGTH("atlas.custom.attribute.key.max.length", 50),
+    CUSTOM_ATTRIBUTE_VALUE_MAX_LENGTH("atlas.custom.attribute.value.max.length", 500),
     IMPORT_TEMP_DIRECTORY("atlas.import.temp.directory", "");
 
     private static final Configuration APPLICATION_PROPERTIES;
diff --git a/intg/src/main/java/org/apache/atlas/AtlasErrorCode.java b/intg/src/main/java/org/apache/atlas/AtlasErrorCode.java
index 9a1aa65..9812356 100644
--- a/intg/src/main/java/org/apache/atlas/AtlasErrorCode.java
+++ b/intg/src/main/java/org/apache/atlas/AtlasErrorCode.java
@@ -154,7 +154,10 @@ public enum AtlasErrorCode {
     INVALID_TIMEBOUNDRY_DATERANGE(400, "ATLAS-400-00-87D", "Invalid dateRange: startTime {0} must be before endTime {1}"),
     PROPAGATED_CLASSIFICATION_REMOVAL_NOT_SUPPORTED(400, "ATLAS-400-00-87E", "Removal of classification {0}, which is propagated from entity {1}, is not supported"),
     IMPORT_ATTEMPTING_EMPTY_ZIP(400, "ATLAS-400-00-87F", "Attempting to import empty ZIP file."),
-    PATCH_MISSING_RELATIONSHIP_LABEL(400, "ATLAS-400-00-880", "{0} - must include relationship label for type {1}"),
+    PATCH_MISSING_RELATIONSHIP_LABEL(400, "ATLAS-400-00-88", "{0} - must include relationship label for type {1}"),
+    INVALID_CUSTOM_ATTRIBUTE_KEY_LENGTH(400, "ATLAS-400-00-89", "Invalid key: {0} in custom attribute, key size should not be greater than 50"),
+    INVALID_CUSTOM_ATTRIBUTE_KEY_CHARACTERS(400, "ATLAS-400-00-90", "Invalid key: {0} in custom attribute, key should only contain alphanumeric characters, '_' or '-'"),
+    INVALID_CUSTOM_ATTRIBUTE_VALUE(400, "ATLAS-400-00-9A", "Invalid value: {0} in custom attribute, value length is greater than {1}"),
 
     UNAUTHORIZED_ACCESS(403, "ATLAS-403-00-001", "{0} is not authorized to perform {1}"),
 
diff --git a/intg/src/main/java/org/apache/atlas/model/instance/AtlasEntity.java b/intg/src/main/java/org/apache/atlas/model/instance/AtlasEntity.java
index 67493ba..af01896 100644
--- a/intg/src/main/java/org/apache/atlas/model/instance/AtlasEntity.java
+++ b/intg/src/main/java/org/apache/atlas/model/instance/AtlasEntity.java
@@ -91,6 +91,7 @@ public class AtlasEntity extends AtlasStruct implements Serializable {
     private Map<String, Object>             relationshipAttributes;
     private List<AtlasClassification>       classifications;
     private List<AtlasTermAssignmentHeader> meanings;
+    private Map<String, String>             customAttributes;
 
     @JsonIgnore
     private static AtomicLong s_nextId = new AtomicLong(System.nanoTime());
@@ -213,6 +214,7 @@ public class AtlasEntity extends AtlasStruct implements Serializable {
             setClassifications(other.getClassifications());
             setRelationshipAttributes(other.getRelationshipAttributes());
             setMeanings(other.getMeanings());
+            setCustomAttributes(other.getCustomAttributes());
         }
     }
 
@@ -335,6 +337,14 @@ public class AtlasEntity extends AtlasStruct implements Serializable {
         return r != null ? r.containsKey(name) : false;
     }
 
+    public Map<String, String> getCustomAttributes() {
+        return customAttributes;
+    }
+
+    public void setCustomAttributes(Map<String, String> customAttributes) {
+        this.customAttributes = customAttributes;
+    }
+
     public List<AtlasClassification> getClassifications() { return classifications; }
 
     public void setClassifications(List<AtlasClassification> classifications) { this.classifications = classifications; }
@@ -382,6 +392,7 @@ public class AtlasEntity extends AtlasStruct implements Serializable {
         setUpdateTime(null);
         setClassifications(null);
         setMeanings(null);
+        setCustomAttributes(null);
     }
 
     private static String nextInternalId() {
@@ -416,6 +427,9 @@ public class AtlasEntity extends AtlasStruct implements Serializable {
         sb.append(", meanings=[");
         AtlasBaseTypeDef.dumpObjects(meanings, sb);
         sb.append(']');
+        sb.append(", customAttributes=[");
+        dumpObjects(customAttributes, sb);
+        sb.append("]");
         sb.append('}');
 
         return sb;
@@ -440,13 +454,14 @@ public class AtlasEntity extends AtlasStruct implements Serializable {
                 Objects.equals(updateTime, that.updateTime) &&
                 Objects.equals(version, that.version) &&
                 Objects.equals(relationshipAttributes, that.relationshipAttributes) &&
+                Objects.equals(customAttributes, that.customAttributes) &&
                 Objects.equals(classifications, that.classifications);
     }
 
     @Override
     public int hashCode() {
         return Objects.hash(super.hashCode(), guid, homeId, isProxy, isIncomplete, provenanceType, status,
-                createdBy, updatedBy, createTime, updateTime, version, relationshipAttributes, classifications);
+                createdBy, updatedBy, createTime, updateTime, version, relationshipAttributes, classifications, customAttributes);
     }
 
     @Override
diff --git a/repository/src/main/java/org/apache/atlas/repository/graph/GraphBackedSearchIndexer.java b/repository/src/main/java/org/apache/atlas/repository/graph/GraphBackedSearchIndexer.java
index 234ec18..5d53cfd 100755
--- a/repository/src/main/java/org/apache/atlas/repository/graph/GraphBackedSearchIndexer.java
+++ b/repository/src/main/java/org/apache/atlas/repository/graph/GraphBackedSearchIndexer.java
@@ -324,6 +324,7 @@ public class GraphBackedSearchIndexer implements SearchIndexer, ActiveStateChang
             createCommonVertexIndex(management, TRAIT_NAMES_PROPERTY_KEY, UniqueKind.NONE, String.class, SET, true, true);
             createCommonVertexIndex(management, PROPAGATED_TRAIT_NAMES_PROPERTY_KEY, UniqueKind.NONE, String.class, LIST, true, true);
             createCommonVertexIndex(management, IS_INCOMPLETE_PROPERTY_KEY, UniqueKind.NONE, Integer.class, SINGLE, true, true);
+            createCommonVertexIndex(management, CUSTOM_ATTRIBUTES_PROPERTY_KEY, UniqueKind.NONE, String.class, SINGLE, true, false);
 
             createCommonVertexIndex(management, PATCH_ID_PROPERTY_KEY, UniqueKind.GLOBAL_UNIQUE, String.class, SINGLE, true, false);
             createCommonVertexIndex(management, PATCH_DESCRIPTION_PROPERTY_KEY, UniqueKind.NONE, String.class, SINGLE, true, false);
diff --git a/repository/src/main/java/org/apache/atlas/repository/graph/GraphHelper.java b/repository/src/main/java/org/apache/atlas/repository/graph/GraphHelper.java
index 3cc91a5..3dad388 100755
--- a/repository/src/main/java/org/apache/atlas/repository/graph/GraphHelper.java
+++ b/repository/src/main/java/org/apache/atlas/repository/graph/GraphHelper.java
@@ -1065,6 +1065,17 @@ public final class GraphHelper {
         return ret;
     }
 
+    public static Map getCustomAttributes(AtlasElement element) {
+        Map    ret               = null;
+        String customAttrsString = element.getProperty(CUSTOM_ATTRIBUTES_PROPERTY_KEY, String.class);
+
+        if (customAttrsString != null) {
+            ret = AtlasType.fromJson(customAttrsString, Map.class);
+        }
+
+        return ret;
+    }
+
     public static Integer getProvenanceType(AtlasElement element) {
         return element.getProperty(Constants.PROVENANCE_TYPE_KEY, Integer.class);
     }
diff --git a/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/AtlasEntityGraphDiscoveryV2.java b/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/AtlasEntityGraphDiscoveryV2.java
index 5de2d7c..5547c39 100644
--- a/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/AtlasEntityGraphDiscoveryV2.java
+++ b/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/AtlasEntityGraphDiscoveryV2.java
@@ -50,6 +50,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
+import static org.apache.atlas.repository.store.graph.v2.EntityGraphMapper.validateCustomAttributes;
 
 public class AtlasEntityGraphDiscoveryV2 implements EntityGraphDiscovery {
     private static final Logger LOG = LoggerFactory.getLogger(AtlasEntityGraphDiscoveryV2.class);
@@ -84,7 +85,7 @@ public class AtlasEntityGraphDiscoveryV2 implements EntityGraphDiscovery {
     public void validateAndNormalize(AtlasEntity entity) throws AtlasBaseException {
         List<String> messages = new ArrayList<>();
 
-        if (! AtlasTypeUtil.isValidGuid(entity.getGuid())) {
+        if (!AtlasTypeUtil.isValidGuid(entity.getGuid())) {
             throw new AtlasBaseException(AtlasErrorCode.INVALID_OBJECT_ID, "invalid guid " + entity.getGuid());
         }
 
@@ -94,6 +95,8 @@ public class AtlasEntityGraphDiscoveryV2 implements EntityGraphDiscovery {
             throw new AtlasBaseException(AtlasErrorCode.TYPE_NAME_INVALID, TypeCategory.ENTITY.name(), entity.getTypeName());
         }
 
+        validateCustomAttributes(entity);
+
         type.validateValue(entity, entity.getTypeName(), messages);
 
         if (!messages.isEmpty()) {
@@ -107,7 +110,7 @@ public class AtlasEntityGraphDiscoveryV2 implements EntityGraphDiscovery {
     public void validateAndNormalizeForUpdate(AtlasEntity entity) throws AtlasBaseException {
         List<String> messages = new ArrayList<>();
 
-        if (! AtlasTypeUtil.isValidGuid(entity.getGuid())) {
+        if (!AtlasTypeUtil.isValidGuid(entity.getGuid())) {
             throw new AtlasBaseException(AtlasErrorCode.INVALID_OBJECT_ID, "invalid guid " + entity.getGuid());
         }
 
@@ -117,6 +120,8 @@ public class AtlasEntityGraphDiscoveryV2 implements EntityGraphDiscovery {
             throw new AtlasBaseException(AtlasErrorCode.TYPE_NAME_INVALID, TypeCategory.ENTITY.name(), entity.getTypeName());
         }
 
+        validateCustomAttributes(entity);
+
         type.validateValueForUpdate(entity, entity.getTypeName(), messages);
 
         if (!messages.isEmpty()) {
diff --git a/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/AtlasEntityStoreV2.java b/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/AtlasEntityStoreV2.java
index 607adc0..06335c0 100644
--- a/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/AtlasEntityStoreV2.java
+++ b/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/AtlasEntityStoreV2.java
@@ -59,6 +59,7 @@ import static java.lang.Boolean.FALSE;
 import static org.apache.atlas.model.instance.EntityMutations.EntityOperation.DELETE;
 import static org.apache.atlas.model.instance.EntityMutations.EntityOperation.UPDATE;
 import static org.apache.atlas.repository.Constants.IS_INCOMPLETE_PROPERTY_KEY;
+import static org.apache.atlas.repository.graph.GraphHelper.getCustomAttributes;
 import static org.apache.atlas.repository.graph.GraphHelper.isEntityIncomplete;
 
 
@@ -814,6 +815,15 @@ public class AtlasEntityStoreV2 implements AtlasEntityStore {
                         }
                     }
 
+                    if (!hasUpdates && entity.getCustomAttributes() != null) {
+                        Map<String, String> currCustomAttributes = getCustomAttributes(vertex);
+                        Map<String, String> newCustomAttributes  = entity.getCustomAttributes();
+
+                        if (!Objects.equals(currCustomAttributes, newCustomAttributes)) {
+                            hasUpdates = true;
+                        }
+                    }
+
                     // if classifications are to be replaced, then skip updates only when no change in classifications
                     if (!hasUpdates && replaceClassifications) {
                         List<AtlasClassification> newVal  = entity.getClassifications();
@@ -921,6 +931,8 @@ public class AtlasEntityStoreV2 implements AtlasEntityStore {
                         requestContext.recordEntityGuidUpdate(entity, guid);
                     }
 
+                    entityGraphMapper.setCustomAttributes(vertex, entity);
+
                     context.addUpdated(guid, entity, entityType, vertex);
                 } else {
                     graphDiscoverer.validateAndNormalize(entity);
diff --git a/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/EntityGraphMapper.java b/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/EntityGraphMapper.java
index 495d788..4130014 100644
--- a/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/EntityGraphMapper.java
+++ b/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/EntityGraphMapper.java
@@ -72,6 +72,8 @@ import org.springframework.stereotype.Component;
 
 import javax.inject.Inject;
 import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
 import static org.apache.atlas.model.TypeCategory.CLASSIFICATION;
@@ -109,10 +111,13 @@ import static org.apache.atlas.type.AtlasStructType.AtlasAttribute.AtlasRelation
 public class EntityGraphMapper {
     private static final Logger LOG = LoggerFactory.getLogger(EntityGraphMapper.class);
 
-    private static final String SOFT_REF_FORMAT               = "%s:%s";
-    private static final int INDEXED_STR_SAFE_LEN             = AtlasConfiguration.GRAPHSTORE_INDEXED_STRING_SAFE_LENGTH.getInt();
-    private static final boolean WARN_ON_NO_RELATIONSHIP       = AtlasConfiguration.RELATIONSHIP_WARN_NO_RELATIONSHIPS.getBoolean();
-    private static final String CLASSIFICATION_NAME_DELIMITER = "|";
+    private static final String  SOFT_REF_FORMAT                   = "%s:%s";
+    private static final int     INDEXED_STR_SAFE_LEN              = AtlasConfiguration.GRAPHSTORE_INDEXED_STRING_SAFE_LENGTH.getInt();
+    private static final boolean WARN_ON_NO_RELATIONSHIP           = AtlasConfiguration.RELATIONSHIP_WARN_NO_RELATIONSHIPS.getBoolean();
+    private static final String  CLASSIFICATION_NAME_DELIMITER     = "|";
+    private static final Pattern CUSTOM_ATTRIBUTE_KEY_REGEX        = Pattern.compile("^[a-zA-Z0-9_-]*$");
+    private static final int     CUSTOM_ATTRIBUTE_KEY_MAX_LENGTH   = AtlasConfiguration.CUSTOM_ATTRIBUTE_KEY_MAX_LENGTH.getInt();
+    private static final int     CUSTOM_ATTRIBUTE_VALUE_MAX_LENGTH = AtlasConfiguration.CUSTOM_ATTRIBUTE_VALUE_MAX_LENGTH.getInt();
 
     private final GraphHelper               graphHelper = GraphHelper.getInstance();
     private final AtlasGraph                graph;
@@ -138,7 +143,7 @@ public class EntityGraphMapper {
         this.fullTextMapperV2     = fullTextMapperV2;
     }
 
-    public AtlasVertex createVertex(AtlasEntity entity) {
+    public AtlasVertex createVertex(AtlasEntity entity) throws AtlasBaseException {
         final String guid = UUID.randomUUID().toString();
         return createVertexWithGuid(entity, guid);
     }
@@ -179,7 +184,7 @@ public class EntityGraphMapper {
         return ret;
     }
 
-    public AtlasVertex createVertexWithGuid(AtlasEntity entity, String guid) {
+    public AtlasVertex createVertexWithGuid(AtlasEntity entity, String guid) throws AtlasBaseException {
         if (LOG.isDebugEnabled()) {
             LOG.debug("==> createVertexWithGuid({})", entity.getTypeName());
         }
@@ -194,12 +199,14 @@ public class EntityGraphMapper {
         AtlasGraphUtilsV2.setEncodedProperty(ret, GUID_PROPERTY_KEY, guid);
         AtlasGraphUtilsV2.setEncodedProperty(ret, VERSION_PROPERTY_KEY, getEntityVersion(entity));
 
+        setCustomAttributes(ret, entity);
+
         GraphTransactionInterceptor.addToVertexCache(guid, ret);
 
         return ret;
     }
 
-    public void updateSystemAttributes(AtlasVertex vertex, AtlasEntity entity) {
+    public void updateSystemAttributes(AtlasVertex vertex, AtlasEntity entity) throws AtlasBaseException {
         if (entity.getVersion() != null) {
             AtlasGraphUtilsV2.setEncodedProperty(vertex, VERSION_PROPERTY_KEY, entity.getVersion());
         }
@@ -231,6 +238,10 @@ public class EntityGraphMapper {
         if (entity.getProvenanceType() != null) {
             AtlasGraphUtilsV2.setEncodedProperty(vertex, PROVENANCE_TYPE_KEY, entity.getProvenanceType());
         }
+
+        if (entity.getCustomAttributes() != null) {
+            setCustomAttributes(vertex, entity);
+        }
     }
 
     public EntityMutationResponse mapAttributesAndClassifications(EntityMutationContext context, final boolean isPartialUpdate, final boolean replaceClassifications) throws AtlasBaseException {
@@ -308,6 +319,14 @@ public class EntityGraphMapper {
         return resp;
     }
 
+    public void setCustomAttributes(AtlasVertex vertex, AtlasEntity entity) throws AtlasBaseException {
+        String customAttributesString = getCustomAttributesString(entity);
+
+        if (customAttributesString != null) {
+            AtlasGraphUtilsV2.setEncodedProperty(vertex, CUSTOM_ATTRIBUTES_PROPERTY_KEY, customAttributesString);
+        }
+    }
+
     private AtlasVertex createStructVertex(AtlasStruct struct) {
         return createStructVertex(struct.getTypeName());
     }
@@ -1150,6 +1169,17 @@ public class EntityGraphMapper {
         return (ret != null) ? ret : 0;
     }
 
+    private String getCustomAttributesString(AtlasEntity entity) {
+        String              ret              = null;
+        Map<String, String> customAttributes = entity.getCustomAttributes();
+
+        if (customAttributes != null) {
+            ret = AtlasType.toJson(customAttributes);
+        }
+
+        return ret;
+    }
+
     private AtlasStructType getStructType(String typeName) throws AtlasBaseException {
         AtlasType objType = typeRegistry.getType(typeName);
 
@@ -2151,4 +2181,29 @@ public class EntityGraphMapper {
         }
         return relGuidsSet;
     }
+
+    public static void validateCustomAttributes(AtlasEntity entity) throws AtlasBaseException {
+        Map<String, String> customAttributes = entity.getCustomAttributes();
+
+        if (MapUtils.isNotEmpty(customAttributes)) {
+            for (Map.Entry<String, String> entry : customAttributes.entrySet()) {
+                String key   = entry.getKey();
+                String value = entry.getValue();
+
+                if (key.length() > CUSTOM_ATTRIBUTE_KEY_MAX_LENGTH) {
+                    throw new AtlasBaseException(AtlasErrorCode.INVALID_CUSTOM_ATTRIBUTE_KEY_LENGTH, key);
+                }
+
+                Matcher matcher = CUSTOM_ATTRIBUTE_KEY_REGEX.matcher(key);
+
+                if (!matcher.matches()) {
+                    throw new AtlasBaseException(AtlasErrorCode.INVALID_CUSTOM_ATTRIBUTE_KEY_CHARACTERS, key);
+                }
+
+                if (value.length() > CUSTOM_ATTRIBUTE_VALUE_MAX_LENGTH) {
+                    throw new AtlasBaseException(AtlasErrorCode.INVALID_CUSTOM_ATTRIBUTE_VALUE, value, String.valueOf(CUSTOM_ATTRIBUTE_VALUE_MAX_LENGTH));
+                }
+            }
+        }
+    }
 }
diff --git a/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/EntityGraphRetriever.java b/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/EntityGraphRetriever.java
index c921130..9d9f6ef 100644
--- a/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/EntityGraphRetriever.java
+++ b/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/EntityGraphRetriever.java
@@ -582,6 +582,7 @@ public class EntityGraphRetriever {
         entity.setIsIncomplete(isEntityIncomplete(entityVertex));
 
         entity.setProvenanceType(GraphHelper.getProvenanceType(entityVertex));
+        entity.setCustomAttributes(getCustomAttributes(entityVertex));
 
         return entity;
     }
diff --git a/repository/src/test/java/org/apache/atlas/repository/store/graph/v2/AtlasEntityStoreV2Test.java b/repository/src/test/java/org/apache/atlas/repository/store/graph/v2/AtlasEntityStoreV2Test.java
index a4edaf0..b1506f8 100644
--- a/repository/src/test/java/org/apache/atlas/repository/store/graph/v2/AtlasEntityStoreV2Test.java
+++ b/repository/src/test/java/org/apache/atlas/repository/store/graph/v2/AtlasEntityStoreV2Test.java
@@ -55,10 +55,12 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
+import static org.apache.atlas.AtlasErrorCode.*;
 import static org.apache.atlas.TestUtilsV2.COLUMNS_ATTR_NAME;
 import static org.apache.atlas.TestUtilsV2.COLUMN_TYPE;
 import static org.apache.atlas.TestUtilsV2.NAME;
 import static org.apache.atlas.TestUtilsV2.TABLE_TYPE;
+import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
 import static org.mockito.Mockito.mock;
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertTrue;
@@ -982,4 +984,128 @@ public class AtlasEntityStoreV2Test extends AtlasEntityTestBase {
         entityStore.deleteClassification(dbEntityGuid, TAG_NAME);
         entityStore.deleteClassification(tblEntityGuid, TAG_NAME);
     }
+
+    @Test (dependsOnMethods = "testCreate")
+    public void addCustomAttributesToEntity() throws AtlasBaseException {
+        AtlasEntity tblEntity = getEntityFromStore(tblEntityGuid);
+
+        Map<String, String> customAttributes = new HashMap<>();
+        customAttributes.put("key1", "val1");
+        customAttributes.put("key2", "val2");
+        customAttributes.put("key3", "val3");
+        customAttributes.put("key4", "val4");
+        customAttributes.put("key5", "val5");
+
+        tblEntity.setCustomAttributes(customAttributes);
+
+        entityStore.createOrUpdate(new AtlasEntityStream(tblEntity), false);
+
+        tblEntity = getEntityFromStore(tblEntityGuid);
+
+        assertEquals(customAttributes, tblEntity.getCustomAttributes());
+    }
+
+    @Test (dependsOnMethods = "addCustomAttributesToEntity")
+    public void updateCustomAttributesToEntity() throws AtlasBaseException {
+        AtlasEntity tblEntity = getEntityFromStore(tblEntityGuid);
+
+        // update custom attributes, remove key3, key4 and key5
+        Map<String, String> customAttributes = new HashMap<>();
+        customAttributes.put("key1", "val1");
+        customAttributes.put("key2", "val2");
+
+        tblEntity.setCustomAttributes(customAttributes);
+
+        entityStore.createOrUpdate(new AtlasEntityStream(tblEntity), false);
+
+        tblEntity = getEntityFromStore(tblEntityGuid);
+
+        assertEquals(customAttributes, tblEntity.getCustomAttributes());
+    }
+
+    @Test (dependsOnMethods = "updateCustomAttributesToEntity")
+    public void deleteCustomAttributesToEntity() throws AtlasBaseException {
+        AtlasEntity         tblEntity             = getEntityFromStore(tblEntityGuid);
+        Map<String, String> emptyCustomAttributes = new HashMap<>();
+
+        // remove all custom attributes
+        tblEntity.setCustomAttributes(emptyCustomAttributes);
+
+        entityStore.createOrUpdate(new AtlasEntityStream(tblEntity), false);
+
+        tblEntity = getEntityFromStore(tblEntityGuid);
+
+        assertEquals(emptyCustomAttributes, tblEntity.getCustomAttributes());
+    }
+
+    @Test (dependsOnMethods = "deleteCustomAttributesToEntity")
+    public void nullCustomAttributesToEntity() throws AtlasBaseException {
+        AtlasEntity tblEntity = getEntityFromStore(tblEntityGuid);
+
+        Map<String, String> customAttributes = new HashMap<>();
+        customAttributes.put("key1", "val1");
+        customAttributes.put("key2", "val2");
+
+        tblEntity.setCustomAttributes(customAttributes);
+
+        entityStore.createOrUpdate(new AtlasEntityStream(tblEntity), false);
+
+        // assign custom attributes to null
+        tblEntity.setCustomAttributes(null);
+
+        entityStore.createOrUpdate(new AtlasEntityStream(tblEntity), false);
+
+        tblEntity = getEntityFromStore(tblEntityGuid);
+
+        assertEquals(customAttributes, tblEntity.getCustomAttributes());
+    }
+
+    @Test (dependsOnMethods = "nullCustomAttributesToEntity")
+    public void addInvalidKeysToEntityCustomAttributes() throws AtlasBaseException {
+        AtlasEntity tblEntity = getEntityFromStore(tblEntityGuid);
+
+        // key should contain 1 to 50 alphanumeric characters, '_' or '-'
+        Map<String, String> invalidCustomAttributes = new HashMap<>();
+        invalidCustomAttributes.put("key0_65765-6565", "val0");
+        invalidCustomAttributes.put("key1-aaa_bbb-ccc", "val1");
+        invalidCustomAttributes.put("key2!@#$%&*()", "val2"); // invalid key characters
+
+        tblEntity.setCustomAttributes(invalidCustomAttributes);
+
+        try {
+            entityStore.createOrUpdate(new AtlasEntityStream(tblEntity), false);
+        } catch (AtlasBaseException ex) {
+            assertEquals(ex.getAtlasErrorCode(), INVALID_CUSTOM_ATTRIBUTE_KEY_CHARACTERS);
+        }
+
+        invalidCustomAttributes = new HashMap<>();
+        invalidCustomAttributes.put("bigValue_lengthEquals_50", randomAlphanumeric(50));
+        invalidCustomAttributes.put("bigValue_lengthEquals_51", randomAlphanumeric(51));
+
+        tblEntity.setCustomAttributes(invalidCustomAttributes);
+
+        try {
+            entityStore.createOrUpdate(new AtlasEntityStream(tblEntity), false);
+        } catch (AtlasBaseException ex) {
+            assertEquals(ex.getAtlasErrorCode(), INVALID_CUSTOM_ATTRIBUTE_KEY_LENGTH);
+        }
+    }
+
+    @Test (dependsOnMethods = "addInvalidKeysToEntityCustomAttributes")
+    public void addInvalidValuesToEntityCustomAttributes() throws AtlasBaseException {
+        AtlasEntity tblEntity = getEntityFromStore(tblEntityGuid);
+
+        // value length is greater than 500
+        Map<String, String> invalidCustomAttributes = new HashMap<>();
+        invalidCustomAttributes.put("key1", randomAlphanumeric(500));
+        invalidCustomAttributes.put("key2", randomAlphanumeric(501));
+
+        tblEntity.setCustomAttributes(invalidCustomAttributes);
+
+        try {
+            entityStore.createOrUpdate(new AtlasEntityStream(tblEntity), false);
+        } catch (AtlasBaseException ex) {
+            assertEquals(ex.getAtlasErrorCode(), INVALID_CUSTOM_ATTRIBUTE_VALUE);
+        }
+    }
 }
\ No newline at end of file