diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/SuggestionRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/SuggestionRepository.java index 783f0f52630..d4ec0e0ea0b 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/SuggestionRepository.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/SuggestionRepository.java @@ -15,7 +15,9 @@ import static org.openmetadata.service.jdbi3.UserRepository.TEAMS_FIELD; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Set; import java.util.UUID; +import java.util.stream.Collectors; import javax.json.JsonPatch; import javax.ws.rs.core.Response; import javax.ws.rs.core.SecurityContext; @@ -39,11 +41,13 @@ import org.openmetadata.service.exception.CatalogExceptionMessage; import org.openmetadata.service.exception.EntityNotFoundException; import org.openmetadata.service.resources.feeds.MessageParser; import org.openmetadata.service.resources.feeds.SuggestionsResource; +import org.openmetadata.service.resources.tags.TagLabelUtil; import org.openmetadata.service.security.AuthorizationException; import org.openmetadata.service.security.Authorizer; import org.openmetadata.service.security.policyevaluator.OperationContext; import org.openmetadata.service.security.policyevaluator.ResourceContext; import org.openmetadata.service.util.EntityUtil; +import org.openmetadata.service.util.FullyQualifiedName; import org.openmetadata.service.util.JsonUtils; import org.openmetadata.service.util.RestUtil; import org.openmetadata.service.util.ResultList; @@ -172,8 +176,7 @@ public class SuggestionRepository { entity, entityLink.getFullyQualifiedFieldValue(), suggestion); } else { if (suggestion.getType().equals(SuggestionType.SuggestTagLabel)) { - List tags = new ArrayList<>(entity.getTags()); - tags.addAll(suggestion.getTagLabels()); + List tags = mergeTags(entity.getTags(), suggestion.getTagLabels()); entity.setTags(tags); return entity; } else if (suggestion.getType().equals(SuggestionType.SuggestDescription)) { @@ -186,6 +189,50 @@ public class SuggestionRepository { } } + private static List mergeTags( + List existingTags, List incomingTags) { + if (incomingTags == null || incomingTags.isEmpty()) { + return existingTags; + } + // Throw an error if incoming tags are mutually exclusive + TagLabelUtil.checkMutuallyExclusive(incomingTags); + + ArrayList tags = new ArrayList<>(); + Set incomingClassification = + incomingTags.stream() + .map(t -> FullyQualifiedName.getParentFQN(t.getTagFQN())) + .collect(Collectors.toSet()); + + // We'll give priority to incoming tags over existing tags + // so we'll skip any existing tag that is mutually exclusive and clashing with incoming + // classification + for (TagLabel tag : existingTags) { + if (TagLabelUtil.mutuallyExclusive(tag) + && incomingClassification.contains(FullyQualifiedName.getParentFQN(tag.getTagFQN()))) { + LOG.debug( + String.format( + "Incoming tags are mutually exclusive with existing tag [%s]", tag.getTagFQN())); + } else { + tags.add(tag); + } + } + return naiveMergeTags(tags, incomingTags); + } + + // Add all tags without repeats + private static List naiveMergeTags( + List existingTags, List incomingTags) { + List tags = new ArrayList<>(existingTags); + Set existingTagFQNs = + existingTags.stream().map(TagLabel::getTagFQN).collect(Collectors.toSet()); + for (TagLabel incomingTag : incomingTags) { + if (!existingTagFQNs.contains(incomingTag.getTagFQN())) { + tags.add(incomingTag); + } + } + return tags; + } + public RestUtil.PutResponse acceptSuggestion( UriInfo uriInfo, Suggestion suggestion, diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/resources/feeds/SuggestionsResourceTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/resources/feeds/SuggestionsResourceTest.java index a3375627643..dea47791288 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/resources/feeds/SuggestionsResourceTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/resources/feeds/SuggestionsResourceTest.java @@ -12,6 +12,8 @@ import static org.openmetadata.service.resources.EntityResourceTest.C1; import static org.openmetadata.service.resources.EntityResourceTest.C2; import static org.openmetadata.service.resources.EntityResourceTest.PERSONAL_DATA_TAG_LABEL; import static org.openmetadata.service.resources.EntityResourceTest.PII_SENSITIVE_TAG_LABEL; +import static org.openmetadata.service.resources.EntityResourceTest.TIER1_TAG_LABEL; +import static org.openmetadata.service.resources.EntityResourceTest.TIER2_TAG_LABEL; import static org.openmetadata.service.security.SecurityUtil.authHeaders; import static org.openmetadata.service.util.TestUtils.ADMIN_AUTH_HEADERS; import static org.openmetadata.service.util.TestUtils.assertResponse; @@ -528,6 +530,37 @@ public class SuggestionsResourceTest extends OpenMetadataApplicationTest { } } + @Test + @Order(6) + void put_acceptSuggestion_mutuallyExclusiveTags_200(TestInfo test) throws IOException { + TableResourceTest tableResourceTest = new TableResourceTest(); + CreateTable createTable = tableResourceTest.createRequest(test); + Table table = tableResourceTest.createAndCheckEntity(createTable, ADMIN_AUTH_HEADERS); + MessageParser.EntityLink entityLink = + new MessageParser.EntityLink(Entity.TABLE, table.getFullyQualifiedName()); + + CreateSuggestion create = createTierSuggestion(TIER1_TAG_LABEL, entityLink); + Suggestion suggestion = createSuggestion(create, USER_AUTH_HEADERS); + Assertions.assertEquals(create.getEntityLink(), suggestion.getEntityLink()); + + // When accepting the suggestion, we'll get the Tier1 tag applied to the table + acceptSuggestion(suggestion.getId(), USER_AUTH_HEADERS); + table = tableResourceTest.getEntity(table.getId(), "tags", USER_AUTH_HEADERS); + List expectedTags = new ArrayList<>(table.getTags()); + expectedTags.addAll(suggestion.getTagLabels()); + validateAppliedTags(expectedTags, table.getTags()); + + // Not, let's try to apply the Tier2 tag, which is mutually exclusive with the Tier1 tag + // The table should then only have the Tier2 + create = createTierSuggestion(TIER2_TAG_LABEL, entityLink); + suggestion = createSuggestion(create, USER_AUTH_HEADERS); + acceptSuggestion(suggestion.getId(), USER_AUTH_HEADERS); + table = tableResourceTest.getEntity(table.getId(), "tags", USER_AUTH_HEADERS); + expectedTags = new ArrayList<>(table.getTags()); + expectedTags.addAll(suggestion.getTagLabels()); + validateAppliedTags(expectedTags, table.getTags()); + } + public Suggestion createSuggestion(CreateSuggestion create, Map authHeaders) throws HttpResponseException { return TestUtils.post(getResource("suggestions"), create, Suggestion.class, authHeaders); @@ -552,6 +585,13 @@ public class SuggestionsResourceTest extends OpenMetadataApplicationTest { .withEntityLink(TABLE_LINK); } + public CreateSuggestion createTierSuggestion(TagLabel tier, MessageParser.EntityLink entityLink) { + return new CreateSuggestion() + .withTagLabels(List.of(tier)) + .withType(SuggestionType.SuggestTagLabel) + .withEntityLink(entityLink.getLinkString()); + } + public Suggestion getSuggestion(UUID id, Map authHeaders) throws HttpResponseException { WebTarget target = getResource("suggestions/" + id);