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: Phillip Webb <pwebb@vmware.com>
This commit is contained in:
Madhura Bhave
2021-06-08 17:23:04 -07:00
committed by Phillip Webb
parent 4d694ddaa8
commit b790c82732
17 changed files with 346 additions and 81 deletions

View File

@@ -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);
}
}
}

View File

@@ -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) {

View File

@@ -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;
}
}

View File

@@ -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;
}
private void write(AbstractJarWriter writer) throws IOException {
Predicate<JarEntry> getEntryFilter() {
return this.entryFilter;
}
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);
}
}
}
}

View File

@@ -197,9 +197,9 @@ abstract class AbstractPackagerTests<P extends Packager> {
libJarFile.setLastModified(JAN_1_1980);
P packager = createPackager();
execute(packager, (callback) -> {
callback.library(new Library(libJarFile, LibraryScope.COMPILE));
callback.library(new Library(libJarFileToUnpack, LibraryScope.COMPILE, true));
callback.library(new Library(libNonJarFile, LibraryScope.COMPILE));
callback.library(newLibrary(libJarFile, LibraryScope.COMPILE, false));
callback.library(newLibrary(libJarFileToUnpack, LibraryScope.COMPILE, true));
callback.library(newLibrary(libNonJarFile, LibraryScope.COMPILE, false));
});
assertThat(hasPackagedEntry("BOOT-INF/lib/" + libJarFile.getName())).isTrue();
assertThat(hasPackagedEntry("BOOT-INF/lib/" + libJarFileToUnpack.getName())).isTrue();
@@ -226,9 +226,9 @@ abstract class AbstractPackagerTests<P extends Packager> {
File file = this.testJarFile.getFile();
P packager = createPackager(file);
execute(packager, (callback) -> {
callback.library(new Library(libJarFile1, LibraryScope.COMPILE));
callback.library(new Library(libJarFile2, LibraryScope.COMPILE));
callback.library(new Library(libJarFile3, LibraryScope.COMPILE));
callback.library(newLibrary(libJarFile1, LibraryScope.COMPILE, false));
callback.library(newLibrary(libJarFile2, LibraryScope.COMPILE, false));
callback.library(newLibrary(libJarFile3, LibraryScope.COMPILE, false));
});
assertThat(hasPackagedEntry("BOOT-INF/classpath.idx")).isTrue();
String index = getPackagedEntryContent("BOOT-INF/classpath.idx");
@@ -258,9 +258,9 @@ abstract class AbstractPackagerTests<P extends Packager> {
packager.setLayers(layers);
packager.setIncludeRelevantJarModeJars(false);
execute(packager, (callback) -> {
callback.library(new Library(libJarFile1, LibraryScope.COMPILE));
callback.library(new Library(libJarFile2, LibraryScope.COMPILE));
callback.library(new Library(libJarFile3, LibraryScope.COMPILE));
callback.library(newLibrary(libJarFile1, LibraryScope.COMPILE, false));
callback.library(newLibrary(libJarFile2, LibraryScope.COMPILE, false));
callback.library(newLibrary(libJarFile3, LibraryScope.COMPILE, false));
});
assertThat(hasPackagedEntry("BOOT-INF/classpath.idx")).isTrue();
String classpathIndex = getPackagedEntryContent("BOOT-INF/classpath.idx");
@@ -316,8 +316,8 @@ abstract class AbstractPackagerTests<P extends Packager> {
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
P packager = createPackager();
assertThatIllegalStateException().isThrownBy(() -> execute(packager, (callback) -> {
callback.library(new Library(libJarFile, LibraryScope.COMPILE, false));
callback.library(new Library(libJarFile, LibraryScope.COMPILE, false));
callback.library(newLibrary(libJarFile, LibraryScope.COMPILE, false));
callback.library(newLibrary(libJarFile, LibraryScope.COMPILE, false));
})).withMessageContaining("Duplicate library");
}
@@ -334,7 +334,7 @@ abstract class AbstractPackagerTests<P extends Packager> {
given(layout.getLibraryLocation(anyString(), eq(scope))).willReturn("test/");
given(layout.getLibraryLocation(anyString(), eq(LibraryScope.COMPILE))).willReturn("test-lib/");
packager.setLayout(layout);
execute(packager, (callback) -> callback.library(new Library(libJarFile, scope)));
execute(packager, (callback) -> callback.library(newLibrary(libJarFile, scope, false)));
assertThat(hasPackagedEntry("test/" + libJarFile.getName())).isTrue();
assertThat(getPackagedManifest().getMainAttributes().getValue("Spring-Boot-Lib")).isEqualTo("test-lib/");
assertThat(getPackagedManifest().getMainAttributes().getValue("Main-Class")).isEqualTo("testLauncher");
@@ -351,7 +351,7 @@ abstract class AbstractPackagerTests<P extends Packager> {
LibraryScope scope = mock(LibraryScope.class);
given(layout.getLauncherClassName()).willReturn("testLauncher");
packager.setLayout(layout);
execute(packager, (callback) -> callback.library(new Library(libJarFile, scope)));
execute(packager, (callback) -> callback.library(newLibrary(libJarFile, scope, false)));
assertThat(getPackagedManifest().getMainAttributes().getValue("Spring-Boot-Lib")).isNull();
assertThat(getPackagedManifest().getMainAttributes().getValue("Main-Class")).isEqualTo("testLauncher");
}
@@ -405,7 +405,7 @@ abstract class AbstractPackagerTests<P extends Packager> {
this.testJarFile.addFile("test/nested.jar", nestedFile);
this.testJarFile.addClass("A.class", ClassWithMainMethod.class);
P packager = createPackager();
execute(packager, (callback) -> callback.library(new Library(nestedFile, LibraryScope.COMPILE)));
execute(packager, (callback) -> callback.library(newLibrary(nestedFile, LibraryScope.COMPILE, false)));
assertThat(getPackagedEntry("BOOT-INF/lib/" + nestedFile.getName()).getMethod()).isEqualTo(ZipEntry.STORED);
assertThat(getPackagedEntry("BOOT-INF/classes/test/nested.jar").getMethod()).isEqualTo(ZipEntry.STORED);
}
@@ -419,7 +419,7 @@ abstract class AbstractPackagerTests<P extends Packager> {
this.testJarFile.addFile(name, nested.getFile());
this.testJarFile.addClass("A.class", ClassWithMainMethod.class);
P packager = createPackager();
execute(packager, (callback) -> callback.library(new Library(nestedFile, LibraryScope.COMPILE, true)));
execute(packager, (callback) -> callback.library(newLibrary(nestedFile, LibraryScope.COMPILE, true)));
assertThat(getPackagedEntry(name).getComment()).startsWith("UNPACK:");
}
@@ -437,7 +437,7 @@ abstract class AbstractPackagerTests<P extends Packager> {
File toZip = new File(this.tempDir, "to-zip");
toZip.createNewFile();
ZipUtil.packEntry(toZip, nestedFile);
callback.library(new Library(nestedFile, LibraryScope.COMPILE));
callback.library(newLibrary(nestedFile, LibraryScope.COMPILE, false));
});
assertThat(getPackagedEntry("BOOT-INF/lib/" + nestedFile.getName()).getSize()).isEqualTo(sourceLength);
}
@@ -498,14 +498,14 @@ abstract class AbstractPackagerTests<P extends Packager> {
@Test
void loaderIsWrittenFirstThenApplicationClassesThenLibraries() throws IOException {
this.testJarFile.addClass("com/example/Application.class", ClassWithMainMethod.class);
File libraryOne = createLibrary();
File libraryTwo = createLibrary();
File libraryThree = createLibrary();
File libraryOne = createLibraryJar();
File libraryTwo = createLibraryJar();
File libraryThree = createLibraryJar();
P packager = createPackager();
execute(packager, (callback) -> {
callback.library(new Library(libraryOne, LibraryScope.COMPILE, false));
callback.library(new Library(libraryTwo, LibraryScope.COMPILE, true));
callback.library(new Library(libraryThree, LibraryScope.COMPILE, false));
callback.library(newLibrary(libraryOne, LibraryScope.COMPILE, false));
callback.library(newLibrary(libraryTwo, LibraryScope.COMPILE, true));
callback.library(newLibrary(libraryThree, LibraryScope.COMPILE, false));
});
assertThat(getPackagedEntryNames()).containsSubsequence("org/springframework/boot/loader/",
"BOOT-INF/classes/com/example/Application.class", "BOOT-INF/lib/" + libraryOne.getName(),
@@ -514,12 +514,12 @@ abstract class AbstractPackagerTests<P extends Packager> {
@Test
void existingEntryThatMatchesUnpackLibraryIsMarkedForUnpack() throws IOException {
File library = createLibrary();
File library = createLibraryJar();
this.testJarFile.addClass("WEB-INF/classes/com/example/Application.class", ClassWithMainMethod.class);
this.testJarFile.addFile("WEB-INF/lib/" + library.getName(), library);
P packager = createPackager(this.testJarFile.getFile("war"));
packager.setLayout(new Layouts.War());
execute(packager, (callback) -> callback.library(new Library(library, LibraryScope.COMPILE, true)));
execute(packager, (callback) -> callback.library(newLibrary(library, LibraryScope.COMPILE, true)));
assertThat(getPackagedEntryNames()).containsSubsequence("org/springframework/boot/loader/",
"WEB-INF/classes/com/example/Application.class", "WEB-INF/lib/" + library.getName());
ZipEntry unpackLibrary = getPackagedEntry("WEB-INF/lib/" + library.getName());
@@ -536,7 +536,7 @@ abstract class AbstractPackagerTests<P extends Packager> {
Layout layout = mock(Layout.class);
LibraryScope scope = mock(LibraryScope.class);
packager.setLayout(layout);
execute(packager, (callback) -> callback.library(new Library(libJarFile, scope)));
execute(packager, (callback) -> callback.library(newLibrary(libJarFile, scope, false)));
assertThat(getPackagedEntryNames()).containsExactly("META-INF/", "META-INF/MANIFEST.MF", "a/", "a/b/",
"a/b/C.class");
}
@@ -583,12 +583,39 @@ abstract class AbstractPackagerTests<P extends Packager> {
assertThat(getPackagedEntry("BOOT-INF/classes/META-INF/test.kotlin_module")).isNotNull();
}
private File createLibrary() throws IOException {
@Test
void entryFiltering() throws Exception {
File webLibrary = createLibraryJar();
File libraryOne = createLibraryJar();
File libraryTwo = createLibraryJar();
this.testJarFile.addClass("WEB-INF/classes/com/example/Application.class", ClassWithMainMethod.class);
this.testJarFile.addFile("WEB-INF/lib/" + webLibrary.getName(), webLibrary);
P packager = createPackager(this.testJarFile.getFile("war"));
packager.setLayout(new Layouts.War());
execute(packager, (callback) -> {
callback.library(newLibrary(webLibrary, LibraryScope.COMPILE, false, false));
callback.library(newLibrary(libraryOne, LibraryScope.COMPILE, false, false));
callback.library(newLibrary(libraryTwo, LibraryScope.COMPILE, false, true));
});
Collection<String> packagedEntryNames = getPackagedEntryNames();
packagedEntryNames.removeIf((name) -> !name.endsWith(".jar"));
assertThat(packagedEntryNames).containsExactly("WEB-INF/lib/" + libraryTwo.getName());
}
private File createLibraryJar() throws IOException {
TestJarFile library = new TestJarFile(this.tempDir);
library.addClass("com/example/library/Library.class", ClassWithoutMainMethod.class);
return library.getFile();
}
private Library newLibrary(File file, LibraryScope scope, boolean unpackRequired) {
return new Library(null, file, scope, null, unpackRequired, false, true);
}
private Library newLibrary(File file, LibraryScope scope, boolean unpackRequired, boolean included) {
return new Library(null, file, scope, null, unpackRequired, false, included);
}
protected final P createPackager() throws IOException {
return createPackager(this.testJarFile.getFile());
}