Commit 56eebc93 authored by Andy Wilkinson's avatar Andy Wilkinson

Update fat jar loader to support multi-release jar files

Closes gh-12523
parent 60b35df2
...@@ -42,9 +42,9 @@ class JarEntry extends java.util.jar.JarEntry implements FileHeader { ...@@ -42,9 +42,9 @@ class JarEntry extends java.util.jar.JarEntry implements FileHeader {
private long localHeaderOffset; private long localHeaderOffset;
JarEntry(JarFile jarFile, CentralDirectoryFileHeader header) { JarEntry(JarFile jarFile, CentralDirectoryFileHeader header, AsciiBytes nameAlias) {
super(header.getName().toString()); super((nameAlias != null) ? nameAlias.toString() : header.getName().toString());
this.name = header.getName(); this.name = (nameAlias != null) ? nameAlias : header.getName();
this.jarFile = jarFile; this.jarFile = jarFile;
this.localHeaderOffset = header.getLocalHeaderOffset(); this.localHeaderOffset = header.getLocalHeaderOffset();
setCompressedSize(header.getCompressedSize()); setCompressedSize(header.getCompressedSize());
......
...@@ -24,6 +24,9 @@ import java.util.Iterator; ...@@ -24,6 +24,9 @@ import java.util.Iterator;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
import java.util.jar.Attributes;
import java.util.jar.Attributes.Name;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import org.springframework.boot.loader.data.RandomAccessData; import org.springframework.boot.loader.data.RandomAccessData;
...@@ -44,6 +47,27 @@ import org.springframework.boot.loader.data.RandomAccessData; ...@@ -44,6 +47,27 @@ import org.springframework.boot.loader.data.RandomAccessData;
*/ */
class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> { class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> {
private static final String META_INF_PREFIX = "META-INF/";
private static final Name MULTI_RELEASE = new Name("Multi-Release");
private static final int BASE_VERSION = 8;
private static final int RUNTIME_VERSION;
static {
int version;
try {
Object runtimeVersion = Runtime.class.getMethod("version").invoke(null);
version = (int) runtimeVersion.getClass().getMethod("major")
.invoke(runtimeVersion);
}
catch (Throwable ex) {
version = 8;
}
RUNTIME_VERSION = version;
}
private static final long LOCAL_FILE_HEADER_SIZE = 30; private static final long LOCAL_FILE_HEADER_SIZE = 30;
private static final char SLASH = '/'; private static final char SLASH = '/';
...@@ -66,6 +90,8 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> { ...@@ -66,6 +90,8 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> {
private int[] positions; private int[] positions;
private Boolean multiReleaseJar;
private final Map<Integer, FileHeader> entriesCache = Collections private final Map<Integer, FileHeader> entriesCache = Collections
.synchronizedMap(new LinkedHashMap<Integer, FileHeader>(16, 0.75f, true) { .synchronizedMap(new LinkedHashMap<Integer, FileHeader>(16, 0.75f, true) {
...@@ -83,6 +109,9 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> { ...@@ -83,6 +109,9 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> {
JarFileEntries(JarFile jarFile, JarEntryFilter filter) { JarFileEntries(JarFile jarFile, JarEntryFilter filter) {
this.jarFile = jarFile; this.jarFile = jarFile;
this.filter = filter; this.filter = filter;
if (RUNTIME_VERSION == BASE_VERSION) {
this.multiReleaseJar = false;
}
} }
@Override @Override
...@@ -216,21 +245,68 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> { ...@@ -216,21 +245,68 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> {
private <T extends FileHeader> T getEntry(CharSequence name, Class<T> type, private <T extends FileHeader> T getEntry(CharSequence name, Class<T> type,
boolean cacheEntry) { boolean cacheEntry) {
T entry = doGetEntry(name, type, cacheEntry, null);
if (isMultiReleaseJar() && !isMetaInfEntry(name)) {
int version = RUNTIME_VERSION;
AsciiBytes nameAlias = (entry instanceof JarEntry)
? ((JarEntry) entry).getAsciiBytesName()
: new AsciiBytes(name.toString());
while (version > BASE_VERSION) {
T versionedEntry = doGetEntry("META-INF/versions/" + version + "/" + name,
type, cacheEntry, nameAlias);
if (versionedEntry != null) {
return versionedEntry;
}
version--;
}
}
return entry;
}
private boolean isMetaInfEntry(CharSequence name) {
return name.toString().startsWith(META_INF_PREFIX);
}
private boolean isMultiReleaseJar() {
Boolean multiRelease = this.multiReleaseJar;
if (multiRelease != null) {
return multiRelease;
}
try {
Manifest manifest = this.jarFile.getManifest();
if (manifest == null) {
multiRelease = false;
}
else {
Attributes attributes = manifest.getMainAttributes();
multiRelease = attributes.containsKey(MULTI_RELEASE);
}
}
catch (IOException ex) {
multiRelease = false;
}
this.multiReleaseJar = multiRelease;
return multiRelease;
}
private <T extends FileHeader> T doGetEntry(CharSequence name, Class<T> type,
boolean cacheEntry, AsciiBytes nameAlias) {
int hashCode = AsciiBytes.hashCode(name); int hashCode = AsciiBytes.hashCode(name);
T entry = getEntry(hashCode, name, NO_SUFFIX, type, cacheEntry); T entry = getEntry(hashCode, name, NO_SUFFIX, type, cacheEntry, nameAlias);
if (entry == null) { if (entry == null) {
hashCode = AsciiBytes.hashCode(hashCode, SLASH); hashCode = AsciiBytes.hashCode(hashCode, SLASH);
entry = getEntry(hashCode, name, SLASH, type, cacheEntry); entry = getEntry(hashCode, name, SLASH, type, cacheEntry, nameAlias);
} }
return entry; return entry;
} }
private <T extends FileHeader> T getEntry(int hashCode, CharSequence name, private <T extends FileHeader> T getEntry(int hashCode, CharSequence name,
char suffix, Class<T> type, boolean cacheEntry) { char suffix, Class<T> type, boolean cacheEntry, AsciiBytes nameAlias) {
int index = getFirstIndex(hashCode); int index = getFirstIndex(hashCode);
while (index >= 0 && index < this.size && this.hashCodes[index] == hashCode) { while (index >= 0 && index < this.size && this.hashCodes[index] == hashCode) {
T entry = getEntry(index, type, cacheEntry); T entry = getEntry(index, type, cacheEntry, nameAlias);
if (entry.hasName(name, suffix)) { if (entry.hasName((nameAlias != null) ? nameAlias.toString() : name,
suffix)) {
return entry; return entry;
} }
index++; index++;
...@@ -240,7 +316,7 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> { ...@@ -240,7 +316,7 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private <T extends FileHeader> T getEntry(int index, Class<T> type, private <T extends FileHeader> T getEntry(int index, Class<T> type,
boolean cacheEntry) { boolean cacheEntry, AsciiBytes nameAlias) {
try { try {
FileHeader cached = this.entriesCache.get(index); FileHeader cached = this.entriesCache.get(index);
FileHeader entry = (cached != null) ? cached FileHeader entry = (cached != null) ? cached
...@@ -249,7 +325,8 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> { ...@@ -249,7 +325,8 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> {
this.centralDirectoryOffsets[index], this.filter); this.centralDirectoryOffsets[index], this.filter);
if (CentralDirectoryFileHeader.class.equals(entry.getClass()) if (CentralDirectoryFileHeader.class.equals(entry.getClass())
&& type.equals(JarEntry.class)) { && type.equals(JarEntry.class)) {
entry = new JarEntry(this.jarFile, (CentralDirectoryFileHeader) entry); entry = new JarEntry(this.jarFile, (CentralDirectoryFileHeader) entry,
nameAlias);
} }
if (cacheEntry && cached != entry) { if (cacheEntry && cached != entry) {
this.entriesCache.put(index, entry); this.entriesCache.put(index, entry);
...@@ -299,7 +376,7 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> { ...@@ -299,7 +376,7 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> {
} }
int entryIndex = JarFileEntries.this.positions[this.index]; int entryIndex = JarFileEntries.this.positions[this.index];
this.index++; this.index++;
return getEntry(entryIndex, JarEntry.class, false); return getEntry(entryIndex, JarEntry.class, false, null);
} }
} }
......
/* /*
* Copyright 2012-2017 the original author or authors. * Copyright 2012-2018 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -52,13 +52,25 @@ public abstract class TestJarCreator { ...@@ -52,13 +52,25 @@ public abstract class TestJarCreator {
writeNestedEntry("nested.jar", unpackNested, jarOutputStream); writeNestedEntry("nested.jar", unpackNested, jarOutputStream);
writeNestedEntry("another-nested.jar", unpackNested, jarOutputStream); writeNestedEntry("another-nested.jar", unpackNested, jarOutputStream);
writeNestedEntry("space nested.jar", unpackNested, jarOutputStream); writeNestedEntry("space nested.jar", unpackNested, jarOutputStream);
writeNestedMultiReleaseEntry("multi-release.jar", unpackNested,
jarOutputStream);
} }
} }
private static void writeNestedEntry(String name, boolean unpackNested, private static void writeNestedEntry(String name, boolean unpackNested,
JarOutputStream jarOutputStream) throws Exception { JarOutputStream jarOutputStream) throws Exception {
writeNestedEntry(name, unpackNested, jarOutputStream, false);
}
private static void writeNestedMultiReleaseEntry(String name, boolean unpackNested,
JarOutputStream jarOutputStream) throws Exception {
writeNestedEntry(name, unpackNested, jarOutputStream, true);
}
private static void writeNestedEntry(String name, boolean unpackNested,
JarOutputStream jarOutputStream, boolean multiRelease) throws Exception {
JarEntry nestedEntry = new JarEntry(name); JarEntry nestedEntry = new JarEntry(name);
byte[] nestedJarData = getNestedJarData(); byte[] nestedJarData = getNestedJarData(multiRelease);
nestedEntry.setSize(nestedJarData.length); nestedEntry.setSize(nestedJarData.length);
nestedEntry.setCompressedSize(nestedJarData.length); nestedEntry.setCompressedSize(nestedJarData.length);
if (unpackNested) { if (unpackNested) {
...@@ -74,23 +86,40 @@ public abstract class TestJarCreator { ...@@ -74,23 +86,40 @@ public abstract class TestJarCreator {
jarOutputStream.closeEntry(); jarOutputStream.closeEntry();
} }
private static byte[] getNestedJarData() throws Exception { private static byte[] getNestedJarData(boolean multiRelease) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
JarOutputStream jarOutputStream = new JarOutputStream(byteArrayOutputStream); JarOutputStream jarOutputStream = new JarOutputStream(byteArrayOutputStream);
writeManifest(jarOutputStream, "j2"); writeManifest(jarOutputStream, "j2", multiRelease);
writeEntry(jarOutputStream, "3.dat", 3); if (multiRelease) {
writeEntry(jarOutputStream, "4.dat", 4); writeEntry(jarOutputStream, "multi-release.dat", 8);
writeEntry(jarOutputStream, "\u00E4.dat", '\u00E4'); writeEntry(jarOutputStream, "META-INF/versions/9/multi-release.dat", 9);
writeEntry(jarOutputStream, "META-INF/versions/10/multi-release.dat", 10);
writeEntry(jarOutputStream, "META-INF/versions/11/multi-release.dat", 11);
}
else {
writeEntry(jarOutputStream, "3.dat", 3);
writeEntry(jarOutputStream, "4.dat", 4);
writeEntry(jarOutputStream, "\u00E4.dat", '\u00E4');
}
jarOutputStream.close(); jarOutputStream.close();
return byteArrayOutputStream.toByteArray(); return byteArrayOutputStream.toByteArray();
} }
private static void writeManifest(JarOutputStream jarOutputStream, String name) private static void writeManifest(JarOutputStream jarOutputStream, String name)
throws Exception { throws Exception {
writeManifest(jarOutputStream, name, false);
}
private static void writeManifest(JarOutputStream jarOutputStream, String name,
boolean multiRelease) throws Exception {
writeDirEntry(jarOutputStream, "META-INF/"); writeDirEntry(jarOutputStream, "META-INF/");
Manifest manifest = new Manifest(); Manifest manifest = new Manifest();
manifest.getMainAttributes().putValue("Built-By", name); manifest.getMainAttributes().putValue("Built-By", name);
manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
if (multiRelease) {
manifest.getMainAttributes().putValue("Multi-Release",
Boolean.toString(true));
}
jarOutputStream.putNextEntry(new ZipEntry("META-INF/MANIFEST.MF")); jarOutputStream.putNextEntry(new ZipEntry("META-INF/MANIFEST.MF"));
manifest.write(jarOutputStream); manifest.write(jarOutputStream);
jarOutputStream.closeEntry(); jarOutputStream.closeEntry();
......
/* /*
* Copyright 2012-2017 the original author or authors. * Copyright 2012-2018 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -108,7 +108,7 @@ public class ExplodedArchiveTests { ...@@ -108,7 +108,7 @@ public class ExplodedArchiveTests {
@Test @Test
public void getEntries() { public void getEntries() {
Map<String, Archive.Entry> entries = getEntriesMap(this.archive); Map<String, Archive.Entry> entries = getEntriesMap(this.archive);
assertThat(entries.size()).isEqualTo(11); assertThat(entries.size()).isEqualTo(12);
} }
@Test @Test
......
...@@ -78,7 +78,7 @@ public class JarFileArchiveTests { ...@@ -78,7 +78,7 @@ public class JarFileArchiveTests {
@Test @Test
public void getEntries() { public void getEntries() {
Map<String, Archive.Entry> entries = getEntriesMap(this.archive); Map<String, Archive.Entry> entries = getEntriesMap(this.archive);
assertThat(entries.size()).isEqualTo(11); assertThat(entries.size()).isEqualTo(12);
} }
@Test @Test
......
/* /*
* Copyright 2012-2017 the original author or authors. * Copyright 2012-2018 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -82,6 +82,7 @@ public class CentralDirectoryParserTests { ...@@ -82,6 +82,7 @@ public class CentralDirectoryParserTests {
assertThat(headers.next().getName().toString()).isEqualTo("nested.jar"); assertThat(headers.next().getName().toString()).isEqualTo("nested.jar");
assertThat(headers.next().getName().toString()).isEqualTo("another-nested.jar"); assertThat(headers.next().getName().toString()).isEqualTo("another-nested.jar");
assertThat(headers.next().getName().toString()).isEqualTo("space nested.jar"); assertThat(headers.next().getName().toString()).isEqualTo("space nested.jar");
assertThat(headers.next().getName().toString()).isEqualTo("multi-release.jar");
assertThat(headers.hasNext()).isFalse(); assertThat(headers.hasNext()).isFalse();
} }
......
...@@ -91,6 +91,7 @@ public class JarFileTests { ...@@ -91,6 +91,7 @@ public class JarFileTests {
assertThat(entries.nextElement().getName()).isEqualTo("nested.jar"); assertThat(entries.nextElement().getName()).isEqualTo("nested.jar");
assertThat(entries.nextElement().getName()).isEqualTo("another-nested.jar"); assertThat(entries.nextElement().getName()).isEqualTo("another-nested.jar");
assertThat(entries.nextElement().getName()).isEqualTo("space nested.jar"); assertThat(entries.nextElement().getName()).isEqualTo("space nested.jar");
assertThat(entries.nextElement().getName()).isEqualTo("multi-release.jar");
assertThat(entries.hasMoreElements()).isFalse(); assertThat(entries.hasMoreElements()).isFalse();
URL jarUrl = new URL("jar:" + this.rootJarFile.toURI() + "!/"); URL jarUrl = new URL("jar:" + this.rootJarFile.toURI() + "!/");
URLClassLoader urlClassLoader = new URLClassLoader(new URL[] { jarUrl }); URLClassLoader urlClassLoader = new URLClassLoader(new URL[] { jarUrl });
...@@ -134,6 +135,7 @@ public class JarFileTests { ...@@ -134,6 +135,7 @@ public class JarFileTests {
assertThat(entries.nextElement().getName()).isEqualTo("nested.jar"); assertThat(entries.nextElement().getName()).isEqualTo("nested.jar");
assertThat(entries.nextElement().getName()).isEqualTo("another-nested.jar"); assertThat(entries.nextElement().getName()).isEqualTo("another-nested.jar");
assertThat(entries.nextElement().getName()).isEqualTo("space nested.jar"); assertThat(entries.nextElement().getName()).isEqualTo("space nested.jar");
assertThat(entries.nextElement().getName()).isEqualTo("multi-release.jar");
assertThat(entries.hasMoreElements()).isFalse(); assertThat(entries.hasMoreElements()).isFalse();
} }
...@@ -499,4 +501,26 @@ public class JarFileTests { ...@@ -499,4 +501,26 @@ public class JarFileTests {
} }
} }
@Test
public void multiReleaseEntry() throws Exception {
JarFile multiRelease = this.jarFile
.getNestedJarFile(this.jarFile.getEntry("multi-release.jar"));
ZipEntry entry = multiRelease.getEntry("multi-release.dat");
assertThat(entry.getName()).isEqualTo("multi-release.dat");
InputStream inputStream = multiRelease.getInputStream(entry);
assertThat(inputStream.available()).isEqualTo(1);
assertThat(inputStream.read()).isEqualTo(getJavaVersion());
}
private int getJavaVersion() {
try {
Object runtimeVersion = Runtime.class.getMethod("version").invoke(null);
return (int) runtimeVersion.getClass().getMethod("major")
.invoke(runtimeVersion);
}
catch (Throwable ex) {
return 8;
}
}
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment