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
pull/19835/head
Phillip Webb 5 years ago
parent aa1954717c
commit 16e6bc89ed

@ -16,11 +16,10 @@ configurations {
dependencies {
api platform(project(':spring-boot-project:spring-boot-parent'))
api "org.apache.commons:commons-compress"
api "org.springframework:spring-core"
compileOnly "ch.qos.logback:logback-classic"
implementation "org.springframework:spring-core"
loader project(":spring-boot-project:spring-boot-tools:spring-boot-loader")
testImplementation "org.assertj:assertj-core"

@ -0,0 +1,388 @@
/*
* 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.BufferedInputStream;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;
import java.util.zip.CRC32;
import java.util.zip.ZipEntry;
import org.apache.commons.compress.archivers.jar.JarArchiveEntry;
import org.apache.commons.compress.archivers.zip.UnixStat;
/**
* Abstract base class for JAR writers.
*
* @author Phillip Webb
* @author Andy Wilkinson
* @author Madhura Bhave
* @since 2.3.0
*/
public abstract class AbstractJarWriter implements LoaderClassesWriter {
private static final String NESTED_LOADER_JAR = "META-INF/loader/spring-boot-loader.jar";
private static final int BUFFER_SIZE = 32 * 1024;
private static final int UNIX_FILE_MODE = UnixStat.FILE_FLAG | UnixStat.DEFAULT_FILE_PERM;
private static final int UNIX_DIR_MODE = UnixStat.DIR_FLAG | UnixStat.DEFAULT_DIR_PERM;
private final Set<String> writtenEntries = new HashSet<>();
/**
* Write the specified manifest.
* @param manifest the manifest to write
* @throws IOException of the manifest cannot be written
*/
public void writeManifest(Manifest manifest) throws IOException {
JarArchiveEntry entry = new JarArchiveEntry("META-INF/MANIFEST.MF");
writeEntry(entry, manifest::write);
}
/**
* Write all entries from the specified jar file.
* @param jarFile the source jar file
* @throws IOException if the entries cannot be written
*/
public void writeEntries(JarFile jarFile) throws IOException {
this.writeEntries(jarFile, EntryTransformer.NONE, UnpackHandler.NEVER);
}
final void writeEntries(JarFile jarFile, EntryTransformer entryTransformer, UnpackHandler unpackHandler)
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);
}
}
}
}
private void setUpEntry(JarFile jarFile, JarArchiveEntry entry) throws IOException {
try (ZipHeaderPeekInputStream inputStream = new ZipHeaderPeekInputStream(jarFile.getInputStream(entry))) {
if (inputStream.hasZipHeader() && entry.getMethod() != ZipEntry.STORED) {
new CrcAndSize(inputStream).setupStoredEntry(entry);
}
else {
entry.setCompressedSize(-1);
}
}
}
/**
* Writes an entry. The {@code inputStream} is closed once the entry has been written
* @param entryName the name of the entry
* @param inputStream the stream from which the entry's data can be read
* @throws IOException if the write fails
*/
@Override
public void writeEntry(String entryName, InputStream inputStream) throws IOException {
JarArchiveEntry entry = new JarArchiveEntry(entryName);
try {
writeEntry(entry, new InputStreamEntryWriter(inputStream));
}
finally {
inputStream.close();
}
}
/**
* Write a nested library.
* @param destination the destination of the library
* @param library the library
* @throws IOException if the write fails
*/
public void writeNestedLibrary(String destination, Library library) throws IOException {
File file = library.getFile();
JarArchiveEntry entry = new JarArchiveEntry(destination + library.getName());
entry.setTime(getNestedLibraryTime(file));
new CrcAndSize(file).setupStoredEntry(entry);
try (FileInputStream input = new FileInputStream(file)) {
writeEntry(entry, new InputStreamEntryWriter(input), new LibraryUnpackHandler(library));
}
}
/**
* Write a simple index file containing the specified UTF-8 lines.
* @param location the location of the index file
* @param lines the lines to write
* @throws IOException if the write fails
* @since 2.3.0
*/
public void writeIndexFile(String location, List<String> lines) throws IOException {
if (location != null) {
JarArchiveEntry entry = new JarArchiveEntry(location);
writeEntry(entry, (outputStream) -> {
BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
for (String line : lines) {
writer.write(line);
writer.write("\n");
}
writer.flush();
});
}
}
private long getNestedLibraryTime(File file) {
try {
try (JarFile jarFile = new JarFile(file)) {
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
if (!entry.isDirectory()) {
return entry.getTime();
}
}
}
}
catch (Exception ex) {
// Ignore and just use the source file timestamp
}
return file.lastModified();
}
/**
* Write the required spring-boot-loader classes to the JAR.
* @throws IOException if the classes cannot be written
*/
@Override
public void writeLoaderClasses() throws IOException {
writeLoaderClasses(NESTED_LOADER_JAR);
}
/**
* Write the required spring-boot-loader classes to the JAR.
* @param loaderJarResourceName the name of the resource containing the loader classes
* to be written
* @throws IOException if the classes cannot be written
*/
@Override
public void writeLoaderClasses(String loaderJarResourceName) throws IOException {
URL loaderJar = getClass().getClassLoader().getResource(loaderJarResourceName);
try (JarInputStream inputStream = new JarInputStream(new BufferedInputStream(loaderJar.openStream()))) {
JarEntry entry;
while ((entry = inputStream.getNextJarEntry()) != null) {
if (entry.getName().endsWith(".class")) {
writeEntry(new JarArchiveEntry(entry), new InputStreamEntryWriter(inputStream));
}
}
}
}
private void writeEntry(JarArchiveEntry entry, EntryWriter entryWriter) throws IOException {
writeEntry(entry, entryWriter, UnpackHandler.NEVER);
}
/**
* Perform the actual write of a {@link JarEntry}. All other write methods delegate to
* this one.
* @param entry the entry to write
* @param entryWriter the entry writer or {@code null} if there is no content
* @param unpackHandler handles possible unpacking for the entry
* @throws IOException in case of I/O errors
*/
private void writeEntry(JarArchiveEntry entry, EntryWriter entryWriter, UnpackHandler unpackHandler)
throws IOException {
String name = entry.getName();
writeParentFolderEntries(name);
if (this.writtenEntries.add(name)) {
entry.setUnixMode(name.endsWith("/") ? UNIX_DIR_MODE : UNIX_FILE_MODE);
entry.getGeneralPurposeBit().useUTF8ForNames(true);
if (!entry.isDirectory() && entry.getSize() == -1) {
entryWriter = SizeCalculatingEntryWriter.get(entryWriter);
entry.setSize(entryWriter.size());
}
entryWriter = addUnpackCommentIfNecessary(entry, entryWriter, unpackHandler);
writeToArchive(entry, entryWriter);
}
}
protected abstract void writeToArchive(ZipEntry entry, EntryWriter entryWriter) throws IOException;
private void writeParentFolderEntries(String name) throws IOException {
String parent = name.endsWith("/") ? name.substring(0, name.length() - 1) : name;
while (parent.lastIndexOf('/') != -1) {
parent = parent.substring(0, parent.lastIndexOf('/'));
if (!parent.isEmpty()) {
writeEntry(new JarArchiveEntry(parent + "/"), null, UnpackHandler.NEVER);
}
}
}
private EntryWriter addUnpackCommentIfNecessary(JarArchiveEntry entry, EntryWriter entryWriter,
UnpackHandler unpackHandler) throws IOException {
if (entryWriter == null || !unpackHandler.requiresUnpack(entry.getName())) {
return entryWriter;
}
ByteArrayOutputStream output = new ByteArrayOutputStream();
entryWriter.write(output);
entry.setComment("UNPACK:" + unpackHandler.sha1Hash(entry.getName()));
return new InputStreamEntryWriter(new ByteArrayInputStream(output.toByteArray()));
}
/**
* {@link EntryWriter} that writes content from an {@link InputStream}.
*/
private static class InputStreamEntryWriter implements EntryWriter {
private final InputStream inputStream;
InputStreamEntryWriter(InputStream inputStream) {
this.inputStream = inputStream;
}
@Override
public void write(OutputStream outputStream) throws IOException {
byte[] buffer = new byte[BUFFER_SIZE];
int bytesRead;
while ((bytesRead = this.inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.flush();
}
}
/**
* Data holder for CRC and Size.
*/
private static class CrcAndSize {
private final CRC32 crc = new CRC32();
private long size;
CrcAndSize(File file) throws IOException {
try (FileInputStream inputStream = new FileInputStream(file)) {
load(inputStream);
}
}
CrcAndSize(InputStream inputStream) throws IOException {
load(inputStream);
}
private void load(InputStream inputStream) throws IOException {
byte[] buffer = new byte[BUFFER_SIZE];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
this.crc.update(buffer, 0, bytesRead);
this.size += bytesRead;
}
}
void setupStoredEntry(JarArchiveEntry entry) {
entry.setSize(this.size);
entry.setCompressedSize(this.size);
entry.setCrc(this.crc.getValue());
entry.setMethod(ZipEntry.STORED);
}
}
/**
* An {@code EntryTransformer} enables the transformation of {@link JarEntry jar
* entries} during the writing process.
*/
@FunctionalInterface
interface EntryTransformer {
/**
* No-op entity transformer.
*/
EntryTransformer NONE = (jarEntry) -> jarEntry;
JarArchiveEntry transform(JarArchiveEntry jarEntry);
}
/**
* An {@code UnpackHandler} determines whether or not unpacking is required and
* provides a SHA1 hash if required.
*/
interface UnpackHandler {
UnpackHandler NEVER = new UnpackHandler() {
@Override
public boolean requiresUnpack(String name) {
return false;
}
@Override
public String sha1Hash(String name) throws IOException {
throw new UnsupportedOperationException();
}
};
boolean requiresUnpack(String name);
String sha1Hash(String name) throws IOException;
}
/**
* {@link UnpackHandler} backed by a {@link Library}.
*/
private static final class LibraryUnpackHandler implements UnpackHandler {
private final Library library;
private LibraryUnpackHandler(Library library) {
this.library = library;
}
@Override
public boolean requiresUnpack(String name) {
return this.library.isUnpackRequired();
}
@Override
public String sha1Hash(String name) throws IOException {
return FileUtils.sha1Hash(this.library.getFile());
}
}
}

