Browse Source
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-19834pull/19835/head
14 changed files with 2132 additions and 1514 deletions
@ -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); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -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())); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -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; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -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); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
Loading…
Reference in new issue