@@ -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) { |
@@ -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 |
@@ -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 | |||
<foreach collection="projectUuids" open="(" close=")" item="uuid" separator=",">#{uuid,jdbcType=VARCHAR}</foreach> | |||
where | |||
p.enabled = ${_true} | |||
and p.uuid = leaf.project_uuid |
@@ -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()); | |||
} | |||
} |
@@ -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<Project> projects); | |||
} |
@@ -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 | |||
* <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); | |||
} |
@@ -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) { |
@@ -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(); | |||
} |
@@ -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(); |
@@ -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"); | |||
} | |||
} |
@@ -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<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())); | |||
} | |||
} |
@@ -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 |