@ -0,0 +1,47 @@
/*
* 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.IOException;
import java.io.OutputStream;
/**
* Interface used to write jar entry data.
*
* @author Phillip Webb
* @since 2.3.0
*/
@FunctionalInterface
public interface EntryWriter {
/**
* Write entry data to the specified output stream.
* @param outputStream the destination for the data
* @throws IOException in case of I/O errors
*/
void write(OutputStream outputStream) throws IOException;
/**
* Return the size of the content that will be written, or {@code -1} if the size is
* not known.
* @return the size of the content
*/
default int size() {
return -1;
}
}

@ -0,0 +1,80 @@
/*
* 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.File;
import java.io.IOException;
import java.util.function.BiConsumer;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;
import org.springframework.util.Assert;
/**
* Utility class that can be used to export a fully packaged archive to an OCI image.
*
* @author Phillip Webb
* @since 2.3.0
*/
public class ImagePackager extends Packager {
/**
* Create a new {@link ImagePackager} instance.
* @param source the source file to package
*/
public ImagePackager(File source) {
super(source, null);
}
/**
* Create an packaged image.
* @param libraries the contained libraries
* @param exporter the exporter used to write the image
* @throws IOException on IO error
*/
public void packageImage(Libraries libraries, BiConsumer<ZipEntry, EntryWriter> exporter) throws IOException {
packageImage(libraries, new DelegatingJarWriter(exporter));
}
private void packageImage(Libraries libraries, AbstractJarWriter writer) throws IOException {
File source = isAlreadyPackaged() ? getBackupFile() : getSource();
Assert.state(source.exists() && source.isFile(), "Unable to read jar file " + source);
Assert.state(!isAlreadyPackaged(source), "Repackaged jar file " + source + " cannot be exported");
try (JarFile sourceJar = new JarFile(source)) {
write(sourceJar, libraries, writer);
}
}
/**
* {@link AbstractJarWriter} that delegates to a {@link BiConsumer}.
*/
private static class DelegatingJarWriter extends AbstractJarWriter {
private BiConsumer<ZipEntry, EntryWriter> exporter;
DelegatingJarWriter(BiConsumer<ZipEntry, EntryWriter> exporter) {
this.exporter = exporter;
}
@Override
protected void writeToArchive(ZipEntry entry, EntryWriter entryWriter) throws IOException {
this.exporter.accept(entry, entryWriter);
}
}
}

