diff --git a/src/main/java/org/dependencytrack/persistence/NotificationQueryManager.java b/src/main/java/org/dependencytrack/persistence/NotificationQueryManager.java index 3f4b6e71ad..48a0702913 100644 --- a/src/main/java/org/dependencytrack/persistence/NotificationQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/NotificationQueryManager.java @@ -239,21 +239,23 @@ public void deleteNotificationPublisher(final NotificationPublisher notification } /** - * @since 4.12.0 + * @since 4.12.3 */ @Override - public boolean bind(final NotificationRule notificationRule, final Collection tags) { + public boolean bind(final NotificationRule notificationRule, final Collection tags, final boolean keepExisting) { assertPersistent(notificationRule, "notificationRule must be persistent"); assertPersistentAll(tags, "tags must be persistent"); return callInTransaction(() -> { boolean modified = false; - for (final Tag existingTag : notificationRule.getTags()) { - if (!tags.contains(existingTag)) { - notificationRule.getTags().remove(existingTag); - existingTag.getNotificationRules().remove(notificationRule); - modified = true; + if (!keepExisting) { + for (final Tag existingTag : notificationRule.getTags()) { + if (!tags.contains(existingTag)) { + notificationRule.getTags().remove(existingTag); + existingTag.getNotificationRules().remove(notificationRule); + modified = true; + } } } @@ -275,4 +277,12 @@ public boolean bind(final NotificationRule notificationRule, final Collection tags) { + return bind(notificationRule, tags, /* keepExisting */ false); + } + } diff --git a/src/main/java/org/dependencytrack/persistence/PolicyQueryManager.java b/src/main/java/org/dependencytrack/persistence/PolicyQueryManager.java index 6ef4cb5e5e..35337499d2 100644 --- a/src/main/java/org/dependencytrack/persistence/PolicyQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/PolicyQueryManager.java @@ -831,21 +831,23 @@ void preprocessACLs(final Query query, final String inputFilter, final Map tags) { + public boolean bind(final Policy policy, final Collection tags, final boolean keepExisting) { assertPersistent(policy, "policy must be persistent"); assertPersistentAll(tags, "tags must be persistent"); return callInTransaction(() -> { boolean modified = false; - for (final Tag existingTag : policy.getTags()) { - if (!tags.contains(existingTag)) { - policy.getTags().remove(existingTag); - existingTag.getPolicies().remove(policy); - modified = true; + if (!keepExisting) { + for (final Tag existingTag : policy.getTags()) { + if (!tags.contains(existingTag)) { + policy.getTags().remove(existingTag); + existingTag.getPolicies().remove(policy); + modified = true; + } } } @@ -867,4 +869,12 @@ public boolean bind(final Policy policy, final Collection tags) { }); } + /** + * @since 4.12.0 + */ + @Override + public boolean bind(final Policy policy, final Collection tags) { + return bind(policy, tags, /* keepExisting */ false); + } + } diff --git a/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java b/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java index 564b7e31f2..728c4a01db 100644 --- a/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java @@ -64,6 +64,7 @@ import java.io.IOException; import java.security.Principal; import java.util.ArrayList; +import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.HashSet; @@ -73,6 +74,9 @@ import java.util.UUID; import java.util.concurrent.atomic.AtomicReference; +import static org.dependencytrack.util.PersistenceUtil.assertPersistent; +import static org.dependencytrack.util.PersistenceUtil.assertPersistentAll; + final class ProjectQueryManager extends QueryManager implements IQueryManager { private static final Logger LOGGER = Logger.getLogger(ProjectQueryManager.class); @@ -934,32 +938,54 @@ public List getProjectProperties(final Project project) { } /** - * Binds the two objects together in a corresponding join table. - * @param project a Project object - * @param tags a List of Tag objects + * @since 4.12.3 */ @Override - public void bind(Project project, List tags) { - runInTransaction(() -> { - final Query query = pm.newQuery(Tag.class, "projects.contains(:project)"); - query.setParameters(project); - final List currentProjectTags = executeAndCloseList(query); - - for (final Tag tag : currentProjectTags) { - if (!tags.contains(tag)) { - tag.getProjects().remove(project); + public boolean bind(final Project project, final Collection tags, final boolean keepExisting) { + assertPersistent(project, "project must be persistent"); + assertPersistentAll(tags, "tags must be persistent"); + + return callInTransaction(() -> { + boolean modified = false; + + if (!keepExisting) { + for (final Tag existingTag : project.getTags()) { + if (!tags.contains(existingTag)) { + project.getTags().remove(existingTag); + existingTag.getProjects().remove(project); + modified = true; + } } } - project.setTags(tags); + for (final Tag tag : tags) { - final List projects = tag.getProjects(); - if (!projects.contains(project)) { - projects.add(project); + if (!project.getTags().contains(tag)) { + project.getTags().add(tag); + + if (tag.getProjects() == null) { + tag.setProjects(new ArrayList<>(List.of(project))); + } else if (!tag.getProjects().contains(project)) { + tag.getProjects().add(project); + } + + modified = true; } } + + return modified; }); } + /** + * Binds the two objects together in a corresponding join table. + * @param project a Project object + * @param tags a List of Tag objects + */ + @Override + public void bind(final Project project, final List tags) { + bind(project, tags, /* keepExisting */ false); + } + /** * Updates the last time a bom was imported. * @param date the date of the last bom import diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index 01c73ebcf2..172812809f 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -1339,14 +1339,26 @@ public void clearComponentAnalysisCache(Date threshold) { getCacheQueryManager().clearComponentAnalysisCache(threshold); } + public boolean bind(final NotificationRule notificationRule, final Collection tags, final boolean keepExisting) { + return getNotificationQueryManager().bind(notificationRule, tags, keepExisting); + } + public boolean bind(final NotificationRule notificationRule, final Collection tags) { return getNotificationQueryManager().bind(notificationRule, tags); } + public boolean bind(final Project project, final Collection tags, final boolean keepExisting) { + return getProjectQueryManager().bind(project, tags, keepExisting); + } + public void bind(Project project, List tags) { getProjectQueryManager().bind(project, tags); } + public boolean bind(final Policy policy, final Collection tags, final boolean keepExisting) { + return getPolicyQueryManager().bind(policy, tags, keepExisting); + } + public boolean bind(final Policy policy, final Collection tags) { return getPolicyQueryManager().bind(policy, tags); } diff --git a/src/main/java/org/dependencytrack/persistence/TagQueryManager.java b/src/main/java/org/dependencytrack/persistence/TagQueryManager.java index 3ced4ec62c..4430ba3f6f 100644 --- a/src/main/java/org/dependencytrack/persistence/TagQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/TagQueryManager.java @@ -459,14 +459,7 @@ public void tagProjects(final String tagName, final Collection projectUu final List projects = executeAndCloseList(projectsQuery); for (final Project project : projects) { - if (project.getTags() == null || project.getTags().isEmpty()) { - project.setTags(List.of(tag)); - continue; - } - - if (!project.getTags().contains(tag)) { - project.getTags().add(tag); - } + bind(project, List.of(tag), /* keepExisting */ true); } }); } @@ -569,7 +562,7 @@ public void tagPolicies(final String tagName, final Collection policyUui final List policies = executeAndCloseList(policiesQuery); for (final Policy policy : policies) { - bind(policy, List.of(tag)); + bind(policy, List.of(tag), /* keepExisting */ true); } }); } @@ -694,7 +687,7 @@ public void tagNotificationRules(final String tagName, final Collection final List notificationRules = executeAndCloseList(notificationRulesQuery); for (final NotificationRule notificationRule : notificationRules) { - bind(notificationRule, List.of(tag)); + bind(notificationRule, List.of(tag), /* keepExisting */ true); } }); } diff --git a/src/test/java/org/dependencytrack/resources/v1/TagResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/TagResourceTest.java index 6982f2d68b..6262b58daf 100644 --- a/src/test/java/org/dependencytrack/resources/v1/TagResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/TagResourceTest.java @@ -700,15 +700,24 @@ public void tagProjectsTest() { qm.createTag("foo"); + final var projectC = new Project(); + projectC.setName("acme-app-c"); + qm.persist(projectC); + + qm.bind(projectC, List.of(qm.createTag("bar"))); + final Response response = jersey.target(V1_TAG + "/foo/project") .request() .header(X_API_KEY, apiKey) - .post(Entity.json(List.of(projectA.getUuid(), projectB.getUuid()))); + .post(Entity.json(List.of(projectA.getUuid(), projectB.getUuid(), projectC.getUuid()))); assertThat(response.getStatus()).isEqualTo(204); qm.getPersistenceManager().evictAll(); assertThat(projectA.getTags()).satisfiesExactly(projectTag -> assertThat(projectTag.getName()).isEqualTo("foo")); assertThat(projectB.getTags()).satisfiesExactly(projectTag -> assertThat(projectTag.getName()).isEqualTo("foo")); + assertThat(projectC.getTags()).satisfiesExactlyInAnyOrder( + projectTag -> assertThat(projectTag.getName()).isEqualTo("foo"), + projectTag -> assertThat(projectTag.getName()).isEqualTo("bar")); } @Test @@ -1126,15 +1135,26 @@ public void tagPoliciesTest() { qm.createTag("foo"); + final var policyC = new Policy(); + policyC.setName("policy-c"); + policyC.setOperator(Policy.Operator.ALL); + policyC.setViolationState(Policy.ViolationState.INFO); + qm.persist(policyC); + + qm.bind(policyC, List.of(qm.createTag("bar"))); + final Response response = jersey.target(V1_TAG + "/foo/policy") .request() .header(X_API_KEY, apiKey) - .post(Entity.json(List.of(policyA.getUuid(), policyB.getUuid()))); + .post(Entity.json(List.of(policyA.getUuid(), policyB.getUuid(), policyC.getUuid()))); assertThat(response.getStatus()).isEqualTo(204); qm.getPersistenceManager().evictAll(); assertThat(policyA.getTags()).satisfiesExactly(policyTag -> assertThat(policyTag.getName()).isEqualTo("foo")); assertThat(policyB.getTags()).satisfiesExactly(policyTag -> assertThat(policyTag.getName()).isEqualTo("foo")); + assertThat(policyC.getTags()).satisfiesExactlyInAnyOrder( + projectTag -> assertThat(projectTag.getName()).isEqualTo("foo"), + projectTag -> assertThat(projectTag.getName()).isEqualTo("bar")); } @Test @@ -1514,15 +1534,25 @@ public void tagNotificationRulesTest() { qm.createTag("foo"); + final var notificationRuleC = new NotificationRule(); + notificationRuleC.setName("rule-c"); + notificationRuleC.setScope(NotificationScope.PORTFOLIO); + qm.persist(notificationRuleC); + + qm.bind(notificationRuleC, List.of(qm.createTag("bar"))); + final Response response = jersey.target(V1_TAG + "/foo/notificationRule") .request() .header(X_API_KEY, apiKey) - .post(Entity.json(List.of(notificationRuleA.getUuid(), notificationRuleB.getUuid()))); + .post(Entity.json(List.of(notificationRuleA.getUuid(), notificationRuleB.getUuid(), notificationRuleC.getUuid()))); assertThat(response.getStatus()).isEqualTo(204); qm.getPersistenceManager().evictAll(); assertThat(notificationRuleA.getTags()).satisfiesExactly(ruleTag -> assertThat(ruleTag.getName()).isEqualTo("foo")); assertThat(notificationRuleB.getTags()).satisfiesExactly(ruleTag -> assertThat(ruleTag.getName()).isEqualTo("foo")); + assertThat(notificationRuleC.getTags()).satisfiesExactlyInAnyOrder( + projectTag -> assertThat(projectTag.getName()).isEqualTo("foo"), + projectTag -> assertThat(projectTag.getName()).isEqualTo("bar")); } @Test