Browse Source

SONAR-15313 drop endpoints from WebAPI deprecated since 7.X

Co-authored-by: Lukasz Jarocki <lukasz.jarocki@sonarsource.com>
Co-authored-by: Philippe Perrin <philippe.perrin@sonarsource.com>
Co-authored-by: MikeBirnstiehl <michael.birnstiehl@sonarsource.com>
tags/9.1.0.47736
Lukasz Jarocki 2 years ago
parent
commit
680aed78d5
65 changed files with 161 additions and 2794 deletions
  1. 0
    105
      server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentKeyUpdaterDao.java
  2. 0
    28
      server/sonar-db-dao/src/main/java/org/sonar/db/source/FileSourceDao.java
  3. 0
    266
      server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentKeyUpdaterDaoTest.java
  4. 0
    68
      server/sonar-db-dao/src/test/java/org/sonar/db/source/FileSourceDaoTest.java
  5. 0
    1
      server/sonar-docs/src/pages/instance-administration/security.md
  6. 4
    1
      server/sonar-docs/src/pages/setup/upgrade-notes.md
  7. 0
    11
      server/sonar-server-common/src/main/java/org/sonar/server/issue/SearchRequest.java
  8. 0
    2
      server/sonar-server-common/src/test/java/org/sonar/server/issue/SearchRequestTest.java
  9. 1
    2
      server/sonar-web/src/main/js/api/issues.ts
  10. 2
    4
      server/sonar-web/src/main/js/apps/issues/components/App.tsx
  11. 0
    5
      server/sonar-web/src/main/js/apps/issues/components/ListItem.tsx
  12. 6
    6
      server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.tsx
  13. 4
    4
      server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx
  14. 58
    0
      server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AuthorFacet-test.tsx
  15. 26
    0
      server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AuthorFacet-test.tsx.snap
  16. 7
    20
      server/sonar-web/src/main/js/apps/issues/utils.ts
  17. 0
    1
      server/sonar-web/src/main/js/types/types.d.ts
  18. 2
    6
      server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueIndex.java
  19. 0
    1
      server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQueryFactory.java
  20. 4
    4
      server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexDebtTest.java
  21. 2
    16
      server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexFacetsTest.java
  22. 0
    2
      server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryFactoryTest.java
  23. 0
    1
      server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryTest.java
  24. 0
    35
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/component/ComponentService.java
  25. 8
    32
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchAction.java
  26. 0
    1
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/permission/ws/PermissionsWsModule.java
  27. 0
    123
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/permission/ws/SearchGlobalPermissionsAction.java
  28. 0
    115
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/permission/ws/SearchProjectPermissionsData.java
  29. 0
    241
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/project/ws/BulkUpdateKeyAction.java
  30. 0
    1
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/project/ws/ProjectsWsModule.java
  31. 0
    23
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/QualityGatesWs.java
  32. 0
    114
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/source/ws/HashAction.java
  33. 0
    1
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/source/ws/SourceWsModule.java
  34. 0
    19
      server/sonar-webserver-webapi/src/main/resources/org/sonar/server/project/ws/bulk_update_key-example.json
  35. 0
    76
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/component/ComponentServiceTest.java
  36. 0
    47
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/component/ComponentServiceUpdateKeyTest.java
  37. 0
    20
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchActionComponentsTest.java
  38. 1
    88
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchActionFacetsTest.java
  39. 2
    35
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchActionTest.java
  40. 1
    1
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/permission/ws/PermissionsWsModuleTest.java
  41. 0
    119
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/permission/ws/SearchGlobalPermissionsActionTest.java
  42. 0
    61
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/permission/ws/SearchProjectPermissionsDataTest.java
  43. 0
    294
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/project/ws/BulkUpdateKeyActionTest.java
  44. 1
    1
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/project/ws/ProjectsWsModuleTest.java
  45. 0
    110
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/source/ws/HashActionTest.java
  46. 1
    1
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/source/ws/SourceWsModuleTest.java
  47. 0
    10
      server/sonar-webserver/src/test/java/org/sonar/server/platform/web/WebServiceReroutingFilterTest.java
  48. 0
    101
      sonar-ws-generator/src/main/resources/snapshot-of-api.json
  49. 0
    14
      sonar-ws/src/main/java/org/sonarqube/ws/client/issue/IssuesWsParameters.java
  50. 0
    3
      sonar-ws/src/main/java/org/sonarqube/ws/client/issues/IssuesService.java
  51. 0
    47
      sonar-ws/src/main/java/org/sonarqube/ws/client/issues/SearchRequest.java
  52. 6
    44
      sonar-ws/src/main/java/org/sonarqube/ws/client/permissions/PermissionsService.java
  53. 0
    33
      sonar-ws/src/main/java/org/sonarqube/ws/client/permissions/SearchGlobalPermissionsRequest.java
  54. 0
    2
      sonar-ws/src/main/java/org/sonarqube/ws/client/project/ProjectsWsParameters.java
  55. 0
    94
      sonar-ws/src/main/java/org/sonarqube/ws/client/projects/BulkUpdateKeyRequest.java
  56. 0
    18
      sonar-ws/src/main/java/org/sonarqube/ws/client/projects/ProjectsService.java
  57. 0
    15
      sonar-ws/src/main/java/org/sonarqube/ws/client/qualitygates/QualitygatesService.java
  58. 0
    47
      sonar-ws/src/main/java/org/sonarqube/ws/client/qualitygates/UnsetDefaultRequest.java
  59. 0
    15
      sonar-ws/src/main/java/org/sonarqube/ws/client/sources/SourcesService.java
  60. 0
    19
      sonar-ws/src/main/java/org/sonarqube/ws/client/views/CreateRequest.java
  61. 0
    102
      sonar-ws/src/main/java/org/sonarqube/ws/client/views/ModeRequest.java
  62. 0
    59
      sonar-ws/src/main/java/org/sonarqube/ws/client/views/RegexpRequest.java
  63. 10
    10
      sonar-ws/src/main/java/org/sonarqube/ws/client/views/SetNoneModeRequest.java
  64. 15
    38
      sonar-ws/src/main/java/org/sonarqube/ws/client/views/ViewsService.java
  65. 0
    11
      sonar-ws/src/main/protobuf/ws-projects.proto

+ 0
- 105
server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentKeyUpdaterDao.java View File

package org.sonar.db.component; package org.sonar.db.component;


import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Lists;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.sonar.api.resources.Qualifiers; import org.sonar.api.resources.Qualifiers;
import org.sonar.db.audit.AuditPersister; import org.sonar.db.audit.AuditPersister;
import org.sonar.db.audit.model.ComponentKeyNewValue; import org.sonar.db.audit.model.ComponentKeyNewValue;


import static org.sonar.core.component.ComponentKeys.checkProjectKey;
import static org.sonar.db.component.ComponentDto.BRANCH_KEY_SEPARATOR; import static org.sonar.db.component.ComponentDto.BRANCH_KEY_SEPARATOR;
import static org.sonar.db.component.ComponentDto.generateBranchKey; import static org.sonar.db.component.ComponentDto.generateBranchKey;


} }
} }


/**
*
* @return a map with currentKey/newKey is a bulk update was executed
*/
public Map<String, String> simulateBulkUpdateKey(DbSession dbSession, String projectUuid, String stringToReplace, String replacementString) {
return collectAllModules(projectUuid, stringToReplace, mapper(dbSession), false)
.stream()
.collect(Collectors.toMap(
ResourceDto::getKey,
component -> {
String newKey = computeNewKey(component.getKey(), stringToReplace, replacementString);
checkProjectKey(newKey);
return newKey;
}));
}

/**
* @return a map with the component key as key, and boolean as true if key already exists in db
*/
public Map<String, Boolean> checkComponentKeys(DbSession dbSession, List<String> newComponentKeys) {
return newComponentKeys.stream().collect(Collectors.toMap(Function.identity(), key -> mapper(dbSession).countResourceByKey(key) > 0));
}

@VisibleForTesting @VisibleForTesting
static String computeNewKey(String key, String stringToReplace, String replacementString) { static String computeNewKey(String key, String stringToReplace, String replacementString) {
return key.replace(stringToReplace, replacementString); return key.replace(stringToReplace, replacementString);
} }


public Set<RekeyedResource> bulkUpdateKey(DbSession session, String projectUuid, String stringToReplace, String replacementString,
Predicate<RekeyedResource> rekeyedResourceFilter) {
ComponentKeyUpdaterMapper mapper = session.getMapper(ComponentKeyUpdaterMapper.class);
// must SELECT first everything
Set<ResourceDto> modules = collectAllModules(projectUuid, stringToReplace, mapper, true);
checkNewNameOfAllModules(modules, stringToReplace, replacementString, mapper);

// add branches (no check should be done as branch keys cannot be changed by the user)
Map<String, String> branchBaseKeys = new HashMap<>();
session.getMapper(BranchMapper.class).selectByProjectUuid(projectUuid).stream()
.filter(branch -> !projectUuid.equals(branch.getUuid()))
.forEach(branch -> {
Set<ResourceDto> branchModules = collectAllModules(branch.getUuid(), stringToReplace, mapper, true);
modules.addAll(branchModules);
branchModules.forEach(module -> branchBaseKeys.put(module.getKey(), branchBaseKey(module.getKey())));
});

Map<ResourceDto, List<ResourceDto>> allResourcesByModuleMap = new HashMap<>();
for (ResourceDto module : modules) {
allResourcesByModuleMap.put(module, mapper.selectProjectResources(module.getUuid()));
}

Set<RekeyedResource> rekeyedResources = new HashSet<>();
// and then proceed with the batch UPDATE at once
for (ResourceDto module : modules) {
String oldModuleKey = module.getKey();
oldModuleKey = branchBaseKeys.getOrDefault(oldModuleKey, oldModuleKey);
String newModuleKey = computeNewKey(oldModuleKey, stringToReplace, replacementString);
Collection<ResourceDto> resources = Lists.newArrayList(module);
resources.addAll(allResourcesByModuleMap.get(module));
runBatchUpdateForAllResources(resources, oldModuleKey, newModuleKey, mapper,
(resource, oldKey) -> {
RekeyedResource rekeyedResource = new RekeyedResource(resource, oldKey);
if (rekeyedResourceFilter.test(rekeyedResource)) {
rekeyedResources.add(rekeyedResource);
}
}, session);
}
return rekeyedResources;
}

private static String branchBaseKey(String key) {
int index = key.lastIndexOf(ComponentDto.BRANCH_KEY_SEPARATOR);
if (index > -1) {
return key.substring(0, index);
}
index = key.lastIndexOf(ComponentDto.PULL_REQUEST_SEPARATOR);
if (index > -1) {
return key.substring(0, index);
}
return key;
}

private void runBatchUpdateForAllResources(Collection<ResourceDto> resources, String oldKey, String newKey, ComponentKeyUpdaterMapper mapper, private void runBatchUpdateForAllResources(Collection<ResourceDto> resources, String oldKey, String newKey, ComponentKeyUpdaterMapper mapper,
@Nullable BiConsumer<ResourceDto, String> consumer, DbSession dbSession) { @Nullable BiConsumer<ResourceDto, String> consumer, DbSession dbSession) {
for (ResourceDto resource : resources) { for (ResourceDto resource : resources) {
} }
} }


private static Set<ResourceDto> collectAllModules(String projectUuid, String stringToReplace, ComponentKeyUpdaterMapper mapper, boolean includeDisabled) {
ResourceDto project = mapper.selectProjectByUuid(projectUuid);
Set<ResourceDto> modules = new HashSet<>();
if (project.getKey().contains(stringToReplace) && (project.isEnabled() || includeDisabled)) {
modules.add(project);
}
for (ResourceDto submodule : mapper.selectDescendantProjects(projectUuid)) {
modules.addAll(collectAllModules(submodule.getUuid(), stringToReplace, mapper, includeDisabled));
}
return modules;
}

private static void checkNewNameOfAllModules(Set<ResourceDto> modules, String stringToReplace, String replacementString, ComponentKeyUpdaterMapper mapper) {
for (ResourceDto module : modules) {
String newKey = computeNewKey(module.getKey(), stringToReplace, replacementString);
checkProjectKey(newKey);
checkExistentKey(mapper, newKey);
}
}

private static ComponentKeyUpdaterMapper mapper(DbSession dbSession) { private static ComponentKeyUpdaterMapper mapper(DbSession dbSession) {
return dbSession.getMapper(ComponentKeyUpdaterMapper.class); return dbSession.getMapper(ComponentKeyUpdaterMapper.class);
} }

+ 0
- 28
server/sonar-db-dao/src/main/java/org/sonar/db/source/FileSourceDao.java View File

package org.sonar.db.source; package org.sonar.db.source;


import com.google.common.base.Splitter; import com.google.common.base.Splitter;
import java.io.Reader;
import java.sql.Connection; import java.sql.Connection;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.function.Consumer;
import javax.annotation.CheckForNull; import javax.annotation.CheckForNull;
import org.apache.commons.io.IOUtils;
import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.ResultHandler;
import org.sonar.db.Dao; import org.sonar.db.Dao;
import org.sonar.db.DatabaseUtils; import org.sonar.db.DatabaseUtils;
} }
} }


public void readLineHashesStream(DbSession dbSession, String fileUuid, Consumer<Reader> consumer) {
Connection connection = dbSession.getConnection();
PreparedStatement pstmt = null;
ResultSet rs = null;
Reader reader = null;
try {
pstmt = connection.prepareStatement("SELECT line_hashes FROM file_sources WHERE file_uuid=?");
pstmt.setString(1, fileUuid);
rs = pstmt.executeQuery();
if (rs.next()) {
reader = rs.getCharacterStream(1);
if (reader != null) {
consumer.accept(reader);
}
}
} catch (SQLException e) {
throw new IllegalStateException("Fail to read FILE_SOURCES.LINE_HASHES of file " + fileUuid, e);
} finally {
IOUtils.closeQuietly(reader);
DatabaseUtils.closeQuietly(rs);
DatabaseUtils.closeQuietly(pstmt);
DatabaseUtils.closeQuietly(connection);
}
}

public void insert(DbSession session, FileSourceDto dto) { public void insert(DbSession session, FileSourceDto dto) {
mapper(session).insert(dto); mapper(session).insert(dto);
} }

+ 0
- 266
server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentKeyUpdaterDaoTest.java View File

import com.google.common.base.Strings; import com.google.common.base.Strings;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.function.Predicate; import java.util.function.Predicate;
import org.assertj.core.groups.Tuple; import org.assertj.core.groups.Tuple;
import org.junit.Rule; import org.junit.Rule;
import org.sonar.db.audit.model.ComponentKeyNewValue; import org.sonar.db.audit.model.ComponentKeyNewValue;
import org.sonar.db.component.ComponentKeyUpdaterDao.RekeyedResource; import org.sonar.db.component.ComponentKeyUpdaterDao.RekeyedResource;


import static com.google.common.collect.Lists.newArrayList;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
.forEach(map -> map.values().forEach(k -> assertThat(k.toString()).startsWith(newProjectKey))); .forEach(map -> map.values().forEach(k -> assertThat(k.toString()).startsWith(newProjectKey)));
} }


@Test
public void bulk_updateKey_updates_branches_too() {
ComponentDto project = db.components().insertPublicProject();
ComponentDto branch = db.components().insertProjectBranch(project);
ComponentDto module = db.components().insertComponent(prefixDbKeyWithKey(newModuleDto(branch), project.getKey()));
ComponentDto file1 = db.components().insertComponent(prefixDbKeyWithKey(newFileDto(module), module.getKey()));
ComponentDto file2 = db.components().insertComponent(prefixDbKeyWithKey(newFileDto(module), module.getKey()));
int branchComponentCount = 4;

String oldProjectKey = project.getKey();
assertThat(dbClient.componentDao().selectAllComponentsFromProjectKey(dbSession, oldProjectKey)).hasSize(1);

String oldBranchKey = branch.getDbKey();
assertThat(dbClient.componentDao().selectAllComponentsFromProjectKey(dbSession, oldBranchKey)).hasSize(branchComponentCount);

String newProjectKey = "newKey";
Set<RekeyedResource> rekeyedResources = underTest.bulkUpdateKey(dbSession, project.uuid(), oldProjectKey, newProjectKey, a -> true);

assertThat(dbClient.componentDao().selectAllComponentsFromProjectKey(dbSession, oldProjectKey)).isEmpty();
assertThat(dbClient.componentDao().selectAllComponentsFromProjectKey(dbSession, oldBranchKey)).isEmpty();

assertThat(dbClient.componentDao().selectAllComponentsFromProjectKey(dbSession, newProjectKey)).hasSize(1);
String newBranchKey = ComponentDto.generateBranchKey(newProjectKey, branch.getBranch());
assertThat(dbClient.componentDao().selectAllComponentsFromProjectKey(dbSession, newBranchKey)).hasSize(branchComponentCount);
db.select(dbSession, "select kee from components")
.forEach(map -> map.values().forEach(k -> assertThat(k.toString()).startsWith(newProjectKey)));

assertThat(rekeyedResources)
.extracting(t -> t.getResource().getUuid())
.containsOnly(project.uuid(), branch.uuid(), module.uuid(), file1.uuid(), file2.uuid());
assertThat(rekeyedResources)
.extracting(t -> t.getResource().getKey())
.allMatch(t -> t.startsWith(newProjectKey));
assertThat(rekeyedResources)
.extracting(RekeyedResource::getOldKey)
.allMatch(t -> t.startsWith(oldProjectKey));
}

@Test
public void bulk_updateKey_on_branch_containing_slash() {
ComponentDto project = db.components().insertPublicProject();
ComponentDto branch = db.components().insertProjectBranch(project, b -> b.setKey("branch/with/slash"));
String newKey = "newKey";

underTest.bulkUpdateKey(dbSession, project.uuid(), project.getKey(), newKey, t -> true);

assertThat(dbClient.componentDao().selectAllComponentsFromProjectKey(dbSession, newKey)).hasSize(1);
assertThat(dbClient.componentDao().selectAllComponentsFromProjectKey(dbSession, ComponentDto.generateBranchKey(newKey, branch.getBranch()))).hasSize(1);
}

@Test
public void bulk_updateKey_updates_pull_requests_too() {
ComponentDto project = db.components().insertPublicProject();
ComponentDto pullRequest = db.components().insertProjectBranch(project, b -> b.setBranchType(PULL_REQUEST));
ComponentDto module = db.components().insertComponent(prefixDbKeyWithKey(newModuleDto(pullRequest), project.getKey()));
ComponentDto file1 = db.components().insertComponent(prefixDbKeyWithKey(newFileDto(module), module.getKey()));
ComponentDto file2 = db.components().insertComponent(prefixDbKeyWithKey(newFileDto(module), module.getKey()));
int branchComponentCount = 4;

String oldProjectKey = project.getKey();
assertThat(dbClient.componentDao().selectAllComponentsFromProjectKey(dbSession, oldProjectKey)).hasSize(1);

String oldPullRequestKey = pullRequest.getDbKey();
assertThat(dbClient.componentDao().selectAllComponentsFromProjectKey(dbSession, oldPullRequestKey)).hasSize(branchComponentCount);

String newProjectKey = "newKey";
String newPullRequestKey = ComponentDto.generatePullRequestKey(newProjectKey, pullRequest.getPullRequest());
Set<RekeyedResource> rekeyedResources = underTest.bulkUpdateKey(dbSession, project.uuid(), oldProjectKey, newProjectKey, t -> true);

assertThat(dbClient.componentDao().selectAllComponentsFromProjectKey(dbSession, oldProjectKey)).isEmpty();
assertThat(dbClient.componentDao().selectAllComponentsFromProjectKey(dbSession, oldPullRequestKey)).isEmpty();

assertThat(dbClient.componentDao().selectAllComponentsFromProjectKey(dbSession, newProjectKey)).hasSize(1);
assertThat(dbClient.componentDao().selectAllComponentsFromProjectKey(dbSession, newPullRequestKey)).hasSize(branchComponentCount);
db.select(dbSession, "select kee from components")
.forEach(map -> map.values().forEach(k -> assertThat(k.toString()).startsWith(newProjectKey)));

assertThat(rekeyedResources)
.extracting(t -> t.getResource().getUuid())
.containsOnly(project.uuid(), pullRequest.uuid(), module.uuid(), file1.uuid(), file2.uuid());
assertThat(rekeyedResources)
.extracting(t -> t.getResource().getKey())
.allMatch(t -> t.startsWith(newProjectKey));
assertThat(rekeyedResources)
.extracting(RekeyedResource::getOldKey)
.allMatch(t -> t.startsWith(oldProjectKey));
}

private ComponentDto prefixDbKeyWithKey(ComponentDto componentDto, String key) { private ComponentDto prefixDbKeyWithKey(ComponentDto componentDto, String key) {
return componentDto.setDbKey(key + ":" + componentDto.getDbKey()); return componentDto.setDbKey(key + ":" + componentDto.getDbKey());
} }
underTest.updateKey(dbSession, "B", "org.struts:struts-ui"); underTest.updateKey(dbSession, "B", "org.struts:struts-ui");
} }


@Test
public void bulk_update_key_updates_disabled_components() {
ComponentDto project = db.components().insertComponent(newPrivateProjectDto("A").setDbKey("my_project"));
db.components().insertComponent(newModuleDto(project).setDbKey("my_project:module"));
db.components().insertComponent(newModuleDto(project).setDbKey("my_project:inactive_module").setEnabled(false));

Set<RekeyedResource> rekeyedResources = underTestWithAuditPersister.bulkUpdateKey(dbSession, "A", "my_", "your_", doNotReturnAnyRekeyedResource());

List<ComponentDto> result = dbClient.componentDao().selectAllComponentsFromProjectKey(dbSession, "your_project");
assertThat(result)
.hasSize(3)
.extracting(ComponentDto::getDbKey)
.containsOnlyOnce("your_project", "your_project:module", "your_project:inactive_module");
}

