3 * Copyright (C) 2009-2020 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.qualitygate.ws;
22 import java.util.Arrays;
23 import java.util.Optional;
24 import java.util.stream.Collectors;
25 import javax.annotation.Nullable;
26 import javax.annotation.concurrent.Immutable;
27 import org.sonar.api.measures.CoreMetrics;
28 import org.sonar.api.server.ws.Change;
29 import org.sonar.api.server.ws.Request;
30 import org.sonar.api.server.ws.Response;
31 import org.sonar.api.server.ws.WebService;
32 import org.sonar.api.web.UserRole;
33 import org.sonar.core.util.Uuids;
34 import org.sonar.db.DbClient;
35 import org.sonar.db.DbSession;
36 import org.sonar.db.component.BranchDto;
37 import org.sonar.db.component.SnapshotDto;
38 import org.sonar.db.measure.LiveMeasureDto;
39 import org.sonar.db.measure.MeasureDto;
40 import org.sonar.db.project.ProjectDto;
41 import org.sonar.server.component.ComponentFinder;
42 import org.sonar.server.exceptions.BadRequestException;
43 import org.sonar.server.user.UserSession;
44 import org.sonar.server.ws.KeyExamples;
45 import org.sonarqube.ws.Qualitygates.ProjectStatusResponse;
47 import static com.google.common.base.Strings.isNullOrEmpty;
48 import static org.sonar.server.exceptions.BadRequestException.checkRequest;
49 import static org.sonar.server.exceptions.NotFoundException.checkFoundWithOptional;
50 import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.ACTION_PROJECT_STATUS;
51 import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.PARAM_ANALYSIS_ID;
52 import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.PARAM_BRANCH;
53 import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.PARAM_PROJECT_ID;
54 import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.PARAM_PROJECT_KEY;
55 import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.PARAM_PULL_REQUEST;
56 import static org.sonar.server.user.AbstractUserSession.insufficientPrivilegesException;
57 import static org.sonar.server.ws.WsUtils.writeProtobuf;
59 public class ProjectStatusAction implements QualityGatesWsAction {
60 private static final String QG_STATUSES_ONE_LINE = Arrays.stream(ProjectStatusResponse.Status.values())
62 .collect(Collectors.joining(", "));
63 private static final String MSG_ONE_PROJECT_PARAMETER_ONLY = String.format("Either '%s', '%s' or '%s' must be provided", PARAM_ANALYSIS_ID, PARAM_PROJECT_ID, PARAM_PROJECT_KEY);
64 private static final String MSG_ONE_BRANCH_PARAMETER_ONLY = String.format("Either '%s' or '%s' can be provided, not both", PARAM_BRANCH, PARAM_PULL_REQUEST);
66 private final DbClient dbClient;
67 private final ComponentFinder componentFinder;
68 private final UserSession userSession;
70 public ProjectStatusAction(DbClient dbClient, ComponentFinder componentFinder, UserSession userSession) {
71 this.dbClient = dbClient;
72 this.componentFinder = componentFinder;
73 this.userSession = userSession;
77 public void define(WebService.NewController controller) {
78 WebService.NewAction action = controller.createAction(ACTION_PROJECT_STATUS)
79 .setDescription(String.format("Get the quality gate status of a project or a Compute Engine task.<br />" +
80 MSG_ONE_PROJECT_PARAMETER_ONLY + "<br />" +
81 "The different statuses returned are: %s. The %s status is returned when there is no quality gate associated with the analysis.<br />" +
82 "Returns an HTTP code 404 if the analysis associated with the task is not found or does not exist.<br />" +
83 "Requires one of the following permissions:" +
85 "<li>'Administer System'</li>" +
86 "<li>'Administer' rights on the specified project</li>" +
87 "<li>'Browse' on the specified project</li>" +
88 "</ul>", QG_STATUSES_ONE_LINE, ProjectStatusResponse.Status.NONE))
89 .setResponseExample(getClass().getResource("project_status-example.json"))
93 new Change("7.7", "The parameters 'branch' and 'pullRequest' were added"),
94 new Change("7.6", "The field 'warning' is deprecated from the response"),
95 new Change("6.4", "The field 'ignoredConditions' is added to the response"));
97 action.createParam(PARAM_ANALYSIS_ID)
98 .setDescription("Analysis id")
99 .setExampleValue(Uuids.UUID_EXAMPLE_04);
101 action.createParam(PARAM_PROJECT_ID)
103 .setDescription("Project UUID. Doesn't work with branches or pull requests")
104 .setExampleValue(Uuids.UUID_EXAMPLE_01);
106 action.createParam(PARAM_PROJECT_KEY)
108 .setDescription("Project key")
109 .setExampleValue(KeyExamples.KEY_PROJECT_EXAMPLE_001);
111 action.createParam(PARAM_BRANCH)
113 .setDescription("Branch key")
114 .setExampleValue(KeyExamples.KEY_BRANCH_EXAMPLE_001);
116 action.createParam(PARAM_PULL_REQUEST)
118 .setDescription("Pull request id")
119 .setExampleValue(KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001);
123 public void handle(Request request, Response response) throws Exception {
124 String analysisId = request.param(PARAM_ANALYSIS_ID);
125 String projectId = request.param(PARAM_PROJECT_ID);
126 String projectKey = request.param(PARAM_PROJECT_KEY);
127 String branchKey = request.param(PARAM_BRANCH);
128 String pullRequestId = request.param(PARAM_PULL_REQUEST);
130 !isNullOrEmpty(analysisId)
131 ^ !isNullOrEmpty(projectId)
132 ^ !isNullOrEmpty(projectKey),
133 MSG_ONE_PROJECT_PARAMETER_ONLY);
134 checkRequest(isNullOrEmpty(branchKey) || isNullOrEmpty(pullRequestId), MSG_ONE_BRANCH_PARAMETER_ONLY);
136 try (DbSession dbSession = dbClient.openSession(false)) {
137 ProjectStatusResponse projectStatusResponse = doHandle(dbSession, analysisId, projectId, projectKey, branchKey, pullRequestId);
138 writeProtobuf(projectStatusResponse, request, response);
142 private ProjectStatusResponse doHandle(DbSession dbSession, @Nullable String analysisId, @Nullable String projectUuid,
143 @Nullable String projectKey, @Nullable String branchKey, @Nullable String pullRequestId) {
144 ProjectAndSnapshot projectAndSnapshot = getProjectAndSnapshot(dbSession, analysisId, projectUuid, projectKey, branchKey, pullRequestId);
145 checkPermission(projectAndSnapshot.project);
146 Optional<String> measureData = loadQualityGateDetails(dbSession, projectAndSnapshot, analysisId != null);
148 return ProjectStatusResponse.newBuilder()
149 .setProjectStatus(new QualityGateDetailsFormatter(measureData, projectAndSnapshot.snapshotDto).format())
153 private ProjectAndSnapshot getProjectAndSnapshot(DbSession dbSession, @Nullable String analysisId, @Nullable String projectUuid,
154 @Nullable String projectKey, @Nullable String branchKey, @Nullable String pullRequestId) {
155 if (!isNullOrEmpty(analysisId)) {
156 return getSnapshotThenProject(dbSession, analysisId);
158 if (!isNullOrEmpty(projectUuid) ^ !isNullOrEmpty(projectKey)) {
159 return getProjectThenSnapshot(dbSession, projectUuid, projectKey, branchKey, pullRequestId);
162 throw BadRequestException.create(MSG_ONE_PROJECT_PARAMETER_ONLY);
165 private ProjectAndSnapshot getProjectThenSnapshot(DbSession dbSession, @Nullable String projectUuid, @Nullable String projectKey,
166 @Nullable String branchKey, @Nullable String pullRequestId) {
167 ProjectDto projectDto;
170 if (projectUuid != null) {
171 projectDto = componentFinder.getProjectByUuid(dbSession, projectUuid);
172 branchDto = componentFinder.getMainBranch(dbSession, projectDto);
174 projectDto = componentFinder.getProjectByKey(dbSession, projectKey);
175 branchDto = componentFinder.getBranchOrPullRequest(dbSession, projectDto, branchKey, pullRequestId);
177 Optional<SnapshotDto> snapshot = dbClient.snapshotDao().selectLastAnalysisByRootComponentUuid(dbSession, branchDto.getUuid());
178 return new ProjectAndSnapshot(projectDto, branchDto, snapshot.orElse(null));
181 private ProjectAndSnapshot getSnapshotThenProject(DbSession dbSession, String analysisUuid) {
182 SnapshotDto snapshotDto = getSnapshot(dbSession, analysisUuid);
183 BranchDto branchDto = dbClient.branchDao().selectByUuid(dbSession, snapshotDto.getComponentUuid())
184 .orElseThrow(() -> new IllegalStateException(String.format("Branch '%s' not found", snapshotDto.getUuid())));
185 ProjectDto projectDto = dbClient.projectDao().selectByUuid(dbSession, branchDto.getProjectUuid())
186 .orElseThrow(() -> new IllegalStateException(String.format("Project '%s' not found", branchDto.getProjectUuid())));
188 return new ProjectAndSnapshot(projectDto, branchDto, snapshotDto);
191 private SnapshotDto getSnapshot(DbSession dbSession, String analysisUuid) {
192 Optional<SnapshotDto> snapshotDto = dbClient.snapshotDao().selectByUuid(dbSession, analysisUuid);
193 return checkFoundWithOptional(snapshotDto, "Analysis with id '%s' is not found", analysisUuid);
196 private Optional<String> loadQualityGateDetails(DbSession dbSession, ProjectAndSnapshot projectAndSnapshot, boolean onAnalysis) {
198 if (!projectAndSnapshot.snapshotDto.isPresent()) {
199 return Optional.empty();
201 // get the gate status as it was computed during the specified analysis
202 String analysisUuid = projectAndSnapshot.snapshotDto.get().getUuid();
203 return dbClient.measureDao().selectMeasure(dbSession, analysisUuid, projectAndSnapshot.branch.getUuid(), CoreMetrics.QUALITY_GATE_DETAILS_KEY)
204 .map(MeasureDto::getData);
207 // do not restrict to a specified analysis, use the live measure
208 Optional<LiveMeasureDto> measure = dbClient.liveMeasureDao().selectMeasure(dbSession, projectAndSnapshot.branch.getUuid(), CoreMetrics.QUALITY_GATE_DETAILS_KEY);
209 return measure.map(LiveMeasureDto::getDataAsString);
212 private void checkPermission(ProjectDto project) {
213 if (!userSession.hasProjectPermission(UserRole.ADMIN, project) &&
214 !userSession.hasProjectPermission(UserRole.USER, project)) {
215 throw insufficientPrivilegesException();
220 private static class ProjectAndSnapshot {
221 private final BranchDto branch;
222 private final Optional<SnapshotDto> snapshotDto;
223 private final ProjectDto project;
225 private ProjectAndSnapshot(ProjectDto project, BranchDto branch, @Nullable SnapshotDto snapshotDto) {
226 this.project = project;
227 this.branch = branch;
228 this.snapshotDto = Optional.ofNullable(snapshotDto);