Skip to content

Commit

Permalink
feat: add small fixes, javadoc, logging and nullables.
Browse files Browse the repository at this point in the history
Signed-off-by: Gordon <[email protected]>
  • Loading branch information
gordonrousselle committed Nov 5, 2024
1 parent 4f0b179 commit e2a2290
Show file tree
Hide file tree
Showing 10 changed files with 160 additions and 47 deletions.
3 changes: 3 additions & 0 deletions src/main/java/org/cyclonedx/gradle/CycloneDxPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
import org.gradle.api.Project;
import org.gradle.api.provider.Provider;

/**
* Entrypoint of the plugin which simply configures one task
*/
public class CycloneDxPlugin implements Plugin<Project> {

public void apply(final Project project) {
Expand Down
22 changes: 18 additions & 4 deletions src/main/java/org/cyclonedx/gradle/CycloneDxTask.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
import javax.annotation.Nullable;
import org.cyclonedx.gradle.model.SbomGraph;
import org.cyclonedx.gradle.utils.CycloneDxUtils;
import org.cyclonedx.model.Bom;
Expand All @@ -36,8 +37,13 @@
import org.gradle.api.tasks.OutputDirectory;
import org.gradle.api.tasks.TaskAction;

/**
* This task mainly acts a container for the user configurations (includeConfigs, projectType, schemaVersion, ...)
* and orchestrating the calls between the core objects (SbomGraphProvider and SbomBuilder)
*/
public abstract class CycloneDxTask extends DefaultTask {

private static final String MESSAGE_WRITING_BOM_OUTPUT = "CycloneDX: Writing BOM output";
private static final String DEFAULT_PROJECT_TYPE = "library";

private final Property<String> outputName;
Expand All @@ -53,8 +59,10 @@ public abstract class CycloneDxTask extends DefaultTask {
private final Property<String> projectType;
private final ListProperty<String> skipProjects;
private final Property<File> destination;
private OrganizationalEntity organizationalEntity;
private LicenseChoice licenseChoice;

@Nullable private OrganizationalEntity organizationalEntity;

@Nullable private LicenseChoice licenseChoice;

public CycloneDxTask() {

Expand Down Expand Up @@ -211,12 +219,12 @@ public void setSkipProjects(final Collection<String> skipProjects) {
}

@Internal
OrganizationalEntity getOrganizationalEntity() {
@Nullable OrganizationalEntity getOrganizationalEntity() {
return organizationalEntity;
}

@Internal
LicenseChoice getLicenseChoice() {
@Nullable LicenseChoice getLicenseChoice() {
return licenseChoice;
}

Expand All @@ -232,13 +240,19 @@ public void setDestination(final File destination) {
this.destination.set(destination);
}

/**
* Executes the main logic of the plugin by loading the dependency graph (SbomGraphProvider.get())
* and providing the result to SbomBuilder
*/
@TaskAction
public void createBom() {

final SbomBuilder builder = new SbomBuilder(getLogger(), this);
final SbomGraph components = getComponents().get();
final Bom bom = builder.buildBom(components.getGraph(), components.getRootComponent());

getLogger().info(MESSAGE_WRITING_BOM_OUTPUT);

CycloneDxUtils.writeBom(
bom,
getDestination().get(),
Expand Down
29 changes: 26 additions & 3 deletions src/main/java/org/cyclonedx/gradle/DependencyGraphTraverser.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import java.io.File;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
Expand All @@ -43,6 +44,10 @@
import org.gradle.api.artifacts.result.ResolvedDependencyResult;
import org.gradle.api.logging.Logger;

/**
* Traverses the dependency graph of a configuration which returns a data model that 1) contains all the information
* required to generate the CycloneDX Bom and 2) is fully serializable to support the build cache
*/
class DependencyGraphTraverser {

private final Logger logger;
Expand All @@ -63,6 +68,16 @@ public DependencyGraphTraverser(
this.mavenHelper = new MavenHelper(logger, task.getIncludeLicenseText().get());
}

/**
* Traverses the dependency graph of a configuration belonging to the specified project
*
* @param rootNode entry point into the graph which is typically represents a project
* @param projectName project to which the configuration belongs to
* @param configName name of the configuration
*
* @return a graph represented as map which is fully serializable. The graph nodes are instances of
* SbomComponent which contain the necessary information to generate the Bom
*/
Map<SbomComponentId, SbomComponent> traverseGraph(
final ResolvedComponentResult rootNode, final String projectName, final String configName) {

Expand All @@ -73,14 +88,20 @@ Map<SbomComponentId, SbomComponent> traverseGraph(
rootGraphNode.inScopeConfiguration(projectName, configName);
queue.add(rootGraphNode);

logger.debug("CycloneDX: Traversal of graph for configuration {} of project {}", configName, projectName);
while (!queue.isEmpty()) {
final GraphNode graphNode = queue.poll();
if (!graph.containsKey(graphNode)) {
graph.put(graphNode, new HashSet<>());
for (DependencyResult dep : graphNode.getResult().getDependencies()) {
logger.debug("CycloneDX: Traversing node with ID {}", graphNode.id);
for (final DependencyResult dep : graphNode.getResult().getDependencies()) {
if (dep instanceof ResolvedDependencyResult) {
final ResolvedComponentResult dependencyComponent =
((ResolvedDependencyResult) dep).getSelected();
logger.debug(
"CycloneDX: Node with ID {} has dependency with ID {}",
graphNode.id,
dependencyComponent);
final GraphNode dependencyNode = new GraphNode(dependencyComponent);
dependencyNode.inScopeConfiguration(projectName, configName);
graph.get(graphNode).add(dependencyNode);
Expand All @@ -107,6 +128,7 @@ private SbomComponent toSbomComponent(final GraphNode node, final Set<GraphNode>
List<License> licenses = null;
SbomMetaData metaData = null;
if (includeMetaData && node.id instanceof ModuleComponentIdentifier) {
logger.debug("CycloneDX: Including meta data for node {}", node.id);
final Component component = new Component();
extractMetaDataFromArtifactPom(artifactFile, component, node.getResult());
licenses = extractMetaDataFromRepository(component, node.getResult());
Expand All @@ -126,12 +148,13 @@ private SbomComponent toSbomComponent(final GraphNode node, final Set<GraphNode>
private void extractMetaDataFromArtifactPom(
final File artifactFile, final Component component, final ResolvedComponentResult result) {

if (artifactFile == null) {
if (artifactFile == null || result.getModuleVersion() == null) {
return;
}

final MavenProject mavenProject = mavenHelper.extractPom(artifactFile, result.getModuleVersion());
if (mavenProject != null) {
logger.debug("CycloneDX: parse artifact pom file of component {}", result.getId());
mavenHelper.getClosestMetadata(artifactFile, mavenProject, component, result.getModuleVersion());
}
}
Expand All @@ -144,7 +167,7 @@ private List<License> extractMetaDataFromRepository(
return mavenProject.getLicenses();
}

return null;
return new ArrayList<>();
}

private Set<SbomComponentId> getSbomDependencies(final Set<GraphNode> dependencyNodes) {
Expand Down
18 changes: 16 additions & 2 deletions src/main/java/org/cyclonedx/gradle/MavenProjectLookup.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import javax.annotation.Nullable;
import org.apache.maven.model.Model;
import org.apache.maven.project.MavenProject;
import org.gradle.api.Project;
Expand All @@ -34,6 +35,9 @@
import org.gradle.maven.MavenModule;
import org.gradle.maven.MavenPomArtifact;

/**
* Finds the pom.xml of a maven project in the gradle repositories and, if exists, instantiates a MavenProject object
*/
class MavenProjectLookup {

private final Project project;
Expand All @@ -44,7 +48,15 @@ class MavenProjectLookup {
this.cache = new HashMap<>();
}

MavenProject getResolvedMavenProject(final ResolvedComponentResult result) {
/**
* Retrieve the MavenProject instance for the provided component
*
* @param result the resolved component for which to find the maven project,
* or null if the pom.xml is not found
*
* @return a MavenProject instance for this component
*/
@Nullable MavenProject getResolvedMavenProject(final ResolvedComponentResult result) {

if (result == null) {
return null;
Expand All @@ -58,6 +70,7 @@ MavenProject getResolvedMavenProject(final ResolvedComponentResult result) {
final File pomFile = buildMavenProject(result.getId());
final MavenProject mavenProject = MavenHelper.readPom(pomFile);
if (mavenProject != null) {
project.getLogger().debug("CycloneDX: parse queried pom file for component {}", result.getId());
final Model model = MavenHelper.resolveEffectivePom(pomFile, project);
if (model != null) {
mavenProject.setLicenses(model.getLicenses());
Expand All @@ -72,7 +85,7 @@ MavenProject getResolvedMavenProject(final ResolvedComponentResult result) {
return null;
}

private File buildMavenProject(final ComponentIdentifier id) {
@Nullable File buildMavenProject(final ComponentIdentifier id) {

final ArtifactResolutionResult result = project.getDependencies()
.createArtifactResolutionQuery()
Expand All @@ -94,6 +107,7 @@ private File buildMavenProject(final ComponentIdentifier id) {

final ArtifactResult artifact = artifactIt.next();
if (artifact instanceof ResolvedArtifactResult) {
project.getLogger().debug("CycloneDX: found pom file for component {}", id);
final ResolvedArtifactResult resolvedArtifact = (ResolvedArtifactResult) artifact;
return resolvedArtifact.getFile();
}
Expand Down
38 changes: 28 additions & 10 deletions src/main/java/org/cyclonedx/gradle/SbomBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@
import com.networknt.schema.utils.StringUtils;
import java.io.File;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -51,9 +51,15 @@
import org.cyclonedx.util.BomUtils;
import org.gradle.api.logging.Logger;

/**
* Generates the CycloneDX Bom from the aggregated dependency graph taking into account the provided
* user configuration (componentName, includeBomSerialNumber,...)
*/
class SbomBuilder {

private static final String MESSAGE_CALCULATING_HASHES = "CycloneDX: Calculating Hashes";
private static final String MESSAGE_CREATING_BOM = "CycloneDX: Creating BOM";

private static final TreeMap<String, String> EMPTY_TYPE = new TreeMap<>();

private final Logger logger;
Expand All @@ -70,8 +76,18 @@ class SbomBuilder {
this.task = task;
}

/**
* Builds the CycloneDX Bom from the aggregated dependency graph
*
* @param resultGraph the aggregated dependency graph across all the configurations
* @param rootComponent the root component of the graph which is the parent project
*
* @return the CycloneDX Bom
*/
Bom buildBom(final Map<SbomComponentId, SbomComponent> resultGraph, final SbomComponent rootComponent) {

task.getLogger().info(MESSAGE_CREATING_BOM);

final Set<Dependency> dependencies = new TreeSet<>(new DependencyComparator());
final Set<Component> components = new TreeSet<>(new ComponentComparator());

Expand Down Expand Up @@ -183,15 +199,17 @@ private Component toComponent(final SbomComponent component, final File artifact
}

private List<Property> buildProperties(final SbomComponent component) {
return component.getInScopeConfigurations().stream()
.map(v -> {
Property property = new Property();
property.setName("inScopeConfiguration");
property.setValue(String.format("%s:%s", v.getProjectName(), v.getConfigName()));
return property;
})
.sorted(Comparator.comparing(Property::getValue))
.collect(Collectors.toList());

final String value = component.getInScopeConfigurations().stream()
.map(v -> String.format(
"%s:%s", URLEncoder.encode(v.getProjectName()), URLEncoder.encode(v.getConfigName())))
.collect(Collectors.joining(","));

final Property property = new Property();
property.setName("inScopeConfiguration");
property.setValue(value);

return Collections.singletonList(property);
}

private List<Hash> calculateHashes(final File artifactFile) {
Expand Down
26 changes: 23 additions & 3 deletions src/main/java/org/cyclonedx/gradle/SbomGraphProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,16 @@
import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.component.ComponentIdentifier;
import org.gradle.api.artifacts.result.ResolvedArtifactResult;

/**
* Provider that lazily calculates the aggregated dependency graph. The usage of a provider is essential to support
* configuration cache and also to ensure that all dependencies have been resolved when the CycloneDxTask is executed.
*/
public class SbomGraphProvider implements Callable<SbomGraph> {

private static final String MESSAGE_RESOLVING_DEPS = "CycloneDX: Resolving Dependencies";

private final Project project;
private final CycloneDxTask task;

Expand All @@ -44,6 +51,15 @@ public SbomGraphProvider(final Project project, final CycloneDxTask task) {
this.task = task;
}

/**
* Calculates the aggregated dependency graph across all the configurations of both the parent project and
* child projects. The steps are as follows:
* 1) generate dependency graphs for the parent project, one for each configuration
* 2) if child projects exist, generate dependency graphs across all the child projects
* 3) merge all generated graphs from the step 1) and 2)
*
* @return the aggregated dependency graph
*/
@Override
public SbomGraph call() throws Exception {

Expand All @@ -53,11 +69,13 @@ public SbomGraph call() throws Exception {
throw new IllegalStateException("Project group and version are required for the CycloneDx task");
}

project.getLogger().info(MESSAGE_RESOLVING_DEPS);

final DependencyGraphTraverser traverser = new DependencyGraphTraverser(
project.getLogger(), getArtifacts(), new MavenProjectLookup(project), task);

final Map<SbomComponentId, SbomComponent> graph = Stream.concat(
traverseParentProject(traverser), traverseChildProjects(traverser))
traverseCurrentProject(traverser), traverseChildProjects(traverser))
.reduce(new HashMap<>(), DependencyUtils::mergeGraphs);

return buildSbomGraph(graph);
Expand All @@ -71,6 +89,7 @@ private SbomGraph buildSbomGraph(final Map<SbomComponentId, SbomComponent> graph
project, rootProject.get().getId(), graph);
return new SbomGraph(graph, rootProject.get());
} else {
project.getLogger().debug("CycloneDX: root project not found. Constructing it.");
final SbomComponentId rootProjectId = new SbomComponentId(
project.getGroup().toString(),
project.getName(),
Expand All @@ -86,7 +105,7 @@ private SbomGraph buildSbomGraph(final Map<SbomComponentId, SbomComponent> graph
}
}

private Stream<Map<SbomComponentId, SbomComponent>> traverseParentProject(
private Stream<Map<SbomComponentId, SbomComponent>> traverseCurrentProject(
final DependencyGraphTraverser traverser) {

if (shouldSkipProject(project)) {
Expand All @@ -103,6 +122,7 @@ private Stream<Map<SbomComponentId, SbomComponent>> traverseParentProject(
private Stream<Map<SbomComponentId, SbomComponent>> traverseChildProjects(
final DependencyGraphTraverser traverser) {
return project.getChildProjects().entrySet().stream()
.filter(project -> !shouldSkipProject(project.getValue()))
.flatMap(project -> project.getValue().getConfigurations().stream()
.filter(configuration -> shouldIncludeConfiguration(configuration)
&& !shouldSkipConfiguration(configuration)
Expand All @@ -123,7 +143,7 @@ private Map<ComponentIdentifier, File> getArtifacts() {
.flatMap(config -> config.getIncoming().getArtifacts().getArtifacts().stream())
.collect(Collectors.toMap(
artifact -> artifact.getId().getComponentIdentifier(),
artifact -> artifact.getFile(),
ResolvedArtifactResult::getFile,
(v1, v2) -> v1));
}

Expand Down
Loading

0 comments on commit e2a2290

Please sign in to comment.