diff --git a/src/it/MWAR-389/invoker.properties b/src/it/MWAR-389/invoker.properties new file mode 100644 index 00000000..3376b9e4 --- /dev/null +++ b/src/it/MWAR-389/invoker.properties @@ -0,0 +1,18 @@ +# 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 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..3b1a1e64 --- /dev/null +++ b/src/it/MWAR-389/main-war/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,24 @@ + + + + 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..3b1a1e64 --- /dev/null +++ b/src/it/MWAR-389/overlay-war/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,24 @@ + + + + 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..cd88e442 --- /dev/null +++ b/src/it/MWAR-389/verify.bsh @@ -0,0 +1,136 @@ +/* + * 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 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" ); + +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; + +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; +} + +// 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" ); + +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; + +for ( int i = 0; i < mainJars.length; i++ ) +{ + String jar = mainJars[i]; + if ( jar.startsWith( "plexus-utils-3.0.24" ) ) + { + mainHasOld = true; + } + if ( jar.startsWith( "plexus-utils-" + managedVersion ) ) + { + mainHasManaged = true; + } +} + +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.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 + return true; +} 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..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,7 +20,18 @@ 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; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.war.Overlay; import org.apache.maven.plugins.war.util.PathSet; @@ -61,8 +72,12 @@ public void performPackaging(WarPackagingContext context) throws MojoExecutionEx // Step1: Extract if necessary final File tmpDir = unpackOverlay(context, overlay); - // Step2: setup - final PathSet includes = getFilesToIncludes(tmpDir, overlay.getIncludes(), overlay.getExcludes()); + // Step1b: Identify jars from overlay that conflict with project dependencies + Set conflictingJars = computeConflictingJars(context, tmpDir); + + // 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()) { @@ -127,4 +142,136 @@ protected File getOverlayTempDirectory(WarPackagingContext context, Overlay over } return result; } + + /** + * 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. + * + * @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 Set computeConflictingJars(WarPackagingContext context, File overlayDir) { + File libDir = new File(overlayDir, "WEB-INF/lib"); + if (!libDir.isDirectory()) { + return Collections.emptySet(); + } + + ScopeArtifactFilter filter = new ScopeArtifactFilter(Artifact.SCOPE_RUNTIME); + Set projectArtifactKeys = new HashSet<>(); + Set projectFallbackIds = new HashSet<>(); + for (Artifact artifact : context.getProject().getArtifacts()) { + if (!artifact.isOptional() && filter.include(artifact) && "jar".equals(artifact.getType())) { + projectArtifactKeys.add(artifact.getGroupId() + ":" + artifact.getArtifactId()); + projectFallbackIds.add(artifact.getArtifactId()); + } + } + + if (projectArtifactKeys.isEmpty()) { + return Collections.emptySet(); + } + + File[] overlayJars = libDir.listFiles((dir, name) -> name.endsWith(".jar")); + if (overlayJars == null) { + return Collections.emptySet(); + } + + Set conflicting = new HashSet<>(); + for (File overlayJar : overlayJars) { + String jarName = overlayJar.getName(); + String artifactId = extractArtifactId(jarName); + 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"); + conflicting.add("WEB-INF/lib/" + jarName); + } + } + 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. + * + * @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}). + * + *

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 + */ + private static String extractArtifactId(String jarName) { + if (jarName == null || !jarName.endsWith(".jar")) { + return null; + } + String baseName = jarName.substring(0, jarName.length() - 4); + for (int i = baseName.length() - 2; i >= 0; i--) { + if (baseName.charAt(i) == '-' && Character.isDigit(baseName.charAt(i + 1))) { + return baseName.substring(0, i); + } + } + return null; + } }