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.

IssueWorkflow.java 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2023 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.issue.workflow;
  21. import java.util.List;
  22. import org.sonar.api.Startable;
  23. import org.sonar.api.ce.ComputeEngineSide;
  24. import org.sonar.api.issue.DefaultTransitions;
  25. import org.sonar.api.issue.Issue;
  26. import org.sonar.api.rules.RuleType;
  27. import org.sonar.api.server.ServerSide;
  28. import org.sonar.api.web.UserRole;
  29. import org.sonar.core.issue.DefaultIssue;
  30. import org.sonar.core.issue.IssueChangeContext;
  31. import org.sonar.core.issue.status.IssueStatus;
  32. import org.sonar.server.issue.IssueFieldsSetter;
  33. import static com.google.common.base.Preconditions.checkArgument;
  34. import static com.google.common.base.Preconditions.checkState;
  35. import static org.sonar.api.issue.Issue.RESOLUTION_ACKNOWLEDGED;
  36. import static org.sonar.api.issue.Issue.RESOLUTION_FALSE_POSITIVE;
  37. import static org.sonar.api.issue.Issue.RESOLUTION_FIXED;
  38. import static org.sonar.api.issue.Issue.RESOLUTION_REMOVED;
  39. import static org.sonar.api.issue.Issue.RESOLUTION_SAFE;
  40. import static org.sonar.api.issue.Issue.RESOLUTION_WONT_FIX;
  41. import static org.sonar.api.issue.Issue.STATUS_CLOSED;
  42. import static org.sonar.api.issue.Issue.STATUS_CONFIRMED;
  43. import static org.sonar.api.issue.Issue.STATUS_OPEN;
  44. import static org.sonar.api.issue.Issue.STATUS_REOPENED;
  45. import static org.sonar.api.issue.Issue.STATUS_RESOLVED;
  46. import static org.sonar.api.issue.Issue.STATUS_REVIEWED;
  47. import static org.sonar.api.issue.Issue.STATUS_TO_REVIEW;
  48. @ServerSide
  49. @ComputeEngineSide
  50. public class IssueWorkflow implements Startable {
  51. private static final String AUTOMATIC_CLOSE_TRANSITION = "automaticclose";
  52. private final FunctionExecutor functionExecutor;
  53. private final IssueFieldsSetter updater;
  54. private StateMachine machine;
  55. public IssueWorkflow(FunctionExecutor functionExecutor, IssueFieldsSetter updater) {
  56. this.functionExecutor = functionExecutor;
  57. this.updater = updater;
  58. }
  59. @Override
  60. public void start() {
  61. StateMachine.Builder builder = StateMachine.builder()
  62. .states(STATUS_OPEN, STATUS_CONFIRMED, STATUS_REOPENED, STATUS_RESOLVED, STATUS_CLOSED,
  63. STATUS_TO_REVIEW, STATUS_REVIEWED);
  64. buildManualTransitions(builder);
  65. buildAutomaticTransitions(builder);
  66. buildSecurityHotspotTransitions(builder);
  67. machine = builder.build();
  68. }
  69. private static void buildManualTransitions(StateMachine.Builder builder) {
  70. builder
  71. // confirm
  72. .transition(Transition.builder(DefaultTransitions.CONFIRM)
  73. .from(STATUS_OPEN).to(STATUS_CONFIRMED)
  74. .conditions(IsNotHotspot.INSTANCE)
  75. .functions(new SetResolution(null))
  76. .build())
  77. .transition(Transition.builder(DefaultTransitions.CONFIRM)
  78. .from(STATUS_REOPENED).to(STATUS_CONFIRMED)
  79. .conditions(IsNotHotspot.INSTANCE)
  80. .functions(new SetResolution(null))
  81. .build())
  82. // resolve as fixed
  83. .transition(Transition.builder(DefaultTransitions.RESOLVE)
  84. .from(STATUS_OPEN).to(STATUS_RESOLVED)
  85. .conditions(IsNotHotspot.INSTANCE)
  86. .functions(new SetResolution(RESOLUTION_FIXED))
  87. .requiredProjectPermission(UserRole.ISSUE_ADMIN)
  88. .build())
  89. .transition(Transition.builder(DefaultTransitions.RESOLVE)
  90. .from(STATUS_REOPENED).to(STATUS_RESOLVED)
  91. .conditions(IsNotHotspot.INSTANCE)
  92. .functions(new SetResolution(RESOLUTION_FIXED))
  93. .requiredProjectPermission(UserRole.ISSUE_ADMIN)
  94. .build())
  95. .transition(Transition.builder(DefaultTransitions.RESOLVE)
  96. .from(STATUS_CONFIRMED).to(STATUS_RESOLVED)
  97. .conditions(IsNotHotspot.INSTANCE)
  98. .functions(new SetResolution(RESOLUTION_FIXED))
  99. .requiredProjectPermission(UserRole.ISSUE_ADMIN)
  100. .build())
  101. // reopen
  102. .transition(Transition.builder(DefaultTransitions.UNCONFIRM)
  103. .from(STATUS_CONFIRMED).to(STATUS_REOPENED)
  104. .conditions(IsNotHotspot.INSTANCE)
  105. .functions(new SetResolution(null))
  106. .build())
  107. .transition(Transition.builder(DefaultTransitions.REOPEN)
  108. .from(STATUS_RESOLVED).to(STATUS_REOPENED)
  109. .conditions(IsNotHotspot.INSTANCE)
  110. .functions(new SetResolution(null))
  111. .build())
  112. // resolve as false-positive
  113. .transition(Transition.builder(DefaultTransitions.FALSE_POSITIVE)
  114. .from(STATUS_OPEN).to(STATUS_RESOLVED)
  115. .conditions(IsNotHotspot.INSTANCE)
  116. .functions(new SetResolution(RESOLUTION_FALSE_POSITIVE), UnsetAssignee.INSTANCE)
  117. .requiredProjectPermission(UserRole.ISSUE_ADMIN)
  118. .build())
  119. .transition(Transition.builder(DefaultTransitions.FALSE_POSITIVE)
  120. .from(STATUS_REOPENED).to(STATUS_RESOLVED)
  121. .conditions(IsNotHotspot.INSTANCE)
  122. .functions(new SetResolution(RESOLUTION_FALSE_POSITIVE), UnsetAssignee.INSTANCE)
  123. .requiredProjectPermission(UserRole.ISSUE_ADMIN)
  124. .build())
  125. .transition(Transition.builder(DefaultTransitions.FALSE_POSITIVE)
  126. .from(STATUS_CONFIRMED).to(STATUS_RESOLVED)
  127. .conditions(IsNotHotspot.INSTANCE)
  128. .functions(new SetResolution(RESOLUTION_FALSE_POSITIVE), UnsetAssignee.INSTANCE)
  129. .requiredProjectPermission(UserRole.ISSUE_ADMIN)
  130. .build())
  131. // resolve as won't fix
  132. .transition(Transition.builder(DefaultTransitions.WONT_FIX)
  133. .from(STATUS_OPEN).to(STATUS_RESOLVED)
  134. .conditions(IsNotHotspot.INSTANCE)
  135. .functions(new SetResolution(RESOLUTION_WONT_FIX), UnsetAssignee.INSTANCE)
  136. .requiredProjectPermission(UserRole.ISSUE_ADMIN)
  137. .build())
  138. .transition(Transition.builder(DefaultTransitions.WONT_FIX)
  139. .from(STATUS_REOPENED).to(STATUS_RESOLVED)
  140. .conditions(IsNotHotspot.INSTANCE)
  141. .functions(new SetResolution(RESOLUTION_WONT_FIX), UnsetAssignee.INSTANCE)
  142. .requiredProjectPermission(UserRole.ISSUE_ADMIN)
  143. .build())
  144. .transition(Transition.builder(DefaultTransitions.WONT_FIX)
  145. .from(STATUS_CONFIRMED).to(STATUS_RESOLVED)
  146. .conditions(IsNotHotspot.INSTANCE)
  147. .functions(new SetResolution(RESOLUTION_WONT_FIX), UnsetAssignee.INSTANCE)
  148. .requiredProjectPermission(UserRole.ISSUE_ADMIN)
  149. .build());
  150. }
  151. private static void buildSecurityHotspotTransitions(StateMachine.Builder builder) {
  152. // hotspot reviewed as fixed, either from TO_REVIEW or from REVIEWED-SAFE or from REVIEWED-ACKNOWLEDGED
  153. Transition.TransitionBuilder reviewedAsFixedBuilder = Transition.builder(DefaultTransitions.RESOLVE_AS_REVIEWED)
  154. .to(STATUS_REVIEWED)
  155. .conditions(new HasType(RuleType.SECURITY_HOTSPOT))
  156. .functions(new SetResolution(RESOLUTION_FIXED))
  157. .requiredProjectPermission(UserRole.SECURITYHOTSPOT_ADMIN);
  158. builder
  159. .transition(reviewedAsFixedBuilder
  160. .from(STATUS_TO_REVIEW)
  161. .conditions(new HasType(RuleType.SECURITY_HOTSPOT))
  162. .build())
  163. .transition(reviewedAsFixedBuilder
  164. .from(STATUS_REVIEWED)
  165. .conditions(new HasType(RuleType.SECURITY_HOTSPOT), new HasResolution(RESOLUTION_SAFE, RESOLUTION_ACKNOWLEDGED))
  166. .build());
  167. // hotspot reviewed as safe, either from TO_REVIEW or from REVIEWED-FIXED or from REVIEWED-ACKNOWLEDGED
  168. Transition.TransitionBuilder resolveAsSafeTransitionBuilder = Transition.builder(DefaultTransitions.RESOLVE_AS_SAFE)
  169. .to(STATUS_REVIEWED)
  170. .functions(new SetResolution(RESOLUTION_SAFE))
  171. .requiredProjectPermission(UserRole.SECURITYHOTSPOT_ADMIN);
  172. builder
  173. .transition(resolveAsSafeTransitionBuilder
  174. .from(STATUS_TO_REVIEW)
  175. .conditions(new HasType(RuleType.SECURITY_HOTSPOT))
  176. .build())
  177. .transition(resolveAsSafeTransitionBuilder
  178. .from(STATUS_REVIEWED)
  179. .conditions(new HasType(RuleType.SECURITY_HOTSPOT), new HasResolution(RESOLUTION_FIXED, RESOLUTION_ACKNOWLEDGED))
  180. .build());
  181. // hotspot reviewed as acknowledged, either from TO_REVIEW or from REVIEWED-FIXED or from REVIEWED-SAFE
  182. Transition.TransitionBuilder resolveAsAcknowledgedTransitionBuilder = Transition.builder(DefaultTransitions.RESOLVE_AS_ACKNOWLEDGED)
  183. .to(STATUS_REVIEWED)
  184. .functions(new SetResolution(RESOLUTION_ACKNOWLEDGED))
  185. .requiredProjectPermission(UserRole.SECURITYHOTSPOT_ADMIN);
  186. builder
  187. .transition(resolveAsAcknowledgedTransitionBuilder
  188. .from(STATUS_TO_REVIEW)
  189. .conditions(new HasType(RuleType.SECURITY_HOTSPOT))
  190. .build())
  191. .transition(resolveAsAcknowledgedTransitionBuilder
  192. .from(STATUS_REVIEWED)
  193. .conditions(new HasType(RuleType.SECURITY_HOTSPOT), new HasResolution(RESOLUTION_FIXED, RESOLUTION_SAFE))
  194. .build());
  195. // put hotspot back into TO_REVIEW
  196. builder
  197. .transition(Transition.builder(DefaultTransitions.RESET_AS_TO_REVIEW)
  198. .from(STATUS_REVIEWED).to(STATUS_TO_REVIEW)
  199. .conditions(new HasType(RuleType.SECURITY_HOTSPOT), new HasResolution(RESOLUTION_FIXED, RESOLUTION_SAFE, RESOLUTION_ACKNOWLEDGED))
  200. .functions(new SetResolution(null))
  201. .requiredProjectPermission(UserRole.SECURITYHOTSPOT_ADMIN)
  202. .build());
  203. }
  204. private static void buildAutomaticTransitions(StateMachine.Builder builder) {
  205. // Close the "end of life" issues (disabled/deleted rule, deleted component)
  206. builder
  207. .transition(Transition.builder(AUTOMATIC_CLOSE_TRANSITION)
  208. .from(STATUS_OPEN).to(STATUS_CLOSED)
  209. .conditions(IsBeingClosed.INSTANCE)
  210. .functions(SetClosed.INSTANCE, SetCloseDate.INSTANCE)
  211. .automatic()
  212. .build())
  213. .transition(Transition.builder(AUTOMATIC_CLOSE_TRANSITION)
  214. .from(STATUS_REOPENED).to(STATUS_CLOSED)
  215. .conditions(IsBeingClosed.INSTANCE)
  216. .functions(SetClosed.INSTANCE, SetCloseDate.INSTANCE)
  217. .automatic()
  218. .build())
  219. .transition(Transition.builder(AUTOMATIC_CLOSE_TRANSITION)
  220. .from(STATUS_CONFIRMED).to(STATUS_CLOSED)
  221. .conditions(IsBeingClosed.INSTANCE)
  222. .functions(SetClosed.INSTANCE, SetCloseDate.INSTANCE)
  223. .automatic()
  224. .build())
  225. .transition(Transition.builder(AUTOMATIC_CLOSE_TRANSITION)
  226. .from(STATUS_RESOLVED).to(STATUS_CLOSED)
  227. .conditions(IsBeingClosed.INSTANCE)
  228. .functions(SetClosed.INSTANCE, SetCloseDate.INSTANCE)
  229. .automatic()
  230. .build())
  231. .transition(Transition.builder(AUTOMATIC_CLOSE_TRANSITION)
  232. .from(STATUS_TO_REVIEW).to(STATUS_CLOSED)
  233. .conditions(IsBeingClosed.INSTANCE, new HasType(RuleType.SECURITY_HOTSPOT))
  234. .functions(SetClosed.INSTANCE, SetCloseDate.INSTANCE)
  235. .automatic()
  236. .build())
  237. .transition(Transition.builder(AUTOMATIC_CLOSE_TRANSITION)
  238. .from(STATUS_REVIEWED).to(STATUS_CLOSED)
  239. .conditions(IsBeingClosed.INSTANCE, new HasType(RuleType.SECURITY_HOTSPOT))
  240. .functions(SetClosed.INSTANCE, SetCloseDate.INSTANCE)
  241. .automatic()
  242. .build())
  243. // Reopen issues that are marked as resolved but that are still alive.
  244. .transition(Transition.builder("automaticreopen")
  245. .from(STATUS_RESOLVED).to(STATUS_REOPENED)
  246. .conditions(new NotCondition(IsBeingClosed.INSTANCE), new HasResolution(RESOLUTION_FIXED), IsNotHotspot.INSTANCE)
  247. .functions(new SetResolution(null), UnsetCloseDate.INSTANCE)
  248. .automatic()
  249. .build())
  250. .transition(Transition.builder("automaticuncloseopen")
  251. .from(STATUS_CLOSED).to(STATUS_OPEN)
  252. .conditions(
  253. new PreviousStatusWas(STATUS_OPEN),
  254. new HasResolution(RESOLUTION_REMOVED, RESOLUTION_FIXED),
  255. IsNotHotspot.INSTANCE)
  256. .functions(RestoreResolutionFunction.INSTANCE, UnsetCloseDate.INSTANCE)
  257. .automatic()
  258. .build())
  259. .transition(Transition.builder("automaticunclosereopen")
  260. .from(STATUS_CLOSED).to(STATUS_REOPENED)
  261. .conditions(
  262. new PreviousStatusWas(STATUS_REOPENED),
  263. new HasResolution(RESOLUTION_REMOVED, RESOLUTION_FIXED),
  264. IsNotHotspot.INSTANCE)
  265. .functions(RestoreResolutionFunction.INSTANCE, UnsetCloseDate.INSTANCE)
  266. .automatic()
  267. .build())
  268. .transition(Transition.builder("automaticuncloseconfirmed")
  269. .from(STATUS_CLOSED).to(STATUS_CONFIRMED)
  270. .conditions(
  271. new PreviousStatusWas(STATUS_CONFIRMED),
  272. new HasResolution(RESOLUTION_REMOVED, RESOLUTION_FIXED),
  273. IsNotHotspot.INSTANCE)
  274. .functions(RestoreResolutionFunction.INSTANCE, UnsetCloseDate.INSTANCE)
  275. .automatic()
  276. .build())
  277. .transition(Transition.builder("automaticuncloseresolved")
  278. .from(STATUS_CLOSED).to(STATUS_RESOLVED)
  279. .conditions(
  280. new PreviousStatusWas(STATUS_RESOLVED),
  281. new HasResolution(RESOLUTION_REMOVED, RESOLUTION_FIXED),
  282. IsNotHotspot.INSTANCE)
  283. .functions(RestoreResolutionFunction.INSTANCE, UnsetCloseDate.INSTANCE)
  284. .automatic()
  285. .build())
  286. // reopen closed hotspots
  287. .transition(Transition.builder("automaticunclosetoreview")
  288. .from(STATUS_CLOSED).to(STATUS_TO_REVIEW)
  289. .conditions(
  290. new PreviousStatusWas(STATUS_TO_REVIEW),
  291. new HasResolution(RESOLUTION_REMOVED, RESOLUTION_FIXED),
  292. IsHotspot.INSTANCE)
  293. .functions(RestoreResolutionFunction.INSTANCE, UnsetCloseDate.INSTANCE)
  294. .automatic()
  295. .build())
  296. .transition(Transition.builder("automaticunclosereviewed")
  297. .from(STATUS_CLOSED).to(STATUS_REVIEWED)
  298. .conditions(
  299. new PreviousStatusWas(STATUS_REVIEWED),
  300. new HasResolution(RESOLUTION_REMOVED, RESOLUTION_FIXED),
  301. IsHotspot.INSTANCE)
  302. .functions(RestoreResolutionFunction.INSTANCE, UnsetCloseDate.INSTANCE)
  303. .automatic()
  304. .build());
  305. }
  306. @Override
  307. public void stop() {
  308. // nothing to do
  309. }
  310. public boolean doManualTransition(DefaultIssue issue, String transitionKey, IssueChangeContext issueChangeContext) {
  311. Transition transition = stateOf(issue).transition(transitionKey);
  312. if (transition.supports(issue) && !transition.automatic()) {
  313. IssueStatus previousIssueStatus = issue.getIssueStatus();
  314. functionExecutor.execute(transition.functions(), issue, issueChangeContext);
  315. updater.setStatus(issue, transition.to(), issueChangeContext);
  316. updater.setIssueStatus(issue, previousIssueStatus, issue.getIssueStatus(), issueChangeContext);
  317. return true;
  318. }
  319. return false;
  320. }
  321. public List<Transition> outTransitions(Issue issue) {
  322. String status = issue.status();
  323. State state = machine.state(status);
  324. checkArgument(state != null, "Unknown status: %s", status);
  325. return state.outManualTransitions(issue);
  326. }
  327. public void doAutomaticTransition(DefaultIssue issue, IssueChangeContext issueChangeContext) {
  328. Transition transition = stateOf(issue).outAutomaticTransition(issue);
  329. if (transition != null) {
  330. IssueStatus previousIssueStatus = issue.getIssueStatus();
  331. functionExecutor.execute(transition.functions(), issue, issueChangeContext);
  332. updater.setStatus(issue, transition.to(), issueChangeContext);
  333. updater.setIssueStatus(issue, previousIssueStatus, issue.getIssueStatus(), issueChangeContext);
  334. }
  335. }
  336. public List<String> statusKeys() {
  337. return machine.stateKeys();
  338. }
  339. private State stateOf(DefaultIssue issue) {
  340. String status = issue.status();
  341. State state = machine.state(status);
  342. String issueKey = issue.key();
  343. checkState(state != null, "Unknown status: %s [issue=%s]", status, issueKey);
  344. return state;
  345. }
  346. }