123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355 |
- /*
- * SonarQube
- * Copyright (C) 2009-2024 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
- package org.sonar.ce.task.projectanalysis.component;
-
- import java.util.ArrayList;
- import java.util.LinkedHashMap;
- import java.util.LinkedList;
- import java.util.List;
- import java.util.Map;
- import java.util.Objects;
- import java.util.function.Function;
- import java.util.function.UnaryOperator;
- import javax.annotation.CheckForNull;
- import javax.annotation.Nullable;
- import org.apache.commons.io.FilenameUtils;
- import org.apache.commons.lang3.StringUtils;
- import org.sonar.ce.task.projectanalysis.analysis.Branch;
- import org.sonar.scanner.protocol.output.ScannerReport;
- import org.sonar.scanner.protocol.output.ScannerReport.Component.FileStatus;
- import org.sonar.server.project.Project;
-
- import static com.google.common.base.Preconditions.checkArgument;
- import static java.lang.String.format;
- import static java.util.Objects.requireNonNull;
- import static org.apache.commons.lang3.StringUtils.removeStart;
- import static org.apache.commons.lang3.StringUtils.trimToNull;
- import static org.sonar.scanner.protocol.output.ScannerReport.Component.ComponentType.FILE;
-
- public class ComponentTreeBuilder {
- private final ComponentKeyGenerator keyGenerator;
- /**
- * Will supply the UUID for any component in the tree, given it's key.
- * <p>
- * The String argument of the {@link Function#apply(Object)} method is the component's key.
- * </p>
- */
- private final Function<String, String> uuidSupplier;
- /**
- * Will supply the {@link ScannerReport.Component} of all the components in the component tree as we crawl it from the
- * root.
- * <p>
- * The Integer argument of the {@link Function#apply(Object)} method is the component's ref.
- * </p>
- */
- private final Function<Integer, ScannerReport.Component> scannerComponentSupplier;
- private final Project project;
- private final Branch branch;
- private final ProjectAttributes projectAttributes;
-
- private ScannerReport.Component rootComponent;
- private String scmBasePath;
-
- public ComponentTreeBuilder(
- ComponentKeyGenerator keyGenerator,
- UnaryOperator<String> uuidSupplier,
- Function<Integer, ScannerReport.Component> scannerComponentSupplier,
- Project project,
- Branch branch,
- ProjectAttributes projectAttributes) {
-
- this.keyGenerator = keyGenerator;
- this.uuidSupplier = uuidSupplier;
- this.scannerComponentSupplier = scannerComponentSupplier;
- this.project = project;
- this.branch = branch;
- this.projectAttributes = requireNonNull(projectAttributes, "projectAttributes can't be null");
- }
-
- public Component buildProject(ScannerReport.Component project, String scmBasePath) {
- this.rootComponent = project;
- this.scmBasePath = trimToNull(scmBasePath);
-
- Node root = createProjectHierarchy(project);
- return buildComponent(root, "", "");
- }
-
- private Node createProjectHierarchy(ScannerReport.Component rootComponent) {
- checkArgument(rootComponent.getType() == ScannerReport.Component.ComponentType.PROJECT, "Expected root component of type 'PROJECT'");
-
- LinkedList<ScannerReport.Component> queue = new LinkedList<>();
- rootComponent.getChildRefList().stream()
- .map(scannerComponentSupplier)
- .forEach(queue::addLast);
-
- Node root = new Node();
- root.reportComponent = rootComponent;
-
- while (!queue.isEmpty()) {
- ScannerReport.Component component = queue.removeFirst();
- checkArgument(component.getType() == FILE, "Unsupported component type '%s'", component.getType());
- addFile(root, component);
- }
- return root;
- }
-
- private static void addFile(Node root, ScannerReport.Component file) {
- checkArgument(!StringUtils.isEmpty(file.getProjectRelativePath()), "Files should have a project relative path: " + file);
- String[] split = StringUtils.split(file.getProjectRelativePath(), '/');
- Node currentNode = root;
-
- for (int i = 0; i < split.length; i++) {
- currentNode = currentNode.children().computeIfAbsent(split[i], k -> new Node());
- }
- currentNode.reportComponent = file;
- }
-
- private Component buildComponent(Node node, String currentPath, String parentPath) {
- List<Component> childComponents = buildChildren(node, currentPath);
- ScannerReport.Component component = node.reportComponent();
-
- if (component != null) {
- if (component.getType() == FILE) {
- return buildFile(component);
- } else if (component.getType() == ScannerReport.Component.ComponentType.PROJECT) {
- return buildProject(childComponents);
- }
- }
-
- return buildDirectory(parentPath, currentPath, childComponents);
- }
-
- private List<Component> buildChildren(Node node, String currentPath) {
- List<Component> children = new ArrayList<>();
-
- for (Map.Entry<String, Node> e : node.children().entrySet()) {
- String path = buildPath(currentPath, e.getKey());
- Node childNode = e.getValue();
-
- // collapse folders that only contain one folder
- while (childNode.children().size() == 1 && childNode.children().values().iterator().next().children().size() > 0) {
- Map.Entry<String, Node> childEntry = childNode.children().entrySet().iterator().next();
- path = buildPath(path, childEntry.getKey());
- childNode = childEntry.getValue();
- }
- children.add(buildComponent(childNode, path, currentPath));
- }
- return children;
- }
-
- private static String buildPath(String currentPath, String file) {
- if (currentPath.isEmpty()) {
- return file;
- }
- return currentPath + "/" + file;
- }
-
- private Component buildProject(List<Component> children) {
- String projectKey = keyGenerator.generateKey(rootComponent.getKey(), null);
- String uuid = uuidSupplier.apply(projectKey);
- ComponentImpl.Builder builder = ComponentImpl.builder(Component.Type.PROJECT)
- .setUuid(uuid)
- .setKey(projectKey)
- .setStatus(convertStatus(rootComponent.getStatus()))
- .setProjectAttributes(projectAttributes)
- .setReportAttributes(createAttributesBuilder(rootComponent.getRef(), rootComponent.getProjectRelativePath(), scmBasePath).build())
- .addChildren(children);
- setNameAndDescription(rootComponent, builder);
- return builder.build();
- }
-
- private ComponentImpl buildFile(ScannerReport.Component component) {
- String key = keyGenerator.generateKey(rootComponent.getKey(), component.getProjectRelativePath());
- return ComponentImpl.builder(Component.Type.FILE)
- .setUuid(uuidSupplier.apply(key))
- .setKey(key)
- .setName(component.getProjectRelativePath())
- .setShortName(FilenameUtils.getName(component.getProjectRelativePath()))
- .setStatus(convertStatus(component.getStatus()))
- .setDescription(trimToNull(component.getDescription()))
- .setReportAttributes(createAttributesBuilder(component.getRef(), component.getProjectRelativePath(), scmBasePath).build())
- .setFileAttributes(createFileAttributes(component))
- .build();
- }
-
- private ComponentImpl buildDirectory(String parentPath, String path, List<Component> children) {
- String key = keyGenerator.generateKey(rootComponent.getKey(), path);
- return ComponentImpl.builder(Component.Type.DIRECTORY)
- .setUuid(uuidSupplier.apply(key))
- .setKey(key)
- .setName(path)
- .setShortName(removeStart(removeStart(path, parentPath), "/"))
- .setStatus(convertStatus(FileStatus.UNAVAILABLE))
- .setReportAttributes(createAttributesBuilder(null, path, scmBasePath).build())
- .addChildren(children)
- .build();
- }
-
- public Component buildChangedComponentTreeRoot(Component project) {
- return buildChangedComponentTree(project);
- }
-
- @Nullable
- private static Component buildChangedComponentTree(Component component) {
- switch (component.getType()) {
- case PROJECT:
- return buildChangedProject(component);
- case DIRECTORY:
- return buildChangedDirectory(component);
- case FILE:
- return buildChangedFile(component);
- default:
- throw new IllegalArgumentException(format("Unsupported component type '%s'", component.getType()));
- }
- }
-
- private static Component buildChangedProject(Component component) {
- return changedComponentBuilder(component, "")
- .setProjectAttributes(new ProjectAttributes(component.getProjectAttributes()))
- .addChildren(buildChangedComponentChildren(component))
- .build();
- }
-
- @Nullable
- private static Component buildChangedDirectory(Component component) {
- List<Component> children = buildChangedComponentChildren(component);
- if (children.isEmpty()) {
- return null;
- }
-
- if (children.size() == 1 && children.get(0).getType() == Component.Type.DIRECTORY) {
- Component child = children.get(0);
- String shortName = component.getShortName() + "/" + child.getShortName();
- return changedComponentBuilder(child, shortName)
- .addChildren(child.getChildren())
- .build();
- } else {
- return changedComponentBuilder(component, component.getShortName())
- .addChildren(children)
- .build();
- }
- }
-
- private static List<Component> buildChangedComponentChildren(Component component) {
- return component.getChildren().stream()
- .map(ComponentTreeBuilder::buildChangedComponentTree)
- .filter(Objects::nonNull)
- .toList();
- }
-
- private static ComponentImpl.Builder changedComponentBuilder(Component component, String newShortName) {
- return ComponentImpl.builder(component.getType())
- .setUuid(component.getUuid())
- .setKey(component.getKey())
- .setStatus(component.getStatus())
- .setReportAttributes(component.getReportAttributes())
- .setName(component.getName())
- .setShortName(newShortName)
- .setDescription(component.getDescription());
- }
-
- @Nullable
- private static Component buildChangedFile(Component component) {
- if (component.getStatus() == Component.Status.SAME) {
- return null;
- }
- return component;
- }
-
- private void setNameAndDescription(ScannerReport.Component component, ComponentImpl.Builder builder) {
- if (branch.isMain()) {
- builder
- .setName(nameOfProject(component))
- .setDescription(component.getDescription());
- } else {
- builder
- .setName(project.getName())
- .setDescription(project.getDescription());
- }
- }
-
- private static Component.Status convertStatus(FileStatus status) {
- switch (status) {
- case ADDED:
- return Component.Status.ADDED;
- case SAME:
- return Component.Status.SAME;
- case CHANGED:
- return Component.Status.CHANGED;
- case UNAVAILABLE:
- return Component.Status.UNAVAILABLE;
- case UNRECOGNIZED:
- default:
- throw new IllegalArgumentException("Unsupported ComponentType value " + status);
- }
- }
-
- private String nameOfProject(ScannerReport.Component component) {
- String name = trimToNull(component.getName());
- if (name != null) {
- return name;
- }
- return project.getName();
- }
-
- private static ReportAttributes.Builder createAttributesBuilder(@Nullable Integer ref, String path, @Nullable String scmBasePath) {
- return ReportAttributes.newBuilder(ref)
- .setScmPath(computeScmPath(scmBasePath, path));
- }
-
- @CheckForNull
- private static String computeScmPath(@Nullable String scmBasePath, String scmRelativePath) {
- if (scmRelativePath.isEmpty()) {
- return scmBasePath;
- }
- if (scmBasePath == null) {
- return scmRelativePath;
- }
-
- return scmBasePath + '/' + scmRelativePath;
- }
-
- private static FileAttributes createFileAttributes(ScannerReport.Component component) {
- checkArgument(component.getType() == FILE);
- checkArgument(component.getLines() > 0, "File '%s' has no line", component.getProjectRelativePath());
- String lang = trimToNull(component.getLanguage());
- return new FileAttributes(
- component.getIsTest(),
- lang != null ? lang.intern() : null,
- component.getLines(),
- component.getMarkedAsUnchanged(),
- component.getOldRelativeFilePath()
- );
- }
-
- private static class Node {
- private final Map<String, Node> children = new LinkedHashMap<>();
- private ScannerReport.Component reportComponent = null;
-
- private Map<String, Node> children() {
- return children;
- }
-
- @CheckForNull
- private ScannerReport.Component reportComponent() {
- return reportComponent;
- }
- }
- }
|