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..6f847f9a 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; @@ -58,6 +63,7 @@ import org.codehaus.plexus.archiver.jar.JarArchiver; import org.codehaus.plexus.archiver.manager.ArchiverManager; import org.codehaus.plexus.archiver.manager.NoSuchArchiverException; +import org.codehaus.plexus.interpolation.InterpolationException; /** * Contains common jobs for WAR mojos. @@ -655,6 +661,10 @@ private class DefaultWarPackagingContext implements WarPackagingContext { outdatedCheckPath = outdatedCheckPath.replace('/', '\\'); } } + // MWAR-443: Compute the expected file names of non-runtime-scope library artifacts + // so files placed by other plugins (e.g., maven-dependency-plugin) are not marked + // as outdated and subsequently deleted. + final Set nonRuntimeArtifactFileNames = getNonRuntimeArtifactFileNames(); Files.walkFileTree(webappDirectory.toPath(), new SimpleFileVisitor() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { @@ -665,7 +675,17 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO .relativize(file) .toString(); if (checkAllPathsForOutdated() || path.startsWith(outdatedCheckPath)) { - outdatedResources.add(path); + // MWAR-443: Files under the artifact lib directory that match a + // non-runtime-scope artifact may have been placed by another plugin + // (e.g., maven-dependency-plugin), so do not mark them as outdated. + // Stale files that are not part of the current project (e.g. outdated + // versions of runtime artifacts) are still removed. + String normalizedPath = path.replace('\\', '/'); + if (!normalizedPath.startsWith(AbstractWarPackagingTask.LIB_PATH) + || !nonRuntimeArtifactFileNames.contains( + file.toFile().getName())) { + outdatedResources.add(path); + } } } return super.visitFile(file, attrs); @@ -682,6 +702,31 @@ protected boolean checkAllPathsForOutdated() { return outdatedCheckPath.equals("/"); } + /** + * Returns the set of expected target filenames in {@code WEB-INF/lib/} for non-runtime-scope library + * 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 getNonRuntimeArtifactFileNames() { + Set fileNames = new HashSet<>(); + ScopeArtifactFilter filter = new ScopeArtifactFilter(Artifact.SCOPE_RUNTIME); + if (project.getArtifacts() != null) { + for (Artifact artifact : project.getArtifacts()) { + if (!filter.include(artifact) && AbstractWarPackagingTask.isLibraryType(artifact.getType())) { + try { + String targetFileName = AbstractWarPackagingTask.evaluateFileNameMapping( + getOutputFileNameMapping(), artifact); + fileNames.add( + AbstractWarPackagingTask.getLibraryFileName(artifact.getType(), targetFileName)); + } catch (InterpolationException 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..a7149894 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 @@ -428,8 +428,22 @@ protected PathSet getFilesToIncludes( */ protected String getArtifactFinalName(WarPackagingContext context, Artifact artifact) throws InterpolationException { - if (context.getOutputFileNameMapping() != null) { - return MappingUtils.evaluateFileNameMapping(context.getOutputFileNameMapping(), artifact); + return evaluateFileNameMapping(context.getOutputFileNameMapping(), artifact); + } + + /** + * Evaluates the file name mapping for the given artifact, using the {@code outputFileNameMapping} when set, + * or the standard naming scheme otherwise. + * + * @param outputFileNameMapping the output file name mapping, may be {@code null} + * @param artifact the artifact + * @return the converted filename of the artifact + * @throws InterpolationException in case of interpolation problem + */ + public static String evaluateFileNameMapping(String outputFileNameMapping, Artifact artifact) + throws InterpolationException { + if (outputFileNameMapping != null) { + return MappingUtils.evaluateFileNameMapping(outputFileNameMapping, artifact); } String classifier = artifact.getClassifier(); @@ -496,4 +510,39 @@ 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); + } + + /** + * Returns the file name the given artifact would have in {@code WEB-INF/lib/}, converting the extension + * for artifact types that are packaged under a different extension (e.g., {@code par} files are packaged + * as {@code .jar}). + * + * @param type the artifact type + * @param targetFileName the mapped file name of the artifact + * @return the file name the artifact will have in {@code WEB-INF/lib/} + */ + public static String getLibraryFileName(String type, String targetFileName) { + if ("par".equals(type)) { + int extensionIndex = targetFileName.lastIndexOf('.'); + if (extensionIndex > 0) { + return targetFileName.substring(0, extensionIndex) + ".jar"; + } + return targetFileName + ".jar"; + } + return targetFileName; + } } 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..6d00a3df 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,15 +98,12 @@ 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"; - copyFile(id, context, artifact.getFile(), LIB_PATH + targetFileName); + } else if (isLibraryType(type)) { + copyFile( + id, + context, + artifact.getFile(), + LIB_PATH + getLibraryFileName(type, targetFileName)); } else if ("war".equals(type)) { // Nothing to do here, it is an overlay and it's already handled context.getLog() 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..c964cb70 100644 --- a/src/test/java/org/apache/maven/plugins/war/WarExplodedMojoTest.java +++ b/src/test/java/org/apache/maven/plugins/war/WarExplodedMojoTest.java @@ -18,15 +18,20 @@ */ 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; import org.apache.maven.api.plugin.testing.MojoExtension; import org.apache.maven.api.plugin.testing.MojoParameter; import org.apache.maven.api.plugin.testing.MojoTest; +import org.apache.maven.artifact.Artifact; 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 +55,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 +943,107 @@ 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, + * while stale files not part of the current project are still removed. + */ + @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()); + + // A provided-scope library artifact is not copied by the WAR plugin itself, but may + // have been placed in WEB-INF/lib by another plugin (e.g., maven-dependency-plugin). + MavenProjectArtifactsStub project = new MavenProjectArtifactsStub(); + JarArtifactStub providedArtifact = new JarArtifactStub(getBasedir(), new DefaultArtifactHandler("jar")); + providedArtifact.setScope(Artifact.SCOPE_PROVIDED); + providedArtifact.setArtifactId("derbyLocale_cs"); + providedArtifact.setVersion("10.14.2.0"); + project.addArtifact(providedArtifact); + mojo.setProject(project); + + File webAppDirectory = mojo.getWebappDirectory(); + File libDir = new File(webAppDirectory, "WEB-INF/lib"); + // file placed by maven-dependency-plugin for the provided-scope artifact, older than the session + File providedJar = new File(libDir, "derbyLocale_cs-10.14.2.0.jar"); + // stale file from a previous build that is not part of the current project + File staleRuntimeJar = new File(libDir, "stale-runtime-1.0.jar"); + try { + libDir.mkdirs(); + providedJar.createNewFile(); + staleRuntimeJar.createNewFile(); + // Set timestamps to the past (before session start) + providedJar.setLastModified(0L); + staleRuntimeJar.setLastModified(0L); + + mojo.execute(); + + // The file placed by another plugin should NOT be deleted + assertTrue(providedJar.exists(), "provided-scope artifact should not be deleted by WAR plugin"); + // Stale files that are not part of the current project are still removed + assertFalse(staleRuntimeJar.exists(), "stale runtime artifact should be deleted by WAR plugin"); + } finally { + FileUtils.deleteDirectory(webAppDirectory); + } + } + + /** + * Test for MWAR-443: With an {@code outputFileNameMapping} configured, files placed by + * maven-dependency-plugin 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/MWAR443TestStripVersion") + @MojoParameter(name = "outdatedCheckPath", value = "WEB-INF/lib/") + @MojoParameter(name = "outputFileNameMapping", value = "@{artifactId}@.@{extension}@") + @Test + public void testProvidedScopeArtifactPlacedByDependencyPluginWithFileNameMappingShouldNotBeDeleted( + WarExplodedMojo mojo) throws Exception { + when(mavenSession.getStartTime()).thenReturn(new Date()); + + MavenProjectArtifactsStub project = new MavenProjectArtifactsStub(); + JarArtifactStub providedArtifact = new JarArtifactStub(getBasedir(), new DefaultArtifactHandler("jar")); + providedArtifact.setScope(Artifact.SCOPE_PROVIDED); + providedArtifact.setArtifactId("derbyLocale_cs"); + providedArtifact.setVersion("10.14.2.0"); + project.addArtifact(providedArtifact); + mojo.setProject(project); + + File webAppDirectory = mojo.getWebappDirectory(); + File libDir = new File(webAppDirectory, "WEB-INF/lib"); + // with stripVersion-style mapping the dependency-plugin places the file without the version + File providedJar = new File(libDir, "derbyLocale_cs.jar"); + try { + libDir.mkdirs(); + providedJar.createNewFile(); + // Set timestamp to the past (before session start) + providedJar.setLastModified(0L); + + mojo.execute(); + + // The file placed by another plugin should NOT be deleted + assertTrue(providedJar.exists(), "provided-scope artifact should not be deleted by WAR plugin"); + } finally { + FileUtils.deleteDirectory(webAppDirectory); + } + } }