From 77befce3dc941c86dd2981add220f9cdfee04546 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 6 Mar 2024 10:24:52 +0100 Subject: [PATCH] Add tests for IndexedJarStructure --- .../boot/jarmode/tools/ExtractCommand.java | 2 +- .../jarmode/tools/IndexedJarStructure.java | 11 +- .../boot/jarmode/tools/JarStructure.java | 12 +- .../tools/IndexedJarStructureTests.java | 173 ++++++++++++++++++ 4 files changed, 191 insertions(+), 7 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/IndexedJarStructureTests.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractCommand.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractCommand.java index dd2a5f4309d6..3ff9bbcbbad2 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractCommand.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractCommand.java @@ -162,7 +162,7 @@ private File getWorkingDirectory(Map options) { } private JarStructure getJarStructure() { - IndexedJarStructure jarStructure = IndexedJarStructure.get(this.context); + IndexedJarStructure jarStructure = IndexedJarStructure.get(this.context.getArchiveFile()); Assert.state(jarStructure != null, "Couldn't read classpath index"); return jarStructure; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/IndexedJarStructure.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/IndexedJarStructure.java index d2c93ba73ad0..f97aa9c8eb59 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/IndexedJarStructure.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/IndexedJarStructure.java @@ -16,8 +16,10 @@ package org.springframework.boot.jarmode.tools; +import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; import java.nio.file.NoSuchFileException; import java.util.ArrayList; @@ -89,8 +91,7 @@ private static List readIndexFile(String indexFile) { } @Override - public Entry resolve(ZipEntry entry) { - String name = entry.getName(); + public Entry resolve(String name) { if (this.classpathEntries.contains(name)) { return new Entry(name, toStructureDependency(name), Type.LIBRARY); } @@ -130,9 +131,9 @@ private static String getMandatoryAttribute(Manifest manifest, String attribute) return value; } - static IndexedJarStructure get(Context context) { + static IndexedJarStructure get(File file) { try { - try (JarFile jarFile = new JarFile(context.getArchiveFile())) { + try (JarFile jarFile = new JarFile(file)) { Manifest manifest = jarFile.getManifest(); String location = getMandatoryAttribute(manifest, "Spring-Boot-Classpath-Index"); ZipEntry entry = jarFile.getEntry(location); @@ -147,7 +148,7 @@ static IndexedJarStructure get(Context context) { return null; } catch (IOException ex) { - throw new IllegalStateException(ex); + throw new UncheckedIOException(ex); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/JarStructure.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/JarStructure.java index bfd3c7028876..ba7aa6b4fd24 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/JarStructure.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/JarStructure.java @@ -34,7 +34,17 @@ interface JarStructure { * @param entry the entry to handle * @return the resolved {@link Entry} */ - Entry resolve(ZipEntry entry); + default Entry resolve(ZipEntry entry) { + return resolve(entry.getName()); + } + + /** + * Resolve the entry with the specified name, return {@code null} if the entry should + * not be handled. + * @param name the name of the entry to handle + * @return the resolved {@link Entry} + */ + Entry resolve(String name); /** * Create the {@link Manifest} for the launcher jar, applying the specified operator diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/IndexedJarStructureTests.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/IndexedJarStructureTests.java new file mode 100644 index 000000000000..d3f6387f8fbb --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/IndexedJarStructureTests.java @@ -0,0 +1,173 @@ +/* + * Copyright 2012-2024 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.jarmode.tools; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.function.UnaryOperator; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.zip.ZipEntry; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.jarmode.tools.JarStructure.Entry; +import org.springframework.boot.jarmode.tools.JarStructure.Entry.Type; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link IndexedJarStructure}. + * + * @author Moritz Halbritter + */ +class IndexedJarStructureTests { + + @Test + void shouldResolveLibraryEntry() throws IOException { + IndexedJarStructure structure = createStructure(); + Entry entry = structure.resolve("BOOT-INF/lib/spring-webmvc-6.1.4.jar"); + assertThat(entry.location()).isEqualTo("spring-webmvc-6.1.4.jar"); + assertThat(entry.originalLocation()).isEqualTo("BOOT-INF/lib/spring-webmvc-6.1.4.jar"); + assertThat(entry.type()).isEqualTo(Type.LIBRARY); + } + + @Test + void shouldResolveApplicationEntry() throws IOException { + IndexedJarStructure structure = createStructure(); + Entry entry = structure.resolve("BOOT-INF/classes/application.properties"); + assertThat(entry.location()).isEqualTo("application.properties"); + assertThat(entry.originalLocation()).isEqualTo("BOOT-INF/classes/application.properties"); + assertThat(entry.type()).isEqualTo(Type.APPLICATION_CLASS_OR_RESOURCE); + } + + @Test + void shouldResolveLoaderEntry() throws IOException { + IndexedJarStructure structure = createStructure(); + Entry entry = structure.resolve("org/springframework/boot/loader/launch/JarLauncher"); + assertThat(entry.location()).isEqualTo("org/springframework/boot/loader/launch/JarLauncher"); + assertThat(entry.originalLocation()).isEqualTo("org/springframework/boot/loader/launch/JarLauncher"); + assertThat(entry.type()).isEqualTo(Type.LOADER); + } + + @Test + void shouldNotResolveNonExistingLibs() throws IOException { + IndexedJarStructure structure = createStructure(); + Entry entry = structure.resolve("BOOT-INF/lib/doesnt-exists.jar"); + assertThat(entry).isNull(); + } + + @Test + void shouldCreateLauncherManifest() throws IOException { + IndexedJarStructure structure = createStructure(); + Manifest manifest = structure.createLauncherManifest(UnaryOperator.identity()); + Map attributes = getAttributes(manifest); + assertThat(attributes).containsEntry("Manifest-Version", "1.0") + .containsEntry("Implementation-Title", "IndexedJarStructureTests") + .containsEntry("Spring-Boot-Version", "3.3.0-SNAPSHOT") + .containsEntry("Implementation-Version", "0.0.1-SNAPSHOT") + .containsEntry("Build-Jdk-Spec", "17") + .containsEntry("Class-Path", + "spring-webmvc-6.1.4.jar spring-web-6.1.4.jar spring-boot-autoconfigure-3.3.0-SNAPSHOT.jar spring-boot-3.3.0-SNAPSHOT.jar jakarta.annotation-api-2.1.1.jar spring-context-6.1.4.jar spring-aop-6.1.4.jar spring-beans-6.1.4.jar spring-expression-6.1.4.jar spring-core-6.1.4.jar snakeyaml-2.2.jar jackson-datatype-jdk8-2.16.1.jar jackson-datatype-jsr310-2.16.1.jar jackson-module-parameter-names-2.16.1.jar jackson-databind-2.16.1.jar tomcat-embed-websocket-10.1.19.jar tomcat-embed-core-10.1.19.jar tomcat-embed-el-10.1.19.jar micrometer-observation-1.13.0-M1.jar logback-classic-1.4.14.jar log4j-to-slf4j-2.23.0.jar jul-to-slf4j-2.0.12.jar spring-jcl-6.1.4.jar jackson-annotations-2.16.1.jar jackson-core-2.16.1.jar micrometer-commons-1.13.0-M1.jar logback-core-1.4.14.jar slf4j-api-2.0.12.jar log4j-api-2.23.0.jar") + .containsEntry("Main-Class", "org.springframework.boot.jarmode.tools.IndexedJarStructureTests") + .doesNotContainKeys("Start-Class", "Spring-Boot-Classes", "Spring-Boot-Lib", "Spring-Boot-Classpath-Index", + "Spring-Boot-Layers-Index"); + } + + @Test + void shouldLoadFromFile(@TempDir File tempDir) throws IOException { + File jarFile = new File(tempDir, "test.jar"); + try (JarOutputStream outputStream = new JarOutputStream(new FileOutputStream(jarFile), createManifest())) { + outputStream.putNextEntry(new ZipEntry("BOOT-INF/classpath.idx")); + outputStream.write(createIndexFile().getBytes(StandardCharsets.UTF_8)); + outputStream.closeEntry(); + } + IndexedJarStructure structure = IndexedJarStructure.get(jarFile); + assertThat(structure).isNotNull(); + assertThat(structure.resolve("BOOT-INF/lib/spring-webmvc-6.1.4.jar")).extracting(Entry::type) + .isEqualTo(Type.LIBRARY); + assertThat(structure.resolve("BOOT-INF/classes/application.properties")).extracting(Entry::type) + .isEqualTo(Type.APPLICATION_CLASS_OR_RESOURCE); + } + + private Map getAttributes(Manifest manifest) { + Map result = new HashMap<>(); + manifest.getMainAttributes().forEach((key, value) -> result.put(key.toString(), value.toString())); + return result; + } + + private IndexedJarStructure createStructure() throws IOException { + return new IndexedJarStructure(createManifest(), createIndexFile()); + } + + private String createIndexFile() { + return """ + - "BOOT-INF/lib/spring-webmvc-6.1.4.jar" + - "BOOT-INF/lib/spring-web-6.1.4.jar" + - "BOOT-INF/lib/spring-boot-autoconfigure-3.3.0-SNAPSHOT.jar" + - "BOOT-INF/lib/spring-boot-3.3.0-SNAPSHOT.jar" + - "BOOT-INF/lib/jakarta.annotation-api-2.1.1.jar" + - "BOOT-INF/lib/spring-context-6.1.4.jar" + - "BOOT-INF/lib/spring-aop-6.1.4.jar" + - "BOOT-INF/lib/spring-beans-6.1.4.jar" + - "BOOT-INF/lib/spring-expression-6.1.4.jar" + - "BOOT-INF/lib/spring-core-6.1.4.jar" + - "BOOT-INF/lib/snakeyaml-2.2.jar" + - "BOOT-INF/lib/jackson-datatype-jdk8-2.16.1.jar" + - "BOOT-INF/lib/jackson-datatype-jsr310-2.16.1.jar" + - "BOOT-INF/lib/jackson-module-parameter-names-2.16.1.jar" + - "BOOT-INF/lib/jackson-databind-2.16.1.jar" + - "BOOT-INF/lib/tomcat-embed-websocket-10.1.19.jar" + - "BOOT-INF/lib/tomcat-embed-core-10.1.19.jar" + - "BOOT-INF/lib/tomcat-embed-el-10.1.19.jar" + - "BOOT-INF/lib/micrometer-observation-1.13.0-M1.jar" + - "BOOT-INF/lib/logback-classic-1.4.14.jar" + - "BOOT-INF/lib/log4j-to-slf4j-2.23.0.jar" + - "BOOT-INF/lib/jul-to-slf4j-2.0.12.jar" + - "BOOT-INF/lib/spring-jcl-6.1.4.jar" + - "BOOT-INF/lib/jackson-annotations-2.16.1.jar" + - "BOOT-INF/lib/jackson-core-2.16.1.jar" + - "BOOT-INF/lib/micrometer-commons-1.13.0-M1.jar" + - "BOOT-INF/lib/logback-core-1.4.14.jar" + - "BOOT-INF/lib/slf4j-api-2.0.12.jar" + - "BOOT-INF/lib/log4j-api-2.23.0.jar" + """; + } + + private Manifest createManifest() throws IOException { + return new Manifest(new ByteArrayInputStream(""" + Manifest-Version: 1.0 + Main-Class: org.springframework.boot.loader.launch.JarLauncher + Start-Class: org.springframework.boot.jarmode.tools.IndexedJarStructureTests + Spring-Boot-Version: 3.3.0-SNAPSHOT + Spring-Boot-Classes: BOOT-INF/classes/ + Spring-Boot-Lib: BOOT-INF/lib/ + Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx + Spring-Boot-Layers-Index: BOOT-INF/layers.idx + Build-Jdk-Spec: 17 + Implementation-Title: IndexedJarStructureTests + Implementation-Version: 0.0.1-SNAPSHOT + """.getBytes(StandardCharsets.UTF_8))); + } + +}