Create a new ImagePackager tools class
Pull functionality from `Repackager` into a new `Packager` base class and develop a variant for Docker image creation. The new `ImagePackager` class provides a general purpose way to construct jar entries without being tied to an actual file. This will allow us to link it to a buildpack and provide application content directly. Closes gh-19834
This commit is contained in:
@@ -0,0 +1,641 @@
|
||||
/*
|
||||
* 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.springframework.boot.loader.tools;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import java.util.jar.Attributes;
|
||||
import java.util.jar.Manifest;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.zip.Deflater;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.zeroturnaround.zip.ZipUtil;
|
||||
|
||||
import org.springframework.boot.loader.tools.sample.ClassWithMainMethod;
|
||||
import org.springframework.boot.loader.tools.sample.ClassWithoutMainMethod;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Abstract class for {@link Packager} based tests.
|
||||
*
|
||||
* @param <P> The packager type
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
* @author Madhura Bhave
|
||||
*/
|
||||
abstract class AbstractPackagerTests<P extends Packager> {
|
||||
|
||||
protected static final Libraries NO_LIBRARIES = (callback) -> {
|
||||
};
|
||||
|
||||
private static final long JAN_1_1980;
|
||||
static {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.set(1980, 0, 1, 0, 0, 0);
|
||||
calendar.set(Calendar.MILLISECOND, 0);
|
||||
JAN_1_1980 = calendar.getTime().getTime();
|
||||
}
|
||||
|
||||
private static final long JAN_1_1985;
|
||||
static {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.set(1985, 0, 1, 0, 0, 0);
|
||||
calendar.set(Calendar.MILLISECOND, 0);
|
||||
JAN_1_1985 = calendar.getTime().getTime();
|
||||
}
|
||||
|
||||
@TempDir
|
||||
File tempDir;
|
||||
|
||||
protected TestJarFile testJarFile;
|
||||
|
||||
@BeforeEach
|
||||
void setup() throws IOException {
|
||||
this.testJarFile = new TestJarFile(this.tempDir);
|
||||
}
|
||||
|
||||
@Test
|
||||
void specificMainClass() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithoutMainMethod.class);
|
||||
P packager = createPackager();
|
||||
packager.setMainClass("a.b.C");
|
||||
execute(packager, NO_LIBRARIES);
|
||||
Manifest actualManifest = getPackagedManifest();
|
||||
assertThat(actualManifest.getMainAttributes().getValue("Main-Class"))
|
||||
.isEqualTo("org.springframework.boot.loader.JarLauncher");
|
||||
assertThat(actualManifest.getMainAttributes().getValue("Start-Class")).isEqualTo("a.b.C");
|
||||
assertThat(hasPackagedLauncherClasses()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void mainClassFromManifest() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithoutMainMethod.class);
|
||||
Manifest manifest = new Manifest();
|
||||
manifest.getMainAttributes().putValue("Manifest-Version", "1.0");
|
||||
manifest.getMainAttributes().putValue("Main-Class", "a.b.C");
|
||||
this.testJarFile.addManifest(manifest);
|
||||
P packager = createPackager();
|
||||
execute(packager, NO_LIBRARIES);
|
||||
Manifest actualManifest = getPackagedManifest();
|
||||
assertThat(actualManifest.getMainAttributes().getValue("Main-Class"))
|
||||
.isEqualTo("org.springframework.boot.loader.JarLauncher");
|
||||
assertThat(actualManifest.getMainAttributes().getValue("Start-Class")).isEqualTo("a.b.C");
|
||||
assertThat(hasPackagedLauncherClasses()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void mainClassFound() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
P packager = createPackager();
|
||||
execute(packager, NO_LIBRARIES);
|
||||
Manifest actualManifest = getPackagedManifest();
|
||||
assertThat(actualManifest.getMainAttributes().getValue("Main-Class"))
|
||||
.isEqualTo("org.springframework.boot.loader.JarLauncher");
|
||||
assertThat(actualManifest.getMainAttributes().getValue("Start-Class")).isEqualTo("a.b.C");
|
||||
assertThat(hasPackagedLauncherClasses()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipleMainClassFound() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
this.testJarFile.addClass("a/b/D.class", ClassWithMainMethod.class);
|
||||
P packager = createPackager();
|
||||
assertThatIllegalStateException().isThrownBy(() -> execute(packager, NO_LIBRARIES)).withMessageContaining(
|
||||
"Unable to find a single main class from the following candidates [a.b.C, a.b.D]");
|
||||
}
|
||||
|
||||
@Test
|
||||
void noMainClass() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithoutMainMethod.class);
|
||||
P packager = createPackager(this.testJarFile.getFile());
|
||||
assertThatIllegalStateException().isThrownBy(() -> execute(packager, NO_LIBRARIES))
|
||||
.withMessageContaining("Unable to find main class");
|
||||
}
|
||||
|
||||
@Test
|
||||
void noMainClassAndLayoutIsNone() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
P packager = createPackager();
|
||||
packager.setLayout(new Layouts.None());
|
||||
execute(packager, NO_LIBRARIES);
|
||||
Manifest actualManifest = getPackagedManifest();
|
||||
assertThat(actualManifest.getMainAttributes().getValue("Main-Class")).isEqualTo("a.b.C");
|
||||
assertThat(hasPackagedLauncherClasses()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void noMainClassAndLayoutIsNoneWithNoMain() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithoutMainMethod.class);
|
||||
P packager = createPackager();
|
||||
packager.setLayout(new Layouts.None());
|
||||
execute(packager, NO_LIBRARIES);
|
||||
Manifest actualManifest = getPackagedManifest();
|
||||
assertThat(actualManifest.getMainAttributes().getValue("Main-Class")).isNull();
|
||||
assertThat(hasPackagedLauncherClasses()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void nullLibraries() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
P packager = createPackager();
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> execute(packager, null))
|
||||
.withMessageContaining("Libraries must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void libraries() throws Exception {
|
||||
TestJarFile libJar = new TestJarFile(this.tempDir);
|
||||
libJar.addClass("a/b/C.class", ClassWithoutMainMethod.class, JAN_1_1985);
|
||||
File libJarFile = libJar.getFile();
|
||||
File libJarFileToUnpack = libJar.getFile();
|
||||
File libNonJarFile = new File(this.tempDir, "non-lib.jar");
|
||||
FileCopyUtils.copy(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8 }, libNonJarFile);
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
this.testJarFile.addFile("BOOT-INF/lib/" + libJarFileToUnpack.getName(), libJarFileToUnpack);
|
||||
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));
|
||||
});
|
||||
assertThat(hasPackagedEntry("BOOT-INF/lib/" + libJarFile.getName())).isTrue();
|
||||
assertThat(hasPackagedEntry("BOOT-INF/lib/" + libJarFileToUnpack.getName())).isTrue();
|
||||
assertThat(hasPackagedEntry("BOOT-INF/lib/" + libNonJarFile.getName())).isFalse();
|
||||
ZipEntry entry = getPackagedEntry("BOOT-INF/lib/" + libJarFile.getName());
|
||||
assertThat(entry.getTime()).isEqualTo(JAN_1_1985);
|
||||
entry = getPackagedEntry("BOOT-INF/lib/" + libJarFileToUnpack.getName());
|
||||
assertThat(entry.getComment()).startsWith("UNPACK:");
|
||||
assertThat(entry.getComment()).hasSize(47);
|
||||
}
|
||||
|
||||
@Test
|
||||
void index() throws Exception {
|
||||
TestJarFile libJar1 = new TestJarFile(this.tempDir);
|
||||
libJar1.addClass("a/b/C.class", ClassWithoutMainMethod.class, JAN_1_1985);
|
||||
File libJarFile1 = libJar1.getFile();
|
||||
TestJarFile libJar2 = new TestJarFile(this.tempDir);
|
||||
libJar2.addClass("a/b/C.class", ClassWithoutMainMethod.class, JAN_1_1985);
|
||||
File libJarFile2 = libJar2.getFile();
|
||||
TestJarFile libJar3 = new TestJarFile(this.tempDir);
|
||||
libJar3.addClass("a/b/C.class", ClassWithoutMainMethod.class, JAN_1_1985);
|
||||
File libJarFile3 = libJar3.getFile();
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
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));
|
||||
});
|
||||
assertThat(hasPackagedEntry("BOOT-INF/classpath.idx")).isTrue();
|
||||
String index = getPackagedEntryContent("BOOT-INF/classpath.idx");
|
||||
String[] libraries = index.split("\\r?\\n");
|
||||
assertThat(Arrays.asList(libraries)).contains("BOOT-INF/lib/" + libJarFile1.getName(),
|
||||
"BOOT-INF/lib/" + libJarFile2.getName(), "BOOT-INF/lib/" + libJarFile3.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void layeredLayout() throws Exception {
|
||||
TestJarFile libJar1 = new TestJarFile(this.tempDir);
|
||||
libJar1.addClass("a/b/C.class", ClassWithoutMainMethod.class, JAN_1_1985);
|
||||
File libJarFile1 = libJar1.getFile();
|
||||
TestJarFile libJar2 = new TestJarFile(this.tempDir);
|
||||
libJar2.addClass("a/b/C.class", ClassWithoutMainMethod.class, JAN_1_1985);
|
||||
File libJarFile2 = libJar2.getFile();
|
||||
TestJarFile libJar3 = new TestJarFile(this.tempDir);
|
||||
libJar3.addClass("a/b/C.class", ClassWithoutMainMethod.class, JAN_1_1985);
|
||||
File libJarFile3 = libJar3.getFile();
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
P packager = createPackager();
|
||||
TestLayers layers = new TestLayers();
|
||||
layers.addLibrary(libJarFile1, "0001");
|
||||
layers.addLibrary(libJarFile2, "0002");
|
||||
layers.addLibrary(libJarFile3, "0003");
|
||||
packager.setLayers(layers);
|
||||
packager.setLayout(new Layouts.LayeredJar());
|
||||
execute(packager, (callback) -> {
|
||||
callback.library(new Library(libJarFile1, LibraryScope.COMPILE));
|
||||
callback.library(new Library(libJarFile2, LibraryScope.COMPILE));
|
||||
callback.library(new Library(libJarFile3, LibraryScope.COMPILE));
|
||||
});
|
||||
assertThat(hasPackagedEntry("BOOT-INF/classpath.idx")).isTrue();
|
||||
String index = getPackagedEntryContent("BOOT-INF/classpath.idx");
|
||||
String[] libraries = index.split("\\n");
|
||||
List<String> expected = new ArrayList<>();
|
||||
expected.add("BOOT-INF/layers/0001/lib/" + libJarFile1.getName());
|
||||
expected.add("BOOT-INF/layers/0002/lib/" + libJarFile2.getName());
|
||||
expected.add("BOOT-INF/layers/0003/lib/" + libJarFile3.getName());
|
||||
assertThat(Arrays.asList(libraries)).containsExactly(expected.toArray(new String[0]));
|
||||
}
|
||||
|
||||
@Test
|
||||
void duplicateLibraries() throws Exception {
|
||||
TestJarFile libJar = new TestJarFile(this.tempDir);
|
||||
libJar.addClass("a/b/C.class", ClassWithoutMainMethod.class);
|
||||
File libJarFile = libJar.getFile();
|
||||
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));
|
||||
})).withMessageContaining("Duplicate library");
|
||||
}
|
||||
|
||||
@Test
|
||||
void customLayout() throws Exception {
|
||||
TestJarFile libJar = new TestJarFile(this.tempDir);
|
||||
libJar.addClass("a/b/C.class", ClassWithoutMainMethod.class);
|
||||
File libJarFile = libJar.getFile();
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
P packager = createPackager();
|
||||
Layout layout = mock(Layout.class);
|
||||
LibraryScope scope = mock(LibraryScope.class);
|
||||
given(layout.getLauncherClassName()).willReturn("testLauncher");
|
||||
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)));
|
||||
assertThat(hasPackagedEntry("test/" + libJarFile.getName())).isTrue();
|
||||
assertThat(getPackagedManifest().getMainAttributes().getValue("Spring-Boot-Lib")).isEqualTo("test-lib/");
|
||||
assertThat(getPackagedManifest().getMainAttributes().getValue("Main-Class")).isEqualTo("testLauncher");
|
||||
}
|
||||
|
||||
@Test
|
||||
void customLayoutNoBootLib() throws Exception {
|
||||
TestJarFile libJar = new TestJarFile(this.tempDir);
|
||||
libJar.addClass("a/b/C.class", ClassWithoutMainMethod.class);
|
||||
File libJarFile = libJar.getFile();
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
P packager = createPackager();
|
||||
Layout layout = mock(Layout.class);
|
||||
LibraryScope scope = mock(LibraryScope.class);
|
||||
given(layout.getLauncherClassName()).willReturn("testLauncher");
|
||||
packager.setLayout(layout);
|
||||
execute(packager, (callback) -> callback.library(new Library(libJarFile, scope)));
|
||||
assertThat(getPackagedManifest().getMainAttributes().getValue("Spring-Boot-Lib")).isNull();
|
||||
assertThat(getPackagedManifest().getMainAttributes().getValue("Main-Class")).isEqualTo("testLauncher");
|
||||
}
|
||||
|
||||
@Test
|
||||
void springBootVersion() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
P packager = createPackager();
|
||||
execute(packager, NO_LIBRARIES);
|
||||
Manifest actualManifest = getPackagedManifest();
|
||||
assertThat(actualManifest.getMainAttributes()).containsKey(new Attributes.Name("Spring-Boot-Version"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void executableJarLayoutAttributes() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
P packager = createPackager();
|
||||
execute(packager, NO_LIBRARIES);
|
||||
Manifest actualManifest = getPackagedManifest();
|
||||
assertThat(actualManifest.getMainAttributes()).containsEntry(new Attributes.Name("Spring-Boot-Lib"),
|
||||
"BOOT-INF/lib/");
|
||||
assertThat(actualManifest.getMainAttributes()).containsEntry(new Attributes.Name("Spring-Boot-Classes"),
|
||||
"BOOT-INF/classes/");
|
||||
}
|
||||
|
||||
@Test
|
||||
void executableWarLayoutAttributes() throws Exception {
|
||||
this.testJarFile.addClass("WEB-INF/classes/a/b/C.class", ClassWithMainMethod.class);
|
||||
P packager = createPackager(this.testJarFile.getFile("war"));
|
||||
execute(packager, NO_LIBRARIES);
|
||||
Manifest actualManifest = getPackagedManifest();
|
||||
assertThat(actualManifest.getMainAttributes()).containsEntry(new Attributes.Name("Spring-Boot-Lib"),
|
||||
"WEB-INF/lib/");
|
||||
assertThat(actualManifest.getMainAttributes()).containsEntry(new Attributes.Name("Spring-Boot-Classes"),
|
||||
"WEB-INF/classes/");
|
||||
}
|
||||
|
||||
@Test
|
||||
void nullCustomLayout() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithoutMainMethod.class);
|
||||
Packager packager = createPackager();
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> packager.setLayout(null))
|
||||
.withMessageContaining("Layout must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void dontRecompressZips() throws Exception {
|
||||
TestJarFile nested = new TestJarFile(this.tempDir);
|
||||
nested.addClass("a/b/C.class", ClassWithoutMainMethod.class);
|
||||
File nestedFile = nested.getFile();
|
||||
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)));
|
||||
assertThat(getPackagedEntry("BOOT-INF/lib/" + nestedFile.getName()).getMethod()).isEqualTo(ZipEntry.STORED);
|
||||
assertThat(getPackagedEntry("BOOT-INF/classes/test/nested.jar").getMethod()).isEqualTo(ZipEntry.STORED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void unpackLibrariesTakePrecedenceOverExistingSourceEntries() throws Exception {
|
||||
TestJarFile nested = new TestJarFile(this.tempDir);
|
||||
nested.addClass("a/b/C.class", ClassWithoutMainMethod.class);
|
||||
File nestedFile = nested.getFile();
|
||||
String name = "BOOT-INF/lib/" + nestedFile.getName();
|
||||
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)));
|
||||
assertThat(getPackagedEntry(name).getComment()).startsWith("UNPACK:");
|
||||
}
|
||||
|
||||
@Test
|
||||
void existingSourceEntriesTakePrecedenceOverStandardLibraries() throws Exception {
|
||||
TestJarFile nested = new TestJarFile(this.tempDir);
|
||||
nested.addClass("a/b/C.class", ClassWithoutMainMethod.class);
|
||||
File nestedFile = nested.getFile();
|
||||
this.testJarFile.addFile("BOOT-INF/lib/" + nestedFile.getName(), nested.getFile());
|
||||
this.testJarFile.addClass("A.class", ClassWithMainMethod.class);
|
||||
P packager = createPackager();
|
||||
long sourceLength = nestedFile.length();
|
||||
execute(packager, (callback) -> {
|
||||
nestedFile.delete();
|
||||
File toZip = new File(this.tempDir, "to-zip");
|
||||
toZip.createNewFile();
|
||||
ZipUtil.packEntry(toZip, nestedFile);
|
||||
callback.library(new Library(nestedFile, LibraryScope.COMPILE));
|
||||
});
|
||||
assertThat(getPackagedEntry("BOOT-INF/lib/" + nestedFile.getName()).getSize()).isEqualTo(sourceLength);
|
||||
}
|
||||
|
||||
@Test
|
||||
void metaInfIndexListIsRemovedFromRepackagedJar() throws Exception {
|
||||
this.testJarFile.addClass("A.class", ClassWithMainMethod.class);
|
||||
File indexList = new File(this.tempDir, "INDEX.LIST");
|
||||
indexList.createNewFile();
|
||||
this.testJarFile.addFile("META-INF/INDEX.LIST", indexList);
|
||||
P packager = createPackager();
|
||||
execute(packager, NO_LIBRARIES);
|
||||
assertThat(getPackagedEntry("META-INF/INDEX.LIST")).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void customLayoutFactoryWithoutLayout() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
P packager = createPackager();
|
||||
packager.setLayoutFactory(new TestLayoutFactory());
|
||||
execute(packager, NO_LIBRARIES);
|
||||
assertThat(getPackagedEntry("test")).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void customLayoutFactoryWithLayout() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
P packager = createPackager();
|
||||
packager.setLayoutFactory(new TestLayoutFactory());
|
||||
packager.setLayout(new Layouts.Jar());
|
||||
execute(packager, NO_LIBRARIES);
|
||||
assertThat(getPackagedEntry("test")).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void metaInfAopXmlIsMovedBeneathBootInfClassesWhenRepackaged() throws Exception {
|
||||
this.testJarFile.addClass("A.class", ClassWithMainMethod.class);
|
||||
File aopXml = new File(this.tempDir, "aop.xml");
|
||||
aopXml.createNewFile();
|
||||
this.testJarFile.addFile("META-INF/aop.xml", aopXml);
|
||||
P packager = createPackager();
|
||||
execute(packager, NO_LIBRARIES);
|
||||
assertThat(getPackagedEntry("META-INF/aop.xml")).isNull();
|
||||
assertThat(getPackagedEntry("BOOT-INF/classes/META-INF/aop.xml")).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void allEntriesUseUnixPlatformAndUtf8NameEncoding() throws IOException {
|
||||
this.testJarFile.addClass("A.class", ClassWithMainMethod.class);
|
||||
P packager = createPackager();
|
||||
execute(packager, NO_LIBRARIES);
|
||||
for (ZipArchiveEntry entry : getAllPackagedEntries()) {
|
||||
assertThat(entry.getPlatform()).isEqualTo(ZipArchiveEntry.PLATFORM_UNIX);
|
||||
assertThat(entry.getGeneralPurposeBit().usesUTF8ForNames()).isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void loaderIsWrittenFirstThenApplicationClassesThenLibraries() throws IOException {
|
||||
this.testJarFile.addClass("com/example/Application.class", ClassWithMainMethod.class);
|
||||
File libraryOne = createLibrary();
|
||||
File libraryTwo = createLibrary();
|
||||
File libraryThree = createLibrary();
|
||||
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));
|
||||
});
|
||||
assertThat(getPackagedEntryNames()).containsSubsequence("org/springframework/boot/loader/",
|
||||
"BOOT-INF/classes/com/example/Application.class", "BOOT-INF/lib/" + libraryOne.getName(),
|
||||
"BOOT-INF/lib/" + libraryTwo.getName(), "BOOT-INF/lib/" + libraryThree.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void existingEntryThatMatchesUnpackLibraryIsMarkedForUnpack() throws IOException {
|
||||
File library = createLibrary();
|
||||
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)));
|
||||
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());
|
||||
assertThat(unpackLibrary.getComment()).startsWith("UNPACK:");
|
||||
}
|
||||
|
||||
@Test
|
||||
void layoutCanOmitLibraries() throws IOException {
|
||||
TestJarFile libJar = new TestJarFile(this.tempDir);
|
||||
libJar.addClass("a/b/C.class", ClassWithoutMainMethod.class);
|
||||
File libJarFile = libJar.getFile();
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
P packager = createPackager();
|
||||
Layout layout = mock(Layout.class);
|
||||
LibraryScope scope = mock(LibraryScope.class);
|
||||
packager.setLayout(layout);
|
||||
execute(packager, (callback) -> callback.library(new Library(libJarFile, scope)));
|
||||
assertThat(getPackagedEntryNames()).containsExactly("META-INF/", "META-INF/MANIFEST.MF", "a/", "a/b/",
|
||||
"a/b/C.class");
|
||||
}
|
||||
|
||||
@Test
|
||||
void jarThatUsesCustomCompressionConfigurationCanBeRepackaged() throws IOException {
|
||||
File source = new File(this.tempDir, "source.jar");
|
||||
ZipOutputStream output = new ZipOutputStream(new FileOutputStream(source)) {
|
||||
{
|
||||
this.def = new Deflater(Deflater.NO_COMPRESSION, true);
|
||||
}
|
||||
};
|
||||
byte[] data = new byte[1024 * 1024];
|
||||
new Random().nextBytes(data);
|
||||
ZipEntry entry = new ZipEntry("entry.dat");
|
||||
output.putNextEntry(entry);
|
||||
output.write(data);
|
||||
output.closeEntry();
|
||||
output.close();
|
||||
P packager = createPackager(source);
|
||||
packager.setMainClass("com.example.Main");
|
||||
execute(packager, NO_LIBRARIES);
|
||||
}
|
||||
|
||||
@Test
|
||||
void moduleInfoClassRemainsInRootOfJarWhenRepackaged() throws Exception {
|
||||
this.testJarFile.addClass("A.class", ClassWithMainMethod.class);
|
||||
this.testJarFile.addClass("module-info.class", ClassWithoutMainMethod.class);
|
||||
P packager = createPackager();
|
||||
execute(packager, NO_LIBRARIES);
|
||||
assertThat(getPackagedEntry("module-info.class")).isNotNull();
|
||||
assertThat(getPackagedEntry("BOOT-INF/classes/module-info.class")).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void kotlinModuleMetadataMovesBeneathBootInfClassesWhenRepackaged() throws Exception {
|
||||
this.testJarFile.addClass("A.class", ClassWithMainMethod.class);
|
||||
File kotlinModule = new File(this.tempDir, "test.kotlin_module");
|
||||
kotlinModule.createNewFile();
|
||||
this.testJarFile.addFile("META-INF/test.kotlin_module", kotlinModule);
|
||||
P packager = createPackager();
|
||||
execute(packager, NO_LIBRARIES);
|
||||
assertThat(getPackagedEntry("META-INF/test.kotlin_module")).isNull();
|
||||
assertThat(getPackagedEntry("BOOT-INF/classes/META-INF/test.kotlin_module")).isNotNull();
|
||||
}
|
||||
|
||||
private File createLibrary() throws IOException {
|
||||
TestJarFile library = new TestJarFile(this.tempDir);
|
||||
library.addClass("com/example/library/Library.class", ClassWithoutMainMethod.class);
|
||||
return library.getFile();
|
||||
}
|
||||
|
||||
protected final P createPackager() throws IOException {
|
||||
return createPackager(this.testJarFile.getFile());
|
||||
}
|
||||
|
||||
protected abstract P createPackager(File source);
|
||||
|
||||
protected abstract void execute(P packager, Libraries libraries) throws IOException;
|
||||
|
||||
protected Collection<String> getPackagedEntryNames() throws IOException {
|
||||
return getAllPackagedEntries().stream().map(ZipArchiveEntry::getName).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
protected boolean hasPackagedLauncherClasses() throws IOException {
|
||||
return hasPackagedEntry("org/springframework/boot/")
|
||||
&& hasPackagedEntry("org/springframework/boot/loader/JarLauncher.class");
|
||||
}
|
||||
|
||||
private boolean hasPackagedEntry(String name) throws IOException {
|
||||
return getPackagedEntry(name) != null;
|
||||
}
|
||||
|
||||
protected ZipEntry getPackagedEntry(String name) throws IOException {
|
||||
return getAllPackagedEntries().stream().filter((entry) -> name.equals(entry.getName())).findFirst()
|
||||
.orElse(null);
|
||||
|
||||
}
|
||||
|
||||
protected abstract Collection<ZipArchiveEntry> getAllPackagedEntries() throws IOException;
|
||||
|
||||
protected abstract Manifest getPackagedManifest() throws IOException;
|
||||
|
||||
protected abstract String getPackagedEntryContent(String name) throws IOException;
|
||||
|
||||
static class TestLayoutFactory implements LayoutFactory {
|
||||
|
||||
@Override
|
||||
public Layout getLayout(File source) {
|
||||
return new TestLayout();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class TestLayout extends Layouts.Jar implements CustomLoaderLayout {
|
||||
|
||||
@Override
|
||||
public void writeLoadedClasses(LoaderClassesWriter writer) throws IOException {
|
||||
writer.writeEntry("test", new ByteArrayInputStream("test".getBytes()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class TestLayers implements Layers {
|
||||
|
||||
private static final Layer DEFAULT_LAYER = new Layer("default");
|
||||
|
||||
private Set<Layer> layers = new LinkedHashSet<Layer>();
|
||||
|
||||
private Map<String, Layer> libraries = new HashMap<>();
|
||||
|
||||
TestLayers() {
|
||||
this.layers.add(DEFAULT_LAYER);
|
||||
}
|
||||
|
||||
void addLibrary(File jarFile, String layerName) {
|
||||
Layer layer = new Layer(layerName);
|
||||
this.layers.add(layer);
|
||||
this.libraries.put(jarFile.getName(), layer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<Layer> iterator() {
|
||||
return this.layers.iterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Layer getLayer(String name) {
|
||||
return DEFAULT_LAYER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Layer getLayer(Library library) {
|
||||
String name = new File(library.getName()).getName();
|
||||
return this.libraries.getOrDefault(name, DEFAULT_LAYER);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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.springframework.boot.loader.tools;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.jar.Manifest;
|
||||
import java.util.zip.ZipEntry;
|
||||
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
|
||||
|
||||
/**
|
||||
* Tests for {@link ImagePackager}
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class ImagePackagerTests extends AbstractPackagerTests<ImagePackager> {
|
||||
|
||||
private Map<ZipArchiveEntry, byte[]> entries;
|
||||
|
||||
@Override
|
||||
protected ImagePackager createPackager(File source) {
|
||||
return new ImagePackager(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void execute(ImagePackager packager, Libraries libraries) throws IOException {
|
||||
this.entries = new LinkedHashMap<>();
|
||||
packager.packageImage(libraries, this::save);
|
||||
}
|
||||
|
||||
private void save(ZipEntry entry, EntryWriter writer) {
|
||||
try {
|
||||
this.entries.put((ZipArchiveEntry) entry, getContent(writer));
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] getContent(EntryWriter writer) throws IOException {
|
||||
if (writer == null) {
|
||||
return null;
|
||||
}
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
writer.write(outputStream);
|
||||
return outputStream.toByteArray();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Collection<ZipArchiveEntry> getAllPackagedEntries() throws IOException {
|
||||
return this.entries.keySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Manifest getPackagedManifest() throws IOException {
|
||||
byte[] bytes = getEntryBytes("META-INF/MANIFEST.MF");
|
||||
return (bytes != null) ? new Manifest(new ByteArrayInputStream(bytes)) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getPackagedEntryContent(String name) throws IOException {
|
||||
byte[] bytes = getEntryBytes(name);
|
||||
return (bytes != null) ? new String(bytes, StandardCharsets.UTF_8) : null;
|
||||
}
|
||||
|
||||
private byte[] getEntryBytes(String name) throws IOException {
|
||||
ZipEntry entry = getPackagedEntry(name);
|
||||
return (entry != null) ? this.entries.get(entry) : null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,52 +16,29 @@
|
||||
|
||||
package org.springframework.boot.loader.tools;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collection;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import java.util.jar.Attributes;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.jar.Manifest;
|
||||
import java.util.zip.Deflater;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.zip.ZipFile;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.zeroturnaround.zip.ZipUtil;
|
||||
|
||||
import org.springframework.boot.loader.tools.sample.ClassWithMainMethod;
|
||||
import org.springframework.boot.loader.tools.sample.ClassWithoutMainMethod;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
import org.springframework.util.StreamUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link Repackager}.
|
||||
@@ -70,33 +47,9 @@ import static org.mockito.Mockito.mock;
|
||||
* @author Andy Wilkinson
|
||||
* @author Madhura Bhave
|
||||
*/
|
||||
class RepackagerTests {
|
||||
class RepackagerTests extends AbstractPackagerTests<Repackager> {
|
||||
|
||||
private static final Libraries NO_LIBRARIES = (callback) -> {
|
||||
};
|
||||
|
||||
private static final long JAN_1_1980;
|
||||
|
||||
private static final long JAN_1_1985;
|
||||
|
||||
static {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.set(1980, 0, 1, 0, 0, 0);
|
||||
calendar.set(Calendar.MILLISECOND, 0);
|
||||
JAN_1_1980 = calendar.getTime().getTime();
|
||||
calendar.set(Calendar.YEAR, 1985);
|
||||
JAN_1_1985 = calendar.getTime().getTime();
|
||||
}
|
||||
|
||||
@TempDir
|
||||
File tempDir;
|
||||
|
||||
private TestJarFile testJarFile;
|
||||
|
||||
@BeforeEach
|
||||
void setup() throws IOException {
|
||||
this.testJarFile = new TestJarFile(this.tempDir);
|
||||
}
|
||||
private File destination;
|
||||
|
||||
@Test
|
||||
void nullSource() {
|
||||
@@ -113,143 +66,55 @@ class RepackagerTests {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new Repackager(this.tempDir));
|
||||
}
|
||||
|
||||
@Test
|
||||
void specificMainClass() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithoutMainMethod.class);
|
||||
File file = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(file);
|
||||
repackager.setMainClass("a.b.C");
|
||||
repackager.repackage(NO_LIBRARIES);
|
||||
Manifest actualManifest = getManifest(file);
|
||||
assertThat(actualManifest.getMainAttributes().getValue("Main-Class"))
|
||||
.isEqualTo("org.springframework.boot.loader.JarLauncher");
|
||||
assertThat(actualManifest.getMainAttributes().getValue("Start-Class")).isEqualTo("a.b.C");
|
||||
assertThat(hasLauncherClasses(file)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void mainClassFromManifest() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithoutMainMethod.class);
|
||||
Manifest manifest = new Manifest();
|
||||
manifest.getMainAttributes().putValue("Manifest-Version", "1.0");
|
||||
manifest.getMainAttributes().putValue("Main-Class", "a.b.C");
|
||||
this.testJarFile.addManifest(manifest);
|
||||
File file = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(file);
|
||||
repackager.repackage(NO_LIBRARIES);
|
||||
Manifest actualManifest = getManifest(file);
|
||||
assertThat(actualManifest.getMainAttributes().getValue("Main-Class"))
|
||||
.isEqualTo("org.springframework.boot.loader.JarLauncher");
|
||||
assertThat(actualManifest.getMainAttributes().getValue("Start-Class")).isEqualTo("a.b.C");
|
||||
assertThat(hasLauncherClasses(file)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void mainClassFound() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
File file = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(file);
|
||||
repackager.repackage(NO_LIBRARIES);
|
||||
Manifest actualManifest = getManifest(file);
|
||||
assertThat(actualManifest.getMainAttributes().getValue("Main-Class"))
|
||||
.isEqualTo("org.springframework.boot.loader.JarLauncher");
|
||||
assertThat(actualManifest.getMainAttributes().getValue("Start-Class")).isEqualTo("a.b.C");
|
||||
assertThat(hasLauncherClasses(file)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void jarIsOnlyRepackagedOnce() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
File file = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(file);
|
||||
Repackager repackager = createRepackager(this.testJarFile.getFile(), false);
|
||||
repackager.repackage(NO_LIBRARIES);
|
||||
repackager.repackage(NO_LIBRARIES);
|
||||
Manifest actualManifest = getManifest(file);
|
||||
Manifest actualManifest = getPackagedManifest();
|
||||
assertThat(actualManifest.getMainAttributes().getValue("Main-Class"))
|
||||
.isEqualTo("org.springframework.boot.loader.JarLauncher");
|
||||
assertThat(actualManifest.getMainAttributes().getValue("Start-Class")).isEqualTo("a.b.C");
|
||||
assertThat(hasLauncherClasses(file)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipleMainClassFound() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
this.testJarFile.addClass("a/b/D.class", ClassWithMainMethod.class);
|
||||
File file = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(file);
|
||||
assertThatIllegalStateException().isThrownBy(() -> repackager.repackage(NO_LIBRARIES)).withMessageContaining(
|
||||
"Unable to find a single main class from the following candidates [a.b.C, a.b.D]");
|
||||
}
|
||||
|
||||
@Test
|
||||
void noMainClass() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithoutMainMethod.class);
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> new Repackager(this.testJarFile.getFile()).repackage(NO_LIBRARIES))
|
||||
.withMessageContaining("Unable to find main class");
|
||||
}
|
||||
|
||||
@Test
|
||||
void noMainClassAndLayoutIsNone() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
File file = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(file);
|
||||
repackager.setLayout(new Layouts.None());
|
||||
repackager.repackage(file, NO_LIBRARIES);
|
||||
Manifest actualManifest = getManifest(file);
|
||||
assertThat(actualManifest.getMainAttributes().getValue("Main-Class")).isEqualTo("a.b.C");
|
||||
assertThat(hasLauncherClasses(file)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void noMainClassAndLayoutIsNoneWithNoMain() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithoutMainMethod.class);
|
||||
File file = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(file);
|
||||
repackager.setLayout(new Layouts.None());
|
||||
repackager.repackage(file, NO_LIBRARIES);
|
||||
Manifest actualManifest = getManifest(file);
|
||||
assertThat(actualManifest.getMainAttributes().getValue("Main-Class")).isNull();
|
||||
assertThat(hasLauncherClasses(file)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void sameSourceAndDestinationWithBackup() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
File file = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(file);
|
||||
repackager.repackage(NO_LIBRARIES);
|
||||
assertThat(new File(file.getParent(), file.getName() + ".original")).exists();
|
||||
assertThat(hasLauncherClasses(file)).isTrue();
|
||||
assertThat(hasPackagedLauncherClasses()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void sameSourceAndDestinationWithoutBackup() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
File file = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(file);
|
||||
Repackager repackager = createRepackager(file, false);
|
||||
repackager.setBackupSource(false);
|
||||
repackager.repackage(NO_LIBRARIES);
|
||||
assertThat(new File(file.getParent(), file.getName() + ".original")).doesNotExist();
|
||||
assertThat(hasLauncherClasses(file)).isTrue();
|
||||
assertThat(hasPackagedLauncherClasses()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void sameSourceAndDestinationWithBackup() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
File file = this.testJarFile.getFile();
|
||||
Repackager repackager = createRepackager(file, false);
|
||||
repackager.repackage(NO_LIBRARIES);
|
||||
assertThat(new File(file.getParent(), file.getName() + ".original")).exists();
|
||||
assertThat(hasPackagedLauncherClasses()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void differentDestination() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
File source = this.testJarFile.getFile();
|
||||
File dest = new File(this.tempDir, "different.jar");
|
||||
Repackager repackager = new Repackager(source);
|
||||
repackager.repackage(dest, NO_LIBRARIES);
|
||||
Repackager repackager = createRepackager(source, true);
|
||||
execute(repackager, NO_LIBRARIES);
|
||||
assertThat(new File(source.getParent(), source.getName() + ".original")).doesNotExist();
|
||||
assertThat(hasLauncherClasses(source)).isFalse();
|
||||
assertThat(hasLauncherClasses(dest)).isTrue();
|
||||
assertThat(hasPackagedLauncherClasses()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void nullDestination() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
Repackager repackager = new Repackager(this.testJarFile.getFile());
|
||||
Repackager repackager = createRepackager(this.testJarFile.getFile(), true);
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> repackager.repackage(null, NO_LIBRARIES))
|
||||
.withMessageContaining("Invalid destination");
|
||||
}
|
||||
@@ -257,7 +122,7 @@ class RepackagerTests {
|
||||
@Test
|
||||
void destinationIsDirectory() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
Repackager repackager = new Repackager(this.testJarFile.getFile());
|
||||
Repackager repackager = createRepackager(this.testJarFile.getFile(), true);
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> repackager.repackage(this.tempDir, NO_LIBRARIES))
|
||||
.withMessageContaining("Invalid destination");
|
||||
}
|
||||
@@ -265,468 +130,32 @@ class RepackagerTests {
|
||||
@Test
|
||||
void overwriteDestination() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
Repackager repackager = new Repackager(this.testJarFile.getFile());
|
||||
File dest = new File(this.tempDir, "dest.jar");
|
||||
dest.createNewFile();
|
||||
repackager.repackage(dest, NO_LIBRARIES);
|
||||
assertThat(hasLauncherClasses(dest)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void nullLibraries() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
File file = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(file);
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> repackager.repackage(file, null))
|
||||
.withMessageContaining("Libraries must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void libraries() throws Exception {
|
||||
TestJarFile libJar = new TestJarFile(this.tempDir);
|
||||
libJar.addClass("a/b/C.class", ClassWithoutMainMethod.class, JAN_1_1985);
|
||||
File libJarFile = libJar.getFile();
|
||||
File libJarFileToUnpack = libJar.getFile();
|
||||
File libNonJarFile = new File(this.tempDir, "non-lib.jar");
|
||||
FileCopyUtils.copy(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8 }, libNonJarFile);
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
this.testJarFile.addFile("BOOT-INF/lib/" + libJarFileToUnpack.getName(), libJarFileToUnpack);
|
||||
File file = this.testJarFile.getFile();
|
||||
libJarFile.setLastModified(JAN_1_1980);
|
||||
Repackager repackager = new Repackager(file);
|
||||
repackager.repackage((callback) -> {
|
||||
callback.library(new Library(libJarFile, LibraryScope.COMPILE));
|
||||
callback.library(new Library(libJarFileToUnpack, LibraryScope.COMPILE, true));
|
||||
callback.library(new Library(libNonJarFile, LibraryScope.COMPILE));
|
||||
});
|
||||
assertThat(hasEntry(file, "BOOT-INF/lib/" + libJarFile.getName())).isTrue();
|
||||
assertThat(hasEntry(file, "BOOT-INF/lib/" + libJarFileToUnpack.getName())).isTrue();
|
||||
assertThat(hasEntry(file, "BOOT-INF/lib/" + libNonJarFile.getName())).isFalse();
|
||||
JarEntry entry = getEntry(file, "BOOT-INF/lib/" + libJarFile.getName());
|
||||
assertThat(entry.getTime()).isEqualTo(JAN_1_1985);
|
||||
entry = getEntry(file, "BOOT-INF/lib/" + libJarFileToUnpack.getName());
|
||||
assertThat(entry.getComment()).startsWith("UNPACK:");
|
||||
assertThat(entry.getComment()).hasSize(47);
|
||||
}
|
||||
|
||||
@Test
|
||||
void index() throws Exception {
|
||||
TestJarFile libJar1 = new TestJarFile(this.tempDir);
|
||||
libJar1.addClass("a/b/C.class", ClassWithoutMainMethod.class, JAN_1_1985);
|
||||
File libJarFile1 = libJar1.getFile();
|
||||
TestJarFile libJar2 = new TestJarFile(this.tempDir);
|
||||
libJar2.addClass("a/b/C.class", ClassWithoutMainMethod.class, JAN_1_1985);
|
||||
File libJarFile2 = libJar2.getFile();
|
||||
TestJarFile libJar3 = new TestJarFile(this.tempDir);
|
||||
libJar3.addClass("a/b/C.class", ClassWithoutMainMethod.class, JAN_1_1985);
|
||||
File libJarFile3 = libJar3.getFile();
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
File file = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(file);
|
||||
repackager.repackage((callback) -> {
|
||||
callback.library(new Library(libJarFile1, LibraryScope.COMPILE));
|
||||
callback.library(new Library(libJarFile2, LibraryScope.COMPILE));
|
||||
callback.library(new Library(libJarFile3, LibraryScope.COMPILE));
|
||||
});
|
||||
assertThat(hasEntry(file, "BOOT-INF/classpath.idx")).isTrue();
|
||||
ZipUtil.unpack(file, new File(file.getParent()));
|
||||
try (FileInputStream inputStream = new FileInputStream(
|
||||
new File(file.getParent() + "/BOOT-INF/classpath.idx"))) {
|
||||
String index = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
|
||||
String[] libraries = index.split("\\r?\\n");
|
||||
assertThat(Arrays.asList(libraries)).contains("BOOT-INF/lib/" + libJarFile1.getName(),
|
||||
"BOOT-INF/lib/" + libJarFile2.getName(), "BOOT-INF/lib/" + libJarFile3.getName());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void layeredLayout() throws Exception {
|
||||
TestJarFile libJar1 = new TestJarFile(this.tempDir);
|
||||
libJar1.addClass("a/b/C.class", ClassWithoutMainMethod.class, JAN_1_1985);
|
||||
File libJarFile1 = libJar1.getFile();
|
||||
TestJarFile libJar2 = new TestJarFile(this.tempDir);
|
||||
libJar2.addClass("a/b/C.class", ClassWithoutMainMethod.class, JAN_1_1985);
|
||||
File libJarFile2 = libJar2.getFile();
|
||||
TestJarFile libJar3 = new TestJarFile(this.tempDir);
|
||||
libJar3.addClass("a/b/C.class", ClassWithoutMainMethod.class, JAN_1_1985);
|
||||
File libJarFile3 = libJar3.getFile();
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
File file = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(file);
|
||||
TestLayers layers = new TestLayers();
|
||||
layers.addLibrary(libJarFile1, "0001");
|
||||
layers.addLibrary(libJarFile2, "0002");
|
||||
layers.addLibrary(libJarFile3, "0003");
|
||||
repackager.setLayers(layers);
|
||||
repackager.setLayout(new Layouts.LayeredJar());
|
||||
repackager.repackage((callback) -> {
|
||||
callback.library(new Library(libJarFile1, LibraryScope.COMPILE));
|
||||
callback.library(new Library(libJarFile2, LibraryScope.COMPILE));
|
||||
callback.library(new Library(libJarFile3, LibraryScope.COMPILE));
|
||||
});
|
||||
assertThat(hasEntry(file, "BOOT-INF/classpath.idx")).isTrue();
|
||||
ZipUtil.unpack(file, new File(file.getParent()));
|
||||
try (FileInputStream inputStream = new FileInputStream(
|
||||
new File(file.getParent() + "/BOOT-INF/classpath.idx"))) {
|
||||
String index = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
|
||||
String[] libraries = index.split("\\r?\\n");
|
||||
List<String> expected = new ArrayList<>();
|
||||
expected.add("BOOT-INF/layers/0001/lib/" + libJarFile1.getName());
|
||||
expected.add("BOOT-INF/layers/0002/lib/" + libJarFile2.getName());
|
||||
expected.add("BOOT-INF/layers/0003/lib/" + libJarFile3.getName());
|
||||
assertThat(Arrays.asList(libraries)).containsExactly(expected.toArray(new String[0]));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void duplicateLibraries() throws Exception {
|
||||
TestJarFile libJar = new TestJarFile(this.tempDir);
|
||||
libJar.addClass("a/b/C.class", ClassWithoutMainMethod.class);
|
||||
File libJarFile = libJar.getFile();
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
File file = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(file);
|
||||
assertThatIllegalStateException().isThrownBy(() -> repackager.repackage((callback) -> {
|
||||
callback.library(new Library(libJarFile, LibraryScope.COMPILE, false));
|
||||
callback.library(new Library(libJarFile, LibraryScope.COMPILE, false));
|
||||
})).withMessageContaining("Duplicate library");
|
||||
}
|
||||
|
||||
@Test
|
||||
void customLayout() throws Exception {
|
||||
TestJarFile libJar = new TestJarFile(this.tempDir);
|
||||
libJar.addClass("a/b/C.class", ClassWithoutMainMethod.class);
|
||||
File libJarFile = libJar.getFile();
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
File file = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(file);
|
||||
Layout layout = mock(Layout.class);
|
||||
LibraryScope scope = mock(LibraryScope.class);
|
||||
given(layout.getLauncherClassName()).willReturn("testLauncher");
|
||||
given(layout.getLibraryLocation(anyString(), eq(scope))).willReturn("test/");
|
||||
given(layout.getLibraryLocation(anyString(), eq(LibraryScope.COMPILE))).willReturn("test-lib/");
|
||||
repackager.setLayout(layout);
|
||||
repackager.repackage((callback) -> callback.library(new Library(libJarFile, scope)));
|
||||
assertThat(hasEntry(file, "test/" + libJarFile.getName())).isTrue();
|
||||
assertThat(getManifest(file).getMainAttributes().getValue("Spring-Boot-Lib")).isEqualTo("test-lib/");
|
||||
assertThat(getManifest(file).getMainAttributes().getValue("Main-Class")).isEqualTo("testLauncher");
|
||||
}
|
||||
|
||||
@Test
|
||||
void customLayoutNoBootLib() throws Exception {
|
||||
TestJarFile libJar = new TestJarFile(this.tempDir);
|
||||
libJar.addClass("a/b/C.class", ClassWithoutMainMethod.class);
|
||||
File libJarFile = libJar.getFile();
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
File file = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(file);
|
||||
Layout layout = mock(Layout.class);
|
||||
LibraryScope scope = mock(LibraryScope.class);
|
||||
given(layout.getLauncherClassName()).willReturn("testLauncher");
|
||||
repackager.setLayout(layout);
|
||||
repackager.repackage((callback) -> callback.library(new Library(libJarFile, scope)));
|
||||
assertThat(getManifest(file).getMainAttributes().getValue("Spring-Boot-Lib")).isNull();
|
||||
assertThat(getManifest(file).getMainAttributes().getValue("Main-Class")).isEqualTo("testLauncher");
|
||||
}
|
||||
|
||||
@Test
|
||||
void springBootVersion() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
File file = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(file);
|
||||
repackager.repackage(NO_LIBRARIES);
|
||||
Manifest actualManifest = getManifest(file);
|
||||
assertThat(actualManifest.getMainAttributes()).containsKey(new Attributes.Name("Spring-Boot-Version"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void executableJarLayoutAttributes() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
File file = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(file);
|
||||
repackager.repackage(NO_LIBRARIES);
|
||||
Manifest actualManifest = getManifest(file);
|
||||
assertThat(actualManifest.getMainAttributes()).containsEntry(new Attributes.Name("Spring-Boot-Lib"),
|
||||
"BOOT-INF/lib/");
|
||||
assertThat(actualManifest.getMainAttributes()).containsEntry(new Attributes.Name("Spring-Boot-Classes"),
|
||||
"BOOT-INF/classes/");
|
||||
}
|
||||
|
||||
@Test
|
||||
void executableWarLayoutAttributes() throws Exception {
|
||||
this.testJarFile.addClass("WEB-INF/classes/a/b/C.class", ClassWithMainMethod.class);
|
||||
File file = this.testJarFile.getFile("war");
|
||||
Repackager repackager = new Repackager(file);
|
||||
repackager.repackage(NO_LIBRARIES);
|
||||
Manifest actualManifest = getManifest(file);
|
||||
assertThat(actualManifest.getMainAttributes()).containsEntry(new Attributes.Name("Spring-Boot-Lib"),
|
||||
"WEB-INF/lib/");
|
||||
assertThat(actualManifest.getMainAttributes()).containsEntry(new Attributes.Name("Spring-Boot-Classes"),
|
||||
"WEB-INF/classes/");
|
||||
}
|
||||
|
||||
@Test
|
||||
void nullCustomLayout() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithoutMainMethod.class);
|
||||
Repackager repackager = new Repackager(this.testJarFile.getFile());
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> repackager.setLayout(null))
|
||||
.withMessageContaining("Layout must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void dontRecompressZips() throws Exception {
|
||||
TestJarFile nested = new TestJarFile(this.tempDir);
|
||||
nested.addClass("a/b/C.class", ClassWithoutMainMethod.class);
|
||||
File nestedFile = nested.getFile();
|
||||
this.testJarFile.addFile("test/nested.jar", nestedFile);
|
||||
this.testJarFile.addClass("A.class", ClassWithMainMethod.class);
|
||||
File file = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(file);
|
||||
repackager.repackage((callback) -> callback.library(new Library(nestedFile, LibraryScope.COMPILE)));
|
||||
|
||||
try (JarFile jarFile = new JarFile(file)) {
|
||||
assertThat(jarFile.getEntry("BOOT-INF/lib/" + nestedFile.getName()).getMethod()).isEqualTo(ZipEntry.STORED);
|
||||
assertThat(jarFile.getEntry("BOOT-INF/classes/test/nested.jar").getMethod()).isEqualTo(ZipEntry.STORED);
|
||||
}
|
||||
Repackager repackager = createRepackager(this.testJarFile.getFile(), true);
|
||||
this.destination.createNewFile();
|
||||
repackager.repackage(this.destination, NO_LIBRARIES);
|
||||
assertThat(hasLauncherClasses(this.destination)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void addLauncherScript() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
File source = this.testJarFile.getFile();
|
||||
File dest = new File(this.tempDir, "dest.jar");
|
||||
Repackager repackager = new Repackager(source);
|
||||
Repackager repackager = createRepackager(source, true);
|
||||
LaunchScript script = new MockLauncherScript("ABC");
|
||||
repackager.repackage(dest, NO_LIBRARIES, script);
|
||||
byte[] bytes = FileCopyUtils.copyToByteArray(dest);
|
||||
repackager.repackage(this.destination, NO_LIBRARIES, script);
|
||||
byte[] bytes = FileCopyUtils.copyToByteArray(this.destination);
|
||||
assertThat(new String(bytes)).startsWith("ABC");
|
||||
assertThat(hasLauncherClasses(source)).isFalse();
|
||||
assertThat(hasLauncherClasses(dest)).isTrue();
|
||||
assertThat(hasLauncherClasses(this.destination)).isTrue();
|
||||
try {
|
||||
assertThat(Files.getPosixFilePermissions(dest.toPath())).contains(PosixFilePermission.OWNER_EXECUTE);
|
||||
assertThat(Files.getPosixFilePermissions(this.destination.toPath()))
|
||||
.contains(PosixFilePermission.OWNER_EXECUTE);
|
||||
}
|
||||
catch (UnsupportedOperationException ex) {
|
||||
// Probably running the test on Windows
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void unpackLibrariesTakePrecedenceOverExistingSourceEntries() throws Exception {
|
||||
TestJarFile nested = new TestJarFile(this.tempDir);
|
||||
nested.addClass("a/b/C.class", ClassWithoutMainMethod.class);
|
||||
File nestedFile = nested.getFile();
|
||||
String name = "BOOT-INF/lib/" + nestedFile.getName();
|
||||
this.testJarFile.addFile(name, nested.getFile());
|
||||
this.testJarFile.addClass("A.class", ClassWithMainMethod.class);
|
||||
File file = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(file);
|
||||
repackager.repackage((callback) -> callback.library(new Library(nestedFile, LibraryScope.COMPILE, true)));
|
||||
try (JarFile jarFile = new JarFile(file)) {
|
||||
assertThat(jarFile.getEntry(name).getComment()).startsWith("UNPACK:");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void existingSourceEntriesTakePrecedenceOverStandardLibraries() throws Exception {
|
||||
TestJarFile nested = new TestJarFile(this.tempDir);
|
||||
nested.addClass("a/b/C.class", ClassWithoutMainMethod.class);
|
||||
File nestedFile = nested.getFile();
|
||||
this.testJarFile.addFile("BOOT-INF/lib/" + nestedFile.getName(), nested.getFile());
|
||||
this.testJarFile.addClass("A.class", ClassWithMainMethod.class);
|
||||
File file = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(file);
|
||||
long sourceLength = nestedFile.length();
|
||||
repackager.repackage((callback) -> {
|
||||
nestedFile.delete();
|
||||
File toZip = new File(this.tempDir, "to-zip");
|
||||
toZip.createNewFile();
|
||||
ZipUtil.packEntry(toZip, nestedFile);
|
||||
callback.library(new Library(nestedFile, LibraryScope.COMPILE));
|
||||
});
|
||||
try (JarFile jarFile = new JarFile(file)) {
|
||||
assertThat(jarFile.getEntry("BOOT-INF/lib/" + nestedFile.getName()).getSize()).isEqualTo(sourceLength);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void metaInfIndexListIsRemovedFromRepackagedJar() throws Exception {
|
||||
this.testJarFile.addClass("A.class", ClassWithMainMethod.class);
|
||||
File indexList = new File(this.tempDir, "INDEX.LIST");
|
||||
indexList.createNewFile();
|
||||
this.testJarFile.addFile("META-INF/INDEX.LIST", indexList);
|
||||
File source = this.testJarFile.getFile();
|
||||
File dest = new File(this.tempDir, "dest.jar");
|
||||
Repackager repackager = new Repackager(source);
|
||||
repackager.repackage(dest, NO_LIBRARIES);
|
||||
try (JarFile jarFile = new JarFile(dest)) {
|
||||
assertThat(jarFile.getEntry("META-INF/INDEX.LIST")).isNull();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void customLayoutFactoryWithoutLayout() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
File source = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(source, new TestLayoutFactory());
|
||||
repackager.repackage(NO_LIBRARIES);
|
||||
JarFile jarFile = new JarFile(source);
|
||||
assertThat(jarFile.getEntry("test")).isNotNull();
|
||||
jarFile.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void customLayoutFactoryWithLayout() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
File source = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(source, new TestLayoutFactory());
|
||||
repackager.setLayout(new Layouts.Jar());
|
||||
repackager.repackage(NO_LIBRARIES);
|
||||
JarFile jarFile = new JarFile(source);
|
||||
assertThat(jarFile.getEntry("test")).isNull();
|
||||
jarFile.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void metaInfAopXmlIsMovedBeneathBootInfClassesWhenRepackaged() throws Exception {
|
||||
this.testJarFile.addClass("A.class", ClassWithMainMethod.class);
|
||||
File aopXml = new File(this.tempDir, "aop.xml");
|
||||
aopXml.createNewFile();
|
||||
this.testJarFile.addFile("META-INF/aop.xml", aopXml);
|
||||
File source = this.testJarFile.getFile();
|
||||
File dest = new File(this.tempDir, "dest.jar");
|
||||
Repackager repackager = new Repackager(source);
|
||||
repackager.repackage(dest, NO_LIBRARIES);
|
||||
try (JarFile jarFile = new JarFile(dest)) {
|
||||
assertThat(jarFile.getEntry("META-INF/aop.xml")).isNull();
|
||||
assertThat(jarFile.getEntry("BOOT-INF/classes/META-INF/aop.xml")).isNotNull();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void allEntriesUseUnixPlatformAndUtf8NameEncoding() throws IOException {
|
||||
this.testJarFile.addClass("A.class", ClassWithMainMethod.class);
|
||||
File source = this.testJarFile.getFile();
|
||||
File dest = new File(this.tempDir, "dest.jar");
|
||||
Repackager repackager = new Repackager(source);
|
||||
repackager.repackage(dest, NO_LIBRARIES);
|
||||
try (ZipFile zip = new ZipFile(dest)) {
|
||||
Enumeration<ZipArchiveEntry> entries = zip.getEntries();
|
||||
while (entries.hasMoreElements()) {
|
||||
ZipArchiveEntry entry = entries.nextElement();
|
||||
assertThat(entry.getPlatform()).isEqualTo(ZipArchiveEntry.PLATFORM_UNIX);
|
||||
assertThat(entry.getGeneralPurposeBit().usesUTF8ForNames()).isTrue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void loaderIsWrittenFirstThenApplicationClassesThenLibraries() throws IOException {
|
||||
this.testJarFile.addClass("com/example/Application.class", ClassWithMainMethod.class);
|
||||
File source = this.testJarFile.getFile();
|
||||
File dest = new File(this.tempDir, "dest.jar");
|
||||
File libraryOne = createLibrary();
|
||||
File libraryTwo = createLibrary();
|
||||
File libraryThree = createLibrary();
|
||||
Repackager repackager = new Repackager(source);
|
||||
repackager.repackage(dest, (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));
|
||||
});
|
||||
assertThat(getEntryNames(dest)).containsSubsequence("org/springframework/boot/loader/",
|
||||
"BOOT-INF/classes/com/example/Application.class", "BOOT-INF/lib/" + libraryOne.getName(),
|
||||
"BOOT-INF/lib/" + libraryTwo.getName(), "BOOT-INF/lib/" + libraryThree.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void existingEntryThatMatchesUnpackLibraryIsMarkedForUnpack() throws IOException {
|
||||
File library = createLibrary();
|
||||
this.testJarFile.addClass("WEB-INF/classes/com/example/Application.class", ClassWithMainMethod.class);
|
||||
this.testJarFile.addFile("WEB-INF/lib/" + library.getName(), library);
|
||||
File source = this.testJarFile.getFile("war");
|
||||
File dest = new File(this.tempDir, "dest.war");
|
||||
Repackager repackager = new Repackager(source);
|
||||
repackager.setLayout(new Layouts.War());
|
||||
repackager.repackage(dest, (callback) -> callback.library(new Library(library, LibraryScope.COMPILE, true)));
|
||||
assertThat(getEntryNames(dest)).containsSubsequence("org/springframework/boot/loader/",
|
||||
"WEB-INF/classes/com/example/Application.class", "WEB-INF/lib/" + library.getName());
|
||||
JarEntry unpackLibrary = getEntry(dest, "WEB-INF/lib/" + library.getName());
|
||||
assertThat(unpackLibrary.getComment()).startsWith("UNPACK:");
|
||||
}
|
||||
|
||||
@Test
|
||||
void layoutCanOmitLibraries() throws IOException {
|
||||
TestJarFile libJar = new TestJarFile(this.tempDir);
|
||||
libJar.addClass("a/b/C.class", ClassWithoutMainMethod.class);
|
||||
File libJarFile = libJar.getFile();
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
File file = this.testJarFile.getFile();
|
||||
Repackager repackager = new Repackager(file);
|
||||
Layout layout = mock(Layout.class);
|
||||
LibraryScope scope = mock(LibraryScope.class);
|
||||
repackager.setLayout(layout);
|
||||
repackager.repackage((callback) -> callback.library(new Library(libJarFile, scope)));
|
||||
assertThat(getEntryNames(file)).containsExactly("META-INF/", "META-INF/MANIFEST.MF", "a/", "a/b/",
|
||||
"a/b/C.class");
|
||||
}
|
||||
|
||||
@Test
|
||||
void jarThatUsesCustomCompressionConfigurationCanBeRepackaged() throws IOException {
|
||||
File source = new File(this.tempDir, "source.jar");
|
||||
ZipOutputStream output = new ZipOutputStream(new FileOutputStream(source)) {
|
||||
{
|
||||
this.def = new Deflater(Deflater.NO_COMPRESSION, true);
|
||||
}
|
||||
};
|
||||
byte[] data = new byte[1024 * 1024];
|
||||
new Random().nextBytes(data);
|
||||
ZipEntry entry = new ZipEntry("entry.dat");
|
||||
output.putNextEntry(entry);
|
||||
output.write(data);
|
||||
output.closeEntry();
|
||||
output.close();
|
||||
File dest = new File(this.tempDir, "dest.jar");
|
||||
Repackager repackager = new Repackager(source);
|
||||
repackager.setMainClass("com.example.Main");
|
||||
repackager.repackage(dest, NO_LIBRARIES);
|
||||
}
|
||||
|
||||
@Test
|
||||
void moduleInfoClassRemainsInRootOfJarWhenRepackaged() throws Exception {
|
||||
this.testJarFile.addClass("A.class", ClassWithMainMethod.class);
|
||||
this.testJarFile.addClass("module-info.class", ClassWithoutMainMethod.class);
|
||||
File source = this.testJarFile.getFile();
|
||||
File dest = new File(this.tempDir, "dest.jar");
|
||||
Repackager repackager = new Repackager(source);
|
||||
repackager.repackage(dest, NO_LIBRARIES);
|
||||
try (JarFile jarFile = new JarFile(dest)) {
|
||||
assertThat(jarFile.getEntry("module-info.class")).isNotNull();
|
||||
assertThat(jarFile.getEntry("BOOT-INF/classes/module-info.class")).isNull();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void kotlinModuleMetadataMovesBeneathBootInfClassesWhenRepackaged() throws Exception {
|
||||
this.testJarFile.addClass("A.class", ClassWithMainMethod.class);
|
||||
File kotlinModule = new File(this.tempDir, "test.kotlin_module");
|
||||
kotlinModule.createNewFile();
|
||||
this.testJarFile.addFile("META-INF/test.kotlin_module", kotlinModule);
|
||||
File source = this.testJarFile.getFile();
|
||||
File dest = new File(this.tempDir, "dest.jar");
|
||||
Repackager repackager = new Repackager(source);
|
||||
repackager.repackage(dest, NO_LIBRARIES);
|
||||
try (JarFile jarFile = new JarFile(dest)) {
|
||||
assertThat(jarFile.getEntry("META-INF/test.kotlin_module")).isNull();
|
||||
assertThat(jarFile.getEntry("BOOT-INF/classes/META-INF/test.kotlin_module")).isNotNull();
|
||||
}
|
||||
}
|
||||
|
||||
private File createLibrary() throws IOException {
|
||||
TestJarFile library = new TestJarFile(this.tempDir);
|
||||
library.addClass("com/example/library/Library.class", ClassWithoutMainMethod.class);
|
||||
return library.getFile();
|
||||
}
|
||||
|
||||
private boolean hasLauncherClasses(File file) throws IOException {
|
||||
return hasEntry(file, "org/springframework/boot/")
|
||||
&& hasEntry(file, "org/springframework/boot/loader/JarLauncher.class");
|
||||
@@ -742,21 +171,51 @@ class RepackagerTests {
|
||||
}
|
||||
}
|
||||
|
||||
private Manifest getManifest(File file) throws IOException {
|
||||
try (JarFile jarFile = new JarFile(file)) {
|
||||
@Override
|
||||
protected Repackager createPackager(File source) {
|
||||
return createRepackager(source, true);
|
||||
}
|
||||
|
||||
private Repackager createRepackager(File source, boolean differentDest) {
|
||||
String ext = StringUtils.getFilenameExtension(source.getName());
|
||||
this.destination = differentDest ? new File(this.tempDir, "dest." + ext) : source;
|
||||
return new Repackager(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void execute(Repackager packager, Libraries libraries) throws IOException {
|
||||
packager.repackage(this.destination, libraries);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Collection<ZipArchiveEntry> getAllPackagedEntries() throws IOException {
|
||||
List<ZipArchiveEntry> result = new ArrayList<>();
|
||||
try (ZipFile zip = new ZipFile(this.destination)) {
|
||||
Enumeration<ZipArchiveEntry> entries = zip.getEntries();
|
||||
while (entries.hasMoreElements()) {
|
||||
result.add(entries.nextElement());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Manifest getPackagedManifest() throws IOException {
|
||||
try (JarFile jarFile = new JarFile(this.destination)) {
|
||||
return jarFile.getManifest();
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> getEntryNames(File file) throws IOException {
|
||||
List<String> entryNames = new ArrayList<>();
|
||||
try (JarFile jarFile = new JarFile(file)) {
|
||||
Enumeration<JarEntry> entries = jarFile.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
entryNames.add(entries.nextElement().getName());
|
||||
@Override
|
||||
protected String getPackagedEntryContent(String name) throws IOException {
|
||||
try (ZipFile zip = new ZipFile(this.destination)) {
|
||||
ZipArchiveEntry entry = zip.getEntry(name);
|
||||
if (entry == null) {
|
||||
return null;
|
||||
}
|
||||
byte[] bytes = FileCopyUtils.copyToByteArray(zip.getInputStream(entry));
|
||||
return new String(bytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
return entryNames;
|
||||
}
|
||||
|
||||
static class MockLauncherScript implements LaunchScript {
|
||||
@@ -774,58 +233,4 @@ class RepackagerTests {
|
||||
|
||||
}
|
||||
|
||||
static class TestLayoutFactory implements LayoutFactory {
|
||||
|
||||
@Override
|
||||
public Layout getLayout(File source) {
|
||||
return new TestLayout();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class TestLayout extends Layouts.Jar implements CustomLoaderLayout {
|
||||
|
||||
@Override
|
||||
public void writeLoadedClasses(LoaderClassesWriter writer) throws IOException {
|
||||
writer.writeEntry("test", new ByteArrayInputStream("test".getBytes()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class TestLayers implements Layers {
|
||||
|
||||
private static final Layer DEFAULT_LAYER = new Layer("default");
|
||||
|
||||
private Set<Layer> layers = new LinkedHashSet<Layer>();
|
||||
|
||||
private Map<String, Layer> libraries = new HashMap<>();
|
||||
|
||||
TestLayers() {
|
||||
this.layers.add(DEFAULT_LAYER);
|
||||
}
|
||||
|
||||
void addLibrary(File jarFile, String layerName) {
|
||||
Layer layer = new Layer(layerName);
|
||||
this.layers.add(layer);
|
||||
this.libraries.put(jarFile.getName(), layer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<Layer> iterator() {
|
||||
return this.layers.iterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Layer getLayer(String name) {
|
||||
return DEFAULT_LAYER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Layer getLayer(Library library) {
|
||||
String name = new File(library.getName()).getName();
|
||||
return this.libraries.getOrDefault(name, DEFAULT_LAYER);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* 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.springframework.boot.loader.tools;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Random;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link SizeCalculatingEntryWriter}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class SizeCalculatingEntryWriterTests {
|
||||
|
||||
@Test
|
||||
void getWhenWithinThreshold() throws Exception {
|
||||
TestEntryWriter original = new TestEntryWriter(SizeCalculatingEntryWriter.THRESHOLD - 1);
|
||||
EntryWriter writer = SizeCalculatingEntryWriter.get(original);
|
||||
assertThat(writer.size()).isEqualTo(original.getBytes().length);
|
||||
assertThat(writeBytes(writer)).isEqualTo(original.getBytes());
|
||||
assertThat(writer).extracting("content").isNotInstanceOf(File.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getWhenExceedingThreshold() throws Exception {
|
||||
TestEntryWriter original = new TestEntryWriter(SizeCalculatingEntryWriter.THRESHOLD + 1);
|
||||
EntryWriter writer = SizeCalculatingEntryWriter.get(original);
|
||||
assertThat(writer.size()).isEqualTo(original.getBytes().length);
|
||||
assertThat(writeBytes(writer)).isEqualTo(original.getBytes());
|
||||
assertThat(writer).extracting("content").isInstanceOf(File.class);
|
||||
}
|
||||
|
||||
private byte[] writeBytes(EntryWriter writer) throws IOException {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
writer.write(outputStream);
|
||||
outputStream.close();
|
||||
return outputStream.toByteArray();
|
||||
}
|
||||
|
||||
private static class TestEntryWriter implements EntryWriter {
|
||||
|
||||
private byte[] bytes;
|
||||
|
||||
TestEntryWriter(int size) {
|
||||
this.bytes = new byte[size];
|
||||
new Random().nextBytes(this.bytes);
|
||||
}
|
||||
|
||||
byte[] getBytes() {
|
||||
return this.bytes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(OutputStream outputStream) throws IOException {
|
||||
outputStream.write(this.bytes);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2019 the original author or authors.
|
||||
* 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.
|
||||
@@ -21,8 +21,6 @@ import java.io.IOException;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.loader.tools.JarWriter.ZipHeaderPeekInputStream;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user