From 05da451bdaeb6ead8e91524808c0f5ac6c147a51 Mon Sep 17 00:00:00 2001 From: Elliotte Rusty Harold Date: Sun, 26 Jul 2026 11:56:15 +0000 Subject: [PATCH 01/10] Add integration test for MWAR-220 overlay dependencyManagement version conflict --- src/it/MWAR-389/invoker.properties | 21 +++ src/it/MWAR-389/main-war/pom.xml | 54 ++++++++ .../main-war/src/main/webapp/WEB-INF/web.xml | 5 + src/it/MWAR-389/overlay-war/pom.xml | 38 ++++++ .../src/main/webapp/WEB-INF/web.xml | 5 + src/it/MWAR-389/pom.xml | 42 ++++++ src/it/MWAR-389/verify.bsh | 124 ++++++++++++++++++ 7 files changed, 289 insertions(+) create mode 100644 src/it/MWAR-389/invoker.properties create mode 100644 src/it/MWAR-389/main-war/pom.xml create mode 100644 src/it/MWAR-389/main-war/src/main/webapp/WEB-INF/web.xml create mode 100644 src/it/MWAR-389/overlay-war/pom.xml create mode 100644 src/it/MWAR-389/overlay-war/src/main/webapp/WEB-INF/web.xml create mode 100644 src/it/MWAR-389/pom.xml create mode 100644 src/it/MWAR-389/verify.bsh diff --git a/src/it/MWAR-389/invoker.properties b/src/it/MWAR-389/invoker.properties new file mode 100644 index 00000000..a2a4ce1e --- /dev/null +++ b/src/it/MWAR-389/invoker.properties @@ -0,0 +1,21 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +invoker.goals=clean install +# This test is expected to fail until MWAR-220 is fixed. +# When the fix is implemented, remove this line. +invoker.failureExpected=true diff --git a/src/it/MWAR-389/main-war/pom.xml b/src/it/MWAR-389/main-war/pom.xml new file mode 100644 index 00000000..4e2440e5 --- /dev/null +++ b/src/it/MWAR-389/main-war/pom.xml @@ -0,0 +1,54 @@ + + + + 4.0.0 + + test + MWAR-389 + 1.0-SNAPSHOT + + main-war + war + + + + + org.codehaus.plexus + plexus-utils + @plexusUtilVersion@ + + + + + + + org.codehaus.plexus + plexus-utils + + + test + overlay-war + 1.0-SNAPSHOT + war + runtime + + + diff --git a/src/it/MWAR-389/main-war/src/main/webapp/WEB-INF/web.xml b/src/it/MWAR-389/main-war/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000..ebde7254 --- /dev/null +++ b/src/it/MWAR-389/main-war/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,5 @@ + + + diff --git a/src/it/MWAR-389/overlay-war/pom.xml b/src/it/MWAR-389/overlay-war/pom.xml new file mode 100644 index 00000000..5f238481 --- /dev/null +++ b/src/it/MWAR-389/overlay-war/pom.xml @@ -0,0 +1,38 @@ + + + + 4.0.0 + + test + MWAR-389 + 1.0-SNAPSHOT + + overlay-war + war + + + + org.codehaus.plexus + plexus-utils + 3.0.24 + + + diff --git a/src/it/MWAR-389/overlay-war/src/main/webapp/WEB-INF/web.xml b/src/it/MWAR-389/overlay-war/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000..ebde7254 --- /dev/null +++ b/src/it/MWAR-389/overlay-war/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,5 @@ + + + diff --git a/src/it/MWAR-389/pom.xml b/src/it/MWAR-389/pom.xml new file mode 100644 index 00000000..c2f57b3d --- /dev/null +++ b/src/it/MWAR-389/pom.xml @@ -0,0 +1,42 @@ + + + + 4.0.0 + test + MWAR-389 + pom + 1.0-SNAPSHOT + MWAR-389 dependencyManagement overlay dependency version conflict + + overlay-war + main-war + + + + + + maven-war-plugin + @pom.version@ + + + + + diff --git a/src/it/MWAR-389/verify.bsh b/src/it/MWAR-389/verify.bsh new file mode 100644 index 00000000..3984561a --- /dev/null +++ b/src/it/MWAR-389/verify.bsh @@ -0,0 +1,124 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import java.io.*; +import java.util.*; +import org.codehaus.plexus.util.*; + +/* + * Test for MWAR-220 / issue #389: + * dependencyManagement with overlays should not result in multiple versions + * of the same dependency in WEB-INF/lib. + * + * The overlay-war is built with plexus-utils 3.0.24. + * The main-war has dependencyManagement pinning plexus-utils to the managed + * version (@plexusUtilVersion@), and uses overlay-war as an overlay. + * + * If the issue still exists, both versions will appear in main-war's WEB-INF/lib. + * If fixed, only the managed version will appear. + */ + +String managedVersion = plexusUtilVersion; + +// --- Check overlay WAR content --- +File overlayTarget = new File( basedir, "overlay-war/target" ); +File overlayWar = new File( overlayTarget, "overlay-war-1.0-SNAPSHOT" ); +File overlayLib = new File( overlayWar, "WEB-INF/lib" ); + +String[] overlayJars = overlayLib.list(); +boolean overlayHasOld = false; +boolean overlayHasManaged = false; + +for ( int i = 0; i < overlayJars.length; i++ ) +{ + String jar = overlayJars[i]; + if ( jar.startsWith( "plexus-utils-3.0.24" ) ) + { + overlayHasOld = true; + } + if ( jar.startsWith( "plexus-utils-" + managedVersion ) ) + { + overlayHasManaged = true; + } +} + +if ( !overlayHasOld ) +{ + System.err.println( "Overlay WAR missing plexus-utils-3.0.24.jar in WEB-INF/lib." ); + return false; +} + +if ( overlayHasManaged ) +{ + System.err.println( "Overlay WAR unexpectedly contains plexus-utils-" + managedVersion + ".jar. " + + "Overlay should only have version 3.0.24." ); + return false; +} + +System.out.println( "Overlay WAR correctly contains only plexus-utils-3.0.24.jar." ); + +// --- Check main WAR content --- +File mainTarget = new File( basedir, "main-war/target" ); +File mainWar = new File( mainTarget, "main-war-1.0-SNAPSHOT" ); +File mainLib = new File( mainWar, "WEB-INF/lib" ); + +String[] mainJars = mainLib.list(); +boolean mainHasOld = false; +boolean mainHasManaged = false; +List duplicateGavs = new ArrayList(); + +for ( int i = 0; i < mainJars.length; i++ ) +{ + String jar = mainJars[i]; + if ( jar.startsWith( "plexus-utils-3.0.24" ) ) + { + mainHasOld = true; + duplicateGavs.add( "plexus-utils:3.0.24 (" + jar + ")" ); + } + if ( jar.startsWith( "plexus-utils-" + managedVersion ) ) + { + mainHasManaged = true; + duplicateGavs.add( "plexus-utils:" + managedVersion + " (" + jar + ")" ); + } +} + +if ( !mainHasManaged ) +{ + System.err.println( "Main WAR missing plexus-utils-" + managedVersion + ".jar in WEB-INF/lib." ); + return false; +} + +if ( mainHasOld ) +{ + // Both versions found -- issue STILL EXISTS + System.out.println( "ISSUE #389 (MWAR-220) STILL EXISTS:" ); + System.out.println( " Both plexus-utils versions found in main WAR WEB-INF/lib:" ); + System.out.println( " - plexus-utils-3.0.24.jar (from overlay unpacking)" ); + System.out.println( " - plexus-utils-" + managedVersion + ".jar (from dependencyManagement)" ); + System.out.println( " dependencyManagement does not filter overlay-provided jars." ); + return false; +} +else +{ + // Only managed version -- issue has been fixed + System.out.println( "ISSUE #389 (MWAR-220) APPEARS FIXED:" ); + System.out.println( " Only plexus-utils-" + managedVersion + ".jar found in WEB-INF/lib." ); + System.out.println( " The overlay's plexus-utils-3.0.24.jar was correctly filtered out." ); + return true; +} From f6e6639c2042429f4218a056f4017fc2d105fde4 Mon Sep 17 00:00:00 2001 From: Elliotte Rusty Harold Date: Sun, 26 Jul 2026 11:59:28 +0000 Subject: [PATCH 02/10] Remove failureExpected so CI will verify the test fails --- src/it/MWAR-389/invoker.properties | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/it/MWAR-389/invoker.properties b/src/it/MWAR-389/invoker.properties index a2a4ce1e..3376b9e4 100644 --- a/src/it/MWAR-389/invoker.properties +++ b/src/it/MWAR-389/invoker.properties @@ -16,6 +16,3 @@ # under the License. invoker.goals=clean install -# This test is expected to fail until MWAR-220 is fixed. -# When the fix is implemented, remove this line. -invoker.failureExpected=true From 64d51468b856f0f88de7ccf1d7a58c0b003e4aae Mon Sep 17 00:00:00 2001 From: Elliotte Rusty Harold Date: Sun, 26 Jul 2026 12:13:13 +0000 Subject: [PATCH 03/10] Fix license headers in MWAR-389 IT web.xml files --- .../main-war/src/main/webapp/WEB-INF/web.xml | 27 ++++++++++++++++--- .../src/main/webapp/WEB-INF/web.xml | 27 ++++++++++++++++--- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/src/it/MWAR-389/main-war/src/main/webapp/WEB-INF/web.xml b/src/it/MWAR-389/main-war/src/main/webapp/WEB-INF/web.xml index ebde7254..3b1a1e64 100644 --- a/src/it/MWAR-389/main-war/src/main/webapp/WEB-INF/web.xml +++ b/src/it/MWAR-389/main-war/src/main/webapp/WEB-INF/web.xml @@ -1,5 +1,24 @@ - - + + + diff --git a/src/it/MWAR-389/overlay-war/src/main/webapp/WEB-INF/web.xml b/src/it/MWAR-389/overlay-war/src/main/webapp/WEB-INF/web.xml index ebde7254..3b1a1e64 100644 --- a/src/it/MWAR-389/overlay-war/src/main/webapp/WEB-INF/web.xml +++ b/src/it/MWAR-389/overlay-war/src/main/webapp/WEB-INF/web.xml @@ -1,5 +1,24 @@ - - + + + From d04d04bd72d20e8b2d4f4249ffb7e4daa6ee3839 Mon Sep 17 00:00:00 2001 From: Elliotte Rusty Harold Date: Sun, 26 Jul 2026 12:31:56 +0000 Subject: [PATCH 04/10] Fix MWAR-220: filter overlay jars that conflict with managed dependencies --- .../war/packaging/OverlayPackagingTask.java | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/src/main/java/org/apache/maven/plugins/war/packaging/OverlayPackagingTask.java b/src/main/java/org/apache/maven/plugins/war/packaging/OverlayPackagingTask.java index e9849923..f9d1b68b 100644 --- a/src/main/java/org/apache/maven/plugins/war/packaging/OverlayPackagingTask.java +++ b/src/main/java/org/apache/maven/plugins/war/packaging/OverlayPackagingTask.java @@ -20,7 +20,10 @@ import java.io.File; import java.io.IOException; +import java.util.HashSet; +import java.util.Set; +import org.apache.maven.artifact.Artifact; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.war.Overlay; import org.apache.maven.plugins.war.util.PathSet; @@ -61,6 +64,9 @@ public void performPackaging(WarPackagingContext context) throws MojoExecutionEx // Step1: Extract if necessary final File tmpDir = unpackOverlay(context, overlay); + // Step1b: Remove jars from overlay that conflict with managed dependencies + filterConflictingDependencyJars(context, tmpDir); + // Step2: setup final PathSet includes = getFilesToIncludes(tmpDir, overlay.getIncludes(), overlay.getExcludes()); @@ -127,4 +133,72 @@ protected File getOverlayTempDirectory(WarPackagingContext context, Overlay over } return result; } + + /** + * Removes jars from the overlay's {@code WEB-INF/lib} that conflict with the project's managed + * dependencies. When a project uses dependencyManagement to pin a version, any jar from the + * overlay with the same artifactId but a different version is removed, ensuring only the + * dependency-managed version ends up in {@code WEB-INF/lib}. + * + * @param context the packaging context + * @param overlayDir the unpacked overlay directory + */ + private void filterConflictingDependencyJars(WarPackagingContext context, File overlayDir) { + File libDir = new File(overlayDir, "WEB-INF/lib"); + if (!libDir.isDirectory()) { + return; + } + + Set projectArtifactIds = new HashSet<>(); + for (Artifact artifact : context.getProject().getArtifacts()) { + if (!artifact.isOptional() && "jar".equals(artifact.getType())) { + projectArtifactIds.add(artifact.getArtifactId()); + } + } + + if (projectArtifactIds.isEmpty()) { + return; + } + + File[] overlayJars = libDir.listFiles((dir, name) -> name.endsWith(".jar")); + if (overlayJars == null) { + return; + } + + for (File overlayJar : overlayJars) { + String jarName = overlayJar.getName(); + String artifactId = extractArtifactId(jarName); + if (artifactId != null && projectArtifactIds.contains(artifactId)) { + context.getLog() + .debug("Removing dependency [" + jarName + "] from overlay [" + overlay.getId() + + "]; managed version in project already provides it"); + overlayJar.delete(); + } + } + } + + /** + * Extracts the Maven artifactId from a jar filename following the + * {@code artifactId-version(-classifier)?.jar} convention. + * + * @param jarName the jar filename + * @return the artifactId, or null if it cannot be determined + */ + private static String extractArtifactId(String jarName) { + if (jarName == null || !jarName.endsWith(".jar")) { + return null; + } + String baseName = jarName.substring(0, jarName.length() - 4); + int versionStart = -1; + for (int i = 0; i < baseName.length(); i++) { + if (Character.isDigit(baseName.charAt(i))) { + versionStart = i; + break; + } + } + if (versionStart > 0 && baseName.charAt(versionStart - 1) == '-') { + return baseName.substring(0, versionStart - 1); + } + return null; + } } From 5683ec201691453142b0d30a10a6136c09eecc03 Mon Sep 17 00:00:00 2001 From: Elliotte Rusty Harold Date: Sun, 26 Jul 2026 13:44:30 +0000 Subject: [PATCH 05/10] Remove System.out.println from verify.bsh --- src/it/MWAR-389/verify.bsh | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/src/it/MWAR-389/verify.bsh b/src/it/MWAR-389/verify.bsh index 3984561a..ab525007 100644 --- a/src/it/MWAR-389/verify.bsh +++ b/src/it/MWAR-389/verify.bsh @@ -18,7 +18,6 @@ */ import java.io.*; -import java.util.*; import org.codehaus.plexus.util.*; /* @@ -71,7 +70,7 @@ if ( overlayHasManaged ) return false; } -System.out.println( "Overlay WAR correctly contains only plexus-utils-3.0.24.jar." ); +// Overlay WAR correctly contains only plexus-utils-3.0.24.jar. // --- Check main WAR content --- File mainTarget = new File( basedir, "main-war/target" ); @@ -81,7 +80,6 @@ File mainLib = new File( mainWar, "WEB-INF/lib" ); String[] mainJars = mainLib.list(); boolean mainHasOld = false; boolean mainHasManaged = false; -List duplicateGavs = new ArrayList(); for ( int i = 0; i < mainJars.length; i++ ) { @@ -89,12 +87,10 @@ for ( int i = 0; i < mainJars.length; i++ ) if ( jar.startsWith( "plexus-utils-3.0.24" ) ) { mainHasOld = true; - duplicateGavs.add( "plexus-utils:3.0.24 (" + jar + ")" ); } if ( jar.startsWith( "plexus-utils-" + managedVersion ) ) { mainHasManaged = true; - duplicateGavs.add( "plexus-utils:" + managedVersion + " (" + jar + ")" ); } } @@ -107,18 +103,12 @@ if ( !mainHasManaged ) if ( mainHasOld ) { // Both versions found -- issue STILL EXISTS - System.out.println( "ISSUE #389 (MWAR-220) STILL EXISTS:" ); - System.out.println( " Both plexus-utils versions found in main WAR WEB-INF/lib:" ); - System.out.println( " - plexus-utils-3.0.24.jar (from overlay unpacking)" ); - System.out.println( " - plexus-utils-" + managedVersion + ".jar (from dependencyManagement)" ); - System.out.println( " dependencyManagement does not filter overlay-provided jars." ); + System.err.println( "Found plexus-utils-3.0.24.jar in main WAR WEB-INF/lib; " + + "dependencyManagement should have excluded it." ); return false; } else { // Only managed version -- issue has been fixed - System.out.println( "ISSUE #389 (MWAR-220) APPEARS FIXED:" ); - System.out.println( " Only plexus-utils-" + managedVersion + ".jar found in WEB-INF/lib." ); - System.out.println( " The overlay's plexus-utils-3.0.24.jar was correctly filtered out." ); return true; } From 0700e6ca10f1ca1a673f68a10e2987809533cec8 Mon Sep 17 00:00:00 2001 From: Elliotte Rusty Harold Date: Sun, 26 Jul 2026 13:54:41 +0000 Subject: [PATCH 06/10] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/it/MWAR-389/verify.bsh | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/it/MWAR-389/verify.bsh b/src/it/MWAR-389/verify.bsh index ab525007..8d344106 100644 --- a/src/it/MWAR-389/verify.bsh +++ b/src/it/MWAR-389/verify.bsh @@ -40,7 +40,18 @@ File overlayTarget = new File( basedir, "overlay-war/target" ); File overlayWar = new File( overlayTarget, "overlay-war-1.0-SNAPSHOT" ); File overlayLib = new File( overlayWar, "WEB-INF/lib" ); +if ( !overlayLib.exists() || !overlayLib.isDirectory() ) +{ + System.err.println( "Overlay WAR WEB-INF/lib directory missing: " + overlayLib ); + return false; +} + String[] overlayJars = overlayLib.list(); +if ( overlayJars == null ) +{ + System.err.println( "Failed to list overlay WAR WEB-INF/lib contents: " + overlayLib ); + return false; +} boolean overlayHasOld = false; boolean overlayHasManaged = false; From f28e3257983a41419bead4d00271fff23c24229f Mon Sep 17 00:00:00 2001 From: Elliotte Rusty Harold Date: Sun, 26 Jul 2026 14:05:40 +0000 Subject: [PATCH 07/10] filterConflictingDependencyJars: filter to runtime-scope artifacts Only consider project artifacts that will actually be packaged into WEB-INF/lib (runtime scope). Otherwise provided-scope artifacts cause overlay jars to be removed even though no project jar replaces them, resulting in a missing dependency at runtime. --- .../maven/plugins/war/packaging/OverlayPackagingTask.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/apache/maven/plugins/war/packaging/OverlayPackagingTask.java b/src/main/java/org/apache/maven/plugins/war/packaging/OverlayPackagingTask.java index f9d1b68b..649205a7 100644 --- a/src/main/java/org/apache/maven/plugins/war/packaging/OverlayPackagingTask.java +++ b/src/main/java/org/apache/maven/plugins/war/packaging/OverlayPackagingTask.java @@ -24,6 +24,7 @@ import java.util.Set; import org.apache.maven.artifact.Artifact; +import org.apache.maven.artifact.resolver.filter.ScopeArtifactFilter; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.war.Overlay; import org.apache.maven.plugins.war.util.PathSet; @@ -149,9 +150,10 @@ private void filterConflictingDependencyJars(WarPackagingContext context, File o return; } + ScopeArtifactFilter filter = new ScopeArtifactFilter(Artifact.SCOPE_RUNTIME); Set projectArtifactIds = new HashSet<>(); for (Artifact artifact : context.getProject().getArtifacts()) { - if (!artifact.isOptional() && "jar".equals(artifact.getType())) { + if (!artifact.isOptional() && filter.include(artifact) && "jar".equals(artifact.getType())) { projectArtifactIds.add(artifact.getArtifactId()); } } From 7cfb492bb7a34e8421feff5efa086b139cfdfbd9 Mon Sep 17 00:00:00 2001 From: Elliotte Rusty Harold Date: Sun, 26 Jul 2026 14:15:26 +0000 Subject: [PATCH 08/10] Address Copilot review comments - Check overlayJar.delete() return value, log warning on failure - Guard mainLib.list() against null return in verify.bsh - Add missing directory existence check for main WAR WEB-INF/lib --- src/it/MWAR-389/verify.bsh | 11 +++++++++++ .../plugins/war/packaging/OverlayPackagingTask.java | 5 ++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/it/MWAR-389/verify.bsh b/src/it/MWAR-389/verify.bsh index 8d344106..cd88e442 100644 --- a/src/it/MWAR-389/verify.bsh +++ b/src/it/MWAR-389/verify.bsh @@ -88,7 +88,18 @@ File mainTarget = new File( basedir, "main-war/target" ); File mainWar = new File( mainTarget, "main-war-1.0-SNAPSHOT" ); File mainLib = new File( mainWar, "WEB-INF/lib" ); +if ( !mainLib.exists() || !mainLib.isDirectory() ) +{ + System.err.println( "Main WAR WEB-INF/lib directory missing: " + mainLib ); + return false; +} + String[] mainJars = mainLib.list(); +if ( mainJars == null ) +{ + System.err.println( "Failed to list main WAR WEB-INF/lib contents: " + mainLib ); + return false; +} boolean mainHasOld = false; boolean mainHasManaged = false; diff --git a/src/main/java/org/apache/maven/plugins/war/packaging/OverlayPackagingTask.java b/src/main/java/org/apache/maven/plugins/war/packaging/OverlayPackagingTask.java index 649205a7..01dfec1a 100644 --- a/src/main/java/org/apache/maven/plugins/war/packaging/OverlayPackagingTask.java +++ b/src/main/java/org/apache/maven/plugins/war/packaging/OverlayPackagingTask.java @@ -174,7 +174,10 @@ private void filterConflictingDependencyJars(WarPackagingContext context, File o context.getLog() .debug("Removing dependency [" + jarName + "] from overlay [" + overlay.getId() + "]; managed version in project already provides it"); - overlayJar.delete(); + if (!overlayJar.delete()) { + context.getLog() + .warn("Failed to delete [" + jarName + "] from overlay [" + overlay.getId() + "]"); + } } } } From 8e1e4571d6836e4440eac2741af4c9fa340d1360 Mon Sep 17 00:00:00 2001 From: Elliotte Rusty Harold Date: Mon, 27 Jul 2026 10:52:08 +0000 Subject: [PATCH 09/10] Address review: fix cache poisoning, extractArtifactId for digit-containing IDs, debug msg, JavaDoc --- .../war/packaging/OverlayPackagingTask.java | 75 ++++++++++++------- 1 file changed, 49 insertions(+), 26 deletions(-) diff --git a/src/main/java/org/apache/maven/plugins/war/packaging/OverlayPackagingTask.java b/src/main/java/org/apache/maven/plugins/war/packaging/OverlayPackagingTask.java index 01dfec1a..2ec2af82 100644 --- a/src/main/java/org/apache/maven/plugins/war/packaging/OverlayPackagingTask.java +++ b/src/main/java/org/apache/maven/plugins/war/packaging/OverlayPackagingTask.java @@ -20,6 +20,8 @@ import java.io.File; import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; import java.util.Set; @@ -65,11 +67,12 @@ public void performPackaging(WarPackagingContext context) throws MojoExecutionEx // Step1: Extract if necessary final File tmpDir = unpackOverlay(context, overlay); - // Step1b: Remove jars from overlay that conflict with managed dependencies - filterConflictingDependencyJars(context, tmpDir); + // Step1b: Identify jars from overlay that conflict with project dependencies + Set conflictingJars = computeConflictingJars(context, tmpDir); - // Step2: setup - final PathSet includes = getFilesToIncludes(tmpDir, overlay.getIncludes(), overlay.getExcludes()); + // Step2: setup, excluding conflicting jars so the cached overlay is not mutated + String[] effectiveExcludes = mergeExcludes(overlay.getExcludes(), conflictingJars); + final PathSet includes = getFilesToIncludes(tmpDir, overlay.getIncludes(), effectiveExcludes); // Copy if (null == overlay.getTargetPath()) { @@ -136,18 +139,21 @@ protected File getOverlayTempDirectory(WarPackagingContext context, Overlay over } /** - * Removes jars from the overlay's {@code WEB-INF/lib} that conflict with the project's managed - * dependencies. When a project uses dependencyManagement to pin a version, any jar from the - * overlay with the same artifactId but a different version is removed, ensuring only the - * dependency-managed version ends up in {@code WEB-INF/lib}. + * Identifies jars from the overlay's {@code WEB-INF/lib} whose artifactId matches a non-optional + * runtime-scope dependency resolved by the project. These jars are candidates for exclusion + * from the overlay copy so that only the project-resolved version ends up in {@code WEB-INF/lib}. + * + *

