Skip to content

Commit

Permalink
Add support for maven optionality, fixes CycloneDX#314
Browse files Browse the repository at this point in the history
Signed-off-by: Kevin Conner <kev.conner@gmail.com>
  • Loading branch information
knrc committed May 6, 2023
1 parent b32e94d commit b110b52
Show file tree
Hide file tree
Showing 16 changed files with 402 additions and 11 deletions.
35 changes: 33 additions & 2 deletions src/main/java/org/cyclonedx/maven/BaseCycloneDxMojo.java
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,16 @@ public abstract class BaseCycloneDxMojo extends AbstractMojo {
@Parameter(property = "excludeTypes", required = false)
private String[] excludeTypes;

/**
* Define component scope (REQUIRED/OPTIONAL) using the maven artifact optionality, determined during dependency analysis.
*
* Note: The default mechanism uses bytecode analysis to determine component scope.
*
* @since 2.7.9
*/
@Parameter(property = "useMavenOptionality", defaultValue = "false")
protected boolean useMavenOptionality;

@org.apache.maven.plugins.annotations.Component(hint = "default")
private RepositorySystem aetherRepositorySystem;

Expand Down Expand Up @@ -280,6 +290,8 @@ public void execute() throws MojoExecutionException {
if (includeSystemScope) scopes.add("system");
if (includeTestScope) scopes.add("test");
metadata.addProperty(newProperty("maven.scopes", String.join(",", scopes)));

metadata.addProperty(newProperty("maven.optionality", Boolean.toString(useMavenOptionality)));
}

final Component rootComponent = metadata.getComponent();
Expand Down Expand Up @@ -426,7 +438,7 @@ protected void populateComponents(final Set<String> topLevelComponents, final Ma
for (Map.Entry<String, Artifact> entry: artifacts.entrySet()) {
final String purl = entry.getKey();
final Artifact artifact = entry.getValue();
final Component.Scope artifactScope = (dependencyAnalysis != null ? inferComponentScope(artifact, dependencyAnalysis) : null);
final Component.Scope artifactScope = getComponentScope(artifact, dependencyAnalysis);
final Component component = components.get(purl);
if (component == null) {
final Component newComponent = convert(artifact);
Expand All @@ -438,6 +450,25 @@ protected void populateComponents(final Set<String> topLevelComponents, final Ma
}
}

/**
* Get the BOM component scope (required/optional/excluded). The scope can either be determined through bytecode
* analysis or through maven dependency resolution.
*
* @param artifact Artifact from maven project
* @param projectDependencyAnalysis Maven Project Dependency Analysis data
*
* @return Component.Scope - REQUIRED, OPTIONAL or null if it cannot be determined
*
* @see useMavenOptionality
*/
private Component.Scope getComponentScope(Artifact artifact, ProjectDependencyAnalysis projectDependencyAnalysis) {
if (useMavenOptionality) {
return (artifact.isOptional() ? Component.Scope.OPTIONAL : Component.Scope.REQUIRED);
} else {
return inferComponentScope(artifact, projectDependencyAnalysis);
}
}

/**
* Infer BOM component scope (required/optional/excluded) based on Maven project dependency analysis.
*
Expand All @@ -446,7 +477,7 @@ protected void populateComponents(final Set<String> topLevelComponents, final Ma
*
* @return Component.Scope - REQUIRED: If the component is used (as detected by project dependency analysis). OPTIONAL: If it is unused
*/
protected Component.Scope inferComponentScope(Artifact artifact, ProjectDependencyAnalysis projectDependencyAnalysis) {
private Component.Scope inferComponentScope(Artifact artifact, ProjectDependencyAnalysis projectDependencyAnalysis) {
if (projectDependencyAnalysis == null) {
return null;
}
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/org/cyclonedx/maven/CycloneDxMojo.java
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ private ProjectDependencyAnalyzer getProjectDependencyAnalyzer() throws MojoExec
}

protected ProjectDependencyAnalysis doProjectDependencyAnalysis(final MavenProject mavenProject, final BomDependencies bomDependencies) throws MojoExecutionException {
if (useMavenOptionality) {
return null;
}
final MavenProject localMavenProject = new MavenProject(mavenProject);
localMavenProject.setArtifacts(new LinkedHashSet<>(bomDependencies.getArtifacts().values()));
localMavenProject.setDependencyArtifacts(new LinkedHashSet<>(bomDependencies.getDependencyArtifacts().values()));
Expand Down
16 changes: 13 additions & 3 deletions src/test/java/org/cyclonedx/maven/BaseMavenVerifier.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Map;
import java.util.Properties;
import org.junit.Rule;

Expand Down Expand Up @@ -55,10 +56,14 @@ protected File cleanAndBuild(final String project, final String[] excludeTypes)
}

protected File cleanAndBuild(final String project, final String[] excludeTypes, final String[] profiles) throws Exception {
return mvnBuild(project, null, excludeTypes, null);
return mvnBuild(project, null, excludeTypes, profiles, null);
}

protected File mvnBuild(final String project, final String[] goals, final String[] excludeTypes, final String[] profiles) throws Exception {
protected File cleanAndBuild(final String project, final Map<String, String> properties, final String[] excludeTypes) throws Exception {
return mvnBuild(project, null, excludeTypes, null, properties);
}

protected File mvnBuild(final String project, final String[] goals, final String[] excludeTypes, final String[] profiles, final Map<String, String> properties) throws Exception {
File projDir = resources.getBasedir(project);

MavenExecution execution = verifier
Expand All @@ -70,7 +75,12 @@ protected File mvnBuild(final String project, final String[] goals, final String
execution = execution.withCliOption("-DexcludeTypes=" + String.join(",", excludeTypes));
}
if ((profiles != null) && (profiles.length > 0)) {
execution = execution.withCliOption("-P" + String.join(",", profiles));
execution.withCliOption("-P" + String.join(",", profiles));
}
if (properties != null) {
for (Map.Entry<String, String> entry: properties.entrySet()) {
execution.withCliOption("-D" + entry.getKey() + "=" + entry.getValue());
}
}
if (goals != null && goals.length > 0) {
execution.execute(goals).assertErrorFreeLog();
Expand Down
2 changes: 1 addition & 1 deletion src/test/java/org/cyclonedx/maven/CyclicTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public void testCyclicDependency() throws Exception {
cleanAndBuild("cyclic", null);
File projDir = null;
try {
projDir = mvnBuild("cyclic", new String[]{"package"}, null, new String[] {"profile"});
projDir = mvnBuild("cyclic", new String[]{"package"}, null, new String[] {"profile"}, null);
} catch (final Exception ex) {
fail("Failed to generate SBOM", ex);
}
Expand Down
2 changes: 1 addition & 1 deletion src/test/java/org/cyclonedx/maven/Issue311Test.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public Issue311Test(MavenRuntimeBuilder runtimeBuilder) throws Exception {

@Test
public void testLatestAndRelease() throws Exception {
final File projDir = mvnBuild("issue-311", new String[]{"clean", "install"}, null, null);
final File projDir = mvnBuild("issue-311", new String[]{"clean", "install"}, null, null, null);

checkLatest(projDir);
checkRelease(projDir);
Expand Down
112 changes: 112 additions & 0 deletions src/test/java/org/cyclonedx/maven/Issue314Test.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package org.cyclonedx.maven;

import java.io.File;
import java.util.HashMap;
import java.util.Map;

import static org.cyclonedx.maven.TestUtils.getComponentNode;
import static org.cyclonedx.maven.TestUtils.getElement;
import static org.cyclonedx.maven.TestUtils.readXML;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

import org.cyclonedx.model.Component;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import io.takari.maven.testing.executor.MavenRuntime.MavenRuntimeBuilder;
import io.takari.maven.testing.executor.MavenVersions;
import io.takari.maven.testing.executor.junit.MavenJUnitTestRunner;

/**
* Fix BOM handling of conflicting dependency tree graphs
*/
@RunWith(MavenJUnitTestRunner.class)
@MavenVersions({"3.6.3"})
public class Issue314Test extends BaseMavenVerifier {

private static final String ISSUE_314_DEPENDENCY_B = "pkg:maven/com.example.issue_314/dependency_B@1.0.0?type=jar";
private static final String ISSUE_314_DEPENDENCY_C = "pkg:maven/com.example.issue_314/dependency_C@1.0.0?type=jar";
private static final String ISSUE_314_DEPENDENCY_D = "pkg:maven/com.example.issue_314/dependency_D@1.0.0?type=jar";

public Issue314Test(MavenRuntimeBuilder runtimeBuilder) throws Exception {
super(runtimeBuilder);
}

/**
* Validate the bytecode analysis components.
* - No component should be marked as optional
*/
@Test
public void testBytecodeDependencyTree() throws Exception {
final Map<String, String> properties = new HashMap<>();
properties.put("useMavenOptionality", "false");
final File projDir = mvnBuild("issue-314", null, null, null, properties);

final String requiredName = Component.Scope.REQUIRED.getScopeName();

final Document bom = readXML(new File(projDir, "dependency_A/target/bom.xml"));

final NodeList componentsList = bom.getElementsByTagName("components");
assertEquals("Expected a single components element", 1, componentsList.getLength());
final Element components = (Element)componentsList.item(0);

final Element componentBNode = getComponentNode(components, ISSUE_314_DEPENDENCY_B);
final Element componentBScope = getElement(componentBNode, "scope");
if (componentBScope != null) {
assertEquals("dependency_B scope should be " + requiredName, requiredName, componentBScope.getTextContent());
}

final Element componentCNode = getComponentNode(components, ISSUE_314_DEPENDENCY_C);
final Element componentCScope = getElement(componentCNode, "scope");
if (componentCScope != null) {
assertEquals("dependency_C scope should be " + requiredName, requiredName, componentCScope.getTextContent());
}

final Element componentDNode = getComponentNode(components, ISSUE_314_DEPENDENCY_D);
final Element componentDScope = getElement(componentDNode, "scope");
if (componentDScope != null) {
assertEquals("dependency_D scope should be " + requiredName, requiredName, componentDScope.getTextContent());
}
}

/**
* Validate the bytecode analysis components.
* - com.example.issue_314:dependency_C:1.0.0 and com.example.issue_314:dependency_D:1.0.0 *should* be marked as optional
*/
@Test
public void testMavenOptionalityDependencyTree() throws Exception {
final Map<String, String> properties = new HashMap<>();
properties.put("useMavenOptionality", "true");
final File projDir = mvnBuild("issue-314", null, null, null, properties);

final String requiredName = Component.Scope.REQUIRED.getScopeName();
final String optionalName = Component.Scope.OPTIONAL.getScopeName();

final Document bom = readXML(new File(projDir, "dependency_A/target/bom.xml"));

final NodeList componentsList = bom.getElementsByTagName("components");
assertEquals("Expected a single components element", 1, componentsList.getLength());
final Element components = (Element)componentsList.item(0);

final Element componentBNode = getComponentNode(components, ISSUE_314_DEPENDENCY_B);
final Element componentBScope = getElement(componentBNode, "scope");
if (componentBScope != null) {
assertEquals("dependency_B scope should be " + requiredName, requiredName, componentBScope.getTextContent());
}

final Element componentCNode = getComponentNode(components, ISSUE_314_DEPENDENCY_C);
final Element componentCScope = getElement(componentCNode, "scope");
assertNotNull("dependency_C is missing its scope", componentCScope);
assertEquals("dependency_C scope should be " + optionalName, optionalName, componentCScope.getTextContent());

final Element componentDNode = getComponentNode(components, ISSUE_314_DEPENDENCY_D);
final Element componentDScope = getElement(componentDNode, "scope");
assertNotNull("dependency_D is missing its scope", componentDScope);
assertEquals("dependency_D scope should be " + optionalName, optionalName, componentDScope.getTextContent());
}
}
26 changes: 22 additions & 4 deletions src/test/java/org/cyclonedx/maven/TestUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,46 @@
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

class TestUtils {
static Node getDependencyNode(final Node dependencies, final String ref) {
static Element getElement(final Element parent, final String elementName) throws Exception {
Element element = null;
Node child = parent.getFirstChild();
while (child != null) {
if (Node.ELEMENT_NODE == child.getNodeType()) {
if (child.getNodeName().equals(elementName)) {
if (element != null) {
throw new Exception("Second instance of element " + elementName + " discovered in " + parent.getNodeName());
}
element = (Element)child;
}
}
child = child.getNextSibling();
}
return element;
}

static Element getDependencyNode(final Node dependencies, final String ref) {
return getChildElement(dependencies, ref, "dependency", "ref");
}

static Node getComponentNode(final Node components, final String ref) {
static Element getComponentNode(final Node components, final String ref) {
return getChildElement(components, ref, "component", "bom-ref");
}

private static Node getChildElement(final Node parent, final String ref, final String elementName, final String attrName) {
private static Element getChildElement(final Node parent, final String ref, final String elementName, final String attrName) {
final NodeList children = parent.getChildNodes();
final int numChildNodes = children.getLength();
for (int index = 0 ; index < numChildNodes ; index++) {
final Node child = children.item(index);
if ((child.getNodeType() == Node.ELEMENT_NODE) && elementName.equals(child.getNodeName())) {
final Node refNode = child.getAttributes().getNamedItem(attrName);
if (ref.equals(refNode.getNodeValue())) {
return child;
return (Element)child;
}
}
}
Expand Down
68 changes: 68 additions & 0 deletions src/test/resources/issue-314/dependency_A/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>com.example.issue_314</groupId>
<artifactId>issue_314_parent</artifactId>
<version>1.0.0</version>
</parent>

<artifactId>dependency_A</artifactId>

<name>Dependency A</name>

<properties>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>1.8</maven.compiler.source>
</properties>

<dependencies>
<dependency>
<groupId>com.example.issue_314</groupId>
<artifactId>dependency_B</artifactId>
<version>1.0.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.example.issue_314</groupId>
<artifactId>dependency_C</artifactId>
<version>1.0.0</version>
<scope>provided</scope>
<optional>true</optional>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.cyclonedx</groupId>
<artifactId>cyclonedx-maven-plugin</artifactId>
<version>${current.version}</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>makeBom</goal>
</goals>
</execution>
</executions>
<configuration>
<projectType>library</projectType>
<schemaVersion>1.4</schemaVersion>
<includeBomSerialNumber>true</includeBomSerialNumber>
<includeCompileScope>true</includeCompileScope>
<includeProvidedScope>true</includeProvidedScope>
<includeRuntimeScope>false</includeRuntimeScope>
<includeSystemScope>false</includeSystemScope>
<includeTestScope>false</includeTestScope>
<includeLicenseText>false</includeLicenseText>
<outputFormat>xml</outputFormat>
</configuration>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.example.issue_314.liba;

import com.example.issue_314.libb.LibB;
import com.example.issue_314.libc.LibC;

public class LibA {
private LibA() {}

public static void main(final String[] args) {
System.out.println("In libA");
LibB.libBMethod();
try {
LibC.libCMethod();
} catch (final NoClassDefFoundError ncdfe) {
System.out.println("Optional library libC not present on classpath");
}
}
}

0 comments on commit b110b52

Please sign in to comment.