You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by gn...@apache.org on 2026/07/24 21:08:02 UTC

(maven-war-plugin) 01/01: Upgrade to Maven 4.0.0-rc-5 and fix IT compatibility issues

This is an automated email from the ASF dual-hosted git repository.

gnodet pushed a commit to branch 4.0.x
in repository https://gitbox.apache.org/repos/asf/maven-war-plugin.git

commit a3ac494d4aefeacc45fe3aed607b5df5281bf4d8
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Fri Jul 24 23:07:49 2026 +0200

    Upgrade to Maven 4.0.0-rc-5 and fix IT compatibility issues
    
    - Upgrade mavenVersion from rc-4 to rc-5
    - Add Providers.java with DI bindings for ProjectManager, BuildContext
      (SimpleBuildContext impl), and ArchiverManager
    - Fix ${basedir} → ${project.basedir} for warSourceDirectory default
    - Fix Overlay excludes/includes parsing for Maven 4 config parser by
      removing ambiguous String setter overloads
    - Rewrite OverlayManager dependency resolution to walk the dependency
      tree instead of using flattenDependencies (which filters non-classpath
      types like war)
    - Add null safety for collectDependencies return values
    - Update test mocks to set up tree walking (getChildren) in addition
      to flattenDependencies
    - All 64 unit tests and 35 integration tests pass with Maven 4.0.0-rc-5
    
    Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
---
 .gitignore                                         |   1 +
 pom.xml                                            |   4 +-
 .../apache/maven/plugins/war/AbstractWarMojo.java  |   2 +-
 .../java/org/apache/maven/plugins/war/Overlay.java |  27 ----
 .../org/apache/maven/plugins/war/Providers.java    | 171 +++++++++++++++++++++
 .../maven/plugins/war/overlay/OverlayManager.java  |  64 ++++----
 .../apache/maven/plugins/war/WarOverlaysTest.java  |  18 +--
 .../plugins/war/overlay/OverlayManagerTest.java    |  10 +-
 .../maven/plugins/war/stub/MockSessionHelper.java  |   5 +
 9 files changed, 234 insertions(+), 68 deletions(-)

diff --git a/.gitignore b/.gitignore
index 45fe8c8..8f0ba66 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,3 +16,4 @@ out/
 .checkstyle
 *.versionsBackup
 
+.claude/
diff --git a/pom.xml b/pom.xml
index 672f9ee..9456c37 100644
--- a/pom.xml
+++ b/pom.xml
@@ -77,7 +77,7 @@
   <properties>
     <mavenArchiverVersion>4.0.0-beta-5</mavenArchiverVersion>
     <mavenFilteringVersion>4.0.0-beta-1</mavenFilteringVersion>
-    <mavenVersion>4.0.0-rc-4</mavenVersion>
+    <mavenVersion>4.0.0-rc-5</mavenVersion>
     <javaVersion>17</javaVersion>
     <guiceVersion>5.1.0</guiceVersion>
     <plexusArchiverVersion>4.12.0</plexusArchiverVersion>
@@ -202,6 +202,8 @@
               -->
               <exclude>src/it/MWAR-167/src/main/resources/MANIFEST.MF</exclude>
               <exclude>src/test/resources/**</exclude>
+              <!-- Created by unit tests (zip overlay) at project root -->
+              <exclude>zipGroupId/**</exclude>
             </excludes>
           </configuration>
         </plugin>
diff --git a/src/main/java/org/apache/maven/plugins/war/AbstractWarMojo.java b/src/main/java/org/apache/maven/plugins/war/AbstractWarMojo.java
index f251842..c1046d9 100644
--- a/src/main/java/org/apache/maven/plugins/war/AbstractWarMojo.java
+++ b/src/main/java/org/apache/maven/plugins/war/AbstractWarMojo.java
@@ -135,7 +135,7 @@ public abstract class AbstractWarMojo implements Mojo {
     /**
      * Single directory for extra files to include in the WAR. This is where you place your JSP files.
      */
