Skip to content

Commit 1008471

Browse files
Strum355claude
andauthored
fix(workspace): parse actual XML output from mvn help:evaluate (#470)
## Summary - `mvn help:evaluate -Dexpression=project.modules` returns XML (`<strings><string>module-a</string>...</strings>`), not the bracket format (`[module-a, module-b]`) the original parser assumed - Replaced regex-based bracket parser with XML DOM parser - Cleaned up inline fully-qualified types into proper imports - Replaced mocked integration tests with real Maven CLI integration tests ## Test plan - [x] Verified actual `mvn help:evaluate` output against the multi-module test fixture - [x] Unit tests pass (`MavenWorkspaceDiscoveryTest`) - [x] Integration tests pass (`MavenWorkspaceDiscoveryIT`) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 421b4f1 commit 1008471

3 files changed

Lines changed: 164 additions & 272 deletions

File tree

src/main/java/io/github/guacsec/trustifyda/impl/ExhortApi.java

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import jakarta.mail.internet.MimeMultipart;
4545
import jakarta.mail.util.ByteArrayDataSource;
4646
import java.io.IOException;
47+
import java.io.StringReader;
4748
import java.net.InetSocketAddress;
4849
import java.net.ProxySelector;
4950
import java.net.URI;
@@ -75,9 +76,11 @@
7576
import java.util.logging.Level;
7677
import java.util.logging.Logger;
7778
import java.util.stream.Collectors;
79+
import javax.xml.parsers.DocumentBuilderFactory;
7880
import org.tomlj.TomlArray;
7981
import org.tomlj.TomlParseResult;
8082
import org.tomlj.TomlTable;
83+
import org.xml.sax.InputSource;
8184

8285
/** Concrete implementation of the Exhort {@link Api} Service. */
8386
public final class ExhortApi implements Api {
@@ -1198,27 +1201,43 @@ private List<String> listMavenModules(Path dir, String mvnBin) {
11981201
}
11991202

12001203
/**
1201-
* Parses the output of {@code mvn help:evaluate -Dexpression=project.modules} which returns a
1202-
* string like {@code [module-a, module-b]}.
1204+
* Parses the XML output of {@code mvn help:evaluate -Dexpression=project.modules}. The output
1205+
* format is {@code <strings><string>module-a</string><string>module-b</string></strings>}.
12031206
*
12041207
* @param raw the raw output string
12051208
* @return list of module name strings
12061209
*/
12071210
static List<String> parseMavenModuleList(String raw) {
1208-
if (raw == null || raw.isEmpty()) {
1211+
if (raw == null || raw.isEmpty() || "null".equals(raw.trim())) {
12091212
return Collections.emptyList();
12101213
}
1211-
// Expected format: [module-a, module-b, ...]
1212-
java.util.regex.Matcher matcher =
1213-
java.util.regex.Pattern.compile("^\\[(.+)]$").matcher(raw.trim());
1214-
if (!matcher.matches()) {
1214+
String trimmed = raw.trim();
1215+
if (!trimmed.startsWith("<strings")) {
1216+
return Collections.emptyList();
1217+
}
1218+
try {
1219+
var factory = DocumentBuilderFactory.newInstance();
1220+
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
1221+
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
1222+
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
1223+
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
1224+
factory.setXIncludeAware(false);
1225+
factory.setExpandEntityReferences(false);
1226+
var builder = factory.newDocumentBuilder();
1227+
var doc = builder.parse(new InputSource(new StringReader(trimmed)));
1228+
var nodes = doc.getElementsByTagName("string");
1229+
List<String> modules = new ArrayList<>(nodes.getLength());
1230+
for (int i = 0; i < nodes.getLength(); i++) {
1231+
String text = nodes.item(i).getTextContent().trim();
1232+
if (!text.isEmpty()) {
1233+
modules.add(text);
1234+
}
1235+
}
1236+
return modules;
1237+
} catch (Exception e) {
1238+
LOG.log(Level.WARNING, "Failed to parse Maven module list XML", e);
12151239
return Collections.emptyList();
12161240
}
1217-
String inner = matcher.group(1);
1218-
return java.util.Arrays.stream(inner.split(","))
1219-
.map(String::trim)
1220-
.filter(s -> !s.isEmpty())
1221-
.toList();
12221241
}
12231242

12241243
/**
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
* Copyright 2023-2025 Trustify Dependency Analytics Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
*
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package io.github.guacsec.trustifyda.impl;
18+
19+
import static org.assertj.core.api.Assertions.assertThat;
20+
21+
import java.io.IOException;
22+
import java.nio.file.Path;
23+
import java.util.List;
24+
import java.util.Set;
25+
import org.junit.jupiter.api.Assumptions;
26+
import org.junit.jupiter.api.BeforeAll;
27+
import org.junit.jupiter.api.Tag;
28+
import org.junit.jupiter.api.Test;
29+
30+
@Tag("IntegrationTest")
31+
class MavenWorkspaceDiscoveryIT {
32+
33+
private static final Path MAVEN_FIXTURES =
34+
Path.of("src/test/resources/tst_manifests/workspace/maven");
35+
36+
@BeforeAll
37+
static void requireMaven() {
38+
boolean mavenAvailable;
39+
try {
40+
Process p = new ProcessBuilder("mvn", "-v").redirectErrorStream(true).start();
41+
mavenAvailable = p.waitFor() == 0;
42+
} catch (Exception e) {
43+
mavenAvailable = false;
44+
}
45+
Assumptions.assumeTrue(mavenAvailable, "mvn not available on PATH");
46+
}
47+
48+
@Test
49+
void discoverWorkspaceManifests_mavenMultiModule() throws IOException {
50+
Path workspaceDir = MAVEN_FIXTURES.resolve("maven_multi_module").toAbsolutePath().normalize();
51+
ExhortApi api = new ExhortApi();
52+
List<Path> manifests = api.discoverWorkspaceManifests(workspaceDir, Set.of());
53+
54+
assertThat(manifests).hasSize(3);
55+
assertThat(manifests).allMatch(p -> p.getFileName().toString().equals("pom.xml"));
56+
assertThat(manifests.getFirst()).isEqualTo(workspaceDir.resolve("pom.xml"));
57+
assertThat(manifests).anyMatch(p -> p.toString().contains("module-a"));
58+
assertThat(manifests).anyMatch(p -> p.toString().contains("module-b"));
59+
}
60+
61+
@Test
62+
void discoverWorkspaceManifests_nestedAggregator() throws IOException {
63+
Path workspaceDir =
64+
MAVEN_FIXTURES.resolve("maven_nested_aggregator").toAbsolutePath().normalize();
65+
ExhortApi api = new ExhortApi();
66+
List<Path> manifests = api.discoverWorkspaceManifests(workspaceDir, Set.of());
67+
68+
assertThat(manifests).hasSize(3);
69+
assertThat(manifests.getFirst()).isEqualTo(workspaceDir.resolve("pom.xml"));
70+
assertThat(manifests)
71+
.anyMatch(p -> p.toString().contains("parent" + java.io.File.separator + "pom.xml"));
72+
assertThat(manifests)
73+
.anyMatch(
74+
p ->
75+
p.toString()
76+
.contains(
77+
"parent"
78+
+ java.io.File.separator
79+
+ "child"
80+
+ java.io.File.separator
81+
+ "pom.xml"));
82+
}
83+
84+
@Test
85+
void discoverWorkspaceManifests_noModules() throws IOException {
86+
Path workspaceDir = MAVEN_FIXTURES.resolve("maven_no_modules").toAbsolutePath().normalize();
87+
ExhortApi api = new ExhortApi();
88+
List<Path> manifests = api.discoverWorkspaceManifests(workspaceDir, Set.of());
89+
90+
assertThat(manifests).hasSize(1);
91+
assertThat(manifests.getFirst()).isEqualTo(workspaceDir.resolve("pom.xml"));
92+
}
93+
94+
@Test
95+
void discoverWorkspaceManifests_missingModuleDirectory() throws IOException {
96+
// Maven fails to read the POM when a declared module directory is missing,
97+
// so graceful degradation returns only the root pom.xml.
98+
Path workspaceDir = MAVEN_FIXTURES.resolve("maven_missing_module").toAbsolutePath().normalize();
99+
ExhortApi api = new ExhortApi();
100+
List<Path> manifests = api.discoverWorkspaceManifests(workspaceDir, Set.of());
101+
102+
assertThat(manifests).hasSize(1);
103+
assertThat(manifests.getFirst()).isEqualTo(workspaceDir.resolve("pom.xml"));
104+
}
105+
106+
@Test
107+
void discoverWorkspaceManifests_ignorePatternFiltering() throws IOException {
108+
Path workspaceDir = MAVEN_FIXTURES.resolve("maven_multi_module").toAbsolutePath().normalize();
109+
ExhortApi api = new ExhortApi();
110+
List<Path> manifests = api.discoverWorkspaceManifests(workspaceDir, Set.of("**/module-b/**"));
111+
112+
assertThat(manifests).anyMatch(p -> p.toString().contains("module-a"));
113+
assertThat(manifests).noneMatch(p -> p.toString().contains("module-b"));
114+
}
115+
}

0 commit comments

Comments
 (0)