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 56b1e6c0..5b05e695 100644 --- a/src/main/java/org/apache/maven/plugins/war/AbstractWarMojo.java +++ b/src/main/java/org/apache/maven/plugins/war/AbstractWarMojo.java @@ -29,11 +29,15 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; import org.apache.maven.archiver.MavenArchiveConfiguration; +import org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager; +import org.apache.maven.artifact.resolver.filter.ScopeArtifactFilter; import org.apache.maven.execution.MavenSession; import org.apache.maven.model.Resource; import org.apache.maven.plugin.AbstractMojo; @@ -42,6 +46,7 @@ import org.apache.maven.plugin.logging.Log; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.war.overlay.OverlayManager; +import org.apache.maven.plugins.war.packaging.AbstractWarPackagingTask; import org.apache.maven.plugins.war.packaging.CopyUserManifestTask; import org.apache.maven.plugins.war.packaging.OverlayPackagingTask; import org.apache.maven.plugins.war.packaging.WarPackagingContext; @@ -54,6 +59,7 @@ import org.apache.maven.shared.filtering.MavenFilteringException; import org.apache.maven.shared.filtering.MavenResourcesExecution; import org.apache.maven.shared.filtering.MavenResourcesFiltering; +import org.apache.maven.shared.mapping.MappingUtils; import org.apache.maven.shared.utils.StringUtils; import org.codehaus.plexus.archiver.jar.JarArchiver; import org.codehaus.plexus.archiver.manager.ArchiverManager; @@ -655,6 +661,10 @@ private class DefaultWarPackagingContext implements WarPackagingContext { outdatedCheckPath = outdatedCheckPath.replace('/', '\\'); } } + // MWAR-443: Compute expected filenames for runtime-scope artifacts + // so we don't mark files placed by other plugins (e.g., maven-dependency-plugin) + // as outdated and subsequently delete them. + final Set runtimeArtifactFileNames = getRuntimeArtifactFileNames(); Files.walkFileTree(webappDirectory.toPath(), new SimpleFileVisitor() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { @@ -665,7 +675,19 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO .relativize(file) .toString(); if (checkAllPathsForOutdated() || path.startsWith(outdatedCheckPath)) { - outdatedResources.add(path); + // MWAR-443: For files under the artifact lib directory, + // only mark as outdated if they match a runtime-scope artifact + // that the WAR plugin would copy. This prevents deleting files + // placed by other plugins (e.g., maven-dependency-plugin) for + // non-runtime-scope dependencies. + String normalizedPath = path.replace('\\', '/'); + if (normalizedPath.startsWith("WEB-INF/lib/") + && !runtimeArtifactFileNames.contains( + file.toFile().getName())) { + // Skip: file was placed by another plugin, not managed by WAR plugin + } else { + outdatedResources.add(path); + } } } return super.visitFile(file, attrs); @@ -682,6 +704,47 @@ protected boolean checkAllPathsForOutdated() { return outdatedCheckPath.equals("/"); } + /** + * Returns the set of expected target filenames for runtime-scope artifacts. + * Used to avoid marking files placed by other plugins (e.g., maven-dependency-plugin) + * as outdated and subsequently deleting them (MWAR-443). + */ + private Set getRuntimeArtifactFileNames() { + Set fileNames = new HashSet<>(); + ScopeArtifactFilter filter = new ScopeArtifactFilter(Artifact.SCOPE_RUNTIME); + if (project.getArtifacts() != null) { + for (Artifact artifact : project.getArtifacts()) { + if (!artifact.isOptional() + && filter.include(artifact) + && AbstractWarPackagingTask.isLibraryType(artifact.getType())) { + try { + String targetFileName; + if (getOutputFileNameMapping() != null) { + targetFileName = + MappingUtils.evaluateFileNameMapping(getOutputFileNameMapping(), artifact); + } else { + String classifier = artifact.getClassifier(); + if (classifier != null && !classifier.trim().isEmpty()) { + targetFileName = MappingUtils.evaluateFileNameMapping( + MappingUtils.DEFAULT_FILE_NAME_MAPPING_CLASSIFIER, artifact); + } else { + targetFileName = MappingUtils.evaluateFileNameMapping( + MappingUtils.DEFAULT_FILE_NAME_MAPPING, artifact); + } + } + if ("par".equals(artifact.getType())) { + targetFileName = targetFileName.substring(0, targetFileName.lastIndexOf('.')) + ".jar"; + } + fileNames.add(targetFileName); + } catch (Exception e) { + getLog().debug("Could not compute expected filename for artifact: " + artifact, e); + } + } + } + } + return fileNames; + } + @Override public MavenProject getProject() { return project; diff --git a/src/main/java/org/apache/maven/plugins/war/packaging/AbstractWarPackagingTask.java b/src/main/java/org/apache/maven/plugins/war/packaging/AbstractWarPackagingTask.java index ee0fa8d1..e6cc23ca 100644 --- a/src/main/java/org/apache/maven/plugins/war/packaging/AbstractWarPackagingTask.java +++ b/src/main/java/org/apache/maven/plugins/war/packaging/AbstractWarPackagingTask.java @@ -346,6 +346,14 @@ protected boolean copyFile( FileUtils.copyFile(source.getCanonicalFile(), destination); // preserve timestamp destination.setLastModified(readAttributes.lastModifiedTime().toMillis()); + // normalize permissions: clear executable, set read-for-all, write-for-owner + // on all copied files (not just WEB-INF/lib jars) + boolean ok = destination.setExecutable(false, false); + ok &= destination.setReadable(true, false); + ok &= destination.setWritable(true, true); + if (!ok) { + context.getLog().debug("Could not normalize permissions for " + targetFilename); + } context.getLog().debug(" + " + targetFilename + " has been copied."); } return true; @@ -360,7 +368,8 @@ protected boolean copyFile( * @throws java.io.IOException if an error occurred while reading the file */ protected String getEncoding(File webXml) throws IOException { - try (XmlStreamReader xmlReader = new XmlStreamReader(webXml)) { + try (XmlStreamReader xmlReader = + XmlStreamReader.builder().setFile(webXml).get()) { return xmlReader.getEncoding(); } } @@ -496,4 +505,19 @@ private boolean isExcluded(String targetFilename, List packagingIncludes } return true; } + + /** + * Returns {@code true} if the artifact with the given type would be placed in {@code WEB-INF/lib/}. + * + * @param type the artifact type + * @return {@code true} if the type is a library type that goes to WEB-INF/lib + */ + public static boolean isLibraryType(String type) { + return "jar".equals(type) + || "ejb".equals(type) + || "ejb-client".equals(type) + || "test-jar".equals(type) + || "bundle".equals(type) + || "par".equals(type); + } } diff --git a/src/main/java/org/apache/maven/plugins/war/packaging/ArtifactsPackagingTask.java b/src/main/java/org/apache/maven/plugins/war/packaging/ArtifactsPackagingTask.java index df33c7e7..d4cf5e8f 100644 --- a/src/main/java/org/apache/maven/plugins/war/packaging/ArtifactsPackagingTask.java +++ b/src/main/java/org/apache/maven/plugins/war/packaging/ArtifactsPackagingTask.java @@ -98,14 +98,10 @@ public void performPackaging(WarPackagingContext context) throws MojoExecutionEx copyFile(id, context, artifact.getFile(), MODULES_PATH + targetFileName); } else if ("xar".equals(type)) { copyFile(id, context, artifact.getFile(), EXTENSIONS_PATH + targetFileName); - } else if ("jar".equals(type) - || "ejb".equals(type) - || "ejb-client".equals(type) - || "test-jar".equals(type) - || "bundle".equals(type)) { - copyFile(id, context, artifact.getFile(), LIB_PATH + targetFileName); - } else if ("par".equals(type)) { - targetFileName = targetFileName.substring(0, targetFileName.lastIndexOf('.')) + ".jar"; + } else if (isLibraryType(type)) { + if ("par".equals(type)) { + targetFileName = targetFileName.substring(0, targetFileName.lastIndexOf('.')) + ".jar"; + } copyFile(id, context, artifact.getFile(), LIB_PATH + targetFileName); } else if ("war".equals(type)) { // Nothing to do here, it is an overlay and it's already handled diff --git a/src/test/java/org/apache/maven/plugins/war/WarExplodedMojoTest.java b/src/test/java/org/apache/maven/plugins/war/WarExplodedMojoTest.java index fda13e50..0a884601 100644 --- a/src/test/java/org/apache/maven/plugins/war/WarExplodedMojoTest.java +++ b/src/test/java/org/apache/maven/plugins/war/WarExplodedMojoTest.java @@ -18,8 +18,11 @@ */ package org.apache.maven.plugins.war; +import javax.inject.Inject; + import java.io.File; import java.text.SimpleDateFormat; +import java.util.Date; import java.util.Locale; import org.apache.maven.api.plugin.testing.InjectMojo; @@ -27,6 +30,7 @@ import org.apache.maven.api.plugin.testing.MojoParameter; import org.apache.maven.api.plugin.testing.MojoTest; import org.apache.maven.artifact.handler.DefaultArtifactHandler; +import org.apache.maven.execution.MavenSession; import org.apache.maven.plugin.testing.stubs.ArtifactStub; import org.apache.maven.plugins.war.stub.AarArtifactStub; import org.apache.maven.plugins.war.stub.EJBArtifactStub; @@ -50,10 +54,14 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; @MojoTest public class WarExplodedMojoTest { + @Inject + private MavenSession mavenSession; + @InjectMojo(goal = "exploded", pom = "src/test/resources/unit/warexplodedmojo/plugin-config.xml") @MojoParameter( name = "classesDirectory", @@ -934,4 +942,47 @@ public void testExplodedWarWithOutputFileNameMappingAndDuplicateDependencies(War expectedEJBArtifact.delete(); expectedEJBDupArtifact.delete(); } + + /** + * Test for MWAR-443: Files placed by maven-dependency-plugin in WEB-INF/lib + * for provided-scope artifacts should not be deleted by the WAR plugin. + */ + @InjectMojo(goal = "exploded", pom = "src/test/resources/unit/warexplodedmojo/plugin-config.xml") + @MojoParameter( + name = "classesDirectory", + value = "target/test-classes/unit/warexplodedmojo/SimpleExplodedWar-test-data/classes/") + @MojoParameter( + name = "warSourceDirectory", + value = "target/test-classes/unit/warexplodedmojo/SimpleExplodedWar-test-data/source/") + @MojoParameter(name = "webappDirectory", value = "target/test-classes/unit/warexplodedmojo/MWAR443Test") + @MojoParameter(name = "outdatedCheckPath", value = "WEB-INF/lib/") + @Test + public void testProvidedScopeArtifactPlacedByDependencyPluginShouldNotBeDeleted(WarExplodedMojo mojo) + throws Exception { + // Ensure session has a start time so the outdated resource detection is active. + // Uses same pattern as WarExplodedMojoFilteringTest which also calls + // when(mavenSession.get...()).thenReturn(...) + when(mavenSession.getStartTime()).thenReturn(new Date()); + + File webAppDirectory = mojo.getWebappDirectory(); + File libDir = new File(webAppDirectory, "WEB-INF/lib"); + File providedJar = new File(libDir, "derbyLocale_cs-10.14.2.0.jar"); + try { + // Setup: Create a file in WEB-INF/lib with an old timestamp, + // simulating a file placed by maven-dependency-plugin for a provided-scope artifact + libDir.mkdirs(); + providedJar.createNewFile(); + // Set timestamp to something in the past (before session start) + providedJar.setLastModified(0L); + + mojo.execute(); + + // The file should NOT be deleted - it was placed by another plugin + assertTrue(providedJar.exists(), "provided-scope artifact should not be deleted by WAR plugin"); + } finally { + // Cleanup + providedJar.delete(); + libDir.delete(); + } + } }