Skip to content

Commit 9511597

Browse files
✨ feat: Include parent poms (recursively) in lockfile (#1444)
Co-authored-by: Adam Kaplan <adam.kaplan@redhat.com>
1 parent 7618508 commit 9511597

File tree

29 files changed

+690
-83
lines changed

29 files changed

+690
-83
lines changed

maven_plugin/src/main/java/io/github/chains_project/maven_lockfile/LockFileFacade.java

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.util.*;
2121
import java.util.stream.Collectors;
2222
import org.apache.maven.artifact.Artifact;
23+
import org.apache.maven.artifact.DefaultArtifact;
2324
import org.apache.maven.artifact.factory.ArtifactFactory;
2425
import org.apache.maven.execution.MavenSession;
2526
import org.apache.maven.project.DefaultProjectBuildingRequest;
@@ -108,7 +109,7 @@ public static LockFile generateLockFileFromProject(
108109
.filter(v -> v.getParent() == null)
109110
.collect(Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(
110111
io.github.chains_project.maven_lockfile.graph.DependencyNode::getComparatorString))));
111-
var pom = new Pom(project, checksumCalculator);
112+
var pom = constructRecursivePom(project, checksumCalculator);
112113
return new LockFile(
113114
GroupId.of(project.getGroupId()),
114115
ArtifactId.of(project.getArtifactId()),
@@ -338,4 +339,58 @@ private static DependencyGraph graph(
338339
return DependencyGraph.of(GraphBuilder.directed().build(), checksumCalculator, reduced);
339340
}
340341
}
342+
343+
/**
344+
* Construct a Pom object containing a full tree of its parent POM references. These parent
345+
* POMs may be relative to the project being built, or are specified from an external POM.
346+
*/
347+
private static Pom constructRecursivePom(
348+
MavenProject initialProject, AbstractChecksumCalculator checksumCalculator) {
349+
String checksumAlgorithm = checksumCalculator.getChecksumAlgorithm();
350+
351+
List<MavenProject> recursiveProjects = new ArrayList<>();
352+
recursiveProjects.add(initialProject);
353+
while (recursiveProjects.get(recursiveProjects.size() - 1).hasParent()) {
354+
recursiveProjects.add(
355+
recursiveProjects.get(recursiveProjects.size() - 1).getParent());
356+
}
357+
358+
Pom lastPom = null;
359+
Collections.reverse(recursiveProjects);
360+
for (MavenProject project : recursiveProjects) {
361+
String relativePath = project.getFile() == null
362+
? null
363+
: initialProject
364+
.getBasedir()
365+
.toPath()
366+
.relativize(project.getFile().toPath())
367+
.toString();
368+
String checksum = null;
369+
if (project.getFile() == null) {
370+
Artifact artifact = project.getArtifact();
371+
Artifact pomArtifact = new DefaultArtifact(
372+
artifact.getGroupId(),
373+
artifact.getArtifactId(),
374+
artifact.getVersion(),
375+
artifact.getScope(),
376+
"pom",
377+
artifact.getClassifier(),
378+
artifact.getArtifactHandler());
379+
checksum = checksumCalculator.calculateArtifactChecksum(pomArtifact);
380+
} else {
381+
checksum = checksumCalculator.calculatePomChecksum(
382+
project.getFile().toPath());
383+
}
384+
lastPom = new Pom(
385+
GroupId.of(project.getGroupId()),
386+
ArtifactId.of(project.getArtifactId()),
387+
VersionNumber.of(project.getVersion()),
388+
relativePath,
389+
checksumAlgorithm,
390+
checksum,
391+
lastPom);
392+
}
393+
394+
return lastPom;
395+
}
341396
}