@Test
public void shouldBulkUpdateKey() {
populateSomeData();

underTest.bulkUpdateKey(dbSession, "A", "org.struts", "org.apache.struts", doNotReturnAnyRekeyedResource());
dbSession.commit();

assertThat(db.select("select uuid as \"UUID\", kee as \"KEE\" from components"))
.extracting(t -> t.get("UUID"), t -> t.get("KEE"))
.containsOnly(
Tuple.tuple("A", "org.apache.struts:struts"),
Tuple.tuple("B", "org.apache.struts:struts-core"),
Tuple.tuple("C", "org.apache.struts:struts-core:/src/org/struts"),
Tuple.tuple("D", "org.apache.struts:struts-core:/src/org/struts/RequestContext.java"),
Tuple.tuple("E", "org.apache.struts:struts-ui"),
Tuple.tuple("F", "org.apache.struts:struts-ui:/src/org/struts"),
Tuple.tuple("G", "org.apache.struts:struts-ui:/src/org/struts/RequestContext.java"),
Tuple.tuple("H", "foo:struts-core"));
}

@Test
public void shouldBulkUpdateKeyOnOnlyOneSubmodule() {
populateSomeData();

underTest.bulkUpdateKey(dbSession, "A", "struts-ui", "struts-web", doNotReturnAnyRekeyedResource());
dbSession.commit();

assertThat(db.select("select uuid as \"UUID\", kee as \"KEE\" from components"))
.extracting(t -> t.get("UUID"), t -> t.get("KEE"))
.containsOnly(
Tuple.tuple("A", "org.struts:struts"),
Tuple.tuple("B", "org.struts:struts-core"),
Tuple.tuple("C", "org.struts:struts-core:/src/org/struts"),
Tuple.tuple("D", "org.struts:struts-core:/src/org/struts/RequestContext.java"),
Tuple.tuple("E", "org.struts:struts-web"),
Tuple.tuple("F", "org.struts:struts-web:/src/org/struts"),
Tuple.tuple("G", "org.struts:struts-web:/src/org/struts/RequestContext.java"),
Tuple.tuple("H", "foo:struts-core"));
}

@Test
public void shouldFailBulkUpdateKeyIfKeyAlreadyExist() {
populateSomeData();

thrown.expect(IllegalArgumentException.class);
thrown.expectMessage("Impossible to update key: a component with key \"foo:struts-core\" already exists.");

underTest.bulkUpdateKey(dbSession, "A", "org.struts", "foo", doNotReturnAnyRekeyedResource());
dbSession.commit();
}

@Test
public void shouldNotUpdateAllSubmodules() {
ComponentDto project1 = db.components().insertPrivateProject(t1 -> t1.setDbKey("org.struts:struts").setUuid("A"));
ComponentDto module1 = db.components().insertComponent(newModuleDto(project1).setDbKey("org.struts:struts-core").setUuid("B"));
ComponentDto directory1 = db.components().insertComponent(newDirectory(module1, "/src/org/struts").setUuid("C"));
db.components().insertComponent(ComponentTesting.newFileDto(module1, directory1).setDbKey("org.struts:struts-core:/src/org/struts/RequestContext.java").setUuid("D"));
ComponentDto module2 = db.components().insertComponent(newModuleDto(project1).setDbKey("foo:struts-ui").setUuid("E"));
ComponentDto directory2 = db.components().insertComponent(newDirectory(module2, "/src/org/struts").setUuid("F"));
db.components().insertComponent(ComponentTesting.newFileDto(module2, directory2).setDbKey("foo:struts-ui:/src/org/struts/RequestContext.java").setUuid("G"));
ComponentDto project2 = db.components().insertPublicProject(t1 -> t1.setDbKey("foo:struts-core").setUuid("H"));

underTest.bulkUpdateKey(dbSession, "A", "org.struts", "org.apache.struts", doNotReturnAnyRekeyedResource());
dbSession.commit();

assertThat(db.select("select uuid as \"UUID\", kee as \"KEE\" from components"))
.extracting(t -> t.get("UUID"), t -> t.get("KEE"))
.containsOnly(
Tuple.tuple("A", "org.apache.struts:struts"),
Tuple.tuple("B", "org.apache.struts:struts-core"),
Tuple.tuple("C", "org.apache.struts:struts-core:/src/org/struts"),
Tuple.tuple("D", "org.apache.struts:struts-core:/src/org/struts/RequestContext.java"),
Tuple.tuple("E", "foo:struts-ui"),
Tuple.tuple("F", "foo:struts-ui:/src/org/struts"),
Tuple.tuple("G", "foo:struts-ui:/src/org/struts/RequestContext.java"),
Tuple.tuple("H", "foo:struts-core"));
}

@Test @Test
public void updateKey_throws_IAE_when_sub_component_key_is_too_long() { public void updateKey_throws_IAE_when_sub_component_key_is_too_long() {
ComponentDto project = newPrivateProjectDto("project-uuid").setDbKey("old-project-key"); ComponentDto project = newPrivateProjectDto("project-uuid").setDbKey("old-project-key");
underTest.updateKey(dbSession, project.uuid(), newLongProjectKey); underTest.updateKey(dbSession, project.uuid(), newLongProjectKey);
} }


@Test
public void fail_when_new_key_is_invalid() {
ComponentDto project = db.components().insertPrivateProject();

thrown.expect(IllegalArgumentException.class);
thrown.expectMessage("Malformed key for 'my?project?key'. Allowed characters are alphanumeric, '-', '_', '.' and ':', with at least one non-digit.");

underTest.bulkUpdateKey(dbSession, project.uuid(), project.getDbKey(), "my?project?key", doNotReturnAnyRekeyedResource());
}

@Test
public void check_component_keys() {
populateSomeData();

Map<String, Boolean> result = underTest.checkComponentKeys(dbSession, newArrayList("foo:struts", "foo:struts-core", "foo:struts-ui"));

assertThat(result)
.hasSize(3)
.containsOnly(entry("foo:struts", false), entry("foo:struts-core", true), entry("foo:struts-ui", false));
}

@Test
public void check_component_keys_checks_inactive_components() {
db.components().insertComponent(ComponentTesting.newPrivateProjectDto().setDbKey("my-project"));
db.components().insertComponent(ComponentTesting.newPrivateProjectDto().setDbKey("your-project").setEnabled(false));

Map<String, Boolean> result = underTest.checkComponentKeys(dbSession, newArrayList("my-project", "your-project", "new-project"));

assertThat(result)
.hasSize(3)
.containsOnly(entry("my-project", true), entry("your-project", true), entry("new-project", false));
}

@Test
public void simulate_bulk_update_key() {
populateSomeData();

Map<String, String> result = underTest.simulateBulkUpdateKey(dbSession, "A", "org.struts", "foo");

assertThat(result)
.hasSize(3)
.containsOnly(entry("org.struts:struts", "foo:struts"), entry("org.struts:struts-core", "foo:struts-core"), entry("org.struts:struts-ui", "foo:struts-ui"));
}

@Test
public void simulate_bulk_update_key_does_not_return_disable_components() {
ComponentDto project = db.components().insertComponent(newPrivateProjectDto("A").setDbKey("project"));
db.components().insertComponent(newModuleDto(project).setDbKey("project:enabled-module"));
db.components().insertComponent(newModuleDto(project).setDbKey("project:disabled-module").setEnabled(false));
db.components().insertComponent(newPrivateProjectDto("D").setDbKey("other-project"));

Map<String, String> result = underTest.simulateBulkUpdateKey(dbSession, "A", "project", "new-project");

assertThat(result).containsOnly(
entry("project", "new-project"),
entry("project:enabled-module", "new-project:enabled-module"));
}

@Test
public void simulate_bulk_update_key_fails_if_invalid_componentKey() {
ComponentDto project = db.components().insertComponent(newPrivateProjectDto("A").setDbKey("project"));
db.components().insertComponent(newModuleDto(project).setDbKey("project:enabled-module"));
db.components().insertComponent(newModuleDto(project).setDbKey("project:disabled-module").setEnabled(false));

thrown.expect(IllegalArgumentException.class);

underTest.simulateBulkUpdateKey(dbSession, "A", "project", "project?");
}

@Test @Test
public void compute_new_key() { public void compute_new_key() {
assertThat(computeNewKey("my_project", "my_", "your_")).isEqualTo("your_project"); assertThat(computeNewKey("my_project", "my_", "your_")).isEqualTo("your_project");
.componentKeyUpdate(any(DbSession.class), any(ComponentKeyNewValue.class), anyString()); .componentKeyUpdate(any(DbSession.class), any(ComponentKeyNewValue.class), anyString());
} }


@Test
public void bulkUpdate_callsAuditPersister() {
ComponentDto project = db.components().insertComponent(newPrivateProjectDto("A").setDbKey("project"));
db.components().insertComponent(newModuleDto(project).setDbKey("project:enabled-module"));
db.components().insertComponent(newModuleDto(project).setDbKey("project:disabled-module").setEnabled(false));

thrown.expect(IllegalArgumentException.class);

underTest.simulateBulkUpdateKey(dbSession, "A", "project", "project?");

verify(auditPersister, times(1))
.componentKeyUpdate(any(DbSession.class), any(ComponentKeyNewValue.class), anyString());
}


private Predicate<RekeyedResource> doNotReturnAnyRekeyedResource() { private Predicate<RekeyedResource> doNotReturnAnyRekeyedResource() {
return a -> false; return a -> false;

+ 0
- 68
server/sonar-db-dao/src/test/java/org/sonar/db/source/FileSourceDaoTest.java View File



import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Random; import java.util.Random;
import java.util.function.Consumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.IntStream; import java.util.stream.IntStream;
import javax.annotation.Nullable;
import org.apache.commons.io.IOUtils;
import org.apache.ibatis.session.ResultContext; import org.apache.ibatis.session.ResultContext;
import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.ResultHandler;
import org.junit.Rule; import org.junit.Rule;
assertThat(fileSourceDto.getLineHashes()).isEqualTo(expected.getLineHashes()); assertThat(fileSourceDto.getLineHashes()).isEqualTo(expected.getLineHashes());
} }


@Test
public void select_line_hashes() {
ComponentDto project = dbTester.components().insertPrivateProject();
ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
FileSourceDto expected = dbTester.fileSources().insertFileSource(file);

ReaderToStringConsumer fn = new ReaderToStringConsumer();
underTest.readLineHashesStream(dbSession, expected.getFileUuid(), fn);

assertThat(fn.result).isEqualTo(expected.getLineHashes().isEmpty() ? null : String.join("\n", expected.getLineHashes()));
}

@Test
public void no_line_hashes_on_unknown_file() {
ComponentDto project = dbTester.components().insertPrivateProject();
ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
dbTester.fileSources().insertFileSource(file);

ReaderToStringConsumer fn = new ReaderToStringConsumer();
underTest.readLineHashesStream(dbSession, "unknown", fn);

assertThat(fn.result).isNull();
}

@Test @Test
public void insert() { public void insert() {
FileSourceDto expected = new FileSourceDto() FileSourceDto expected = new FileSourceDto()
assertThat(underTest.selectLineHashesVersion(dbSession, "FILE2_UUID")).isEqualTo(LineHashVersion.WITH_SIGNIFICANT_CODE); assertThat(underTest.selectLineHashesVersion(dbSession, "FILE2_UUID")).isEqualTo(LineHashVersion.WITH_SIGNIFICANT_CODE);
} }


@Test
public void readLineHashesStream_does_not_fail_when_lineshashes_is_null() {
underTest.insert(dbSession, new FileSourceDto()
.setUuid(Uuids.createFast())
.setProjectUuid("PRJ_UUID")
.setFileUuid("FILE2_UUID")
.setBinaryData("FILE2_BINARY_DATA".getBytes())
.setDataHash("FILE2_DATA_HASH")
.setSrcHash("FILE2_HASH")
.setCreatedAt(1500000000000L)
.setUpdatedAt(1500000000001L)
.setRevision("123456789"));
dbSession.commit();

boolean[] flag = {false};
underTest.readLineHashesStream(dbSession, "FILE2_UUID", new Consumer<Reader>() {
@Override
public void accept(@Nullable Reader input) {
fail("function must never been called since there is no data to read");
flag[0] = true;
}
});
assertThat(flag[0]).isFalse();
}

@Test @Test
public void scrollLineHashes_has_no_effect_if_no_uuids() { public void scrollLineHashes_has_no_effect_if_no_uuids() {
underTest.scrollLineHashes(dbSession, emptySet(), resultContext -> fail("handler should not be called")); underTest.scrollLineHashes(dbSession, emptySet(), resultContext -> fail("handler should not be called"));
assertThat(res.getLineHashes()).isEmpty(); assertThat(res.getLineHashes()).isEmpty();
assertThat(res.getLineCount()).isEqualTo(1); assertThat(res.getLineCount()).isEqualTo(1);
} }

