]> source.dussan.org Git - sonarqube.git/blob
688b514ab5eee2c8673b469b2c820265a9e8132f
[sonarqube.git] /
1 /*
2  * SonarQube
3  * Copyright (C) 2009-2019 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 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;
27 import java.util.Map;
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;
39
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;
45
46 public class ComponentTreeBuilder {
47
48   private final ComponentKeyGenerator keyGenerator;
49   private final ComponentKeyGenerator publicKeyGenerator;
50   /**
51    * Will supply the UUID for any component in the tree, given it's key.
52    * <p>
53    * The String argument of the {@link Function#apply(Object)} method is the component's key.
54    * </p>
55    */
56   private final Function<String, String> uuidSupplier;
57   /**
58    * Will supply the {@link ScannerReport.Component} of all the components in the component tree as we crawl it from the
59    * root.
60    * <p>
61    * The Integer argument of the {@link Function#apply(Object)} method is the component's ref.
62    * </p>
63    */
64   private final Function<Integer, ScannerReport.Component> scannerComponentSupplier;
65   private final Project project;
66   private final Branch branch;
67   @Nullable
68   private final ProjectAttributes projectAttributes;
69
70   private ScannerReport.Component rootComponent;
71   private String scmBasePath;
72
73   public ComponentTreeBuilder(
74     ComponentKeyGenerator keyGenerator,
75     ComponentKeyGenerator publicKeyGenerator,
76     Function<String, String> uuidSupplier,
77     Function<Integer, ScannerReport.Component> scannerComponentSupplier,
78     Project project,
79     Branch branch,
80     ProjectAttributes projectAttributes) {
81
82     this.keyGenerator = keyGenerator;
83     this.publicKeyGenerator = publicKeyGenerator;
84     this.uuidSupplier = uuidSupplier;
85     this.scannerComponentSupplier = scannerComponentSupplier;
86     this.project = project;
87     this.branch = branch;
88     this.projectAttributes = requireNonNull(projectAttributes, "projectAttributes can't be null");
89   }
90
91   public Component buildProject(ScannerReport.Component project, String scmBasePath) {
92     this.rootComponent = project;
93     this.scmBasePath = trimToNull(scmBasePath);
94
95     Node root = createProjectHierarchy(project);
96     return buildComponent(root, "", "");
97   }
98
99   private Node createProjectHierarchy(ScannerReport.Component rootComponent) {
100     Preconditions.checkArgument(rootComponent.getType() == ScannerReport.Component.ComponentType.PROJECT, "Expected root component of type 'PROJECT'");
101
102     LinkedList<ScannerReport.Component> queue = new LinkedList<>();
103     rootComponent.getChildRefList()
104       .stream()
105       .map(scannerComponentSupplier)
106       .forEach(queue::addLast);
107
108     Node root = new Node();
109     root.reportComponent = rootComponent;
110
111     while (!queue.isEmpty()) {
112       ScannerReport.Component component = queue.removeFirst();
113       switch (component.getType()) {
114         case FILE:
115           addFile(root, component);
116           break;
117         default:
118           throw new IllegalArgumentException(format("Unsupported component type '%s'", component.getType()));
119       }
120     }
121     return root;
122   }
123
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;
128
129     for (int i = 0; i < split.length; i++) {
130       currentNode = currentNode.children().computeIfAbsent(split[i], k -> new Node());
131     }
132     currentNode.reportComponent = file;
133   }
134
135   private Component buildComponent(Node node, String currentPath, String parentPath) {
136     List<Component> childComponents = buildChildren(node, currentPath);
137     ScannerReport.Component component = node.reportComponent();
138
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);
144       }
145     }
146
147     return buildDirectory(parentPath, currentPath, childComponents);
148   }
149
150   private List<Component> buildChildren(Node node, String currentPath) {
151     List<Component> children = new ArrayList<>();
152
153     for (Map.Entry<String, Node> e : node.children().entrySet()) {
154       String path = buildPath(currentPath, e.getKey());
155       Node childNode = e.getValue();
156
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();
162       }
163       children.add(buildComponent(childNode, path, currentPath));
164     }
165     return children;
166   }
167
168   private static String buildPath(String currentPath, String file) {
169     if (currentPath.isEmpty()) {
170       return file;
171     }
172     return currentPath + "/" + file;
173   }
174
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)
180       .setUuid(uuid)
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();
189   }
190
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))
196       .setDbKey(key)
197       .setKey(publicKey)
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))
204       .build();
205   }
206
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))
212       .setDbKey(key)
213       .setKey(publicKey)
214       .setName(path)
215       .setShortName(removeStart(removeStart(path, parentPath), "/"))
216       .setStatus(convertStatus(FileStatus.UNAVAILABLE))
217       .setReportAttributes(createAttributesBuilder(null, path, scmBasePath).build())
218       .addChildren(children)
219       .build();
220   }
221
222   public Component buildChangedComponentTreeRoot(Component project) {
223     return buildChangedComponentTree(project);
224   }
225
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());
236   }
237
238   @Nullable
239   private static Component buildChangedComponentTree(Component component) {
240     switch (component.getType()) {
241       case PROJECT:
242         return buildChangedProject(component);
243       case DIRECTORY:
244         return buildChangedIntermediate(component);
245       case FILE:
246         return buildChangedFile(component);
247
248       default:
249         throw new IllegalArgumentException(format("Unsupported component type '%s'", component.getType()));
250     }
251   }
252
253   private static Component buildChangedProject(Component component) {
254     return changedComponentBuilder(component)
255       .setProjectAttributes(new ProjectAttributes(component.getProjectAttributes()))
256       .addChildren(buildChangedComponentChildren(component))
257       .build();
258   }
259
260   @Nullable
261   private static Component buildChangedIntermediate(Component component) {
262     List<Component> children = buildChangedComponentChildren(component);
263     if (children.isEmpty()) {
264       return null;
265     }
266     return changedComponentBuilder(component)
267       .addChildren(children)
268       .build();
269   }
270
271   @Nullable
272   private static Component buildChangedFile(Component component) {
273     if (component.getStatus() == Component.Status.SAME) {
274       return null;
275     }
276     return changedComponentBuilder(component)
277       .setFileAttributes(component.getFileAttributes())
278       .build();
279   }
280
281   private static List<Component> buildChangedComponentChildren(Component component) {
282     return component.getChildren()
283       .stream()
284       .map(ComponentTreeBuilder::buildChangedComponentTree)
285       .filter(Objects::nonNull)
286       .collect(MoreCollectors.toList());
287   }
288
289   private void setNameAndDescription(ScannerReport.Component component, ComponentImpl.Builder builder) {
290     if (branch.isMain()) {
291       builder
292         .setName(nameOfProject(component))
293         .setDescription(component.getDescription());
294     } else {
295       builder
296         .setName(project.getName())
297         .setDescription(project.getDescription());
298     }
299   }
300
301   private static Component.Status convertStatus(FileStatus status) {
302     switch (status) {
303       case ADDED:
304         return Component.Status.ADDED;
305       case SAME:
306         return Component.Status.SAME;
307       case CHANGED:
308         return Component.Status.CHANGED;
309       case UNAVAILABLE:
310         return Component.Status.UNAVAILABLE;
311       case UNRECOGNIZED:
312       default:
313         throw new IllegalArgumentException("Unsupported ComponentType value " + status);
314     }
315   }
316
317   private String nameOfProject(ScannerReport.Component component) {
318     String name = trimToNull(component.getName());
319     if (name != null) {
320       return name;
321     }
322     return project.getName();
323   }
324
325   private static ReportAttributes.Builder createAttributesBuilder(@Nullable Integer ref, String path, @Nullable String scmBasePath) {
326     return ReportAttributes.newBuilder(ref)
327       .setScmPath(computeScmPath(scmBasePath, path));
328   }
329
330   @CheckForNull
331   private static String computeScmPath(@Nullable String scmBasePath, String scmRelativePath) {
332     if (scmRelativePath.isEmpty()) {
333       return scmBasePath;
334     }
335     if (scmBasePath == null) {
336       return scmRelativePath;
337     }
338
339     return scmBasePath + '/' + scmRelativePath;
340   }
341
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());
349   }
350
351   private static class Node {
352     private final Map<String, Node> children = new LinkedHashMap<>();
353     private ScannerReport.Component reportComponent;
354
355     private Map<String, Node> children() {
356       return children;
357     }
358
359     @CheckForNull
360     private ScannerReport.Component reportComponent() {
361       return reportComponent;
362     }
363   }
364 }