From: Sébastien Lesaint Date: Wed, 28 Mar 2018 13:49:57 +0000 (+0200) Subject: GOV-331 trigger views refresh on api/projects/bulk_delete X-Git-Tag: 7.5~1390 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=182432a56dd115b1f7b76aba1eb91ee7387172db;p=sonarqube.git GOV-331 trigger views refresh on api/projects/bulk_delete --- diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentDao.java index eeea7944264..136114d437f 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentDao.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentDao.java @@ -47,6 +47,7 @@ import static org.sonar.core.util.stream.MoreCollectors.toList; import static org.sonar.db.DaoDatabaseUtils.buildLikeValue; import static org.sonar.db.DatabaseUtils.checkThatNotTooManyConditions; import static org.sonar.db.DatabaseUtils.executeLargeInputs; +import static org.sonar.db.DatabaseUtils.executeLargeInputsIntoSet; import static org.sonar.db.DatabaseUtils.executeLargeUpdates; import static org.sonar.db.WildcardPosition.BEFORE_AND_AFTER; import static org.sonar.db.component.ComponentDto.generateBranchKey; @@ -268,8 +269,10 @@ public class ComponentDao implements Dao { /** * Used by Governance */ - public Set selectViewKeysWithEnabledCopyOfProject(DbSession session, String projectUuid) { - return mapper(session).selectViewKeysWithEnabledCopyOfProject(projectUuid); + public Set selectViewKeysWithEnabledCopyOfProject(DbSession session, Set projectUuids) { + return executeLargeInputsIntoSet(projectUuids, + partition -> mapper(session).selectViewKeysWithEnabledCopyOfProject(partition), + i -> i); } public List selectProjectsFromView(DbSession session, String viewUuid, String projectViewUuid) { diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentMapper.java b/server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentMapper.java index 95025cd3290..b33559917e7 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentMapper.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentMapper.java @@ -121,7 +121,7 @@ public interface ComponentMapper { */ List selectUuidsByKeyFromProjectKey(@Param("projectKey") String projectKey); - Set selectViewKeysWithEnabledCopyOfProject(@Param("projectUuid") String projectUuid); + Set selectViewKeysWithEnabledCopyOfProject(@Param("projectUuids") Collection projectUuids); /** * Return technical projects from a view or a sub-view diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/component/ComponentMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/component/ComponentMapper.xml index b3e23af4572..cac715f12a8 100644 --- a/server/sonar-db-dao/src/main/resources/org/sonar/db/component/ComponentMapper.xml +++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/component/ComponentMapper.xml @@ -409,7 +409,8 @@ leaf.qualifier = 'TRK' and leaf.scope = 'FIL' and leaf.enabled = ${_true} - and leaf.copy_component_uuid = #{projectUuid,jdbcType=VARCHAR} + and leaf.copy_component_uuid in + #{uuid,jdbcType=VARCHAR} where p.enabled = ${_true} and p.uuid = leaf.project_uuid diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentDaoTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentDaoTest.java index 95e2aaf1543..caeeb7dee17 100644 --- a/server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentDaoTest.java +++ b/server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentDaoTest.java @@ -35,6 +35,7 @@ import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.LongStream; +import java.util.stream.Stream; import javax.annotation.Nullable; import org.assertj.core.api.ListAssert; import org.junit.Rule; @@ -50,12 +51,15 @@ import org.sonar.db.RowNotFoundException; import org.sonar.db.organization.OrganizationDto; import org.sonar.db.source.FileSourceDto; +import static com.google.common.collect.ImmutableSet.of; import static com.google.common.collect.Lists.newArrayList; import static com.google.common.collect.Sets.newHashSet; import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static java.util.Collections.emptySet; +import static java.util.Collections.singleton; import static java.util.Collections.singletonList; +import static java.util.stream.Collectors.toSet; import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; @@ -658,10 +662,27 @@ public class ComponentDaoTest { } @Test - public void selectViewKeysWithEnabledCopyOfProject_returns_empty_when_there_is_no_view() { - String projectUuid = randomAlphabetic(5); + public void selectViewKeysWithEnabledCopyOfProject_returns_empty_when_set_is_empty() { + assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, emptySet())) + .isEmpty(); + } + + @Test + @UseDataProvider("oneOrMoreProjects") + public void selectViewKeysWithEnabledCopyOfProject_returns_empty_when_there_is_no_view(int projectCount) { + Set projectUuids = IntStream.range(0, projectCount) + .mapToObj(i -> randomAlphabetic(5)) + .collect(toSet()); + + assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, projectUuids)).isEmpty(); + } - assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, projectUuid)).isEmpty(); + @DataProvider + public static Object[][] oneOrMoreProjects() { + return new Object[][] { + {1}, + {1 + new Random().nextInt(10)} + }; } @Test @@ -672,9 +693,47 @@ public class ComponentDaoTest { ComponentDto view = insertView(organization, rootViewQualifier); insertProjectCopy(view, project); - Set keys = underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, project.uuid()); + Set keys = underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, singleton(project.uuid())); assertThat(keys).containsOnly(view.getDbKey()); + + assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, shuffleWithInexistingUuids(project.uuid()))) + .isEqualTo(keys); + } + + @Test + @UseDataProvider("portfolioOrApplicationRootViewQualifier") + public void selectViewKeysWithEnabledCopyOfProject_returns_root_views_with_direct_copy_of_projects(String rootViewQualifier) { + OrganizationDto organization = db.organizations().insert(); + ComponentDto project1 = insertProject(organization); + ComponentDto project2 = insertProject(organization); + ComponentDto view = insertView(organization, rootViewQualifier); + insertProjectCopy(view, project1); + insertProjectCopy(view, project2); + ComponentDto view2 = insertView(organization, rootViewQualifier); + ComponentDto project3 = insertProject(organization); + insertProjectCopy(view2, project3); + + assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, singleton(project1.uuid()))) + .containsOnly(view.getDbKey()); + assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, shuffleWithInexistingUuids(project1.uuid()))) + .containsOnly(view.getDbKey()); + assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, singleton(project2.uuid()))) + .containsOnly(view.getDbKey()); + assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, shuffleWithInexistingUuids(project2.uuid()))) + .containsOnly(view.getDbKey()); + assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, singleton(project3.uuid()))) + .containsOnly(view2.getDbKey()); + assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, shuffleWithInexistingUuids(project3.uuid()))) + .containsOnly(view2.getDbKey()); + assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, of(project2.uuid(), project1.uuid()))) + .containsOnly(view.getDbKey()); + assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, shuffleWithInexistingUuids(project2.uuid(), project1.uuid()))) + .containsOnly(view.getDbKey()); + assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, of(project1.uuid(), project3.uuid()))) + .containsOnly(view.getDbKey(), view2.getDbKey()); + assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, shuffleWithInexistingUuids(project1.uuid(), project3.uuid()))) + .containsOnly(view.getDbKey(), view2.getDbKey()); } @Test @@ -688,9 +747,12 @@ public class ComponentDaoTest { ComponentDto view2 = insertView(organization, rootViewQualifier); insertProjectCopy(view2, project2); - Set keys = underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, project2.uuid()); + Set keys = underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, singleton(project2.uuid())); assertThat(keys).containsOnly(view2.getDbKey()); + + assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, shuffleWithInexistingUuids(project2.uuid()))) + .isEqualTo(keys); } @Test @@ -703,9 +765,12 @@ public class ComponentDaoTest { ComponentDto view2 = insertView(organization, rootViewQualifier); insertProjectCopy(view2, project, t -> t.setEnabled(false)); - Set keys = underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, project.uuid()); + Set keys = underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, singleton(project.uuid())); assertThat(keys).containsOnly(view1.getDbKey()); + + assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, shuffleWithInexistingUuids(project.uuid()))) + .isEqualTo(keys); } @Test @@ -718,9 +783,12 @@ public class ComponentDaoTest { ComponentDto view2 = insertView(organization, rootViewQualifier); insertProjectCopy(view2, project); - Set keys = underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, project.uuid()); + Set keys = underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, singleton(project.uuid())); assertThat(keys).containsOnly(view2.getDbKey()); + + assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, shuffleWithInexistingUuids(project.uuid()))) + .isEqualTo(keys); } @Test @@ -732,9 +800,49 @@ public class ComponentDaoTest { ComponentDto lowestSubview = insertSubviews(view); insertProjectCopy(lowestSubview, project); - Set keys = underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, project.uuid()); + Set keys = underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, singleton(project.uuid())); assertThat(keys).containsOnly(view.getDbKey()); + + assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, shuffleWithInexistingUuids(project.uuid()))) + .isEqualTo(keys); + } + + @Test + @UseDataProvider("portfolioOrApplicationRootViewQualifier") + public void selectViewKeysWithEnabledCopyOfProject_returns_root_views_with_indirect_copy_of_projects(String rootViewQualifier) { + OrganizationDto organization = db.organizations().insert(); + ComponentDto project1 = insertProject(organization); + ComponentDto project2 = insertProject(organization); + ComponentDto view1 = insertView(organization, rootViewQualifier); + ComponentDto lowestSubview1 = insertSubviews(view1); + insertProjectCopy(lowestSubview1, project1); + insertProjectCopy(lowestSubview1, project2); + ComponentDto view2 = insertView(organization, rootViewQualifier); + ComponentDto lowestSubview2 = insertSubviews(view2); + ComponentDto project3 = insertProject(organization); + insertProjectCopy(lowestSubview2, project3); + + assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, singleton(project1.uuid()))) + .containsOnly(view1.getDbKey()); + assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, shuffleWithInexistingUuids(project1.uuid()))) + .containsOnly(view1.getDbKey()); + assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, singleton(project2.uuid()))) + .containsOnly(view1.getDbKey()); + assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, shuffleWithInexistingUuids(project2.uuid()))) + .containsOnly(view1.getDbKey()); + assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, singleton(project3.uuid()))) + .containsOnly(view2.getDbKey()); + assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, shuffleWithInexistingUuids(project3.uuid()))) + .containsOnly(view2.getDbKey()); + assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, of(project2.uuid(), project1.uuid()))) + .containsOnly(view1.getDbKey()); + assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, shuffleWithInexistingUuids(project2.uuid(), project1.uuid()))) + .containsOnly(view1.getDbKey()); + assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, of(project1.uuid(), project3.uuid()))) + .containsOnly(view1.getDbKey(), view2.getDbKey()); + assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, shuffleWithInexistingUuids(project1.uuid(), project3.uuid()))) + .containsOnly(view1.getDbKey(), view2.getDbKey()); } @Test @@ -750,9 +858,12 @@ public class ComponentDaoTest { ComponentDto lowestSubview2 = insertSubviews(view2); insertProjectCopy(lowestSubview2, project2); - Set keys = underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, project2.uuid()); + Set keys = underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, singleton(project2.uuid())); assertThat(keys).containsOnly(view2.getDbKey()); + + assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, shuffleWithInexistingUuids(project2.uuid()))) + .isEqualTo(keys); } @Test @@ -767,9 +878,12 @@ public class ComponentDaoTest { ComponentDto lowestSubview2 = insertSubviews(view2); insertProjectCopy(lowestSubview2, project, t -> t.setEnabled(false)); - Set keys = underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, project.uuid()); + Set keys = underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, singleton(project.uuid())); assertThat(keys).containsOnly(view1.getDbKey()); + + assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, shuffleWithInexistingUuids(project.uuid()))) + .isEqualTo(keys); } @Test @@ -784,9 +898,12 @@ public class ComponentDaoTest { ComponentDto lowestSubview2 = insertSubviews(view2); insertProjectCopy(lowestSubview2, project); - Set keys = underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, project.uuid()); + Set keys = underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, singleton(project.uuid())); assertThat(keys).containsOnly(view2.getDbKey()); + + assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, shuffleWithInexistingUuids(project.uuid()))) + .isEqualTo(keys); } @DataProvider @@ -992,7 +1109,7 @@ public class ComponentDaoTest { @Test public void countByQuery_throws_IAE_if_too_many_component_ids() { - Set ids = LongStream.range(0L, 1_010L).boxed().collect(Collectors.toSet()); + Set ids = LongStream.range(0L, 1_010L).boxed().collect(toSet()); ComponentQuery.Builder query = ComponentQuery.builder() .setQualifiers(Qualifiers.PROJECT) .setComponentIds(ids); @@ -1002,7 +1119,7 @@ public class ComponentDaoTest { @Test public void countByQuery_throws_IAE_if_too_many_component_keys() { - Set keys = IntStream.range(0, 1_010).mapToObj(String::valueOf).collect(Collectors.toSet()); + Set keys = IntStream.range(0, 1_010).mapToObj(String::valueOf).collect(toSet()); ComponentQuery.Builder query = ComponentQuery.builder() .setQualifiers(Qualifiers.PROJECT) .setComponentKeys(keys); @@ -1012,7 +1129,7 @@ public class ComponentDaoTest { @Test public void countByQuery_throws_IAE_if_too_many_component_uuids() { - Set uuids = IntStream.range(0, 1_010).mapToObj(String::valueOf).collect(Collectors.toSet()); + Set uuids = IntStream.range(0, 1_010).mapToObj(String::valueOf).collect(toSet()); ComponentQuery.Builder query = ComponentQuery.builder() .setQualifiers(Qualifiers.PROJECT) .setComponentUuids(uuids); @@ -1258,7 +1375,7 @@ public class ComponentDaoTest { @Test public void selectByQuery_throws_IAE_if_too_many_component_ids() { - Set ids = LongStream.range(0L, 1_010L).boxed().collect(Collectors.toSet()); + Set ids = LongStream.range(0L, 1_010L).boxed().collect(toSet()); ComponentQuery.Builder query = ComponentQuery.builder() .setQualifiers(Qualifiers.PROJECT) .setComponentIds(ids); @@ -1268,7 +1385,7 @@ public class ComponentDaoTest { @Test public void selectByQuery_throws_IAE_if_too_many_component_keys() { - Set keys = IntStream.range(0, 1_010).mapToObj(String::valueOf).collect(Collectors.toSet()); + Set keys = IntStream.range(0, 1_010).mapToObj(String::valueOf).collect(toSet()); ComponentQuery.Builder query = ComponentQuery.builder() .setQualifiers(Qualifiers.PROJECT) .setComponentKeys(keys); @@ -1278,7 +1395,7 @@ public class ComponentDaoTest { @Test public void selectByQuery_throws_IAE_if_too_many_component_uuids() { - Set uuids = IntStream.range(0, 1_010).mapToObj(String::valueOf).collect(Collectors.toSet()); + Set uuids = IntStream.range(0, 1_010).mapToObj(String::valueOf).collect(toSet()); ComponentQuery.Builder query = ComponentQuery.builder() .setQualifiers(Qualifiers.PROJECT) .setComponentUuids(uuids); @@ -1769,4 +1886,11 @@ public class ComponentDaoTest { return underTest.selectByUuid(db.getSession(), uuid).get().isPrivate(); } + private static Set shuffleWithInexistingUuids(String... uuids) { + return Stream.concat( + IntStream.range(0, 1 + new Random().nextInt(5)).mapToObj(i -> randomAlphabetic(9)), + Arrays.stream(uuids)) + .collect(toSet()); + } + } diff --git a/server/sonar-server/src/main/java/org/sonar/server/project/ProjectLifeCycleListener.java b/server/sonar-server/src/main/java/org/sonar/server/project/ProjectLifeCycleListener.java index 5a53ef9062d..16601035f75 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/project/ProjectLifeCycleListener.java +++ b/server/sonar-server/src/main/java/org/sonar/server/project/ProjectLifeCycleListener.java @@ -19,12 +19,13 @@ */ package org.sonar.server.project; +import java.util.Set; import org.sonar.api.server.ServerSide; @ServerSide public interface ProjectLifeCycleListener { /** - * This method is called after the specified project has been deleted. + * This method is called after the specified projects have been deleted. */ - void onProjectDeleted(Project project); + void onProjectsDeleted(Set projects); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/project/ProjectLifeCycleListeners.java b/server/sonar-server/src/main/java/org/sonar/server/project/ProjectLifeCycleListeners.java index 0198ae28a3f..352826a2677 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/project/ProjectLifeCycleListeners.java +++ b/server/sonar-server/src/main/java/org/sonar/server/project/ProjectLifeCycleListeners.java @@ -19,14 +19,16 @@ */ package org.sonar.server.project; +import java.util.Set; + public interface ProjectLifeCycleListeners { /** - * This method is called after the specified project has been deleted and will call method - * {@link ProjectLifeCycleListener#onProjectDeleted(Project) onProjectDeleted(Project)} of all known + * This method is called after the specified projects have been deleted and will call method + * {@link ProjectLifeCycleListener#onProjectsDeleted(Set) onProjectsDeleted(Set)} of all known * {@link ProjectLifeCycleListener} implementations. - * - * This method will ensure all {@link ProjectLifeCycleListener} implementations are called, even if one or more of + *

+ * This method ensures all {@link ProjectLifeCycleListener} implementations are called, even if one or more of * them fail with an exception. */ - void onProjectDeleted(Project project); + void onProjectsDeleted(Set projects); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/project/ProjectLifeCycleListenersImpl.java b/server/sonar-server/src/main/java/org/sonar/server/project/ProjectLifeCycleListenersImpl.java index 03cfbc958e6..3e43c0ab7be 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/project/ProjectLifeCycleListenersImpl.java +++ b/server/sonar-server/src/main/java/org/sonar/server/project/ProjectLifeCycleListenersImpl.java @@ -21,6 +21,7 @@ package org.sonar.server.project; import com.google.common.base.Preconditions; import java.util.Arrays; +import java.util.Set; import java.util.function.Consumer; import org.sonar.api.utils.log.Logger; import org.sonar.api.utils.log.Loggers; @@ -37,16 +38,22 @@ public class ProjectLifeCycleListenersImpl implements ProjectLifeCycleListeners this.listeners = new ProjectLifeCycleListener[0]; } + /** + * Used by Pico when there is at least one ProjectLifeCycleListener implementation in container. + */ public ProjectLifeCycleListenersImpl(ProjectLifeCycleListener[] listeners) { this.listeners = listeners; } @Override - public void onProjectDeleted(Project project) { - Preconditions.checkNotNull(project, "project can't be null"); + public void onProjectsDeleted(Set projects) { + Preconditions.checkNotNull(projects, "projects can't be null"); + if (projects.isEmpty()) { + return; + } Arrays.stream(listeners) - .forEach(safelyCallListener(listener -> listener.onProjectDeleted(project))); + .forEach(safelyCallListener(listener -> listener.onProjectsDeleted(projects))); } private static Consumer safelyCallListener(Consumer task) { diff --git a/server/sonar-server/src/main/java/org/sonar/server/project/ws/BulkDeleteAction.java b/server/sonar-server/src/main/java/org/sonar/server/project/ws/BulkDeleteAction.java index dbd400435d7..c7910e1f183 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/project/ws/BulkDeleteAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/project/ws/BulkDeleteAction.java @@ -27,12 +27,16 @@ import org.sonar.api.server.ws.Request; import org.sonar.api.server.ws.Response; import org.sonar.api.server.ws.WebService; import org.sonar.api.server.ws.WebService.Param; +import org.sonar.core.util.stream.MoreCollectors; import org.sonar.db.DbClient; import org.sonar.db.DbSession; +import org.sonar.db.component.ComponentDto; import org.sonar.db.component.ComponentQuery; import org.sonar.db.organization.OrganizationDto; import org.sonar.db.permission.OrganizationPermission; import org.sonar.server.component.ComponentCleanerService; +import org.sonar.server.project.Project; +import org.sonar.server.project.ProjectLifeCycleListeners; import org.sonar.server.project.Visibility; import org.sonar.server.user.UserSession; @@ -61,13 +65,15 @@ public class BulkDeleteAction implements ProjectsWsAction { private final DbClient dbClient; private final UserSession userSession; private final ProjectsWsSupport support; + private final ProjectLifeCycleListeners projectLifeCycleListeners; public BulkDeleteAction(ComponentCleanerService componentCleanerService, DbClient dbClient, UserSession userSession, - ProjectsWsSupport support) { + ProjectsWsSupport support, ProjectLifeCycleListeners projectLifeCycleListeners) { this.componentCleanerService = componentCleanerService; this.dbClient = dbClient; this.userSession = userSession; this.support = support; + this.projectLifeCycleListeners = projectLifeCycleListeners; } @Override @@ -139,8 +145,12 @@ public class BulkDeleteAction implements ProjectsWsAction { userSession.checkPermission(OrganizationPermission.ADMINISTER, organization); ComponentQuery query = buildDbQuery(searchRequest); - dbClient.componentDao().selectByQuery(dbSession, organization.getUuid(), query, 0, Integer.MAX_VALUE) - .forEach(p -> componentCleanerService.delete(dbSession, p)); + List componentDtos = dbClient.componentDao().selectByQuery(dbSession, organization.getUuid(), query, 0, Integer.MAX_VALUE); + try { + componentDtos.forEach(p -> componentCleanerService.delete(dbSession, p)); + } finally { + projectLifeCycleListeners.onProjectsDeleted(componentDtos.stream().map(Project::from).collect(MoreCollectors.toSet(componentDtos.size()))); + } } response.noContent(); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/project/ws/DeleteAction.java b/server/sonar-server/src/main/java/org/sonar/server/project/ws/DeleteAction.java index 50b7f34e808..eafa3f9205d 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/project/ws/DeleteAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/project/ws/DeleteAction.java @@ -32,6 +32,7 @@ import org.sonar.server.component.ComponentFinder; import org.sonar.server.project.ProjectLifeCycleListeners; import org.sonar.server.user.UserSession; +import static java.util.Collections.singleton; import static org.sonar.server.component.ComponentFinder.ParamNames.PROJECT_ID_AND_PROJECT; import static org.sonar.server.project.Project.from; import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001; @@ -91,7 +92,7 @@ public class DeleteAction implements ProjectsWsAction { ComponentDto project = componentFinder.getByUuidOrKey(dbSession, uuid, key, PROJECT_ID_AND_PROJECT); checkPermission(project); componentCleanerService.delete(dbSession, project); - projectLifeCycleListeners.onProjectDeleted(from(project)); + projectLifeCycleListeners.onProjectsDeleted(singleton(from(project))); } response.noContent(); diff --git a/server/sonar-server/src/test/java/org/sonar/server/project/ProjectLifeCycleListenersImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/project/ProjectLifeCycleListenersImplTest.java new file mode 100644 index 00000000000..6fb489bbf40 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/project/ProjectLifeCycleListenersImplTest.java @@ -0,0 +1,145 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.project; + +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; +import java.util.Collections; +import java.util.Random; +import java.util.Set; +import java.util.stream.IntStream; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.InOrder; +import org.mockito.Mockito; +import org.sonar.core.util.stream.MoreCollectors; + +import static java.util.Collections.singleton; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyZeroInteractions; + +@RunWith(DataProviderRunner.class) +public class ProjectLifeCycleListenersImplTest { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private ProjectLifeCycleListener listener1 = mock(ProjectLifeCycleListener.class); + private ProjectLifeCycleListener listener2 = mock(ProjectLifeCycleListener.class); + private ProjectLifeCycleListener listener3 = mock(ProjectLifeCycleListener.class); + private ProjectLifeCycleListenersImpl underTestNoListeners = new ProjectLifeCycleListenersImpl(); + private ProjectLifeCycleListenersImpl underTestWithListeners = new ProjectLifeCycleListenersImpl( + new ProjectLifeCycleListener[] {listener1, listener2, listener3}); + + @Test + public void onProjectsDeleted_throws_NPE_if_set_is_null() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("projects can't be null"); + + underTestWithListeners.onProjectsDeleted(null); + } + + @Test + public void onProjectsDeleted_throws_NPE_if_set_is_null_even_if_no_listeners() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("projects can't be null"); + + underTestNoListeners.onProjectsDeleted(null); + } + + @Test + public void onProjectsDeleted_has_no_effect_if_set_is_empty() { + underTestNoListeners.onProjectsDeleted(Collections.emptySet()); + + underTestWithListeners.onProjectsDeleted(Collections.emptySet()); + verifyZeroInteractions(listener1, listener2, listener3); + } + + @Test + @UseDataProvider("oneOrManyProjects") + public void onProjectsDeleted_does_not_fail_if_there_is_no_listener(Set projects) { + underTestNoListeners.onProjectsDeleted(projects); + } + + @Test + @UseDataProvider("oneOrManyProjects") + public void onProjectsDeleted_calls_all_listeners_in_order_of_addition_to_constructor(Set projects) { + InOrder inOrder = Mockito.inOrder(listener1, listener2, listener3); + + underTestWithListeners.onProjectsDeleted(projects); + + inOrder.verify(listener1).onProjectsDeleted(same(projects)); + inOrder.verify(listener2).onProjectsDeleted(same(projects)); + inOrder.verify(listener3).onProjectsDeleted(same(projects)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + @UseDataProvider("oneOrManyProjects") + public void onProjectsDeleted_calls_all_listeners_even_if_one_throws_an_Exception(Set projects) { + InOrder inOrder = Mockito.inOrder(listener1, listener2, listener3); + doThrow(new RuntimeException("Faking listener2 throwing an exception")) + .when(listener2) + .onProjectsDeleted(any()); + + underTestWithListeners.onProjectsDeleted(projects); + + inOrder.verify(listener1).onProjectsDeleted(same(projects)); + inOrder.verify(listener2).onProjectsDeleted(same(projects)); + inOrder.verify(listener3).onProjectsDeleted(same(projects)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + @UseDataProvider("oneOrManyProjects") + public void onProjectsDeleted_calls_all_listeners_even_if_one_throws_an_Error(Set projects) { + InOrder inOrder = Mockito.inOrder(listener1, listener2, listener3); + doThrow(new Error("Faking listener2 throwing an Error")) + .when(listener2) + .onProjectsDeleted(any()); + + underTestWithListeners.onProjectsDeleted(projects); + + inOrder.verify(listener1).onProjectsDeleted(same(projects)); + inOrder.verify(listener2).onProjectsDeleted(same(projects)); + inOrder.verify(listener3).onProjectsDeleted(same(projects)); + inOrder.verifyNoMoreInteractions(); + } + + @DataProvider + public static Object[][] oneOrManyProjects() { + return new Object[][] { + {singleton(newUniqueProject())}, + {IntStream.range(0, 1 + new Random().nextInt(10)).mapToObj(i -> newUniqueProject()).collect(MoreCollectors.toSet())} + }; + } + + private static int counter = 3_989; + + private static Project newUniqueProject() { + int base = counter++; + return new Project(base + "_uuid", base + "_key", base + "_name"); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/project/ws/BulkDeleteActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/project/ws/BulkDeleteActionTest.java index 874b67032d3..c1ec2aa5218 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/project/ws/BulkDeleteActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/project/ws/BulkDeleteActionTest.java @@ -20,8 +20,11 @@ package org.sonar.server.project.ws; import java.net.HttpURLConnection; +import java.util.Arrays; import java.util.Date; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import java.util.stream.IntStream; import org.apache.commons.lang.StringUtils; import org.junit.Before; @@ -43,12 +46,15 @@ import org.sonar.server.exceptions.ForbiddenException; import org.sonar.server.exceptions.UnauthorizedException; import org.sonar.server.organization.BillingValidationsProxy; import org.sonar.server.organization.TestDefaultOrganizationProvider; +import org.sonar.server.project.Project; +import org.sonar.server.project.ProjectLifeCycleListeners; import org.sonar.server.tester.UserSessionRule; import org.sonar.server.ws.TestResponse; import org.sonar.server.ws.WsActionTester; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -74,8 +80,9 @@ public class BulkDeleteActionTest { private ComponentCleanerService componentCleanerService = mock(ComponentCleanerService.class); private DbClient dbClient = db.getDbClient(); private ProjectsWsSupport support = new ProjectsWsSupport(dbClient, TestDefaultOrganizationProvider.from(db), mock(BillingValidationsProxy.class)); + private ProjectLifeCycleListeners projectLifeCycleListeners = mock(ProjectLifeCycleListeners.class); - private BulkDeleteAction underTest = new BulkDeleteAction(componentCleanerService, dbClient, userSession, support); + private BulkDeleteAction underTest = new BulkDeleteAction(componentCleanerService, dbClient, userSession, support, projectLifeCycleListeners); private WsActionTester ws = new WsActionTester(underTest); private OrganizationDto org1; @@ -102,6 +109,7 @@ public class BulkDeleteActionTest { assertThat(result.getStatus()).isEqualTo(HttpURLConnection.HTTP_NO_CONTENT); assertThat(result.getInput()).isEmpty(); verifyDeleted(toDeleteInOrg2); + verifyListenersOnProjectsDeleted(toDeleteInOrg2); } @Test @@ -117,6 +125,7 @@ public class BulkDeleteActionTest { .execute(); verifyDeleted(toDeleteInOrg1, toDeleteInOrg2); + verifyListenersOnProjectsDeleted(toDeleteInOrg1, toDeleteInOrg2); } @Test @@ -131,6 +140,7 @@ public class BulkDeleteActionTest { .execute(); verifyDeleted(toDelete1, toDelete2); + verifyListenersOnProjectsDeleted(toDelete1, toDelete2); } @Test @@ -149,6 +159,7 @@ public class BulkDeleteActionTest { .execute(); verifyDeleted(oldProject); + verifyListenersOnProjectsDeleted(oldProject); } @Test @@ -161,6 +172,7 @@ public class BulkDeleteActionTest { ws.newRequest().setParam(PARAM_ON_PROVISIONED_ONLY, "true").execute(); verifyDeleted(provisionedProject); + verifyListenersOnProjectsDeleted(provisionedProject); } @Test @@ -171,6 +183,7 @@ public class BulkDeleteActionTest { ws.newRequest().execute(); verifyDeleted(projects); + verifyListenersOnProjectsDeleted(projects); } @Test @@ -182,6 +195,7 @@ public class BulkDeleteActionTest { ws.newRequest().setParam(PARAM_QUALIFIERS, String.join(",", Qualifiers.PROJECT, Qualifiers.VIEW)).execute(); verifyDeleted(project, view); + verifyListenersOnProjectsDeleted(project, view); } @Test @@ -194,6 +208,7 @@ public class BulkDeleteActionTest { ws.newRequest().setParam(Param.TEXT_QUERY, "JeCt-_%-k").execute(); verifyDeleted(matchKeyProject, matchUppercaseKeyProject); + verifyListenersOnProjectsDeleted(matchKeyProject, matchUppercaseKeyProject); } @Test @@ -204,11 +219,14 @@ public class BulkDeleteActionTest { expectedException.expect(ForbiddenException.class); expectedException.expectMessage("Insufficient privileges"); - ws.newRequest() - .setParam("projects", project.getDbKey()) - .execute(); - - verifyNoDeletions(); + try { + ws.newRequest() + .setParam("projects", project.getDbKey()) + .execute(); + } finally { + verifyNoDeletions(); + verifyZeroInteractions(projectLifeCycleListeners); + } } /** @@ -226,6 +244,33 @@ public class BulkDeleteActionTest { .execute(); verify(componentCleanerService, times(1_000)).delete(any(DbSession.class), any(ComponentDto.class)); + ArgumentCaptor> projectsCaptor = ArgumentCaptor.forClass(Set.class); + verify(projectLifeCycleListeners).onProjectsDeleted(projectsCaptor.capture()); + assertThat(projectsCaptor.getValue()).hasSize(1_000); + } + + @Test + public void projectLifeCycleListeners_onProjectsDeleted_called_even_if_delete_fails() { + userSession.logIn().addPermission(ADMINISTER, org1); + ComponentDto project1 = db.components().insertPrivateProject(org1); + ComponentDto project2 = db.components().insertPrivateProject(org1); + ComponentDto project3 = db.components().insertPrivateProject(org1); + ComponentCleanerService componentCleanerService = mock(ComponentCleanerService.class); + RuntimeException expectedException = new RuntimeException("Faking delete failing on 2nd project"); + doNothing() + .doThrow(expectedException) + .when(componentCleanerService) + .delete(any(), any(ComponentDto.class)); + + try { + ws.newRequest() + .setParam("organization", org1.getKey()) + .setParam("projects", project1.getDbKey() + "," + project2.getDbKey() + "," + project3.getDbKey()) + .execute(); + } catch (RuntimeException e) { + assertThat(e).isSameAs(expectedException); + verifyListenersOnProjectsDeleted(project1, project2, project3); + } } @Test @@ -240,6 +285,7 @@ public class BulkDeleteActionTest { .execute(); verifyDeleted(toDelete); + verifyListenersOnProjectsDeleted(toDelete); } @Test @@ -251,6 +297,7 @@ public class BulkDeleteActionTest { .setParam("ids", "whatever-the-uuid").execute(); verifyNoDeletions(); + verifyZeroInteractions(projectLifeCycleListeners); } @Test @@ -264,6 +311,7 @@ public class BulkDeleteActionTest { .setParam("ids", "whatever-the-uuid").execute(); verifyNoDeletions(); + verifyZeroInteractions(projectLifeCycleListeners); } @Test @@ -279,6 +327,7 @@ public class BulkDeleteActionTest { .execute(); verifyNoDeletions(); + verifyZeroInteractions(projectLifeCycleListeners); } private void verifyDeleted(ComponentDto... projects) { @@ -293,4 +342,9 @@ public class BulkDeleteActionTest { private void verifyNoDeletions() { verifyZeroInteractions(componentCleanerService); } + + private void verifyListenersOnProjectsDeleted(ComponentDto... components) { + verify(projectLifeCycleListeners) + .onProjectsDeleted(Arrays.stream(components).map(Project::from).collect(Collectors.toSet())); + } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/project/ws/DeleteActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/project/ws/DeleteActionTest.java index e09adfaf197..b4955d63117 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/project/ws/DeleteActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/project/ws/DeleteActionTest.java @@ -45,6 +45,7 @@ import org.sonar.server.project.ProjectLifeCycleListeners; import org.sonar.server.tester.UserSessionRule; import org.sonar.server.ws.WsTester; +import static java.util.Collections.singleton; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; @@ -93,7 +94,7 @@ public class DeleteActionTest { call(request); assertThat(verifyDeletedKey()).isEqualTo(project.getDbKey()); - verify(projectLifeCycleListeners).onProjectDeleted(Project.from(project)); + verify(projectLifeCycleListeners).onProjectsDeleted(singleton(Project.from(project))); } @Test @@ -104,7 +105,7 @@ public class DeleteActionTest { call(newRequest().setParam(PARAM_PROJECT, project.getDbKey())); assertThat(verifyDeletedKey()).isEqualTo(project.getDbKey()); - verify(projectLifeCycleListeners).onProjectDeleted(Project.from(project)); + verify(projectLifeCycleListeners).onProjectsDeleted(singleton(Project.from(project))); } @Test @@ -115,7 +116,7 @@ public class DeleteActionTest { call(newRequest().setParam(PARAM_PROJECT_ID, project.uuid())); assertThat(verifyDeletedKey()).isEqualTo(project.getDbKey()); - verify(projectLifeCycleListeners).onProjectDeleted(Project.from(project)); + verify(projectLifeCycleListeners).onProjectsDeleted(singleton(Project.from(project))); } @Test @@ -126,7 +127,7 @@ public class DeleteActionTest { call(newRequest().setParam(PARAM_PROJECT, project.getDbKey())); assertThat(verifyDeletedKey()).isEqualTo(project.getDbKey()); - verify(projectLifeCycleListeners).onProjectDeleted(Project.from(project)); + verify(projectLifeCycleListeners).onProjectsDeleted(singleton(Project.from(project))); } @Test