]> source.dussan.org Git - sonarqube.git/commitdiff
GOV-331 trigger views refresh on api/projects/bulk_delete
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Wed, 28 Mar 2018 13:49:57 +0000 (15:49 +0200)
committerSonarTech <sonartech@sonarsource.com>
Fri, 6 Apr 2018 18:21:52 +0000 (20:21 +0200)
12 files changed:
server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentDao.java
server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentMapper.java
server/sonar-db-dao/src/main/resources/org/sonar/db/component/ComponentMapper.xml
server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentDaoTest.java
server/sonar-server/src/main/java/org/sonar/server/project/ProjectLifeCycleListener.java
server/sonar-server/src/main/java/org/sonar/server/project/ProjectLifeCycleListeners.java
server/sonar-server/src/main/java/org/sonar/server/project/ProjectLifeCycleListenersImpl.java
server/sonar-server/src/main/java/org/sonar/server/project/ws/BulkDeleteAction.java
server/sonar-server/src/main/java/org/sonar/server/project/ws/DeleteAction.java
server/sonar-server/src/test/java/org/sonar/server/project/ProjectLifeCycleListenersImplTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/project/ws/BulkDeleteActionTest.java
server/sonar-server/src/test/java/org/sonar/server/project/ws/DeleteActionTest.java

index eeea7944264c581ea0fe554c0c6c7059b562790f..136114d437fc0a436f122f805ca017df76613d46 100644 (file)
@@ -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<String> selectViewKeysWithEnabledCopyOfProject(DbSession session, String projectUuid) {
-    return mapper(session).selectViewKeysWithEnabledCopyOfProject(projectUuid);
+  public Set<String> selectViewKeysWithEnabledCopyOfProject(DbSession session, Set<String> projectUuids) {
+    return executeLargeInputsIntoSet(projectUuids,
+      partition -> mapper(session).selectViewKeysWithEnabledCopyOfProject(partition),
+      i -> i);
   }
 
   public List<String> selectProjectsFromView(DbSession session, String viewUuid, String projectViewUuid) {
index 95025cd329057a488d9b2d93a87c7af2775bcfdb..b33559917e74d1a5549dccedf5fc7d95de35d256 100644 (file)
@@ -121,7 +121,7 @@ public interface ComponentMapper {
    */
   List<KeyWithUuidDto> selectUuidsByKeyFromProjectKey(@Param("projectKey") String projectKey);
 
-  Set<String> selectViewKeysWithEnabledCopyOfProject(@Param("projectUuid") String projectUuid);
+  Set<String> selectViewKeysWithEnabledCopyOfProject(@Param("projectUuids") Collection<String> projectUuids);
 
   /**
    * Return technical projects from a view or a sub-view
index b3e23af45727f2b5275f71e2cdffe069cfbc427e..cac715f12a8dde1fbbfb1ce159a6474e0b8c585a 100644 (file)
       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
+        <foreach collection="projectUuids" open="(" close=")" item="uuid" separator=",">#{uuid,jdbcType=VARCHAR}</foreach>
     where
       p.enabled = ${_true}
       and p.uuid = leaf.project_uuid
index 95e2aaf1543e1b51c0e40a9a0bf848075d2c2e03..caeeb7dee17c62e7ef936a97c3970c3e63863818 100644 (file)
@@ -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<String> 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<String> keys = underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, project.uuid());
+    Set<String> 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<String> keys = underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, project2.uuid());
+    Set<String> 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<String> keys = underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, project.uuid());
+    Set<String> 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<String> keys = underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, project.uuid());
+    Set<String> 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<String> keys = underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, project.uuid());
+    Set<String> 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<String> keys = underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, project2.uuid());
+    Set<String> 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<String> keys = underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, project.uuid());
+    Set<String> 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<String> keys = underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, project.uuid());
+    Set<String> 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<Long> ids = LongStream.range(0L, 1_010L).boxed().collect(Collectors.toSet());
+    Set<Long> 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<String> keys = IntStream.range(0, 1_010).mapToObj(String::valueOf).collect(Collectors.toSet());
+    Set<String> 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<String> uuids = IntStream.range(0, 1_010).mapToObj(String::valueOf).collect(Collectors.toSet());
+    Set<String> 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<Long> ids = LongStream.range(0L, 1_010L).boxed().collect(Collectors.toSet());
+    Set<Long> 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<String> keys = IntStream.range(0, 1_010).mapToObj(String::valueOf).collect(Collectors.toSet());
+    Set<String> 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<String> uuids = IntStream.range(0, 1_010).mapToObj(String::valueOf).collect(Collectors.toSet());
+    Set<String> 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<String> shuffleWithInexistingUuids(String... uuids) {
+    return Stream.concat(
+      IntStream.range(0, 1 + new Random().nextInt(5)).mapToObj(i -> randomAlphabetic(9)),
+      Arrays.stream(uuids))
+      .collect(toSet());
+  }
+
 }
index 5a53ef9062d2e0591cd48b6e65d635e7b34e5086..16601035f75ccbd0ce312d291b718a561796437c 100644 (file)
  */
 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<Project> projects);
 }
index 0198ae28a3f2aa3f196a469b96c9368b688c9d85..352826a2677236f60d91a7ca8d6add47b342f270 100644 (file)
  */
 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
+   * <p>
+   * 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<Project> projects);
 }
index 03cfbc958e6467e158b72fd7707468ede737f3d0..3e43c0ab7be337a16fb3dc00506f88c011cacbe2 100644 (file)
@@ -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<Project> 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<ProjectLifeCycleListener> safelyCallListener(Consumer<ProjectLifeCycleListener> task) {
index dbd400435d76c241cdb5996a4107451d2b019136..c7910e1f183d64724aae0bfd16f6aecaa7f2117d 100644 (file)
@@ -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<ComponentDto> 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();
   }
index 50b7f34e808b6da928648bd9cfbe2385681c768f..eafa3f9205d2b47d253a8be2b1e8e5a20fa6f414 100644 (file)
@@ -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 (file)
index 0000000..6fb489b
--- /dev/null
@@ -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<Project> projects) {
+    underTestNoListeners.onProjectsDeleted(projects);
+  }
+
+  @Test
+  @UseDataProvider("oneOrManyProjects")
+  public void onProjectsDeleted_calls_all_listeners_in_order_of_addition_to_constructor(Set<Project> 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<Project> 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<Project> 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");
+  }
+}
index 874b67032d39cdb8fe810561635bf34c98b760ab..c1ec2aa5218cc9f32fad143713740ff130066601 100644 (file)
 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<Set<Project>> 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()));
+  }
 }
index e09adfaf1974bac0571852e2078caa7332f062b6..b4955d63117c9424ba530c88add23e23bc01b3e2 100644 (file)
@@ -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