From 6bb7ec68cbe247547792fe6edf626dd46c306a9c Mon Sep 17 00:00:00 2001 From: Christopher Grote Date: Fri, 28 May 2021 08:56:16 +0100 Subject: [PATCH] Revises mappings for HBase metadata Signed-off-by: Christopher Grote --- .../ApacheAtlasOMRSMetadataCollection.java | 670 +++++++++--------- .../mapping/ClassificationDefMapping.java | 4 +- .../mapping/EntityMappingAtlas2OMRS.java | 15 +- .../mapping/RelationshipDefMapping.java | 19 +- .../stores/TypeDefStore.java | 29 +- .../src/main/resources/TypeDefMappings.json | 242 +++++-- .../atlas/mocks/MockServerExpectations.java | 6 +- .../repositoryconnector/ConnectorTest.java | 30 +- docs/mappings/data-objects.drawio | 2 +- 9 files changed, 595 insertions(+), 422 deletions(-) diff --git a/apache-atlas-adapter/src/main/java/org/odpi/egeria/connectors/apache/atlas/repositoryconnector/ApacheAtlasOMRSMetadataCollection.java b/apache-atlas-adapter/src/main/java/org/odpi/egeria/connectors/apache/atlas/repositoryconnector/ApacheAtlasOMRSMetadataCollection.java index f28cc77..c832aa4 100644 --- a/apache-atlas-adapter/src/main/java/org/odpi/egeria/connectors/apache/atlas/repositoryconnector/ApacheAtlasOMRSMetadataCollection.java +++ b/apache-atlas-adapter/src/main/java/org/odpi/egeria/connectors/apache/atlas/repositoryconnector/ApacheAtlasOMRSMetadataCollection.java @@ -1217,8 +1217,8 @@ public List findEntitiesByPropertyValue(String userId, // Add all textual properties of the provided entity as matchProperties, // for an OR-based search of their values - Map> mappingsToSearch = getMappingsToSearch(entityTypeGUID, userId); - for (Map.Entry> entryToSearch : mappingsToSearch.entrySet()) { + Map>> mappingsToSearch = getMappingsToSearch(entityTypeGUID, userId); + for (Map.Entry>> entryToSearch : mappingsToSearch.entrySet()) { String omrsTypeName = entryToSearch.getKey(); String omrsTypeGUID = typeDefStore.getTypeDefByName(omrsTypeName).getGUID(); @@ -1639,14 +1639,14 @@ public void setEventMapper(ApacheAtlasOMRSRepositoryEventMapper eventMapper) { * * @param entityTypeGUID the GUID of the OMRS entity type for which to search * @param userId the userId through which to search - * @return {@code Map>} + * @return {@code Map>>} * @throws RepositoryErrorException on any unexpected error */ - private Map> getMappingsToSearch(String entityTypeGUID, String userId) throws + private Map>> getMappingsToSearch(String entityTypeGUID, String userId) throws RepositoryErrorException { - Map> results = new HashMap<>(); - Map atlasTypeNamesByPrefix = new HashMap<>(); + Map>> results = new HashMap<>(); + Map> atlasTypeNamesByPrefix = new HashMap<>(); String requestedTypeName = null; if (entityTypeGUID != null) { TypeDef typeDef = typeDefStore.getTypeDefByGUID(entityTypeGUID, false); @@ -1663,7 +1663,9 @@ private Map> getMappingsToSearch(String entityTypeGU requestedTypeName = unimplemented.getName(); } } else { - atlasTypeNamesByPrefix.put(null, "Referenceable"); + Set set = new HashSet<>(); + set.add("Referenceable"); + atlasTypeNamesByPrefix.put(null, set); results.put("Referenceable", atlasTypeNamesByPrefix); } @@ -1677,7 +1679,7 @@ private Map> getMappingsToSearch(String entityTypeGU if (!typeDefName.equals("Referenceable") && !typeDefStore.isUnimplementedSupertype(typeDefName) && repositoryHelper.isTypeOf(metadataCollectionId, typeDefName, requestedTypeName)) { - Map mappings = typeDefStore.getAllMappedAtlasTypeDefNames(typeDefName); + Map> mappings = typeDefStore.getAllMappedAtlasTypeDefNames(typeDefName); if (!mappings.isEmpty()) { results.put(typeDefName, mappings); } @@ -1734,190 +1736,193 @@ private List buildAndRunDSLSearch(String methodName, // If we need to order the results, it will probably be more efficient to use Atlas's DSL query language // to do the search boolean skipSearch = false; - StringBuilder sb = new StringBuilder(); // Run multiple searches, if there are multiple types mapped to the OMRS type... - Map> mappingsToSearch = getMappingsToSearch(entityTypeGUID, userId); + Map>> mappingsToSearch = getMappingsToSearch(entityTypeGUID, userId); List totalResults = new ArrayList<>(); - for (Map.Entry> entryToSearch : mappingsToSearch.entrySet()) { + for (Map.Entry>> entryToSearch : mappingsToSearch.entrySet()) { String omrsTypeName = entryToSearch.getKey(); - Map atlasTypeNamesByPrefix = entryToSearch.getValue(); + Map> atlasTypeNamesByPrefix = entryToSearch.getValue(); - for (Map.Entry entry : atlasTypeNamesByPrefix.entrySet()) { + for (Map.Entry> entry : atlasTypeNamesByPrefix.entrySet()) { String prefix = entry.getKey(); - String atlasTypeName = entry.getValue(); - Map omrsPropertyMap = typeDefStore.getPropertyMappingsForOMRSTypeDef(omrsTypeName, prefix); - - sb.append("from "); - sb.append(atlasTypeName); - boolean bWhereClauseAdded = false; - - // Add the multiple classification criteria, if requested - // (recall that OMRS classification name should be identical to Atlas classification name -- no translation needed) - if (limitResultsByClassification != null) { - List classifications = new ArrayList<>(); - for (String classificationName : limitResultsByClassification) { - classifications.add(atlasTypeName + " isa " + classificationName); - } - if (!classifications.isEmpty()) { - sb.append(" where "); - sb.append(String.join(" and ", classifications)); - bWhereClauseAdded = true; + Set atlasTypeNames = entry.getValue(); + + for (String atlasTypeName : atlasTypeNames) { + Map omrsPropertyMap = typeDefStore.getPropertyMappingsForOMRSTypeDef(omrsTypeName, prefix); + + StringBuilder sb = new StringBuilder(); + sb.append("from "); + sb.append(atlasTypeName); + boolean bWhereClauseAdded = false; + + // Add the multiple classification criteria, if requested + // (recall that OMRS classification name should be identical to Atlas classification name -- no translation needed) + if (limitResultsByClassification != null) { + List classifications = new ArrayList<>(); + for (String classificationName : limitResultsByClassification) { + classifications.add(atlasTypeName + " isa " + classificationName); + } + if (!classifications.isEmpty()) { + sb.append(" where "); + sb.append(String.join(" and ", classifications)); + bWhereClauseAdded = true; + } } - } - // Add match properties, if requested - if (matchProperties != null) { - List propertyCriteria = new ArrayList<>(); - Map properties = matchProperties.getInstanceProperties(); - // By default, include only Referenceable's properties (as these will be the only properties that exist - // across ALL entity types) - Map omrsAttrTypeDefs = typeDefStore.getAllTypeDefAttributesForName(omrsTypeName); - if (properties != null) { - for (Map.Entry property : properties.entrySet()) { - String omrsPropertyName = property.getKey(); - InstancePropertyValue value = property.getValue(); - boolean added = addSearchConditionFromValue( - propertyCriteria, - omrsPropertyName, - value, - omrsPropertyMap, - omrsAttrTypeDefs, - (matchCriteria != null) && matchCriteria.equals(MatchCriteria.NONE), - true - ); - if (!added) { - if (matchCriteria == null || matchCriteria.equals(MatchCriteria.ALL)) { - // If we are asked to find everything, but one of the properties cannot be searched, - // then we should skip the search - skipSearch = true; - log.info("Skipping search ({}) -- ALL criteria should match, but this one is unmapped: {}", entityTypeGUID, omrsPropertyName); + // Add match properties, if requested + if (matchProperties != null) { + List propertyCriteria = new ArrayList<>(); + Map properties = matchProperties.getInstanceProperties(); + // By default, include only Referenceable's properties (as these will be the only properties that exist + // across ALL entity types) + Map omrsAttrTypeDefs = typeDefStore.getAllTypeDefAttributesForName(omrsTypeName); + if (properties != null) { + for (Map.Entry property : properties.entrySet()) { + String omrsPropertyName = property.getKey(); + InstancePropertyValue value = property.getValue(); + boolean added = addSearchConditionFromValue( + propertyCriteria, + omrsPropertyName, + value, + omrsPropertyMap, + omrsAttrTypeDefs, + (matchCriteria != null) && matchCriteria.equals(MatchCriteria.NONE), + true + ); + if (!added) { + if (matchCriteria == null || matchCriteria.equals(MatchCriteria.ALL)) { + // If we are asked to find everything, but one of the properties cannot be searched, + // then we should skip the search + skipSearch = true; + log.info("Skipping search ({}) -- ALL criteria should match, but this one is unmapped: {}", entityTypeGUID, omrsPropertyName); + } } } } - } - if (!propertyCriteria.isEmpty()) { - String propertyMatchDelim = " and "; - if (matchCriteria != null && matchCriteria.equals(MatchCriteria.ANY)) { - propertyMatchDelim = " or "; - } - if (!bWhereClauseAdded) { - sb.append(" where"); + if (!propertyCriteria.isEmpty()) { + String propertyMatchDelim = " and "; + if (matchCriteria != null && matchCriteria.equals(MatchCriteria.ANY)) { + propertyMatchDelim = " or "; + } + if (!bWhereClauseAdded) { + sb.append(" where"); + } + sb.append(" "); + sb.append(String.join(propertyMatchDelim, propertyCriteria)); } - sb.append(" "); - sb.append(String.join(propertyMatchDelim, propertyCriteria)); } - } - // Add status limiters, if requested - boolean unsupportedStatusRequested = false; - if (limitResultsByStatus != null) { - List states = new ArrayList<>(); - Set limitSet = new HashSet<>(limitResultsByStatus); - for (InstanceStatus requestedStatus : limitSet) { - switch (requestedStatus) { - case ACTIVE: - states.add("__state = 'ACTIVE'"); - break; - case DELETED: - states.add("__state = 'DELETED'"); - break; - default: - unsupportedStatusRequested = true; - break; + // Add status limiters, if requested + boolean unsupportedStatusRequested = false; + if (limitResultsByStatus != null) { + List states = new ArrayList<>(); + Set limitSet = new HashSet<>(limitResultsByStatus); + for (InstanceStatus requestedStatus : limitSet) { + switch (requestedStatus) { + case ACTIVE: + states.add("__state = 'ACTIVE'"); + break; + case DELETED: + states.add("__state = 'DELETED'"); + break; + default: + unsupportedStatusRequested = true; + break; + } } - } - if (!states.isEmpty()) { - if (!bWhereClauseAdded) { - sb.append(" where"); + if (!states.isEmpty()) { + if (!bWhereClauseAdded) { + sb.append(" where"); + } + sb.append(" "); + sb.append(String.join(" or ", states)); + } else if (unsupportedStatusRequested) { + // We are searching only for a state that Atlas does not support, so we should ensure no + // results are returned (in fact, skip searching entirely). + skipSearch = true; + log.info("Skipping search ({}) -- one or more unsupported statuses requested: {}", entityTypeGUID, limitSet); } - sb.append(" "); - sb.append(String.join(" or ", states)); - } else if (unsupportedStatusRequested) { - // We are searching only for a state that Atlas does not support, so we should ensure no - // results are returned (in fact, skip searching entirely). - skipSearch = true; - log.info("Skipping search ({}) -- one or more unsupported statuses requested: {}", entityTypeGUID, limitSet); } - } - if (!skipSearch) { - // Add sorting criteria, if requested - if (sequencingOrder != null) { - switch (sequencingOrder) { - case GUID: - sb.append(" orderby __guid asc"); - break; - case LAST_UPDATE_OLDEST: - sb.append(" orderby __modificationTimestamp asc"); - break; - case LAST_UPDATE_RECENT: - sb.append(" orderby __modificationTimestamp desc"); - break; - case CREATION_DATE_OLDEST: - sb.append(" orderby __timestamp asc"); - break; - case CREATION_DATE_RECENT: - sb.append(" orderby __timestamp desc"); - break; - case PROPERTY_ASCENDING: - if (sequencingProperty != null) { - String atlasPropertyName = omrsPropertyMap.get(sequencingProperty); - if (atlasPropertyName != null) { - sb.append(" orderby "); - sb.append(atlasPropertyName); - sb.append(" asc"); + if (!skipSearch) { + // Add sorting criteria, if requested + if (sequencingOrder != null) { + switch (sequencingOrder) { + case GUID: + sb.append(" orderby __guid asc"); + break; + case LAST_UPDATE_OLDEST: + sb.append(" orderby __modificationTimestamp asc"); + break; + case LAST_UPDATE_RECENT: + sb.append(" orderby __modificationTimestamp desc"); + break; + case CREATION_DATE_OLDEST: + sb.append(" orderby __timestamp asc"); + break; + case CREATION_DATE_RECENT: + sb.append(" orderby __timestamp desc"); + break; + case PROPERTY_ASCENDING: + if (sequencingProperty != null) { + String atlasPropertyName = omrsPropertyMap.get(sequencingProperty); + if (atlasPropertyName != null) { + sb.append(" orderby "); + sb.append(atlasPropertyName); + sb.append(" asc"); + } else { + log.warn("Unable to find mapped Atlas property for sorting for: {}", sequencingProperty); + sb.append(" orderby __guid asc"); + } } else { - log.warn("Unable to find mapped Atlas property for sorting for: {}", sequencingProperty); + log.warn("No property for sorting provided, defaulting to GUID."); sb.append(" orderby __guid asc"); } - } else { - log.warn("No property for sorting provided, defaulting to GUID."); - sb.append(" orderby __guid asc"); - } - break; - case PROPERTY_DESCENDING: - if (sequencingProperty != null) { - String atlasPropertyName = omrsPropertyMap.get(sequencingProperty); - if (atlasPropertyName != null) { - sb.append(" orderby "); - sb.append(atlasPropertyName); - sb.append(" desc"); + break; + case PROPERTY_DESCENDING: + if (sequencingProperty != null) { + String atlasPropertyName = omrsPropertyMap.get(sequencingProperty); + if (atlasPropertyName != null) { + sb.append(" orderby "); + sb.append(atlasPropertyName); + sb.append(" desc"); + } else { + log.warn("Unable to find mapped Atlas property for sorting for: {}", sequencingProperty); + sb.append(" orderby __guid asc"); + } } else { - log.warn("Unable to find mapped Atlas property for sorting for: {}", sequencingProperty); - sb.append(" orderby __guid asc"); + log.warn("No property for sorting provided, defaulting to GUID."); + sb.append(" orderby __guid desc"); } - } else { - log.warn("No property for sorting provided, defaulting to GUID."); - sb.append(" orderby __guid desc"); - } - break; - default: - // Do nothing -- no sorting - break; + break; + default: + // Do nothing -- no sorting + break; + } } - } - // Add paging criteria, if requested - if (pageSize > 0) { - sb.append(" limit "); - sb.append(pageSize); - } - // TODO: can we use fromEntityElement already here if there is a multi-type map? - if (fromEntityElement > 0) { - sb.append(" offset "); - sb.append(fromEntityElement); - } + // Add paging criteria, if requested + if (pageSize > 0) { + sb.append(" limit "); + sb.append(pageSize); + } + // TODO: can we use fromEntityElement already here if there is a multi-type map? + if (fromEntityElement > 0) { + sb.append(" offset "); + sb.append(fromEntityElement); + } - AtlasSearchResult results = null; - try { - results = atlasRepositoryConnector.searchWithDSL(sb.toString()); - } catch (AtlasServiceException e) { - raiseRepositoryErrorException(ApacheAtlasOMRSErrorCode.INVALID_SEARCH, methodName, e, sb.toString()); - } - if (results != null) { - totalResults.add(results); + AtlasSearchResult results = null; + try { + results = atlasRepositoryConnector.searchWithDSL(sb.toString()); + } catch (AtlasServiceException e) { + raiseRepositoryErrorException(ApacheAtlasOMRSErrorCode.INVALID_SEARCH, methodName, e, sb.toString()); + } + if (results != null) { + totalResults.add(results); + } } } @@ -1966,107 +1971,109 @@ private List buildAndRunBasicSearch(String methodName, RepositoryErrorException { // Run multiple searches, if there are multiple types mapped to the OMRS type... - Map> mappingsToSearch = getMappingsToSearch(entityTypeGUID, userId); + Map>> mappingsToSearch = getMappingsToSearch(entityTypeGUID, userId); List totalResults = new ArrayList<>(); - for (Map.Entry> entryToSearch : mappingsToSearch.entrySet()) { + for (Map.Entry>> entryToSearch : mappingsToSearch.entrySet()) { String omrsTypeName = entryToSearch.getKey(); - Map atlasTypeNamesByPrefix = entryToSearch.getValue(); + Map> atlasTypeNamesByPrefix = entryToSearch.getValue(); - for (Map.Entry entry : atlasTypeNamesByPrefix.entrySet()) { + for (Map.Entry> entry : atlasTypeNamesByPrefix.entrySet()) { String prefix = entry.getKey(); - String atlasTypeName = entry.getValue(); + Set atlasTypeNames = entry.getValue(); - // Otherwise Atlas's "basic" search is likely to be significantly faster - SearchParameters searchParameters = new SearchParameters(); - if (atlasTypeName != null) { - searchParameters.setTypeName(atlasTypeName); - } - searchParameters.setIncludeClassificationAttributes(true); - searchParameters.setIncludeSubClassifications(true); - searchParameters.setIncludeSubTypes(true); - // TODO: can we use fromEntityElement already here if there is a multi-type map? - searchParameters.setOffset(fromEntityElement); - searchParameters.setLimit(pageSize); - - Map omrsPropertyMap = typeDefStore.getPropertyMappingsForOMRSTypeDef(omrsTypeName, prefix); - Map omrsAttrTypeDefs = typeDefStore.getAllTypeDefAttributesForName(omrsTypeName); - List criteria = new ArrayList<>(); - - boolean skipSearch = false; - if (matchProperties != null) { - Map properties = matchProperties.getInstanceProperties(); - // By default, include only Referenceable's properties (as these will be the only properties that exist - // across ALL entity types) - if (properties != null) { - for (Map.Entry property : properties.entrySet()) { - String omrsPropertyName = property.getKey(); - InstancePropertyValue value = property.getValue(); - boolean added = addSearchConditionFromValue( - criteria, - omrsPropertyName, - value, - omrsPropertyMap, - omrsAttrTypeDefs, - (matchCriteria != null) && matchCriteria.equals(MatchCriteria.NONE), - false - ); - if (!added) { - if (matchCriteria == null || matchCriteria.equals(MatchCriteria.ALL)) { - // If we are asked to find everything, but one of the properties cannot be searched, - // then we should skip the search - skipSearch = true; - log.info("Skipping search ({}) -- ALL criteria should match, but this one is unmapped: {}", entityTypeGUID, omrsPropertyName); + for (String atlasTypeName : atlasTypeNames) { + // Otherwise Atlas's "basic" search is likely to be significantly faster + SearchParameters searchParameters = new SearchParameters(); + if (atlasTypeName != null) { + searchParameters.setTypeName(atlasTypeName); + } + searchParameters.setIncludeClassificationAttributes(true); + searchParameters.setIncludeSubClassifications(true); + searchParameters.setIncludeSubTypes(true); + // TODO: can we use fromEntityElement already here if there is a multi-type map? + searchParameters.setOffset(fromEntityElement); + searchParameters.setLimit(pageSize); + + Map omrsPropertyMap = typeDefStore.getPropertyMappingsForOMRSTypeDef(omrsTypeName, prefix); + Map omrsAttrTypeDefs = typeDefStore.getAllTypeDefAttributesForName(omrsTypeName); + List criteria = new ArrayList<>(); + + boolean skipSearch = false; + if (matchProperties != null) { + Map properties = matchProperties.getInstanceProperties(); + // By default, include only Referenceable's properties (as these will be the only properties that exist + // across ALL entity types) + if (properties != null) { + for (Map.Entry property : properties.entrySet()) { + String omrsPropertyName = property.getKey(); + InstancePropertyValue value = property.getValue(); + boolean added = addSearchConditionFromValue( + criteria, + omrsPropertyName, + value, + omrsPropertyMap, + omrsAttrTypeDefs, + (matchCriteria != null) && matchCriteria.equals(MatchCriteria.NONE), + false + ); + if (!added) { + if (matchCriteria == null || matchCriteria.equals(MatchCriteria.ALL)) { + // If we are asked to find everything, but one of the properties cannot be searched, + // then we should skip the search + skipSearch = true; + log.info("Skipping search ({}) -- ALL criteria should match, but this one is unmapped: {}", entityTypeGUID, omrsPropertyName); + } } } } - } - } else if (fullTextQuery != null) { - - // Note that while it would be great to use the 'setQuery' of Atlas for this, it unfortunately does - // not work for all kinds of cases where things like '.' or '/' are involved (which even appear in - // Atlas's own sample metadata for properties like qualifiedName). Therefore we must do an OR-based - // search explicitly across all string-based properties. - - // Setup a new PrimitivePropertyValue for the full text itself, that we can use for all of the - // various string attributes - PrimitivePropertyValue primitivePropertyValue = new PrimitivePropertyValue(); - primitivePropertyValue.setPrimitiveDefCategory(PrimitiveDefCategory.OM_PRIMITIVE_TYPE_STRING); - primitivePropertyValue.setPrimitiveValue(fullTextQuery); - primitivePropertyValue.setTypeName(PrimitiveDefCategory.OM_PRIMITIVE_TYPE_STRING.getName()); - primitivePropertyValue.setTypeGUID(PrimitiveDefCategory.OM_PRIMITIVE_TYPE_STRING.getGUID()); - - // Iterate through all of the string attributes, and for any that are actually mapped to OMRS - // add a search condition for them - for (Map.Entry mapEntry : omrsAttrTypeDefs.entrySet()) { - String omrsPropertyName = mapEntry.getKey(); - TypeDefAttribute typeDefAttribute = mapEntry.getValue(); - log.debug("Considering attribute: {}", omrsPropertyName); - AttributeTypeDef attributeTypeDef = typeDefAttribute.getAttributeType(); - if (attributeTypeDef.getCategory().equals(AttributeTypeDefCategory.PRIMITIVE)) { - PrimitiveDef primitiveDef = (PrimitiveDef) attributeTypeDef; - if (primitiveDef.getPrimitiveDefCategory().equals(PrimitiveDefCategory.OM_PRIMITIVE_TYPE_STRING)) { - log.debug(" ... attribute is a String, continuing ..."); - if (omrsPropertyMap.containsKey(omrsPropertyName)) { - String atlasPropertyName = omrsPropertyMap.get(omrsPropertyName); - log.debug(" ... attribute is mapped, to: {}", atlasPropertyName); - if (atlasPropertyName != null) { - log.debug(" ... adding criterion for value: {}", primitivePropertyValue); - boolean added = addSearchConditionFromValue( - criteria, - omrsPropertyName, - primitivePropertyValue, - omrsPropertyMap, - omrsAttrTypeDefs, - (matchCriteria != null) && matchCriteria.equals(MatchCriteria.NONE), - false - ); - if (!added) { - if (matchCriteria == null || matchCriteria.equals(MatchCriteria.ALL)) { - // If we are asked to find everything, but one of the properties cannot be searched, - // then we should skip the search - skipSearch = true; - log.info("Skipping search ({}) -- ALL criteria should match, but this one is unmapped: {}", entityTypeGUID, omrsPropertyName); + } else if (fullTextQuery != null) { + + // Note that while it would be great to use the 'setQuery' of Atlas for this, it unfortunately does + // not work for all kinds of cases where things like '.' or '/' are involved (which even appear in + // Atlas's own sample metadata for properties like qualifiedName). Therefore we must do an OR-based + // search explicitly across all string-based properties. + + // Setup a new PrimitivePropertyValue for the full text itself, that we can use for all of the + // various string attributes + PrimitivePropertyValue primitivePropertyValue = new PrimitivePropertyValue(); + primitivePropertyValue.setPrimitiveDefCategory(PrimitiveDefCategory.OM_PRIMITIVE_TYPE_STRING); + primitivePropertyValue.setPrimitiveValue(fullTextQuery); + primitivePropertyValue.setTypeName(PrimitiveDefCategory.OM_PRIMITIVE_TYPE_STRING.getName()); + primitivePropertyValue.setTypeGUID(PrimitiveDefCategory.OM_PRIMITIVE_TYPE_STRING.getGUID()); + + // Iterate through all of the string attributes, and for any that are actually mapped to OMRS + // add a search condition for them + for (Map.Entry mapEntry : omrsAttrTypeDefs.entrySet()) { + String omrsPropertyName = mapEntry.getKey(); + TypeDefAttribute typeDefAttribute = mapEntry.getValue(); + log.debug("Considering attribute: {}", omrsPropertyName); + AttributeTypeDef attributeTypeDef = typeDefAttribute.getAttributeType(); + if (attributeTypeDef.getCategory().equals(AttributeTypeDefCategory.PRIMITIVE)) { + PrimitiveDef primitiveDef = (PrimitiveDef) attributeTypeDef; + if (primitiveDef.getPrimitiveDefCategory().equals(PrimitiveDefCategory.OM_PRIMITIVE_TYPE_STRING)) { + log.debug(" ... attribute is a String, continuing ..."); + if (omrsPropertyMap.containsKey(omrsPropertyName)) { + String atlasPropertyName = omrsPropertyMap.get(omrsPropertyName); + log.debug(" ... attribute is mapped, to: {}", atlasPropertyName); + if (atlasPropertyName != null) { + log.debug(" ... adding criterion for value: {}", primitivePropertyValue); + boolean added = addSearchConditionFromValue( + criteria, + omrsPropertyName, + primitivePropertyValue, + omrsPropertyMap, + omrsAttrTypeDefs, + (matchCriteria != null) && matchCriteria.equals(MatchCriteria.NONE), + false + ); + if (!added) { + if (matchCriteria == null || matchCriteria.equals(MatchCriteria.ALL)) { + // If we are asked to find everything, but one of the properties cannot be searched, + // then we should skip the search + skipSearch = true; + log.info("Skipping search ({}) -- ALL criteria should match, but this one is unmapped: {}", entityTypeGUID, omrsPropertyName); + } } } } @@ -2074,64 +2081,65 @@ private List buildAndRunBasicSearch(String methodName, } } } - } - SearchParameters.FilterCriteria entityFilters = new SearchParameters.FilterCriteria(); - if (criteria.size() > 1) { - entityFilters.setCriterion(criteria); - if (matchCriteria != null) { - // If matchCriteria were provided, use them - switch (matchCriteria) { - case ALL: - case NONE: - entityFilters.setCondition(SearchParameters.FilterCriteria.Condition.AND); - break; - case ANY: - entityFilters.setCondition(SearchParameters.FilterCriteria.Condition.OR); - break; + SearchParameters.FilterCriteria entityFilters = new SearchParameters.FilterCriteria(); + if (criteria.size() > 1) { + entityFilters.setCriterion(criteria); + if (matchCriteria != null) { + // If matchCriteria were provided, use them + switch (matchCriteria) { + case ALL: + case NONE: + entityFilters.setCondition(SearchParameters.FilterCriteria.Condition.AND); + break; + case ANY: + entityFilters.setCondition(SearchParameters.FilterCriteria.Condition.OR); + break; + } + } else if (fullTextQuery != null) { + // If none were provided, but a fullTextQuery was, we should use an OR-based semantic + entityFilters.setCondition(SearchParameters.FilterCriteria.Condition.OR); + } else { + // Otherwise we should default to an AND-based semantic + entityFilters.setCondition(SearchParameters.FilterCriteria.Condition.AND); } - } else if (fullTextQuery != null) { - // If none were provided, but a fullTextQuery was, we should use an OR-based semantic - entityFilters.setCondition(SearchParameters.FilterCriteria.Condition.OR); - } else { - // Otherwise we should default to an AND-based semantic - entityFilters.setCondition(SearchParameters.FilterCriteria.Condition.AND); + } else if (criteria.size() == 1) { + entityFilters = criteria.get(0); } - } else if (criteria.size() == 1) { - entityFilters = criteria.get(0); - } - searchParameters.setEntityFilters(entityFilters); - - if (limitResultsByStatus != null) { - Set limitSet = new HashSet<>(limitResultsByStatus); - if (limitSet.equals(availableStates) || (limitSet.size() == 1 && limitSet.contains(InstanceStatus.DELETED))) { - // If we're to search for deleted, do not exclude deleted - searchParameters.setExcludeDeletedEntities(false); - } else if (limitSet.size() == 1 && limitSet.contains(InstanceStatus.ACTIVE)) { - // Otherwise if we are only after active, do exclude deleted - searchParameters.setExcludeDeletedEntities(true); - } else if (!limitSet.isEmpty()) { - // Otherwise we must be searching only for states that Atlas does not support, so we should ensure - // that no results are returned (by skipping the search entirely). - skipSearch = true; - log.info("Skipping search ({}) -- searching for unsupported states: {}", entityTypeGUID, limitSet); + searchParameters.setEntityFilters(entityFilters); + + if (limitResultsByStatus != null) { + Set limitSet = new HashSet<>(limitResultsByStatus); + if (limitSet.equals(availableStates) || (limitSet.size() == 1 && limitSet.contains(InstanceStatus.DELETED))) { + // If we're to search for deleted, do not exclude deleted + searchParameters.setExcludeDeletedEntities(false); + } else if (limitSet.size() == 1 && limitSet.contains(InstanceStatus.ACTIVE)) { + // Otherwise if we are only after active, do exclude deleted + searchParameters.setExcludeDeletedEntities(true); + } else if (!limitSet.isEmpty()) { + // Otherwise we must be searching only for states that Atlas does not support, so we should ensure + // that no results are returned (by skipping the search entirely). + skipSearch = true; + log.info("Skipping search ({}) -- searching for unsupported states: {}", entityTypeGUID, limitSet); + } } - } - if (!skipSearch) { + if (!skipSearch) { - if (limitResultsByClassification != null) { - searchParameters.setClassification(limitResultsByClassification); - } + if (limitResultsByClassification != null) { + searchParameters.setClassification(limitResultsByClassification); + } + + AtlasSearchResult results = null; + try { + results = atlasRepositoryConnector.searchForEntities(searchParameters); + } catch (AtlasServiceException e) { + raiseRepositoryErrorException(ApacheAtlasOMRSErrorCode.INVALID_SEARCH, methodName, e, searchParameters.toString()); + } + if (results != null) { + totalResults.add(results); + } - AtlasSearchResult results = null; - try { - results = atlasRepositoryConnector.searchForEntities(searchParameters); - } catch (AtlasServiceException e) { - raiseRepositoryErrorException(ApacheAtlasOMRSErrorCode.INVALID_SEARCH, methodName, e, searchParameters.toString()); - } - if (results != null) { - totalResults.add(results); } } @@ -2240,28 +2248,40 @@ private List getEntityDetailsFromAtlasResults(List entityDetails = new ArrayList<>(); + // If requested type is for a generated type, we need to retrieve it differently -- i.e. prefix the GUID + // ... so we should determine what prefixes to use based on the requested entity type GUID + Set prefixes = new HashSet<>(); + Map>> mappings = getMappingsToSearch(entityTypeGUID, userId); + for (Map> atlasTypeNamesByPrefix : mappings.values()) { + prefixes.addAll(atlasTypeNamesByPrefix.keySet()); + } + if (atlasEntities != null) { for (AtlasEntityHeader atlasEntityHeader : atlasEntities) { - try { - EntityDetail detail = getEntityDetail(userId, atlasEntityHeader.getGuid()); - // Depending on prefix, this could come back with results that should not be included - // (ie. for generated types or non-generated types, depending on requested entityTypeGUID), - // so only include those that were requested - if (detail != null) { - String typeName = detail.getType().getTypeDefName(); - try { - TypeDef typeDef = repositoryHelper.getTypeDef(repositoryName, "entityTypeGUID", entityTypeGUID, "getEntityDetailsFromAtlasResults"); - if (repositoryHelper.isTypeOf(repositoryName, typeName, typeDef.getName())) { - entityDetails.add(detail); + for (String prefix : prefixes) { + try { + AtlasGuid guid = new AtlasGuid(atlasEntityHeader.getGuid(), prefix); + // ... and retrieve the entity itself only based on those types (prefixes) that have been requested + EntityDetail detail = getEntityDetail(userId, guid.toString()); + // Depending on prefix, this could come back with results that should not be included + // (ie. for generated types or non-generated types, depending on requested entityTypeGUID), + // so only include those that were requested + if (detail != null) { + String typeName = detail.getType().getTypeDefName(); + try { + TypeDef typeDef = repositoryHelper.getTypeDef(repositoryName, "entityTypeGUID", entityTypeGUID, "getEntityDetailsFromAtlasResults"); + if (repositoryHelper.isTypeOf(repositoryName, typeName, typeDef.getName())) { + entityDetails.add(detail); + } + } catch (TypeErrorException e) { + log.error("Unable to find any TypeDef for entityTypeGUID: {}", entityTypeGUID); } - } catch (TypeErrorException e) { - log.error("Unable to find any TypeDef for entityTypeGUID: {}", entityTypeGUID); + } else { + log.error("Entity with GUID {} not mapped -- excluding from results.", atlasEntityHeader.getGuid()); } - } else { - log.error("Entity with GUID {} not mapped -- excluding from results.", atlasEntityHeader.getGuid()); + } catch (EntityNotKnownException e) { + log.error("Entity with GUID {} not known -- excluding from results.", atlasEntityHeader.getGuid()); } - } catch (EntityNotKnownException e) { - log.error("Entity with GUID {} not known -- excluding from results.", atlasEntityHeader.getGuid()); } } } diff --git a/apache-atlas-adapter/src/main/java/org/odpi/egeria/connectors/apache/atlas/repositoryconnector/mapping/ClassificationDefMapping.java b/apache-atlas-adapter/src/main/java/org/odpi/egeria/connectors/apache/atlas/repositoryconnector/mapping/ClassificationDefMapping.java index c39bce4..b9078cd 100644 --- a/apache-atlas-adapter/src/main/java/org/odpi/egeria/connectors/apache/atlas/repositoryconnector/mapping/ClassificationDefMapping.java +++ b/apache-atlas-adapter/src/main/java/org/odpi/egeria/connectors/apache/atlas/repositoryconnector/mapping/ClassificationDefMapping.java @@ -128,9 +128,9 @@ private static AtlasTypesDef setupClassificationType(ClassificationDef omrsClass for (TypeDefLink typeDefLink : validEntities) { String omrsEntityName = typeDefLink.getName(); // TODO: assumes all classifications remain one-to-one (never generated, so never a prefix) - String atlasEntityName = typeDefStore.getMappedAtlasTypeDefName(omrsEntityName, null); + Set atlasEntityName = typeDefStore.getMappedAtlasTypeDefNames(omrsEntityName, null); if (atlasEntityName != null) { - entitiesForAtlas.add(atlasEntityName); + entitiesForAtlas.addAll(atlasEntityName); } } if (entitiesForAtlas.isEmpty()) { diff --git a/apache-atlas-adapter/src/main/java/org/odpi/egeria/connectors/apache/atlas/repositoryconnector/mapping/EntityMappingAtlas2OMRS.java b/apache-atlas-adapter/src/main/java/org/odpi/egeria/connectors/apache/atlas/repositoryconnector/mapping/EntityMappingAtlas2OMRS.java index 75fae20..bf948c5 100644 --- a/apache-atlas-adapter/src/main/java/org/odpi/egeria/connectors/apache/atlas/repositoryconnector/mapping/EntityMappingAtlas2OMRS.java +++ b/apache-atlas-adapter/src/main/java/org/odpi/egeria/connectors/apache/atlas/repositoryconnector/mapping/EntityMappingAtlas2OMRS.java @@ -300,13 +300,13 @@ public List getRelationships(String relationshipTypeGUID, TypeDef typeDef = typeDefStore.getTypeDefByGUID(relationshipTypeGUID); if (typeDef != null) { String omrsTypeDefName = typeDef.getName(); - Map atlasTypesByPrefix = typeDefStore.getAllMappedAtlasTypeDefNames(omrsTypeDefName); - for (Map.Entry entry : atlasTypesByPrefix.entrySet()) { + Map> atlasTypesByPrefix = typeDefStore.getAllMappedAtlasTypeDefNames(omrsTypeDefName); + for (Map.Entry> entry : atlasTypesByPrefix.entrySet()) { String prefixForType = entry.getKey(); - String atlasTypeName = entry.getValue(); + Set atlasTypeNames = entry.getValue(); // TODO: Only generate the generated relationships (normally-mapped should be covered already above) if (prefixForType != null) { - log.info("Have not yet implemented this relationship: ({}) {}", prefixForType, atlasTypeName); + log.info("Have not yet implemented this relationship: ({}) {}", prefixForType, atlasTypeNames); } } } @@ -551,6 +551,13 @@ private final class EntityMapping { public String getAtlasTypeName() { return atlasTypeName; } public String getOmrsTypeName() { return omrsTypeName; } + @Override + public String toString() { + return "EntityMapping{" + + "atlasTypeName='" + atlasTypeName + '\'' + + ", omrsTypeName='" + omrsTypeName + '\'' + + '}'; + } } /** diff --git a/apache-atlas-adapter/src/main/java/org/odpi/egeria/connectors/apache/atlas/repositoryconnector/mapping/RelationshipDefMapping.java b/apache-atlas-adapter/src/main/java/org/odpi/egeria/connectors/apache/atlas/repositoryconnector/mapping/RelationshipDefMapping.java index c0ff202..f10824f 100644 --- a/apache-atlas-adapter/src/main/java/org/odpi/egeria/connectors/apache/atlas/repositoryconnector/mapping/RelationshipDefMapping.java +++ b/apache-atlas-adapter/src/main/java/org/odpi/egeria/connectors/apache/atlas/repositoryconnector/mapping/RelationshipDefMapping.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Set; /** * Class that generically handles converting between Apache Atlas and OMRS Relationship TypeDefs. @@ -196,8 +197,22 @@ private static boolean setupRelationshipEnds(RelationshipDef omrsRelationshipDef // TODO: at the moment this method is only used when adding new relationship types to Atlas, // so this assumes that we will not use generated objects / prefixes for these net-new relationship types - String atlasTypeName1 = typeDefStore.getMappedAtlasTypeDefName(omrsTypeName1, null); - String atlasTypeName2 = typeDefStore.getMappedAtlasTypeDefName(omrsTypeName2, null); + Set atlasTypeNames1 = typeDefStore.getMappedAtlasTypeDefNames(omrsTypeName1, null); + if (atlasTypeNames1.size() > 1) { + log.warn("Found multiple mapped types, when expected only one: {}", omrsTypeName1); + } + String atlasTypeName1 = null; + for (String candidate : atlasTypeNames1) { + atlasTypeName1 = candidate; + } + Set atlasTypeNames2 = typeDefStore.getMappedAtlasTypeDefNames(omrsTypeName2, null); + if (atlasTypeNames2.size() > 1) { + log.warn("Found multiple mapped types, when expected only one: {}", omrsTypeName2); + } + String atlasTypeName2 = null; + for (String candidate : atlasTypeNames2) { + atlasTypeName2 = candidate; + } // These look inverted - see comment above for rationale String attrNameForAtlas1 = omrs2.getAttributeName(); diff --git a/apache-atlas-adapter/src/main/java/org/odpi/egeria/connectors/apache/atlas/repositoryconnector/stores/TypeDefStore.java b/apache-atlas-adapter/src/main/java/org/odpi/egeria/connectors/apache/atlas/repositoryconnector/stores/TypeDefStore.java index 699868e..7cca429 100644 --- a/apache-atlas-adapter/src/main/java/org/odpi/egeria/connectors/apache/atlas/repositoryconnector/stores/TypeDefStore.java +++ b/apache-atlas-adapter/src/main/java/org/odpi/egeria/connectors/apache/atlas/repositoryconnector/stores/TypeDefStore.java @@ -29,7 +29,7 @@ public class TypeDefStore { // Mapping details private Map prefixToOmrsTypeName; - private Map> omrsNameToAtlasNamesByPrefix; + private Map>> omrsNameToAtlasNamesByPrefix; private Map> atlasNameToOmrsNamesByPrefix; private Map>> omrsNameToAttributeMapByPrefix; private Map>> atlasNameToAttributeMapByPrefix; @@ -92,7 +92,10 @@ private void loadMappings() { if (!omrsNameToAtlasNamesByPrefix.containsKey(omrsName)) { omrsNameToAtlasNamesByPrefix.put(omrsName, new HashMap<>()); } - omrsNameToAtlasNamesByPrefix.get(omrsName).put(prefix, atlasName); + if (!omrsNameToAtlasNamesByPrefix.get(omrsName).containsKey(prefix)) { + omrsNameToAtlasNamesByPrefix.get(omrsName).put(prefix, new HashSet<>()); + } + omrsNameToAtlasNamesByPrefix.get(omrsName).get(prefix).add(atlasName); if (!atlasNameToOmrsNamesByPrefix.containsKey(atlasName)) { atlasNameToOmrsNamesByPrefix.put(atlasName, new HashMap<>()); } @@ -279,14 +282,16 @@ public Map getAllEndpointMappingsFromAtlasName(String a * name for that prefix. * * @param omrsName the name of the OMRS TypeDef - * @return {@code Map} + * @return {@code Map>} */ - public Map getAllMappedAtlasTypeDefNames(String omrsName) { + public Map> getAllMappedAtlasTypeDefNames(String omrsName) { if (isTypeDefMapped(omrsName)) { return omrsNameToAtlasNamesByPrefix.get(omrsName); } else if (omrsNameToGuid.containsKey(omrsName)) { - Map map = new HashMap<>(); - map.put(null, omrsName); + Map> map = new HashMap<>(); + Set set = new HashSet<>(); + set.add(omrsName); + map.put(null, set); return map; } else { return Collections.emptyMap(); @@ -299,15 +304,17 @@ public Map getAllMappedAtlasTypeDefNames(String omrsName) { * * @param omrsName the name of the OMRS TypeDef * @param prefix the prefix (if any) when mappings to multiple types exist - * @return String + * @return {@code String} */ - public String getMappedAtlasTypeDefName(String omrsName, String prefix) { + public Set getMappedAtlasTypeDefNames(String omrsName, String prefix) { if (isTypeDefMapped(omrsName)) { - return omrsNameToAtlasNamesByPrefix.get(omrsName).getOrDefault(prefix, null); + return omrsNameToAtlasNamesByPrefix.get(omrsName).getOrDefault(prefix, Collections.emptySet()); } else if (omrsNameToGuid.containsKey(omrsName)) { - return omrsName; + Set set = new HashSet<>(); + set.add(omrsName); + return set; } else { - return null; + return Collections.emptySet(); } } diff --git a/apache-atlas-adapter/src/main/resources/TypeDefMappings.json b/apache-atlas-adapter/src/main/resources/TypeDefMappings.json index c0e69c5..3fa03ab 100644 --- a/apache-atlas-adapter/src/main/resources/TypeDefMappings.json +++ b/apache-atlas-adapter/src/main/resources/TypeDefMappings.json @@ -80,7 +80,7 @@ }, { "atlas": "hbase_namespace", - "omrs": "DataStore", + "omrs": "Database", "propertyMappings": [ { "atlas": "qualifiedName", @@ -180,8 +180,9 @@ ] }, { - "atlas": "hbase_table", - "omrs": "DataSet", + "atlas": "hbase_namespace", + "omrs": "DeployedDatabaseSchema", + "prefix": "HBNDDS", "propertyMappings": [ { "atlas": "qualifiedName", @@ -194,39 +195,107 @@ { "atlas": "description", "omrs": "description" + } + ] + }, + { + "atlas": "hbase_namespace", + "omrs": "RelationalDBSchemaType", + "prefix": "HBNRDBST", + "propertyMappings": [ + { + "atlas": "qualifiedName", + "omrs": "qualifiedName" + }, + { + "atlas": "name", + "omrs": "displayName" }, { "atlas": "owner", - "omrs": "owner" + "omrs": "author" + }, + { + "atlas": "typeName", + "omrs": "encodingStandard" + }, + { + "atlas": "description", + "omrs": "description" } ] }, { - "atlas": "hbase_table_namespace", + "atlas": "hbase_namespace", "omrs": "DataContentForDataSet", + "prefix": "HBNDCDS", "endpointMappings": [ { - "atlas": "namespace", "omrs": "dataContent" }, { - "atlas": "tables", - "omrs": "supportedDataSets" + "omrs": "supportedDataSets", + "prefix": "HBNDDS" } ] }, { - "omrs": "SchemaElement" + "atlas": "hbase_namespace", + "omrs": "AssetSchemaType", + "prefix": "HBNAST", + "endpointMappings": [ + { + "omrs": "describesAssets", + "prefix": "HBNDDS" + }, + { + "omrs": "schema", + "prefix": "HBNRDBST" + } + ] }, { - "omrs": "SchemaType" + "atlas": "hbase_table_namespace", + "omrs": "AttributeForSchema", + "invertedEndpoints": true, + "endpointMappings": [ + { + "omrs": "parentSchemas", + "atlas": "namespace", + "prefix": "HBNRDBST" + }, + { + "omrs": "attributes", + "atlas": "tables" + } + ] }, { - "omrs": "ComplexSchemaType" + "atlas": "hbase_table", + "omrs": "RelationalTable", + "propertyMappings": [ + { + "atlas": "qualifiedName", + "omrs": "qualifiedName" + }, + { + "atlas": "name", + "omrs": "name" + }, + { + "atlas": "description", + "omrs": "description" + }, + { + "atlas": "owner", + "omrs": "owner" + } + ] }, { - "atlas": "hbase_column_family", - "omrs": "TabularSchemaType", + "atlas": "hbase_table", + "omrs": "RelationalTableType", + "prefix": "HBRTT", "propertyMappings": [ { "atlas": "qualifiedName", @@ -234,38 +303,50 @@ }, { "atlas": "name", - "omrs": "displayName" + "omrs": "name" }, { - "atlas": "owner", - "omrs": "author" + "atlas": "description", + "omrs": "description" }, { - "atlas": "dataBlockEncoding", - "omrs": "encodingStandard" + "atlas": "owner", + "omrs": "owner" } ] }, { - "atlas": "hbase_table_column_families", - "omrs": "AssetSchemaType", + "atlas": "hbase_table", + "omrs": "SchemaAttributeType", + "prefix": "HBTSAT", "endpointMappings": [ { - "atlas": "table", - "omrs": "describesAssets" + "omrs": "usedInSchemas" }, { - "atlas": "column_families", - "omrs": "schema" + "omrs": "type", + "prefix": "HBRTT" } ] }, { - "omrs": "SchemaAttribute" + "atlas": "hbase_column_family", + "omrs": "RelationalColumn", + "propertyMappings": [ + { + "atlas": "qualifiedName", + "omrs": "qualifiedName" + }, + { + "atlas": "name", + "omrs": "displayName" + } + ] }, { - "atlas": "hbase_column", - "omrs": "TabularColumn", + "atlas": "hbase_column_family", + "omrs": "RelationalColumnType", + "prefix": "HCFRCT", "propertyMappings": [ { "atlas": "qualifiedName", @@ -276,25 +357,56 @@ "omrs": "displayName" }, { - "atlas": "description", - "omrs": "description" + "atlas": "owner", + "omrs": "author" + }, + { + "atlas": "dataBlockEncoding", + "omrs": "encodingStandard" } ] }, { - "atlas": "hbase_column_family_columns", + "atlas": "hbase_column_family", + "omrs": "SchemaAttributeType", + "prefix": "HCFSAT", + "endpointMappings": [ + { + "omrs": "usedInSchemas" + }, + { + "omrs": "type", + "prefix": "HCFRCT" + } + ] + }, + { + "atlas": "hbase_table_column_families", "omrs": "AttributeForSchema", "endpointMappings": [ { - "atlas": "column_family", - "omrs": "parentSchemas" + "omrs": "parentSchemas", + "atlas": "table", + "prefix": "HBRTT" }, { - "atlas": "columns", - "omrs": "attributes" + "omrs": "attributes", + "atlas": "columns" } ] }, + { + "omrs": "SchemaElement" + }, + { + "omrs": "SchemaType" + }, + { + "omrs": "ComplexSchemaType" + }, + { + "omrs": "SchemaAttribute" + }, { "atlas": "hdfs_path", "omrs": "FileFolder", @@ -432,28 +544,16 @@ }, { "atlas": "hive_db", - "omrs": "RelationalDBSchemaType", - "prefix": "RDBST", + "omrs": "DeployedDatabaseSchema", + "prefix": "HDBDDS", "propertyMappings": [ { "atlas": "qualifiedName", "omrs": "qualifiedName" }, - { - "atlas": "location", - "omrs": "namespace" - }, { "atlas": "name", - "omrs": "displayName" - }, - { - "atlas": "owner", - "omrs": "author" - }, - { - "atlas": "typeName", - "omrs": "encodingStandard" + "omrs": "name" }, { "atlas": "description", @@ -463,16 +563,28 @@ }, { "atlas": "hive_db", - "omrs": "DeployedDatabaseSchema", - "prefix": "DDS", + "omrs": "RelationalDBSchemaType", + "prefix": "HDBRDBST", "propertyMappings": [ { "atlas": "qualifiedName", "omrs": "qualifiedName" }, + { + "atlas": "location", + "omrs": "namespace" + }, { "atlas": "name", - "omrs": "name" + "omrs": "displayName" + }, + { + "atlas": "owner", + "omrs": "author" + }, + { + "atlas": "typeName", + "omrs": "encodingStandard" }, { "atlas": "description", @@ -490,7 +602,7 @@ }, { "omrs": "supportedDataSets", - "prefix": "DDS" + "prefix": "HDBDDS" } ] }, @@ -501,11 +613,11 @@ "endpointMappings": [ { "omrs": "describesAssets", - "prefix": "DDS" + "prefix": "HDBDDS" }, { "omrs": "schema", - "prefix": "RDBST" + "prefix": "HDBRDBST" } ] }, @@ -517,7 +629,7 @@ { "omrs": "parentSchemas", "atlas": "db", - "prefix": "RDBST" + "prefix": "HDBRDBST" }, { "omrs": "attributes", @@ -528,7 +640,7 @@ { "atlas": "hive_table", "omrs": "RelationalTableType", - "prefix": "RTT", + "prefix": "HRTT", "propertyMappings": [ { "atlas": "qualifiedName", @@ -574,6 +686,20 @@ } ] }, + { + "atlas": "hive_table", + "omrs": "SchemaAttributeType", + "prefix": "HTSAT", + "endpointMappings": [ + { + "omrs": "usedInSchemas" + }, + { + "omrs": "type", + "prefix": "HRTT" + } + ] + }, { "atlas": "hive_column", "omrs": "RelationalColumn", @@ -649,7 +775,7 @@ { "omrs": "parentSchemas", "atlas": "table", - "prefix": "RTT" + "prefix": "HRTT" }, { "omrs": "attributes", diff --git a/apache-atlas-adapter/src/test/java/org/odpi/egeria/connectors/apache/atlas/mocks/MockServerExpectations.java b/apache-atlas-adapter/src/test/java/org/odpi/egeria/connectors/apache/atlas/mocks/MockServerExpectations.java index 994ee54..d46c59b 100644 --- a/apache-atlas-adapter/src/test/java/org/odpi/egeria/connectors/apache/atlas/mocks/MockServerExpectations.java +++ b/apache-atlas-adapter/src/test/java/org/odpi/egeria/connectors/apache/atlas/mocks/MockServerExpectations.java @@ -196,10 +196,8 @@ private void setSearchByProperty(MockServerClient mockServerClient) { .respond(withResponse(getResourceFileContents("by_case" + File.separator + caseName + File.separator + "results_ANY.json"))); mockServerClient .when(basicSearchRequest( - json( - "{\"typeName\":\"hbase_column_family\",\"excludeDeletedEntities\":false,\"includeClassificationAttributes\":true}", - MatchType.ONLY_MATCHING_FIELDS - ))) + "{\"typeName\":\"hbase_column_family\",\"excludeDeletedEntities\":false,\"includeClassificationAttributes\":true,\"includeSubTypes\":true,\"includeSubClassifications\":true,\"limit\":100,\"offset\":0,\"entityFilters\":{}}" + )) .respond(withResponse(getResourceFileContents("by_case" + File.separator + caseName + File.separator + "results_all.json"))); mockServerClient .when(basicSearchRequest( diff --git a/apache-atlas-adapter/src/test/java/org/odpi/egeria/connectors/apache/atlas/repositoryconnector/ConnectorTest.java b/apache-atlas-adapter/src/test/java/org/odpi/egeria/connectors/apache/atlas/repositoryconnector/ConnectorTest.java index 5f963bd..1c2f6cb 100644 --- a/apache-atlas-adapter/src/test/java/org/odpi/egeria/connectors/apache/atlas/repositoryconnector/ConnectorTest.java +++ b/apache-atlas-adapter/src/test/java/org/odpi/egeria/connectors/apache/atlas/repositoryconnector/ConnectorTest.java @@ -284,7 +284,7 @@ public void testTypeDefStore() { TypeDefStore.EndpointMapping endpointMapping = typeDefStore.getEndpointMappingFromAtlasName(nonExistentType, null); assertNull(endpointMapping); - Map map = typeDefStore.getAllMappedAtlasTypeDefNames(nonExistentType); + Map> map = typeDefStore.getAllMappedAtlasTypeDefNames(nonExistentType); assertTrue(map.isEmpty()); map = typeDefStore.getAllMappedAtlasTypeDefNames(classification); assertNotNull(map); @@ -295,10 +295,10 @@ public void testTypeDefStore() { name = typeDefStore.getMappedOMRSTypeDefName(classification, null); assertEquals(name, classification); - map = typeDefStore.getMappedOMRSTypeDefNameWithPrefixes(nonExistentType); - assertTrue(map.isEmpty()); - map = typeDefStore.getMappedOMRSTypeDefNameWithPrefixes(classification); - assertFalse(map.isEmpty()); + Map map2 = typeDefStore.getMappedOMRSTypeDefNameWithPrefixes(nonExistentType); + assertTrue(map2.isEmpty()); + map2 = typeDefStore.getMappedOMRSTypeDefNameWithPrefixes(classification); + assertFalse(map2.isEmpty()); assertNull(typeDefStore.getAllTypeDefAttributesForName(nonExistentType)); @@ -648,8 +648,8 @@ public void testSearchByProperty() { final String methodName = "testSearchByProperty"; - String typeGUID = "248975ec-8019-4b8a-9caf-084c8b724233"; - String typeName = "TabularSchemaType"; + String typeGUID = "f0438d80-6eb9-4fac-bcc1-5efee5babcfc"; + String typeName = "RelationalColumnType"; InstanceProperties ip = new InstanceProperties(); @@ -697,7 +697,7 @@ public void testSearchByProperty() { ip = repositoryHelper.addStringPropertyToInstance(sourceName, ip, "qualifiedName", repositoryHelper.getExactMatchRegex("default:atlas_janus@Sandbox"), methodName); testFindEntitiesByProperty( null, - "DataSet", + "RelationalTable", ip, MatchCriteria.ALL, 5, @@ -721,8 +721,8 @@ public void testSearchByPropertySorting() { final String methodName = "testSearchByPropertySorting"; - String typeGUID = "248975ec-8019-4b8a-9caf-084c8b724233"; - String typeName = "TabularSchemaType"; + String typeGUID = "aa8d5470-6dbc-4648-9e2f-045e5df9d2f9"; + String typeName = "RelationalColumn"; InstanceProperties ip = new InstanceProperties(); @@ -878,9 +878,9 @@ public void testSearchByPropertySorting() { @Test public void testSearchByPropertyValue() { - String typeGUID = "248975ec-8019-4b8a-9caf-084c8b724233"; + String typeGUID = "f0438d80-6eb9-4fac-bcc1-5efee5babcfc"; Set possibleTypes = new HashSet<>(); - possibleTypes.add("TabularSchemaType"); + possibleTypes.add("RelationalColumnType"); // Search by detailed type GUID first testFindEntitiesByPropertyValue( @@ -891,9 +891,9 @@ public void testSearchByPropertyValue() { MockConstants.EGERIA_PAGESIZE, 10); - // TODO: Same search again, by supertype + // Same search again, by supertype testFindEntitiesByPropertyValue( - "786a6199-0ce8-47bf-b006-9ace1c5510e4", + "a7392281-348d-48a4-bad7-f9742d7696fe", possibleTypes, null, repositoryHelper.getExactMatchRegex("atlas"), @@ -1218,7 +1218,7 @@ public void stopConnector() { atlasRepositoryEventMapper.disconnect(); atlasRepositoryConnector.disconnect(); } catch (ConnectorCheckedException e) { - log.error("Unable to property disconnect connector.", e); + log.error("Unable to properly disconnect connector.", e); } } diff --git a/docs/mappings/data-objects.drawio b/docs/mappings/data-objects.drawio index 7e9abbf..82d797a 100644 --- a/docs/mappings/data-objects.drawio +++ b/docs/mappings/data-objects.drawio @@ -1 +1 @@ -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 \ No newline at end of file +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 \ No newline at end of file