Commit b790c827 authored by Madhura Bhave's avatar Madhura Bhave Committed by Phillip Webb

Apply exclusions to existing war entries

Update `RepackageMojo` and supporting classes so that `exclusions`
on the repackage goal apply to both the contributed libraries and any
existing jar entries already contained in the original war.

Prior to this commit, exclusions would apply to contributed jars (for
example, those in `WEB-INF/lib-provided`) but not jars that were
packaged directly into `WEB-INF/lib` by the war plugin

Fixes gh-15808
Co-authored-by: 's avatarPhillip Webb <pwebb@vmware.com>
parent 4d694dda
......@@ -198,7 +198,7 @@ abstract class ArchiveCommand extends OptionParsingCommand {
List<Library> libraries = new ArrayList<>();
for (URL dependency : dependencies) {
File file = new File(dependency.toURI());
libraries.add(new Library(file, getLibraryScope(file)));
libraries.add(new Library(null, file, getLibraryScope(file), null, false, false, true));
}
return libraries;
}
......@@ -256,7 +256,7 @@ abstract class ArchiveCommand extends OptionParsingCommand {
List<Library> libraries = new ArrayList<>();
for (MatchedResource entry : entries) {
if (entry.isRoot()) {
libraries.add(new Library(entry.getFile(), LibraryScope.COMPILE));
libraries.add(new Library(null, entry.getFile(), LibraryScope.COMPILE, null, false, false, true));
}
else {
writeClasspathEntry(writer, entry);
......
......@@ -24,6 +24,7 @@ import org.gradle.api.specs.Spec;
import org.springframework.boot.gradle.tasks.bundling.ResolvedDependencies.DependencyDescriptor;
import org.springframework.boot.loader.tools.Layer;
import org.springframework.boot.loader.tools.Library;
import org.springframework.boot.loader.tools.LibraryCoordinates;
/**
* Resolver backed by a {@link LayeredSpec} that provides the destination {@link Layer}
......@@ -77,9 +78,12 @@ class LayerResolver {
private Library asLibrary(FileCopyDetails details) {
File file = details.getFile();
DependencyDescriptor dependency = this.resolvedDependencies.find(file);
return (dependency != null)
? new Library(null, file, null, dependency.getCoordinates(), false, dependency.isProjectDependency())
: new Library(file, null);
if (dependency == null) {
return new Library(null, file, null, null, false, false, true);
}
LibraryCoordinates coordinates = dependency.getCoordinates();
boolean projectDependency = dependency.isProjectDependency();
return new Library(null, file, null, coordinates, false, projectDependency, true);
}
}
......@@ -30,6 +30,7 @@ import java.util.Collection;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Predicate;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarInputStream;
......@@ -88,23 +89,42 @@ public abstract class AbstractJarWriter implements LoaderClassesWriter {
* Write all entries from the specified jar file.
* @param jarFile the source jar file
* @throws IOException if the entries cannot be written
* @deprecated since 2.4.8 for removal in 2.6.0 in favor of
* {@link #writeEntries(JarFile, EntryTransformer, UnpackHandler, Predicate)}
*/
@Deprecated
public void writeEntries(JarFile jarFile) throws IOException {
writeEntries(jarFile, EntryTransformer.NONE, UnpackHandler.NEVER);
writeEntries(jarFile, EntryTransformer.NONE, UnpackHandler.NEVER, (entry) -> true);
}
final void writeEntries(JarFile jarFile, EntryTransformer entryTransformer, UnpackHandler unpackHandler)
throws IOException {
/**
* Write required entries from the specified jar file.
* @param jarFile the source jar file
* @param entryTransformer the entity transformer used to change the entry
* @param unpackHandler the unpack handler
* @param entryFilter a predicate used to filter the written entries
* @throws IOException if the entries cannot be written
* @since 2.4.8
*/
public void writeEntries(JarFile jarFile, EntryTransformer entryTransformer, UnpackHandler unpackHandler,
Predicate<JarEntry> entryFilter) throws IOException {
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarArchiveEntry entry = new JarArchiveEntry(entries.nextElement());
setUpEntry(jarFile, entry);
try (ZipHeaderPeekInputStream inputStream = new ZipHeaderPeekInputStream(jarFile.getInputStream(entry))) {
EntryWriter entryWriter = new InputStreamEntryWriter(inputStream);
JarArchiveEntry transformedEntry = entryTransformer.transform(entry);
if (transformedEntry != null) {
writeEntry(transformedEntry, entryWriter, unpackHandler, true);
}
JarEntry entry = entries.nextElement();
if (entryFilter.test(entry)) {
writeEntry(jarFile, entryTransformer, unpackHandler, new JarArchiveEntry(entry));
}
}
}
private void writeEntry(JarFile jarFile, EntryTransformer entryTransformer, UnpackHandler unpackHandler,
JarArchiveEntry entry) throws IOException {
setUpEntry(jarFile, entry);
try (ZipHeaderPeekInputStream inputStream = new ZipHeaderPeekInputStream(jarFile.getInputStream(entry))) {
EntryWriter entryWriter = new InputStreamEntryWriter(inputStream);
JarArchiveEntry transformedEntry = entryTransformer.transform(entry);
if (transformedEntry != null) {
writeEntry(transformedEntry, entryWriter, unpackHandler, true);
}
}
}
......
......@@ -42,7 +42,7 @@ public class JarModeLibrary extends Library {
}
public JarModeLibrary(LibraryCoordinates coordinates) {
super(getJarName(coordinates), null, LibraryScope.RUNTIME, coordinates, false);
super(getJarName(coordinates), null, LibraryScope.RUNTIME, coordinates, false, false, true);
}
private static LibraryCoordinates createCoordinates(String artifactId) {
......
......@@ -43,11 +43,16 @@ public class Library {
private final boolean local;
private final boolean included;
/**
* Create a new {@link Library}.
* @param file the source file
* @param scope the scope of the library
* @deprecated since 2.4.8 for removal in 2.6.0 in favor of
* {@link #Library(String, File, LibraryScope, LibraryCoordinates, boolean, boolean, boolean)}
*/
@Deprecated
public Library(File file, LibraryScope scope) {
this(file, scope, false);
}
......@@ -57,7 +62,10 @@ public class Library {
* @param file the source file
* @param scope the scope of the library
* @param unpackRequired if the library needs to be unpacked before it can be used
* @deprecated since 2.4.8 for removal in 2.6.0 in favor of
* {@link #Library(String, File, LibraryScope, LibraryCoordinates, boolean, boolean, boolean)}
*/
@Deprecated
public Library(File file, LibraryScope scope, boolean unpackRequired) {
this(null, file, scope, unpackRequired);
}
......@@ -69,7 +77,10 @@ public class Library {
* @param file the source file
* @param scope the scope of the library
* @param unpackRequired if the library needs to be unpacked before it can be used
* @deprecated since 2.4.8 for removal in 2.6.0 in favor of
* {@link #Library(String, File, LibraryScope, LibraryCoordinates, boolean, boolean, boolean)}
*/
@Deprecated
public Library(String name, File file, LibraryScope scope, boolean unpackRequired) {
this(name, file, scope, null, unpackRequired);
}
......@@ -82,7 +93,10 @@ public class Library {
* @param scope the scope of the library
* @param coordinates the library coordinates or {@code null}
* @param unpackRequired if the library needs to be unpacked before it can be used
* @deprecated since 2.4.8 for removal in 2.6.0 in favor of
* {@link #Library(String, File, LibraryScope, LibraryCoordinates, boolean, boolean, boolean)}
*/
@Deprecated
public Library(String name, File file, LibraryScope scope, LibraryCoordinates coordinates, boolean unpackRequired) {
this(name, file, scope, coordinates, unpackRequired, false);
}
......@@ -98,15 +112,37 @@ public class Library {
* @param local if the library is local (part of the same build) to the application
* that is being packaged
* @since 2.4.0
* @deprecated since 2.4.8 for removal in 2.6.0 in favor of
* {@link #Library(String, File, LibraryScope, LibraryCoordinates, boolean, boolean, boolean)}
*/
@Deprecated
public Library(String name, File file, LibraryScope scope, LibraryCoordinates coordinates, boolean unpackRequired,
boolean local) {
this(name, file, scope, coordinates, unpackRequired, local, true);
}
/**
* Create a new {@link Library}.
* @param name the name of the library as it should be written or {@code null} to use
* the file name
* @param file the source file
* @param scope the scope of the library
* @param coordinates the library coordinates or {@code null}
* @param unpackRequired if the library needs to be unpacked before it can be used
* @param local if the library is local (part of the same build) to the application
* that is being packaged
* @param included if the library is included in the fat jar
* @since 2.4.8
*/
public Library(String name, File file, LibraryScope scope, LibraryCoordinates coordinates, boolean unpackRequired,
boolean local, boolean included) {
this.name = (name != null) ? name : file.getName();
this.file = file;
this.scope = scope;
this.coordinates = coordinates;
this.unpackRequired = unpackRequired;
this.local = local;
this.included = included;
}
/**
......@@ -172,4 +208,12 @@ public class Library {
return this.local;
}
/**
* Return if the library is included in the fat jar.
* @return if the library is included
*/
public boolean isIncluded() {
return this.included;
}
}
......@@ -25,7 +25,9 @@ import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.stream.Collectors;
......@@ -191,14 +193,18 @@ public abstract class Packager {
protected final void write(JarFile sourceJar, Libraries libraries, AbstractJarWriter writer) throws IOException {
Assert.notNull(libraries, "Libraries must not be null");
WritableLibraries writeableLibraries = new WritableLibraries(libraries);
write(sourceJar, writer, new PackagedLibraries(libraries));
}
private void write(JarFile sourceJar, AbstractJarWriter writer, PackagedLibraries libraries) throws IOException {
if (isLayered()) {
writer.useLayers(this.layers, this.layersIndex);
}
writer.writeManifest(buildManifest(sourceJar));
writeLoaderClasses(writer);
writer.writeEntries(sourceJar, getEntityTransformer(), writeableLibraries);
writeableLibraries.write(writer);
writer.writeEntries(sourceJar, getEntityTransformer(), libraries.getUnpackHandler(),
libraries.getEntryFilter());
libraries.write(writer);
if (isLayered()) {
writeLayerIndex(writer);
}
......@@ -456,11 +462,15 @@ public abstract class Packager {
* An {@link UnpackHandler} that determines that an entry needs to be unpacked if a
* library that requires unpacking has a matching entry name.
*/
private final class WritableLibraries implements UnpackHandler {
private final class PackagedLibraries {
private final Map<String, Library> libraries = new LinkedHashMap<>();
WritableLibraries(Libraries libraries) throws IOException {
private final UnpackHandler unpackHandler;
private final Predicate<JarEntry> entryFilter;
PackagedLibraries(Libraries libraries) throws IOException {
libraries.doWithLibraries((library) -> {
if (isZip(library::openStream)) {
addLibrary(library);
......@@ -469,6 +479,8 @@ public abstract class Packager {
if (isLayered() && Packager.this.includeRelevantJarModeJars) {
addLibrary(JarModeLibrary.LAYER_TOOLS);
}
this.unpackHandler = new PackagedLibrariesUnpackHandler();
this.entryFilter = this::isIncluded;
}
private void addLibrary(Library library) {
......@@ -480,37 +492,58 @@ public abstract class Packager {
}
}
@Override
public boolean requiresUnpack(String name) {
Library library = this.libraries.get(name);
return library != null && library.isUnpackRequired();
private boolean isIncluded(JarEntry entry) {
Library library = this.libraries.get(entry.getName());
return library == null || library.isIncluded();
}
@Override
public String sha1Hash(String name) throws IOException {
Library library = this.libraries.get(name);
Assert.notNull(library, () -> "No library found for entry name '" + name + "'");
return Digest.sha1(library::openStream);
UnpackHandler getUnpackHandler() {
return this.unpackHandler;
}
Predicate<JarEntry> getEntryFilter() {
return this.entryFilter;
}
private void write(AbstractJarWriter writer) throws IOException {
void write(AbstractJarWriter writer) throws IOException {
List<String> writtenPaths = new ArrayList<>();
for (Entry<String, Library> entry : this.libraries.entrySet()) {
String path = entry.getKey();
Library library = entry.getValue();
String location = path.substring(0, path.lastIndexOf('/') + 1);
writer.writeNestedLibrary(location, library);
if (library.isIncluded()) {
String location = path.substring(0, path.lastIndexOf('/') + 1);
writer.writeNestedLibrary(location, library);
writtenPaths.add(path);
}
}
if (getLayout() instanceof RepackagingLayout) {
writeClasspathIndex((RepackagingLayout) getLayout(), writer);
writeClasspathIndex(writtenPaths, (RepackagingLayout) getLayout(), writer);
}
}
private void writeClasspathIndex(RepackagingLayout layout, AbstractJarWriter writer) throws IOException {
List<String> names = this.libraries.keySet().stream().map((path) -> "- \"" + path + "\"")
.collect(Collectors.toList());
private void writeClasspathIndex(List<String> paths, RepackagingLayout layout, AbstractJarWriter writer)
throws IOException {
List<String> names = paths.stream().map((path) -> "- \"" + path + "\"").collect(Collectors.toList());
writer.writeIndexFile(layout.getClasspathIndexFileLocation(), names);
}
private class PackagedLibrariesUnpackHandler implements UnpackHandler {
@Override
public boolean requiresUnpack(String name) {
Library library = PackagedLibraries.this.libraries.get(name);
return library != null && library.isUnpackRequired();
}
@Override
public String sha1Hash(String name) throws IOException {
Library library = PackagedLibraries.this.libraries.get(name);
Assert.notNull(library, () -> "No library found for entry name '" + name + "'");
return Digest.sha1(library::openStream);
}
}
}
}
......@@ -89,4 +89,13 @@ class WarIntegrationTests extends AbstractArchiveIntegrationTests {
});
}
@TestTemplate
void whenEntryIsExcludedItShouldNotBePresentInTheRepackagedWar(MavenBuild mavenBuild) {
mavenBuild.project("war-exclude-entry").execute((project) -> {
File war = new File(project, "target/war-exclude-entry-0.0.1.BUILD-SNAPSHOT.war");
assertThat(jar(war)).hasEntryWithNameStartingWith("WEB-INF/lib/spring-context")
.doesNotHaveEntryWithNameStartingWith("WEB-INF/lib/spring-core");
});
}
}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.boot.maven.it</groupId>
<artifactId>war-exclude-entry</artifactId>
<version>0.0.1.BUILD-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>@java.version@</maven.compiler.source>
<maven.compiler.target>@java.version@</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<groupId>@project.groupId@</groupId>
<artifactId>@project.artifactId@</artifactId>
<version>@project.version@</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
<configuration>
<excludes>
<exclude>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</exclude>
</excludes>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>@maven-war-plugin.version@</version>
<configuration>
<archive>
<manifestEntries>
<Not-Used>Foo</Not-Used>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>@spring-framework.version@</version>
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>@jakarta-servlet.version@</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed 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
*
* https://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.test;
public class SampleApplication {
public static void main(String[] args) {
}
}
......@@ -76,7 +76,7 @@ public abstract class AbstractDependencyFilterMojo extends AbstractMojo {
this.excludeGroupIds = excludeGroupIds;
}
protected Set<Artifact> filterDependencies(Set<Artifact> dependencies, FilterArtifacts filters)
protected final Set<Artifact> filterDependencies(Set<Artifact> dependencies, FilterArtifacts filters)
throws MojoExecutionException {
try {
Set<Artifact> filtered = new LinkedHashSet<>(dependencies);
......
......@@ -182,8 +182,9 @@ public abstract class AbstractPackagerMojo extends AbstractDependencyFilterMojo
* @throws MojoExecutionException on execution error
*/
protected final Libraries getLibraries(Collection<Dependency> unpacks) throws MojoExecutionException {
Set<Artifact> artifacts = filterDependencies(this.project.getArtifacts(), getFilters(getAdditionalFilters()));
return new ArtifactsLibraries(artifacts, this.session.getProjects(), unpacks, getLog());
Set<Artifact> artifacts = this.project.getArtifacts();
Set<Artifact> includedArtifacts = filterDependencies(artifacts, getFilters(getAdditionalFilters()));
return new ArtifactsLibraries(artifacts, includedArtifacts, this.session.getProjects(), unpacks, getLog());
}
private ArtifactsFilter[] getAdditionalFilters() {
......
......@@ -16,6 +16,7 @@
package org.springframework.boot.maven;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
......@@ -59,6 +60,8 @@ public class ArtifactsLibraries implements Libraries {
private final Set<Artifact> artifacts;
private final Set<Artifact> includedArtifacts;
private final Collection<MavenProject> localProjects;
private final Collection<Dependency> unpacks;
......@@ -89,7 +92,23 @@ public class ArtifactsLibraries implements Libraries {
*/
public ArtifactsLibraries(Set<Artifact> artifacts, Collection<MavenProject> localProjects,
Collection<Dependency> unpacks, Log log) {
this(artifacts, artifacts, localProjects, unpacks, log);
}
/**
* Creates a new {@code ArtifactsLibraries} from the given {@code artifacts}.
* @param artifacts all artifacts that can be represented as libraries
* @param includedArtifacts the actual artifacts to include in the fat jar
* @param localProjects projects for which {@link Library#isLocal() local} libraries
* should be created
* @param unpacks artifacts that should be unpacked on launch
* @param log the log
* @since 2.4.8
*/
public ArtifactsLibraries(Set<Artifact> artifacts, Set<Artifact> includedArtifacts,
Collection<MavenProject> localProjects, Collection<Dependency> unpacks, Log log) {
this.artifacts = artifacts;
this.includedArtifacts = includedArtifacts;
this.localProjects = localProjects;
this.unpacks = unpacks;
this.log = log;
......@@ -99,18 +118,22 @@ public class ArtifactsLibraries implements Libraries {
public void doWithLibraries(LibraryCallback callback) throws IOException {
Set<String> duplicates = getDuplicates(this.artifacts);
for (Artifact artifact : this.artifacts) {
String name = getFileName(artifact);
File file = artifact.getFile();
LibraryScope scope = SCOPES.get(artifact.getScope());
if (scope != null && artifact.getFile() != null) {
String name = getFileName(artifact);
if (duplicates.contains(name)) {
this.log.debug("Duplicate found: " + name);
name = artifact.getGroupId() + "-" + name;
this.log.debug("Renamed to: " + name);
}
LibraryCoordinates coordinates = new ArtifactLibraryCoordinates(artifact);
callback.library(new Library(name, artifact.getFile(), scope, coordinates, isUnpackRequired(artifact),
isLocal(artifact)));
if (scope == null || file == null) {
continue;
}
if (duplicates.contains(name)) {
this.log.debug("Duplicate found: " + name);
name = artifact.getGroupId() + "-" + name;
this.log.debug("Renamed to: " + name);
}
LibraryCoordinates coordinates = new ArtifactLibraryCoordinates(artifact);
boolean unpackRequired = isUnpackRequired(artifact);
boolean local = isLocal(artifact);
boolean included = this.includedArtifacts.contains(artifact);
callback.library(new Library(name, file, scope, coordinates, unpackRequired, local, included));
}
}
......
......@@ -189,8 +189,8 @@ public class BuildImageMojo extends AbstractPackagerMojo {
}
/**
* Return the layout factory that will be used to determine the {@link LayoutType} if
* no explicit layout is set.
* Return the layout factory that will be used to determine the
* {@link AbstractPackagerMojo.LayoutType} if no explicit layout is set.
* @return the value of the {@code layoutFactory} parameter, or {@code null} if the
* parameter is not provided
*/
......
......@@ -183,8 +183,8 @@ public class RepackageMojo extends AbstractPackagerMojo {
}
/**
* Return the layout factory that will be used to determine the {@link LayoutType} if
* no explicit layout is set.
* Return the layout factory that will be used to determine the
* {@link AbstractPackagerMojo.LayoutType} if no explicit layout is set.
* @return the value of the {@code layoutFactory} parameter, or {@code null} if the
* parameter is not provided
*/
......
......@@ -144,6 +144,7 @@ class ArtifactsLibrariesTests {
this.artifacts = Collections.singleton(snapshotArtifact);
new ArtifactsLibraries(this.artifacts, Collections.emptyList(), null, mock(Log.class))
.doWithLibraries((library) -> {
assertThat(library.isIncluded()).isTrue();
assertThat(library.isLocal()).isFalse();
assertThat(library.getCoordinates().getVersion()).isEqualTo("1.0-SNAPSHOT");
});
......@@ -181,4 +182,19 @@ class ArtifactsLibrariesTests {
.doWithLibraries((library) -> assertThat(library.isLocal()).isTrue());
}
@Test
void nonIncludedArtifact() throws IOException {
Artifact artifact = mock(Artifact.class);
given(artifact.getScope()).willReturn("compile");
given(artifact.getArtifactId()).willReturn("artifact");
given(artifact.getBaseVersion()).willReturn("1.0-SNAPSHOT");
given(artifact.getFile()).willReturn(new File("a"));
given(artifact.getArtifactHandler()).willReturn(this.artifactHandler);
MavenProject mavenProject = mock(MavenProject.class);
given(mavenProject.getArtifact()).willReturn(artifact);
this.artifacts = Collections.singleton(artifact);
new ArtifactsLibraries(this.artifacts, Collections.emptySet(), Collections.singleton(mavenProject), null,
mock(Log.class)).doWithLibraries((library) -> assertThat(library.isIncluded()).isFalse());
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment