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.

TreeAction.java 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  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.component.ws;
  21. import com.google.common.base.Function;
  22. import com.google.common.collect.ImmutableMap;
  23. import com.google.common.collect.ImmutableSortedSet;
  24. import com.google.common.collect.Ordering;
  25. import com.google.common.collect.Sets;
  26. import java.util.ArrayList;
  27. import java.util.HashSet;
  28. import java.util.List;
  29. import java.util.Map;
  30. import java.util.Objects;
  31. import java.util.Optional;
  32. import java.util.Set;
  33. import javax.annotation.CheckForNull;
  34. import javax.annotation.Nullable;
  35. import org.sonar.api.i18n.I18n;
  36. import org.sonar.api.resources.ResourceTypes;
  37. import org.sonar.api.server.ws.Change;
  38. import org.sonar.api.server.ws.Response;
  39. import org.sonar.api.server.ws.WebService;
  40. import org.sonar.api.server.ws.WebService.Param;
  41. import org.sonar.api.utils.Paging;
  42. import org.sonar.api.web.UserRole;
  43. import org.sonar.core.util.stream.MoreCollectors;
  44. import org.sonar.db.DbClient;
  45. import org.sonar.db.DbSession;
  46. import org.sonar.db.component.ComponentDto;
  47. import org.sonar.db.component.ComponentTreeQuery;
  48. import org.sonar.db.component.ComponentTreeQuery.Strategy;
  49. import org.sonar.db.organization.OrganizationDto;
  50. import org.sonar.server.component.ComponentFinder;
  51. import org.sonar.server.user.UserSession;
  52. import org.sonarqube.ws.Components;
  53. import org.sonarqube.ws.Components.TreeWsResponse;
  54. import static com.google.common.base.Preconditions.checkArgument;
  55. import static java.lang.String.CASE_INSENSITIVE_ORDER;
  56. import static java.lang.String.format;
  57. import static java.util.Collections.emptyMap;
  58. import static org.sonar.api.utils.Paging.offset;
  59. import static org.sonar.core.util.Uuids.UUID_EXAMPLE_02;
  60. import static org.sonar.core.util.stream.MoreCollectors.toList;
  61. import static org.sonar.db.component.ComponentTreeQuery.Strategy.CHILDREN;
  62. import static org.sonar.db.component.ComponentTreeQuery.Strategy.LEAVES;
  63. import static org.sonar.server.component.ComponentFinder.ParamNames.COMPONENT_ID_AND_COMPONENT;
  64. import static org.sonar.server.component.ws.ComponentDtoToWsComponent.componentDtoToWsComponent;
  65. import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001;
  66. import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
  67. import static org.sonar.server.ws.KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001;
  68. import static org.sonar.server.ws.WsParameterBuilder.QualifierParameterContext.newQualifierParameterContext;
  69. import static org.sonar.server.ws.WsParameterBuilder.createQualifiersParameter;
  70. import static org.sonar.server.ws.WsUtils.checkRequest;
  71. import static org.sonar.server.ws.WsUtils.writeProtobuf;
  72. import static org.sonarqube.ws.client.component.ComponentsWsParameters.ACTION_TREE;
  73. import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_BRANCH;
  74. import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_COMPONENT;
  75. import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_COMPONENT_ID;
  76. import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_PULL_REQUEST;
  77. import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_QUALIFIERS;
  78. import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_STRATEGY;
  79. public class TreeAction implements ComponentsWsAction {
  80. private static final int MAX_SIZE = 500;
  81. private static final int QUERY_MINIMUM_LENGTH = 3;
  82. private static final String ALL_STRATEGY = "all";
  83. private static final String CHILDREN_STRATEGY = "children";
  84. private static final String LEAVES_STRATEGY = "leaves";
  85. private static final Map<String, Strategy> STRATEGIES = ImmutableMap.of(
  86. ALL_STRATEGY, LEAVES,
  87. CHILDREN_STRATEGY, CHILDREN,
  88. LEAVES_STRATEGY, LEAVES);
  89. private static final String NAME_SORT = "name";
  90. private static final String PATH_SORT = "path";
  91. private static final String QUALIFIER_SORT = "qualifier";
  92. private static final Set<String> SORTS = ImmutableSortedSet.of(NAME_SORT, PATH_SORT, QUALIFIER_SORT);
  93. private final DbClient dbClient;
  94. private final ComponentFinder componentFinder;
  95. private final ResourceTypes resourceTypes;
  96. private final UserSession userSession;
  97. private final I18n i18n;
  98. public TreeAction(DbClient dbClient, ComponentFinder componentFinder, ResourceTypes resourceTypes, UserSession userSession, I18n i18n) {
  99. this.dbClient = dbClient;
  100. this.componentFinder = componentFinder;
  101. this.resourceTypes = resourceTypes;
  102. this.userSession = userSession;
  103. this.i18n = i18n;
  104. }
  105. @Override
  106. public void define(WebService.NewController context) {
  107. WebService.NewAction action = context.createAction(ACTION_TREE)
  108. .setDescription(format("Navigate through components based on the chosen strategy. The %s or the %s parameter must be provided.<br>" +
  109. "Requires the following permission: 'Browse' on the specified project.<br>" +
  110. "When limiting search with the %s parameter, directories are not returned.",
  111. PARAM_COMPONENT_ID, PARAM_COMPONENT, Param.TEXT_QUERY))
  112. .setSince("5.4")
  113. .setResponseExample(getClass().getResource("tree-example.json"))
  114. .setChangelog(
  115. new Change("6.4", "The field 'id' is deprecated in the response"))
  116. .setHandler(this)
  117. .addPagingParams(100, MAX_SIZE);
  118. action.createParam(PARAM_COMPONENT_ID)
  119. .setDescription("Base component id. The search is based on this component.")
  120. .setDeprecatedKey("baseComponentId", "6.4")
  121. .setDeprecatedSince("6.4")
  122. .setExampleValue(UUID_EXAMPLE_02);
  123. action.createParam(PARAM_COMPONENT)
  124. .setDescription("Base component key. The search is based on this component.")
  125. .setDeprecatedKey("baseComponentKey", "6.4")
  126. .setExampleValue(KEY_PROJECT_EXAMPLE_001);
  127. action.createParam(PARAM_BRANCH)
  128. .setDescription("Branch key")
  129. .setExampleValue(KEY_BRANCH_EXAMPLE_001)
  130. .setInternal(true)
  131. .setSince("6.6");
  132. action.createParam(PARAM_PULL_REQUEST)
  133. .setDescription("Pull request id")
  134. .setExampleValue(KEY_PULL_REQUEST_EXAMPLE_001)
  135. .setInternal(true)
  136. .setSince("7.1");
  137. action.createSortParams(SORTS, NAME_SORT, true)
  138. .setDescription("Comma-separated list of sort fields")
  139. .setExampleValue(NAME_SORT + ", " + PATH_SORT);
  140. action.createParam(Param.TEXT_QUERY)
  141. .setDescription("Limit search to: <ul>" +
  142. "<li>component names that contain the supplied string</li>" +
  143. "<li>component keys that are exactly the same as the supplied string</li>" +
  144. "</ul>")
  145. .setMinimumLength(QUERY_MINIMUM_LENGTH)
  146. .setExampleValue("FILE_NAM");
  147. createQualifiersParameter(action, newQualifierParameterContext(i18n, resourceTypes));
  148. action.createParam(PARAM_STRATEGY)
  149. .setDescription("Strategy to search for base component descendants:" +
  150. "<ul>" +
  151. "<li>children: return the children components of the base component. Grandchildren components are not returned</li>" +
  152. "<li>all: return all the descendants components of the base component. Grandchildren are returned.</li>" +
  153. "<li>leaves: return all the descendant components (files, in general) which don't have other children. They are the leaves of the component tree.</li>" +
  154. "</ul>")
  155. .setPossibleValues(STRATEGIES.keySet())
  156. .setDefaultValue(ALL_STRATEGY);
  157. }
  158. @Override
  159. public void handle(org.sonar.api.server.ws.Request request, Response response) throws Exception {
  160. TreeWsResponse treeWsResponse = doHandle(toTreeWsRequest(request));
  161. writeProtobuf(treeWsResponse, request, response);
  162. }
  163. private TreeWsResponse doHandle(Request treeRequest) {
  164. try (DbSession dbSession = dbClient.openSession(false)) {
  165. ComponentDto baseComponent = loadComponent(dbSession, treeRequest);
  166. checkPermissions(baseComponent);
  167. OrganizationDto organizationDto = componentFinder.getOrganization(dbSession, baseComponent);
  168. ComponentTreeQuery query = toComponentTreeQuery(treeRequest, baseComponent);
  169. List<ComponentDto> components = dbClient.componentDao().selectDescendants(dbSession, query);
  170. int total = components.size();
  171. components = sortComponents(components, treeRequest);
  172. components = paginateComponents(components, treeRequest);
  173. Map<String, ComponentDto> referenceComponentsByUuid = searchReferenceComponentsByUuid(dbSession, components);
  174. return buildResponse(baseComponent, organizationDto, components, referenceComponentsByUuid,
  175. Paging.forPageIndex(treeRequest.getPage()).withPageSize(treeRequest.getPageSize()).andTotal(total));
  176. }
  177. }
  178. private ComponentDto loadComponent(DbSession dbSession, Request request) {
  179. String componentId = request.getBaseComponentId();
  180. String componentKey = request.getComponent();
  181. String branch = request.getBranch();
  182. String pullRequest = request.getPullRequest();
  183. checkArgument(componentId == null || (branch == null && pullRequest == null), "Parameter '%s' cannot be used at the same time as '%s' or '%s'", PARAM_COMPONENT_ID,
  184. PARAM_BRANCH, PARAM_PULL_REQUEST);
  185. if (branch == null && pullRequest == null) {
  186. return componentFinder.getByUuidOrKey(dbSession, componentId, componentKey, COMPONENT_ID_AND_COMPONENT);
  187. }
  188. checkRequest(componentKey != null, "The '%s' parameter is missing", PARAM_COMPONENT);
  189. return componentFinder.getByKeyAndOptionalBranchOrPullRequest(dbSession, componentKey, branch, pullRequest);
  190. }
  191. private Map<String, ComponentDto> searchReferenceComponentsByUuid(DbSession dbSession, List<ComponentDto> components) {
  192. List<String> referenceComponentIds = components.stream()
  193. .map(ComponentDto::getCopyResourceUuid)
  194. .filter(Objects::nonNull)
  195. .collect(toList());
  196. if (referenceComponentIds.isEmpty()) {
  197. return emptyMap();
  198. }
  199. return dbClient.componentDao().selectByUuids(dbSession, referenceComponentIds).stream()
  200. .collect(MoreCollectors.uniqueIndex(ComponentDto::uuid));
  201. }
  202. private void checkPermissions(ComponentDto baseComponent) {
  203. userSession.checkComponentPermission(UserRole.USER, baseComponent);
  204. }
  205. private static TreeWsResponse buildResponse(ComponentDto baseComponent, OrganizationDto organizationDto, List<ComponentDto> components,
  206. Map<String, ComponentDto> referenceComponentsByUuid, Paging paging) {
  207. TreeWsResponse.Builder response = TreeWsResponse.newBuilder();
  208. response.getPagingBuilder()
  209. .setPageIndex(paging.pageIndex())
  210. .setPageSize(paging.pageSize())
  211. .setTotal(paging.total())
  212. .build();
  213. response.setBaseComponent(toWsComponent(baseComponent, organizationDto, referenceComponentsByUuid));
  214. for (ComponentDto dto : components) {
  215. response.addComponents(toWsComponent(dto, organizationDto, referenceComponentsByUuid));
  216. }
  217. return response.build();
  218. }
  219. private static Components.Component.Builder toWsComponent(ComponentDto component, OrganizationDto organizationDto,
  220. Map<String, ComponentDto> referenceComponentsByUuid) {
  221. Components.Component.Builder wsComponent = componentDtoToWsComponent(component, organizationDto, Optional.empty());
  222. ComponentDto referenceComponent = referenceComponentsByUuid.get(component.getCopyResourceUuid());
  223. if (referenceComponent != null) {
  224. wsComponent.setRefId(referenceComponent.uuid());
  225. wsComponent.setRefKey(referenceComponent.getDbKey());
  226. }
  227. return wsComponent;
  228. }
  229. private ComponentTreeQuery toComponentTreeQuery(Request request, ComponentDto baseComponent) {
  230. List<String> childrenQualifiers = childrenQualifiers(request, baseComponent.qualifier());
  231. ComponentTreeQuery.Builder query = ComponentTreeQuery.builder()
  232. .setBaseUuid(baseComponent.uuid())
  233. .setStrategy(STRATEGIES.get(request.getStrategy()));
  234. if (request.getQuery() != null) {
  235. query.setNameOrKeyQuery(request.getQuery());
  236. }
  237. if (childrenQualifiers != null) {
  238. query.setQualifiers(childrenQualifiers);
  239. }
  240. return query.build();
  241. }
  242. @CheckForNull
  243. private List<String> childrenQualifiers(Request request, String baseQualifier) {
  244. List<String> requestQualifiers = request.getQualifiers();
  245. List<String> childrenQualifiers = null;
  246. if (LEAVES_STRATEGY.equals(request.getStrategy())) {
  247. childrenQualifiers = resourceTypes.getLeavesQualifiers(baseQualifier);
  248. }
  249. if (requestQualifiers == null) {
  250. return childrenQualifiers;
  251. }
  252. if (childrenQualifiers == null) {
  253. return requestQualifiers;
  254. }
  255. Sets.SetView<String> qualifiersIntersection = Sets.intersection(new HashSet<>(childrenQualifiers), new HashSet<>(requestQualifiers));
  256. return new ArrayList<>(qualifiersIntersection);
  257. }
  258. private static Request toTreeWsRequest(org.sonar.api.server.ws.Request request) {
  259. return new Request()
  260. .setBaseComponentId(request.param(PARAM_COMPONENT_ID))
  261. .setComponent(request.param(PARAM_COMPONENT))
  262. .setBranch(request.param(PARAM_BRANCH))
  263. .setPullRequest(request.param(PARAM_PULL_REQUEST))
  264. .setStrategy(request.mandatoryParam(PARAM_STRATEGY))
  265. .setQuery(request.param(Param.TEXT_QUERY))
  266. .setQualifiers(request.paramAsStrings(PARAM_QUALIFIERS))
  267. .setSort(request.mandatoryParamAsStrings(Param.SORT))
  268. .setAsc(request.mandatoryParamAsBoolean(Param.ASCENDING))
  269. .setPage(request.mandatoryParamAsInt(Param.PAGE))
  270. .setPageSize(request.mandatoryParamAsInt(Param.PAGE_SIZE));
  271. }
  272. private static List<ComponentDto> paginateComponents(List<ComponentDto> components, Request wsRequest) {
  273. return components.stream().skip(offset(wsRequest.getPage(), wsRequest.getPageSize()))
  274. .limit(wsRequest.getPageSize()).collect(toList());
  275. }
  276. private static List<ComponentDto> sortComponents(List<ComponentDto> components, Request wsRequest) {
  277. List<String> sortParameters = wsRequest.getSort();
  278. if (sortParameters == null || sortParameters.isEmpty()) {
  279. return components;
  280. }
  281. boolean isAscending = wsRequest.getAsc();
  282. Map<String, Ordering<ComponentDto>> orderingsBySortField = ImmutableMap.<String, Ordering<ComponentDto>>builder()
  283. .put(NAME_SORT, stringOrdering(isAscending, ComponentDto::name))
  284. .put(QUALIFIER_SORT, stringOrdering(isAscending, ComponentDto::qualifier))
  285. .put(PATH_SORT, stringOrdering(isAscending, ComponentDto::path))
  286. .build();
  287. String firstSortParameter = sortParameters.get(0);
  288. Ordering<ComponentDto> primaryOrdering = orderingsBySortField.get(firstSortParameter);
  289. if (sortParameters.size() > 1) {
  290. for (int i = 1; i < sortParameters.size(); i++) {
  291. String secondarySortParameter = sortParameters.get(i);
  292. Ordering<ComponentDto> secondaryOrdering = orderingsBySortField.get(secondarySortParameter);
  293. primaryOrdering = primaryOrdering.compound(secondaryOrdering);
  294. }
  295. }
  296. return primaryOrdering.immutableSortedCopy(components);
  297. }
  298. private static Ordering<ComponentDto> stringOrdering(boolean isAscending, Function<ComponentDto, String> function) {
  299. Ordering<String> ordering = Ordering.from(CASE_INSENSITIVE_ORDER);
  300. if (!isAscending) {
  301. ordering = ordering.reverse();
  302. }
  303. return ordering.nullsLast().onResultOf(function);
  304. }
  305. private static class Request {
  306. private String baseComponentId;
  307. private String component;
  308. private String branch;
  309. private String pullRequest;
  310. private String strategy;
  311. private List<String> qualifiers;
  312. private String query;
  313. private List<String> sort;
  314. private Boolean asc;
  315. private Integer page;
  316. private Integer pageSize;
  317. /**
  318. * @deprecated since 6.4, please use {@link #getComponent()} instead
  319. */
  320. @Deprecated
  321. @CheckForNull
  322. private String getBaseComponentId() {
  323. return baseComponentId;
  324. }
  325. /**
  326. * @deprecated since 6.4, please use {@link #setComponent(String)} instead
  327. */
  328. @Deprecated
  329. private Request setBaseComponentId(@Nullable String baseComponentId) {
  330. this.baseComponentId = baseComponentId;
  331. return this;
  332. }
  333. public Request setComponent(@Nullable String component) {
  334. this.component = component;
  335. return this;
  336. }
  337. @CheckForNull
  338. private String getComponent() {
  339. return component;
  340. }
  341. @CheckForNull
  342. private String getBranch() {
  343. return branch;
  344. }
  345. private Request setBranch(@Nullable String branch) {
  346. this.branch = branch;
  347. return this;
  348. }
  349. @CheckForNull
  350. public String getPullRequest() {
  351. return pullRequest;
  352. }
  353. public Request setPullRequest(@Nullable String pullRequest) {
  354. this.pullRequest = pullRequest;
  355. return this;
  356. }
  357. @CheckForNull
  358. private String getStrategy() {
  359. return strategy;
  360. }
  361. private Request setStrategy(@Nullable String strategy) {
  362. this.strategy = strategy;
  363. return this;
  364. }
  365. @CheckForNull
  366. private List<String> getQualifiers() {
  367. return qualifiers;
  368. }
  369. private Request setQualifiers(@Nullable List<String> qualifiers) {
  370. this.qualifiers = qualifiers;
  371. return this;
  372. }
  373. @CheckForNull
  374. private String getQuery() {
  375. return query;
  376. }
  377. private Request setQuery(@Nullable String query) {
  378. this.query = query;
  379. return this;
  380. }
  381. @CheckForNull
  382. private List<String> getSort() {
  383. return sort;
  384. }
  385. private Request setSort(@Nullable List<String> sort) {
  386. this.sort = sort;
  387. return this;
  388. }
  389. private Boolean getAsc() {
  390. return asc;
  391. }
  392. private Request setAsc(@Nullable Boolean asc) {
  393. this.asc = asc;
  394. return this;
  395. }
  396. @CheckForNull
  397. private Integer getPage() {
  398. return page;
  399. }
  400. private Request setPage(@Nullable Integer page) {
  401. this.page = page;
  402. return this;
  403. }
  404. @CheckForNull
  405. private Integer getPageSize() {
  406. return pageSize;
  407. }
  408. private Request setPageSize(@Nullable Integer pageSize) {
  409. this.pageSize = pageSize;
  410. return this;
  411. }
  412. }
  413. }