You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

SetAction.java 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2024 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  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.
  10. *
  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.
  15. *
  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.
  19. */
  20. package org.sonar.server.newcodeperiod.ws;
  21. import com.google.common.base.Preconditions;
  22. import java.util.EnumSet;
  23. import java.util.Locale;
  24. import java.util.Set;
  25. import javax.annotation.Nullable;
  26. import org.sonar.api.server.ws.Request;
  27. import org.sonar.api.server.ws.Response;
  28. import org.sonar.api.server.ws.WebService;
  29. import org.sonar.api.web.UserRole;
  30. import org.sonar.core.documentation.DocumentationLinkGenerator;
  31. import org.sonar.core.platform.EditionProvider;
  32. import org.sonar.core.platform.PlatformEditionProvider;
  33. import org.sonar.db.DbClient;
  34. import org.sonar.db.DbSession;
  35. import org.sonar.db.component.BranchDto;
  36. import org.sonar.db.component.SnapshotDto;
  37. import org.sonar.db.newcodeperiod.NewCodePeriodDao;
  38. import org.sonar.db.newcodeperiod.NewCodePeriodDto;
  39. import org.sonar.db.newcodeperiod.NewCodePeriodParser;
  40. import org.sonar.db.newcodeperiod.NewCodePeriodType;
  41. import org.sonar.db.project.ProjectDto;
  42. import org.sonar.server.component.ComponentFinder;
  43. import org.sonar.server.exceptions.NotFoundException;
  44. import org.sonar.server.common.newcodeperiod.CaycUtils;
  45. import org.sonar.server.user.UserSession;
  46. import static com.google.common.base.Preconditions.checkArgument;
  47. import static java.lang.String.format;
  48. import static org.sonar.db.newcodeperiod.NewCodePeriodType.NUMBER_OF_DAYS;
  49. import static org.sonar.db.newcodeperiod.NewCodePeriodType.PREVIOUS_VERSION;
  50. import static org.sonar.db.newcodeperiod.NewCodePeriodType.REFERENCE_BRANCH;
  51. import static org.sonar.db.newcodeperiod.NewCodePeriodType.SPECIFIC_ANALYSIS;
  52. import static org.sonar.server.ws.WsUtils.createHtmlExternalLink;
  53. public class SetAction implements NewCodePeriodsWsAction {
  54. private static final String PARAM_BRANCH = "branch";
  55. private static final String PARAM_PROJECT = "project";
  56. private static final String PARAM_TYPE = "type";
  57. private static final String PARAM_VALUE = "value";
  58. private static final String BEGIN_LIST = "<ul>";
  59. private static final String END_LIST = "</ul>";
  60. private static final String BEGIN_ITEM_LIST = "<li>";
  61. private static final String END_ITEM_LIST = "</li>";
  62. private static final Set<NewCodePeriodType> OVERALL_TYPES = EnumSet.of(PREVIOUS_VERSION, NUMBER_OF_DAYS);
  63. private static final Set<NewCodePeriodType> PROJECT_TYPES = EnumSet.of(PREVIOUS_VERSION, NUMBER_OF_DAYS, REFERENCE_BRANCH);
  64. private static final Set<NewCodePeriodType> BRANCH_TYPES = EnumSet.of(PREVIOUS_VERSION, NUMBER_OF_DAYS, SPECIFIC_ANALYSIS,
  65. REFERENCE_BRANCH);
  66. private final DbClient dbClient;
  67. private final UserSession userSession;
  68. private final ComponentFinder componentFinder;
  69. private final PlatformEditionProvider editionProvider;
  70. private final NewCodePeriodDao newCodePeriodDao;
  71. private final String newCodeDefinitionDocumentationUrl;
  72. public SetAction(DbClient dbClient, UserSession userSession, ComponentFinder componentFinder, PlatformEditionProvider editionProvider,
  73. NewCodePeriodDao newCodePeriodDao, DocumentationLinkGenerator documentationLinkGenerator) {
  74. this.dbClient = dbClient;
  75. this.userSession = userSession;
  76. this.componentFinder = componentFinder;
  77. this.editionProvider = editionProvider;
  78. this.newCodePeriodDao = newCodePeriodDao;
  79. this.newCodeDefinitionDocumentationUrl = documentationLinkGenerator.getDocumentationLink("/project-administration/defining-new-code/");
  80. }
  81. @Override
  82. public void define(WebService.NewController context) {
  83. WebService.NewAction action = context.createAction("set")
  84. .setPost(true)
  85. .setDescription("Updates the " + createHtmlExternalLink(newCodeDefinitionDocumentationUrl, "new code definition") +
  86. " on different levels:<br>" +
  87. BEGIN_LIST +
  88. BEGIN_ITEM_LIST + "Not providing a project key and a branch key will update the default value at global level. " +
  89. "Existing projects or branches having a specific new code definition will not be impacted" + END_ITEM_LIST +
  90. BEGIN_ITEM_LIST + "Project key must be provided to update the value for a project" + END_ITEM_LIST +
  91. BEGIN_ITEM_LIST + "Both project and branch keys must be provided to update the value for a branch" + END_ITEM_LIST +
  92. END_LIST +
  93. "Requires one of the following permissions: " +
  94. BEGIN_LIST +
  95. BEGIN_ITEM_LIST + "'Administer System' to change the global setting" + END_ITEM_LIST +
  96. BEGIN_ITEM_LIST + "'Administer' rights on the specified project to change the project setting" + END_ITEM_LIST +
  97. END_LIST)
  98. .setSince("8.0")
  99. .setHandler(this);
  100. action.createParam(PARAM_PROJECT)
  101. .setDescription("Project key");
  102. action.createParam(PARAM_BRANCH)
  103. .setDescription("Branch key");
  104. action.createParam(PARAM_TYPE)
  105. .setRequired(true)
  106. .setDescription("Type<br/>" +
  107. "New code definitions of the following types are allowed:" +
  108. BEGIN_LIST +
  109. BEGIN_ITEM_LIST + SPECIFIC_ANALYSIS.name() + " - can be set at branch level only" + END_ITEM_LIST +
  110. BEGIN_ITEM_LIST + PREVIOUS_VERSION.name() + " - can be set at any level (global, project, branch)" + END_ITEM_LIST +
  111. BEGIN_ITEM_LIST + NUMBER_OF_DAYS.name() + " - can be set at any level (global, project, branch)" + END_ITEM_LIST +
  112. BEGIN_ITEM_LIST + REFERENCE_BRANCH.name() + " - can only be set for projects and branches" + END_ITEM_LIST +
  113. END_LIST
  114. );
  115. action.createParam(PARAM_VALUE)
  116. .setDescription("Value<br/>" +
  117. "For each type, a different value is expected:" +
  118. BEGIN_LIST +
  119. BEGIN_ITEM_LIST + "the uuid of an analysis, when type is " + SPECIFIC_ANALYSIS.name() + END_ITEM_LIST +
  120. BEGIN_ITEM_LIST + "no value, when type is " + PREVIOUS_VERSION.name() + END_ITEM_LIST +
  121. BEGIN_ITEM_LIST + "a number between 1 and 90, when type is " + NUMBER_OF_DAYS.name() + END_ITEM_LIST +
  122. BEGIN_ITEM_LIST + "a string, when type is " + REFERENCE_BRANCH.name() + END_ITEM_LIST +
  123. END_LIST
  124. );
  125. }
  126. @Override
  127. public void handle(Request request, Response response) throws Exception {
  128. String projectKey = request.getParam(PARAM_PROJECT).emptyAsNull().or(() -> null);
  129. String branchKey = request.getParam(PARAM_BRANCH).emptyAsNull().or(() -> null);
  130. if (projectKey == null && branchKey != null) {
  131. throw new IllegalArgumentException("If branch key is specified, project key needs to be specified too");
  132. }
  133. try (DbSession dbSession = dbClient.openSession(false)) {
  134. String typeStr = request.mandatoryParam(PARAM_TYPE);
  135. String valueStr = request.getParam(PARAM_VALUE).emptyAsNull().or(() -> null);
  136. boolean isCommunityEdition = editionProvider.get().filter(t -> t == EditionProvider.Edition.COMMUNITY).isPresent();
  137. NewCodePeriodType type = validateType(typeStr, projectKey == null, branchKey != null || isCommunityEdition);
  138. NewCodePeriodDto dto = new NewCodePeriodDto();
  139. dto.setType(type);
  140. ProjectDto project = null;
  141. BranchDto branch = null;
  142. if (projectKey != null) {
  143. project = getProject(dbSession, projectKey);
  144. userSession.checkEntityPermission(UserRole.ADMIN, project);
  145. if (branchKey != null) {
  146. branch = getBranch(dbSession, project, branchKey);
  147. dto.setBranchUuid(branch.getUuid());
  148. } else if (isCommunityEdition) {
  149. // in CE set main branch value instead of project value
  150. branch = getMainBranch(dbSession, project);
  151. dto.setBranchUuid(branch.getUuid());
  152. }
  153. dto.setProjectUuid(project.getUuid());
  154. } else {
  155. userSession.checkIsSystemAdministrator();
  156. }
  157. setValue(dbSession, dto, type, project, branch, valueStr);
  158. if (!CaycUtils.isNewCodePeriodCompliant(dto.getType(), dto.getValue())) {
  159. throw new IllegalArgumentException("Failed to set the New Code Definition. The given value is not compatible with the Clean as " +
  160. "You Code methodology. Please refer to the documentation for compliant options.");
  161. }
  162. newCodePeriodDao.upsert(dbSession, dto);
  163. dbSession.commit();
  164. }
  165. }
  166. private void setValue(DbSession dbSession, NewCodePeriodDto dto, NewCodePeriodType type, @Nullable ProjectDto project,
  167. @Nullable BranchDto branch, @Nullable String value) {
  168. switch (type) {
  169. case PREVIOUS_VERSION -> Preconditions.checkArgument(value == null, "Unexpected value for type '%s'", type);
  170. case NUMBER_OF_DAYS -> {
  171. requireValue(type, value);
  172. dto.setValue(parseDays(value));
  173. }
  174. case SPECIFIC_ANALYSIS -> {
  175. checkValuesForSpecificBranch(type, value, project, branch);
  176. dto.setValue(getAnalysis(dbSession, value, project, branch).getUuid());
  177. }
  178. case REFERENCE_BRANCH -> {
  179. requireValue(type, value);
  180. dto.setValue(value);
  181. }
  182. default -> throw new IllegalStateException("Unexpected type: " + type);
  183. }
  184. }
  185. private static void checkValuesForSpecificBranch(NewCodePeriodType type, @Nullable String value, @Nullable ProjectDto project, @Nullable BranchDto branch) {
  186. requireValue(type, value);
  187. requireProject(type, project);
  188. requireBranch(type, branch);
  189. }
  190. private static String parseDays(String value) {
  191. try {
  192. return Integer.toString(NewCodePeriodParser.parseDays(value));
  193. } catch (Exception e) {
  194. throw new IllegalArgumentException("Failed to parse number of days: " + value);
  195. }
  196. }
  197. private static void requireValue(NewCodePeriodType type, @Nullable String value) {
  198. Preconditions.checkArgument(value != null, "New code definition type '%s' requires a value", type);
  199. }
  200. private static void requireBranch(NewCodePeriodType type, @Nullable BranchDto branch) {
  201. Preconditions.checkArgument(branch != null, "New code definition type '%s' requires a branch", type);
  202. }
  203. private static void requireProject(NewCodePeriodType type, @Nullable ProjectDto project) {
  204. Preconditions.checkArgument(project != null, "New code definition type '%s' requires a project", type);
  205. }
  206. private BranchDto getBranch(DbSession dbSession, ProjectDto project, String branchKey) {
  207. return dbClient.branchDao().selectByBranchKey(dbSession, project.getUuid(), branchKey)
  208. .orElseThrow(() -> new NotFoundException(format("Branch '%s' in project '%s' not found", branchKey, project.getKey())));
  209. }
  210. private ProjectDto getProject(DbSession dbSession, String projectKey) {
  211. return componentFinder.getProjectByKey(dbSession, projectKey);
  212. }
  213. private BranchDto getMainBranch(DbSession dbSession, ProjectDto project) {
  214. return dbClient.branchDao().selectByProject(dbSession, project)
  215. .stream().filter(BranchDto::isMain)
  216. .findFirst()
  217. .orElseThrow(() -> new NotFoundException(format("Main branch in project '%s' is not found", project.getKey())));
  218. }
  219. private static NewCodePeriodType validateType(String typeStr, boolean isOverall, boolean isBranch) {
  220. NewCodePeriodType type;
  221. try {
  222. type = NewCodePeriodType.valueOf(typeStr.toUpperCase(Locale.US));
  223. } catch (IllegalArgumentException e) {
  224. throw new IllegalArgumentException("Invalid type: " + typeStr);
  225. }
  226. if (isOverall) {
  227. checkType("Overall setting", OVERALL_TYPES, type);
  228. } else if (isBranch) {
  229. checkType("Branches", BRANCH_TYPES, type);
  230. } else {
  231. checkType("Projects", PROJECT_TYPES, type);
  232. }
  233. return type;
  234. }
  235. private SnapshotDto getAnalysis(DbSession dbSession, String analysisUuid, ProjectDto project, BranchDto branch) {
  236. SnapshotDto snapshotDto = dbClient.snapshotDao().selectByUuid(dbSession, analysisUuid)
  237. .orElseThrow(() -> new NotFoundException(format("Analysis '%s' is not found", analysisUuid)));
  238. checkAnalysis(dbSession, project, branch, snapshotDto);
  239. return snapshotDto;
  240. }
  241. private void checkAnalysis(DbSession dbSession, ProjectDto project, BranchDto branch, SnapshotDto analysis) {
  242. BranchDto analysisBranch = dbClient.branchDao().selectByUuid(dbSession, analysis.getRootComponentUuid()).orElse(null);
  243. boolean analysisMatchesProjectBranch = analysisBranch != null && analysisBranch.getUuid().equals(branch.getUuid());
  244. checkArgument(analysisMatchesProjectBranch,
  245. "Analysis '%s' does not belong to branch '%s' of project '%s'",
  246. analysis.getUuid(), branch.getKey(), project.getKey());
  247. }
  248. private static void checkType(String name, Set<NewCodePeriodType> validTypes, NewCodePeriodType type) {
  249. if (!validTypes.contains(type)) {
  250. throw new IllegalArgumentException(String.format("Invalid type '%s'. %s can only be set with types: %s", type, name, validTypes));
  251. }
  252. }
  253. }