-    @Parameter(defaultValue = "${basedir}/src/main/webapp", required = true)
+    @Parameter(defaultValue = "${project.basedir}/src/main/webapp", required = true)
     private File warSourceDirectory;
 
     /**
diff --git a/src/main/java/org/apache/maven/plugins/war/Overlay.java b/src/main/java/org/apache/maven/plugins/war/Overlay.java
index 68c3a42..bb2f8cb 100644
--- a/src/main/java/org/apache/maven/plugins/war/Overlay.java
+++ b/src/main/java/org/apache/maven/plugins/war/Overlay.java
@@ -18,9 +18,7 @@
  */
 package org.apache.maven.plugins.war;
 
-import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.List;
 
 import org.apache.maven.api.DownloadedArtifact;
 
@@ -180,13 +178,6 @@ public class Overlay {
         return includes;
     }
 
-    /**
-     * @param includes {@link #includes}
-     */
-    public void setIncludes(String includes) {
-        this.includes = parse(includes);
-    }
-
     /**
      * @param includes {@link #includes}
      */
@@ -201,13 +192,6 @@ public class Overlay {
         return excludes;
     }
 
-    /**
-     * @param excludes {@link #excludes}
-     */
-    public void setExcludes(String excludes) {
-        this.excludes = parse(excludes);
-    }
-
     /**
      * @param excludes {@link #excludes}
      */
@@ -322,15 +306,4 @@ public class Overlay {
         result = 31 * result + (excludes != null ? Arrays.hashCode(excludes) : 0);
         return result;
     }
-
-    private String[] parse(String s) {
-        final List<String> result = new ArrayList<>();
-        if (s != null) {
-            String[] tokens = s.split(",");
-            for (String token : tokens) {
-                result.add(token.trim());
-            }
-        }
-        return result.toArray(new String[0]);
-    }
 }
diff --git a/src/main/java/org/apache/maven/plugins/war/Providers.java b/src/main/java/org/apache/maven/plugins/war/Providers.java
new file mode 100644
index 0000000..58615c9
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugins/war/Providers.java
@@ -0,0 +1,171 @@
+/*
+ * 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.
+ */
+package org.apache.maven.plugins.war;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.maven.api.Session;
+import org.apache.maven.api.di.Named;
+import org.apache.maven.api.di.Provides;
+import org.apache.maven.api.services.ProjectManager;
+import org.codehaus.plexus.archiver.Archiver;
+import org.codehaus.plexus.archiver.UnArchiver;
+import org.codehaus.plexus.archiver.jar.JarArchiver;
+import org.codehaus.plexus.archiver.jar.JarUnArchiver;
+import org.codehaus.plexus.archiver.manager.ArchiverManager;
+import org.codehaus.plexus.archiver.manager.DefaultArchiverManager;
+import org.codehaus.plexus.archiver.war.WarArchiver;
+import org.codehaus.plexus.archiver.war.WarUnArchiver;
+import org.codehaus.plexus.archiver.zip.ZipArchiver;
+import org.codehaus.plexus.archiver.zip.ZipUnArchiver;
+import org.codehaus.plexus.util.DirectoryScanner;
+import org.codehaus.plexus.util.Scanner;
+import org.sonatype.plexus.build.incremental.BuildContext;
+
+/**
+ * DI provider bindings for services not directly available in the Maven 4 plugin DI container.
+ */
+@Named
+class Providers {
+
+    @Provides
+    static ProjectManager projectManager(Session session) {
+        return session.getService(ProjectManager.class);
+    }
+
+    @Provides
+    static BuildContext buildContext() {
+        return new SimpleBuildContext();
+    }
+
+    @Provides
+    static ArchiverManager archiverManager() {
+        Map<String, javax.inject.Provider<Archiver>> archivers = new HashMap<>();
+        archivers.put("jar", JarArchiver::new);
+        archivers.put("war", WarArchiver::new);
+        archivers.put("zip", ZipArchiver::new);
+
+        Map<String, javax.inject.Provider<UnArchiver>> unarchivers = new HashMap<>();
+        unarchivers.put("jar", JarUnArchiver::new);
+        unarchivers.put("war", WarUnArchiver::new);
+        unarchivers.put("zip", ZipUnArchiver::new);
+
+        return new DefaultArchiverManager(archivers, unarchivers, new HashMap<>());
+    }
+
+    /**
+     * A simple {@link BuildContext} implementation that does not depend on
+     * {@code plexus-container-default} (which is unavailable in Maven 4).
+     */
+    static class SimpleBuildContext implements BuildContext {
+
+        @Override
+        public boolean hasDelta(String relpath) {
+            return true;
+        }
+
+        @Override
+        public boolean hasDelta(File file) {
+            return true;
+        }
+
+        @Override
+        @SuppressWarnings("rawtypes")
+        public boolean hasDelta(List relpaths) {
+            return true;
+        }
+
+        @Override
+        public void refresh(File file) {
+            // no-op
+        }
+
+        @Override
+        public OutputStream newFileOutputStream(File file) throws IOException {
+            return Files.newOutputStream(file.toPath());
+        }
+
+        @Override
+        public Scanner newScanner(File basedir) {
+            DirectoryScanner scanner = new DirectoryScanner();
+            scanner.setBasedir(basedir);
+            return scanner;
+        }
+
+        @Override
+        public Scanner newDeleteScanner(File basedir) {
+            return newScanner(basedir);
+        }
+
+        @Override
+        public Scanner newScanner(File basedir, boolean ignoreDelta) {
+            return newScanner(basedir);
+        }
+
+        @Override
+        public boolean isIncremental() {
+            return false;
+        }
+
+        @Override
+        public void setValue(String key, Object value) {
+            // no-op
+        }
+
+        @Override
+        public Object getValue(String key) {
+            return null;
+        }
+
+        @Override
+        public void addWarning(File file, int line, int column, String message, Throwable cause) {
+            // no-op
+        }
+
+        @Override
+        public void addError(File file, int line, int column, String message, Throwable cause) {
+            // no-op
+        }
+
+        @Override
+        public void addMessage(File file, int line, int column, String message, int severity, Throwable cause) {
+            // no-op
+        }
+
+        @Override
+        public void removeMessages(File file) {
+            // no-op
+        }
+
+        @Override
+        public boolean isUptodate(File output, File input) {
+            return output != null
+                    && output.exists()
+                    && input != null
+                    && input.exists()
+                    && output.lastModified() > input.lastModified();
+        }
+    }
+}
diff --git a/src/main/java/org/apache/maven/plugins/war/overlay/OverlayManager.java b/src/main/java/org/apache/maven/plugins/war/overlay/OverlayManager.java
index 752645d..0ca7e34 100644
--- a/src/main/java/org/apache/maven/plugins/war/overlay/OverlayManager.java
+++ b/src/main/java/org/apache/maven/plugins/war/overlay/OverlayManager.java
@@ -204,12 +204,13 @@ public class OverlayManager {
      * @return boolean true if equals
      */
     private boolean compareOverlayWithArtifact(Overlay overlay, DownloadedArtifact artifact) {
+        String oc = overlay.getClassifier() == null ? "" : overlay.getClassifier();
+        String ac = artifact.getClassifier() == null ? "" : artifact.getClassifier();
         return (Objects.equals(overlay.getGroupId(), artifact.getGroupId())
                 && Objects.equals(overlay.getArtifactId(), artifact.getArtifactId())
                 && Objects.equals(overlay.getType(), artifact.getExtension())
                 // MWAR-241 Make sure to treat null and "" as equal when comparing the classifier
-                && Objects.equals(
-                        Objects.toString(overlay.getClassifier()), Objects.toString(artifact.getClassifier())));
+                && Objects.equals(oc, ac));
     }
 
     /**
@@ -219,21 +220,9 @@ public class OverlayManager {
      */
     private List<DownloadedArtifact> getOverlaysAsArtifacts() {
         final List<DownloadedArtifact> result = new ArrayList<>();
-        try {
-            Node root = session.collectDependencies(project, PathScope.MAIN_RUNTIME);
-            List<Node> nodes = session.flattenDependencies(root, PathScope.MAIN_RUNTIME);
-            for (Node node : nodes) {
-                org.apache.maven.api.Dependency dep = node.getDependency();
-                if (dep != null
-                        && !dep.isOptional()
-                        && isRuntimeScope(dep.getScope())
-                        && "war".equals(dep.getExtension())) {
-                    DownloadedArtifact resolved = session.resolveArtifact(dep);
-                    result.add(resolved);
-                }
-            }
-        } catch (Exception e) {
-            // If dependency resolution fails, return empty list
+        Node root = session.collectDependencies(project, PathScope.MAIN_RUNTIME);
+        if (root != null) {
+            collectArtifacts(root, result, true);
         }
         return result;
     }
@@ -243,20 +232,43 @@ public class OverlayManager {
      */
     private List<DownloadedArtifact> getAllResolvedArtifacts() {
         final List<DownloadedArtifact> result = new ArrayList<>();
-        try {
-            Node root = session.collectDependencies(project, PathScope.MAIN_RUNTIME);
-            List<Node> nodes = session.flattenDependencies(root, PathScope.MAIN_RUNTIME);
-            for (Node node : nodes) {
-                org.apache.maven.api.Dependency dep = node.getDependency();
-                if (dep != null) {
+        Node root = session.collectDependencies(project, PathScope.MAIN_RUNTIME);
+        if (root != null) {
+            collectArtifacts(root, result, false);
+        }
+        return result;
+    }
+
+    /**
+     * Walks the dependency tree and collects resolved artifacts.
+     * Uses manual tree walking instead of {@code flattenDependencies} because the latter
+     * filters out non-classpath types (like war), which we need for overlay resolution.
+     *
+     * @param node the root node
+     * @param result the list to collect into
+     * @param warOnly if true, only collect WAR artifacts with runtime-compatible scope
+     */
+    private void collectArtifacts(Node node, List<DownloadedArtifact> result, boolean warOnly) {
+        org.apache.maven.api.Dependency dep = node.getDependency();
+        if (dep != null) {
+            boolean include = !warOnly
+                    || (!dep.isOptional() && isRuntimeScope(dep.getScope()) && "war".equals(dep.getExtension()));
+            if (include) {
+                try {
                     DownloadedArtifact resolved = session.resolveArtifact(dep);
                     result.add(resolved);
+                } catch (Exception e) {
+                    // Skip unresolvable artifacts
+                }
+            }
+        }
+        if (node.getChildren() != null) {
+            for (Node child : node.getChildren()) {
+                if (child != null) {
+                    collectArtifacts(child, result, warOnly);
                 }
             }
-        } catch (Exception e) {
-            // If dependency resolution fails, return empty list
         }
-        return result;
     }
 
     /**
diff --git a/src/test/java/org/apache/maven/plugins/war/WarOverlaysTest.java b/src/test/java/org/apache/maven/plugins/war/WarOverlaysTest.java
index abe69e6..4973912 100644
--- a/src/test/java/org/apache/maven/plugins/war/WarOverlaysTest.java
+++ b/src/test/java/org/apache/maven/plugins/war/WarOverlaysTest.java
@@ -405,14 +405,14 @@ public class WarOverlaysTest {
         mojo.setProject(project);
 
         Overlay over1 = new DefaultOverlay(overlay3);
-        over1.setExcludes("**/a.*,**/c.*,**/*.xml");
+        over1.setExcludes(new String[] {"**/a.*", "**/c.*", "**/*.xml"});
 
         Overlay over2 = new DefaultOverlay(overlay1);
-        over2.setIncludes("jsp/d/*");
-        over2.setExcludes("jsp/d/a.jsp");
+        over2.setIncludes(new String[] {"jsp/d/*"});
+        over2.setExcludes(new String[] {"jsp/d/a.jsp"});
 
         Overlay over3 = new DefaultOverlay(overlay3);
-        over3.setIncludes("**/*.jsp");
+        over3.setIncludes(new String[] {"**/*.jsp"});
 
         Overlay over4 = new DefaultOverlay(overlay2);
 
@@ -493,15 +493,15 @@ public class WarOverlaysTest {
         mojo.setProject(project);
 
         Overlay over1 = new DefaultOverlay(overlay3);
-        over1.setExcludes("**/a.*,**/c.*,**/*.xml,jsp/b.jsp");
+        over1.setExcludes(new String[] {"**/a.*", "**/c.*", "**/*.xml", "jsp/b.jsp"});
 
         Overlay over2 = new DefaultOverlay(overlay1);
-        over2.setIncludes("jsp/d/*");
-        over2.setExcludes("jsp/d/a.jsp");
+        over2.setIncludes(new String[] {"jsp/d/*"});
+        over2.setExcludes(new String[] {"jsp/d/a.jsp"});
 
         Overlay over3 = new DefaultOverlay(overlay3);
-        over3.setIncludes("**/*.jsp");
-        over3.setExcludes("jsp/b.jsp");
+        over3.setIncludes(new String[] {"**/*.jsp"});
+        over3.setExcludes(new String[] {"jsp/b.jsp"});
 
         Overlay over4 = new DefaultOverlay(overlay2);
 
diff --git a/src/test/java/org/apache/maven/plugins/war/overlay/OverlayManagerTest.java b/src/test/java/org/apache/maven/plugins/war/overlay/OverlayManagerTest.java
index 4966f90..73a83da 100644
--- a/src/test/java/org/apache/maven/plugins/war/overlay/OverlayManagerTest.java
+++ b/src/test/java/org/apache/maven/plugins/war/overlay/OverlayManagerTest.java
@@ -155,6 +155,7 @@ class OverlayManagerTest {
 
     /**
      * Creates a mock Session that returns the given artifacts as resolved war dependencies.
+     * The root node's children are set up for tree walking (used by OverlayManager).
      */
     @SuppressWarnings("unchecked")
     private Session createMockSession(List<? extends DownloadedArtifact> artifacts) {
@@ -165,7 +166,7 @@ class OverlayManagerTest {
             when(session.collectDependencies(any(Project.class), any(PathScope.class)))
                     .thenReturn(rootNode);
 
-            List<Node> nodes = new ArrayList<>();
+            List<Node> childNodes = new ArrayList<>();
             for (DownloadedArtifact artifact : artifacts) {
                 Node node = mock(Node.class);
                 org.apache.maven.api.Dependency dep = mock(org.apache.maven.api.Dependency.class);
@@ -176,12 +177,13 @@ class OverlayManagerTest {
                 when(dep.getScope()).thenReturn(DependencyScope.COMPILE);
                 when(dep.isOptional()).thenReturn(false);
                 when(node.getDependency()).thenReturn(dep);
+                when(node.getChildren()).thenReturn(Collections.emptyList());
                 when(session.resolveArtifact(eq(dep))).thenReturn(artifact);
-                nodes.add(node);
+                childNodes.add(node);
             }
 
-            when(session.flattenDependencies(eq(rootNode), any(PathScope.class)))
-                    .thenReturn((List) nodes);
+            // Set up root node's children for tree walking
+            when(rootNode.getChildren()).thenReturn(childNodes);
         } catch (Exception e) {
             throw new RuntimeException(e);
         }
diff --git a/src/test/java/org/apache/maven/plugins/war/stub/MockSessionHelper.java b/src/test/java/org/apache/maven/plugins/war/stub/MockSessionHelper.java
index fe048a6..f52c069 100644
--- a/src/test/java/org/apache/maven/plugins/war/stub/MockSessionHelper.java
+++ b/src/test/java/org/apache/maven/plugins/war/stub/MockSessionHelper.java
@@ -19,6 +19,7 @@
 package org.apache.maven.plugins.war.stub;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -79,12 +80,16 @@ public final class MockSessionHelper {
             lenient().when(dep.isOptional()).thenReturn(artifact.isOptional());
 
             lenient().when(node.getDependency()).thenReturn(dep);
+            lenient().when(node.getChildren()).thenReturn(Collections.emptyList());
 
             // Track this mock -> artifact mapping for resolveArtifact
             resolveMap.put(dep, artifact);
 
             nodes.add(node);
         }
+        // Set up root node's children for tree walking (used by OverlayManager)
+        lenient().when(rootNode.getChildren()).thenReturn(nodes);
+
         lenient()
                 .when(session.flattenDependencies(any(Node.class), any(PathScope.class)))
                 .thenReturn((List) nodes);