3 * Copyright (C) 2009-2024 SonarSource SA
4 * mailto:info AT sonarsource DOT com
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 3 of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this program; if not, write to the Free Software Foundation,
18 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 package org.sonar.server.almintegration.ws.gitlab;
22 import java.util.List;
24 import java.util.Optional;
26 import java.util.function.BinaryOperator;
27 import java.util.function.Function;
28 import java.util.stream.Collectors;
29 import org.sonar.alm.client.gitlab.GitlabApplicationClient;
30 import org.sonar.alm.client.gitlab.Project;
31 import org.sonar.alm.client.gitlab.ProjectList;
32 import org.sonar.api.server.ws.Request;
33 import org.sonar.api.server.ws.Response;
34 import org.sonar.api.server.ws.WebService;
35 import org.sonar.db.DbClient;
36 import org.sonar.db.DbSession;
37 import org.sonar.db.alm.pat.AlmPatDto;
38 import org.sonar.db.alm.setting.AlmSettingDto;
39 import org.sonar.db.alm.setting.ProjectAlmSettingDto;
40 import org.sonar.server.almintegration.ws.AlmIntegrationsWsAction;
41 import org.sonar.server.exceptions.NotFoundException;
42 import org.sonar.server.user.UserSession;
43 import org.sonarqube.ws.AlmIntegrations;
44 import org.sonarqube.ws.AlmIntegrations.GitlabRepository;
45 import org.sonarqube.ws.Common.Paging;
47 import static java.util.Objects.requireNonNull;
48 import static java.util.stream.Collectors.toSet;
49 import static org.sonar.db.permission.GlobalPermission.PROVISION_PROJECTS;
50 import static org.sonar.server.ws.WsUtils.writeProtobuf;
52 public class SearchGitlabReposAction implements AlmIntegrationsWsAction {
54 private static final String PARAM_ALM_SETTING = "almSetting";
55 private static final String PARAM_PROJECT_NAME = "projectName";
56 private static final int DEFAULT_PAGE_SIZE = 20;
57 private static final int MAX_PAGE_SIZE = 500;
59 private final DbClient dbClient;
60 private final UserSession userSession;
61 private final GitlabApplicationClient gitlabApplicationClient;
63 public SearchGitlabReposAction(DbClient dbClient, UserSession userSession, GitlabApplicationClient gitlabApplicationClient) {
64 this.dbClient = dbClient;
65 this.userSession = userSession;
66 this.gitlabApplicationClient = gitlabApplicationClient;
70 public void define(WebService.NewController context) {
71 WebService.NewAction action = context.createAction("search_gitlab_repos")
72 .setDescription("Search the GitLab projects.<br/>" +
73 "Requires the 'Create Projects' permission")
78 action.createParam(PARAM_ALM_SETTING)
80 .setMaximumLength(200)
81 .setDescription("DevOps Platform setting key");
82 action.createParam(PARAM_PROJECT_NAME)
84 .setMaximumLength(200)
85 .setDescription("Project name filter");
87 action.addPagingParams(DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE);
89 action.setResponseExample(getClass().getResource("search_gitlab_repos.json"));
93 public void handle(Request request, Response response) {
94 AlmIntegrations.SearchGitlabReposWsResponse wsResponse = doHandle(request);
95 writeProtobuf(wsResponse, request, response);
98 private AlmIntegrations.SearchGitlabReposWsResponse doHandle(Request request) {
99 String almSettingKey = request.mandatoryParam(PARAM_ALM_SETTING);
100 String projectName = request.param(PARAM_PROJECT_NAME);
102 int pageNumber = request.mandatoryParamAsInt("p");
103 int pageSize = request.mandatoryParamAsInt("ps");
105 try (DbSession dbSession = dbClient.openSession(false)) {
106 userSession.checkLoggedIn().checkPermission(PROVISION_PROJECTS);
108 String userUuid = requireNonNull(userSession.getUuid(), "User UUID cannot be null");
109 AlmSettingDto almSettingDto = dbClient.almSettingDao().selectByKey(dbSession, almSettingKey)
110 .orElseThrow(() -> new NotFoundException(String.format("DevOps Platform Setting '%s' not found", almSettingKey)));
111 Optional<AlmPatDto> almPatDto = dbClient.almPatDao().selectByUserAndAlmSetting(dbSession, userUuid, almSettingDto);
113 String personalAccessToken = almPatDto.map(AlmPatDto::getPersonalAccessToken).orElseThrow(() -> new IllegalArgumentException("No personal access token found"));
114 String gitlabUrl = requireNonNull(almSettingDto.getUrl(), "DevOps Platform url cannot be null");
116 ProjectList gitlabProjectList = gitlabApplicationClient
117 .searchProjects(gitlabUrl, personalAccessToken, projectName, pageNumber, pageSize);
119 Map<String, ProjectKeyName> sqProjectsKeyByGitlabProjectId = getSqProjectsKeyByGitlabProjectId(dbSession, almSettingDto, gitlabProjectList);
121 List<GitlabRepository> gitlabRepositories = gitlabProjectList.getProjects().stream()
122 .map(project -> toGitlabRepository(project, sqProjectsKeyByGitlabProjectId))
125 Paging.Builder pagingBuilder = Paging.newBuilder()
126 .setPageIndex(gitlabProjectList.getPageNumber())
127 .setPageSize(gitlabProjectList.getPageSize());
128 Integer gitlabProjectListTotal = gitlabProjectList.getTotal();
129 if (gitlabProjectListTotal != null) {
130 pagingBuilder.setTotal(gitlabProjectListTotal);
132 return AlmIntegrations.SearchGitlabReposWsResponse.newBuilder()
133 .addAllRepositories(gitlabRepositories)
134 .setPaging(pagingBuilder.build())
139 private Map<String, ProjectKeyName> getSqProjectsKeyByGitlabProjectId(DbSession dbSession, AlmSettingDto almSettingDto,
140 ProjectList gitlabProjectList) {
141 Set<String> gitlabProjectIds = gitlabProjectList.getProjects().stream().map(Project::getId).map(String::valueOf)
143 Map<String, ProjectAlmSettingDto> projectAlmSettingDtos = dbClient.projectAlmSettingDao()
144 .selectByAlmSettingAndRepos(dbSession, almSettingDto, gitlabProjectIds)
145 .stream().collect(Collectors.toMap(ProjectAlmSettingDto::getProjectUuid, Function.identity()));
147 return dbClient.projectDao().selectByUuids(dbSession, projectAlmSettingDtos.keySet())
149 .collect(Collectors.toMap(projectDto -> projectAlmSettingDtos.get(projectDto.getUuid()).getAlmRepo(),
150 p -> new ProjectKeyName(p.getKey(), p.getName()), resolveNameCollisionOperatorByNaturalOrder()));
153 private static BinaryOperator<ProjectKeyName> resolveNameCollisionOperatorByNaturalOrder() {
154 return (a, b) -> b.key.compareTo(a.key) > 0 ? a : b;
157 private static GitlabRepository toGitlabRepository(Project project, Map<String, ProjectKeyName> sqProjectsKeyByGitlabProjectId) {
158 String name = project.getName();
159 String pathName = removeLastOccurrenceOfString(project.getNameWithNamespace(), " / " + name);
161 String slug = project.getPath();
162 String pathSlug = removeLastOccurrenceOfString(project.getPathWithNamespace(), "/" + slug);
164 GitlabRepository.Builder builder = GitlabRepository.newBuilder()
165 .setId(project.getId())
167 .setPathName(pathName)
169 .setPathSlug(pathSlug)
170 .setUrl(project.getWebUrl());
172 String projectIdAsString = String.valueOf(project.getId());
173 Optional.ofNullable(sqProjectsKeyByGitlabProjectId.get(projectIdAsString))
174 .ifPresent(p -> builder
175 .setSqProjectKey(p.key)
176 .setSqProjectName(p.name));
178 return builder.build();
181 private static String removeLastOccurrenceOfString(String string, String stringToRemove) {
182 StringBuilder resultString = new StringBuilder(string);
183 int index = resultString.lastIndexOf(stringToRemove);
185 resultString.delete(index, string.length() + index);
187 return resultString.toString();
190 static class ProjectKeyName {
194 ProjectKeyName(String key, String name) {