The overlay's cached unpack directory is not mutated; the returned set is used + * as additional excludes during the copy step. * * @param context the packaging context * @param overlayDir the unpacked overlay directory + * @return set of paths (relative to the overlay dir) to exclude from the overlay copy */ - private void filterConflictingDependencyJars(WarPackagingContext context, File overlayDir) { + private Set computeConflictingJars(WarPackagingContext context, File overlayDir) { File libDir = new File(overlayDir, "WEB-INF/lib"); if (!libDir.isDirectory()) { - return; + return Collections.emptySet(); } ScopeArtifactFilter filter = new ScopeArtifactFilter(Artifact.SCOPE_RUNTIME); @@ -159,33 +165,55 @@ private void filterConflictingDependencyJars(WarPackagingContext context, File o } if (projectArtifactIds.isEmpty()) { - return; + return Collections.emptySet(); } File[] overlayJars = libDir.listFiles((dir, name) -> name.endsWith(".jar")); if (overlayJars == null) { - return; + return Collections.emptySet(); } + Set conflicting = new HashSet<>(); for (File overlayJar : overlayJars) { String jarName = overlayJar.getName(); String artifactId = extractArtifactId(jarName); if (artifactId != null && projectArtifactIds.contains(artifactId)) { context.getLog() - .debug("Removing dependency [" + jarName + "] from overlay [" + overlay.getId() - + "]; managed version in project already provides it"); - if (!overlayJar.delete()) { - context.getLog() - .warn("Failed to delete [" + jarName + "] from overlay [" + overlay.getId() + "]"); - } + .debug("Excluding dependency [" + jarName + "] from overlay [" + overlay.getId() + + "]; project runtime dependencies already include a version"); + conflicting.add("WEB-INF/lib/" + jarName); } } + return conflicting; + } + + /** + * Merges the overlay's existing excludes with additional jar filenames to exclude. + * + * @param overlayExcludes excludes from the overlay configuration, may be null + * @param additionalJars paths to add as excludes + * @return merged excludes array + */ + private static String[] mergeExcludes(String[] overlayExcludes, Set additionalJars) { + if (additionalJars.isEmpty()) { + return overlayExcludes; + } + if (overlayExcludes == null || overlayExcludes.length == 0) { + return additionalJars.toArray(new String[0]); + } + String[] merged = Arrays.copyOf(overlayExcludes, overlayExcludes.length + additionalJars.size()); + System.arraycopy( + additionalJars.toArray(new String[0]), 0, merged, overlayExcludes.length, additionalJars.size()); + return merged; } /** * Extracts the Maven artifactId from a jar filename following the * {@code artifactId-version(-classifier)?.jar} convention. * + *

Scans right-to-left for the last {@code -} followed by a digit, which correctly + * handles artifactIds that contain digits (e.g. {@code commons-lang3-3.20.0.jar}). + * * @param jarName the jar filename * @return the artifactId, or null if it cannot be determined */ @@ -194,16 +222,11 @@ private static String extractArtifactId(String jarName) { return null; } String baseName = jarName.substring(0, jarName.length() - 4); - int versionStart = -1; - for (int i = 0; i < baseName.length(); i++) { - if (Character.isDigit(baseName.charAt(i))) { - versionStart = i; - break; + for (int i = baseName.length() - 2; i >= 0; i--) { + if (baseName.charAt(i) == '-' && Character.isDigit(baseName.charAt(i + 1))) { + return baseName.substring(0, i); } } - if (versionStart > 0 && baseName.charAt(versionStart - 1) == '-') { - return baseName.substring(0, versionStart - 1); - } return null; } } From fc54f70df1b940890e51c2a75fc90e6fa11096e6 Mon Sep 17 00:00:00 2001 From: Elliotte Rusty Harold Date: Tue, 28 Jul 2026 12:39:12 +0000 Subject: [PATCH 10/10] Address Copilot review: groupId:artifactId matching via jar metadata --- .../war/packaging/OverlayPackagingTask.java | 59 ++++++++++++++++--- 1 file changed, 52 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/apache/maven/plugins/war/packaging/OverlayPackagingTask.java b/src/main/java/org/apache/maven/plugins/war/packaging/OverlayPackagingTask.java index 2ec2af82..51059c4e 100644 --- a/src/main/java/org/apache/maven/plugins/war/packaging/OverlayPackagingTask.java +++ b/src/main/java/org/apache/maven/plugins/war/packaging/OverlayPackagingTask.java @@ -20,10 +20,15 @@ import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.util.Arrays; import java.util.Collections; +import java.util.Enumeration; import java.util.HashSet; +import java.util.Properties; import java.util.Set; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; import org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.resolver.filter.ScopeArtifactFilter; @@ -139,9 +144,10 @@ protected File getOverlayTempDirectory(WarPackagingContext context, Overlay over } /** - * Identifies jars from the overlay's {@code WEB-INF/lib} whose artifactId matches a non-optional - * runtime-scope dependency resolved by the project. These jars are candidates for exclusion - * from the overlay copy so that only the project-resolved version ends up in {@code WEB-INF/lib}. + * Identifies jars from the overlay's {@code WEB-INF/lib} whose groupId:artifactId matches a + * non-optional runtime-scope dependency resolved by the project. When the overlay jar contains + * {@code META-INF/maven/**\/pom.properties}, the groupId is read from that metadata for a precise + * match. Otherwise, matching falls back to artifactId only. * *

The overlay's cached unpack directory is not mutated; the returned set is used * as additional excludes during the copy step. @@ -157,14 +163,16 @@ private Set computeConflictingJars(WarPackagingContext context, File ove } ScopeArtifactFilter filter = new ScopeArtifactFilter(Artifact.SCOPE_RUNTIME); - Set projectArtifactIds = new HashSet<>(); + Set projectArtifactKeys = new HashSet<>(); + Set projectFallbackIds = new HashSet<>(); for (Artifact artifact : context.getProject().getArtifacts()) { if (!artifact.isOptional() && filter.include(artifact) && "jar".equals(artifact.getType())) { - projectArtifactIds.add(artifact.getArtifactId()); + projectArtifactKeys.add(artifact.getGroupId() + ":" + artifact.getArtifactId()); + projectFallbackIds.add(artifact.getArtifactId()); } } - if (projectArtifactIds.isEmpty()) { + if (projectArtifactKeys.isEmpty()) { return Collections.emptySet(); } @@ -177,7 +185,14 @@ private Set computeConflictingJars(WarPackagingContext context, File ove for (File overlayJar : overlayJars) { String jarName = overlayJar.getName(); String artifactId = extractArtifactId(jarName); - if (artifactId != null && projectArtifactIds.contains(artifactId)) { + if (artifactId == null) { + continue; + } + + String groupId = getJarGroupId(overlayJar); + String key = (groupId != null) ? groupId + ":" + artifactId : artifactId; + if ((groupId != null && projectArtifactKeys.contains(key)) + || (groupId == null && projectFallbackIds.contains(artifactId))) { context.getLog() .debug("Excluding dependency [" + jarName + "] from overlay [" + overlay.getId() + "]; project runtime dependencies already include a version"); @@ -187,6 +202,33 @@ private Set computeConflictingJars(WarPackagingContext context, File ove return conflicting; } + /** + * Reads the Maven groupId from a jar's {@code META-INF/maven/**\/pom.properties} metadata, if + * present. + * + * @param jarFile the jar file + * @return the groupId, or null if it could not be determined + */ + private static String getJarGroupId(File jarFile) { + try (ZipFile zip = new ZipFile(jarFile)) { + Enumeration entries = zip.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + String name = entry.getName(); + if (name.startsWith("META-INF/maven/") && name.endsWith("/pom.properties")) { + Properties props = new Properties(); + try (InputStream is = zip.getInputStream(entry)) { + props.load(is); + } + return props.getProperty("groupId"); + } + } + } catch (IOException e) { + // groupId not available; fall back to artifactId-only matching + } + return null; + } + /** * Merges the overlay's existing excludes with additional jar filenames to exclude. * @@ -214,6 +256,9 @@ private static String[] mergeExcludes(String[] overlayExcludes, Set addi *

Scans right-to-left for the last {@code -} followed by a digit, which correctly * handles artifactIds that contain digits (e.g. {@code commons-lang3-3.20.0.jar}). * + *

The extracted artifactId is used together with the groupId obtained from + * {@link #getJarGroupId(File)} for precise {@code groupId:artifactId} matching. + * * @param jarName the jar filename * @return the artifactId, or null if it cannot be determined */