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 extends ZipEntry> 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;
+ }
}