maven_plugin/src/main/java/io/github/chains_project/maven_lockfile/ValidateChecksumMojo.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,11 @@ public void execute() throws MojoExecutionException {
6464
}
6565
}
6666
if (!Objects.equals(lockFileFromFile.getPom(), lockFileFromProject.getPom())) {
67-
String sb = "Pom checksum mismatch. Differences:" + "\n" + "Your lockfile pom path and checksum:\n"
68-
+ lockFileFromFile.getPom().getPath()
69-
+ " " + lockFileFromFile.getPom().getChecksum() + "\n" + "Your project pom path and checksum:\n"
70-
+ lockFileFromProject.getPom().getPath()
71-
+ " " + lockFileFromProject.getPom().getChecksum() + "\n";
67+
String sb = "Pom checksum mismatch. Differences:\nYour lockfile pom:\n"
68+
+ JsonUtils.toJson(lockFileFromFile.getPom())
69+
+ "\n" + "Your project pom:\n"
70+
+ JsonUtils.toJson(lockFileFromProject.getPom())
71+
+ "\n";
7272

7373
switch (config.getOnPomValidationFailure()) {
7474
case Warn:
Lines changed: 77 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,46 @@
11
package io.github.chains_project.maven_lockfile.data;
22

3-
import io.github.chains_project.maven_lockfile.checksum.AbstractChecksumCalculator;
4-
import org.apache.maven.project.MavenProject;
5-
63
public class Pom implements Comparable<Pom> {
74

8-
private final String path;
5+
private final GroupId groupId;
6+
private final ArtifactId artifactId;
7+
private final VersionNumber version;
8+
private final String relativePath;
99
private final String checksumAlgorithm;
1010
private final String checksum;
11+
private final Pom parent;
1112

12-
public Pom(MavenProject project, AbstractChecksumCalculator checksumCalculator) {
13-
this.path = project.getBasedir()
14-
.toPath()
15-
.relativize(project.getFile().toPath())
16-
.toString();
17-
this.checksumAlgorithm = checksumCalculator.getChecksumAlgorithm();
18-
this.checksum =
19-
checksumCalculator.calculatePomChecksum(project.getFile().toPath());
20-
}
21-
22-
public Pom(String path, String checksumAlgorithm, String checksum) {
23-
this.path = path;
13+
public Pom(
14+
GroupId groupId,
15+
ArtifactId artifactId,
16+
VersionNumber version,
17+
String relativePath,
18+
String checksumAlgorithm,
19+
String checksum,
20+
Pom parent) {
21+
this.groupId = groupId;
22+
this.artifactId = artifactId;
23+
this.version = version;
24+
this.relativePath = relativePath;
2425
this.checksumAlgorithm = checksumAlgorithm;
2526
this.checksum = checksum;
27+
this.parent = parent;
28+
}
29+
30+
public GroupId getGroupId() {
31+
return groupId;
32+
}
33+
34+
public ArtifactId getArtifactId() {
35+
return artifactId;
2636
}
2737

28-
public String getPath() {
29-
return path;
38+
public VersionNumber getVersion() {
39+
return version;
40+
}
41+
42+
public String getRelativePath() {
43+
return relativePath;
3044
}
3145

3246
public String getChecksumAlgorithm() {
@@ -37,10 +51,29 @@ public String getChecksum() {
3751
return checksum;
3852
}
3953

54+
public Pom getParent() {
55+
return parent;
56+
}
57+
4058
@Override
4159
public int compareTo(Pom o) {
42-
if (this.path.compareTo(o.path) != 0) {
43-
return this.path.compareTo(o.path);
60+
if (this.groupId.compareTo(o.groupId) != 0) {
61+
return this.groupId.compareTo(o.groupId);
62+
}
63+
64+
if (this.artifactId.compareTo(o.artifactId) != 0) {
65+
return this.artifactId.compareTo(o.artifactId);
66+
}
67+
68+
if (this.version.compareTo(o.version) != 0) {
69+
return this.version.compareTo(o.version);
70+
}
71+
72+
String pathCmp = this.relativePath == null ? "" : this.relativePath;
73+
String oPathCmp = o.relativePath == null ? "" : o.relativePath;
74+
75+
if (pathCmp.compareTo(oPathCmp) != 0) {
76+
return pathCmp.compareTo(oPathCmp);
4477
}
4578

4679
if (this.checksumAlgorithm.compareTo(o.checksumAlgorithm) != 0) {
@@ -51,6 +84,18 @@ public int compareTo(Pom o) {
5184
return this.checksum.compareTo(o.checksum);
5285
}
5386

87+
if (this.parent == null && o.parent != null) {
88+
return -1;
89+
}
90+
91+
if (this.parent != null && o.parent == null) {
92+
return 1;
93+
}
94+
95+
if (this.parent != null && o.parent != null && this.parent.compareTo(o.parent) != 0) {
96+
return this.parent.compareTo(o.parent);
97+
}
98+
5499
return 0;
55100
}
56101

@@ -63,8 +108,18 @@ public boolean equals(Object obj) {
63108
return false;
64109
}
65110
Pom other = (Pom) obj;
66-
return this.path.equals(other.path)
111+
String pathCmp = this.relativePath == null ? "" : this.relativePath;
112+
String otherPathCmp = other.relativePath == null ? "" : other.relativePath;
113+
114+
boolean parentEqual = (this.parent == null && other.parent == null)
115+
|| (this.parent != null && other.parent != null && this.parent.equals(other.parent));
116+
117+
return this.groupId.equals(other.groupId)
118+
&& this.artifactId.equals(other.artifactId)
119+
&& this.version.equals(other.version)
120+
&& pathCmp.equals(otherPathCmp)
67121
&& this.checksumAlgorithm.equals(other.checksumAlgorithm)
68-
&& this.checksum.equals(other.checksum);
122+
&& this.checksum.equals(other.checksum)
123+
&& parentEqual;
69124
}
70125
}

maven_plugin/src/test/java/io/github/chains_project/maven_lockfile/graph/LockfileTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ void shouldLockFilesEqualWhenOrderIsChanged() {
4242
groupId,
4343
artifactId,
4444
version,
45-
new Pom("pom.xml", "SHA-256", "POM-CHECKSUM"),
45+
new Pom(groupId, artifactId, version, "pom.xml", "SHA-256", "POM-CHECKSUM", null),
4646
Set.of(dependencyNodeA(dependencyNodeAChild1(), dependencyNodeAChild2()), dependencyNodeB()),
4747
Set.of(pluginA(), pluginB()),
4848
metadata);
@@ -51,7 +51,7 @@ void shouldLockFilesEqualWhenOrderIsChanged() {
5151
groupId,
5252
artifactId,
5353
version,
54-
new Pom("pom.xml", "SHA-256", "POM-CHECKSUM"),
54+
new Pom(groupId, artifactId, version, "pom.xml", "SHA-256", "POM-CHECKSUM", null),
5555
Set.of(dependencyNodeB(), dependencyNodeA(dependencyNodeAChild1(), dependencyNodeAChild2())),
5656
Set.of(pluginB(), pluginA()),
5757
metadata);

maven_plugin/src/test/java/it/IntegrationTestsIT.java

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,16 @@ public void pomCheckShouldFail(MavenExecutionResult result) throws Exception {
524524
assertThat(stdout.contains("Pom checksum mismatch.")).isTrue();
525525
}
526526

527+
@MavenTest
528+
public void pomParentCheckShouldFail(MavenExecutionResult result) throws Exception {
529+
// contract: if the pom checksum of a parent of the pom does not match is should fail with reason being pom
530+
// didn't match.
531+
assertThat(result).isFailure();
532+
String stdout = Files.readString(result.getMavenLog().getStdout());
533+
assertThat(stdout.contains("42a499ef30a02d54a826cdc21f289cf1eabfe561a7f0c5ca9e0ab7d9a5bb1a10_TAMPER_ATTACK"))
534+
.isTrue();
535+
}
536+
527537
@MavenTest
528538
public void environmentalCheckShouldFail(MavenExecutionResult result) throws Exception {
529539
// contract: if the pom checksum does not match is should fail with reason being pom didn't match.
@@ -532,4 +542,69 @@ public void environmentalCheckShouldFail(MavenExecutionResult result) throws Exc
532542
String stdout = Files.readString(result.getMavenLog().getStdout());
533543
assertThat(stdout.contains("Failed verifying environment.")).isTrue();
534544
}
545+
546+
@MavenTest
547+
public void externalParentPom(MavenExecutionResult result) throws Exception {
548+
// contract: a project with an external parent POM (e.g., Spring Boot) should generate
549+
// a lockfile containing the full parent pom hierarchy with checksums
550+
System.out.println("Running 'externalParentPom' integration test.");
551+
assertThat(result).isSuccessful();
552+
Path lockFilePath = findFile(result, "lockfile.json");
553+
assertThat(lockFilePath).exists();
554+
var lockFile = LockFile.readLockFile(lockFilePath);
555+
556+
// Verify pom is present
557+
var pom = lockFile.getPom();
558+
assertThat(pom).isNotNull();
559+
assertThat(pom.getGroupId()).extracting(GroupId::getValue).isEqualTo("com.mycompany.app");
560+
assertThat(pom.getArtifactId()).extracting(ArtifactId::getValue).isEqualTo("external-parent-pom");
561+
assertThat(pom.getChecksum()).isNotBlank();
562+
563+
// Verify external parent pom is present (Spring Boot starter parent)
564+
var parentPom = pom.getParent();
565+
assertThat(parentPom).isNotNull();
566+
assertThat(parentPom.getGroupId()).extracting(GroupId::getValue).isEqualTo("org.springframework.boot");
567+
assertThat(parentPom.getArtifactId()).extracting(ArtifactId::getValue).isEqualTo("spring-boot-starter-parent");
568+
assertThat(parentPom.getChecksum()).isNotBlank();
569+
// External pom should not have a relativePath
570+
assertThat(parentPom.getRelativePath()).isNull();
571+
572+
// Verify grandparent pom is present (Spring Boot dependencies)
573+
var grandparentPom = parentPom.getParent();
574+
assertThat(grandparentPom).isNotNull();
575+
assertThat(grandparentPom.getGroupId()).extracting(GroupId::getValue).isEqualTo("org.springframework.boot");
576+
assertThat(grandparentPom.getArtifactId())
577+
.extracting(ArtifactId::getValue)
578+
.isEqualTo("spring-boot-dependencies");
579+
assertThat(grandparentPom.getChecksum()).isNotBlank();
580+
}
581+
582+
@MavenTest
583+
public void relativeParentPom(MavenExecutionResult result) throws Exception {
584+
// contract: a project with a relative parent POM (multi-module project) should generate
585+
// a lockfile containing the parent pom with relativePath and checksums
586+
System.out.println("Running 'relativeParentPom' integration test.");
587+
assertThat(result).isSuccessful();
588+
Path lockFilePath = findFile(result, "lockfile.json");
589+
assertThat(lockFilePath).exists();
590+
var lockFile = LockFile.readLockFile(lockFilePath);
591+
592+
// Verify pom is present
593+
var pom = lockFile.getPom();
594+
assertThat(pom).isNotNull();
595+
assertThat(pom.getGroupId()).extracting(GroupId::getValue).isEqualTo("com.mycompany.app");
596+
assertThat(pom.getArtifactId()).extracting(ArtifactId::getValue).isEqualTo("relative-parent-pom-child-module");
597+
assertThat(pom.getChecksum()).isNotBlank();
598+
assertThat(pom.getRelativePath()).isEqualTo("pom.xml");
599+
600+
// Verify parent pom is present with relativePath
601+
var parentPom = pom.getParent();
602+
assertThat(parentPom).isNotNull();
603+
assertThat(parentPom.getGroupId()).extracting(GroupId::getValue).isEqualTo("com.mycompany.app");
604+
assertThat(parentPom.getArtifactId())
605+
.extracting(ArtifactId::getValue)
606+
.isEqualTo("relative-parent-pom-parent-project");
607+
assertThat(parentPom.getChecksum()).isNotBlank();
608+
assertThat(parentPom.getRelativePath()).isEqualTo("../pom.xml");
609+
}
535610
}

0 commit comments

Comments
 (0)