diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java index 733d4fe8b86..9dca479ad9f 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java @@ -42,9 +42,9 @@ class JarEntry extends java.util.jar.JarEntry implements FileHeader { private long localHeaderOffset; - JarEntry(JarFile jarFile, CentralDirectoryFileHeader header) { - super(header.getName().toString()); - this.name = header.getName(); + JarEntry(JarFile jarFile, CentralDirectoryFileHeader header, AsciiBytes nameAlias) { + super((nameAlias != null) ? nameAlias.toString() : header.getName().toString()); + this.name = (nameAlias != null) ? nameAlias : header.getName(); this.jarFile = jarFile; this.localHeaderOffset = header.getLocalHeaderOffset(); setCompressedSize(header.getCompressedSize()); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java index 48196a81960..bf6a0d5a69d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java @@ -24,6 +24,9 @@ import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; 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 org.springframework.boot.loader.data.RandomAccessData; @@ -44,6 +47,27 @@ import org.springframework.boot.loader.data.RandomAccessData; */ class JarFileEntries implements CentralDirectoryVisitor, Iterable { + 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 char SLASH = '/'; @@ -66,6 +90,8 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable { private int[] positions; + private Boolean multiReleaseJar; + private final Map entriesCache = Collections .synchronizedMap(new LinkedHashMap(16, 0.75f, true) { @@ -83,6 +109,9 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable { JarFileEntries(JarFile jarFile, JarEntryFilter filter) { this.jarFile = jarFile; this.filter = filter; + if (RUNTIME_VERSION == BASE_VERSION) { + this.multiReleaseJar = false; + } } @Override @@ -216,21 +245,68 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable { private T getEntry(CharSequence name, Class type, 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 doGetEntry(CharSequence name, Class type, + boolean cacheEntry, AsciiBytes nameAlias) { 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) { hashCode = AsciiBytes.hashCode(hashCode, SLASH); - entry = getEntry(hashCode, name, SLASH, type, cacheEntry); + entry = getEntry(hashCode, name, SLASH, type, cacheEntry, nameAlias); } return entry; } private T getEntry(int hashCode, CharSequence name, - char suffix, Class type, boolean cacheEntry) { + char suffix, Class type, boolean cacheEntry, AsciiBytes nameAlias) { int index = getFirstIndex(hashCode); while (index >= 0 && index < this.size && this.hashCodes[index] == hashCode) { - T entry = getEntry(index, type, cacheEntry); - if (entry.hasName(name, suffix)) { + T entry = getEntry(index, type, cacheEntry, nameAlias); + if (entry.hasName((nameAlias != null) ? nameAlias.toString() : name, + suffix)) { return entry; } index++; @@ -240,7 +316,7 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable { @SuppressWarnings("unchecked") private T getEntry(int index, Class type, - boolean cacheEntry) { + boolean cacheEntry, AsciiBytes nameAlias) { try { FileHeader cached = this.entriesCache.get(index); FileHeader entry = (cached != null) ? cached @@ -249,7 +325,8 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable { this.centralDirectoryOffsets[index], this.filter); if (CentralDirectoryFileHeader.class.equals(entry.getClass()) && type.equals(JarEntry.class)) { - entry = new JarEntry(this.jarFile, (CentralDirectoryFileHeader) entry); + entry = new JarEntry(this.jarFile, (CentralDirectoryFileHeader) entry, + nameAlias); } if (cacheEntry && cached != entry) { this.entriesCache.put(index, entry); @@ -299,7 +376,7 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable { } int entryIndex = JarFileEntries.this.positions[this.index]; this.index++; - return getEntry(entryIndex, JarEntry.class, false); + return getEntry(entryIndex, JarEntry.class, false, null); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/TestJarCreator.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/TestJarCreator.java index 20b686cac4a..0ddc9ede727 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/TestJarCreator.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/TestJarCreator.java @@ -1,5 +1,5 @@ /* - * 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"); * you may not use this file except in compliance with the License. @@ -52,13 +52,25 @@ public abstract class TestJarCreator { writeNestedEntry("nested.jar", unpackNested, jarOutputStream); writeNestedEntry("another-nested.jar", unpackNested, jarOutputStream); writeNestedEntry("space nested.jar", unpackNested, jarOutputStream); + writeNestedMultiReleaseEntry("multi-release.jar", unpackNested, + jarOutputStream); } } private static void writeNestedEntry(String name, boolean unpackNested, 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); - byte[] nestedJarData = getNestedJarData(); + byte[] nestedJarData = getNestedJarData(multiRelease); nestedEntry.setSize(nestedJarData.length); nestedEntry.setCompressedSize(nestedJarData.length); if (unpackNested) { @@ -74,23 +86,40 @@ public abstract class TestJarCreator { jarOutputStream.closeEntry(); } - private static byte[] getNestedJarData() throws Exception { + private static byte[] getNestedJarData(boolean multiRelease) throws Exception { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); JarOutputStream jarOutputStream = new JarOutputStream(byteArrayOutputStream); - writeManifest(jarOutputStream, "j2"); - writeEntry(jarOutputStream, "3.dat", 3); - writeEntry(jarOutputStream, "4.dat", 4); - writeEntry(jarOutputStream, "\u00E4.dat", '\u00E4'); + writeManifest(jarOutputStream, "j2", multiRelease); + if (multiRelease) { + writeEntry(jarOutputStream, "multi-release.dat", 8); + 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(); return byteArrayOutputStream.toByteArray(); } private static void writeManifest(JarOutputStream jarOutputStream, String name) throws Exception { + writeManifest(jarOutputStream, name, false); + } + + private static void writeManifest(JarOutputStream jarOutputStream, String name, + boolean multiRelease) throws Exception { writeDirEntry(jarOutputStream, "META-INF/"); Manifest manifest = new Manifest(); manifest.getMainAttributes().putValue("Built-By", name); 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")); manifest.write(jarOutputStream); jarOutputStream.closeEntry(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/ExplodedArchiveTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/ExplodedArchiveTests.java index 86bcce96ec5..c392124d4a3 100755 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/ExplodedArchiveTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/ExplodedArchiveTests.java @@ -1,5 +1,5 @@ /* - * 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"); * you may not use this file except in compliance with the License. @@ -108,7 +108,7 @@ public class ExplodedArchiveTests { @Test public void getEntries() { Map entries = getEntriesMap(this.archive); - assertThat(entries.size()).isEqualTo(11); + assertThat(entries.size()).isEqualTo(12); } @Test diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java index cd173de51cf..e923c0af5bc 100755 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java @@ -78,7 +78,7 @@ public class JarFileArchiveTests { @Test public void getEntries() { Map entries = getEntriesMap(this.archive); - assertThat(entries.size()).isEqualTo(11); + assertThat(entries.size()).isEqualTo(12); } @Test diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/CentralDirectoryParserTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/CentralDirectoryParserTests.java index 32d79779f99..b34baef839b 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/CentralDirectoryParserTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/CentralDirectoryParserTests.java @@ -1,5 +1,5 @@ /* - * 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"); * you may not use this file except in compliance with the License. @@ -82,6 +82,7 @@ public class CentralDirectoryParserTests { assertThat(headers.next().getName().toString()).isEqualTo("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("multi-release.jar"); assertThat(headers.hasNext()).isFalse(); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java index bdf57d00e9f..6f302d43027 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java @@ -91,6 +91,7 @@ public class JarFileTests { assertThat(entries.nextElement().getName()).isEqualTo("nested.jar"); assertThat(entries.nextElement().getName()).isEqualTo("another-nested.jar"); assertThat(entries.nextElement().getName()).isEqualTo("space nested.jar"); + assertThat(entries.nextElement().getName()).isEqualTo("multi-release.jar"); assertThat(entries.hasMoreElements()).isFalse(); URL jarUrl = new URL("jar:" + this.rootJarFile.toURI() + "!/"); URLClassLoader urlClassLoader = new URLClassLoader(new URL[] { jarUrl }); @@ -134,6 +135,7 @@ public class JarFileTests { assertThat(entries.nextElement().getName()).isEqualTo("nested.jar"); assertThat(entries.nextElement().getName()).isEqualTo("another-nested.jar"); assertThat(entries.nextElement().getName()).isEqualTo("space nested.jar"); + assertThat(entries.nextElement().getName()).isEqualTo("multi-release.jar"); assertThat(entries.hasMoreElements()).isFalse(); } @@ -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; + } + } + }