From 7718b18e6a168e9b01212d2479cbbb949fe32485 Mon Sep 17 00:00:00 2001
From: Sylwester Lachiewicz
Date: Fri, 10 Oct 2025 23:24:01 +0200
Subject: [PATCH] Fix excludePackageNames wildcard handling to correctly match
subpackages
---
.../plugins/javadoc/AbstractJavadocMojo.java | 13 +++-
.../maven/plugins/javadoc/JavadocUtil.java | 18 +++--
.../plugins/javadoc/JavadocUtilTest.java | 76 +++++++++++++++++++
3 files changed, 98 insertions(+), 9 deletions(-)
diff --git a/src/main/java/org/apache/maven/plugins/javadoc/AbstractJavadocMojo.java b/src/main/java/org/apache/maven/plugins/javadoc/AbstractJavadocMojo.java
index d00714cf3..a6d6bf6b2 100644
--- a/src/main/java/org/apache/maven/plugins/javadoc/AbstractJavadocMojo.java
+++ b/src/main/java/org/apache/maven/plugins/javadoc/AbstractJavadocMojo.java
@@ -678,16 +678,23 @@ public AbstractJavadocMojo(
* -subpackages. Multiple packages can be separated by commas (,), colons (:)
* or semicolons (;).
*
- * Wildcards work as followed:
+ * Wildcards work as follows:
*
- * - a wildcard at the beginning should match one or more directories
- * - any other wildcard must match exactly one directory
+ * - a wildcard at the beginning should match one or more package name segments
+ * - a wildcard at the end should match one or more package name segments (to include all subpackages)
+ * - a wildcard in the middle should match exactly one package name segment
*
*
* Example:
*
* <excludePackageNames>*.internal:org.acme.exclude1.*:org.acme.exclude2</excludePackageNames>
*
+ * This will exclude:
+ *
+ * - All packages ending with
.internal (e.g., com.example.internal, org.internal)
+ * - All subpackages under
org.acme.exclude1 (e.g., org.acme.exclude1.sub, org.acme.exclude1.sub.deep)
+ * - Only the package
org.acme.exclude2 (not its subpackages)
+ *
* @see Javadoc option exclude.
*/
@Parameter(property = "excludePackageNames")
diff --git a/src/main/java/org/apache/maven/plugins/javadoc/JavadocUtil.java b/src/main/java/org/apache/maven/plugins/javadoc/JavadocUtil.java
index 040e9992e..fdb3c96a0 100644
--- a/src/main/java/org/apache/maven/plugins/javadoc/JavadocUtil.java
+++ b/src/main/java/org/apache/maven/plugins/javadoc/JavadocUtil.java
@@ -389,13 +389,19 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
for (String excludePackagename : excludePackagenames) {
// Usage of wildcard was bad specified and bad implemented, i.e. using String.contains()
// without respecting surrounding context
- // Following implementation should match requirements as defined in the examples:
+ // Following implementation should match requirements as aligned with javadoc -exclude behavior:
// - A wildcard at the beginning should match one or more directories
- // - Any other wildcard must match exactly one directory
- Pattern p = Pattern.compile(excludePackagename
- .replace(".", regexFileSeparator)
- .replaceFirst("^\\*", ".+")
- .replace("*", "[^" + regexFileSeparator + "]+"));
+ // - A wildcard at the end should match one or more directories (to include all subpackages)
+ // - A wildcard in the middle should match exactly one directory
+ String pattern = excludePackagename.replace(".", regexFileSeparator);
+ // Handle leading wildcard: match one or more directory levels
+ pattern = pattern.replaceFirst("^\\*", ".+");
+ // Handle trailing wildcard: match one or more directory levels (for subpackages)
+ pattern = pattern.replaceFirst("\\*$", ".+");
+ // Handle wildcards in the middle: match exactly one directory level
+ pattern = pattern.replace("*", "[^" + regexFileSeparator + "]+");
+
+ Pattern p = Pattern.compile(pattern);
for (String aFileList : fileList) {
if (p.matcher(aFileList).matches()) {
diff --git a/src/test/java/org/apache/maven/plugins/javadoc/JavadocUtilTest.java b/src/test/java/org/apache/maven/plugins/javadoc/JavadocUtilTest.java
index 3b4f50e33..9e1255c51 100644
--- a/src/test/java/org/apache/maven/plugins/javadoc/JavadocUtilTest.java
+++ b/src/test/java/org/apache/maven/plugins/javadoc/JavadocUtilTest.java
@@ -681,4 +681,80 @@ public void testToList() {
List values = JavadocUtil.toList(value);
assertThat(values).containsExactly("*.internal", "org.acme.exclude1.*", "org.acme.exclude2");
}
+
+ /**
+ * Test for getExcludedPackages with wildcard patterns
+ */
+ public void testGetExcludedPackages() throws Exception {
+ // Create test directory structure
+ File testDir = new File(getBasedir(), "target/test/unit/exclude-packages-test");
+ if (testDir.exists()) {
+ FileUtils.deleteDirectory(testDir);
+ }
+
+ // Create package structure:
+ // org.example (with Main.java)
+ // org.example.sub1 (with Class1.java)
+ // org.example.sub2 (with Class2.java)
+ // org.example.sub2.subsub (with Class3.java)
+ // org.other (with Other.java)
+ // com.internal (with Internal.java)
+
+ File orgExample = new File(testDir, "org/example");
+ File orgExampleSub1 = new File(testDir, "org/example/sub1");
+ File orgExampleSub2 = new File(testDir, "org/example/sub2");
+ File orgExampleSub2Subsub = new File(testDir, "org/example/sub2/subsub");
+ File orgOther = new File(testDir, "org/other");
+ File comInternal = new File(testDir, "com/internal");
+
+ assertTrue(orgExample.mkdirs());
+ assertTrue(orgExampleSub1.mkdirs());
+ assertTrue(orgExampleSub2.mkdirs());
+ assertTrue(orgExampleSub2Subsub.mkdirs());
+ assertTrue(orgOther.mkdirs());
+ assertTrue(comInternal.mkdirs());
+
+ // Create Java files in each directory
+ new File(orgExample, "Main.java").createNewFile();
+ new File(orgExampleSub1, "Class1.java").createNewFile();
+ new File(orgExampleSub2, "Class2.java").createNewFile();
+ new File(orgExampleSub2Subsub, "Class3.java").createNewFile();
+ new File(orgOther, "Other.java").createNewFile();
+ new File(comInternal, "Internal.java").createNewFile();
+
+ Path testPath = testDir.toPath();
+
+ // Test 1: org.example.* should match all subpackages of org.example
+ // Expected: org.example.sub1, org.example.sub2, org.example.sub2.subsub
+ Collection excludes1 = Collections.singletonList("org.example.*");
+ Collection result1 = JavadocUtil.getExcludedPackages(testPath, excludes1);
+ assertThat(result1)
+ .as("org.example.* should match all subpackages of org.example")
+ .containsExactlyInAnyOrder("org.example.sub1", "org.example.sub2", "org.example.sub2.subsub");
+
+ // Test 2: org.example should match only org.example itself
+ Collection excludes2 = Collections.singletonList("org.example");
+ Collection result2 = JavadocUtil.getExcludedPackages(testPath, excludes2);
+ assertThat(result2)
+ .as("org.example should match only org.example package")
+ .containsExactly("org.example");
+
+ // Test 3: *.internal should match any package ending with .internal
+ Collection excludes3 = Collections.singletonList("*.internal");
+ Collection result3 = JavadocUtil.getExcludedPackages(testPath, excludes3);
+ assertThat(result3).as("*.internal should match com.internal").containsExactly("com.internal");
+
+ // Test 4: org.*.sub1 should match org.example.sub1 (wildcard in the middle matches exactly one level)
+ Collection excludes4 = Collections.singletonList("org.*.sub1");
+ Collection result4 = JavadocUtil.getExcludedPackages(testPath, excludes4);
+ assertThat(result4).as("org.*.sub1 should match org.example.sub1").containsExactly("org.example.sub1");
+
+ // Test 5: Multiple patterns
+ Collection excludes5 = Arrays.asList("org.example.*", "org.other");
+ Collection result5 = JavadocUtil.getExcludedPackages(testPath, excludes5);
+ assertThat(result5)
+ .as("Multiple patterns should work together")
+ .containsExactlyInAnyOrder(
+ "org.example.sub1", "org.example.sub2", "org.example.sub2.subsub", "org.other");
+ }
}