3 * Copyright (C) 2009-2019 SonarSource SA
4 * mailto:info AT sonarsource DOT com
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.
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.
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.
20 package org.sonar.ce.task.projectanalysis.component;
22 import com.google.common.base.Preconditions;
23 import java.util.ArrayList;
24 import java.util.LinkedHashMap;
25 import java.util.LinkedList;
26 import java.util.List;
28 import java.util.Objects;
29 import java.util.function.Function;
30 import javax.annotation.CheckForNull;
31 import javax.annotation.Nullable;
32 import org.apache.commons.io.FilenameUtils;
33 import org.apache.commons.lang.StringUtils;
34 import org.sonar.ce.task.projectanalysis.analysis.Branch;
35 import org.sonar.core.util.stream.MoreCollectors;
36 import org.sonar.scanner.protocol.output.ScannerReport;
37 import org.sonar.scanner.protocol.output.ScannerReport.Component.FileStatus;
38 import org.sonar.server.project.Project;
40 import static com.google.common.base.Preconditions.checkArgument;
41 import static java.lang.String.format;
42 import static java.util.Objects.requireNonNull;
43 import static org.apache.commons.lang.StringUtils.removeStart;
44 import static org.apache.commons.lang.StringUtils.trimToNull;
46 public class ComponentTreeBuilder {
48 private final ComponentKeyGenerator keyGenerator;
49 private final ComponentKeyGenerator publicKeyGenerator;
51 * Will supply the UUID for any component in the tree, given it's key.
53 * The String argument of the {@link Function#apply(Object)} method is the component's key.
56 private final Function<String, String> uuidSupplier;
58 * Will supply the {@link ScannerReport.Component} of all the components in the component tree as we crawl it from the
61 * The Integer argument of the {@link Function#apply(Object)} method is the component's ref.
64 private final Function<Integer, ScannerReport.Component> scannerComponentSupplier;
65 private final Project project;
66 private final Branch branch;
68 private final ProjectAttributes projectAttributes;
70 private ScannerReport.Component rootComponent;
71 private String scmBasePath;
73 public ComponentTreeBuilder(
74 ComponentKeyGenerator keyGenerator,
75 ComponentKeyGenerator publicKeyGenerator,
76 Function<String, String> uuidSupplier,
77 Function<Integer, ScannerReport.Component> scannerComponentSupplier,
80 ProjectAttributes projectAttributes) {
82 this.keyGenerator = keyGenerator;
83 this.publicKeyGenerator = publicKeyGenerator;
84 this.uuidSupplier = uuidSupplier;
85 this.scannerComponentSupplier = scannerComponentSupplier;
86 this.project = project;
88 this.projectAttributes = requireNonNull(projectAttributes, "projectAttributes can't be null");
91 public Component buildProject(ScannerReport.Component project, String scmBasePath) {
92 this.rootComponent = project;
93 this.scmBasePath = trimToNull(scmBasePath);
95 Node root = createProjectHierarchy(project);
96 return buildComponent(root, "", "");
99 private Node createProjectHierarchy(ScannerReport.Component rootComponent) {
100 Preconditions.checkArgument(rootComponent.getType() == ScannerReport.Component.ComponentType.PROJECT, "Expected root component of type 'PROJECT'");
102 LinkedList<ScannerReport.Component> queue = new LinkedList<>();
103 rootComponent.getChildRefList()
105 .map(scannerComponentSupplier)
106 .forEach(queue::addLast);
108 Node root = new Node();
109 root.reportComponent = rootComponent;
111 while (!queue.isEmpty()) {
112 ScannerReport.Component component = queue.removeFirst();
113 switch (component.getType()) {
115 addFile(root, component);
118 throw new IllegalArgumentException(format("Unsupported component type '%s'", component.getType()));
124 private static void addFile(Node root, ScannerReport.Component file) {
125 Preconditions.checkArgument(!StringUtils.isEmpty(file.getProjectRelativePath()), "Files should have a project relative path: " + file);
126 String[] split = StringUtils.split(file.getProjectRelativePath(), '/');
127 Node currentNode = root;
129 for (int i = 0; i < split.length; i++) {
130 currentNode = currentNode.children().computeIfAbsent(split[i], k -> new Node());
132 currentNode.reportComponent = file;
135 private Component buildComponent(Node node, String currentPath, String parentPath) {
136 List<Component> childComponents = buildChildren(node, currentPath);
137 ScannerReport.Component component = node.reportComponent();
139 if (component != null) {
140 if (component.getType() == ScannerReport.Component.ComponentType.FILE) {
141 return buildFile(component);
142 } else if (component.getType() == ScannerReport.Component.ComponentType.PROJECT) {
143 return buildProject(childComponents);
147 return buildDirectory(parentPath, currentPath, childComponents);
150 private List<Component> buildChildren(Node node, String currentPath) {
151 List<Component> children = new ArrayList<>();
153 for (Map.Entry<String, Node> e : node.children().entrySet()) {
154 String path = buildPath(currentPath, e.getKey());
155 Node childNode = e.getValue();
157 // collapse folders that only contain one folder
158 while (childNode.children().size() == 1 && childNode.children().values().iterator().next().children().size() > 0) {
159 Map.Entry<String, Node> childEntry = childNode.children().entrySet().iterator().next();
160 path = buildPath(path, childEntry.getKey());
161 childNode = childEntry.getValue();
163 children.add(buildComponent(childNode, path, currentPath));
168 private static String buildPath(String currentPath, String file) {
169 if (currentPath.isEmpty()) {
172 return currentPath + "/" + file;
175 private Component buildProject(List<Component> children) {
176 String projectKey = keyGenerator.generateKey(rootComponent.getKey(), null);
177 String uuid = uuidSupplier.apply(projectKey);
178 String projectPublicKey = publicKeyGenerator.generateKey(rootComponent.getKey(), null);
179 ComponentImpl.Builder builder = ComponentImpl.builder(Component.Type.PROJECT)
181 .setDbKey(projectKey)
182 .setKey(projectPublicKey)
183 .setStatus(convertStatus(rootComponent.getStatus()))
184 .setProjectAttributes(projectAttributes)
185 .setReportAttributes(createAttributesBuilder(rootComponent.getRef(), rootComponent.getProjectRelativePath(), scmBasePath).build())
186 .addChildren(children);
187 setNameAndDescription(rootComponent, builder);
188 return builder.build();
191 private ComponentImpl buildFile(ScannerReport.Component component) {
192 String key = keyGenerator.generateKey(rootComponent.getKey(), component.getProjectRelativePath());
193 String publicKey = publicKeyGenerator.generateKey(rootComponent.getKey(), component.getProjectRelativePath());
194 return ComponentImpl.builder(Component.Type.FILE)
195 .setUuid(uuidSupplier.apply(key))
198 .setName(component.getProjectRelativePath())
199 .setShortName(FilenameUtils.getName(component.getProjectRelativePath()))
200 .setStatus(convertStatus(component.getStatus()))
201 .setDescription(trimToNull(component.getDescription()))
202 .setReportAttributes(createAttributesBuilder(component.getRef(), component.getProjectRelativePath(), scmBasePath).build())
203 .setFileAttributes(createFileAttributes(component))
207 private ComponentImpl buildDirectory(String parentPath, String path, List<Component> children) {
208 String key = keyGenerator.generateKey(rootComponent.getKey(), path);
209 String publicKey = publicKeyGenerator.generateKey(rootComponent.getKey(), path);
210 return ComponentImpl.builder(Component.Type.DIRECTORY)
211 .setUuid(uuidSupplier.apply(key))
215 .setShortName(removeStart(removeStart(path, parentPath), "/"))
216 .setStatus(convertStatus(FileStatus.UNAVAILABLE))
217 .setReportAttributes(createAttributesBuilder(null, path, scmBasePath).build())
218 .addChildren(children)
222 public Component buildChangedComponentTreeRoot(Component project) {
223 return buildChangedComponentTree(project);
226 private static ComponentImpl.Builder changedComponentBuilder(Component component) {
227 return ComponentImpl.builder(component.getType())
228 .setUuid(component.getUuid())
229 .setDbKey(component.getDbKey())
230 .setKey(component.getKey())
231 .setStatus(component.getStatus())
232 .setReportAttributes(component.getReportAttributes())
233 .setName(component.getName())
234 .setShortName(component.getShortName())
235 .setDescription(component.getDescription());
239 private static Component buildChangedComponentTree(Component component) {
240 switch (component.getType()) {
242 return buildChangedProject(component);
244 return buildChangedIntermediate(component);
246 return buildChangedFile(component);
249 throw new IllegalArgumentException(format("Unsupported component type '%s'", component.getType()));
253 private static Component buildChangedProject(Component component) {
254 return changedComponentBuilder(component)
255 .setProjectAttributes(new ProjectAttributes(component.getProjectAttributes()))
256 .addChildren(buildChangedComponentChildren(component))
261 private static Component buildChangedIntermediate(Component component) {
262 List<Component> children = buildChangedComponentChildren(component);
263 if (children.isEmpty()) {
266 return changedComponentBuilder(component)
267 .addChildren(children)
272 private static Component buildChangedFile(Component component) {
273 if (component.getStatus() == Component.Status.SAME) {
276 return changedComponentBuilder(component)
277 .setFileAttributes(component.getFileAttributes())
281 private static List<Component> buildChangedComponentChildren(Component component) {
282 return component.getChildren()
284 .map(ComponentTreeBuilder::buildChangedComponentTree)
285 .filter(Objects::nonNull)
286 .collect(MoreCollectors.toList());
289 private void setNameAndDescription(ScannerReport.Component component, ComponentImpl.Builder builder) {
290 if (branch.isMain()) {
292 .setName(nameOfProject(component))
293 .setDescription(component.getDescription());
296 .setName(project.getName())
297 .setDescription(project.getDescription());
301 private static Component.Status convertStatus(FileStatus status) {
304 return Component.Status.ADDED;
306 return Component.Status.SAME;
308 return Component.Status.CHANGED;
310 return Component.Status.UNAVAILABLE;
313 throw new IllegalArgumentException("Unsupported ComponentType value " + status);
317 private String nameOfProject(ScannerReport.Component component) {
318 String name = trimToNull(component.getName());
322 return project.getName();
325 private static ReportAttributes.Builder createAttributesBuilder(@Nullable Integer ref, String path, @Nullable String scmBasePath) {
326 return ReportAttributes.newBuilder(ref)
327 .setScmPath(computeScmPath(scmBasePath, path));
331 private static String computeScmPath(@Nullable String scmBasePath, String scmRelativePath) {
332 if (scmRelativePath.isEmpty()) {
335 if (scmBasePath == null) {
336 return scmRelativePath;
339 return scmBasePath + '/' + scmRelativePath;
342 private static FileAttributes createFileAttributes(ScannerReport.Component component) {
343 checkArgument(component.getType() == ScannerReport.Component.ComponentType.FILE);
344 checkArgument(component.getLines() > 0, "File '%s' has no line", component.getProjectRelativePath());
345 return new FileAttributes(
346 component.getIsTest(),
347 trimToNull(component.getLanguage()),
348 component.getLines());
351 private static class Node {
352 private final Map<String, Node> children = new LinkedHashMap<>();
353 private ScannerReport.Component reportComponent;
355 private Map<String, Node> children() {
360 private ScannerReport.Component reportComponent() {
361 return reportComponent;