@ -16,39 +16,20 @@
package org.springframework.boot.loader.tools;
import java.io.BufferedInputStream;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;
import java.util.zip.CRC32;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import org.apache.commons.compress.archivers.jar.JarArchiveEntry;
import org.apache.commons.compress.archivers.jar.JarArchiveOutputStream;
import org.apache.commons.compress.archivers.zip.UnixStat;
/**
* Writes JAR content, ensuring valid directory entries are always created and duplicate
@ -59,19 +40,9 @@ import org.apache.commons.compress.archivers.zip.UnixStat;
* @author Madhura Bhave
* @since 1.0.0
*/
public class JarWriter implements LoaderClassesWriter, AutoCloseable {
public class JarWriter extends AbstractJarWriter implements AutoCloseable {
private static final String NESTED_LOADER_JAR = "META-INF/loader/spring-boot-loader.jar";
private static final int BUFFER_SIZE = 32 * 1024;
private static final int UNIX_FILE_MODE = UnixStat.FILE_FLAG | UnixStat.DEFAULT_FILE_PERM;
private static final int UNIX_DIR_MODE = UnixStat.DIR_FLAG | UnixStat.DEFAULT_DIR_PERM;
private final JarArchiveOutputStream jarOutput;
private final Set<String> writtenEntries = new HashSet<>();
private final JarArchiveOutputStream jarOutputStream;
/**
* Create a new {@link JarWriter} instance.
@ -96,8 +67,8 @@ public class JarWriter implements LoaderClassesWriter, AutoCloseable {
fileOutputStream.write(launchScript.toByteArray());
setExecutableFilePermission(file);
}
this.jarOutput = new JarArchiveOutputStream(fileOutputStream);
this.jarOutput.setEncoding("UTF-8");
this.jarOutputStream = new JarArchiveOutputStream(fileOutputStream);
this.jarOutputStream.setEncoding("UTF-8");
}
private void setExecutableFilePermission(File file) {
@ -112,155 +83,20 @@ public class JarWriter implements LoaderClassesWriter, AutoCloseable {
}
}
/**
* Write the specified manifest.
* @param manifest the manifest to write
* @throws IOException of the manifest cannot be written
*/
public void writeManifest(Manifest manifest) throws IOException {
JarArchiveEntry entry = new JarArchiveEntry("META-INF/MANIFEST.MF");
writeEntry(entry, manifest::write);
}
/**
* Write all entries from the specified jar file.
* @param jarFile the source jar file
* @throws IOException if the entries cannot be written
*/
public void writeEntries(JarFile jarFile) throws IOException {
this.writeEntries(jarFile, EntryTransformer.NONE, UnpackHandler.NEVER);
}
void writeEntries(JarFile jarFile, UnpackHandler unpackHandler) throws IOException {
this.writeEntries(jarFile, EntryTransformer.NONE, unpackHandler);
}
void writeEntries(JarFile jarFile, EntryTransformer entryTransformer, UnpackHandler unpackHandler)
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);
}
}
}
}
private void setUpEntry(JarFile jarFile, JarArchiveEntry entry) throws IOException {
try (ZipHeaderPeekInputStream inputStream = new ZipHeaderPeekInputStream(jarFile.getInputStream(entry))) {
if (inputStream.hasZipHeader() && entry.getMethod() != ZipEntry.STORED) {
new CrcAndSize(inputStream).setupStoredEntry(entry);
}
else {
entry.setCompressedSize(-1);
}
}
}
/**
* Writes an entry. The {@code inputStream} is closed once the entry has been written
* @param entryName the name of the entry
* @param inputStream the stream from which the entry's data can be read
* @throws IOException if the write fails
*/
@Override
public void writeEntry(String entryName, InputStream inputStream) throws IOException {
JarArchiveEntry entry = new JarArchiveEntry(entryName);
try {
writeEntry(entry, new InputStreamEntryWriter(inputStream));
}
finally {
inputStream.close();
}
}
/**
* Write a nested library.
* @param destination the destination of the library
* @param library the library
* @throws IOException if the write fails
*/
public void writeNestedLibrary(String destination, Library library) throws IOException {
File file = library.getFile();
JarArchiveEntry entry = new JarArchiveEntry(destination + library.getName());
entry.setTime(getNestedLibraryTime(file));
new CrcAndSize(file).setupStoredEntry(entry);
try (FileInputStream input = new FileInputStream(file)) {
writeEntry(entry, new InputStreamEntryWriter(input), new LibraryUnpackHandler(library));
}
}
/**
* Write a simple index file containing the specified UTF-8 lines.
* @param location the location of the index file
* @param lines the lines to write
* @throws IOException if the write fails
* @since 2.3.0
*/
public void writeIndexFile(String location, List<String> lines) throws IOException {
if (location != null) {
JarArchiveEntry entry = new JarArchiveEntry(location);
writeEntry(entry, (outputStream) -> {
BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
for (String line : lines) {
writer.write(line);
writer.write("\n");
}
writer.flush();
});
}
}
private long getNestedLibraryTime(File file) {
try {
try (JarFile jarFile = new JarFile(file)) {
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
if (!entry.isDirectory()) {
return entry.getTime();
}
}
}
}
catch (Exception ex) {
// Ignore and just use the source file timestamp
}
return file.lastModified();
protected void writeToArchive(ZipEntry entry, EntryWriter entryWriter) throws IOException {
this.jarOutputStream.putArchiveEntry(asJarArchiveEntry(entry));
if (entryWriter != null) {
entryWriter.write(this.jarOutputStream);
}
/**
* Write the required spring-boot-loader classes to the JAR.
* @throws IOException if the classes cannot be written
*/
@Override
public void writeLoaderClasses() throws IOException {
writeLoaderClasses(NESTED_LOADER_JAR);
this.jarOutputStream.closeArchiveEntry();
}
/**
* Write the required spring-boot-loader classes to the JAR.
* @param loaderJarResourceName the name of the resource containing the loader classes
* to be written
* @throws IOException if the classes cannot be written
*/
@Override
public void writeLoaderClasses(String loaderJarResourceName) throws IOException {
URL loaderJar = getClass().getClassLoader().getResource(loaderJarResourceName);
try (JarInputStream inputStream = new JarInputStream(new BufferedInputStream(loaderJar.openStream()))) {
JarEntry entry;
while ((entry = inputStream.getNextJarEntry()) != null) {
if (entry.getName().endsWith(".class")) {
writeEntry(new JarArchiveEntry(entry), new InputStreamEntryWriter(inputStream));
}
}
private JarArchiveEntry asJarArchiveEntry(ZipEntry entry) throws ZipException {
if (entry instanceof JarArchiveEntry) {
return (JarArchiveEntry) entry;
}
return new JarArchiveEntry(entry);
}
/**
@ -269,264 +105,7 @@ public class JarWriter implements LoaderClassesWriter, AutoCloseable {
*/
@Override
public void close() throws IOException {
this.jarOutput.close();
}
private void writeEntry(JarArchiveEntry entry, EntryWriter entryWriter) throws IOException {
writeEntry(entry, entryWriter, UnpackHandler.NEVER);
}
/**
* Perform the actual write of a {@link JarEntry}. All other write methods delegate to
* this one.
* @param entry the entry to write
* @param entryWriter the entry writer or {@code null} if there is no content
* @param unpackHandler handles possible unpacking for the entry
* @throws IOException in case of I/O errors
*/
private void writeEntry(JarArchiveEntry entry, EntryWriter entryWriter, UnpackHandler unpackHandler)
throws IOException {
String name = entry.getName();
writeParentFolderEntries(name);
if (this.writtenEntries.add(name)) {
entry.setUnixMode(name.endsWith("/") ? UNIX_DIR_MODE : UNIX_FILE_MODE);
entryWriter = addUnpackCommentIfNecessary(entry, entryWriter, unpackHandler);
this.jarOutput.putArchiveEntry(entry);
if (entryWriter != null) {
entryWriter.write(this.jarOutput);
}
this.jarOutput.closeArchiveEntry();
}
}
private void writeParentFolderEntries(String name) throws IOException {
String parent = name.endsWith("/") ? name.substring(0, name.length() - 1) : name;
while (parent.lastIndexOf('/') != -1) {
parent = parent.substring(0, parent.lastIndexOf('/'));
if (!parent.isEmpty()) {
writeEntry(new JarArchiveEntry(parent + "/"), null, UnpackHandler.NEVER);
}
}
}
private EntryWriter addUnpackCommentIfNecessary(JarArchiveEntry entry, EntryWriter entryWriter,
UnpackHandler unpackHandler) throws IOException {
if (entryWriter == null || !unpackHandler.requiresUnpack(entry.getName())) {
return entryWriter;
}
ByteArrayOutputStream output = new ByteArrayOutputStream();
entryWriter.write(output);
entry.setComment("UNPACK:" + unpackHandler.sha1Hash(entry.getName()));
return new InputStreamEntryWriter(new ByteArrayInputStream(output.toByteArray()));
}
/**
* Interface used to write jar entry date.
*/
private interface EntryWriter {
/**
* Write entry data to the specified output stream.
* @param outputStream the destination for the data
* @throws IOException in case of I/O errors
*/
void write(OutputStream outputStream) throws IOException;
}
/**
* {@link EntryWriter} that writes content from an {@link InputStream}.
*/
private static class InputStreamEntryWriter implements EntryWriter {
private final InputStream inputStream;
InputStreamEntryWriter(InputStream inputStream) {
this.inputStream = inputStream;
}
@Override
public void write(OutputStream outputStream) throws IOException {
byte[] buffer = new byte[BUFFER_SIZE];
int bytesRead;
while ((bytesRead = this.inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.flush();
}
}
/**
* {@link InputStream} that can peek ahead at zip header bytes.
*/
static class ZipHeaderPeekInputStream extends FilterInputStream {
private static final byte[] ZIP_HEADER = new byte[] { 0x50, 0x4b, 0x03, 0x04 };
private final byte[] header;
private final int headerLength;
private int position;
private ByteArrayInputStream headerStream;
protected ZipHeaderPeekInputStream(InputStream in) throws IOException {
super(in);
this.header = new byte[4];
this.headerLength = in.read(this.header);
this.headerStream = new ByteArrayInputStream(this.header, 0, this.headerLength);
}
@Override
public int read() throws IOException {
int read = (this.headerStream != null) ? this.headerStream.read() : -1;
if (read != -1) {
this.position++;
if (this.position >= this.headerLength) {
this.headerStream = null;
}
return read;
}
return super.read();
}
@Override
public int read(byte[] b) throws IOException {
return read(b, 0, b.length);
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int read = (this.headerStream != null) ? this.headerStream.read(b, off, len) : -1;
if (read <= 0) {
return readRemainder(b, off, len);
}
this.position += read;
if (read < len) {
int remainderRead = readRemainder(b, off + read, len - read);
if (remainderRead > 0) {
read += remainderRead;
}
}
if (this.position >= this.headerLength) {
this.headerStream = null;
}
return read;
}
boolean hasZipHeader() {
return Arrays.equals(this.header, ZIP_HEADER);
}
private int readRemainder(byte[] b, int off, int len) throws IOException {
int read = super.read(b, off, len);
if (read > 0) {
this.position += read;
}
return read;
}
}
/**
* Data holder for CRC and Size.
*/
private static class CrcAndSize {
private final CRC32 crc = new CRC32();
private long size;
CrcAndSize(File file) throws IOException {
try (FileInputStream inputStream = new FileInputStream(file)) {
load(inputStream);
}
}
CrcAndSize(InputStream inputStream) throws IOException {
load(inputStream);
}
private void load(InputStream inputStream) throws IOException {
byte[] buffer = new byte[BUFFER_SIZE];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
this.crc.update(buffer, 0, bytesRead);
this.size += bytesRead;
}
}
void setupStoredEntry(JarArchiveEntry entry) {
entry.setSize(this.size);
entry.setCompressedSize(this.size);
entry.setCrc(this.crc.getValue());
entry.setMethod(ZipEntry.STORED);
}
}
/**
* An {@code EntryTransformer} enables the transformation of {@link JarEntry jar
* entries} during the writing process.
*/
@FunctionalInterface
interface EntryTransformer {
/**
* No-op entity transformer.
*/
EntryTransformer NONE = (jarEntry) -> jarEntry;
JarArchiveEntry transform(JarArchiveEntry jarEntry);
}
/**
* An {@code UnpackHandler} determines whether or not unpacking is required and
* provides a SHA1 hash if required.
*/
interface UnpackHandler {
UnpackHandler NEVER = new UnpackHandler() {
@Override
public boolean requiresUnpack(String name) {
return false;
}
@Override
public String sha1Hash(String name) throws IOException {
throw new UnsupportedOperationException();
}
};
boolean requiresUnpack(String name);
String sha1Hash(String name) throws IOException;
}
private static final class LibraryUnpackHandler implements UnpackHandler {
private final Library library;
private LibraryUnpackHandler(Library library) {
this.library = library;
}
@Override
public boolean requiresUnpack(String name) {
return this.library.isUnpackRequired();
}
@Override
public String sha1Hash(String name) throws IOException {
return FileUtils.sha1Hash(this.library.getFile());
}
this.jarOutputStream.close();
}
}

@ -0,0 +1,468 @@
/*
* 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.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import org.apache.commons.compress.archivers.jar.JarArchiveEntry;
import org.springframework.boot.loader.tools.AbstractJarWriter.EntryTransformer;
import org.springframework.boot.loader.tools.AbstractJarWriter.UnpackHandler;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Abstract base class for packagers.
*
* @author Phillip Webb
* @author Andy Wilkinson
* @author Stephane Nicoll
* @author Madhura Bhave
* @since 2.3.0
*/
public abstract class Packager {
private static final String MAIN_CLASS_ATTRIBUTE = "Main-Class";
private static final String START_CLASS_ATTRIBUTE = "Start-Class";
private static final String BOOT_VERSION_ATTRIBUTE = "Spring-Boot-Version";
private static final String BOOT_CLASSES_ATTRIBUTE = "Spring-Boot-Classes";
private static final String BOOT_LIB_ATTRIBUTE = "Spring-Boot-Lib";
private static final String BOOT_CLASSPATH_INDEX_ATTRIBUTE = "Spring-Boot-Classpath-Index";
private static final String BOOT_LAYERS_INDEX_ATTRIBUTE = "Spring-Boot-Layers-Index";
private static final byte[] ZIP_FILE_HEADER = new byte[] { 'P', 'K', 3, 4 };
private static final long FIND_WARNING_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
private static final String SPRING_BOOT_APPLICATION_CLASS_NAME = "org.springframework.boot.autoconfigure.SpringBootApplication";
private List<MainClassTimeoutWarningListener> mainClassTimeoutListeners = new ArrayList<>();
private String mainClass;
private final File source;
private Layout layout;
private LayoutFactory layoutFactory;
private Layers layers = Layers.IMPLICIT;
/**
* Create a new {@link Packager} instance.
* @param source the source JAR file to package
* @param layoutFactory the layout factory to use or {@code null}
*/
protected Packager(File source, LayoutFactory layoutFactory) {
Assert.notNull(source, "Source file must not be null");
Assert.isTrue(source.exists() && source.isFile(),
"Source must refer to an existing file, got " + source.getAbsolutePath());
this.source = source.getAbsoluteFile();
this.layoutFactory = layoutFactory;
}
/**
* Add a listener that will be triggered to display a warning if searching for the
* main class takes too long.
* @param listener the listener to add
*/
public void addMainClassTimeoutWarningListener(MainClassTimeoutWarningListener listener) {
this.mainClassTimeoutListeners.add(listener);
}
/**
* Sets the main class that should be run. If not specified the value from the
* MANIFEST will be used, or if no manifest entry is found the archive will be
* searched for a suitable class.
* @param mainClass the main class name
*/
public void setMainClass(String mainClass) {
this.mainClass = mainClass;
}
/**
* Sets the layout to use for the jar. Defaults to {@link Layouts#forFile(File)}.
* @param layout the layout
*/
public void setLayout(Layout layout) {
Assert.notNull(layout, "Layout must not be null");
this.layout = layout;
}
/**
* Sets the layout factory for the jar. The factory can be used when no specific
* layout is specified.
* @param layoutFactory the layout factory to set
*/
public void setLayoutFactory(LayoutFactory layoutFactory) {
this.layoutFactory = layoutFactory;
}
/**
* Sets the layers that should be used in the jar.
* @param layers the jar layers
* @see LayeredLayout
*/
public void setLayers(Layers layers) {
Assert.notNull(layers, "Layers must not be null");
this.layers = layers;
}
protected final boolean isAlreadyPackaged() throws IOException {
return isAlreadyPackaged(this.source);
}
protected final boolean isAlreadyPackaged(File file) throws IOException {
try (JarFile jarFile = new JarFile(file)) {
Manifest manifest = jarFile.getManifest();
return (manifest != null && manifest.getMainAttributes().getValue(BOOT_VERSION_ATTRIBUTE) != null);
}
}
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);
writer.writeManifest(buildManifest(sourceJar));
writeLoaderClasses(writer);
writer.writeEntries(sourceJar, getEntityTransformer(), writeableLibraries);
writeableLibraries.write(writer);
}
private void writeLoaderClasses(AbstractJarWriter writer) throws IOException {
Layout layout = getLayout();
if (layout instanceof CustomLoaderLayout) {
((CustomLoaderLayout) getLayout()).writeLoadedClasses(writer);
}
else if (layout.isExecutable()) {
writer.writeLoaderClasses();
}
}
private EntryTransformer getEntityTransformer() {
if (getLayout() instanceof RepackagingLayout) {
return new RepackagingEntryTransformer((RepackagingLayout) getLayout(), this.layers);
}
return EntryTransformer.NONE;
}
private boolean isZip(File file) {
try {
try (FileInputStream inputStream = new FileInputStream(file)) {
return isZip(inputStream);
}
}
catch (IOException ex) {
return false;
}
}
private boolean isZip(InputStream inputStream) throws IOException {
for (byte magicByte : ZIP_FILE_HEADER) {
if (inputStream.read() != magicByte) {
return false;
}
}
return true;
}
private Manifest buildManifest(JarFile source) throws IOException {
Manifest manifest = createInitialManifest(source);
addMainAndStartAttributes(source, manifest);
addBootAttributes(manifest.getMainAttributes());
return manifest;
}
private Manifest createInitialManifest(JarFile source) throws IOException {
if (source.getManifest() != null) {
return new Manifest(source.getManifest());
}
Manifest manifest = new Manifest();
manifest.getMainAttributes().putValue("Manifest-Version", "1.0");
return manifest;
}
private void addMainAndStartAttributes(JarFile source, Manifest manifest) throws IOException {
String mainClass = getMainClass(source, manifest);
String launcherClass = getLayout().getLauncherClassName();
if (launcherClass != null) {
Assert.state(mainClass != null, "Unable to find main class");
manifest.getMainAttributes().putValue(MAIN_CLASS_ATTRIBUTE, launcherClass);
manifest.getMainAttributes().putValue(START_CLASS_ATTRIBUTE, mainClass);
}
else if (mainClass != null) {
manifest.getMainAttributes().putValue(MAIN_CLASS_ATTRIBUTE, mainClass);
}
}
private String getMainClass(JarFile source, Manifest manifest) throws IOException {
if (this.mainClass != null) {
return this.mainClass;
}
String attributeValue = manifest.getMainAttributes().getValue(MAIN_CLASS_ATTRIBUTE);
if (attributeValue != null) {
return attributeValue;
}
return findMainMethodWithTimeoutWarning(source);
}
private String findMainMethodWithTimeoutWarning(JarFile source) throws IOException {
long startTime = System.currentTimeMillis();
String mainMethod = findMainMethod(source);
long duration = System.currentTimeMillis() - startTime;
if (duration > FIND_WARNING_TIMEOUT) {
for (MainClassTimeoutWarningListener listener : this.mainClassTimeoutListeners) {
listener.handleTimeoutWarning(duration, mainMethod);
}
}
return mainMethod;
}
protected String findMainMethod(JarFile source) throws IOException {
return MainClassFinder.findSingleMainClass(source, getLayout().getClassesLocation(),
SPRING_BOOT_APPLICATION_CLASS_NAME);
}
/**
* Return the {@link File} to use to backup the original source.
* @return the file to use to backup the original source
*/
public final File getBackupFile() {
File source = getSource();
return new File(source.getParentFile(), source.getName() + ".original");
}
protected final File getSource() {
return this.source;
}
protected final Layout getLayout() {
if (this.layout == null) {
Layout createdLayout = getLayoutFactory().getLayout(this.source);
Assert.state(createdLayout != null, "Unable to detect layout");
this.layout = createdLayout;
}
return this.layout;
}
private LayoutFactory getLayoutFactory() {
if (this.layoutFactory != null) {
return this.layoutFactory;
}
List<LayoutFactory> factories = SpringFactoriesLoader.loadFactories(LayoutFactory.class, null);
if (factories.isEmpty()) {
return new DefaultLayoutFactory();
}
Assert.state(factories.size() == 1, "No unique LayoutFactory found");
return factories.get(0);
}
private void addBootAttributes(Attributes attributes) {
attributes.putValue(BOOT_VERSION_ATTRIBUTE, getClass().getPackage().getImplementationVersion());
Layout layout = getLayout();
if (layout instanceof LayeredLayout) {
addBootBootAttributesForLayeredLayout(attributes, (LayeredLayout) layout);
}
else if (layout instanceof RepackagingLayout) {
addBootBootAttributesForRepackagingLayout(attributes, (RepackagingLayout) layout);
}
else {
addBootBootAttributesForPlainLayout(attributes, layout);
}
}
private void addBootBootAttributesForLayeredLayout(Attributes attributes, LayeredLayout layout) {
String layersIndexFileLocation = layout.getLayersIndexFileLocation();
putIfHasLength(attributes, BOOT_LAYERS_INDEX_ATTRIBUTE, layersIndexFileLocation);
putIfHasLength(attributes, BOOT_CLASSPATH_INDEX_ATTRIBUTE, layout.getClasspathIndexFileLocation());
}
private void addBootBootAttributesForRepackagingLayout(Attributes attributes, RepackagingLayout layout) {
attributes.putValue(BOOT_CLASSES_ATTRIBUTE, layout.getRepackagedClassesLocation());
putIfHasLength(attributes, BOOT_LIB_ATTRIBUTE, getLayout().getLibraryLocation("", LibraryScope.COMPILE));
putIfHasLength(attributes, BOOT_CLASSPATH_INDEX_ATTRIBUTE, layout.getClasspathIndexFileLocation());
}
private void addBootBootAttributesForPlainLayout(Attributes attributes, Layout layout) {
attributes.putValue(BOOT_CLASSES_ATTRIBUTE, getLayout().getClassesLocation());
putIfHasLength(attributes, BOOT_LIB_ATTRIBUTE, getLayout().getLibraryLocation("", LibraryScope.COMPILE));
}
private void putIfHasLength(Attributes attributes, String name, String value) {
if (StringUtils.hasLength(value)) {
attributes.putValue(name, value);
}
}
/**
* Callback interface used to present a warning when finding the main class takes too
* long.
*/
@FunctionalInterface
public interface MainClassTimeoutWarningListener {
/**
* Handle a timeout warning.
* @param duration the amount of time it took to find the main method
* @param mainMethod the main method that was actually found
*/
void handleTimeoutWarning(long duration, String mainMethod);
}
/**
* An {@code EntryTransformer} that renames entries by applying a prefix.
*/
private static final class RepackagingEntryTransformer implements EntryTransformer {
private final RepackagingLayout layout;
private final Layers layers;
private RepackagingEntryTransformer(RepackagingLayout layout, Layers layers) {
this.layout = layout;
this.layers = layers;
}
@Override
public JarArchiveEntry transform(JarArchiveEntry entry) {
if (entry.getName().equals("META-INF/INDEX.LIST")) {
return null;
}
if (!isTransformable(entry)) {
return entry;
}
String transformedName = transformName(entry.getName());
JarArchiveEntry transformedEntry = new JarArchiveEntry(transformedName);
transformedEntry.setTime(entry.getTime());
transformedEntry.setSize(entry.getSize());
transformedEntry.setMethod(entry.getMethod());
if (entry.getComment() != null) {
transformedEntry.setComment(entry.getComment());
}
transformedEntry.setCompressedSize(entry.getCompressedSize());
transformedEntry.setCrc(entry.getCrc());
if (entry.getCreationTime() != null) {
transformedEntry.setCreationTime(entry.getCreationTime());
}
if (entry.getExtra() != null) {
transformedEntry.setExtra(entry.getExtra());
}
if (entry.getLastAccessTime() != null) {
transformedEntry.setLastAccessTime(entry.getLastAccessTime());
}
if (entry.getLastModifiedTime() != null) {
transformedEntry.setLastModifiedTime(entry.getLastModifiedTime());
}
return transformedEntry;
}
private String transformName(String name) {
if (this.layout instanceof LayeredLayout) {
Layer layer = this.layers.getLayer(name);
Assert.state(layer != null, "Invalid 'null' layer from " + this.layers.getClass().getName());
return ((LayeredLayout) this.layout).getRepackagedClassesLocation(layer) + name;
}
return this.layout.getRepackagedClassesLocation() + name;
}
private boolean isTransformable(JarArchiveEntry entry) {
String name = entry.getName();
if (name.startsWith("META-INF/")) {
return name.equals("META-INF/aop.xml") || name.endsWith(".kotlin_module");
}
return !name.startsWith("BOOT-INF/") && !name.equals("module-info.class");
}
}
/**
* 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 Map<String, Library> libraryEntryNames = new LinkedHashMap<>();
WritableLibraries(Libraries libraries) throws IOException {
libraries.doWithLibraries((library) -> {
if (isZip(library.getFile())) {
String location = getLocation(library);
if (location != null) {
Library existing = this.libraryEntryNames.putIfAbsent(location + library.getName(), library);
Assert.state(existing == null, "Duplicate library " + library.getName());
}
}
});
}
private String getLocation(Library library) {
Layout layout = getLayout();
if (layout instanceof LayeredLayout) {
Layers layers = Packager.this.layers;
Layer layer = layers.getLayer(library);
Assert.state(layer != null, "Invalid 'null' library layer from " + layers.getClass().getName());
return ((LayeredLayout) layout).getLibraryLocation(library.getName(), library.getScope(), layer);
}
return layout.getLibraryLocation(library.getName(), library.getScope());
}
@Override
public boolean requiresUnpack(String name) {
Library library = this.libraryEntryNames.get(name);
return library != null && library.isUnpackRequired();
}
@Override
public String sha1Hash(String name) throws IOException {
Library library = this.libraryEntryNames.get(name);
Assert.notNull(library, "No library found for entry name '" + name + "'");
return FileUtils.sha1Hash(library.getFile());
}
private void write(AbstractJarWriter writer) throws IOException {
for (Entry<String, Library> entry : this.libraryEntryNames.entrySet()) {
writer.writeNestedLibrary(entry.getKey().substring(0, entry.getKey().lastIndexOf('/') + 1),
entry.getValue());
}
if (getLayout() instanceof RepackagingLayout) {
String location = ((RepackagingLayout) getLayout()).getClasspathIndexFileLocation();
writer.writeIndexFile(location, new ArrayList<>(this.libraryEntryNames.keySet()));
}
}
}
}

@ -17,26 +17,10 @@
package org.springframework.boot.loader.tools;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import org.apache.commons.compress.archivers.jar.JarArchiveEntry;
import org.springframework.boot.loader.tools.JarWriter.EntryTransformer;
import org.springframework.boot.loader.tools.JarWriter.UnpackHandler;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Utility class that can be used to repackage an archive so that it can be executed using
@ -48,71 +32,16 @@ import org.springframework.util.StringUtils;
* @author Madhura Bhave
* @since 1.0.0
*/
public class Repackager {
private static final String MAIN_CLASS_ATTRIBUTE = "Main-Class";
private static final String START_CLASS_ATTRIBUTE = "Start-Class";
private static final String BOOT_VERSION_ATTRIBUTE = "Spring-Boot-Version";
private static final String BOOT_CLASSES_ATTRIBUTE = "Spring-Boot-Classes";
private static final String BOOT_LIB_ATTRIBUTE = "Spring-Boot-Lib";
private static final String BOOT_CLASSPATH_INDEX_ATTRIBUTE = "Spring-Boot-Classpath-Index";
private static final String BOOT_LAYERS_INDEX_ATTRIBUTE = "Spring-Boot-Layers-Index";
private static final byte[] ZIP_FILE_HEADER = new byte[] { 'P', 'K', 3, 4 };
private static final long FIND_WARNING_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
private static final String SPRING_BOOT_APPLICATION_CLASS_NAME = "org.springframework.boot.autoconfigure.SpringBootApplication";
private List<MainClassTimeoutWarningListener> mainClassTimeoutListeners = new ArrayList<>();
private String mainClass;
public class Repackager extends Packager {
private boolean backupSource = true;
private final File source;
private Layout layout;
private LayoutFactory layoutFactory;
private Layers layers = Layers.IMPLICIT;
public Repackager(File source) {
this(source, null);
}
public Repackager(File source, LayoutFactory layoutFactory) {
Assert.notNull(source, "Source file must be provided");
Assert.isTrue(source.exists() && source.isFile(),
"Source must refer to an existing file, got " + source.getAbsolutePath());
this.source = source.getAbsoluteFile();
this.layoutFactory = layoutFactory;
}
/**
* Add a listener that will be triggered to display a warning if searching for the
* main class takes too long.
* @param listener the listener to add
*/
public void addMainClassTimeoutWarningListener(MainClassTimeoutWarningListener listener) {
this.mainClassTimeoutListeners.add(listener);
}
/**
* Sets the main class that should be run. If not specified the value from the
* MANIFEST will be used, or if no manifest entry is found the archive will be
* searched for a suitable class.
* @param mainClass the main class name
*/
public void setMainClass(String mainClass) {
this.mainClass = mainClass;
super(source, layoutFactory);
}
/**
@ -123,41 +52,13 @@ public class Repackager {
this.backupSource = backupSource;
}
/**
* Sets the layout to use for the jar. Defaults to {@link Layouts#forFile(File)}.
* @param layout the layout
*/
public void setLayout(Layout layout) {
Assert.notNull(layout, "Layout must not be null");
this.layout = layout;
}
/**
* Sets the layers that should be used in the jar.
* @param layers the jar layers
* @see LayeredLayout
*/
public void setLayers(Layers layers) {
Assert.notNull(layers, "Layers must not be null");
this.layers = layers;
}
/**
* Sets the layout factory for the jar. The factory can be used when no specific
* layout is specified.
* @param layoutFactory the layout factory to set
*/
public void setLayoutFactory(LayoutFactory layoutFactory) {
this.layoutFactory = layoutFactory;
}
/**
* Repackage the source file so that it can be run using '{@literal java -jar}'.
* @param libraries the libraries required to run the archive
* @throws IOException if the file cannot be repackaged
*/
public void repackage(Libraries libraries) throws IOException {
repackage(this.source, libraries);
repackage(getSource(), libraries);
}
/**
@ -182,197 +83,34 @@ public class Repackager {
*/
public void repackage(File destination, Libraries libraries, LaunchScript launchScript) throws IOException {
Assert.isTrue(destination != null && !destination.isDirectory(), "Invalid destination");
Assert.notNull(libraries, "Libraries must not be null");
if (this.layout == null) {
this.layout = getLayoutFactory().getLayout(this.source);
}
destination = destination.getAbsoluteFile();
File workingSource = this.source;
if (alreadyRepackaged() && this.source.equals(destination)) {
File source = getSource();
if (isAlreadyPackaged() && source.equals(destination)) {
return;
}
if (this.source.equals(destination)) {
File workingSource = source;
if (source.equals(destination)) {
workingSource = getBackupFile();
workingSource.delete();
renameFile(this.source, workingSource);
renameFile(source, workingSource);
}
destination.delete();
try {
try (JarFile jarFileSource = new JarFile(workingSource)) {
repackage(jarFileSource, destination, libraries, launchScript);
try (JarFile sourceJar = new JarFile(workingSource)) {
repackage(sourceJar, destination, libraries, launchScript);
}
}
finally {
if (!this.backupSource && !this.source.equals(workingSource)) {
if (!this.backupSource && !source.equals(workingSource)) {
deleteFile(workingSource);
}
}
}
private LayoutFactory getLayoutFactory() {
if (this.layoutFactory != null) {
return this.layoutFactory;
}
List<LayoutFactory> factories = SpringFactoriesLoader.loadFactories(LayoutFactory.class, null);
if (factories.isEmpty()) {
return new DefaultLayoutFactory();
}
Assert.state(factories.size() == 1, "No unique LayoutFactory found");
return factories.get(0);
}
/**
* Return the {@link File} to use to backup the original source.
* @return the file to use to backup the original source
*/
public final File getBackupFile() {
return new File(this.source.getParentFile(), this.source.getName() + ".original");
}
private boolean alreadyRepackaged() throws IOException {
try (JarFile jarFile = new JarFile(this.source)) {
Manifest manifest = jarFile.getManifest();
return (manifest != null && manifest.getMainAttributes().getValue(BOOT_VERSION_ATTRIBUTE) != null);
}
}
private void repackage(JarFile sourceJar, File destination, Libraries libraries, LaunchScript launchScript)
throws IOException {
WritableLibraries writeableLibraries = new WritableLibraries(libraries);
try (JarWriter writer = new JarWriter(destination, launchScript)) {
writer.writeManifest(buildManifest(sourceJar));
writeLoaderClasses(writer);
writer.writeEntries(sourceJar, getEntityTransformer(), writeableLibraries);
writeableLibraries.write(writer);
}
}
private void writeLoaderClasses(JarWriter writer) throws IOException {
if (this.layout instanceof CustomLoaderLayout) {
((CustomLoaderLayout) this.layout).writeLoadedClasses(writer);
}
else if (this.layout.isExecutable()) {
writer.writeLoaderClasses();
}
}
private EntryTransformer getEntityTransformer() {
if (this.layout instanceof RepackagingLayout) {
return new RepackagingEntryTransformer((RepackagingLayout) this.layout, this.layers);
}
return EntryTransformer.NONE;
}
private boolean isZip(File file) {
try {
try (FileInputStream fileInputStream = new FileInputStream(file)) {
return isZip(fileInputStream);
}
}
catch (IOException ex) {
return false;
}
}
private boolean isZip(InputStream inputStream) throws IOException {
for (byte magicByte : ZIP_FILE_HEADER) {
if (inputStream.read() != magicByte) {
return false;
}
}
return true;
}
private Manifest buildManifest(JarFile source) throws IOException {
Manifest manifest = createInitialManifest(source);
addMainAndStartAttributes(source, manifest);
addBootAttributes(manifest.getMainAttributes());
return manifest;
}
private Manifest createInitialManifest(JarFile source) throws IOException {
if (source.getManifest() != null) {
return new Manifest(source.getManifest());
}
Manifest manifest = new Manifest();
manifest.getMainAttributes().putValue("Manifest-Version", "1.0");
return manifest;
}
private void addMainAndStartAttributes(JarFile source, Manifest manifest) throws IOException {
String mainClass = getMainClass(source, manifest);
String launcherClass = this.layout.getLauncherClassName();
if (launcherClass != null) {
Assert.state(mainClass != null, "Unable to find main class");
manifest.getMainAttributes().putValue(MAIN_CLASS_ATTRIBUTE, launcherClass);
manifest.getMainAttributes().putValue(START_CLASS_ATTRIBUTE, mainClass);
}
else if (mainClass != null) {
manifest.getMainAttributes().putValue(MAIN_CLASS_ATTRIBUTE, mainClass);
}
}
private String getMainClass(JarFile source, Manifest manifest) throws IOException {
if (this.mainClass != null) {
return this.mainClass;
}
String attributeValue = manifest.getMainAttributes().getValue(MAIN_CLASS_ATTRIBUTE);
if (attributeValue != null) {
return attributeValue;
}
return findMainMethodWithTimeoutWarning(source);
}
private String findMainMethodWithTimeoutWarning(JarFile source) throws IOException {
long startTime = System.currentTimeMillis();
String mainMethod = findMainMethod(source);
long duration = System.currentTimeMillis() - startTime;
if (duration > FIND_WARNING_TIMEOUT) {
for (MainClassTimeoutWarningListener listener : this.mainClassTimeoutListeners) {
listener.handleTimeoutWarning(duration, mainMethod);
}
}
return mainMethod;
}
protected String findMainMethod(JarFile source) throws IOException {
return MainClassFinder.findSingleMainClass(source, this.layout.getClassesLocation(),
SPRING_BOOT_APPLICATION_CLASS_NAME);
}
private void addBootAttributes(Attributes attributes) {
attributes.putValue(BOOT_VERSION_ATTRIBUTE, getClass().getPackage().getImplementationVersion());
if (this.layout instanceof LayeredLayout) {
addBootBootAttributesForLayeredLayout(attributes, (LayeredLayout) this.layout);
}
else if (this.layout instanceof RepackagingLayout) {
addBootBootAttributesForRepackagingLayout(attributes, (RepackagingLayout) this.layout);
}
else {
addBootBootAttributesForPlainLayout(attributes, this.layout);
}
}
private void addBootBootAttributesForLayeredLayout(Attributes attributes, LayeredLayout layout) {
String layersIndexFileLocation = layout.getLayersIndexFileLocation();
putIfHasLength(attributes, BOOT_LAYERS_INDEX_ATTRIBUTE, layersIndexFileLocation);
putIfHasLength(attributes, BOOT_CLASSPATH_INDEX_ATTRIBUTE, layout.getClasspathIndexFileLocation());
}
private void addBootBootAttributesForRepackagingLayout(Attributes attributes, RepackagingLayout layout) {
attributes.putValue(BOOT_CLASSES_ATTRIBUTE, layout.getRepackagedClassesLocation());
putIfHasLength(attributes, BOOT_LIB_ATTRIBUTE, this.layout.getLibraryLocation("", LibraryScope.COMPILE));
putIfHasLength(attributes, BOOT_CLASSPATH_INDEX_ATTRIBUTE, layout.getClasspathIndexFileLocation());
}
private void addBootBootAttributesForPlainLayout(Attributes attributes, Layout layout) {
attributes.putValue(BOOT_CLASSES_ATTRIBUTE, this.layout.getClassesLocation());
putIfHasLength(attributes, BOOT_LIB_ATTRIBUTE, this.layout.getLibraryLocation("", LibraryScope.COMPILE));
}
private void putIfHasLength(Attributes attributes, String name, String value) {
if (StringUtils.hasLength(value)) {
attributes.putValue(name, value);
write(sourceJar, libraries, writer);
}
}
@ -388,143 +126,4 @@ public class Repackager {
}
}
/**
* Callback interface used to present a warning when finding the main class takes too
* long.
*/
@FunctionalInterface
public interface MainClassTimeoutWarningListener {
/**
* Handle a timeout warning.
* @param duration the amount of time it took to find the main method
* @param mainMethod the main method that was actually found
*/
void handleTimeoutWarning(long duration, String mainMethod);
}
/**
* An {@code EntryTransformer} that renames entries by applying a prefix.
*/
private static final class RepackagingEntryTransformer implements EntryTransformer {
private final RepackagingLayout layout;
private final Layers layers;
private RepackagingEntryTransformer(RepackagingLayout layout, Layers layers) {
this.layout = layout;
this.layers = layers;
}
@Override
public JarArchiveEntry transform(JarArchiveEntry entry) {
if (entry.getName().equals("META-INF/INDEX.LIST")) {
return null;
}
if (!isTransformable(entry)) {
return entry;
}
String transformedName = transformName(entry.getName());
JarArchiveEntry transformedEntry = new JarArchiveEntry(transformedName);
transformedEntry.setTime(entry.getTime());
transformedEntry.setSize(entry.getSize());
transformedEntry.setMethod(entry.getMethod());
if (entry.getComment() != null) {
transformedEntry.setComment(entry.getComment());
}
transformedEntry.setCompressedSize(entry.getCompressedSize());
transformedEntry.setCrc(entry.getCrc());
if (entry.getCreationTime() != null) {
transformedEntry.setCreationTime(entry.getCreationTime());
}
if (entry.getExtra() != null) {
transformedEntry.setExtra(entry.getExtra());
}
if (entry.getLastAccessTime() != null) {
transformedEntry.setLastAccessTime(entry.getLastAccessTime());
}
if (entry.getLastModifiedTime() != null) {
transformedEntry.setLastModifiedTime(entry.getLastModifiedTime());
}
return transformedEntry;
}
private String transformName(String name) {
if (this.layout instanceof LayeredLayout) {
Layer layer = this.layers.getLayer(name);
Assert.state(layer != null, "Invalid 'null' layer from " + this.layers.getClass().getName());
return ((LayeredLayout) this.layout).getRepackagedClassesLocation(layer) + name;
}
return this.layout.getRepackagedClassesLocation() + name;
}
private boolean isTransformable(JarArchiveEntry entry) {
String name = entry.getName();
if (name.startsWith("META-INF/")) {
return name.equals("META-INF/aop.xml") || name.endsWith(".kotlin_module");
}
return !name.startsWith("BOOT-INF/") && !name.equals("module-info.class");
}
}
/**
* 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 Map<String, Library> libraryEntryNames = new LinkedHashMap<>();
private WritableLibraries(Libraries libraries) throws IOException {
libraries.doWithLibraries((library) -> {
if (isZip(library.getFile())) {
String location = getLocation(library);
if (location != null) {
Library existing = this.libraryEntryNames.putIfAbsent(location + library.getName(), library);
Assert.state(existing == null, "Duplicate library " + library.getName());
}
}
});
}
private String getLocation(Library library) {
Layout layout = Repackager.this.layout;
if (layout instanceof LayeredLayout) {
Layers layers = Repackager.this.layers;
Layer layer = layers.getLayer(library);
Assert.state(layer != null, "Invalid 'null' library layer from " + layers.getClass().getName());
return ((LayeredLayout) layout).getLibraryLocation(library.getName(), library.getScope(), layer);
}
return layout.getLibraryLocation(library.getName(), library.getScope());
}
@Override
public boolean requiresUnpack(String name) {
Library library = this.libraryEntryNames.get(name);
return library != null && library.isUnpackRequired();
}
@Override
public String sha1Hash(String name) throws IOException {
Library library = this.libraryEntryNames.get(name);
Assert.notNull(library, "No library found for entry name '" + name + "'");
return FileUtils.sha1Hash(library.getFile());
}
private void write(JarWriter writer) throws IOException {
for (Entry<String, Library> entry : this.libraryEntryNames.entrySet()) {
writer.writeNestedLibrary(entry.getKey().substring(0, entry.getKey().lastIndexOf('/') + 1),
entry.getValue());
}
if (Repackager.this.layout instanceof RepackagingLayout) {
String location = ((RepackagingLayout) (Repackager.this.layout)).getClasspathIndexFileLocation();
writer.writeIndexFile(location, new ArrayList<>(this.libraryEntryNames.keySet()));
}
}
}
}

@ -0,0 +1,143 @@
/*
* 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.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.springframework.util.StreamUtils;
/**
* {@link EntryWriter} that always provides size information.
*
* @author Phillip Webb
*/
final class SizeCalculatingEntryWriter implements EntryWriter {
static final int THRESHOLD = 1024 * 20;
private final Object content;
private final int size;
private SizeCalculatingEntryWriter(EntryWriter entryWriter) throws IOException {
SizeCalculatingOutputStream outputStream = new SizeCalculatingOutputStream();
try {
entryWriter.write(outputStream);
}
finally {
outputStream.close();
}
this.content = outputStream.getContent();
this.size = outputStream.getSize();
}
@Override
public void write(OutputStream outputStream) throws IOException {
InputStream inputStream = getContentInputStream();
copy(inputStream, outputStream);
}
private InputStream getContentInputStream() throws FileNotFoundException {
if (this.content instanceof File) {
return new FileInputStream((File) this.content);
}
return new ByteArrayInputStream((byte[]) this.content);
}
private void copy(InputStream inputStream, OutputStream outputStream) throws IOException {
try {
StreamUtils.copy(inputStream, outputStream);
}
finally {
inputStream.close();
}
}
@Override
public int size() {
return this.size;
}
static EntryWriter get(EntryWriter entryWriter) throws IOException {
if (entryWriter == null || entryWriter.size() != -1) {
return entryWriter;
}
return new SizeCalculatingEntryWriter(entryWriter);
}
/**
* {@link OutputStream} to calculate the size and allow content to be written again.
*/
private static class SizeCalculatingOutputStream extends OutputStream {
private int size = 0;
private File tempFile;
private OutputStream outputStream;
SizeCalculatingOutputStream() throws IOException {
this.tempFile = File.createTempFile("springboot-", "-entrycontent");
this.outputStream = new ByteArrayOutputStream();
}
@Override
public void write(int b) throws IOException {
write(new byte[] { (byte) b }, 0, 1);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
int updatedSize = this.size + len;
if (updatedSize > THRESHOLD && this.outputStream instanceof ByteArrayOutputStream) {
this.outputStream = convertToFileOutputStream((ByteArrayOutputStream) this.outputStream);
}
this.outputStream.write(b, off, len);
this.size = updatedSize;
}
private OutputStream convertToFileOutputStream(ByteArrayOutputStream byteArrayOutputStream) throws IOException {
FileOutputStream fileOutputStream = new FileOutputStream(this.tempFile);
StreamUtils.copy(byteArrayOutputStream.toByteArray(), fileOutputStream);
return fileOutputStream;
}
@Override
public void close() throws IOException {
this.outputStream.close();
}
Object getContent() {
return (this.outputStream instanceof ByteArrayOutputStream)
? ((ByteArrayOutputStream) this.outputStream).toByteArray() : this.tempFile;
}
int getSize() {
return this.size;
}
}
}

@ -0,0 +1,98 @@
/*
* 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.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
/**
* {@link InputStream} that can peek ahead at zip header bytes.
*
* @author Phillip Webb
*/
class ZipHeaderPeekInputStream extends FilterInputStream {
private static final byte[] ZIP_HEADER = new byte[] { 0x50, 0x4b, 0x03, 0x04 };
private final byte[] header;
private final int headerLength;
private int position;
private ByteArrayInputStream headerStream;
protected ZipHeaderPeekInputStream(InputStream in) throws IOException {
super(in);
this.header = new byte[4];
this.headerLength = in.read(this.header);
this.headerStream = new ByteArrayInputStream(this.header, 0, this.headerLength);
}
@Override
public int read() throws IOException {
int read = (this.headerStream != null) ? this.headerStream.read() : -1;
if (read != -1) {
this.position++;
if (this.position >= this.headerLength) {
this.headerStream = null;
}
return read;
}
return super.read();
}
@Override
public int read(byte[] b) throws IOException {
return read(b, 0, b.length);
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int read = (this.headerStream != null) ? this.headerStream.read(b, off, len) : -1;
if (read <= 0) {
return readRemainder(b, off, len);
}
this.position += read;
if (read < len) {
int remainderRead = readRemainder(b, off + read, len - read);
if (remainderRead > 0) {
read += remainderRead;
}
}
if (this.position >= this.headerLength) {
this.headerStream = null;
}
return read;
}
boolean hasZipHeader() {
return Arrays.equals(this.header, ZIP_HEADER);
}
private int readRemainder(byte[] b, int off, int len) throws IOException {
int read = super.read(b, off, len);
if (read > 0) {
this.position += read;
}
return read;
}
}

@ -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();
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);
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 {
void sameSourceAndDestinationWithoutBackup() 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();
Repackager repackager = createRepackager(file, false);
repackager.setBackupSource(false);
repackager.repackage(NO_LIBRARIES);
assertThat(new File(file.getParent(), file.getName() + ".original")).doesNotExist();
assertThat(hasPackagedLauncherClasses()).isTrue();
}
@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 repackager = createRepackager(file, false);
repackager.repackage(NO_LIBRARIES);
assertThat(new File(file.getParent(), file.getName() + ".original")).exists();
assertThat(hasLauncherClasses(file)).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.setBackupSource(false);
repackager.repackage(NO_LIBRARIES);
assertThat(new File(file.getParent(), file.getName() + ".original")).doesNotExist();
assertThat(hasLauncherClasses(file)).isTrue();
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,88 +171,64 @@ class RepackagerTests {
}
}
private Manifest getManifest(File file) throws IOException {
try (JarFile jarFile = new JarFile(file)) {
return jarFile.getManifest();
}
@Override
protected Repackager createPackager(File source) {
return createRepackager(source, true);
}
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());
}
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);
}
return entryNames;
}
static class MockLauncherScript implements LaunchScript {
private final byte[] bytes;
MockLauncherScript(String script) {
this.bytes = script.getBytes();
@Override
protected void execute(Repackager packager, Libraries libraries) throws IOException {
packager.repackage(this.destination, libraries);
}
@Override
public byte[] toByteArray() {
return this.bytes;
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());
}
}
static class TestLayoutFactory implements LayoutFactory {
return result;
}
@Override
public Layout getLayout(File source) {
return new TestLayout();
protected Manifest getPackagedManifest() throws IOException {
try (JarFile jarFile = new JarFile(this.destination)) {
return jarFile.getManifest();
}
}
static class TestLayout extends Layouts.Jar implements CustomLoaderLayout {
@Override
public void writeLoadedClasses(LoaderClassesWriter writer) throws IOException {
writer.writeEntry("test", new ByteArrayInputStream("test".getBytes()));
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);
}
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);
}
static class MockLauncherScript implements LaunchScript {
@Override
public Iterator<Layer> iterator() {
return this.layers.iterator();
}
private final byte[] bytes;
@Override
public Layer getLayer(String name) {
return DEFAULT_LAYER;
MockLauncherScript(String script) {
this.bytes = script.getBytes();
}
@Override
public Layer getLayer(Library library) {
String name = new File(library.getName()).getName();
return this.libraries.getOrDefault(name, DEFAULT_LAYER);
public byte[] toByteArray() {
return this.bytes;
}
}

@ -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;
/**

Loading…
Cancel
Save