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