private static class ReaderToStringConsumer implements Consumer<Reader> {

String result = null;

@Override
public void accept(Reader input) {
try {
result = IOUtils.toString(input);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
} }

+ 0
- 1
server/sonar-docs/src/pages/instance-administration/security.md View File

| * api/system/status | * api/system/status
| * api/system/upgrades | * api/system/upgrades
| * api/users/search | * api/users/search
| * api/views/run
| * api/webservices/list | * api/webservices/list
| * api/webservices/response_example | * api/webservices/response_example



+ 4
- 1
server/sonar-docs/src/pages/setup/upgrade-notes.md View File



## Release 9.1 Upgrade Notes ## Release 9.1 Upgrade Notes
**Custom measures feature has been dropped** **Custom measures feature has been dropped**
The custom measures feature, which was previously deprecated, has been removed. ([SONAR-10762)[https://jira.sonarsource.com/browse/SONAR-10762]).
The custom measures feature, which was previously deprecated, has been removed. ([SONAR-10762](https://jira.sonarsource.com/browse/SONAR-10762)).

**Deprecated WebAPI endpoints and parameters removal**
The WebAPI endpoints and parameters deprecated during the 7.X release cycle have been removed. For a complete list of removed endpoints and parameters see [SONAR-15313](https://jira.sonarsource.com/browse/SONAR-15313).


## Release 9.0 Upgrade Notes ## Release 9.0 Upgrade Notes
**Scanners require Java 11** **Scanners require Java 11**

+ 0
- 11
server/sonar-server-common/src/main/java/org/sonar/server/issue/SearchRequest.java View File

private List<String> issues; private List<String> issues;
private Set<String> scopes; private Set<String> scopes;
private List<String> languages; private List<String> languages;
private List<String> moduleUuids;
private Boolean onComponentOnly; private Boolean onComponentOnly;
private String branch; private String branch;
private String pullRequest; private String pullRequest;
return this; return this;
} }


@CheckForNull
public List<String> getModuleUuids() {
return moduleUuids;
}

public SearchRequest setModuleUuids(@Nullable List<String> moduleUuids) {
this.moduleUuids = moduleUuids;
return this;
}

@CheckForNull @CheckForNull
public Boolean getOnComponentOnly() { public Boolean getOnComponentOnly() {
return onComponentOnly; return onComponentOnly;

+ 0
- 2
server/sonar-server-common/src/test/java/org/sonar/server/issue/SearchRequestTest.java View File

.setResolutions(singletonList("FALSE-POSITIVE")) .setResolutions(singletonList("FALSE-POSITIVE"))
.setResolved(true) .setResolved(true)
.setProjects(singletonList("project-a")) .setProjects(singletonList("project-a"))
.setModuleUuids(singletonList("module-a"))
.setDirectories(singletonList("aDirPath")) .setDirectories(singletonList("aDirPath"))
.setFiles(asList("file-a", "file-b")) .setFiles(asList("file-a", "file-b"))
.setAssigneesUuid(asList("user-a", "user-b")) .setAssigneesUuid(asList("user-a", "user-b"))
assertThat(underTest.getResolutions()).containsExactly("FALSE-POSITIVE"); assertThat(underTest.getResolutions()).containsExactly("FALSE-POSITIVE");
assertThat(underTest.getResolved()).isTrue(); assertThat(underTest.getResolved()).isTrue();
assertThat(underTest.getProjects()).containsExactly("project-a"); assertThat(underTest.getProjects()).containsExactly("project-a");
assertThat(underTest.getModuleUuids()).containsExactly("module-a");
assertThat(underTest.getDirectories()).containsExactly("aDirPath"); assertThat(underTest.getDirectories()).containsExactly("aDirPath");
assertThat(underTest.getFiles()).containsExactly("file-a", "file-b"); assertThat(underTest.getFiles()).containsExactly("file-a", "file-b");
assertThat(underTest.getAssigneeUuids()).containsExactly("user-a", "user-b"); assertThat(underTest.getAssigneeUuids()).containsExactly("user-a", "user-b");

+ 1
- 2
server/sonar-web/src/main/js/api/issues.ts View File

type FacetName = type FacetName =
| 'assigned_to_me' | 'assigned_to_me'
| 'assignees' | 'assignees'
| 'authors'
| 'author'
| 'createdAt' | 'createdAt'
| 'cwe' | 'cwe'
| 'directories' | 'directories'
| 'files' | 'files'
| 'languages' | 'languages'
| 'modules'
| 'owaspTop10' | 'owaspTop10'
| 'projects' | 'projects'
| 'reporters' | 'reporters'

+ 2
- 4
server/sonar-web/src/main/js/apps/issues/components/App.tsx View File

areMyIssuesSelected, areMyIssuesSelected,
areQueriesEqual, areQueriesEqual,
getOpen, getOpen,
mapFacet,
parseFacets, parseFacets,
parseQuery, parseQuery,
Query, Query,
const facets = requestFacets const facets = requestFacets
? Object.keys(openFacets) ? Object.keys(openFacets)
.filter(facet => facet !== STANDARDS) .filter(facet => facet !== STANDARDS)
.map(mapFacet)
.join(',') .join(',')
: undefined; : undefined;


}; };


fetchFacet = (facet: string) => { fetchFacet = (facet: string) => {
return this.fetchIssues({ ps: 1, facets: mapFacet(facet) }, false).then(
return this.fetchIssues({ ps: 1, facets: facet }, false).then(
({ facets, ...other }) => { ({ facets, ...other }) => {
if (this.mounted) { if (this.mounted) {
this.setState(state => ({ this.setState(state => ({
const parameters = { const parameters = {
...getBranchLikeQuery(this.props.branchLike), ...getBranchLikeQuery(this.props.branchLike),
componentKeys: component && component.key, componentKeys: component && component.key,
facets: mapFacet(property),
facets: property,
s: 'FILE_LINE', s: 'FILE_LINE',
...serializeQuery({ ...query, ...changes }), ...serializeQuery({ ...query, ...changes }),
ps: 1 ps: 1

+ 0
- 5
server/sonar-web/src/main/js/apps/issues/components/ListItem.tsx View File

case 'project': case 'project':
onFilterChange({ ...issuesReset, projects: [issue.projectKey] }); onFilterChange({ ...issuesReset, projects: [issue.projectKey] });
break; break;
case 'module':
if (issue.subProjectUuid) {
onFilterChange({ ...issuesReset, modules: [issue.subProjectUuid] });
}
break;
case 'file': case 'file':
onFilterChange({ ...issuesReset, files: [issue.componentUuid] }); onFilterChange({ ...issuesReset, files: [issue.componentUuid] });
} }

+ 6
- 6
server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.tsx View File

open: boolean; open: boolean;
query: Query; query: Query;
stats: T.Dict<number> | undefined; stats: T.Dict<number> | undefined;
authors: string[];
author: string[];
} }


const SEARCH_SIZE = 100; const SEARCH_SIZE = 100;
}).then(authors => ({ maxResults: authors.length === SEARCH_SIZE, results: authors })); }).then(authors => ({ maxResults: authors.length === SEARCH_SIZE, results: authors }));
}; };


loadSearchResultCount = (authors: string[]) => {
return this.props.loadSearchResultCount('authors', { authors });
loadSearchResultCount = (author: string[]) => {
return this.props.loadSearchResultCount('author', { author });
}; };


renderSearchResult = (author: string, term: string) => { renderSearchResult = (author: string, term: string) => {
onSearch={this.handleSearch} onSearch={this.handleSearch}
onToggle={this.props.onToggle} onToggle={this.props.onToggle}
open={this.props.open} open={this.props.open}
property="authors"
query={omit(this.props.query, 'authors')}
property="author"
query={omit(this.props.query, 'author')}
renderFacetItem={this.identity} renderFacetItem={this.identity}
renderSearchResult={this.renderSearchResult} renderSearchResult={this.renderSearchResult}
searchPlaceholder={translate('search.search_for_authors')} searchPlaceholder={translate('search.search_for_authors')}
stats={this.props.stats} stats={this.props.stats}
values={this.props.authors}
values={this.props.author}
/> />
); );
} }

+ 4
- 4
server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx View File

)} )}
{displayAuthorFacet && !this.props.disableDeveloperAggregatedInfo && ( {displayAuthorFacet && !this.props.disableDeveloperAggregatedInfo && (
<AuthorFacet <AuthorFacet
authors={query.authors}
author={query.author}
component={component} component={component}
fetching={this.props.loadingFacets.authors === true}
fetching={this.props.loadingFacets.author === true}
loadSearchResultCount={this.props.loadSearchResultCount} loadSearchResultCount={this.props.loadSearchResultCount}
onChange={this.props.onFilterChange} onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle} onToggle={this.props.onFacetToggle}
open={!!openFacets.authors}
open={!!openFacets.author}
query={query} query={query}
stats={facets.authors}
stats={facets.author}
/> />
)} )}
</> </>

+ 58
- 0
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AuthorFacet-test.tsx View File

/*
* SonarQube
* Copyright (C) 2009-2021 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.
*/

import { shallow } from 'enzyme';
import * as React from 'react';
import ListStyleFacet from '../../../../components/facet/ListStyleFacet';
import { mockComponent } from '../../../../helpers/testMocks';
import { Query } from '../../utils';
import AuthorFacet from '../AuthorFacet';

it('should render correctly', () => {
const wrapper = shallowRender();
expect(wrapper).toMatchSnapshot();
});

it('should notify of search result count correctly', () => {
const loadSearchResultCount = jest.fn();

const wrapper = shallowRender({ loadSearchResultCount });

wrapper.find(ListStyleFacet).props().loadSearchResultCount!(['1', '2']);

expect(loadSearchResultCount).toHaveBeenCalled();
});

function shallowRender(props: Partial<AuthorFacet['props']> = {}) {
return shallow<AuthorFacet>(
<AuthorFacet
component={mockComponent()}
fetching={false}
loadSearchResultCount={jest.fn()}
onChange={jest.fn()}
onToggle={jest.fn()}
open={true}
query={{} as Query}
stats={{}}
author={[]}
{...props}
/>
);
}

+ 26
- 0
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AuthorFacet-test.tsx.snap View File

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly 1`] = `
<ListStyleFacet
facetHeader="issues.facet.authors"
fetching={false}
getFacetItemText={[Function]}
getSearchResultKey={[Function]}
getSearchResultText={[Function]}
loadSearchResultCount={[Function]}
maxInitialItems={15}
maxItems={100}
minSearchLength={2}
onChange={[MockFunction]}
onSearch={[Function]}
onToggle={[MockFunction]}
open={true}
property="author"
query={Object {}}
renderFacetItem={[Function]}
renderSearchResult={[Function]}
searchPlaceholder="search.search_for_authors"
stats={Object {}}
values={Array []}
/>
`;

+ 7
- 20
server/sonar-web/src/main/js/apps/issues/utils.ts View File

* along with this program; if not, write to the Free Software Foundation, * along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/ */
import { isArray } from 'lodash';
import { searchUsers } from '../../api/users'; import { searchUsers } from '../../api/users';
import { formatMeasure } from '../../helpers/measures'; import { formatMeasure } from '../../helpers/measures';
import { import {
} from '../../helpers/query'; } from '../../helpers/query';
import { scrollToElement } from '../../helpers/scrolling'; import { scrollToElement } from '../../helpers/scrolling';
import { get, save } from '../../helpers/storage'; import { get, save } from '../../helpers/storage';
import { isDefined } from '../../helpers/types';
import { Facet, RawFacet } from '../../types/issues'; import { Facet, RawFacet } from '../../types/issues';
import { SecurityStandard, StandardType } from '../../types/security'; import { SecurityStandard, StandardType } from '../../types/security';


export interface Query { export interface Query {
assigned: boolean; assigned: boolean;
assignees: string[]; assignees: string[];
authors: string[];
author: string[];
createdAfter: Date | undefined; createdAfter: Date | undefined;
createdAt: string; createdAt: string;
createdBefore: Date | undefined; createdBefore: Date | undefined;
files: string[]; files: string[];
issues: string[]; issues: string[];
languages: string[]; languages: string[];
modules: string[];
owaspTop10: string[]; owaspTop10: string[];
projects: string[]; projects: string[];
resolutions: string[]; resolutions: string[];
return { return {
assigned: parseAsBoolean(query.assigned), assigned: parseAsBoolean(query.assigned),
assignees: parseAsArray(query.assignees, parseAsString), assignees: parseAsArray(query.assignees, parseAsString),
authors: parseAsArray(query.authors, parseAsString),
author: isArray(query.author) ? query.author : [query.author].filter(isDefined),
createdAfter: parseAsDate(query.createdAfter), createdAfter: parseAsDate(query.createdAfter),
createdAt: parseAsString(query.createdAt), createdAt: parseAsString(query.createdAt),
createdBefore: parseAsDate(query.createdBefore), createdBefore: parseAsDate(query.createdBefore),
files: parseAsArray(query.files, parseAsString), files: parseAsArray(query.files, parseAsString),
issues: parseAsArray(query.issues, parseAsString), issues: parseAsArray(query.issues, parseAsString),
languages: parseAsArray(query.languages, parseAsString), languages: parseAsArray(query.languages, parseAsString),
modules: parseAsArray(query.moduleUuids, parseAsString),
owaspTop10: parseAsArray(query.owaspTop10, parseAsString), owaspTop10: parseAsArray(query.owaspTop10, parseAsString),
projects: parseAsArray(query.projects, parseAsString), projects: parseAsArray(query.projects, parseAsString),
resolutions: parseAsArray(query.resolutions, parseAsString), resolutions: parseAsArray(query.resolutions, parseAsString),
const filter = { const filter = {
assigned: query.assigned ? undefined : 'false', assigned: query.assigned ? undefined : 'false',
assignees: serializeStringArray(query.assignees), assignees: serializeStringArray(query.assignees),
authors: serializeStringArray(query.authors),
author: query.author,
createdAfter: serializeDateShort(query.createdAfter), createdAfter: serializeDateShort(query.createdAfter),
createdAt: serializeString(query.createdAt), createdAt: serializeString(query.createdAt),
createdBefore: serializeDateShort(query.createdBefore), createdBefore: serializeDateShort(query.createdBefore),
files: serializeStringArray(query.files), files: serializeStringArray(query.files),
issues: serializeStringArray(query.issues), issues: serializeStringArray(query.issues),
languages: serializeStringArray(query.languages), languages: serializeStringArray(query.languages),
moduleUuids: serializeStringArray(query.modules),
owaspTop10: serializeStringArray(query.owaspTop10), owaspTop10: serializeStringArray(query.owaspTop10),
projects: serializeStringArray(query.projects), projects: serializeStringArray(query.projects),
resolutions: serializeStringArray(query.resolutions), resolutions: serializeStringArray(query.resolutions),
tags: serializeStringArray(query.tags), tags: serializeStringArray(query.tags),
types: serializeStringArray(query.types) types: serializeStringArray(query.types)
}; };

return cleanQuery(filter); return cleanQuery(filter);
} }


export const areQueriesEqual = (a: T.RawQuery, b: T.RawQuery) => export const areQueriesEqual = (a: T.RawQuery, b: T.RawQuery) =>
queriesEqual(parseQuery(a), parseQuery(b)); queriesEqual(parseQuery(a), parseQuery(b));


export function mapFacet(facet: string) {
const propertyMapping: T.Dict<string> = {
modules: 'moduleUuids'
};
return propertyMapping[facet] || facet;
}

export function parseFacets(facets: RawFacet[]): T.Dict<Facet> { export function parseFacets(facets: RawFacet[]): T.Dict<Facet> {
if (!facets) { if (!facets) {
return {}; return {};
} }


// for readability purpose
const propertyMapping: T.Dict<string> = {
moduleUuids: 'modules'
};

const result: T.Dict<Facet> = {}; const result: T.Dict<Facet> = {};
facets.forEach(facet => { facets.forEach(facet => {
const values: Facet = {}; const values: Facet = {};
facet.values.forEach(value => { facet.values.forEach(value => {
values[value.val] = value.count; values[value.val] = value.count;
}); });
const finalProperty = propertyMapping[facet.property] || facet.property;
result[finalProperty] = values;
result[facet.property] = values;
}); });
return result; return result;
} }

+ 0
- 1
server/sonar-web/src/main/js/types/types.d.ts View File

status: string; status: string;
subProject?: string; subProject?: string;
subProjectName?: string; subProjectName?: string;
subProjectUuid?: string;
tags?: string[]; tags?: string[];
textRange?: TextRange; textRange?: TextRange;
transitions: string[]; transitions: string[];

+ 2
- 6
server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueIndex.java View File

import static org.sonar.server.issue.index.IssueIndex.Facet.ASSIGNED_TO_ME; import static org.sonar.server.issue.index.IssueIndex.Facet.ASSIGNED_TO_ME;
import static org.sonar.server.issue.index.IssueIndex.Facet.ASSIGNEES; import static org.sonar.server.issue.index.IssueIndex.Facet.ASSIGNEES;
import static org.sonar.server.issue.index.IssueIndex.Facet.AUTHOR; import static org.sonar.server.issue.index.IssueIndex.Facet.AUTHOR;
import static org.sonar.server.issue.index.IssueIndex.Facet.AUTHORS;
import static org.sonar.server.issue.index.IssueIndex.Facet.CREATED_AT; import static org.sonar.server.issue.index.IssueIndex.Facet.CREATED_AT;
import static org.sonar.server.issue.index.IssueIndex.Facet.CWE; import static org.sonar.server.issue.index.IssueIndex.Facet.CWE;
import static org.sonar.server.issue.index.IssueIndex.Facet.DIRECTORIES; import static org.sonar.server.issue.index.IssueIndex.Facet.DIRECTORIES;
import static org.sonar.server.security.SecurityStandards.SANS_TOP_25_POROUS_DEFENSES; import static org.sonar.server.security.SecurityStandards.SANS_TOP_25_POROUS_DEFENSES;
import static org.sonar.server.security.SecurityStandards.SANS_TOP_25_RISKY_RESOURCE; import static org.sonar.server.security.SecurityStandards.SANS_TOP_25_RISKY_RESOURCE;
import static org.sonar.server.view.index.ViewIndexDefinition.TYPE_VIEW; import static org.sonar.server.view.index.ViewIndexDefinition.TYPE_VIEW;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.DEPRECATED_PARAM_AUTHORS;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.FACET_MODE_EFFORT; import static org.sonarqube.ws.client.issue.IssuesWsParameters.FACET_MODE_EFFORT;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ASSIGNEES; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ASSIGNEES;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_AUTHOR; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_AUTHOR;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_DIRECTORIES; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_DIRECTORIES;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_FILES; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_FILES;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_LANGUAGES; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_LANGUAGES;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_MODULE_UUIDS;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_OWASP_TOP_10; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_OWASP_TOP_10;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_RESOLUTIONS; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_RESOLUTIONS;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_RULES; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_RULES;


public static final String FACET_PROJECTS = "projects"; public static final String FACET_PROJECTS = "projects";
public static final String FACET_ASSIGNED_TO_ME = "assigned_to_me"; public static final String FACET_ASSIGNED_TO_ME = "assigned_to_me";
public static final String FACET_MODULES = "moduleUuids";


private static final int DEFAULT_FACET_SIZE = 15; private static final int DEFAULT_FACET_SIZE = 15;
private static final int MAX_FACET_SIZE = 100; private static final int MAX_FACET_SIZE = 100;
LANGUAGES(PARAM_LANGUAGES, FIELD_ISSUE_LANGUAGE, STICKY, MAX_FACET_SIZE), LANGUAGES(PARAM_LANGUAGES, FIELD_ISSUE_LANGUAGE, STICKY, MAX_FACET_SIZE),
RULES(PARAM_RULES, FIELD_ISSUE_RULE_UUID, STICKY, MAX_FACET_SIZE), RULES(PARAM_RULES, FIELD_ISSUE_RULE_UUID, STICKY, MAX_FACET_SIZE),
TAGS(PARAM_TAGS, FIELD_ISSUE_TAGS, STICKY, MAX_FACET_SIZE), TAGS(PARAM_TAGS, FIELD_ISSUE_TAGS, STICKY, MAX_FACET_SIZE),
AUTHORS(DEPRECATED_PARAM_AUTHORS, FIELD_ISSUE_AUTHOR_LOGIN, STICKY, MAX_FACET_SIZE),
AUTHOR(PARAM_AUTHOR, FIELD_ISSUE_AUTHOR_LOGIN, STICKY, MAX_FACET_SIZE), AUTHOR(PARAM_AUTHOR, FIELD_ISSUE_AUTHOR_LOGIN, STICKY, MAX_FACET_SIZE),
PROJECT_UUIDS(FACET_PROJECTS, FIELD_ISSUE_PROJECT_UUID, STICKY, MAX_FACET_SIZE), PROJECT_UUIDS(FACET_PROJECTS, FIELD_ISSUE_PROJECT_UUID, STICKY, MAX_FACET_SIZE),
MODULE_UUIDS(PARAM_MODULE_UUIDS, FIELD_ISSUE_MODULE_UUID, STICKY, MAX_FACET_SIZE),
MODULE_UUIDS(FACET_MODULES, FIELD_ISSUE_MODULE_UUID, STICKY, MAX_FACET_SIZE),
FILES(PARAM_FILES, FIELD_ISSUE_FILE_PATH, STICKY, MAX_FACET_SIZE), FILES(PARAM_FILES, FIELD_ISSUE_FILE_PATH, STICKY, MAX_FACET_SIZE),
DIRECTORIES(PARAM_DIRECTORIES, FIELD_ISSUE_DIRECTORY_PATH, STICKY, MAX_FACET_SIZE), DIRECTORIES(PARAM_DIRECTORIES, FIELD_ISSUE_DIRECTORY_PATH, STICKY, MAX_FACET_SIZE),
ASSIGNEES(PARAM_ASSIGNEES, FIELD_ISSUE_ASSIGNEE_UUID, STICKY, MAX_FACET_SIZE), ASSIGNEES(PARAM_ASSIGNEES, FIELD_ISSUE_ASSIGNEE_UUID, STICKY, MAX_FACET_SIZE),
addFacetIfNeeded(options, aggregationHelper, esRequest, SCOPES, query.scopes().toArray()); addFacetIfNeeded(options, aggregationHelper, esRequest, SCOPES, query.scopes().toArray());
addFacetIfNeeded(options, aggregationHelper, esRequest, LANGUAGES, query.languages().toArray()); addFacetIfNeeded(options, aggregationHelper, esRequest, LANGUAGES, query.languages().toArray());
addFacetIfNeeded(options, aggregationHelper, esRequest, RULES, query.ruleUuids().toArray()); addFacetIfNeeded(options, aggregationHelper, esRequest, RULES, query.ruleUuids().toArray());
addFacetIfNeeded(options, aggregationHelper, esRequest, AUTHORS, query.authors().toArray());
addFacetIfNeeded(options, aggregationHelper, esRequest, AUTHOR, query.authors().toArray()); addFacetIfNeeded(options, aggregationHelper, esRequest, AUTHOR, query.authors().toArray());
addFacetIfNeeded(options, aggregationHelper, esRequest, TAGS, query.tags().toArray()); addFacetIfNeeded(options, aggregationHelper, esRequest, TAGS, query.tags().toArray());
addFacetIfNeeded(options, aggregationHelper, esRequest, TYPES, query.types().toArray()); addFacetIfNeeded(options, aggregationHelper, esRequest, TYPES, query.types().toArray());

+ 0
- 1
server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQueryFactory.java View File

builder.projectUuids(projects.stream().map(IssueQueryFactory::toProjectUuid).collect(toList())); builder.projectUuids(projects.stream().map(IssueQueryFactory::toProjectUuid).collect(toList()));
setBranch(builder, projects.get(0), request.getBranch(), request.getPullRequest()); setBranch(builder, projects.get(0), request.getBranch(), request.getPullRequest());
} }
builder.moduleUuids(request.getModuleUuids());
builder.directories(request.getDirectories()); builder.directories(request.getDirectories());
builder.files(request.getFiles()); builder.files(request.getFiles());



+ 4
- 4
server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexDebtTest.java View File

} }


@Test @Test
public void facets_on_authors() {
public void facets_on_author() {
ComponentDto project = ComponentTesting.newPrivateProjectDto(); ComponentDto project = ComponentTesting.newPrivateProjectDto();
ComponentDto file = ComponentTesting.newFileDto(project, null); ComponentDto file = ComponentTesting.newFileDto(project, null);


IssueDocTesting.newDoc("I3", file).setAuthorLogin("simon").setEffort(10L), IssueDocTesting.newDoc("I3", file).setAuthorLogin("simon").setEffort(10L),
IssueDocTesting.newDoc("I4", file).setAuthorLogin(null).setEffort(10L)); IssueDocTesting.newDoc("I4", file).setAuthorLogin(null).setEffort(10L));


Facets facets = new Facets(underTest.search(newQueryBuilder().build(), new SearchOptions().addFacets(singletonList("authors"))), system2.getDefaultTimeZone().toZoneId());
assertThat(facets.getNames()).containsOnly("authors", FACET_MODE_EFFORT);
assertThat(facets.get("authors")).containsOnly(entry("steph", 10L), entry("simon", 20L));
Facets facets = new Facets(underTest.search(newQueryBuilder().build(), new SearchOptions().addFacets(singletonList("author"))), system2.getDefaultTimeZone().toZoneId());
assertThat(facets.getNames()).containsOnly("author", FACET_MODE_EFFORT);
assertThat(facets.get("author")).containsOnly(entry("steph", 10L), entry("simon", 20L));
assertThat(facets.get(FACET_MODE_EFFORT)).containsOnly(entry("total", 40L)); assertThat(facets.get(FACET_MODE_EFFORT)).containsOnly(entry("total", 40L));
} }



+ 2
- 16
server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexFacetsTest.java View File

assertThatFacetHasOnly(IssueQuery.builder(), "author", entry("steph", 1L), entry("marcel", 2L)); assertThatFacetHasOnly(IssueQuery.builder(), "author", entry("steph", 1L), entry("marcel", 2L));
} }


@Test
public void facets_on_deprecated_authors() {
ComponentDto project = newPrivateProjectDto();
ComponentDto file = newFileDto(project, null);

indexIssues(
newDoc("I1", file).setAuthorLogin("steph"),
newDoc("I2", file).setAuthorLogin("marcel"),
newDoc("I3", file).setAuthorLogin("marcel"),
newDoc("I4", file).setAuthorLogin(null));

assertThatFacetHasOnly(IssueQuery.builder(), "authors", entry("steph", 1L), entry("marcel", 2L));
}

@Test @Test
public void facets_on_authors_return_100_entries_plus_selected_values() { public void facets_on_authors_return_100_entries_plus_selected_values() {
ComponentDto project = newPrivateProjectDto(); ComponentDto project = newPrivateProjectDto();
IssueDoc issue2 = newDoc(newFileDto(project, null)).setAuthorLogin("user2"); IssueDoc issue2 = newDoc(newFileDto(project, null)).setAuthorLogin("user2");
indexIssues(issue1, issue2); indexIssues(issue1, issue2);


assertThatFacetHasSize(IssueQuery.builder().build(), "authors", 100);
assertThatFacetHasSize(IssueQuery.builder().authors(asList(issue1.authorLogin(), issue2.authorLogin())).build(), "authors", 102);
assertThatFacetHasSize(IssueQuery.builder().build(), "author", 100);
assertThatFacetHasSize(IssueQuery.builder().authors(asList(issue1.authorLogin(), issue2.authorLogin())).build(), "author", 102);
} }


@Test @Test

+ 0
- 2
server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryFactoryTest.java View File

.setResolutions(asList("FALSE-POSITIVE")) .setResolutions(asList("FALSE-POSITIVE"))
.setResolved(true) .setResolved(true)
.setProjects(asList(project.getDbKey())) .setProjects(asList(project.getDbKey()))
.setModuleUuids(asList(module.uuid()))
.setDirectories(asList("aDirPath")) .setDirectories(asList("aDirPath"))
.setFiles(asList(file.uuid())) .setFiles(asList(file.uuid()))
.setAssigneesUuid(asList(user.getUuid())) .setAssigneesUuid(asList(user.getUuid()))
assertThat(query.resolutions()).containsOnly("FALSE-POSITIVE"); assertThat(query.resolutions()).containsOnly("FALSE-POSITIVE");
assertThat(query.resolved()).isTrue(); assertThat(query.resolved()).isTrue();
assertThat(query.projectUuids()).containsOnly(project.uuid()); assertThat(query.projectUuids()).containsOnly(project.uuid());
assertThat(query.moduleUuids()).containsOnly(module.uuid());
assertThat(query.files()).containsOnly(file.uuid()); assertThat(query.files()).containsOnly(file.uuid());
assertThat(query.assignees()).containsOnly(user.getUuid()); assertThat(query.assignees()).containsOnly(user.getUuid());
assertThat(query.scopes()).containsOnly("TEST", "MAIN"); assertThat(query.scopes()).containsOnly("TEST", "MAIN");

+ 0
- 1
server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryTest.java View File

import org.sonar.server.issue.index.IssueQuery.PeriodStart; import org.sonar.server.issue.index.IssueQuery.PeriodStart;


import static com.google.common.collect.Lists.newArrayList; import static com.google.common.collect.Lists.newArrayList;
import static org.apache.commons.lang.math.RandomUtils.nextInt;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry; import static org.assertj.core.api.Assertions.entry;



+ 0
- 35
server/sonar-webserver-webapi/src/main/java/org/sonar/server/component/ComponentService.java View File

*/ */
package org.sonar.server.component; package org.sonar.server.component;


import java.util.Set;
import org.sonar.api.resources.Qualifiers;
import org.sonar.api.resources.Scopes;
import org.sonar.api.server.ServerSide; import org.sonar.api.server.ServerSide;
import org.sonar.api.web.UserRole; import org.sonar.api.web.UserRole;
import org.sonar.core.util.stream.MoreCollectors;
import org.sonar.db.DbClient; import org.sonar.db.DbClient;
import org.sonar.db.DbSession; import org.sonar.db.DbSession;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.component.ComponentKeyUpdaterDao;
import org.sonar.db.component.ResourceDto;
import org.sonar.db.project.ProjectDto; import org.sonar.db.project.ProjectDto;
import org.sonar.server.es.ProjectIndexer; import org.sonar.server.es.ProjectIndexer;
import org.sonar.server.es.ProjectIndexers; import org.sonar.server.es.ProjectIndexers;
import org.sonar.server.project.RekeyedProject; import org.sonar.server.project.RekeyedProject;
import org.sonar.server.user.UserSession; import org.sonar.server.user.UserSession;


import static java.util.Collections.emptyList;
import static java.util.Collections.singleton; import static java.util.Collections.singleton;
import static java.util.Collections.singletonList; import static java.util.Collections.singletonList;
import static org.sonar.core.component.ComponentKeys.checkProjectKey; import static org.sonar.core.component.ComponentKeys.checkProjectKey;
projectLifeCycleListeners.onProjectsRekeyed(singleton(new RekeyedProject(newProject, project.getKey()))); projectLifeCycleListeners.onProjectsRekeyed(singleton(new RekeyedProject(newProject, project.getKey())));
} }


public void bulkUpdateKey(DbSession dbSession, ProjectDto project, String stringToReplace, String replacementString) {
Set<ComponentKeyUpdaterDao.RekeyedResource> rekeyedProjects = dbClient.componentKeyUpdaterDao().bulkUpdateKey(
dbSession, project.getUuid(), stringToReplace, replacementString,
ComponentService::isMainProject);
projectIndexers.commitAndIndexProjects(dbSession, singletonList(project), ProjectIndexer.Cause.PROJECT_KEY_UPDATE);
if (!rekeyedProjects.isEmpty()) {
projectLifeCycleListeners.onProjectsRekeyed(rekeyedProjects.stream()
.map(ComponentService::toRekeyedProject)
.collect(MoreCollectors.toSet(rekeyedProjects.size())));
}
}

private static boolean isMainProject(ComponentKeyUpdaterDao.RekeyedResource rekeyedResource) {
ResourceDto resource = rekeyedResource.getResource();
String resourceKey = resource.getKey();
return Scopes.PROJECT.equals(resource.getScope())
&& Qualifiers.PROJECT.equals(resource.getQualifier())
&& !resourceKey.contains(ComponentDto.BRANCH_KEY_SEPARATOR)
&& !resourceKey.contains(ComponentDto.PULL_REQUEST_SEPARATOR);
}

private static RekeyedProject toRekeyedProject(ComponentKeyUpdaterDao.RekeyedResource rekeyedResource) {
ResourceDto resource = rekeyedResource.getResource();
Project project = new Project(resource.getUuid(), resource.getKey(), resource.getName(), resource.getDescription(), emptyList());
return new RekeyedProject(project, rekeyedResource.getOldKey());
}

} }

+ 8
- 32
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchAction.java View File

import static org.sonar.core.util.stream.MoreCollectors.toSet; import static org.sonar.core.util.stream.MoreCollectors.toSet;
import static org.sonar.server.es.SearchOptions.MAX_PAGE_SIZE; import static org.sonar.server.es.SearchOptions.MAX_PAGE_SIZE;
import static org.sonar.server.issue.index.IssueIndex.FACET_ASSIGNED_TO_ME; import static org.sonar.server.issue.index.IssueIndex.FACET_ASSIGNED_TO_ME;
import static org.sonar.server.issue.index.IssueIndex.FACET_MODULES;
import static org.sonar.server.issue.index.IssueIndex.FACET_PROJECTS; import static org.sonar.server.issue.index.IssueIndex.FACET_PROJECTS;
import static org.sonar.server.issue.index.IssueQuery.SORT_BY_ASSIGNEE; import static org.sonar.server.issue.index.IssueQuery.SORT_BY_ASSIGNEE;
import static org.sonar.server.issue.index.IssueQueryFactory.ISSUE_STATUSES; import static org.sonar.server.issue.index.IssueQueryFactory.ISSUE_STATUSES;
import static org.sonar.server.ws.KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001; import static org.sonar.server.ws.KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001;
import static org.sonar.server.ws.WsUtils.writeProtobuf; import static org.sonar.server.ws.WsUtils.writeProtobuf;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.ACTION_SEARCH; import static org.sonarqube.ws.client.issue.IssuesWsParameters.ACTION_SEARCH;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.DEPRECATED_PARAM_AUTHORS;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.FACET_MODE;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.FACET_MODE_COUNT;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.FACET_MODE_EFFORT;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ADDITIONAL_FIELDS; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ADDITIONAL_FIELDS;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ASC; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ASC;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ASSIGNED; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ASSIGNED;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_FILES; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_FILES;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ISSUES; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ISSUES;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_LANGUAGES; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_LANGUAGES;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_MODULE_UUIDS;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ON_COMPONENT_ONLY; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ON_COMPONENT_ONLY;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_OWASP_TOP_10; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_OWASP_TOP_10;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_PROJECTS; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_PROJECTS;


static final List<String> SUPPORTED_FACETS = List.of( static final List<String> SUPPORTED_FACETS = List.of(
FACET_PROJECTS, FACET_PROJECTS,
PARAM_MODULE_UUIDS,
FACET_MODULES,
PARAM_FILES, PARAM_FILES,
FACET_ASSIGNED_TO_ME, FACET_ASSIGNED_TO_ME,
PARAM_SEVERITIES, PARAM_SEVERITIES,
PARAM_RESOLUTIONS, PARAM_RESOLUTIONS,
PARAM_RULES, PARAM_RULES,
PARAM_ASSIGNEES, PARAM_ASSIGNEES,
DEPRECATED_PARAM_AUTHORS,
PARAM_AUTHOR, PARAM_AUTHOR,
PARAM_DIRECTORIES, PARAM_DIRECTORIES,
PARAM_SCOPES, PARAM_SCOPES,
); );


private static final String INTERNAL_PARAMETER_DISCLAIMER = "This parameter is mostly used by the Issues page, please prefer usage of the componentKeys parameter. "; private static final String INTERNAL_PARAMETER_DISCLAIMER = "This parameter is mostly used by the Issues page, please prefer usage of the componentKeys parameter. ";
private static final Set<String> FACETS_REQUIRING_PROJECT = newHashSet(PARAM_MODULE_UUIDS, PARAM_FILES, PARAM_DIRECTORIES);
private static final Set<String> FACETS_REQUIRING_PROJECT = newHashSet(FACET_MODULES, PARAM_FILES, PARAM_DIRECTORIES);


private final UserSession userSession; private final UserSession userSession;
private final IssueIndex issueIndex; private final IssueIndex issueIndex;
+ "<br/>When issue indexation is in progress returns 503 service unavailable HTTP code.") + "<br/>When issue indexation is in progress returns 503 service unavailable HTTP code.")
.setSince("3.6") .setSince("3.6")
.setChangelog( .setChangelog(
new Change("9.1", "Deprecated parameters 'authors', 'facetMode' and 'moduleUuids' were dropped"),
new Change("8.6", "Parameter 'timeZone' added"), new Change("8.6", "Parameter 'timeZone' added"),
new Change("8.5", "Facet 'fileUuids' is dropped in favour of the new facet 'files'" + new Change("8.5", "Facet 'fileUuids' is dropped in favour of the new facet 'files'" +
"Note that they are not strictly identical, the latter returns the file paths."), "Note that they are not strictly identical, the latter returns the file paths."),
new Change("8.2", "Status 'IN_REVIEW' for Security Hotspots has been deprecated"), new Change("8.2", "Status 'IN_REVIEW' for Security Hotspots has been deprecated"),
new Change("7.8", format("added new Security Hotspots statuses : %s, %s and %s", STATUS_TO_REVIEW, STATUS_IN_REVIEW, STATUS_REVIEWED)), new Change("7.8", format("added new Security Hotspots statuses : %s, %s and %s", STATUS_TO_REVIEW, STATUS_IN_REVIEW, STATUS_REVIEWED)),
new Change("7.8", "Security hotspots are returned by default"), new Change("7.8", "Security hotspots are returned by default"),
new Change("7.7", format("Value '%s' in parameter '%s' is deprecated, please use '%s' instead", DEPRECATED_PARAM_AUTHORS, FACETS, PARAM_AUTHOR)),
new Change("7.7", format("Value 'authors' in parameter '%s' is deprecated, please use '%s' instead", FACETS, PARAM_AUTHOR)),
new Change("7.6", format("The use of module keys in parameter '%s' is deprecated", PARAM_COMPONENT_KEYS)), new Change("7.6", format("The use of module keys in parameter '%s' is deprecated", PARAM_COMPONENT_KEYS)),
new Change("7.4", "The facet 'projectUuids' is dropped in favour of the new facet 'projects'. " + new Change("7.4", "The facet 'projectUuids' is dropped in favour of the new facet 'projects'. " +
"Note that they are not strictly identical, the latter returns the project keys."), "Note that they are not strictly identical, the latter returns the project keys."),
new Change("7.4", format("Parameter '%s' does not accept anymore deprecated value 'debt'", FACET_MODE)),
new Change("7.4", "Parameter 'facetMode' does not accept anymore deprecated value 'debt'"),
new Change("7.3", "response field 'fromHotspot' added to issues that are security hotspots"), new Change("7.3", "response field 'fromHotspot' added to issues that are security hotspots"),
new Change("7.3", "added facets 'sansTop25', 'owaspTop10' and 'cwe'"), new Change("7.3", "added facets 'sansTop25', 'owaspTop10' and 'cwe'"),
new Change("7.2", "response field 'externalRuleEngine' added to issues that have been imported from an external rule engine"), new Change("7.2", "response field 'externalRuleEngine' added to issues that have been imported from an external rule engine"),
action.createParam(FACETS) action.createParam(FACETS)
.setDescription("Comma-separated list of the facets to be computed. No facet is computed by default.") .setDescription("Comma-separated list of the facets to be computed. No facet is computed by default.")
.setPossibleValues(SUPPORTED_FACETS); .setPossibleValues(SUPPORTED_FACETS);
action.createParam(FACET_MODE)
.setDefaultValue(FACET_MODE_COUNT)
.setDeprecatedSince("7.9")
.setDescription("Choose the returned value for facet items, either count of issues or sum of remediation effort.")
.setPossibleValues(FACET_MODE_COUNT, FACET_MODE_EFFORT);
action.addSortParams(IssueQuery.SORTS, null, true); action.addSortParams(IssueQuery.SORTS, null, true);
action.createParam(PARAM_ADDITIONAL_FIELDS) action.createParam(PARAM_ADDITIONAL_FIELDS)
.setSince("5.2") .setSince("5.2")
" with any category") " with any category")
.setSince("7.8") .setSince("7.8")
.setPossibleValues(Arrays.stream(SQCategory.values()).map(SQCategory::getKey).collect(Collectors.toList())); .setPossibleValues(Arrays.stream(SQCategory.values()).map(SQCategory::getKey).collect(Collectors.toList()));
action.createParam(DEPRECATED_PARAM_AUTHORS)
.setDeprecatedSince("7.7")
.setDescription("This parameter is deprecated, please use '%s' instead", PARAM_AUTHOR)
.setExampleValue("torvalds@linux-foundation.org");
action.createParam(PARAM_AUTHOR) action.createParam(PARAM_AUTHOR)
.setDescription("SCM accounts. To set several values, the parameter must be called once for each value.") .setDescription("SCM accounts. To set several values, the parameter must be called once for each value.")
.setExampleValue("author=torvalds@linux-foundation.org&author=linux@fondation.org"); .setExampleValue("author=torvalds@linux-foundation.org&author=linux@fondation.org");
.setInternal(true) .setInternal(true)
.setExampleValue(KEY_PROJECT_EXAMPLE_001); .setExampleValue(KEY_PROJECT_EXAMPLE_001);


action.createParam(PARAM_MODULE_UUIDS)
.setDescription("To retrieve issues associated to a specific list of modules (comma-separated list of module IDs). " +
INTERNAL_PARAMETER_DISCLAIMER)
.setInternal(true)
.setDeprecatedSince("7.6")
.setExampleValue("7d8749e8-3070-4903-9188-bdd82933bb92");

action.createParam(PARAM_DIRECTORIES) action.createParam(PARAM_DIRECTORIES)
.setDescription("To retrieve issues associated to a specific list of directories (comma-separated list of directory paths). " + .setDescription("To retrieve issues associated to a specific list of directories (comma-separated list of directory paths). " +
"This parameter is only meaningful when a module is selected. " + "This parameter is only meaningful when a module is selected. " +
addMandatoryValuesToFacet(facets, PARAM_STATUSES, ISSUE_STATUSES); addMandatoryValuesToFacet(facets, PARAM_STATUSES, ISSUE_STATUSES);
addMandatoryValuesToFacet(facets, PARAM_RESOLUTIONS, concat(singletonList(""), RESOLUTIONS)); addMandatoryValuesToFacet(facets, PARAM_RESOLUTIONS, concat(singletonList(""), RESOLUTIONS));
addMandatoryValuesToFacet(facets, FACET_PROJECTS, query.projectUuids()); addMandatoryValuesToFacet(facets, FACET_PROJECTS, query.projectUuids());
addMandatoryValuesToFacet(facets, PARAM_MODULE_UUIDS, query.moduleUuids());
addMandatoryValuesToFacet(facets, FACET_MODULES, query.moduleUuids());
addMandatoryValuesToFacet(facets, PARAM_FILES, query.files()); addMandatoryValuesToFacet(facets, PARAM_FILES, query.files());


List<String> assignees = Lists.newArrayList(""); List<String> assignees = Lists.newArrayList("");


private static void collectFacets(SearchResponseLoader.Collector collector, Facets facets) { private static void collectFacets(SearchResponseLoader.Collector collector, Facets facets) {
collector.addProjectUuids(facets.getBucketKeys(FACET_PROJECTS)); collector.addProjectUuids(facets.getBucketKeys(FACET_PROJECTS));
collector.addComponentUuids(facets.getBucketKeys(PARAM_MODULE_UUIDS));
collector.addRuleIds(facets.getBucketKeys(PARAM_RULES)); collector.addRuleIds(facets.getBucketKeys(PARAM_RULES));
collector.addUserUuids(facets.getBucketKeys(PARAM_ASSIGNEES)); collector.addUserUuids(facets.getBucketKeys(PARAM_ASSIGNEES));
} }


private static void collectRequestParams(SearchResponseLoader.Collector collector, SearchRequest request) { private static void collectRequestParams(SearchResponseLoader.Collector collector, SearchRequest request) {
collector.addComponentUuids(request.getModuleUuids());
collector.addUserUuids(request.getAssigneeUuids()); collector.addUserUuids(request.getAssigneeUuids());
} }


.setAsc(request.mandatoryParamAsBoolean(PARAM_ASC)) .setAsc(request.mandatoryParamAsBoolean(PARAM_ASC))
.setAssigned(request.paramAsBoolean(PARAM_ASSIGNED)) .setAssigned(request.paramAsBoolean(PARAM_ASSIGNED))
.setAssigneesUuid(getLogins(dbSession, request.paramAsStrings(PARAM_ASSIGNEES))) .setAssigneesUuid(getLogins(dbSession, request.paramAsStrings(PARAM_ASSIGNEES)))
.setAuthors(request.hasParam(PARAM_AUTHOR) ? request.multiParam(PARAM_AUTHOR) : request.paramAsStrings(DEPRECATED_PARAM_AUTHORS))
.setAuthors(request.multiParam(PARAM_AUTHOR))
.setComponents(request.paramAsStrings(PARAM_COMPONENT_KEYS)) .setComponents(request.paramAsStrings(PARAM_COMPONENT_KEYS))
.setCreatedAfter(request.param(PARAM_CREATED_AFTER)) .setCreatedAfter(request.param(PARAM_CREATED_AFTER))
.setCreatedAt(request.param(PARAM_CREATED_AT)) .setCreatedAt(request.param(PARAM_CREATED_AT))
.setCreatedBefore(request.param(PARAM_CREATED_BEFORE)) .setCreatedBefore(request.param(PARAM_CREATED_BEFORE))
.setCreatedInLast(request.param(PARAM_CREATED_IN_LAST)) .setCreatedInLast(request.param(PARAM_CREATED_IN_LAST))
.setDirectories(request.paramAsStrings(PARAM_DIRECTORIES)) .setDirectories(request.paramAsStrings(PARAM_DIRECTORIES))
.setFacetMode(request.mandatoryParam(FACET_MODE))
.setFacets(request.paramAsStrings(FACETS)) .setFacets(request.paramAsStrings(FACETS))
.setFiles(request.paramAsStrings(PARAM_FILES)) .setFiles(request.paramAsStrings(PARAM_FILES))
.setIssues(request.paramAsStrings(PARAM_ISSUES)) .setIssues(request.paramAsStrings(PARAM_ISSUES))
.setScopes(request.paramAsStrings(PARAM_SCOPES)) .setScopes(request.paramAsStrings(PARAM_SCOPES))
.setLanguages(request.paramAsStrings(PARAM_LANGUAGES)) .setLanguages(request.paramAsStrings(PARAM_LANGUAGES))
.setModuleUuids(request.paramAsStrings(PARAM_MODULE_UUIDS))
.setOnComponentOnly(request.paramAsBoolean(PARAM_ON_COMPONENT_ONLY)) .setOnComponentOnly(request.paramAsBoolean(PARAM_ON_COMPONENT_ONLY))
.setBranch(request.param(PARAM_BRANCH)) .setBranch(request.param(PARAM_BRANCH))
.setPullRequest(request.param(PARAM_PULL_REQUEST)) .setPullRequest(request.param(PARAM_PULL_REQUEST))

+ 0
- 1
server/sonar-webserver-webapi/src/main/java/org/sonar/server/permission/ws/PermissionsWsModule.java View File

RemoveUserAction.class, RemoveUserAction.class,
UsersAction.class, UsersAction.class,
GroupsAction.class, GroupsAction.class,
SearchGlobalPermissionsAction.class,
RemoveUserFromTemplateAction.class, RemoveUserFromTemplateAction.class,
AddUserToTemplateAction.class, AddUserToTemplateAction.class,
AddGroupToTemplateAction.class, AddGroupToTemplateAction.class,

+ 0
- 123
server/sonar-webserver-webapi/src/main/java/org/sonar/server/permission/ws/SearchGlobalPermissionsAction.java View File

/*
* SonarQube
* Copyright (C) 2009-2021 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.permission.ws;

import java.util.Locale;
import org.sonar.api.server.ws.Request;
import org.sonar.api.server.ws.Response;
import org.sonar.api.server.ws.WebService;
import org.sonar.core.i18n.I18n;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.permission.GlobalPermission;
import org.sonar.db.permission.PermissionQuery;
import org.sonar.server.permission.PermissionService;
import org.sonar.server.user.UserSession;
import org.sonarqube.ws.Permissions.Permission;
import org.sonarqube.ws.Permissions.WsSearchGlobalPermissionsResponse;

import static org.sonar.server.permission.PermissionPrivilegeChecker.checkGlobalAdmin;
import static org.sonar.server.ws.WsUtils.writeProtobuf;
import static org.sonarqube.ws.Permissions.Permission.newBuilder;

public class SearchGlobalPermissionsAction implements PermissionsWsAction {

public static final String ACTION = "search_global_permissions";
private static final String PROPERTY_PREFIX = "global_permissions.";
private static final String DESCRIPTION_SUFFIX = ".desc";

private final DbClient dbClient;
private final UserSession userSession;
private final I18n i18n;
private final PermissionService permissionService;

public SearchGlobalPermissionsAction(DbClient dbClient, UserSession userSession, I18n i18n, PermissionService permissionService) {
this.dbClient = dbClient;
this.userSession = userSession;
this.i18n = i18n;
this.permissionService = permissionService;
}

@Override
public void define(WebService.NewController context) {
context.createAction(ACTION)
.setDescription("List global permissions. <br />" +
"Requires the following permission: 'Administer System'")
.setResponseExample(getClass().getResource("search_global_permissions-example.json"))
.setSince("5.2")
.setDeprecatedSince("6.5")
.setHandler(this);
}

@Override
public void handle(Request wsRequest, Response wsResponse) throws Exception {
try (DbSession dbSession = dbClient.openSession(false)) {
checkGlobalAdmin(userSession);

WsSearchGlobalPermissionsResponse response = buildResponse(dbSession);
writeProtobuf(response, wsRequest, wsResponse);
}
}

private WsSearchGlobalPermissionsResponse buildResponse(DbSession dbSession) {
WsSearchGlobalPermissionsResponse.Builder response = WsSearchGlobalPermissionsResponse.newBuilder();
Permission.Builder permission = newBuilder();

permissionService.getGlobalPermissions().stream()
.map(GlobalPermission::getKey)
.forEach(permissionKey -> {
PermissionQuery query = permissionQuery(permissionKey);
response.addPermissions(
permission
.clear()
.setKey(permissionKey)
.setName(i18nName(permissionKey))
.setDescription(i18nDescriptionMessage(permissionKey))
.setUsersCount(countUsers(dbSession, query))
.setGroupsCount(countGroups(dbSession, permissionKey)));
});

return response.build();
}

private String i18nDescriptionMessage(String permissionKey) {
return i18n.message(Locale.ENGLISH, PROPERTY_PREFIX + permissionKey + DESCRIPTION_SUFFIX, "");
}

private String i18nName(String permissionKey) {
return i18n.message(Locale.ENGLISH, PROPERTY_PREFIX + permissionKey, permissionKey);
}

private int countGroups(DbSession dbSession, String permission) {
PermissionQuery query = PermissionQuery.builder().setPermission(permission).build();
return dbClient.groupPermissionDao().countGroupsByQuery(dbSession, query);
}

private int countUsers(DbSession dbSession, PermissionQuery permissionQuery) {
return dbClient.userPermissionDao().countUsersByQuery(dbSession, permissionQuery);
}

private static PermissionQuery permissionQuery(String permissionKey) {
return PermissionQuery.builder()
.setPermission(permissionKey)
.withAtLeastOnePermission()
.build();
}
}

+ 0
- 115
server/sonar-webserver-webapi/src/main/java/org/sonar/server/permission/ws/SearchProjectPermissionsData.java View File

/*
* SonarQube
* Copyright (C) 2009-2021 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.permission.ws;

import com.google.common.collect.FluentIterable;
import com.google.common.collect.Iterables;
import com.google.common.collect.Ordering;
import com.google.common.collect.Table;
import java.util.List;
import java.util.Set;
import org.sonar.api.utils.Paging;
import org.sonar.db.component.ComponentDto;

import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableList.copyOf;
import static com.google.common.collect.ImmutableTable.copyOf;

class SearchProjectPermissionsData {
private final List<ComponentDto> rootComponents;
private final Paging paging;
private final Table<String, String, Integer> userCountByProjectUuidAndPermission;
private final Table<String, String, Integer> groupCountByProjectUuidAndPermission;

private SearchProjectPermissionsData(Builder builder) {
this.rootComponents = copyOf(builder.projects);
this.paging = builder.paging;
this.userCountByProjectUuidAndPermission = copyOf(builder.userCountByProjectUuidAndPermission);
this.groupCountByProjectUuidAndPermission = copyOf(builder.groupCountByProjectUuidAndPermission);
}

static Builder newBuilder() {
return new Builder();
}

List<ComponentDto> rootComponents() {
return rootComponents;
}

Paging paging() {
return paging;
}

int userCount(String rootComponentUuid, String permission) {
return firstNonNull(userCountByProjectUuidAndPermission.get(rootComponentUuid, permission), 0);
}

int groupCount(String rootComponentUuid, String permission) {
return firstNonNull(groupCountByProjectUuidAndPermission.get(rootComponentUuid, permission), 0);
}

Set<String> permissions(String rootComponentUuid) {
return FluentIterable.from(
Iterables.concat(
userCountByProjectUuidAndPermission.row(rootComponentUuid).keySet(),
groupCountByProjectUuidAndPermission.row(rootComponentUuid).keySet()))
.toSortedSet(Ordering.natural());
}

static class Builder {
private List<ComponentDto> projects;
private Paging paging;
private Table<String, String, Integer> userCountByProjectUuidAndPermission;
private Table<String, String, Integer> groupCountByProjectUuidAndPermission;

private Builder() {
// prevents instantiation outside main class
}

SearchProjectPermissionsData build() {
checkState(projects != null);
checkState(userCountByProjectUuidAndPermission != null);
checkState(groupCountByProjectUuidAndPermission != null);

return new SearchProjectPermissionsData(this);
}

Builder rootComponents(List<ComponentDto> projects) {
this.projects = projects;
return this;
}

Builder paging(Paging paging) {
this.paging = paging;
return this;
}

Builder userCountByProjectIdAndPermission(Table<String, String, Integer> userCountByProjectIdAndPermission) {
this.userCountByProjectUuidAndPermission = userCountByProjectIdAndPermission;
return this;
}

Builder groupCountByProjectIdAndPermission(Table<String, String, Integer> groupCountByProjectIdAndPermission) {
this.groupCountByProjectUuidAndPermission = groupCountByProjectIdAndPermission;
return this;
}
}
}

+ 0
- 241
server/sonar-webserver-webapi/src/main/java/org/sonar/server/project/ws/BulkUpdateKeyAction.java View File

/*
* SonarQube
* Copyright (C) 2009-2021 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.ws;

import com.google.common.collect.ImmutableList;
import java.util.Map;
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.web.UserRole;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.component.ComponentKeyUpdaterDao;
import org.sonar.db.project.ProjectDto;
import org.sonar.server.component.ComponentFinder;
import org.sonar.server.component.ComponentService;
import org.sonar.server.user.UserSession;
import org.sonarqube.ws.Projects.BulkUpdateKeyWsResponse;

import static com.google.common.base.Preconditions.checkArgument;
import static org.sonar.server.exceptions.BadRequestException.checkRequest;
import static org.sonar.server.ws.WsUtils.writeProtobuf;
import static org.sonarqube.ws.client.project.ProjectsWsParameters.ACTION_BULK_UPDATE_KEY;
import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_DRY_RUN;
import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_FROM;
import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_PROJECT;
import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_TO;

public class BulkUpdateKeyAction implements ProjectsWsAction {
private final DbClient dbClient;
private final ComponentFinder componentFinder;
private final ComponentKeyUpdaterDao componentKeyUpdater;
private final ComponentService componentService;
private final UserSession userSession;

public BulkUpdateKeyAction(DbClient dbClient, ComponentFinder componentFinder, ComponentService componentService, UserSession userSession) {
this.dbClient = dbClient;
this.componentKeyUpdater = dbClient.componentKeyUpdaterDao();
this.componentFinder = componentFinder;
this.componentService = componentService;
this.userSession = userSession;
}

@Override
public void define(WebService.NewController context) {
doDefine(context);
}

public WebService.NewAction doDefine(WebService.NewController context) {
WebService.NewAction action = context.createAction(ACTION_BULK_UPDATE_KEY)
.setDescription("Bulk update a project key and all its sub-components keys. " +
"The bulk update allows to replace a part of the current key by another string on the current project.<br>" +
"It's possible to simulate the bulk update by setting the parameter '%s' at true. No key is updated with a dry run.<br>" +
"Ex: to rename a project with key 'my_project' to 'my_new_project' and all its sub-components keys, call the WS with parameters:" +
"<ul>" +
" <li>%s: my_project</li>" +
" <li>%s: my_</li>" +
" <li>%s: my_new_</li>" +
"</ul>" +
"Requires one of the following permissions: " +
"<ul>" +
"<li>'Administer System'</li>" +
"<li>'Administer' rights on the specified project</li>" +
"</ul>",
PARAM_DRY_RUN,
PARAM_PROJECT, PARAM_FROM, PARAM_TO)
.setDeprecatedSince("7.6")
.setSince("6.1")
.setPost(true)
.setResponseExample(getClass().getResource("bulk_update_key-example.json"))
.setHandler(this);

action.createParam(PARAM_PROJECT)
.setDescription("Project key")
.setRequired(true)
.setExampleValue("my_old_project");

action.createParam(PARAM_FROM)
.setDescription("String to match in components keys")
.setRequired(true)
.setExampleValue("_old");

action.createParam(PARAM_TO)
.setDescription("String replacement in components keys")
.setRequired(true)
.setExampleValue("_new");

action.createParam(PARAM_DRY_RUN)
.setDescription("Simulate bulk update. No component key is updated.")
.setBooleanPossibleValues()
.setDefaultValue(false);

return action;
}

@Override
public void handle(Request request, Response response) throws Exception {
writeProtobuf(doHandle(toWsRequest(request)), request, response);
}

private BulkUpdateKeyWsResponse doHandle(BulkUpdateKeyRequest request) {
try (DbSession dbSession = dbClient.openSession(false)) {
ProjectDto project = componentFinder.getProjectByKey(dbSession, request.getProjectKey());
userSession.checkProjectPermission(UserRole.ADMIN, project);

Map<String, String> newKeysByOldKeys = componentKeyUpdater.simulateBulkUpdateKey(dbSession, project.getUuid(), request.getFrom(), request.getTo());
Map<String, Boolean> newKeysWithDuplicateMap = componentKeyUpdater.checkComponentKeys(dbSession, ImmutableList.copyOf(newKeysByOldKeys.values()));

if (!request.isDryRun()) {
checkNoDuplicate(newKeysWithDuplicateMap);
bulkUpdateKey(dbSession, request, project);
}

return buildResponse(newKeysByOldKeys, newKeysWithDuplicateMap);
}
}

private static void checkNoDuplicate(Map<String, Boolean> newKeysWithDuplicateMap) {
newKeysWithDuplicateMap.forEach((key, value) -> checkRequest(!value, "Impossible to update key: a component with key \"%s\" already exists.", key));
}

private void bulkUpdateKey(DbSession dbSession, BulkUpdateKeyRequest request, ProjectDto project) {
componentService.bulkUpdateKey(dbSession, project, request.getFrom(), request.getTo());
}

private static BulkUpdateKeyWsResponse buildResponse(Map<String, String> newKeysByOldKeys, Map<String, Boolean> newKeysWithDuplicateMap) {
BulkUpdateKeyWsResponse.Builder response = BulkUpdateKeyWsResponse.newBuilder();

newKeysByOldKeys.entrySet().stream()
// sort by old key
.sorted(Map.Entry.comparingByKey())
.forEach(
entry -> {
String newKey = entry.getValue();
response.addKeysBuilder()
.setKey(entry.getKey())
.setNewKey(newKey)
.setDuplicate(newKeysWithDuplicateMap.getOrDefault(newKey, false));
});

return response.build();
}

private static BulkUpdateKeyRequest toWsRequest(Request request) {
return BulkUpdateKeyRequest.builder()
.setProjectKey(request.mandatoryParam(PARAM_PROJECT))
.setFrom(request.mandatoryParam(PARAM_FROM))
.setTo(request.mandatoryParam(PARAM_TO))
.setDryRun(request.mandatoryParamAsBoolean(PARAM_DRY_RUN))
.build();
}

private static class BulkUpdateKeyRequest {
private final String projectKey;
private final String from;
private final String to;
private final boolean dryRun;

public BulkUpdateKeyRequest(Builder builder) {
this.projectKey = builder.projectKey;
this.from = builder.from;
this.to = builder.to;
this.dryRun = builder.dryRun;
}

public String getProjectKey() {
return projectKey;
}

public String getFrom() {
return from;
}

public String getTo() {
return to;
}

public boolean isDryRun() {
return dryRun;
}

public static Builder builder() {
return new Builder();
}
}

public static class Builder {
private String projectKey;
private String from;
private String to;
private boolean dryRun;

private Builder() {
// enforce method constructor
}

public Builder setProjectKey(String projectKey) {
this.projectKey = projectKey;
return this;
}

public Builder setFrom(String from) {
this.from = from;
return this;
}

public Builder setTo(String to) {
this.to = to;
return this;
}

public Builder setDryRun(boolean dryRun) {
this.dryRun = dryRun;
return this;
}

public BulkUpdateKeyRequest build() {
checkArgument(projectKey != null && !projectKey.isEmpty(), "The key must not be empty");
checkArgument(from != null && !from.isEmpty(), "The string to match must not be empty");
checkArgument(to != null && !to.isEmpty(), "The string replacement must not be empty");
return new BulkUpdateKeyRequest(this);
}
}
}

+ 0
- 1
server/sonar-webserver-webapi/src/main/java/org/sonar/server/project/ws/ProjectsWsModule.java View File

BulkDeleteAction.class, BulkDeleteAction.class,
DeleteAction.class, DeleteAction.class,
UpdateKeyAction.class, UpdateKeyAction.class,
BulkUpdateKeyAction.class,
SearchMyProjectsAction.class, SearchMyProjectsAction.class,
SearchAction.class, SearchAction.class,
UpdateVisibilityAction.class, UpdateVisibilityAction.class,

+ 0
- 23
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/QualityGatesWs.java View File

import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.sonar.api.server.ws.Change;
import org.sonar.api.server.ws.Request;
import org.sonar.api.server.ws.WebService; import org.sonar.api.server.ws.WebService;
import org.sonar.server.exceptions.BadRequestException;
import org.sonar.server.qualitygate.Condition; import org.sonar.server.qualitygate.Condition;
import org.sonar.server.ws.RemovedWebServiceHandler;


import static org.sonar.server.qualitygate.QualityGateConditionsUpdater.INVALID_METRIC_KEYS; import static org.sonar.server.qualitygate.QualityGateConditionsUpdater.INVALID_METRIC_KEYS;
import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.CONTROLLER_QUALITY_GATES; import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.CONTROLLER_QUALITY_GATES;
action.define(controller); action.define(controller);
} }


// unset_default is no more authorized
controller.createAction("unset_default")
.setDescription("This webservice is no more available : a default quality gate is mandatory.")
.setSince("4.3")
.setDeprecatedSince("7.0")
.setPost(true)
.setHandler(RemovedWebServiceHandler.INSTANCE)
.setResponseExample(RemovedWebServiceHandler.INSTANCE.getResponseExample())
.setChangelog(
new Change("7.0", "Unset a quality gate is no more authorized"));

controller.done(); controller.done();
} }


.collect(Collectors.joining()); .collect(Collectors.joining());
} }


static Long parseId(Request request, String paramName) {
try {
return Long.valueOf(request.mandatoryParam(paramName));
} catch (NumberFormatException badFormat) {
throw BadRequestException.create(paramName + " must be a valid long value");
}
}

private static Set<String> getPossibleOperators() { private static Set<String> getPossibleOperators() {
return Stream.of(Condition.Operator.values()) return Stream.of(Condition.Operator.values())
.map(Condition.Operator::getDbValue) .map(Condition.Operator::getDbValue)

+ 0
- 114
server/sonar-webserver-webapi/src/main/java/org/sonar/server/source/ws/HashAction.java View File

/*
* SonarQube
* Copyright (C) 2009-2021 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.source.ws;

import com.google.common.io.CharStreams;
import com.google.common.io.Resources;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.util.function.Consumer;
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.web.UserRole;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.component.ComponentDto;
import org.sonar.server.component.ComponentFinder;
import org.sonar.server.user.UserSession;

import static org.sonar.server.ws.KeyExamples.KEY_FILE_EXAMPLE_001;

public class HashAction implements SourcesWsAction {

private final DbClient dbClient;
private final UserSession userSession;
private final ComponentFinder componentFinder;

public HashAction(DbClient dbClient, UserSession userSession, ComponentFinder componentFinder) {
this.dbClient = dbClient;
this.userSession = userSession;
this.componentFinder = componentFinder;
}

@Override
public void define(WebService.NewController controller) {
WebService.NewAction action = controller.createAction("hash")
.setDescription("Show line line hashes for a given file. Require See Source Code permission on file's project<br/>")
.setSince("5.0")
.setInternal(true)
.setDeprecatedSince("7.7")
.setResponseExample(Resources.getResource(getClass(), "example-hash.txt"))
.setHandler(this);

action
.createParam("key")
.setRequired(true)
.setDescription("File key")
.setExampleValue(KEY_FILE_EXAMPLE_001);
}

@Override
public void handle(Request request, Response response) throws Exception {
try (DbSession session = dbClient.openSession(false)) {
String componentKey = request.mandatoryParam("key");
ComponentDto component = componentFinder.getByKey(session, componentKey);
userSession.checkComponentPermission(UserRole.USER, component);

response.stream().setMediaType("text/plain");
try (OutputStreamWriter writer = new OutputStreamWriter(response.stream().output(), StandardCharsets.UTF_8)) {
HashConsumer hashFunction = new HashConsumer(writer, componentKey);
dbClient.fileSourceDao().readLineHashesStream(session, component.uuid(), hashFunction);
if (!hashFunction.hasData()) {
response.noContent();
}
}
}
}

private static class HashConsumer implements Consumer<Reader> {

private final OutputStreamWriter writer;
private final String componentKey;
private boolean hasData = false;

public HashConsumer(OutputStreamWriter writer, String componentKey) {
this.writer = writer;
this.componentKey = componentKey;
}

@Override
public void accept(Reader input) {
try {
hasData = true;
CharStreams.copy(input, writer);
} catch (IOException e) {
throw new IllegalStateException(String.format("Can't read line hashes of file '%s'", componentKey), e);
}
}

public boolean hasData() {
return hasData;
}
}

}

+ 0
- 1
server/sonar-webserver-webapi/src/main/java/org/sonar/server/source/ws/SourceWsModule.java View File

ShowAction.class, ShowAction.class,
IssueSnippetsAction.class, IssueSnippetsAction.class,
LinesAction.class, LinesAction.class,
HashAction.class,
RawAction.class, RawAction.class,
IndexAction.class, IndexAction.class,
ScmAction.class ScmAction.class

+ 0
- 19
server/sonar-webserver-webapi/src/main/resources/org/sonar/server/project/ws/bulk_update_key-example.json View File

{
"keys": [
{
"key": "my_project",
"newKey": "my_new_project",
"duplicate": false
},
{
"key": "my_project:module_1",
"newKey": "my_new_project:module_1",
"duplicate": true
},
{
"key": "my_project:module_2",
"newKey": "my_new_project:module_2",
"duplicate": false
}
]
}

+ 0
- 76
server/sonar-webserver-webapi/src/test/java/org/sonar/server/component/ComponentServiceTest.java View File

/*
* SonarQube
* Copyright (C) 2009-2021 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.component;

import org.junit.Rule;
import org.junit.Test;
import org.sonar.api.utils.System2;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.DbTester;
import org.sonar.db.component.ComponentDbTester;
import org.sonar.db.component.ComponentDto;
import org.sonar.server.es.TestProjectIndexers;
import org.sonar.server.project.ProjectLifeCycleListeners;
import org.sonar.server.tester.UserSessionRule;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.sonar.db.component.ComponentTesting.newFileDto;
import static org.sonar.db.component.ComponentTesting.newModuleDto;

public class ComponentServiceTest {

@Rule
public final UserSessionRule userSession = UserSessionRule.standalone();
@Rule
public final DbTester dbTester = DbTester.create(System2.INSTANCE);

private final ComponentDbTester componentDb = new ComponentDbTester(dbTester);
private final DbClient dbClient = dbTester.getDbClient();
private final DbSession dbSession = dbTester.getSession();
private final TestProjectIndexers projectIndexers = new TestProjectIndexers();
private final ProjectLifeCycleListeners projectLifeCycleListeners = mock(ProjectLifeCycleListeners.class);

private final ComponentService underTest = new ComponentService(dbClient, userSession, projectIndexers, projectLifeCycleListeners);

@Test
public void bulk_update() {
ComponentDto project = componentDb.insertPublicProject(c -> c.setDbKey("my_project"));
ComponentDto module = componentDb.insertComponent(newModuleDto(project).setDbKey("my_project:root:module"));
ComponentDto inactiveModule = componentDb.insertComponent(newModuleDto(project).setDbKey("my_project:root:inactive_module").setEnabled(false));
ComponentDto file = componentDb.insertComponent(newFileDto(module, null).setDbKey("my_project:root:module:src/File.xoo"));
ComponentDto inactiveFile = componentDb.insertComponent(newFileDto(module, null).setDbKey("my_project:root:module:src/InactiveFile.xoo").setEnabled(false));

underTest.bulkUpdateKey(dbSession, componentDb.getProjectDto(project), "my_", "your_");

assertComponentKeyUpdated(project.getDbKey(), "your_project");
assertComponentKeyUpdated(module.getDbKey(), "your_project:root:module");
assertComponentKeyUpdated(file.getDbKey(), "your_project:root:module:src/File.xoo");
assertComponentKeyUpdated(inactiveModule.getDbKey(), "your_project:root:inactive_module");
assertComponentKeyUpdated(inactiveFile.getDbKey(), "your_project:root:module:src/InactiveFile.xoo");
}

private void assertComponentKeyUpdated(String oldKey, String newKey) {
assertThat(dbClient.componentDao().selectByKey(dbSession, oldKey)).isEmpty();
assertThat(dbClient.componentDao().selectByKey(dbSession, newKey)).isPresent();
}

}

+ 0
- 47
server/sonar-webserver-webapi/src/test/java/org/sonar/server/component/ComponentServiceUpdateKeyTest.java View File

*/ */
package org.sonar.server.component; package org.sonar.server.component;


import com.google.common.collect.ImmutableSet;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.sonar.api.utils.System2; import org.sonar.api.utils.System2;
import org.sonar.server.es.ProjectIndexer; import org.sonar.server.es.ProjectIndexer;
import org.sonar.server.es.TestProjectIndexers; import org.sonar.server.es.TestProjectIndexers;
import org.sonar.server.exceptions.ForbiddenException; import org.sonar.server.exceptions.ForbiddenException;
import org.sonar.server.project.Project;
import org.sonar.server.project.ProjectLifeCycleListeners; import org.sonar.server.project.ProjectLifeCycleListeners;
import org.sonar.server.project.RekeyedProject;
import org.sonar.server.tester.UserSessionRule; import org.sonar.server.tester.UserSessionRule;


import static java.util.Collections.emptyList;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.sonar.db.component.ComponentTesting.newFileDto;
import static org.sonar.db.component.ComponentTesting.newModuleDto;


public class ComponentServiceUpdateKeyTest { public class ComponentServiceUpdateKeyTest {


.hasMessage("Malformed key for 'sample?root'. Allowed characters are alphanumeric, '-', '_', '.' and ':', with at least one non-digit."); .hasMessage("Malformed key for 'sample?root'. Allowed characters are alphanumeric, '-', '_', '.' and ':', with at least one non-digit.");
} }


@Test
public void bulk_update_key() {
ComponentDto project = componentDb.insertPublicProject(c -> c.setDbKey("my_project"));
ComponentDto module = componentDb.insertComponent(newModuleDto(project).setDbKey("my_project:root:module"));
ComponentDto inactiveModule = componentDb.insertComponent(newModuleDto(project).setDbKey("my_project:root:inactive_module").setEnabled(false));
ComponentDto file = componentDb.insertComponent(newFileDto(module, null).setDbKey("my_project:root:module:src/File.xoo"));
ComponentDto inactiveFile = componentDb.insertComponent(newFileDto(module, null).setDbKey("my_project:root:module:src/InactiveFile.xoo").setEnabled(false));

underTest.bulkUpdateKey(dbSession, componentDb.getProjectDto(project), "my_", "your_");

assertComponentKeyUpdated(project.getDbKey(), "your_project");
assertComponentKeyUpdated(module.getDbKey(), "your_project:root:module");
assertComponentKeyUpdated(file.getDbKey(), "your_project:root:module:src/File.xoo");
assertComponentKeyUpdated(inactiveModule.getDbKey(), "your_project:root:inactive_module");
assertComponentKeyUpdated(inactiveFile.getDbKey(), "your_project:root:module:src/InactiveFile.xoo");
verify(projectLifeCycleListeners).onProjectsRekeyed(ImmutableSet.of(
new RekeyedProject(new Project(project.uuid(), "your_project", project.name(), project.uuid(), emptyList()), "my_project")));
}

@Test
public void bulk_update_key_with_branch_and_pr() {
ComponentDto project = componentDb.insertPublicProject(c -> c.setDbKey("my_project"));
ComponentDto branch = componentDb.insertProjectBranch(project);
ComponentDto module = componentDb.insertComponent(newModuleDto(branch).setDbKey("my_project:root:module"));
ComponentDto file = componentDb.insertComponent(newFileDto(module, null).setDbKey("my_project:root:module:src/File.xoo"));

underTest.bulkUpdateKey(dbSession, componentDb.getProjectDto(project), "my_", "your_");

assertComponentKeyUpdated(project.getDbKey(), "your_project");
assertComponentKeyUpdated(module.getDbKey(), "your_project:root:module");
assertComponentKeyUpdated(file.getDbKey(), "your_project:root:module:src/File.xoo");
verify(projectLifeCycleListeners).onProjectsRekeyed(ImmutableSet.of(
new RekeyedProject(new Project(project.uuid(), "your_project", project.name(), project.uuid(), emptyList()), "my_project")));
}

private void assertComponentKeyUpdated(String oldKey, String newKey) {
assertThat(dbClient.componentDao().selectByKey(dbSession, oldKey)).isEmpty();
assertThat(dbClient.componentDao().selectByKey(dbSession, newKey)).isPresent();
}

private ComponentDto insertSampleProject() { private ComponentDto insertSampleProject() {
return insertProject("sample:root"); return insertProject("sample:root");
} }

+ 0
- 20
server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchActionComponentsTest.java View File

import static org.sonar.db.component.ComponentTesting.newFileDto; import static org.sonar.db.component.ComponentTesting.newFileDto;
import static org.sonar.db.component.ComponentTesting.newModuleDto; import static org.sonar.db.component.ComponentTesting.newModuleDto;
import static org.sonar.db.component.ComponentTesting.newProjectCopy; import static org.sonar.db.component.ComponentTesting.newProjectCopy;
import static org.sonar.db.component.ComponentTesting.newSubPortfolio;
import static org.sonar.db.component.ComponentTesting.newPortfolio;
import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_BRANCH; import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_BRANCH;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_COMPONENT_KEYS; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_COMPONENT_KEYS;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_DIRECTORIES; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_DIRECTORIES;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_FILES; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_FILES;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_MODULE_UUIDS;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_PROJECTS; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_PROJECTS;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_PULL_REQUEST; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_PULL_REQUEST;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_SINCE_LEAK_PERIOD; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_SINCE_LEAK_PERIOD;
.setParam(PARAM_COMPONENT_KEYS, module1.getKey()) .setParam(PARAM_COMPONENT_KEYS, module1.getKey())
.executeProtobuf(SearchWsResponse.class).getIssuesList()).extracting(Issue::getKey) .executeProtobuf(SearchWsResponse.class).getIssuesList()).extracting(Issue::getKey)
.containsExactlyInAnyOrder(issue1.getKey()); .containsExactlyInAnyOrder(issue1.getKey());

assertThat(ws.newRequest()
.setParam(PARAM_MODULE_UUIDS, module1.uuid())
.executeProtobuf(SearchWsResponse.class).getIssuesList()).extracting(Issue::getKey)
.containsExactlyInAnyOrder(issue1.getKey());
} }


@Test @Test
.execute() .execute()
.assertJson(this.getClass(), "no_issue.json"); .assertJson(this.getClass(), "no_issue.json");


ws.newRequest()
.setParam(PARAM_MODULE_UUIDS, module1.uuid())
.setParam(PARAM_DIRECTORIES, "src/main/java/dir")
.execute()
.assertJson(this.getClass(), "search_by_directory_uuid.json");

ws.newRequest()
.setParam(PARAM_MODULE_UUIDS, module2.uuid())
.setParam(PARAM_DIRECTORIES, "src/main/java/dir")
.execute()
.assertJson(this.getClass(), "no_issue.json");

ws.newRequest() ws.newRequest()
.setParam(PARAM_DIRECTORIES, "src/main/java/dir") .setParam(PARAM_DIRECTORIES, "src/main/java/dir")
.execute() .execute()

+ 1
- 88
server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchActionFacetsTest.java View File

import static org.sonar.db.component.ComponentTesting.newFileDto; import static org.sonar.db.component.ComponentTesting.newFileDto;
import static org.sonar.db.component.ComponentTesting.newModuleDto; import static org.sonar.db.component.ComponentTesting.newModuleDto;
import static org.sonar.server.tester.UserSessionRule.standalone; import static org.sonar.server.tester.UserSessionRule.standalone;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.FACET_MODE_EFFORT;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_COMPONENT_KEYS; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_COMPONENT_KEYS;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_COMPONENT_UUIDS;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_FILES; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_FILES;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_MODULE_UUIDS;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_PROJECTS; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_PROJECTS;


public class SearchActionFacetsTest { public class SearchActionFacetsTest {
tuple("assignees", of("", 0L, user.getLogin(), 1L))); tuple("assignees", of("", 0L, user.getLogin(), 1L)));
} }


@Test
public void display_facets_in_effort_mode() {
ComponentDto project = db.components().insertPublicProject();
ComponentDto file = db.components().insertComponent(newFileDto(project));
RuleDefinitionDto rule = db.rules().insertIssueRule();
db.issues().insertIssue(rule, project, file, i -> i
.setSeverity("MAJOR")
.setStatus("OPEN")
.setType(RuleType.CODE_SMELL)
.setEffort(10L)
.setAssigneeUuid(null));
indexPermissions();
indexIssues();

SearchWsResponse response = ws.newRequest()
.setParam(PARAM_COMPONENT_KEYS, project.getKey())
.setParam(FACETS, "severities,statuses,resolutions,rules,types,languages,projects,files,assignees")
.setParam("facetMode", FACET_MODE_EFFORT)
.executeProtobuf(SearchWsResponse.class);

Map<String, Number> expectedStatuses = ImmutableMap.<String, Number>builder().put("OPEN", 10L).put("CONFIRMED", 0L)
.put("REOPENED", 0L).put("RESOLVED", 0L).put("CLOSED", 0L).build();

assertThat(response.getFacets().getFacetsList())
.extracting(Common.Facet::getProperty, facet -> facet.getValuesList().stream().collect(toMap(FacetValue::getVal, FacetValue::getCount)))
.containsExactlyInAnyOrder(
tuple("severities", of("INFO", 0L, "MINOR", 0L, "MAJOR", 10L, "CRITICAL", 0L, "BLOCKER", 0L)),
tuple("statuses", expectedStatuses),
tuple("resolutions", of("", 10L, "FALSE-POSITIVE", 0L, "FIXED", 0L, "REMOVED", 0L, "WONTFIX", 0L)),
tuple("rules", of(rule.getKey().toString(), 10L)),
tuple("types", of("CODE_SMELL", 10L, "BUG", 0L, "VULNERABILITY", 0L)),
tuple("languages", of(rule.getLanguage(), 10L)),
tuple("projects", of(project.getKey(), 10L)),
tuple("files", of(file.path(), 10L)),
tuple("assignees", of("", 10L)));
}

@Test @Test
public void display_projects_facet() { public void display_projects_facet() {
ComponentDto project = db.components().insertPublicProject(); ComponentDto project = db.components().insertPublicProject();
.containsExactlyInAnyOrder(tuple("projects", of(project1.getKey(), 1L, project2.getKey(), 1L, project3.getKey(), 1L))); .containsExactlyInAnyOrder(tuple("projects", of(project1.getKey(), 1L, project2.getKey(), 1L, project3.getKey(), 1L)));
} }


@Test
public void display_moduleUuids_facet_using_project() {
ComponentDto project = db.components().insertPublicProject();
ComponentDto module = db.components().insertComponent(newModuleDto(project));
ComponentDto subModule1 = db.components().insertComponent(newModuleDto(module));
ComponentDto subModule2 = db.components().insertComponent(newModuleDto(module));
ComponentDto subModule3 = db.components().insertComponent(newModuleDto(module));
ComponentDto file1 = db.components().insertComponent(newFileDto(subModule1));
ComponentDto file2 = db.components().insertComponent(newFileDto(subModule2));
RuleDefinitionDto rule = db.rules().insertIssueRule();
db.issues().insertIssue(rule, project, file1);
db.issues().insertIssue(rule, project, file2);
indexPermissions();
indexIssues();

SearchWsResponse response = ws.newRequest()
.setParam(PARAM_PROJECTS, project.getKey())
.setParam(PARAM_COMPONENT_UUIDS, module.uuid())
.setParam(PARAM_MODULE_UUIDS, subModule1.uuid())
.setParam(WebService.Param.FACETS, "moduleUuids")
.executeProtobuf(SearchWsResponse.class);

assertThat(response.getFacets().getFacetsList())
.extracting(Common.Facet::getProperty, facet -> facet.getValuesList().stream().collect(toMap(FacetValue::getVal, FacetValue::getCount)))
.containsExactlyInAnyOrder(tuple("moduleUuids", of(subModule1.uuid(), 1L, subModule2.uuid(), 1L)));
}

@Test
public void fail_to_display_module_facet_when_no_project_is_set() {
ComponentDto project = db.components().insertPublicProject();
ComponentDto module = db.components().insertComponent(newModuleDto(project));
ComponentDto file = db.components().insertComponent(newFileDto(module, null));
RuleDefinitionDto rule = db.rules().insertIssueRule();
db.issues().insertIssue(rule, project, file);
indexPermissions();
indexIssues();

expectedException.expect(IllegalArgumentException.class);
expectedException.expectMessage("Facet(s) 'moduleUuids' require to also filter by project");

ws.newRequest()
.setParam(PARAM_COMPONENT_UUIDS, module.uuid())
.setParam(WebService.Param.FACETS, "moduleUuids")
.execute();
}

@Test @Test
public void display_directory_facet_using_project() { public void display_directory_facet_using_project() {
ComponentDto project = db.components().insertPublicProject(); ComponentDto project = db.components().insertPublicProject();


SearchWsResponse response = ws.newRequest() SearchWsResponse response = ws.newRequest()
.setParam(PARAM_PROJECTS, project1.getKey() + "," + project2.getKey()) .setParam(PARAM_PROJECTS, project1.getKey() + "," + project2.getKey())
.setParam(PARAM_MODULE_UUIDS, module1.uuid() + "," + module2.uuid())
.setParam(PARAM_FILES, file1.path() + "," + file2.path()) .setParam(PARAM_FILES, file1.path() + "," + file2.path())
.setParam("rules", rule1.getKey().toString() + "," + rule2.getKey().toString()) .setParam("rules", rule1.getKey().toString() + "," + rule2.getKey().toString())
.setParam("severities", "MAJOR,MINOR") .setParam("severities", "MAJOR,MINOR")
tuple("types", of("CODE_SMELL", 1L, "BUG", 0L, "VULNERABILITY", 0L)), tuple("types", of("CODE_SMELL", 1L, "BUG", 0L, "VULNERABILITY", 0L)),
tuple("languages", of(rule1.getLanguage(), 1L, rule2.getLanguage(), 0L)), tuple("languages", of(rule1.getLanguage(), 1L, rule2.getLanguage(), 0L)),
tuple("projects", of(project1.getKey(), 1L, project2.getKey(), 0L)), tuple("projects", of(project1.getKey(), 1L, project2.getKey(), 0L)),
tuple("moduleUuids", of(module1.uuid(), 1L, module2.uuid(), 0L)),
tuple("moduleUuids", of(module1.uuid(), 1L)),
tuple("files", of(file1.path(), 1L, file2.path(), 0L)), tuple("files", of(file1.path(), 1L, file2.path(), 0L)),
tuple("assignees", of("", 0L, user1.getLogin(), 1L, user2.getLogin(), 0L))); tuple("assignees", of("", 0L, user1.getLogin(), 1L, user2.getLogin(), 0L)));
} }

+ 2
- 35
server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchActionTest.java View File

.assertJson(this.getClass(), "filter_by_main_scope_2.json"); .assertJson(this.getClass(), "filter_by_main_scope_2.json");
} }


@Test
public void search_by_deprecated_authors_parameter() {
ComponentDto project = db.components().insertPublicProject();
ComponentDto file = db.components().insertComponent(newFileDto(project, null));
RuleDefinitionDto rule = db.rules().insertIssueRule();
IssueDto issue1 = db.issues().insertIssue(rule, project, file, i -> i.setAuthorLogin("leia"));
IssueDto issue2 = db.issues().insertIssue(rule, project, file, i -> i.setAuthorLogin("luke"));
indexPermissions();
indexIssues();

SearchWsResponse response = ws.newRequest()
.setParam("authors", "leia")
.setParam(FACETS, "authors")
.executeProtobuf(SearchWsResponse.class);
assertThat(response.getIssuesList()).extracting(Issue::getKey).containsExactlyInAnyOrder(issue1.getKey());
Common.Facet facet = response.getFacets().getFacetsList().get(0);
assertThat(facet.getProperty()).isEqualTo("authors");
assertThat(facet.getValuesList())
.extracting(Common.FacetValue::getVal, Common.FacetValue::getCount)
.containsExactlyInAnyOrder(
tuple("leia", 1L),
tuple("luke", 1L));

// Deprecated parameter 'authors' will be ignored if new parameter 'author' is set
assertThat(ws.newRequest()
.setMultiParam("author", singletonList("luke"))
// This parameter will be ignored
.setParam("authors", "leia")
.executeProtobuf(SearchWsResponse.class).getIssuesList())
.extracting(Issue::getKey)
.containsExactlyInAnyOrder(issue2.getKey());
}

@Test @Test
public void sort_by_updated_at() { public void sort_by_updated_at() {
RuleDto rule = newIssueRule(); RuleDto rule = newIssueRule();
assertThat(def.responseExampleAsString()).isNotEmpty(); assertThat(def.responseExampleAsString()).isNotEmpty();


assertThat(def.params()).extracting("key").containsExactlyInAnyOrder( assertThat(def.params()).extracting("key").containsExactlyInAnyOrder(
"additionalFields", "asc", "assigned", "assignees", "authors", "author", "componentKeys", "branch", "pullRequest", "createdAfter", "createdAt",
"createdBefore", "createdInLast", "directories", "facetMode", "facets", "files", "issues", "scopes", "languages", "moduleUuids", "onComponentOnly",
"additionalFields", "asc", "assigned", "assignees", "author", "componentKeys", "branch", "pullRequest", "createdAfter", "createdAt",
"createdBefore", "createdInLast", "directories", "facets", "files", "issues", "scopes", "languages", "onComponentOnly",
"p", "projects", "ps", "resolutions", "resolved", "rules", "s", "severities", "sinceLeakPeriod", "statuses", "tags", "types", "owaspTop10", "sansTop25", "p", "projects", "ps", "resolutions", "resolved", "rules", "s", "severities", "sinceLeakPeriod", "statuses", "tags", "types", "owaspTop10", "sansTop25",
"cwe", "sonarsourceSecurity", "timeZone"); "cwe", "sonarsourceSecurity", "timeZone");



+ 1
- 1
server/sonar-webserver-webapi/src/test/java/org/sonar/server/permission/ws/PermissionsWsModuleTest.java View File

public void verify_count_of_added_components() { public void verify_count_of_added_components() {
ComponentContainer container = new ComponentContainer(); ComponentContainer container = new ComponentContainer();
new PermissionsWsModule().configure(container); new PermissionsWsModule().configure(container);
assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 27);
assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 26);
} }
} }

+ 0
- 119
server/sonar-webserver-webapi/src/test/java/org/sonar/server/permission/ws/SearchGlobalPermissionsActionTest.java View File

/*
* SonarQube
* Copyright (C) 2009-2021 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.permission.ws;

import org.junit.Before;
import org.junit.Test;
import org.sonar.api.resources.Qualifiers;
import org.sonar.api.resources.ResourceTypes;
import org.sonar.core.permission.GlobalPermissions;
import org.sonar.db.component.ResourceTypesRule;
import org.sonar.db.user.UserDto;
import org.sonar.server.exceptions.ForbiddenException;
import org.sonar.server.exceptions.UnauthorizedException;
import org.sonar.server.l18n.I18nRule;
import org.sonar.server.permission.PermissionService;
import org.sonar.server.permission.PermissionServiceImpl;
import org.sonarqube.ws.Permissions;

import static org.assertj.core.api.Assertions.assertThat;
import static org.sonar.core.permission.GlobalPermissions.SCAN_EXECUTION;
import static org.sonar.db.permission.GlobalPermission.SCAN;

public class SearchGlobalPermissionsActionTest extends BasePermissionWsTest<SearchGlobalPermissionsAction> {

private I18nRule i18n = new I18nRule();
private ResourceTypes resourceTypes = new ResourceTypesRule().setRootQualifiers(Qualifiers.PROJECT);
private PermissionService permissionService = new PermissionServiceImpl(resourceTypes);

@Override
protected SearchGlobalPermissionsAction buildWsAction() {
return new SearchGlobalPermissionsAction(db.getDbClient(), userSession, i18n, permissionService);
}

@Before
public void setUp() {
initI18nMessages();
}

@Test
public void search() {
loginAsAdmin();

UserDto user = db.users().insertUser();
db.users().insertPermissionOnUser(user, SCAN);

Permissions.WsSearchGlobalPermissionsResponse result = newRequest()
.executeProtobuf(Permissions.WsSearchGlobalPermissionsResponse.class);

assertThat(result.getPermissionsCount()).isEqualTo(GlobalPermissions.ALL.size());
for (Permissions.Permission permission : result.getPermissionsList()) {
if (permission.getKey().equals(SCAN_EXECUTION)) {
assertThat(permission.getUsersCount()).isEqualTo(1);
} else {
assertThat(permission.getUsersCount()).isZero();
}
}
}

@Test
public void supports_protobuf_response() {
loginAsAdmin();

Permissions.WsSearchGlobalPermissionsResponse result = newRequest()
.executeProtobuf(Permissions.WsSearchGlobalPermissionsResponse.class);

assertThat(result).isNotNull();
}

@Test
public void fail_if_not_admin() {
userSession.logIn();

expectedException.expect(ForbiddenException.class);

newRequest()
.execute();
}

@Test
public void fail_if_not_logged_in() {
userSession.anonymous();

expectedException.expect(UnauthorizedException.class);

newRequest().execute();
}

private void initI18nMessages() {
i18n.put("global_permissions.admin", "Administer System");
i18n.put("global_permissions.admin.desc", "Ability to perform all administration functions for the instance: " +
"global configuration and personalization of default dashboards.");
i18n.put("global_permissions.profileadmin", "Administer Quality Profiles");
i18n.put("global_permissions.profileadmin.desc", "Ability to perform any action on the quality profiles.");
i18n.put("global_permissions.gateadmin", "Administer Quality Gates");
i18n.put("global_permissions.gateadmin.desc", "Ability to perform any action on the quality gates.");
i18n.put("global_permissions.scan", "Execute Analysis");
i18n.put("global_permissions.scan.desc", "Ability to execute analyses, and to get all settings required to perform the analysis, " +
"even the secured ones like the scm account password, the jira account password, and so on.");
i18n.put("global_permissions.provisioning", "Create Projects");
i18n.put("global_permissions.provisioning.desc", "Ability to initialize project structure before first analysis.");
}
}

+ 0
- 61
server/sonar-webserver-webapi/src/test/java/org/sonar/server/permission/ws/SearchProjectPermissionsDataTest.java View File

/*
* SonarQube
* Copyright (C) 2009-2021 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.permission.ws;

import com.google.common.collect.HashBasedTable;
import java.util.Collections;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

public class SearchProjectPermissionsDataTest {
@Rule
public ExpectedException expectedException = ExpectedException.none();

@Test
public void fail_if_no_projects() {
expectedException.expect(IllegalStateException.class);

SearchProjectPermissionsData.newBuilder()
.groupCountByProjectIdAndPermission(HashBasedTable.create())
.userCountByProjectIdAndPermission(HashBasedTable.create())
.build();
}

@Test
public void fail_if_no_group_count() {
expectedException.expect(IllegalStateException.class);

SearchProjectPermissionsData.newBuilder()
.rootComponents(Collections.emptyList())
.userCountByProjectIdAndPermission(HashBasedTable.create())
.build();
}

@Test
public void fail_if_no_user_count() {
expectedException.expect(IllegalStateException.class);

SearchProjectPermissionsData.newBuilder()
.rootComponents(Collections.emptyList())
.groupCountByProjectIdAndPermission(HashBasedTable.create())
.build();
}
}

+ 0
- 294
server/sonar-webserver-webapi/src/test/java/org/sonar/server/project/ws/BulkUpdateKeyActionTest.java View File

/*
* SonarQube
* Copyright (C) 2009-2021 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.ws;

import javax.annotation.Nullable;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.sonar.api.server.ws.WebService;
import org.sonar.api.utils.System2;
import org.sonar.api.web.UserRole;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.DbTester;
import org.sonar.db.component.ComponentDbTester;
import org.sonar.db.component.ComponentDto;
import org.sonar.server.component.ComponentFinder;
import org.sonar.server.component.ComponentService;
import org.sonar.server.component.TestComponentFinder;
import org.sonar.server.es.EsTester;
import org.sonar.server.exceptions.BadRequestException;
import org.sonar.server.exceptions.ForbiddenException;
import org.sonar.server.exceptions.NotFoundException;
import org.sonar.server.tester.UserSessionRule;
import org.sonar.server.ws.TestRequest;
import org.sonar.server.ws.WsActionTester;
import org.sonarqube.ws.Projects.BulkUpdateKeyWsResponse;
import org.sonarqube.ws.Projects.BulkUpdateKeyWsResponse.Key;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.tuple;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.sonar.db.component.ComponentTesting.newFileDto;
import static org.sonar.db.component.ComponentTesting.newModuleDto;
import static org.sonar.test.JsonAssert.assertJson;
import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_DRY_RUN;
import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_FROM;
import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_PROJECT;
import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_TO;

public class BulkUpdateKeyActionTest {

private static final String MY_PROJECT_KEY = "my_project";
private static final String FROM = "my_";
private static final String TO = "your_";

private System2 system2 = System2.INSTANCE;

@Rule
public ExpectedException expectedException = ExpectedException.none();
@Rule
public UserSessionRule userSession = UserSessionRule.standalone().logIn().setRoot();
@Rule
public EsTester es = EsTester.create();
@Rule
public DbTester db = DbTester.create(system2);

private ComponentDbTester componentDb = new ComponentDbTester(db);
private DbClient dbClient = db.getDbClient();
private ComponentFinder componentFinder = TestComponentFinder.from(db);
private ComponentService componentService = mock(ComponentService.class);
private WsActionTester ws = new WsActionTester(
new BulkUpdateKeyAction(dbClient, componentFinder, componentService, userSession));

@Test
public void json_example() {
ComponentDto project = componentDb.insertPrivateProject(c -> c.setDbKey("my_project"));
componentDb.insertComponent(newModuleDto(project).setDbKey("my_project:module_1"));
ComponentDto anotherProject = componentDb.insertPrivateProject(c -> c.setDbKey("another_project"));
componentDb.insertComponent(newModuleDto(anotherProject).setDbKey("my_new_project:module_1"));
ComponentDto module2 = componentDb.insertComponent(newModuleDto(project).setDbKey("my_project:module_2"));
componentDb.insertComponent(newFileDto(module2, null));

String result = ws.newRequest()
.setParam(PARAM_PROJECT, "my_project")
.setParam(PARAM_FROM, "my_")
.setParam(PARAM_TO, "my_new_")
.setParam(PARAM_DRY_RUN, String.valueOf(true))
.execute().getInput();

assertJson(result).withStrictArrayOrder().isSimilarTo(getClass().getResource("bulk_update_key-example.json"));
}

@Test
public void dry_run_by_key() {
insertMyProject();

BulkUpdateKeyWsResponse result = callDryRunByKey(MY_PROJECT_KEY, FROM, TO);

assertThat(result.getKeysCount()).isEqualTo(1);
assertThat(result.getKeys(0).getNewKey()).isEqualTo("your_project");
}

@Test
public void bulk_update_project_key() {
ComponentDto project = insertMyProject();
ComponentDto module = componentDb.insertComponent(newModuleDto(project).setDbKey("my_project:root:module"));
ComponentDto inactiveModule = componentDb.insertComponent(newModuleDto(project).setDbKey("my_project:root:inactive_module").setEnabled(false));
ComponentDto file = componentDb.insertComponent(newFileDto(module, null).setDbKey("my_project:root:module:src/File.xoo"));
ComponentDto inactiveFile = componentDb.insertComponent(newFileDto(module, null).setDbKey("my_project:root:module:src/InactiveFile.xoo").setEnabled(false));

BulkUpdateKeyWsResponse result = callByKey(project.getDbKey(), FROM, TO);

assertThat(result.getKeysCount()).isEqualTo(2);
assertThat(result.getKeysList()).extracting(Key::getKey, Key::getNewKey, Key::getDuplicate)
.containsExactly(
tuple(project.getDbKey(), "your_project", false),
tuple(module.getDbKey(), "your_project:root:module", false));

verify(componentService).bulkUpdateKey(any(DbSession.class), eq(componentDb.getProjectDto(project)), eq(FROM), eq(TO));
}

@Test
public void bulk_update_provisioned_project_key() {
String newKey = "provisionedProject2";
ComponentDto provisionedProject = componentDb.insertPrivateProject();

callByKey(provisionedProject.getDbKey(), provisionedProject.getDbKey(), newKey);

verify(componentService).bulkUpdateKey(any(DbSession.class), eq(componentDb.getProjectDto(provisionedProject)), eq(provisionedProject.getDbKey()), eq(newKey));
}

@Test
public void fail_to_bulk_update_key_using_branch_db_key() {
ComponentDto project = db.components().insertPrivateProject();
ComponentDto branch = db.components().insertProjectBranch(project);
userSession.addProjectPermission(UserRole.USER, project);

expectedException.expect(NotFoundException.class);
expectedException.expectMessage(String.format("Project '%s' not found", branch.getDbKey()));

callByKey(branch.getDbKey(), FROM, TO);
}

@Test
public void fail_to_bulk_if_a_component_already_exists_with_the_same_key() {
componentDb.insertPrivateProject(c -> c.setDbKey("my_project"));
componentDb.insertPrivateProject(c -> c.setDbKey("your_project"));

expectedException.expect(BadRequestException.class);
expectedException.expectMessage("Impossible to update key: a component with key \"your_project\" already exists.");

callByKey("my_project", "my_", "your_");
}

@Test
public void fail_to_bulk_update_with_invalid_new_key() {
insertMyProject();

expectedException.expect(IllegalArgumentException.class);
expectedException.expectMessage("Malformed key for 'my?project'. Allowed characters are alphanumeric, '-', '_', '.' and ':', with at least one non-digit.");

callByKey(MY_PROJECT_KEY, FROM, "my?");
}

@Test
public void fail_to_dry_bulk_update_with_invalid_new_key() {
insertMyProject();

expectedException.expect(IllegalArgumentException.class);
expectedException.expectMessage("Malformed key for 'my?project'. Allowed characters are alphanumeric, '-', '_', '.' and ':', with at least one non-digit.");

callDryRunByKey(MY_PROJECT_KEY, FROM, "my?");
}

@Test
public void fail_to_bulk_update_if_not_project_or_module() {
ComponentDto project = insertMyProject();
ComponentDto file = componentDb.insertComponent(newFileDto(project, null));

expectedException.expect(NotFoundException.class);
expectedException.expectMessage(String.format("Project '%s' not found", file.getDbKey()));

callByKey(file.getDbKey(), FROM, TO);
}

@Test
public void fail_if_from_string_is_not_provided() {
expectedException.expect(IllegalArgumentException.class);

ComponentDto project = insertMyProject();

callDryRunByKey(project.getDbKey(), null, TO);
}

@Test
public void fail_if_to_string_is_not_provided() {
expectedException.expect(IllegalArgumentException.class);

ComponentDto project = insertMyProject();

callDryRunByKey(project.getDbKey(), FROM, null);
}

@Test
public void fail_if_key_not_provided() {
expectedException.expect(IllegalArgumentException.class);

call(null, FROM, TO, false);
}

@Test
public void fail_if_project_does_not_exist() {
expectedException.expect(NotFoundException.class);

callDryRunByKey("UNKNOWN_KEY", FROM, TO);
}

@Test
public void throw_ForbiddenException_if_not_project_administrator() {
userSession.logIn();
ComponentDto project = insertMyProject();

expectedException.expect(ForbiddenException.class);

callDryRunByKey(project.getDbKey(), FROM, TO);
}

@Test
public void fail_when_using_branch_db_key() {
ComponentDto project = db.components().insertPrivateProject();
userSession.logIn().addProjectPermission(UserRole.USER, project);
ComponentDto branch = db.components().insertProjectBranch(project);

expectedException.expect(NotFoundException.class);
expectedException.expectMessage(String.format("Project '%s' not found", branch.getDbKey()));

callByKey(branch.getDbKey(), FROM, TO);
}

@Test
public void api_definition() {
WebService.Action definition = ws.getDef();

assertThat(definition.isPost()).isTrue();
assertThat(definition.since()).isEqualTo("6.1");
assertThat(definition.key()).isEqualTo("bulk_update_key");
assertThat(definition.params())
.hasSize(4)
.extracting(WebService.Param::key)
.containsOnlyOnce("project", "from", "to", "dryRun");
}

private ComponentDto insertMyProject() {
return componentDb.insertPublicProject(c -> c.setDbKey(MY_PROJECT_KEY));
}

private BulkUpdateKeyWsResponse callDryRunByKey(@Nullable String key, @Nullable String from, @Nullable String to) {
return call(key, from, to, true);
}

private BulkUpdateKeyWsResponse callByKey(@Nullable String key, @Nullable String from, @Nullable String to) {
return call(key, from, to, false);
}

private BulkUpdateKeyWsResponse call(@Nullable String key, @Nullable String from, @Nullable String to, @Nullable Boolean dryRun) {
TestRequest request = ws.newRequest();

if (key != null) {
request.setParam(PARAM_PROJECT, key);
}
if (from != null) {
request.setParam(PARAM_FROM, from);
}
if (to != null) {
request.setParam(PARAM_TO, to);
}
if (dryRun != null) {
request.setParam(PARAM_DRY_RUN, String.valueOf(dryRun));
}

return request.executeProtobuf(BulkUpdateKeyWsResponse.class);
}
}

+ 1
- 1
server/sonar-webserver-webapi/src/test/java/org/sonar/server/project/ws/ProjectsWsModuleTest.java View File

public void verify_count_of_added_components_on_SonarQube() { public void verify_count_of_added_components_on_SonarQube() {
ComponentContainer container = new ComponentContainer(); ComponentContainer container = new ComponentContainer();
new ProjectsWsModule().configure(container); new ProjectsWsModule().configure(container);
assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 12);
assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 11);
} }


} }

+ 0
- 110
server/sonar-webserver-webapi/src/test/java/org/sonar/server/source/ws/HashActionTest.java View File

/*
* SonarQube
* Copyright (C) 2009-2021 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.source.ws;

import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.sonar.api.utils.System2;
import org.sonar.db.DbTester;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.source.FileSourceDto;
import org.sonar.server.component.TestComponentFinder;
import org.sonar.server.exceptions.ForbiddenException;
import org.sonar.server.exceptions.NotFoundException;
import org.sonar.server.tester.UserSessionRule;
import org.sonar.server.ws.TestRequest;
import org.sonar.server.ws.WsActionTester;

import static java.lang.String.format;
import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.sonar.api.web.UserRole.USER;
import static org.sonar.db.component.ComponentTesting.newFileDto;

public class HashActionTest {

@Rule
public ExpectedException expectedException = ExpectedException.none();
@Rule
public DbTester db = DbTester.create(System2.INSTANCE);
@Rule
public UserSessionRule userSessionRule = UserSessionRule.standalone();

private final WsActionTester tester = new WsActionTester(new HashAction(db.getDbClient(), userSessionRule, TestComponentFinder.from(db)));

@Test
public void show_hashes() {
ComponentDto project = db.components().insertPrivateProject();
ComponentDto file = db.components().insertComponent(newFileDto(project));
FileSourceDto fileSource = db.fileSources().insertFileSource(file, f -> f.setLineHashes(singletonList("ABC")));
loginAsProjectViewer(project);

TestRequest request = tester.newRequest().setParam("key", file.getDbKey());

assertThat(request.execute().getInput()).isEqualTo("ABC");
}

@Test
public void hashes_empty_if_no_source() {
ComponentDto project = db.components().insertPrivateProject();
ComponentDto file = db.components().insertComponent(newFileDto(project));
loginAsProjectViewer(project);

TestRequest request = tester.newRequest().setParam("key", file.getKey());

assertThat(request.execute().getStatus()).isEqualTo(204);
}

@Test
public void fail_to_show_hashes_if_file_does_not_exist() {
expectedException.expect(NotFoundException.class);

tester.newRequest().setParam("key", "unknown").execute();
}

@Test
public void fail_when_using_branch_db_key() {
ComponentDto project = db.components().insertPrivateProject();
ComponentDto branch = db.components().insertProjectBranch(project);
loginAsProjectViewer(project);

expectedException.expect(NotFoundException.class);
expectedException.expectMessage(format("Component key '%s' not found", branch.getDbKey()));

tester.newRequest().setParam("key", branch.getDbKey()).execute();
}

@Test
public void fail_on_missing_permission() {
ComponentDto project = db.components().insertPrivateProject();
ComponentDto file = db.components().insertComponent(newFileDto(project));
FileSourceDto fileSource = db.fileSources().insertFileSource(file);
userSessionRule.logIn(db.users().insertUser());

expectedException.expect(ForbiddenException.class);

tester.newRequest().setParam("key", file.getKey()).execute();
}

private void loginAsProjectViewer(ComponentDto project) {
userSessionRule.logIn(db.users().insertUser()).addProjectPermission(USER, project);
}
}

+ 1
- 1
server/sonar-webserver-webapi/src/test/java/org/sonar/server/source/ws/SourceWsModuleTest.java View File

public void verify_count_of_actions() { public void verify_count_of_actions() {
ComponentContainer container = new ComponentContainer(); ComponentContainer container = new ComponentContainer();
underTest.configure(container); underTest.configure(container);
assertThat(container.getPicoContainer().getComponentAdapters(WsAction.class)).hasSize(7);
assertThat(container.getPicoContainer().getComponentAdapters(WsAction.class)).hasSize(6);
} }


} }

+ 0
- 10
server/sonar-webserver/src/test/java/org/sonar/server/platform/web/WebServiceReroutingFilterTest.java View File

assertRedirection("/api/projects/update_key", "POST"); assertRedirection("/api/projects/update_key", "POST");
} }


@Test
public void redirect_components_bulk_update_key() {
when(request.getServletPath()).thenReturn("/api/components/bulk_update_key");
when(request.getMethod()).thenReturn("POST");

underTest.doFilter(request, response, chain);

assertRedirection("/api/projects/bulk_update_key", "POST");
}

private void assertRedirection(String path, String method) { private void assertRedirection(String path, String method) {
verify(webServiceEngine).execute(servletRequestCaptor.capture(), any(ServletResponse.class)); verify(webServiceEngine).execute(servletRequestCaptor.capture(), any(ServletResponse.class));
assertThat(servletRequestCaptor.getValue().getPath()).isEqualTo(path); assertThat(servletRequestCaptor.getValue().getPath()).isEqualTo(path);

+ 0
- 101
sonar-ws-generator/src/main/resources/snapshot-of-api.json View File

} }
] ]
}, },
{
"key": "search_global_permissions",
"description": "List global permissions. <br />Requires the following permission: 'Administer System'",
"since": "5.2",
"deprecatedSince": "6.5",
"internal": false,
"post": false,
"hasResponseExample": true,
"changelog": [],
"params": [
{
"key": "organization",
"description": "Key of organization, used when group name is set",
"since": "6.2",
"required": false,
"internal": true,
"exampleValue": "my-org"
}
]
},
{
"key": "search_project_permissions",
"description": "List project permissions. A project can be a technical project, a view or a developer.<br />Requires one of the following permissions:<ul><li>'Administer System'</li><li>'Administer' rights on the specified project</li></ul>",
"since": "5.2",
"deprecatedSince": "6.5",
"internal": false,
"post": false,
"hasResponseExample": true,
"changelog": [],
"params": [
{
"key": "p",
"description": "1-based page number",
"required": false,
"internal": false,
"defaultValue": "1",
"exampleValue": "42",
"deprecatedKey": "pageIndex",
"deprecatedKeySince": "5.2"
},
{
"key": "projectId",
"description": "Project id",
"required": false,
"internal": false,
"exampleValue": "ce4c03d6-430f-40a9-b777-ad877c00aa4d"
},
{
"key": "projectKey",
"description": "Project key",
"required": false,
"internal": false,
"exampleValue": "my_project"
},
{
"key": "ps",
"description": "Page size. Must be greater than 0.",
"required": false,
"internal": false,
"defaultValue": "25",
"exampleValue": "20",
"deprecatedKey": "pageSize",
"deprecatedKeySince": "5.2"
},
{
"key": "q",
"description": "Limit search to: <ul><li>project names that contain the supplied string</li><li>project keys that are exactly the same as the supplied string</li></ul>",
"required": false,
"internal": false,
"exampleValue": "apac"
},
{
"key": "qualifier",
"description": "Project qualifier. Filter the results with the specified qualifier. Possible values are:<ul><li>TRK - Projects</li></ul>",
"since": "5.3",
"required": false,
"internal": false,
"possibleValues": [
"TRK"
]
}
]
},
{ {
"key": "search_templates", "key": "search_templates",
"description": "List permission templates.<br />Requires the following permission: 'Administer System'.", "description": "List permission templates.<br />Requires the following permission: 'Administer System'.",
} }
] ]
}, },
{
"key": "unset_default",
"description": "Unset a quality gate as the default quality gate.<br>Requires the 'Administer Quality Gates' permission.",
"since": "4.3",
"internal": false,
"post": true,
"hasResponseExample": false,
"changelog": [],
"params": [
{
"key": "id",
"description": "ID of the quality gate to unset as default",
"required": true,
"internal": false,
"exampleValue": "1"
}
]
},
{ {
"key": "update_condition", "key": "update_condition",
"description": "Update a condition attached to a quality gate.<br>Requires the 'Administer Quality Gates' permission.", "description": "Update a condition attached to a quality gate.<br>Requires the 'Administer Quality Gates' permission.",

+ 0
- 14
sonar-ws/src/main/java/org/sonarqube/ws/client/issue/IssuesWsParameters.java View File

public static final String PARAM_RESOLVED = "resolved"; public static final String PARAM_RESOLVED = "resolved";
public static final String PARAM_COMPONENT_KEYS = "componentKeys"; public static final String PARAM_COMPONENT_KEYS = "componentKeys";
public static final String PARAM_COMPONENT_UUIDS = "componentUuids"; public static final String PARAM_COMPONENT_UUIDS = "componentUuids";
public static final String PARAM_MODULE_UUIDS = "moduleUuids";
public static final String PARAM_PROJECTS = "projects"; public static final String PARAM_PROJECTS = "projects";
public static final String PARAM_DIRECTORIES = "directories"; public static final String PARAM_DIRECTORIES = "directories";
public static final String PARAM_FILES = "files"; public static final String PARAM_FILES = "files";
public static final String PARAM_SEND_NOTIFICATIONS = "sendNotifications"; public static final String PARAM_SEND_NOTIFICATIONS = "sendNotifications";
public static final String PARAM_ASSIGNEES = "assignees"; public static final String PARAM_ASSIGNEES = "assignees";


/**
* @deprecated since 7.7, please use 'author' instead
*/
@Deprecated
public static final String DEPRECATED_PARAM_AUTHORS = "authors";


public static final String PARAM_AUTHOR = "author"; public static final String PARAM_AUTHOR = "author";
public static final String PARAM_SCOPES = "scopes"; public static final String PARAM_SCOPES = "scopes";
public static final String PARAM_CREATED_BEFORE = "createdBefore"; public static final String PARAM_CREATED_BEFORE = "createdBefore";
public static final String PARAM_CREATED_IN_LAST = "createdInLast"; public static final String PARAM_CREATED_IN_LAST = "createdInLast";
public static final String PARAM_SINCE_LEAK_PERIOD = "sinceLeakPeriod"; public static final String PARAM_SINCE_LEAK_PERIOD = "sinceLeakPeriod";
public static final String PARAM_PAGE_SIZE = "pageSize";
public static final String PARAM_PAGE_INDEX = "pageIndex";
public static final String PARAM_ASC = "asc"; public static final String PARAM_ASC = "asc";
public static final String PARAM_ADDITIONAL_FIELDS = "additionalFields"; public static final String PARAM_ADDITIONAL_FIELDS = "additionalFields";
public static final String PARAM_TIMEZONE = "timeZone"; public static final String PARAM_TIMEZONE = "timeZone";


/**
* @deprecated since 7.9
*/
@Deprecated
public static final String FACET_MODE = "facetMode";
public static final String FACET_MODE_COUNT = "count";
public static final String FACET_MODE_EFFORT = "effort"; public static final String FACET_MODE_EFFORT = "effort";


private IssuesWsParameters() { private IssuesWsParameters() {

+ 0
- 3
sonar-ws/src/main/java/org/sonarqube/ws/client/issues/IssuesService.java View File

.setParam("assigned", request.getAssigned()) .setParam("assigned", request.getAssigned())
.setParam("assignees", request.getAssignees() == null ? null : request.getAssignees().stream().collect(Collectors.joining(","))) .setParam("assignees", request.getAssignees() == null ? null : request.getAssignees().stream().collect(Collectors.joining(",")))
.setParam("author", request.getAuthor()) .setParam("author", request.getAuthor())
.setParam("authors", request.getAuthors() == null ? null : request.getAuthors().stream().collect(Collectors.joining(",")))
.setParam("branch", request.getBranch()) .setParam("branch", request.getBranch())
.setParam("componentKeys", request.getComponentKeys() == null ? null : request.getComponentKeys().stream().collect(Collectors.joining(","))) .setParam("componentKeys", request.getComponentKeys() == null ? null : request.getComponentKeys().stream().collect(Collectors.joining(",")))
.setParam("componentUuids", request.getComponentUuids() == null ? null : request.getComponentUuids().stream().collect(Collectors.joining(","))) .setParam("componentUuids", request.getComponentUuids() == null ? null : request.getComponentUuids().stream().collect(Collectors.joining(",")))
.setParam("createdInLast", request.getCreatedInLast()) .setParam("createdInLast", request.getCreatedInLast())
.setParam("cwe", request.getCwe() == null ? null : request.getCwe().stream().collect(Collectors.joining(","))) .setParam("cwe", request.getCwe() == null ? null : request.getCwe().stream().collect(Collectors.joining(",")))
.setParam("directories", request.getDirectories() == null ? null : request.getDirectories().stream().collect(Collectors.joining(","))) .setParam("directories", request.getDirectories() == null ? null : request.getDirectories().stream().collect(Collectors.joining(",")))
.setParam("facetMode", request.getFacetMode())
.setParam("facets", request.getFacets() == null ? null : request.getFacets().stream().collect(Collectors.joining(","))) .setParam("facets", request.getFacets() == null ? null : request.getFacets().stream().collect(Collectors.joining(",")))
.setParam("fileUuids", request.getFileUuids() == null ? null : request.getFileUuids().stream().collect(Collectors.joining(","))) .setParam("fileUuids", request.getFileUuids() == null ? null : request.getFileUuids().stream().collect(Collectors.joining(",")))
.setParam("issues", request.getIssues() == null ? null : request.getIssues().stream().collect(Collectors.joining(","))) .setParam("issues", request.getIssues() == null ? null : request.getIssues().stream().collect(Collectors.joining(",")))
.setParam("languages", request.getLanguages() == null ? null : request.getLanguages().stream().collect(Collectors.joining(","))) .setParam("languages", request.getLanguages() == null ? null : request.getLanguages().stream().collect(Collectors.joining(",")))
.setParam("moduleUuids", request.getModuleUuids() == null ? null : request.getModuleUuids().stream().collect(Collectors.joining(",")))
.setParam("onComponentOnly", request.getOnComponentOnly()) .setParam("onComponentOnly", request.getOnComponentOnly())
.setParam("owaspTop10", request.getOwaspTop10() == null ? null : request.getOwaspTop10().stream().collect(Collectors.joining(","))) .setParam("owaspTop10", request.getOwaspTop10() == null ? null : request.getOwaspTop10().stream().collect(Collectors.joining(",")))
.setParam("p", request.getP()) .setParam("p", request.getP())

+ 0
- 47
sonar-ws/src/main/java/org/sonarqube/ws/client/issues/SearchRequest.java View File

private String assigned; private String assigned;
private List<String> assignees; private List<String> assignees;
private List<String> author; private List<String> author;
private List<String> authors;
private String branch; private String branch;
private List<String> componentKeys; private List<String> componentKeys;
private List<String> componentUuids; private List<String> componentUuids;
private List<String> fileUuids; private List<String> fileUuids;
private List<String> issues; private List<String> issues;
private List<String> languages; private List<String> languages;
private List<String> moduleUuids;
private String onComponentOnly; private String onComponentOnly;
private List<String> owaspTop10; private List<String> owaspTop10;
private String p; private String p;
return author; return author;
} }


/**
* Example value: "torvalds@linux-foundation.org"
* @deprecated since 7.7
*/
@Deprecated
public SearchRequest setAuthors(List<String> authors) {
this.authors = authors;
return this;
}

public List<String> getAuthors() {
return authors;
}

/** /**
* This is part of the internal API. * This is part of the internal API.
* Example value: "feature/my_branch" * Example value: "feature/my_branch"
return directories; return directories;
} }


/**
* Possible values:
* <ul>
* <li>"count"</li>
* <li>"effort"</li>
* </ul>
*/
public SearchRequest setFacetMode(String facetMode) {
this.facetMode = facetMode;
return this;
}

public String getFacetMode() {
return facetMode;
}

/** /**
* Possible values: * Possible values:
* <ul> * <ul>
return languages; return languages;
} }


/**
* This is part of the internal API.
* Example value: "7d8749e8-3070-4903-9188-bdd82933bb92"
* @deprecated since 7.6
*/
@Deprecated
public SearchRequest setModuleUuids(List<String> moduleUuids) {
this.moduleUuids = moduleUuids;
return this;
}

public List<String> getModuleUuids() {
return moduleUuids;
}

/** /**
* Possible values: * Possible values:
* <ul> * <ul>

+ 6
- 44
sonar-ws/src/main/java/org/sonarqube/ws/client/permissions/PermissionsService.java View File

import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.annotation.Generated; import javax.annotation.Generated;
import org.sonarqube.ws.MediaTypes; import org.sonarqube.ws.MediaTypes;
import org.sonarqube.ws.client.BaseService;
import org.sonarqube.ws.client.GetRequest;
import org.sonarqube.ws.client.PostRequest;
import org.sonarqube.ws.client.WsConnector;
import org.sonarqube.ws.Permissions.CreateTemplateWsResponse; import org.sonarqube.ws.Permissions.CreateTemplateWsResponse;
import org.sonarqube.ws.Permissions.WsGroupsResponse;
import org.sonarqube.ws.Permissions.WsSearchGlobalPermissionsResponse;
import org.sonarqube.ws.Permissions.SearchProjectPermissionsWsResponse;
import org.sonarqube.ws.Permissions.SearchTemplatesWsResponse; import org.sonarqube.ws.Permissions.SearchTemplatesWsResponse;
import org.sonarqube.ws.Permissions.WsTemplateGroupsResponse;
import org.sonarqube.ws.Permissions.UpdateTemplateWsResponse; import org.sonarqube.ws.Permissions.UpdateTemplateWsResponse;
import org.sonarqube.ws.Permissions.UsersWsResponse; import org.sonarqube.ws.Permissions.UsersWsResponse;
import org.sonarqube.ws.Permissions.WsGroupsResponse;
import org.sonarqube.ws.Permissions.WsTemplateGroupsResponse;
import org.sonarqube.ws.client.BaseService;
import org.sonarqube.ws.client.GetRequest;
import org.sonarqube.ws.client.PostRequest;
import org.sonarqube.ws.client.WsConnector;


/** /**
* @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/permissions">Further information about this web service online</a> * @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/permissions">Further information about this web service online</a>
).content(); ).content();
} }


/**
*
* This is part of the internal API.
* This is a GET request.
* @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/permissions/search_global_permissions">Further information about this action online (including a response example)</a>
* @since 5.2
* @deprecated since 6.5
*/
@Deprecated
public WsSearchGlobalPermissionsResponse searchGlobalPermissions(SearchGlobalPermissionsRequest request) {
return call(
new GetRequest(path("search_global_permissions")),
WsSearchGlobalPermissionsResponse.parser());
}

/**
*
* This is part of the internal API.
* This is a GET request.
* @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/permissions/search_project_permissions">Further information about this action online (including a response example)</a>
* @since 5.2
* @deprecated since 6.5
*/
@Deprecated
public SearchProjectPermissionsWsResponse searchProjectPermissions(SearchProjectPermissionsRequest request) {
return call(
new GetRequest(path("search_project_permissions"))
.setParam("p", request.getP())
.setParam("projectId", request.getProjectId())
.setParam("projectKey", request.getProjectKey())
.setParam("ps", request.getPs())
.setParam("q", request.getQ())
.setParam("qualifier", request.getQualifier()),
SearchProjectPermissionsWsResponse.parser());
}

/** /**
* *
* This is part of the internal API. * This is part of the internal API.

+ 0
- 33
sonar-ws/src/main/java/org/sonarqube/ws/client/permissions/SearchGlobalPermissionsRequest.java View File

/*
* SonarQube
* Copyright (C) 2009-2021 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.sonarqube.ws.client.permissions;

import javax.annotation.Generated;

/**
* This is part of the internal API.
* This is a POST request.
* @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/permissions/search_global_permissions">Further information about this action online (including a response example)</a>
* @since 5.2
*/
@Generated("sonar-ws-generator")
public class SearchGlobalPermissionsRequest {

}

+ 0
- 2
sonar-ws/src/main/java/org/sonarqube/ws/client/project/ProjectsWsParameters.java View File

public static final String CONTROLLER = "api/projects"; public static final String CONTROLLER = "api/projects";


public static final String ACTION_CREATE = "create"; public static final String ACTION_CREATE = "create";
public static final String ACTION_INDEX = "index";
public static final String ACTION_SEARCH = "search"; public static final String ACTION_SEARCH = "search";
public static final String ACTION_UPDATE_KEY = "update_key"; public static final String ACTION_UPDATE_KEY = "update_key";
public static final String ACTION_BULK_UPDATE_KEY = "bulk_update_key";
public static final String ACTION_UPDATE_VISIBILITY = "update_visibility"; public static final String ACTION_UPDATE_VISIBILITY = "update_visibility";


public static final String PARAM_PROJECT = "project"; public static final String PARAM_PROJECT = "project";

+ 0
- 94
sonar-ws/src/main/java/org/sonarqube/ws/client/projects/BulkUpdateKeyRequest.java View File

/*
* SonarQube
* Copyright (C) 2009-2021 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.sonarqube.ws.client.projects;

import javax.annotation.Generated;

/**
* This is part of the internal API.
* This is a POST request.
* @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/projects/bulk_update_key">Further information about this action online (including a response example)</a>
* @since 6.1
*/
@Generated("sonar-ws-generator")
public class BulkUpdateKeyRequest {

private String dryRun;
private String from;
private String project;
private String to;

/**
* Possible values:
* <ul>
* <li>"true"</li>
* <li>"false"</li>
* <li>"yes"</li>
* <li>"no"</li>
* </ul>
*/
public BulkUpdateKeyRequest setDryRun(String dryRun) {
this.dryRun = dryRun;
return this;
}

public String getDryRun() {
return dryRun;
}

/**
* This is a mandatory parameter.
* Example value: "_old"
*/
public BulkUpdateKeyRequest setFrom(String from) {
this.from = from;
return this;
}

public String getFrom() {
return from;
}

/**
* This is a mandatory parameter.
* Example value: "my_old_project"
*/
public BulkUpdateKeyRequest setProject(String project) {
this.project = project;
return this;
}

public String getProject() {
return project;
}

/**
* This is a mandatory parameter.
* Example value: "_new"
*/
public BulkUpdateKeyRequest setTo(String to) {
this.to = to;
return this;
}

public String getTo() {
return to;
}
}

+ 0
- 18
sonar-ws/src/main/java/org/sonarqube/ws/client/projects/ProjectsService.java View File

import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.annotation.Generated; import javax.annotation.Generated;
import org.sonarqube.ws.MediaTypes; import org.sonarqube.ws.MediaTypes;
import org.sonarqube.ws.Projects.BulkUpdateKeyWsResponse;
import org.sonarqube.ws.Projects.CreateWsResponse; import org.sonarqube.ws.Projects.CreateWsResponse;
import org.sonarqube.ws.Projects.SearchMyProjectsWsResponse; import org.sonarqube.ws.Projects.SearchMyProjectsWsResponse;
import org.sonarqube.ws.Projects.SearchWsResponse; import org.sonarqube.ws.Projects.SearchWsResponse;
).content(); ).content();
} }


/**
*
* This is part of the internal API.
* This is a POST request.
* @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/projects/bulk_update_key">Further information about this action online (including a response example)</a>
* @since 6.1
*/
public BulkUpdateKeyWsResponse bulkUpdateKey(BulkUpdateKeyRequest request) {
return call(
new PostRequest(path("bulk_update_key"))
.setParam("dryRun", request.getDryRun())
.setParam("from", request.getFrom())
.setParam("project", request.getProject())
.setParam("to", request.getTo()),
BulkUpdateKeyWsResponse.parser());
}

/** /**
* *
* This is part of the internal API. * This is part of the internal API.

+ 0
- 15
sonar-ws/src/main/java/org/sonarqube/ws/client/qualitygates/QualitygatesService.java View File

ShowWsResponse.parser()); ShowWsResponse.parser());
} }


/**
* This is part of the internal API.
* This is a POST request.
*
* @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/qualitygates/unset_default">Further information about this action online (including a response example)</a>
* @since 4.3
* @deprecated since 7.0
*/
@Deprecated
public String unsetDefault() {
return call(
new PostRequest(path("unset_default"))
.setMediaType(MediaTypes.JSON)).content();
}

/** /**
* This is part of the internal API. * This is part of the internal API.
* This is a POST request. * This is a POST request.

+ 0
- 47
sonar-ws/src/main/java/org/sonarqube/ws/client/qualitygates/UnsetDefaultRequest.java View File

/*
* SonarQube
* Copyright (C) 2009-2021 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.sonarqube.ws.client.qualitygates;

import javax.annotation.Generated;

/**
* This is part of the internal API.
* This is a POST request.
* @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/qualitygates/unset_default">Further information about this action online (including a response example)</a>
* @since 4.3
*/
@Generated("sonar-ws-generator")
public class UnsetDefaultRequest {

private String id;

/**
* This is a mandatory parameter.
* Example value: "1"
*/
public UnsetDefaultRequest setId(String id) {
this.id = id;
return this;
}

public String getId() {
return id;
}
}

+ 0
- 15
sonar-ws/src/main/java/org/sonarqube/ws/client/sources/SourcesService.java View File

super(wsConnector, "api/sources"); super(wsConnector, "api/sources");
} }


/**
*
* This is part of the internal API.
* This is a GET request.
* @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/sources/hash">Further information about this action online (including a response example)</a>
* @since 5.0
*/
public String hash(HashRequest request) {
return call(
new GetRequest(path("hash"))
.setParam("key", request.getKey())
.setMediaType(MediaTypes.JSON)
).content();
}

/** /**
* *
* This is part of the internal API. * This is part of the internal API.

+ 0
- 19
sonar-ws/src/main/java/org/sonarqube/ws/client/views/CreateRequest.java View File

private String description; private String description;
private String key; private String key;
private String name; private String name;
private String qualifier;
private String visibility; private String visibility;


/** /**
return name; return name;
} }


/**
* Possible values:
* <ul>
* <li>"VW"</li>
* <li>"APP"</li>
* </ul>
* @deprecated since 7.3
*/
@Deprecated
public CreateRequest setQualifier(String qualifier) {
this.qualifier = qualifier;
return this;
}

public String getQualifier() {
return qualifier;
}

/** /**
* Possible values: * Possible values:
* <ul> * <ul>

+ 0
- 102
sonar-ws/src/main/java/org/sonarqube/ws/client/views/ModeRequest.java View File

/*
* SonarQube
* Copyright (C) 2009-2021 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.sonarqube.ws.client.views;

import javax.annotation.Generated;

/**
* This is part of the internal API.
* This is a POST request.
* @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/views/mode">Further information about this action online (including a response example)</a>
* @since 2.6
*/
@Generated("sonar-ws-generator")
public class ModeRequest {

private String key;
private String measure;
private String regexp;
private String selectionMode;
private String value;

/**
* This is a mandatory parameter.
*/
public ModeRequest setKey(String key) {
this.key = key;
return this;
}

public String getKey() {
return key;
}

/**
*/
public ModeRequest setMeasure(String measure) {
this.measure = measure;
return this;
}

public String getMeasure() {
return measure;
}

/**
*/
public ModeRequest setRegexp(String regexp) {
this.regexp = regexp;
return this;
}

public String getRegexp() {
return regexp;
}

/**
* This is a mandatory parameter.
* Possible values:
* <ul>
* <li>"MANUAL"</li>
* <li>"REGEXP"</li>
* <li>"MANUAL_MEASURE"</li>
* <li>"REST"</li>
* </ul>
*/
public ModeRequest setSelectionMode(String selectionMode) {
this.selectionMode = selectionMode;
return this;
}

public String getSelectionMode() {
return selectionMode;
}

/**
*/
public ModeRequest setValue(String value) {
this.value = value;
return this;
}

public String getValue() {
return value;
}
}

+ 0
- 59
sonar-ws/src/main/java/org/sonarqube/ws/client/views/RegexpRequest.java View File

/*
* SonarQube
* Copyright (C) 2009-2021 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.sonarqube.ws.client.views;

import javax.annotation.Generated;

/**
* This is part of the internal API.
* This is a POST request.
* @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/views/regexp">Further information about this action online (including a response example)</a>
* @since 1.0
*/
@Generated("sonar-ws-generator")
public class RegexpRequest {

private String key;
private String regexp;

/**
* This is a mandatory parameter.
*/
public RegexpRequest setKey(String key) {
this.key = key;
return this;
}

public String getKey() {
return key;
}

/**
* This is a mandatory parameter.
*/
public RegexpRequest setRegexp(String regexp) {
this.regexp = regexp;
return this;
}

public String getRegexp() {
return regexp;
}
}

sonar-ws/src/main/java/org/sonarqube/ws/client/sources/HashRequest.java → sonar-ws/src/main/java/org/sonarqube/ws/client/views/SetNoneModeRequest.java View File

* along with this program; if not, write to the Free Software Foundation, * along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/ */
package org.sonarqube.ws.client.sources;
package org.sonarqube.ws.client.views;


import javax.annotation.Generated; import javax.annotation.Generated;


/** /**
* This is part of the internal API. * This is part of the internal API.
* This is a POST request. * This is a POST request.
* @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/sources/hash">Further information about this action online (including a response example)</a>
* @since 5.0
* @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/views/set_none_mode">Further information about this action online (including a response example)</a>
* @since 9.1
*/ */
@Generated("sonar-ws-generator") @Generated("sonar-ws-generator")
public class HashRequest {
public class SetNoneModeRequest {


private String key;
private String portfolio;


/** /**
* This is a mandatory parameter. * This is a mandatory parameter.
* Example value: "my_project:/src/foo/Bar.php"
*/ */
public HashRequest setKey(String key) {
this.key = key;
public SetNoneModeRequest setPortfolio(String portfolio) {
this.portfolio = portfolio;
return this; return this;
} }


public String getKey() {
return key;
public String getPortfolio() {
return portfolio;
} }

} }

+ 15
- 38
sonar-ws/src/main/java/org/sonarqube/ws/client/views/ViewsService.java View File

.setParam("description", request.getDescription()) .setParam("description", request.getDescription())
.setParam("key", request.getKey()) .setParam("key", request.getKey())
.setParam("name", request.getName()) .setParam("name", request.getName())
.setParam("qualifier", request.getQualifier())
.setParam("visibility", request.getVisibility()) .setParam("visibility", request.getVisibility())
.setMediaType(MediaTypes.JSON) .setMediaType(MediaTypes.JSON)
).content(); ).content();
).content(); ).content();
} }


/**
*
* This is part of the internal API.
* This is a POST request.
* @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/views/mode">Further information about this action online (including a response example)</a>
* @since 2.6
* @deprecated since 7.4
*/
public void mode(ModeRequest request) {
call(
new PostRequest(path("mode"))
.setParam("key", request.getKey())
.setParam("measure", request.getMeasure())
.setParam("regexp", request.getRegexp())
.setParam("selectionMode", request.getSelectionMode())
.setParam("value", request.getValue())
.setMediaType(MediaTypes.JSON)
).content();
}

/** /**
* *
* This is part of the internal API. * This is part of the internal API.
).content(); ).content();
} }


/**
*
* This is part of the internal API.
* This is a POST request.
* @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/views/regexp">Further information about this action online (including a response example)</a>
* @since 1.0
* @deprecated since 7.4
*/
public void regexp(RegexpRequest request) {
call(
new PostRequest(path("regexp"))
.setParam("key", request.getKey())
.setParam("regexp", request.getRegexp())
.setMediaType(MediaTypes.JSON)
).content();
}

/** /**
* *
* This is part of the internal API. * This is part of the internal API.
).content(); ).content();
} }


/**
*
* This is part of the internal API.
* This is a POST request.
* @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/views/set_none_mode">Further information about this action online (including a response example)</a>
* @since 9.1
*/
public void setNoneMode(SetNoneModeRequest request) {
call(
new PostRequest(path("set_none_mode"))
.setParam("portfolio", request.getPortfolio())
.setMediaType(MediaTypes.JSON)
).content();
}

/** /**
* *
* This is part of the internal API. * This is part of the internal API.

+ 0
- 11
sonar-ws/src/main/protobuf/ws-projects.proto View File

optional string revision = 8; optional string revision = 8;
} }
} }

// WS api/projects/prepare_bulk_update_key
message BulkUpdateKeyWsResponse {
repeated Key keys = 1;

message Key {
optional string key = 1;
optional string newKey = 2;
optional bool duplicate = 3;
}
}

Loading…
Cancel
Save