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.

ComponentTreeBuilder.java 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  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.ce.task.projectanalysis.component;
  21. import java.util.ArrayList;
  22. import java.util.LinkedHashMap;
  23. import java.util.LinkedList;
  24. import java.util.List;
  25. import java.util.Map;
  26. import java.util.Objects;
  27. import java.util.function.Function;
  28. import java.util.function.UnaryOperator;
  29. import javax.annotation.CheckForNull;
  30. import javax.annotation.Nullable;
  31. import org.apache.commons.io.FilenameUtils;
  32. import org.apache.commons.lang3.StringUtils;
  33. import org.sonar.ce.task.projectanalysis.analysis.Branch;
  34. import org.sonar.scanner.protocol.output.ScannerReport;
  35. import org.sonar.scanner.protocol.output.ScannerReport.Component.FileStatus;
  36. import org.sonar.server.project.Project;
  37. import static com.google.common.base.Preconditions.checkArgument;
  38. import static java.lang.String.format;
  39. import static java.util.Objects.requireNonNull;
  40. import static org.apache.commons.lang3.StringUtils.removeStart;
  41. import static org.apache.commons.lang3.StringUtils.trimToNull;
  42. import static org.sonar.scanner.protocol.output.ScannerReport.Component.ComponentType.FILE;
  43. public class ComponentTreeBuilder {
  44. private final ComponentKeyGenerator keyGenerator;
  45. /**
  46. * Will supply the UUID for any component in the tree, given it's key.
  47. * <p>
  48. * The String argument of the {@link Function#apply(Object)} method is the component's key.
  49. * </p>
  50. */
  51. private final Function<String, String> uuidSupplier;
  52. /**
  53. * Will supply the {@link ScannerReport.Component} of all the components in the component tree as we crawl it from the
  54. * root.
  55. * <p>
  56. * The Integer argument of the {@link Function#apply(Object)} method is the component's ref.
  57. * </p>
  58. */
  59. private final Function<Integer, ScannerReport.Component> scannerComponentSupplier;
  60. private final Project project;
  61. private final Branch branch;
  62. private final ProjectAttributes projectAttributes;
  63. private ScannerReport.Component rootComponent;
  64. private String scmBasePath;
  65. public ComponentTreeBuilder(
  66. ComponentKeyGenerator keyGenerator,
  67. UnaryOperator<String> uuidSupplier,
  68. Function<Integer, ScannerReport.Component> scannerComponentSupplier,
  69. Project project,
  70. Branch branch,
  71. ProjectAttributes projectAttributes) {
  72. this.keyGenerator = keyGenerator;
  73. this.uuidSupplier = uuidSupplier;
  74. this.scannerComponentSupplier = scannerComponentSupplier;
  75. this.project = project;
  76. this.branch = branch;
  77. this.projectAttributes = requireNonNull(projectAttributes, "projectAttributes can't be null");
  78. }
  79. public Component buildProject(ScannerReport.Component project, String scmBasePath) {
  80. this.rootComponent = project;
  81. this.scmBasePath = trimToNull(scmBasePath);
  82. Node root = createProjectHierarchy(project);
  83. return buildComponent(root, "", "");
  84. }
  85. private Node createProjectHierarchy(ScannerReport.Component rootComponent) {
  86. checkArgument(rootComponent.getType() == ScannerReport.Component.ComponentType.PROJECT, "Expected root component of type 'PROJECT'");
  87. LinkedList<ScannerReport.Component> queue = new LinkedList<>();
  88. rootComponent.getChildRefList().stream()
  89. .map(scannerComponentSupplier)
  90. .forEach(queue::addLast);
  91. Node root = new Node();
  92. root.reportComponent = rootComponent;
  93. while (!queue.isEmpty()) {
  94. ScannerReport.Component component = queue.removeFirst();
  95. checkArgument(component.getType() == FILE, "Unsupported component type '%s'", component.getType());
  96. addFile(root, component);
  97. }
  98. return root;
  99. }
  100. private static void addFile(Node root, ScannerReport.Component file) {
  101. checkArgument(!StringUtils.isEmpty(file.getProjectRelativePath()), "Files should have a project relative path: " + file);
  102. String[] split = StringUtils.split(file.getProjectRelativePath(), '/');
  103. Node currentNode = root;
  104. for (int i = 0; i < split.length; i++) {
  105. currentNode = currentNode.children().computeIfAbsent(split[i], k -> new Node());
  106. }
  107. currentNode.reportComponent = file;
  108. }
  109. private Component buildComponent(Node node, String currentPath, String parentPath) {
  110. List<Component> childComponents = buildChildren(node, currentPath);
  111. ScannerReport.Component component = node.reportComponent();
  112. if (component != null) {
  113. if (component.getType() == FILE) {
  114. return buildFile(component);
  115. } else if (component.getType() == ScannerReport.Component.ComponentType.PROJECT) {
  116. return buildProject(childComponents);
  117. }
  118. }
  119. return buildDirectory(parentPath, currentPath, childComponents);
  120. }
  121. private List<Component> buildChildren(Node node, String currentPath) {
  122. List<Component> children = new ArrayList<>();
  123. for (Map.Entry<String, Node> e : node.children().entrySet()) {
  124. String path = buildPath(currentPath, e.getKey());
  125. Node childNode = e.getValue();
  126. // collapse folders that only contain one folder
  127. while (childNode.children().size() == 1 && childNode.children().values().iterator().next().children().size() > 0) {
  128. Map.Entry<String, Node> childEntry = childNode.children().entrySet().iterator().next();
  129. path = buildPath(path, childEntry.getKey());
  130. childNode = childEntry.getValue();
  131. }
  132. children.add(buildComponent(childNode, path, currentPath));
  133. }
  134. return children;
  135. }
  136. private static String buildPath(String currentPath, String file) {
  137. if (currentPath.isEmpty()) {
  138. return file;
  139. }
  140. return currentPath + "/" + file;
  141. }
  142. private Component buildProject(List<Component> children) {
  143. String projectKey = keyGenerator.generateKey(rootComponent.getKey(), null);
  144. String uuid = uuidSupplier.apply(projectKey);
  145. ComponentImpl.Builder builder = ComponentImpl.builder(Component.Type.PROJECT)
  146. .setUuid(uuid)
  147. .setKey(projectKey)
  148. .setStatus(convertStatus(rootComponent.getStatus()))
  149. .setProjectAttributes(projectAttributes)
  150. .setReportAttributes(createAttributesBuilder(rootComponent.getRef(), rootComponent.getProjectRelativePath(), scmBasePath).build())
  151. .addChildren(children);
  152. setNameAndDescription(rootComponent, builder);
  153. return builder.build();
  154. }
  155. private ComponentImpl buildFile(ScannerReport.Component component) {
  156. String key = keyGenerator.generateKey(rootComponent.getKey(), component.getProjectRelativePath());
  157. return ComponentImpl.builder(Component.Type.FILE)
  158. .setUuid(uuidSupplier.apply(key))
  159. .setKey(key)
  160. .setName(component.getProjectRelativePath())
  161. .setShortName(FilenameUtils.getName(component.getProjectRelativePath()))
  162. .setStatus(convertStatus(component.getStatus()))
  163. .setDescription(trimToNull(component.getDescription()))
  164. .setReportAttributes(createAttributesBuilder(component.getRef(), component.getProjectRelativePath(), scmBasePath).build())
  165. .setFileAttributes(createFileAttributes(component))
  166. .build();
  167. }
  168. private ComponentImpl buildDirectory(String parentPath, String path, List<Component> children) {
  169. String key = keyGenerator.generateKey(rootComponent.getKey(), path);
  170. return ComponentImpl.builder(Component.Type.DIRECTORY)
  171. .setUuid(uuidSupplier.apply(key))
  172. .setKey(key)
  173. .setName(path)
  174. .setShortName(removeStart(removeStart(path, parentPath), "/"))
  175. .setStatus(convertStatus(FileStatus.UNAVAILABLE))
  176. .setReportAttributes(createAttributesBuilder(null, path, scmBasePath).build())
  177. .addChildren(children)
  178. .build();
  179. }
  180. public Component buildChangedComponentTreeRoot(Component project) {
  181. return buildChangedComponentTree(project);
  182. }
  183. @Nullable
  184. private static Component buildChangedComponentTree(Component component) {
  185. switch (component.getType()) {
  186. case PROJECT:
  187. return buildChangedProject(component);
  188. case DIRECTORY:
  189. return buildChangedDirectory(component);
  190. case FILE:
  191. return buildChangedFile(component);
  192. default:
  193. throw new IllegalArgumentException(format("Unsupported component type '%s'", component.getType()));
  194. }
  195. }
  196. private static Component buildChangedProject(Component component) {
  197. return changedComponentBuilder(component, "")
  198. .setProjectAttributes(new ProjectAttributes(component.getProjectAttributes()))
  199. .addChildren(buildChangedComponentChildren(component))
  200. .build();
  201. }
  202. @Nullable
  203. private static Component buildChangedDirectory(Component component) {
  204. List<Component> children = buildChangedComponentChildren(component);
  205. if (children.isEmpty()) {
  206. return null;
  207. }
  208. if (children.size() == 1 && children.get(0).getType() == Component.Type.DIRECTORY) {
  209. Component child = children.get(0);
  210. String shortName = component.getShortName() + "/" + child.getShortName();
  211. return changedComponentBuilder(child, shortName)
  212. .addChildren(child.getChildren())
  213. .build();
  214. } else {
  215. return changedComponentBuilder(component, component.getShortName())
  216. .addChildren(children)
  217. .build();
  218. }
  219. }
  220. private static List<Component> buildChangedComponentChildren(Component component) {
  221. return component.getChildren().stream()
  222. .map(ComponentTreeBuilder::buildChangedComponentTree)
  223. .filter(Objects::nonNull)
  224. .toList();
  225. }
  226. private static ComponentImpl.Builder changedComponentBuilder(Component component, String newShortName) {
  227. return ComponentImpl.builder(component.getType())
  228. .setUuid(component.getUuid())
  229. .setKey(component.getKey())
  230. .setStatus(component.getStatus())
  231. .setReportAttributes(component.getReportAttributes())
  232. .setName(component.getName())
  233. .setShortName(newShortName)
  234. .setDescription(component.getDescription());
  235. }
  236. @Nullable
  237. private static Component buildChangedFile(Component component) {
  238. if (component.getStatus() == Component.Status.SAME) {
  239. return null;
  240. }
  241. return component;
  242. }
  243. private void setNameAndDescription(ScannerReport.Component component, ComponentImpl.Builder builder) {
  244. if (branch.isMain()) {
  245. builder
  246. .setName(nameOfProject(component))
  247. .setDescription(component.getDescription());
  248. } else {
  249. builder
  250. .setName(project.getName())
  251. .setDescription(project.getDescription());
  252. }
  253. }
  254. private static Component.Status convertStatus(FileStatus status) {
  255. switch (status) {
  256. case ADDED:
  257. return Component.Status.ADDED;
  258. case SAME:
  259. return Component.Status.SAME;
  260. case CHANGED:
  261. return Component.Status.CHANGED;
  262. case UNAVAILABLE:
  263. return Component.Status.UNAVAILABLE;
  264. case UNRECOGNIZED:
  265. default:
  266. throw new IllegalArgumentException("Unsupported ComponentType value " + status);
  267. }
  268. }
  269. private String nameOfProject(ScannerReport.Component component) {
  270. String name = trimToNull(component.getName());
  271. if (name != null) {
  272. return name;
  273. }
  274. return project.getName();
  275. }
  276. private static ReportAttributes.Builder createAttributesBuilder(@Nullable Integer ref, String path, @Nullable String scmBasePath) {
  277. return ReportAttributes.newBuilder(ref)
  278. .setScmPath(computeScmPath(scmBasePath, path));
  279. }
  280. @CheckForNull
  281. private static String computeScmPath(@Nullable String scmBasePath, String scmRelativePath) {
  282. if (scmRelativePath.isEmpty()) {
  283. return scmBasePath;
  284. }
  285. if (scmBasePath == null) {
  286. return scmRelativePath;
  287. }
  288. return scmBasePath + '/' + scmRelativePath;
  289. }
  290. private static FileAttributes createFileAttributes(ScannerReport.Component component) {
  291. checkArgument(component.getType() == FILE);
  292. checkArgument(component.getLines() > 0, "File '%s' has no line", component.getProjectRelativePath());
  293. String lang = trimToNull(component.getLanguage());
  294. return new FileAttributes(
  295. component.getIsTest(),
  296. lang != null ? lang.intern() : null,
  297. component.getLines(),
  298. component.getMarkedAsUnchanged(),
  299. component.getOldRelativeFilePath()
  300. );
  301. }
  302. private static class Node {
  303. private final Map<String, Node> children = new LinkedHashMap<>();
  304. private ScannerReport.Component reportComponent = null;
  305. private Map<String, Node> children() {
  306. return children;
  307. }
  308. @CheckForNull
  309. private ScannerReport.Component reportComponent() {
  310. return reportComponent;
  311. }
  312. }
  313. }