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.

ActivityAction.java 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2019 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.ce.ws;
  21. import com.google.common.base.Joiner;
  22. import com.google.common.collect.ImmutableList;
  23. import java.util.Collections;
  24. import java.util.Date;
  25. import java.util.LinkedHashSet;
  26. import java.util.List;
  27. import java.util.Optional;
  28. import java.util.Set;
  29. import javax.annotation.CheckForNull;
  30. import javax.annotation.Nullable;
  31. import org.sonar.api.resources.Qualifiers;
  32. import org.sonar.api.server.ws.Change;
  33. import org.sonar.api.server.ws.Response;
  34. import org.sonar.api.server.ws.WebService;
  35. import org.sonar.api.server.ws.WebService.Param;
  36. import org.sonar.api.web.UserRole;
  37. import org.sonar.ce.task.taskprocessor.CeTaskProcessor;
  38. import org.sonar.core.util.Uuids;
  39. import org.sonar.db.DbClient;
  40. import org.sonar.db.DbSession;
  41. import org.sonar.db.ce.CeActivityDto;
  42. import org.sonar.db.ce.CeQueueDto;
  43. import org.sonar.db.ce.CeTaskQuery;
  44. import org.sonar.db.ce.CeTaskTypes;
  45. import org.sonar.db.component.ComponentDto;
  46. import org.sonar.db.component.ComponentQuery;
  47. import org.sonar.server.user.UserSession;
  48. import org.sonarqube.ws.Ce;
  49. import org.sonarqube.ws.Ce.ActivityResponse;
  50. import static java.lang.Boolean.parseBoolean;
  51. import static java.lang.Integer.parseInt;
  52. import static java.lang.String.format;
  53. import static java.util.Arrays.asList;
  54. import static java.util.Collections.emptyList;
  55. import static java.util.Collections.singletonList;
  56. import static org.sonar.api.server.ws.WebService.Param.TEXT_QUERY;
  57. import static org.sonar.api.utils.DateUtils.parseEndingDateOrDateTime;
  58. import static org.sonar.api.utils.DateUtils.parseStartingDateOrDateTime;
  59. import static org.sonar.core.util.stream.MoreCollectors.toList;
  60. import static org.sonar.db.Pagination.forPage;
  61. import static org.sonar.server.ce.ws.CeWsParameters.PARAM_COMPONENT;
  62. import static org.sonar.server.ce.ws.CeWsParameters.PARAM_COMPONENT_ID;
  63. import static org.sonar.server.ce.ws.CeWsParameters.PARAM_MAX_EXECUTED_AT;
  64. import static org.sonar.server.ce.ws.CeWsParameters.PARAM_MIN_SUBMITTED_AT;
  65. import static org.sonar.server.ce.ws.CeWsParameters.PARAM_ONLY_CURRENTS;
  66. import static org.sonar.server.ce.ws.CeWsParameters.PARAM_STATUS;
  67. import static org.sonar.server.ce.ws.CeWsParameters.PARAM_TYPE;
  68. import static org.sonar.server.exceptions.BadRequestException.checkRequest;
  69. import static org.sonar.server.exceptions.NotFoundException.checkFoundWithOptional;
  70. import static org.sonar.server.ws.WsUtils.writeProtobuf;
  71. public class ActivityAction implements CeWsAction {
  72. private static final int MAX_PAGE_SIZE = 1000;
  73. private static final String[] POSSIBLE_QUALIFIERS = new String[] {Qualifiers.PROJECT, Qualifiers.APP, Qualifiers.VIEW};
  74. private static final String INVALID_QUERY_PARAM_ERROR_MESSAGE = "%s and %s must not be set at the same time";
  75. private final UserSession userSession;
  76. private final DbClient dbClient;
  77. private final TaskFormatter formatter;
  78. private final Set<String> taskTypes;
  79. public ActivityAction(UserSession userSession, DbClient dbClient, TaskFormatter formatter, CeTaskProcessor[] taskProcessors) {
  80. this.userSession = userSession;
  81. this.dbClient = dbClient;
  82. this.formatter = formatter;
  83. this.taskTypes = new LinkedHashSet<>();
  84. for (CeTaskProcessor taskProcessor : taskProcessors) {
  85. taskTypes.addAll(taskProcessor.getHandledCeTaskTypes());
  86. }
  87. }
  88. @Override
  89. public void define(WebService.NewController controller) {
  90. WebService.NewAction action = controller.createAction("activity")
  91. .setDescription(format("Search for tasks.<br> " +
  92. "Either %s or %s can be provided, but not both.<br> " +
  93. "Requires the system administration permission, " +
  94. "or project administration permission if %s or %s is set.",
  95. PARAM_COMPONENT_ID, PARAM_COMPONENT, PARAM_COMPONENT_ID, PARAM_COMPONENT))
  96. .setResponseExample(getClass().getResource("activity-example.json"))
  97. .setHandler(this)
  98. .setChangelog(
  99. new Change("5.5", "it's no more possible to specify the page parameter."),
  100. new Change("6.1", "field \"logs\" is deprecated and its value is always false"),
  101. new Change("6.6", "fields \"branch\" and \"branchType\" added"),
  102. new Change("7.1", "field \"pullRequest\" added"),
  103. new Change("7.6", format("The use of module keys in parameters '%s' is deprecated", TEXT_QUERY)))
  104. .setSince("5.2");
  105. action.createParam(PARAM_COMPONENT_ID)
  106. .setDescription("Id of the component (project) to filter on")
  107. .setDeprecatedSince("8.0")
  108. .setExampleValue(Uuids.UUID_EXAMPLE_03);
  109. action.createParam(PARAM_COMPONENT)
  110. .setDescription("Key of the component (project) to filter on")
  111. .setExampleValue("projectKey")
  112. .setSince("8.0");
  113. action.createParam(TEXT_QUERY)
  114. .setDescription(format("Limit search to: <ul>" +
  115. "<li>component names that contain the supplied string</li>" +
  116. "<li>component keys that are exactly the same as the supplied string</li>" +
  117. "<li>task ids that are exactly the same as the supplied string</li>" +
  118. "</ul>" +
  119. "Must not be set together with %s", PARAM_COMPONENT_ID))
  120. .setExampleValue("Apache")
  121. .setSince("5.5");
  122. action.createParam(PARAM_STATUS)
  123. .setDescription("Comma separated list of task statuses")
  124. .setPossibleValues(ImmutableList.builder()
  125. .addAll(asList(CeActivityDto.Status.values()))
  126. .addAll(asList(CeQueueDto.Status.values())).build())
  127. .setExampleValue(Joiner.on(",").join(CeQueueDto.Status.IN_PROGRESS, CeActivityDto.Status.SUCCESS))
  128. // activity statuses by default to be backward compatible
  129. // queued tasks have been added in 5.5
  130. .setDefaultValue(Joiner.on(",").join(CeActivityDto.Status.values()));
  131. action.createParam(PARAM_ONLY_CURRENTS)
  132. .setDescription("Filter on the last tasks (only the most recent finished task by project)")
  133. .setBooleanPossibleValues()
  134. .setDefaultValue("false");
  135. action.createParam(PARAM_TYPE)
  136. .setDescription("Task type")
  137. .setExampleValue(CeTaskTypes.REPORT)
  138. .setPossibleValues(taskTypes);
  139. action.createParam(PARAM_MIN_SUBMITTED_AT)
  140. .setDescription("Minimum date of task submission (inclusive)")
  141. .setExampleValue("2017-10-19T13:00:00+0200");
  142. action.createParam(PARAM_MAX_EXECUTED_AT)
  143. .setDescription("Maximum date of end of task processing (inclusive)")
  144. .setExampleValue("2017-10-19T13:00:00+0200");
  145. action.createPageSize(100, MAX_PAGE_SIZE);
  146. }
  147. @Override
  148. public void handle(org.sonar.api.server.ws.Request wsRequest, Response wsResponse) throws Exception {
  149. ActivityResponse activityResponse = doHandle(toSearchWsRequest(wsRequest));
  150. writeProtobuf(activityResponse, wsRequest, wsResponse);
  151. }
  152. private ActivityResponse doHandle(Request request) {
  153. try (DbSession dbSession = dbClient.openSession(false)) {
  154. ComponentDto component = loadComponent(dbSession, request);
  155. checkPermission(component);
  156. // if a task searched by uuid is found all other parameters are ignored
  157. Optional<Ce.Task> taskSearchedById = searchTaskByUuid(dbSession, request);
  158. if (taskSearchedById.isPresent()) {
  159. return buildResponse(
  160. singletonList(taskSearchedById.get()),
  161. Collections.emptyList(),
  162. parseInt(request.getPs()));
  163. }
  164. CeTaskQuery query = buildQuery(dbSession, request, component);
  165. List<Ce.Task> queuedTasks = loadQueuedTasks(dbSession, request, query);
  166. List<Ce.Task> pastTasks = loadPastTasks(dbSession, request, query);
  167. return buildResponse(
  168. queuedTasks,
  169. pastTasks,
  170. parseInt(request.getPs()));
  171. }
  172. }
  173. @CheckForNull
  174. private ComponentDto loadComponent(DbSession dbSession, Request request) {
  175. String componentId = request.getComponentId();
  176. String componentKey = request.getComponent();
  177. Optional<ComponentDto> foundComponent;
  178. if (componentId != null) {
  179. foundComponent = dbClient.componentDao().selectByUuid(dbSession, componentId);
  180. return checkFoundWithOptional(foundComponent, "Component '%s' does not exist", componentId);
  181. } else if (componentKey != null) {
  182. foundComponent = dbClient.componentDao().selectByKey(dbSession, componentKey);
  183. return checkFoundWithOptional(foundComponent, "Component '%s' does not exist", componentKey);
  184. }
  185. return null;
  186. }
  187. private void checkPermission(@Nullable ComponentDto component) {
  188. // fail fast if not logged in
  189. userSession.checkLoggedIn();
  190. if (component == null) {
  191. userSession.checkIsSystemAdministrator();
  192. } else {
  193. userSession.checkComponentPermission(UserRole.ADMIN, component);
  194. }
  195. }
  196. private Optional<Ce.Task> searchTaskByUuid(DbSession dbSession, Request request) {
  197. String textQuery = request.getQ();
  198. if (textQuery == null) {
  199. return Optional.empty();
  200. }
  201. Optional<CeQueueDto> queue = dbClient.ceQueueDao().selectByUuid(dbSession, textQuery);
  202. if (queue.isPresent()) {
  203. return Optional.of(formatter.formatQueue(dbSession, queue.get()));
  204. }
  205. Optional<CeActivityDto> activity = dbClient.ceActivityDao().selectByUuid(dbSession, textQuery);
  206. return activity.map(ceActivityDto -> formatter.formatActivity(dbSession, ceActivityDto, null, emptyList()));
  207. }
  208. private CeTaskQuery buildQuery(DbSession dbSession, Request request, @Nullable ComponentDto component) {
  209. CeTaskQuery query = new CeTaskQuery();
  210. query.setType(request.getType());
  211. query.setOnlyCurrents(parseBoolean(request.getOnlyCurrents()));
  212. Date minSubmittedAt = parseStartingDateOrDateTime(request.getMinSubmittedAt());
  213. query.setMinSubmittedAt(minSubmittedAt == null ? null : minSubmittedAt.getTime());
  214. Date maxExecutedAt = parseEndingDateOrDateTime(request.getMaxExecutedAt());
  215. query.setMaxExecutedAt(maxExecutedAt == null ? null : maxExecutedAt.getTime());
  216. List<String> statuses = request.getStatus();
  217. if (statuses != null && !statuses.isEmpty()) {
  218. query.setStatuses(request.getStatus());
  219. }
  220. String componentQuery = request.getQ();
  221. if (component != null) {
  222. query.setMainComponentUuid(component.uuid());
  223. } else if (componentQuery != null) {
  224. query.setMainComponentUuids(loadComponents(dbSession, componentQuery).stream()
  225. .map(ComponentDto::uuid)
  226. .collect(toList()));
  227. }
  228. return query;
  229. }
  230. private List<ComponentDto> loadComponents(DbSession dbSession, String componentQuery) {
  231. ComponentQuery componentDtoQuery = ComponentQuery.builder()
  232. .setNameOrKeyQuery(componentQuery)
  233. .setQualifiers(POSSIBLE_QUALIFIERS)
  234. .build();
  235. return dbClient.componentDao().selectByQuery(dbSession, componentDtoQuery, 0, CeTaskQuery.MAX_COMPONENT_UUIDS);
  236. }
  237. private List<Ce.Task> loadQueuedTasks(DbSession dbSession, Request request, CeTaskQuery query) {
  238. List<CeQueueDto> dtos = dbClient.ceQueueDao().selectByQueryInDescOrder(dbSession, query, parseInt(request.getPs()));
  239. return formatter.formatQueue(dbSession, dtos);
  240. }
  241. private List<Ce.Task> loadPastTasks(DbSession dbSession, Request request, CeTaskQuery query) {
  242. List<CeActivityDto> dtos = dbClient.ceActivityDao().selectByQuery(dbSession, query, forPage(1).andSize(parseInt(request.getPs())));
  243. return formatter.formatActivity(dbSession, dtos);
  244. }
  245. private static ActivityResponse buildResponse(Iterable<Ce.Task> queuedTasks, Iterable<Ce.Task> pastTasks, int pageSize) {
  246. Ce.ActivityResponse.Builder wsResponseBuilder = Ce.ActivityResponse.newBuilder();
  247. int nbInsertedTasks = 0;
  248. for (Ce.Task queuedTask : queuedTasks) {
  249. if (nbInsertedTasks < pageSize) {
  250. wsResponseBuilder.addTasks(queuedTask);
  251. nbInsertedTasks++;
  252. }
  253. }
  254. for (Ce.Task pastTask : pastTasks) {
  255. if (nbInsertedTasks < pageSize) {
  256. wsResponseBuilder.addTasks(pastTask);
  257. nbInsertedTasks++;
  258. }
  259. }
  260. return wsResponseBuilder.build();
  261. }
  262. private static Request toSearchWsRequest(org.sonar.api.server.ws.Request request) {
  263. Request activityWsRequest = new Request()
  264. .setComponentId(request.param(PARAM_COMPONENT_ID))
  265. .setComponent(request.param(PARAM_COMPONENT))
  266. .setQ(request.param(TEXT_QUERY))
  267. .setStatus(request.paramAsStrings(PARAM_STATUS))
  268. .setType(request.param(PARAM_TYPE))
  269. .setMinSubmittedAt(request.param(PARAM_MIN_SUBMITTED_AT))
  270. .setMaxExecutedAt(request.param(PARAM_MAX_EXECUTED_AT))
  271. .setOnlyCurrents(String.valueOf(request.paramAsBoolean(PARAM_ONLY_CURRENTS)))
  272. .setPs(String.valueOf(request.mandatoryParamAsInt(Param.PAGE_SIZE)));
  273. checkRequest(activityWsRequest.getComponentId() == null || activityWsRequest.getQ() == null, INVALID_QUERY_PARAM_ERROR_MESSAGE,
  274. PARAM_COMPONENT_ID, TEXT_QUERY);
  275. checkRequest(activityWsRequest.getComponent() == null || activityWsRequest.getQ() == null, INVALID_QUERY_PARAM_ERROR_MESSAGE,
  276. PARAM_COMPONENT, TEXT_QUERY);
  277. checkRequest(activityWsRequest.getComponentId() == null || activityWsRequest.getComponent() == null, INVALID_QUERY_PARAM_ERROR_MESSAGE,
  278. PARAM_COMPONENT_ID, PARAM_COMPONENT);
  279. return activityWsRequest;
  280. }
  281. private static class Request {
  282. private String componentId;
  283. private String component;
  284. private String maxExecutedAt;
  285. private String minSubmittedAt;
  286. private String onlyCurrents;
  287. private String ps;
  288. private String q;
  289. private List<String> status;
  290. private String type;
  291. Request() {
  292. // Nothing to do
  293. }
  294. /**
  295. * Example value: "AU-TpxcA-iU5OvuD2FL0"
  296. */
  297. private Request setComponentId(@Nullable String componentId) {
  298. this.componentId = componentId;
  299. return this;
  300. }
  301. @CheckForNull
  302. private String getComponentId() {
  303. return componentId;
  304. }
  305. /**
  306. * Example value: "sample:src/main/xoo/sample/Sample2.xoo"
  307. */
  308. private Request setComponent(@Nullable String component) {
  309. this.component = component;
  310. return this;
  311. }
  312. @CheckForNull
  313. private String getComponent() {
  314. return component;
  315. }
  316. /**
  317. * Example value: "2017-10-19T13:00:00+0200"
  318. */
  319. private Request setMaxExecutedAt(@Nullable String maxExecutedAt) {
  320. this.maxExecutedAt = maxExecutedAt;
  321. return this;
  322. }
  323. @CheckForNull
  324. private String getMaxExecutedAt() {
  325. return maxExecutedAt;
  326. }
  327. /**
  328. * Example value: "2017-10-19T13:00:00+0200"
  329. */
  330. private Request setMinSubmittedAt(@Nullable String minSubmittedAt) {
  331. this.minSubmittedAt = minSubmittedAt;
  332. return this;
  333. }
  334. @CheckForNull
  335. private String getMinSubmittedAt() {
  336. return minSubmittedAt;
  337. }
  338. /**
  339. * Possible values:
  340. * <ul>
  341. * <li>"true"</li>
  342. * <li>"false"</li>
  343. * <li>"yes"</li>
  344. * <li>"no"</li>
  345. * </ul>
  346. */
  347. private Request setOnlyCurrents(String onlyCurrents) {
  348. this.onlyCurrents = onlyCurrents;
  349. return this;
  350. }
  351. private String getOnlyCurrents() {
  352. return onlyCurrents;
  353. }
  354. /**
  355. * Example value: "20"
  356. */
  357. private Request setPs(String ps) {
  358. this.ps = ps;
  359. return this;
  360. }
  361. private String getPs() {
  362. return ps;
  363. }
  364. /**
  365. * Example value: "Apache"
  366. */
  367. private Request setQ(@Nullable String q) {
  368. this.q = q;
  369. return this;
  370. }
  371. @CheckForNull
  372. private String getQ() {
  373. return q;
  374. }
  375. /**
  376. * Example value: "IN_PROGRESS,SUCCESS"
  377. * Possible values:
  378. * <ul>
  379. * <li>"SUCCESS"</li>
  380. * <li>"FAILED"</li>
  381. * <li>"CANCELED"</li>
  382. * <li>"PENDING"</li>
  383. * <li>"IN_PROGRESS"</li>
  384. * </ul>
  385. */
  386. private Request setStatus(@Nullable List<String> status) {
  387. this.status = status;
  388. return this;
  389. }
  390. @CheckForNull
  391. private List<String> getStatus() {
  392. return status;
  393. }
  394. /**
  395. * Example value: "REPORT"
  396. * Possible values:
  397. * <ul>
  398. * <li>"REPORT"</li>
  399. * </ul>
  400. */
  401. private Request setType(@Nullable String type) {
  402. this.type = type;
  403. return this;
  404. }
  405. @CheckForNull
  406. private String getType() {
  407. return type;
  408. }
  409. }
  410. }