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 15KB

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