Reintroduce spring-boot-loader modules
Restore the `spring-boot-loader` with the previous loader code so that we can develop it further. See gh-37669pull/37640/head
parent
aeb6537f57
commit
a89057b7c7
@ -0,0 +1,23 @@
|
|||||||
|
plugins {
|
||||||
|
id "java-library"
|
||||||
|
id "org.springframework.boot.conventions"
|
||||||
|
id "org.springframework.boot.deployed"
|
||||||
|
}
|
||||||
|
|
||||||
|
description = "Spring Boot Loader"
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compileOnly("org.springframework:spring-core")
|
||||||
|
|
||||||
|
testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support"))
|
||||||
|
testImplementation("org.assertj:assertj-core")
|
||||||
|
testImplementation("org.awaitility:awaitility")
|
||||||
|
testImplementation("org.junit.jupiter:junit-jupiter")
|
||||||
|
testImplementation("org.mockito:mockito-core")
|
||||||
|
testImplementation("org.springframework:spring-test")
|
||||||
|
testImplementation("org.springframework:spring-core-test")
|
||||||
|
|
||||||
|
testRuntimeOnly("ch.qos.logback:logback-classic")
|
||||||
|
testRuntimeOnly("org.bouncycastle:bcprov-jdk18on:1.71")
|
||||||
|
testRuntimeOnly("org.springframework:spring-webmvc")
|
||||||
|
}
|
@ -0,0 +1,123 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class path index file that provides ordering information for JARs.
|
||||||
|
*
|
||||||
|
* @author Madhura Bhave
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
final class ClassPathIndexFile {
|
||||||
|
|
||||||
|
private final File root;
|
||||||
|
|
||||||
|
private final List<String> lines;
|
||||||
|
|
||||||
|
private ClassPathIndexFile(File root, List<String> lines) {
|
||||||
|
this.root = root;
|
||||||
|
this.lines = lines.stream().map(this::extractName).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractName(String line) {
|
||||||
|
if (line.startsWith("- \"") && line.endsWith("\"")) {
|
||||||
|
return line.substring(3, line.length() - 1);
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Malformed classpath index line [" + line + "]");
|
||||||
|
}
|
||||||
|
|
||||||
|
int size() {
|
||||||
|
return this.lines.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean containsEntry(String name) {
|
||||||
|
if (name == null || name.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this.lines.contains(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<URL> getUrls() {
|
||||||
|
return this.lines.stream().map(this::asUrl).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private URL asUrl(String line) {
|
||||||
|
try {
|
||||||
|
return new File(this.root, line).toURI().toURL();
|
||||||
|
}
|
||||||
|
catch (MalformedURLException ex) {
|
||||||
|
throw new IllegalStateException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static ClassPathIndexFile loadIfPossible(URL root, String location) throws IOException {
|
||||||
|
return loadIfPossible(asFile(root), location);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ClassPathIndexFile loadIfPossible(File root, String location) throws IOException {
|
||||||
|
return loadIfPossible(root, new File(root, location));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ClassPathIndexFile loadIfPossible(File root, File indexFile) throws IOException {
|
||||||
|
if (indexFile.exists() && indexFile.isFile()) {
|
||||||
|
try (InputStream inputStream = new FileInputStream(indexFile)) {
|
||||||
|
return new ClassPathIndexFile(root, loadLines(inputStream));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> loadLines(InputStream inputStream) throws IOException {
|
||||||
|
List<String> lines = new ArrayList<>();
|
||||||
|
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
|
||||||
|
String line = reader.readLine();
|
||||||
|
while (line != null) {
|
||||||
|
if (!line.trim().isEmpty()) {
|
||||||
|
lines.add(line);
|
||||||
|
}
|
||||||
|
line = reader.readLine();
|
||||||
|
}
|
||||||
|
return Collections.unmodifiableList(lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static File asFile(URL url) {
|
||||||
|
if (!"file".equals(url.getProtocol())) {
|
||||||
|
throw new IllegalArgumentException("URL does not reference a file");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return new File(url.toURI());
|
||||||
|
}
|
||||||
|
catch (URISyntaxException ex) {
|
||||||
|
return new File(url.getPath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,207 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.jar.Attributes;
|
||||||
|
import java.util.jar.Manifest;
|
||||||
|
|
||||||
|
import org.springframework.boot.loader.archive.Archive;
|
||||||
|
import org.springframework.boot.loader.archive.ExplodedArchive;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for executable archive {@link Launcher}s.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Madhura Bhave
|
||||||
|
* @author Scott Frederick
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
public abstract class ExecutableArchiveLauncher extends Launcher {
|
||||||
|
|
||||||
|
private static final String START_CLASS_ATTRIBUTE = "Start-Class";
|
||||||
|
|
||||||
|
protected static final String BOOT_CLASSPATH_INDEX_ATTRIBUTE = "Spring-Boot-Classpath-Index";
|
||||||
|
|
||||||
|
protected static final String DEFAULT_CLASSPATH_INDEX_FILE_NAME = "classpath.idx";
|
||||||
|
|
||||||
|
private final Archive archive;
|
||||||
|
|
||||||
|
private final ClassPathIndexFile classPathIndex;
|
||||||
|
|
||||||
|
public ExecutableArchiveLauncher() {
|
||||||
|
try {
|
||||||
|
this.archive = createArchive();
|
||||||
|
this.classPathIndex = getClassPathIndex(this.archive);
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
throw new IllegalStateException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ExecutableArchiveLauncher(Archive archive) {
|
||||||
|
try {
|
||||||
|
this.archive = archive;
|
||||||
|
this.classPathIndex = getClassPathIndex(this.archive);
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
throw new IllegalStateException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ClassPathIndexFile getClassPathIndex(Archive archive) throws IOException {
|
||||||
|
// Only needed for exploded archives, regular ones already have a defined order
|
||||||
|
if (archive instanceof ExplodedArchive) {
|
||||||
|
String location = getClassPathIndexFileLocation(archive);
|
||||||
|
return ClassPathIndexFile.loadIfPossible(archive.getUrl(), location);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getClassPathIndexFileLocation(Archive archive) throws IOException {
|
||||||
|
Manifest manifest = archive.getManifest();
|
||||||
|
Attributes attributes = (manifest != null) ? manifest.getMainAttributes() : null;
|
||||||
|
String location = (attributes != null) ? attributes.getValue(BOOT_CLASSPATH_INDEX_ATTRIBUTE) : null;
|
||||||
|
return (location != null) ? location : getArchiveEntryPathPrefix() + DEFAULT_CLASSPATH_INDEX_FILE_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getMainClass() throws Exception {
|
||||||
|
Manifest manifest = this.archive.getManifest();
|
||||||
|
String mainClass = null;
|
||||||
|
if (manifest != null) {
|
||||||
|
mainClass = manifest.getMainAttributes().getValue(START_CLASS_ATTRIBUTE);
|
||||||
|
}
|
||||||
|
if (mainClass == null) {
|
||||||
|
throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this);
|
||||||
|
}
|
||||||
|
return mainClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ClassLoader createClassLoader(Iterator<Archive> archives) throws Exception {
|
||||||
|
List<URL> urls = new ArrayList<>(guessClassPathSize());
|
||||||
|
while (archives.hasNext()) {
|
||||||
|
urls.add(archives.next().getUrl());
|
||||||
|
}
|
||||||
|
if (this.classPathIndex != null) {
|
||||||
|
urls.addAll(this.classPathIndex.getUrls());
|
||||||
|
}
|
||||||
|
return createClassLoader(urls.toArray(new URL[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
private int guessClassPathSize() {
|
||||||
|
if (this.classPathIndex != null) {
|
||||||
|
return this.classPathIndex.size() + 10;
|
||||||
|
}
|
||||||
|
return 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Iterator<Archive> getClassPathArchivesIterator() throws Exception {
|
||||||
|
Archive.EntryFilter searchFilter = this::isSearchCandidate;
|
||||||
|
Iterator<Archive> archives = this.archive.getNestedArchives(searchFilter,
|
||||||
|
(entry) -> isNestedArchive(entry) && !isEntryIndexed(entry));
|
||||||
|
if (isPostProcessingClassPathArchives()) {
|
||||||
|
archives = applyClassPathArchivePostProcessing(archives);
|
||||||
|
}
|
||||||
|
return archives;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isEntryIndexed(Archive.Entry entry) {
|
||||||
|
if (this.classPathIndex != null) {
|
||||||
|
return this.classPathIndex.containsEntry(entry.getName());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Iterator<Archive> applyClassPathArchivePostProcessing(Iterator<Archive> archives) throws Exception {
|
||||||
|
List<Archive> list = new ArrayList<>();
|
||||||
|
while (archives.hasNext()) {
|
||||||
|
list.add(archives.next());
|
||||||
|
}
|
||||||
|
postProcessClassPathArchives(list);
|
||||||
|
return list.iterator();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the specified entry is a candidate for further searching.
|
||||||
|
* @param entry the entry to check
|
||||||
|
* @return {@code true} if the entry is a candidate for further searching
|
||||||
|
* @since 2.3.0
|
||||||
|
*/
|
||||||
|
protected boolean isSearchCandidate(Archive.Entry entry) {
|
||||||
|
if (getArchiveEntryPathPrefix() == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return entry.getName().startsWith(getArchiveEntryPathPrefix());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the specified entry is a nested item that should be added to the
|
||||||
|
* classpath.
|
||||||
|
* @param entry the entry to check
|
||||||
|
* @return {@code true} if the entry is a nested item (jar or directory)
|
||||||
|
*/
|
||||||
|
protected abstract boolean isNestedArchive(Archive.Entry entry);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return if post-processing needs to be applied to the archives. For back
|
||||||
|
* compatibility this method returns {@code true}, but subclasses that don't override
|
||||||
|
* {@link #postProcessClassPathArchives(List)} should provide an implementation that
|
||||||
|
* returns {@code false}.
|
||||||
|
* @return if the {@link #postProcessClassPathArchives(List)} method is implemented
|
||||||
|
* @since 2.3.0
|
||||||
|
*/
|
||||||
|
protected boolean isPostProcessingClassPathArchives() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called to post-process archive entries before they are used. Implementations can
|
||||||
|
* add and remove entries.
|
||||||
|
* @param archives the archives
|
||||||
|
* @throws Exception if the post-processing fails
|
||||||
|
* @see #isPostProcessingClassPathArchives()
|
||||||
|
*/
|
||||||
|
protected void postProcessClassPathArchives(List<Archive> archives) throws Exception {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the path prefix for entries in the archive.
|
||||||
|
* @return the path prefix
|
||||||
|
*/
|
||||||
|
protected String getArchiveEntryPathPrefix() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isExploded() {
|
||||||
|
return this.archive.isExploded();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected final Archive getArchive() {
|
||||||
|
return this.archive;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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;
|
||||||
|
|
||||||
|
import org.springframework.boot.loader.archive.Archive;
|
||||||
|
import org.springframework.boot.loader.archive.Archive.EntryFilter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link Launcher} for JAR based archives. This launcher assumes that dependency jars are
|
||||||
|
* included inside a {@code /BOOT-INF/lib} directory and that application classes are
|
||||||
|
* included inside a {@code /BOOT-INF/classes} directory.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Madhura Bhave
|
||||||
|
* @author Scott Frederick
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
public class JarLauncher extends ExecutableArchiveLauncher {
|
||||||
|
|
||||||
|
static final EntryFilter NESTED_ARCHIVE_ENTRY_FILTER = (entry) -> {
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
return entry.getName().equals("BOOT-INF/classes/");
|
||||||
|
}
|
||||||
|
return entry.getName().startsWith("BOOT-INF/lib/");
|
||||||
|
};
|
||||||
|
|
||||||
|
public JarLauncher() {
|
||||||
|
}
|
||||||
|
|
||||||
|
protected JarLauncher(Archive archive) {
|
||||||
|
super(archive);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isPostProcessingClassPathArchives() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isNestedArchive(Archive.Entry entry) {
|
||||||
|
return NESTED_ARCHIVE_ENTRY_FILTER.matches(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getArchiveEntryPathPrefix() {
|
||||||
|
return "BOOT-INF/";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
new JarLauncher().launch(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,366 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.JarURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.URLClassLoader;
|
||||||
|
import java.net.URLConnection;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
import java.util.jar.JarFile;
|
||||||
|
import java.util.jar.Manifest;
|
||||||
|
|
||||||
|
import org.springframework.boot.loader.archive.Archive;
|
||||||
|
import org.springframework.boot.loader.jar.Handler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link ClassLoader} used by the {@link Launcher}.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @author Dave Syer
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
public class LaunchedURLClassLoader extends URLClassLoader {
|
||||||
|
|
||||||
|
private static final int BUFFER_SIZE = 4096;
|
||||||
|
|
||||||
|
static {
|
||||||
|
ClassLoader.registerAsParallelCapable();
|
||||||
|
}
|
||||||
|
|
||||||
|
private final boolean exploded;
|
||||||
|
|
||||||
|
private final Archive rootArchive;
|
||||||
|
|
||||||
|
private final Object packageLock = new Object();
|
||||||
|
|
||||||
|
private volatile DefinePackageCallType definePackageCallType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link LaunchedURLClassLoader} instance.
|
||||||
|
* @param urls the URLs from which to load classes and resources
|
||||||
|
* @param parent the parent class loader for delegation
|
||||||
|
*/
|
||||||
|
public LaunchedURLClassLoader(URL[] urls, ClassLoader parent) {
|
||||||
|
this(false, urls, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link LaunchedURLClassLoader} instance.
|
||||||
|
* @param exploded if the underlying archive is exploded
|
||||||
|
* @param urls the URLs from which to load classes and resources
|
||||||
|
* @param parent the parent class loader for delegation
|
||||||
|
*/
|
||||||
|
public LaunchedURLClassLoader(boolean exploded, URL[] urls, ClassLoader parent) {
|
||||||
|
this(exploded, null, urls, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link LaunchedURLClassLoader} instance.
|
||||||
|
* @param exploded if the underlying archive is exploded
|
||||||
|
* @param rootArchive the root archive or {@code null}
|
||||||
|
* @param urls the URLs from which to load classes and resources
|
||||||
|
* @param parent the parent class loader for delegation
|
||||||
|
* @since 2.3.1
|
||||||
|
*/
|
||||||
|
public LaunchedURLClassLoader(boolean exploded, Archive rootArchive, URL[] urls, ClassLoader parent) {
|
||||||
|
super(urls, parent);
|
||||||
|
this.exploded = exploded;
|
||||||
|
this.rootArchive = rootArchive;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public URL findResource(String name) {
|
||||||
|
if (this.exploded) {
|
||||||
|
return super.findResource(name);
|
||||||
|
}
|
||||||
|
Handler.setUseFastConnectionExceptions(true);
|
||||||
|
try {
|
||||||
|
return super.findResource(name);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
Handler.setUseFastConnectionExceptions(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Enumeration<URL> findResources(String name) throws IOException {
|
||||||
|
if (this.exploded) {
|
||||||
|
return super.findResources(name);
|
||||||
|
}
|
||||||
|
Handler.setUseFastConnectionExceptions(true);
|
||||||
|
try {
|
||||||
|
return new UseFastConnectionExceptionsEnumeration(super.findResources(name));
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
Handler.setUseFastConnectionExceptions(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
|
||||||
|
if (name.startsWith("org.springframework.boot.loader.jarmode.")) {
|
||||||
|
try {
|
||||||
|
Class<?> result = loadClassInLaunchedClassLoader(name);
|
||||||
|
if (resolve) {
|
||||||
|
resolveClass(result);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (ClassNotFoundException ex) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.exploded) {
|
||||||
|
return super.loadClass(name, resolve);
|
||||||
|
}
|
||||||
|
Handler.setUseFastConnectionExceptions(true);
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
definePackageIfNecessary(name);
|
||||||
|
}
|
||||||
|
catch (IllegalArgumentException ex) {
|
||||||
|
// Tolerate race condition due to being parallel capable
|
||||||
|
if (getDefinedPackage(name) == null) {
|
||||||
|
// This should never happen as the IllegalArgumentException indicates
|
||||||
|
// that the package has already been defined and, therefore,
|
||||||
|
// getDefinedPackage(name) should not return null.
|
||||||
|
throw new AssertionError("Package " + name + " has already been defined but it could not be found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.loadClass(name, resolve);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
Handler.setUseFastConnectionExceptions(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Class<?> loadClassInLaunchedClassLoader(String name) throws ClassNotFoundException {
|
||||||
|
String internalName = name.replace('.', '/') + ".class";
|
||||||
|
InputStream inputStream = getParent().getResourceAsStream(internalName);
|
||||||
|
if (inputStream == null) {
|
||||||
|
throw new ClassNotFoundException(name);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
|
byte[] buffer = new byte[BUFFER_SIZE];
|
||||||
|
int bytesRead = -1;
|
||||||
|
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||||
|
outputStream.write(buffer, 0, bytesRead);
|
||||||
|
}
|
||||||
|
inputStream.close();
|
||||||
|
byte[] bytes = outputStream.toByteArray();
|
||||||
|
Class<?> definedClass = defineClass(name, bytes, 0, bytes.length);
|
||||||
|
definePackageIfNecessary(name);
|
||||||
|
return definedClass;
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
inputStream.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException ex) {
|
||||||
|
throw new ClassNotFoundException("Cannot load resource for class [" + name + "]", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define a package before a {@code findClass} call is made. This is necessary to
|
||||||
|
* ensure that the appropriate manifest for nested JARs is associated with the
|
||||||
|
* package.
|
||||||
|
* @param className the class name being found
|
||||||
|
*/
|
||||||
|
private void definePackageIfNecessary(String className) {
|
||||||
|
int lastDot = className.lastIndexOf('.');
|
||||||
|
if (lastDot >= 0) {
|
||||||
|
String packageName = className.substring(0, lastDot);
|
||||||
|
if (getDefinedPackage(packageName) == null) {
|
||||||
|
try {
|
||||||
|
definePackage(className, packageName);
|
||||||
|
}
|
||||||
|
catch (IllegalArgumentException ex) {
|
||||||
|
// Tolerate race condition due to being parallel capable
|
||||||
|
if (getDefinedPackage(packageName) == null) {
|
||||||
|
// This should never happen as the IllegalArgumentException
|
||||||
|
// indicates that the package has already been defined and,
|
||||||
|
// therefore, getDefinedPackage(name) should not have returned
|
||||||
|
// null.
|
||||||
|
throw new AssertionError(
|
||||||
|
"Package " + packageName + " has already been defined but it could not be found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void definePackage(String className, String packageName) {
|
||||||
|
String packageEntryName = packageName.replace('.', '/') + "/";
|
||||||
|
String classEntryName = className.replace('.', '/') + ".class";
|
||||||
|
for (URL url : getURLs()) {
|
||||||
|
try {
|
||||||
|
URLConnection connection = url.openConnection();
|
||||||
|
if (connection instanceof JarURLConnection jarURLConnection) {
|
||||||
|
JarFile jarFile = jarURLConnection.getJarFile();
|
||||||
|
if (jarFile.getEntry(classEntryName) != null && jarFile.getEntry(packageEntryName) != null
|
||||||
|
&& jarFile.getManifest() != null) {
|
||||||
|
definePackage(packageName, jarFile.getManifest(), url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException ex) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Package definePackage(String name, Manifest man, URL url) throws IllegalArgumentException {
|
||||||
|
if (!this.exploded) {
|
||||||
|
return super.definePackage(name, man, url);
|
||||||
|
}
|
||||||
|
synchronized (this.packageLock) {
|
||||||
|
return doDefinePackage(DefinePackageCallType.MANIFEST, () -> super.definePackage(name, man, url));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Package definePackage(String name, String specTitle, String specVersion, String specVendor,
|
||||||
|
String implTitle, String implVersion, String implVendor, URL sealBase) throws IllegalArgumentException {
|
||||||
|
if (!this.exploded) {
|
||||||
|
return super.definePackage(name, specTitle, specVersion, specVendor, implTitle, implVersion, implVendor,
|
||||||
|
sealBase);
|
||||||
|
}
|
||||||
|
synchronized (this.packageLock) {
|
||||||
|
if (this.definePackageCallType == null) {
|
||||||
|
// We're not part of a call chain which means that the URLClassLoader
|
||||||
|
// is trying to define a package for our exploded JAR. We use the
|
||||||
|
// manifest version to ensure package attributes are set
|
||||||
|
Manifest manifest = getManifest(this.rootArchive);
|
||||||
|
if (manifest != null) {
|
||||||
|
return definePackage(name, manifest, sealBase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return doDefinePackage(DefinePackageCallType.ATTRIBUTES, () -> super.definePackage(name, specTitle,
|
||||||
|
specVersion, specVendor, implTitle, implVersion, implVendor, sealBase));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Manifest getManifest(Archive archive) {
|
||||||
|
try {
|
||||||
|
return (archive != null) ? archive.getManifest() : null;
|
||||||
|
}
|
||||||
|
catch (IOException ex) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> T doDefinePackage(DefinePackageCallType type, Supplier<T> call) {
|
||||||
|
DefinePackageCallType existingType = this.definePackageCallType;
|
||||||
|
try {
|
||||||
|
this.definePackageCallType = type;
|
||||||
|
return call.get();
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
this.definePackageCallType = existingType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear URL caches.
|
||||||
|
*/
|
||||||
|
public void clearCache() {
|
||||||
|
if (this.exploded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (URL url : getURLs()) {
|
||||||
|
try {
|
||||||
|
URLConnection connection = url.openConnection();
|
||||||
|
if (connection instanceof JarURLConnection) {
|
||||||
|
clearCache(connection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException ex) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearCache(URLConnection connection) throws IOException {
|
||||||
|
Object jarFile = ((JarURLConnection) connection).getJarFile();
|
||||||
|
if (jarFile instanceof org.springframework.boot.loader.jar.JarFile) {
|
||||||
|
((org.springframework.boot.loader.jar.JarFile) jarFile).clearCache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class UseFastConnectionExceptionsEnumeration implements Enumeration<URL> {
|
||||||
|
|
||||||
|
private final Enumeration<URL> delegate;
|
||||||
|
|
||||||
|
UseFastConnectionExceptionsEnumeration(Enumeration<URL> delegate) {
|
||||||
|
this.delegate = delegate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasMoreElements() {
|
||||||
|
Handler.setUseFastConnectionExceptions(true);
|
||||||
|
try {
|
||||||
|
return this.delegate.hasMoreElements();
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
Handler.setUseFastConnectionExceptions(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public URL nextElement() {
|
||||||
|
Handler.setUseFastConnectionExceptions(true);
|
||||||
|
try {
|
||||||
|
return this.delegate.nextElement();
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
Handler.setUseFastConnectionExceptions(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The different types of call made to define a package. We track these for exploded
|
||||||
|
* jars so that we can detect packages that should have manifest attributes applied.
|
||||||
|
*/
|
||||||
|
private enum DefinePackageCallType {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A define package call from a resource that has a manifest.
|
||||||
|
*/
|
||||||
|
MANIFEST,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A define package call with a direct set of attributes.
|
||||||
|
*/
|
||||||
|
ATTRIBUTES
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,159 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.security.CodeSource;
|
||||||
|
import java.security.ProtectionDomain;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.boot.loader.archive.Archive;
|
||||||
|
import org.springframework.boot.loader.archive.ExplodedArchive;
|
||||||
|
import org.springframework.boot.loader.archive.JarFileArchive;
|
||||||
|
import org.springframework.boot.loader.jar.JarFile;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for launchers that can start an application with a fully configured
|
||||||
|
* classpath backed by one or more {@link Archive}s.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @author Dave Syer
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
public abstract class Launcher {
|
||||||
|
|
||||||
|
private static final String JAR_MODE_LAUNCHER = "org.springframework.boot.loader.jarmode.JarModeLauncher";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launch the application. This method is the initial entry point that should be
|
||||||
|
* called by a subclass {@code public static void main(String[] args)} method.
|
||||||
|
* @param args the incoming arguments
|
||||||
|
* @throws Exception if the application fails to launch
|
||||||
|
*/
|
||||||
|
protected void launch(String[] args) throws Exception {
|
||||||
|
if (!isExploded()) {
|
||||||
|
JarFile.registerUrlProtocolHandler();
|
||||||
|
}
|
||||||
|
ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());
|
||||||
|
String jarMode = System.getProperty("jarmode");
|
||||||
|
String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass();
|
||||||
|
launch(args, launchClass, classLoader);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a classloader for the specified archives.
|
||||||
|
* @param archives the archives
|
||||||
|
* @return the classloader
|
||||||
|
* @throws Exception if the classloader cannot be created
|
||||||
|
* @since 2.3.0
|
||||||
|
*/
|
||||||
|
protected ClassLoader createClassLoader(Iterator<Archive> archives) throws Exception {
|
||||||
|
List<URL> urls = new ArrayList<>(50);
|
||||||
|
while (archives.hasNext()) {
|
||||||
|
urls.add(archives.next().getUrl());
|
||||||
|
}
|
||||||
|
return createClassLoader(urls.toArray(new URL[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a classloader for the specified URLs.
|
||||||
|
* @param urls the URLs
|
||||||
|
* @return the classloader
|
||||||
|
* @throws Exception if the classloader cannot be created
|
||||||
|
*/
|
||||||
|
protected ClassLoader createClassLoader(URL[] urls) throws Exception {
|
||||||
|
return new LaunchedURLClassLoader(isExploded(), getArchive(), urls, getClass().getClassLoader());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launch the application given the archive file and a fully configured classloader.
|
||||||
|
* @param args the incoming arguments
|
||||||
|
* @param launchClass the launch class to run
|
||||||
|
* @param classLoader the classloader
|
||||||
|
* @throws Exception if the launch fails
|
||||||
|
*/
|
||||||
|
protected void launch(String[] args, String launchClass, ClassLoader classLoader) throws Exception {
|
||||||
|
Thread.currentThread().setContextClassLoader(classLoader);
|
||||||
|
createMainMethodRunner(launchClass, args, classLoader).run();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the {@code MainMethodRunner} used to launch the application.
|
||||||
|
* @param mainClass the main class
|
||||||
|
* @param args the incoming arguments
|
||||||
|
* @param classLoader the classloader
|
||||||
|
* @return the main method runner
|
||||||
|
*/
|
||||||
|
protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) {
|
||||||
|
return new MainMethodRunner(mainClass, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the main class that should be launched.
|
||||||
|
* @return the name of the main class
|
||||||
|
* @throws Exception if the main class cannot be obtained
|
||||||
|
*/
|
||||||
|
protected abstract String getMainClass() throws Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the archives that will be used to construct the class path.
|
||||||
|
* @return the class path archives
|
||||||
|
* @throws Exception if the class path archives cannot be obtained
|
||||||
|
* @since 2.3.0
|
||||||
|
*/
|
||||||
|
protected abstract Iterator<Archive> getClassPathArchivesIterator() throws Exception;
|
||||||
|
|
||||||
|
protected final Archive createArchive() throws Exception {
|
||||||
|
ProtectionDomain protectionDomain = getClass().getProtectionDomain();
|
||||||
|
CodeSource codeSource = protectionDomain.getCodeSource();
|
||||||
|
URI location = (codeSource != null) ? codeSource.getLocation().toURI() : null;
|
||||||
|
String path = (location != null) ? location.getSchemeSpecificPart() : null;
|
||||||
|
if (path == null) {
|
||||||
|
throw new IllegalStateException("Unable to determine code source archive");
|
||||||
|
}
|
||||||
|
File root = new File(path);
|
||||||
|
if (!root.exists()) {
|
||||||
|
throw new IllegalStateException("Unable to determine code source archive from " + root);
|
||||||
|
}
|
||||||
|
return (root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns if the launcher is running in an exploded mode. If this method returns
|
||||||
|
* {@code true} then only regular JARs are supported and the additional URL and
|
||||||
|
* ClassLoader support infrastructure can be optimized.
|
||||||
|
* @return if the jar is exploded.
|
||||||
|
* @since 2.3.0
|
||||||
|
*/
|
||||||
|
protected boolean isExploded() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the root archive.
|
||||||
|
* @return the root archive
|
||||||
|
* @since 2.3.1
|
||||||
|
*/
|
||||||
|
protected Archive getArchive() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class that is used by {@link Launcher}s to call a main method. The class
|
||||||
|
* containing the main method is loaded using the thread context class loader.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
public class MainMethodRunner {
|
||||||
|
|
||||||
|
private final String mainClassName;
|
||||||
|
|
||||||
|
private final String[] args;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link MainMethodRunner} instance.
|
||||||
|
* @param mainClass the main class
|
||||||
|
* @param args incoming arguments
|
||||||
|
*/
|
||||||
|
public MainMethodRunner(String mainClass, String[] args) {
|
||||||
|
this.mainClassName = mainClass;
|
||||||
|
this.args = (args != null) ? args.clone() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void run() throws Exception {
|
||||||
|
Class<?> mainClass = Class.forName(this.mainClassName, false, Thread.currentThread().getContextClassLoader());
|
||||||
|
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
|
||||||
|
mainMethod.setAccessible(true);
|
||||||
|
mainMethod.invoke(null, new Object[] { this.args });
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,726 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.URLConnection;
|
||||||
|
import java.net.URLDecoder;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Properties;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.jar.Manifest;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import org.springframework.boot.loader.archive.Archive;
|
||||||
|
import org.springframework.boot.loader.archive.Archive.Entry;
|
||||||
|
import org.springframework.boot.loader.archive.Archive.EntryFilter;
|
||||||
|
import org.springframework.boot.loader.archive.ExplodedArchive;
|
||||||
|
import org.springframework.boot.loader.archive.JarFileArchive;
|
||||||
|
import org.springframework.boot.loader.util.SystemPropertyUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link Launcher} for archives with user-configured classpath and main class through a
|
||||||
|
* properties file. This model is often more flexible and more amenable to creating
|
||||||
|
* well-behaved OS-level services than a model based on executable jars.
|
||||||
|
* <p>
|
||||||
|
* Looks in various places for a properties file to extract loader settings, defaulting to
|
||||||
|
* {@code loader.properties} either on the current classpath or in the current working
|
||||||
|
* directory. The name of the properties file can be changed by setting a System property
|
||||||
|
* {@code loader.config.name} (e.g. {@code -Dloader.config.name=foo} will look for
|
||||||
|
* {@code foo.properties}. If that file doesn't exist then tries
|
||||||
|
* {@code loader.config.location} (with allowed prefixes {@code classpath:} and
|
||||||
|
* {@code file:} or any valid URL). Once that file is located turns it into Properties and
|
||||||
|
* extracts optional values (which can also be provided overridden as System properties in
|
||||||
|
* case the file doesn't exist):
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code loader.path}: a comma-separated list of directories (containing file
|
||||||
|
* resources and/or nested archives in *.jar or *.zip or archives) or archives to append
|
||||||
|
* to the classpath. {@code BOOT-INF/classes,BOOT-INF/lib} in the application archive are
|
||||||
|
* always used</li>
|
||||||
|
* <li>{@code loader.main}: the main method to delegate execution to once the class loader
|
||||||
|
* is set up. No default, but will fall back to looking for a {@code Start-Class} in a
|
||||||
|
* {@code MANIFEST.MF}, if there is one in <code>${loader.home}/META-INF</code>.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @author Dave Syer
|
||||||
|
* @author Janne Valkealahti
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
public class PropertiesLauncher extends Launcher {
|
||||||
|
|
||||||
|
private static final Class<?>[] PARENT_ONLY_PARAMS = new Class<?>[] { ClassLoader.class };
|
||||||
|
|
||||||
|
private static final Class<?>[] URLS_AND_PARENT_PARAMS = new Class<?>[] { URL[].class, ClassLoader.class };
|
||||||
|
|
||||||
|
private static final Class<?>[] NO_PARAMS = new Class<?>[] {};
|
||||||
|
|
||||||
|
private static final URL[] NO_URLS = new URL[0];
|
||||||
|
|
||||||
|
private static final String DEBUG = "loader.debug";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Properties key for main class. As a manifest entry can also be specified as
|
||||||
|
* {@code Start-Class}.
|
||||||
|
*/
|
||||||
|
public static final String MAIN = "loader.main";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Properties key for classpath entries (directories possibly containing jars or
|
||||||
|
* jars). Multiple entries can be specified using a comma-separated list. {@code
|
||||||
|
* BOOT-INF/classes,BOOT-INF/lib} in the application archive are always used.
|
||||||
|
*/
|
||||||
|
public static final String PATH = "loader.path";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Properties key for home directory. This is the location of external configuration
|
||||||
|
* if not on classpath, and also the base path for any relative paths in the
|
||||||
|
* {@link #PATH loader path}. Defaults to current working directory (
|
||||||
|
* <code>${user.dir}</code>).
|
||||||
|
*/
|
||||||
|
public static final String HOME = "loader.home";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Properties key for default command line arguments. These arguments (if present) are
|
||||||
|
* prepended to the main method arguments before launching.
|
||||||
|
*/
|
||||||
|
public static final String ARGS = "loader.args";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Properties key for name of external configuration file (excluding suffix). Defaults
|
||||||
|
* to "application". Ignored if {@link #CONFIG_LOCATION loader config location} is
|
||||||
|
* provided instead.
|
||||||
|
*/
|
||||||
|
public static final String CONFIG_NAME = "loader.config.name";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Properties key for config file location (including optional classpath:, file: or
|
||||||
|
* URL prefix).
|
||||||
|
*/
|
||||||
|
public static final String CONFIG_LOCATION = "loader.config.location";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Properties key for boolean flag (default false) which, if set, will cause the
|
||||||
|
* external configuration properties to be copied to System properties (assuming that
|
||||||
|
* is allowed by Java security).
|
||||||
|
*/
|
||||||
|
public static final String SET_SYSTEM_PROPERTIES = "loader.system";
|
||||||
|
|
||||||
|
private static final Pattern WORD_SEPARATOR = Pattern.compile("\\W+");
|
||||||
|
|
||||||
|
private static final String NESTED_ARCHIVE_SEPARATOR = "!" + File.separator;
|
||||||
|
|
||||||
|
private final File home;
|
||||||
|
|
||||||
|
private List<String> paths = new ArrayList<>();
|
||||||
|
|
||||||
|
private final Properties properties = new Properties();
|
||||||
|
|
||||||
|
private final Archive parent;
|
||||||
|
|
||||||
|
private volatile ClassPathArchives classPathArchives;
|
||||||
|
|
||||||
|
public PropertiesLauncher() {
|
||||||
|
try {
|
||||||
|
this.home = getHomeDirectory();
|
||||||
|
initializeProperties();
|
||||||
|
initializePaths();
|
||||||
|
this.parent = createArchive();
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
throw new IllegalStateException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected File getHomeDirectory() {
|
||||||
|
try {
|
||||||
|
return new File(getPropertyWithDefault(HOME, "${user.dir}"));
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
throw new IllegalStateException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeProperties() throws Exception {
|
||||||
|
List<String> configs = new ArrayList<>();
|
||||||
|
if (getProperty(CONFIG_LOCATION) != null) {
|
||||||
|
configs.add(getProperty(CONFIG_LOCATION));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
String[] names = getPropertyWithDefault(CONFIG_NAME, "loader").split(",");
|
||||||
|
for (String name : names) {
|
||||||
|
configs.add("file:" + getHomeDirectory() + "/" + name + ".properties");
|
||||||
|
configs.add("classpath:" + name + ".properties");
|
||||||
|
configs.add("classpath:BOOT-INF/classes/" + name + ".properties");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (String config : configs) {
|
||||||
|
try (InputStream resource = getResource(config)) {
|
||||||
|
if (resource != null) {
|
||||||
|
debug("Found: " + config);
|
||||||
|
loadResource(resource);
|
||||||
|
// Load the first one we find
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
debug("Not found: " + config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadResource(InputStream resource) throws Exception {
|
||||||
|
this.properties.load(resource);
|
||||||
|
for (Object key : Collections.list(this.properties.propertyNames())) {
|
||||||
|
String text = this.properties.getProperty((String) key);
|
||||||
|
String value = SystemPropertyUtils.resolvePlaceholders(this.properties, text);
|
||||||
|
if (value != null) {
|
||||||
|
this.properties.put(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ("true".equals(getProperty(SET_SYSTEM_PROPERTIES))) {
|
||||||
|
debug("Adding resolved properties to System properties");
|
||||||
|
for (Object key : Collections.list(this.properties.propertyNames())) {
|
||||||
|
String value = this.properties.getProperty((String) key);
|
||||||
|
System.setProperty((String) key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private InputStream getResource(String config) throws Exception {
|
||||||
|
if (config.startsWith("classpath:")) {
|
||||||
|
return getClasspathResource(config.substring("classpath:".length()));
|
||||||
|
}
|
||||||
|
config = handleUrl(config);
|
||||||
|
if (isUrl(config)) {
|
||||||
|
return getURLResource(config);
|
||||||
|
}
|
||||||
|
return getFileResource(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String handleUrl(String path) throws UnsupportedEncodingException {
|
||||||
|
if (path.startsWith("jar:file:") || path.startsWith("file:")) {
|
||||||
|
path = URLDecoder.decode(path, "UTF-8");
|
||||||
|
if (path.startsWith("file:")) {
|
||||||
|
path = path.substring("file:".length());
|
||||||
|
if (path.startsWith("//")) {
|
||||||
|
path = path.substring(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isUrl(String config) {
|
||||||
|
return config.contains("://");
|
||||||
|
}
|
||||||
|
|
||||||
|
private InputStream getClasspathResource(String config) {
|
||||||
|
while (config.startsWith("/")) {
|
||||||
|
config = config.substring(1);
|
||||||
|
}
|
||||||
|
config = "/" + config;
|
||||||
|
debug("Trying classpath: " + config);
|
||||||
|
return getClass().getResourceAsStream(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
private InputStream getFileResource(String config) throws Exception {
|
||||||
|
File file = new File(config);
|
||||||
|
debug("Trying file: " + config);
|
||||||
|
if (file.canRead()) {
|
||||||
|
return new FileInputStream(file);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private InputStream getURLResource(String config) throws Exception {
|
||||||
|
URL url = new URL(config);
|
||||||
|
if (exists(url)) {
|
||||||
|
URLConnection con = url.openConnection();
|
||||||
|
try {
|
||||||
|
return con.getInputStream();
|
||||||
|
}
|
||||||
|
catch (IOException ex) {
|
||||||
|
// Close the HTTP connection (if applicable).
|
||||||
|
if (con instanceof HttpURLConnection httpURLConnection) {
|
||||||
|
httpURLConnection.disconnect();
|
||||||
|
}
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean exists(URL url) throws IOException {
|
||||||
|
// Try a URL connection content-length header...
|
||||||
|
URLConnection connection = url.openConnection();
|
||||||
|
try {
|
||||||
|
connection.setUseCaches(connection.getClass().getSimpleName().startsWith("JNLP"));
|
||||||
|
if (connection instanceof HttpURLConnection httpConnection) {
|
||||||
|
httpConnection.setRequestMethod("HEAD");
|
||||||
|
int responseCode = httpConnection.getResponseCode();
|
||||||
|
if (responseCode == HttpURLConnection.HTTP_OK) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (responseCode == HttpURLConnection.HTTP_NOT_FOUND) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (connection.getContentLength() >= 0);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if (connection instanceof HttpURLConnection httpURLConnection) {
|
||||||
|
httpURLConnection.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializePaths() throws Exception {
|
||||||
|
String path = getProperty(PATH);
|
||||||
|
if (path != null) {
|
||||||
|
this.paths = parsePathsProperty(path);
|
||||||
|
}
|
||||||
|
debug("Nested archive paths: " + this.paths);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> parsePathsProperty(String commaSeparatedPaths) {
|
||||||
|
List<String> paths = new ArrayList<>();
|
||||||
|
for (String path : commaSeparatedPaths.split(",")) {
|
||||||
|
path = cleanupPath(path);
|
||||||
|
// "" means the user wants root of archive but not current directory
|
||||||
|
path = (path == null || path.isEmpty()) ? "/" : path;
|
||||||
|
paths.add(path);
|
||||||
|
}
|
||||||
|
if (paths.isEmpty()) {
|
||||||
|
paths.add("lib");
|
||||||
|
}
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String[] getArgs(String... args) throws Exception {
|
||||||
|
String loaderArgs = getProperty(ARGS);
|
||||||
|
if (loaderArgs != null) {
|
||||||
|
String[] defaultArgs = loaderArgs.split("\\s+");
|
||||||
|
String[] additionalArgs = args;
|
||||||
|
args = new String[defaultArgs.length + additionalArgs.length];
|
||||||
|
System.arraycopy(defaultArgs, 0, args, 0, defaultArgs.length);
|
||||||
|
System.arraycopy(additionalArgs, 0, args, defaultArgs.length, additionalArgs.length);
|
||||||
|
}
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getMainClass() throws Exception {
|
||||||
|
String mainClass = getProperty(MAIN, "Start-Class");
|
||||||
|
if (mainClass == null) {
|
||||||
|
throw new IllegalStateException("No '" + MAIN + "' or 'Start-Class' specified");
|
||||||
|
}
|
||||||
|
return mainClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ClassLoader createClassLoader(Iterator<Archive> archives) throws Exception {
|
||||||
|
String customLoaderClassName = getProperty("loader.classLoader");
|
||||||
|
if (customLoaderClassName == null) {
|
||||||
|
return super.createClassLoader(archives);
|
||||||
|
}
|
||||||
|
Set<URL> urls = new LinkedHashSet<>();
|
||||||
|
while (archives.hasNext()) {
|
||||||
|
urls.add(archives.next().getUrl());
|
||||||
|
}
|
||||||
|
ClassLoader loader = new LaunchedURLClassLoader(urls.toArray(NO_URLS), getClass().getClassLoader());
|
||||||
|
debug("Classpath for custom loader: " + urls);
|
||||||
|
loader = wrapWithCustomClassLoader(loader, customLoaderClassName);
|
||||||
|
debug("Using custom class loader: " + customLoaderClassName);
|
||||||
|
return loader;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private ClassLoader wrapWithCustomClassLoader(ClassLoader parent, String className) throws Exception {
|
||||||
|
Class<ClassLoader> type = (Class<ClassLoader>) Class.forName(className, true, parent);
|
||||||
|
ClassLoader classLoader = newClassLoader(type, PARENT_ONLY_PARAMS, parent);
|
||||||
|
if (classLoader == null) {
|
||||||
|
classLoader = newClassLoader(type, URLS_AND_PARENT_PARAMS, NO_URLS, parent);
|
||||||
|
}
|
||||||
|
if (classLoader == null) {
|
||||||
|
classLoader = newClassLoader(type, NO_PARAMS);
|
||||||
|
}
|
||||||
|
if (classLoader == null) {
|
||||||
|
throw new IllegalArgumentException("Unable to create class loader for " + className);
|
||||||
|
}
|
||||||
|
return classLoader;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ClassLoader newClassLoader(Class<ClassLoader> loaderClass, Class<?>[] parameterTypes, Object... initargs)
|
||||||
|
throws Exception {
|
||||||
|
try {
|
||||||
|
Constructor<ClassLoader> constructor = loaderClass.getDeclaredConstructor(parameterTypes);
|
||||||
|
constructor.setAccessible(true);
|
||||||
|
return constructor.newInstance(initargs);
|
||||||
|
}
|
||||||
|
catch (NoSuchMethodException ex) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getProperty(String propertyKey) throws Exception {
|
||||||
|
return getProperty(propertyKey, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getProperty(String propertyKey, String manifestKey) throws Exception {
|
||||||
|
return getProperty(propertyKey, manifestKey, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getPropertyWithDefault(String propertyKey, String defaultValue) throws Exception {
|
||||||
|
return getProperty(propertyKey, null, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getProperty(String propertyKey, String manifestKey, String defaultValue) throws Exception {
|
||||||
|
if (manifestKey == null) {
|
||||||
|
manifestKey = propertyKey.replace('.', '-');
|
||||||
|
manifestKey = toCamelCase(manifestKey);
|
||||||
|
}
|
||||||
|
String property = SystemPropertyUtils.getProperty(propertyKey);
|
||||||
|
if (property != null) {
|
||||||
|
String value = SystemPropertyUtils.resolvePlaceholders(this.properties, property);
|
||||||
|
debug("Property '" + propertyKey + "' from environment: " + value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (this.properties.containsKey(propertyKey)) {
|
||||||
|
String value = SystemPropertyUtils.resolvePlaceholders(this.properties,
|
||||||
|
this.properties.getProperty(propertyKey));
|
||||||
|
debug("Property '" + propertyKey + "' from properties: " + value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (this.home != null) {
|
||||||
|
// Prefer home dir for MANIFEST if there is one
|
||||||
|
try (ExplodedArchive archive = new ExplodedArchive(this.home, false)) {
|
||||||
|
Manifest manifest = archive.getManifest();
|
||||||
|
if (manifest != null) {
|
||||||
|
String value = manifest.getMainAttributes().getValue(manifestKey);
|
||||||
|
if (value != null) {
|
||||||
|
debug("Property '" + manifestKey + "' from home directory manifest: " + value);
|
||||||
|
return SystemPropertyUtils.resolvePlaceholders(this.properties, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IllegalStateException ex) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
// Otherwise try the parent archive
|
||||||
|
Manifest manifest = createArchive().getManifest();
|
||||||
|
if (manifest != null) {
|
||||||
|
String value = manifest.getMainAttributes().getValue(manifestKey);
|
||||||
|
if (value != null) {
|
||||||
|
debug("Property '" + manifestKey + "' from archive manifest: " + value);
|
||||||
|
return SystemPropertyUtils.resolvePlaceholders(this.properties, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (defaultValue != null) ? SystemPropertyUtils.resolvePlaceholders(this.properties, defaultValue)
|
||||||
|
: defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Iterator<Archive> getClassPathArchivesIterator() throws Exception {
|
||||||
|
ClassPathArchives classPathArchives = this.classPathArchives;
|
||||||
|
if (classPathArchives == null) {
|
||||||
|
classPathArchives = new ClassPathArchives();
|
||||||
|
this.classPathArchives = classPathArchives;
|
||||||
|
}
|
||||||
|
return classPathArchives.iterator();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
PropertiesLauncher launcher = new PropertiesLauncher();
|
||||||
|
args = launcher.getArgs(args);
|
||||||
|
launcher.launch(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String toCamelCase(CharSequence string) {
|
||||||
|
if (string == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
Matcher matcher = WORD_SEPARATOR.matcher(string);
|
||||||
|
int pos = 0;
|
||||||
|
while (matcher.find()) {
|
||||||
|
builder.append(capitalize(string.subSequence(pos, matcher.end()).toString()));
|
||||||
|
pos = matcher.end();
|
||||||
|
}
|
||||||
|
builder.append(capitalize(string.subSequence(pos, string.length()).toString()));
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String capitalize(String str) {
|
||||||
|
return Character.toUpperCase(str.charAt(0)) + str.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void debug(String message) {
|
||||||
|
if (Boolean.getBoolean(DEBUG)) {
|
||||||
|
System.out.println(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String cleanupPath(String path) {
|
||||||
|
path = path.trim();
|
||||||
|
// No need for current dir path
|
||||||
|
if (path.startsWith("./")) {
|
||||||
|
path = path.substring(2);
|
||||||
|
}
|
||||||
|
String lowerCasePath = path.toLowerCase(Locale.ENGLISH);
|
||||||
|
if (lowerCasePath.endsWith(".jar") || lowerCasePath.endsWith(".zip")) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
if (path.endsWith("/*")) {
|
||||||
|
path = path.substring(0, path.length() - 1);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// It's a directory
|
||||||
|
if (!path.endsWith("/") && !path.equals(".")) {
|
||||||
|
path = path + "/";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
void close() throws Exception {
|
||||||
|
if (this.classPathArchives != null) {
|
||||||
|
this.classPathArchives.close();
|
||||||
|
}
|
||||||
|
if (this.parent != null) {
|
||||||
|
this.parent.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An iterable collection of the classpath archives.
|
||||||
|
*/
|
||||||
|
private class ClassPathArchives implements Iterable<Archive> {
|
||||||
|
|
||||||
|
private final List<Archive> classPathArchives;
|
||||||
|
|
||||||
|
private final List<JarFileArchive> jarFileArchives = new ArrayList<>();
|
||||||
|
|
||||||
|
ClassPathArchives() throws Exception {
|
||||||
|
this.classPathArchives = new ArrayList<>();
|
||||||
|
for (String path : PropertiesLauncher.this.paths) {
|
||||||
|
for (Archive archive : getClassPathArchives(path)) {
|
||||||
|
addClassPathArchive(archive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addNestedEntries();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addClassPathArchive(Archive archive) throws IOException {
|
||||||
|
if (!(archive instanceof ExplodedArchive)) {
|
||||||
|
this.classPathArchives.add(archive);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.classPathArchives.add(archive);
|
||||||
|
this.classPathArchives.addAll(asList(archive.getNestedArchives(null, new ArchiveEntryFilter())));
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Archive> getClassPathArchives(String path) throws Exception {
|
||||||
|
String root = cleanupPath(handleUrl(path));
|
||||||
|
List<Archive> lib = new ArrayList<>();
|
||||||
|
File file = new File(root);
|
||||||
|
if (!"/".equals(root)) {
|
||||||
|
if (!isAbsolutePath(root)) {
|
||||||
|
file = new File(PropertiesLauncher.this.home, root);
|
||||||
|
}
|
||||||
|
if (file.isDirectory()) {
|
||||||
|
debug("Adding classpath entries from " + file);
|
||||||
|
Archive archive = new ExplodedArchive(file, false);
|
||||||
|
lib.add(archive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Archive archive = getArchive(file);
|
||||||
|
if (archive != null) {
|
||||||
|
debug("Adding classpath entries from archive " + archive.getUrl() + root);
|
||||||
|
lib.add(archive);
|
||||||
|
}
|
||||||
|
List<Archive> nestedArchives = getNestedArchives(root);
|
||||||
|
if (nestedArchives != null) {
|
||||||
|
debug("Adding classpath entries from nested " + root);
|
||||||
|
lib.addAll(nestedArchives);
|
||||||
|
}
|
||||||
|
return lib;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isAbsolutePath(String root) {
|
||||||
|
// Windows contains ":" others start with "/"
|
||||||
|
return root.contains(":") || root.startsWith("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Archive getArchive(File file) throws IOException {
|
||||||
|
if (isNestedArchivePath(file)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String name = file.getName().toLowerCase(Locale.ENGLISH);
|
||||||
|
if (name.endsWith(".jar") || name.endsWith(".zip")) {
|
||||||
|
return getJarFileArchive(file);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isNestedArchivePath(File file) {
|
||||||
|
return file.getPath().contains(NESTED_ARCHIVE_SEPARATOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Archive> getNestedArchives(String path) throws Exception {
|
||||||
|
Archive parent = PropertiesLauncher.this.parent;
|
||||||
|
String root = path;
|
||||||
|
if (!root.equals("/") && root.startsWith("/")
|
||||||
|
|| parent.getUrl().toURI().equals(PropertiesLauncher.this.home.toURI())) {
|
||||||
|
// If home dir is same as parent archive, no need to add it twice.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int index = root.indexOf('!');
|
||||||
|
if (index != -1) {
|
||||||
|
File file = new File(PropertiesLauncher.this.home, root.substring(0, index));
|
||||||
|
if (root.startsWith("jar:file:")) {
|
||||||
|
file = new File(root.substring("jar:file:".length(), index));
|
||||||
|
}
|
||||||
|
parent = getJarFileArchive(file);
|
||||||
|
root = root.substring(index + 1);
|
||||||
|
while (root.startsWith("/")) {
|
||||||
|
root = root.substring(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (root.endsWith(".jar")) {
|
||||||
|
File file = new File(PropertiesLauncher.this.home, root);
|
||||||
|
if (file.exists()) {
|
||||||
|
parent = getJarFileArchive(file);
|
||||||
|
root = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (root.equals("/") || root.equals("./") || root.equals(".")) {
|
||||||
|
// The prefix for nested jars is actually empty if it's at the root
|
||||||
|
root = "";
|
||||||
|
}
|
||||||
|
EntryFilter filter = new PrefixMatchingArchiveFilter(root);
|
||||||
|
List<Archive> archives = asList(parent.getNestedArchives(null, filter));
|
||||||
|
if ((root == null || root.isEmpty() || ".".equals(root)) && !path.endsWith(".jar")
|
||||||
|
&& parent != PropertiesLauncher.this.parent) {
|
||||||
|
// You can't find the root with an entry filter so it has to be added
|
||||||
|
// explicitly. But don't add the root of the parent archive.
|
||||||
|
archives.add(parent);
|
||||||
|
}
|
||||||
|
return archives;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addNestedEntries() {
|
||||||
|
// The parent archive might have "BOOT-INF/lib/" and "BOOT-INF/classes/"
|
||||||
|
// directories, meaning we are running from an executable JAR. We add nested
|
||||||
|
// entries from there with low priority (i.e. at end).
|
||||||
|
try {
|
||||||
|
Iterator<Archive> archives = PropertiesLauncher.this.parent.getNestedArchives(null,
|
||||||
|
JarLauncher.NESTED_ARCHIVE_ENTRY_FILTER);
|
||||||
|
while (archives.hasNext()) {
|
||||||
|
this.classPathArchives.add(archives.next());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException ex) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Archive> asList(Iterator<Archive> iterator) {
|
||||||
|
List<Archive> list = new ArrayList<>();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
list.add(iterator.next());
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JarFileArchive getJarFileArchive(File file) throws IOException {
|
||||||
|
JarFileArchive archive = new JarFileArchive(file);
|
||||||
|
this.jarFileArchives.add(archive);
|
||||||
|
return archive;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Iterator<Archive> iterator() {
|
||||||
|
return this.classPathArchives.iterator();
|
||||||
|
}
|
||||||
|
|
||||||
|
void close() throws IOException {
|
||||||
|
for (JarFileArchive archive : this.jarFileArchives) {
|
||||||
|
archive.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience class for finding nested archives that have a prefix in their file path
|
||||||
|
* (e.g. "lib/").
|
||||||
|
*/
|
||||||
|
private static final class PrefixMatchingArchiveFilter implements EntryFilter {
|
||||||
|
|
||||||
|
private final String prefix;
|
||||||
|
|
||||||
|
private final ArchiveEntryFilter filter = new ArchiveEntryFilter();
|
||||||
|
|
||||||
|
private PrefixMatchingArchiveFilter(String prefix) {
|
||||||
|
this.prefix = prefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean matches(Entry entry) {
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
return entry.getName().equals(this.prefix);
|
||||||
|
}
|
||||||
|
return entry.getName().startsWith(this.prefix) && this.filter.matches(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience class for finding nested archives (archive entries that can be
|
||||||
|
* classpath entries).
|
||||||
|
*/
|
||||||
|
private static final class ArchiveEntryFilter implements EntryFilter {
|
||||||
|
|
||||||
|
private static final String DOT_JAR = ".jar";
|
||||||
|
|
||||||
|
private static final String DOT_ZIP = ".zip";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean matches(Entry entry) {
|
||||||
|
return entry.getName().endsWith(DOT_JAR) || entry.getName().endsWith(DOT_ZIP);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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;
|
||||||
|
|
||||||
|
import org.springframework.boot.loader.archive.Archive;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link Launcher} for WAR based archives. This launcher for standard WAR archives.
|
||||||
|
* Supports dependencies in {@code WEB-INF/lib} as well as {@code WEB-INF/lib-provided},
|
||||||
|
* classes are loaded from {@code WEB-INF/classes}.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Scott Frederick
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
public class WarLauncher extends ExecutableArchiveLauncher {
|
||||||
|
|
||||||
|
public WarLauncher() {
|
||||||
|
}
|
||||||
|
|
||||||
|
protected WarLauncher(Archive archive) {
|
||||||
|
super(archive);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isPostProcessingClassPathArchives() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isNestedArchive(Archive.Entry entry) {
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
return entry.getName().equals("WEB-INF/classes/");
|
||||||
|
}
|
||||||
|
return entry.getName().startsWith("WEB-INF/lib/") || entry.getName().startsWith("WEB-INF/lib-provided/");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getArchiveEntryPathPrefix() {
|
||||||
|
return "WEB-INF/";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
new WarLauncher().launch(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,115 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.archive;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.jar.Manifest;
|
||||||
|
|
||||||
|
import org.springframework.boot.loader.Launcher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An archive that can be launched by the {@link Launcher}.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @since 1.0.0
|
||||||
|
* @see JarFileArchive
|
||||||
|
*/
|
||||||
|
public interface Archive extends Iterable<Archive.Entry>, AutoCloseable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a URL that can be used to load the archive.
|
||||||
|
* @return the archive URL
|
||||||
|
* @throws MalformedURLException if the URL is malformed
|
||||||
|
*/
|
||||||
|
URL getUrl() throws MalformedURLException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the manifest of the archive.
|
||||||
|
* @return the manifest
|
||||||
|
* @throws IOException if the manifest cannot be read
|
||||||
|
*/
|
||||||
|
Manifest getManifest() throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns nested {@link Archive}s for entries that match the specified filters.
|
||||||
|
* @param searchFilter filter used to limit when additional sub-entry searching is
|
||||||
|
* required or {@code null} if all entries should be considered.
|
||||||
|
* @param includeFilter filter used to determine which entries should be included in
|
||||||
|
* the result or {@code null} if all entries should be included
|
||||||
|
* @return the nested archives
|
||||||
|
* @throws IOException on IO error
|
||||||
|
* @since 2.3.0
|
||||||
|
*/
|
||||||
|
Iterator<Archive> getNestedArchives(EntryFilter searchFilter, EntryFilter includeFilter) throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return if the archive is exploded (already unpacked).
|
||||||
|
* @return if the archive is exploded
|
||||||
|
* @since 2.3.0
|
||||||
|
*/
|
||||||
|
default boolean isExploded() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the {@code Archive}, releasing any open resources.
|
||||||
|
* @throws Exception if an error occurs during close processing
|
||||||
|
* @since 2.2.0
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
default void close() throws Exception {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a single entry in the archive.
|
||||||
|
*/
|
||||||
|
interface Entry {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@code true} if the entry represents a directory.
|
||||||
|
* @return if the entry is a directory
|
||||||
|
*/
|
||||||
|
boolean isDirectory();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the name of the entry.
|
||||||
|
* @return the name of the entry
|
||||||
|
*/
|
||||||
|
String getName();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strategy interface to filter {@link Entry Entries}.
|
||||||
|
*/
|
||||||
|
@FunctionalInterface
|
||||||
|
interface EntryFilter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the jar entry filter.
|
||||||
|
* @param entry the entry to filter
|
||||||
|
* @return {@code true} if the filter matches
|
||||||
|
*/
|
||||||
|
boolean matches(Entry entry);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,342 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.archive;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.Deque;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.jar.Manifest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link Archive} implementation backed by an exploded archive directory.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Madhura Bhave
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
public class ExplodedArchive implements Archive {
|
||||||
|
|
||||||
|
private static final Set<String> SKIPPED_NAMES = new HashSet<>(Arrays.asList(".", ".."));
|
||||||
|
|
||||||
|
private final File root;
|
||||||
|
|
||||||
|
private final boolean recursive;
|
||||||
|
|
||||||
|
private final File manifestFile;
|
||||||
|
|
||||||
|
private Manifest manifest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link ExplodedArchive} instance.
|
||||||
|
* @param root the root directory
|
||||||
|
*/
|
||||||
|
public ExplodedArchive(File root) {
|
||||||
|
this(root, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link ExplodedArchive} instance.
|
||||||
|
* @param root the root directory
|
||||||
|
* @param recursive if recursive searching should be used to locate the manifest.
|
||||||
|
* Defaults to {@code true}, directories with a large tree might want to set this to
|
||||||
|
* {@code false}.
|
||||||
|
*/
|
||||||
|
public ExplodedArchive(File root, boolean recursive) {
|
||||||
|
if (!root.exists() || !root.isDirectory()) {
|
||||||
|
throw new IllegalArgumentException("Invalid source directory " + root);
|
||||||
|
}
|
||||||
|
this.root = root;
|
||||||
|
this.recursive = recursive;
|
||||||
|
this.manifestFile = getManifestFile(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
private File getManifestFile(File root) {
|
||||||
|
File metaInf = new File(root, "META-INF");
|
||||||
|
return new File(metaInf, "MANIFEST.MF");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public URL getUrl() throws MalformedURLException {
|
||||||
|
return this.root.toURI().toURL();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Manifest getManifest() throws IOException {
|
||||||
|
if (this.manifest == null && this.manifestFile.exists()) {
|
||||||
|
try (FileInputStream inputStream = new FileInputStream(this.manifestFile)) {
|
||||||
|
this.manifest = new Manifest(inputStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.manifest;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Iterator<Archive> getNestedArchives(EntryFilter searchFilter, EntryFilter includeFilter) throws IOException {
|
||||||
|
return new ArchiveIterator(this.root, this.recursive, searchFilter, includeFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Deprecated(since = "2.3.10", forRemoval = false)
|
||||||
|
public Iterator<Entry> iterator() {
|
||||||
|
return new EntryIterator(this.root, this.recursive, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Archive getNestedArchive(Entry entry) {
|
||||||
|
File file = ((FileEntry) entry).getFile();
|
||||||
|
return (file.isDirectory() ? new ExplodedArchive(file) : new SimpleJarFileArchive((FileEntry) entry));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isExploded() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
try {
|
||||||
|
return getUrl().toString();
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
return "exploded archive";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* File based {@link Entry} {@link Iterator}.
|
||||||
|
*/
|
||||||
|
private abstract static class AbstractIterator<T> implements Iterator<T> {
|
||||||
|
|
||||||
|
private static final Comparator<File> entryComparator = Comparator.comparing(File::getAbsolutePath);
|
||||||
|
|
||||||
|
private final File root;
|
||||||
|
|
||||||
|
private final boolean recursive;
|
||||||
|
|
||||||
|
private final EntryFilter searchFilter;
|
||||||
|
|
||||||
|
private final EntryFilter includeFilter;
|
||||||
|
|
||||||
|
private final Deque<Iterator<File>> stack = new LinkedList<>();
|
||||||
|
|
||||||
|
private FileEntry current;
|
||||||
|
|
||||||
|
private final String rootUrl;
|
||||||
|
|
||||||
|
AbstractIterator(File root, boolean recursive, EntryFilter searchFilter, EntryFilter includeFilter) {
|
||||||
|
this.root = root;
|
||||||
|
this.rootUrl = this.root.toURI().getPath();
|
||||||
|
this.recursive = recursive;
|
||||||
|
this.searchFilter = searchFilter;
|
||||||
|
this.includeFilter = includeFilter;
|
||||||
|
this.stack.add(listFiles(root));
|
||||||
|
this.current = poll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasNext() {
|
||||||
|
return this.current != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public T next() {
|
||||||
|
FileEntry entry = this.current;
|
||||||
|
if (entry == null) {
|
||||||
|
throw new NoSuchElementException();
|
||||||
|
}
|
||||||
|
this.current = poll();
|
||||||
|
return adapt(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
private FileEntry poll() {
|
||||||
|
while (!this.stack.isEmpty()) {
|
||||||
|
while (this.stack.peek().hasNext()) {
|
||||||
|
File file = this.stack.peek().next();
|
||||||
|
if (SKIPPED_NAMES.contains(file.getName())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
FileEntry entry = getFileEntry(file);
|
||||||
|
if (isListable(entry)) {
|
||||||
|
this.stack.addFirst(listFiles(file));
|
||||||
|
}
|
||||||
|
if (this.includeFilter == null || this.includeFilter.matches(entry)) {
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.stack.poll();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private FileEntry getFileEntry(File file) {
|
||||||
|
URI uri = file.toURI();
|
||||||
|
String name = uri.getPath().substring(this.rootUrl.length());
|
||||||
|
try {
|
||||||
|
return new FileEntry(name, file, uri.toURL());
|
||||||
|
}
|
||||||
|
catch (MalformedURLException ex) {
|
||||||
|
throw new IllegalStateException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isListable(FileEntry entry) {
|
||||||
|
return entry.isDirectory() && (this.recursive || entry.getFile().getParentFile().equals(this.root))
|
||||||
|
&& (this.searchFilter == null || this.searchFilter.matches(entry))
|
||||||
|
&& (this.includeFilter == null || !this.includeFilter.matches(entry));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Iterator<File> listFiles(File file) {
|
||||||
|
File[] files = file.listFiles();
|
||||||
|
if (files == null) {
|
||||||
|
return Collections.emptyIterator();
|
||||||
|
}
|
||||||
|
Arrays.sort(files, entryComparator);
|
||||||
|
return Arrays.asList(files).iterator();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void remove() {
|
||||||
|
throw new UnsupportedOperationException("remove");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract T adapt(FileEntry entry);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class EntryIterator extends AbstractIterator<Entry> {
|
||||||
|
|
||||||
|
EntryIterator(File root, boolean recursive, EntryFilter searchFilter, EntryFilter includeFilter) {
|
||||||
|
super(root, recursive, searchFilter, includeFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Entry adapt(FileEntry entry) {
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ArchiveIterator extends AbstractIterator<Archive> {
|
||||||
|
|
||||||
|
ArchiveIterator(File root, boolean recursive, EntryFilter searchFilter, EntryFilter includeFilter) {
|
||||||
|
super(root, recursive, searchFilter, includeFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Archive adapt(FileEntry entry) {
|
||||||
|
File file = entry.getFile();
|
||||||
|
return (file.isDirectory() ? new ExplodedArchive(file) : new SimpleJarFileArchive(entry));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link Entry} backed by a File.
|
||||||
|
*/
|
||||||
|
private static class FileEntry implements Entry {
|
||||||
|
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
private final File file;
|
||||||
|
|
||||||
|
private final URL url;
|
||||||
|
|
||||||
|
FileEntry(String name, File file, URL url) {
|
||||||
|
this.name = name;
|
||||||
|
this.file = file;
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
File getFile() {
|
||||||
|
return this.file;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isDirectory() {
|
||||||
|
return this.file.isDirectory();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
URL getUrl() {
|
||||||
|
return this.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link Archive} implementation backed by a simple JAR file that doesn't itself
|
||||||
|
* contain nested archives.
|
||||||
|
*/
|
||||||
|
private static class SimpleJarFileArchive implements Archive {
|
||||||
|
|
||||||
|
private final URL url;
|
||||||
|
|
||||||
|
SimpleJarFileArchive(FileEntry file) {
|
||||||
|
this.url = file.getUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public URL getUrl() throws MalformedURLException {
|
||||||
|
return this.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Manifest getManifest() throws IOException {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Iterator<Archive> getNestedArchives(EntryFilter searchFilter, EntryFilter includeFilter)
|
||||||
|
throws IOException {
|
||||||
|
return Collections.emptyIterator();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Deprecated(since = "2.3.10", forRemoval = false)
|
||||||
|
public Iterator<Entry> iterator() {
|
||||||
|
return Collections.emptyIterator();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
try {
|
||||||
|
return getUrl().toString();
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
return "jar archive";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,310 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.archive;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.file.FileSystem;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.nio.file.StandardOpenOption;
|
||||||
|
import java.nio.file.attribute.FileAttribute;
|
||||||
|
import java.nio.file.attribute.PosixFilePermission;
|
||||||
|
import java.nio.file.attribute.PosixFilePermissions;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.jar.JarEntry;
|
||||||
|
import java.util.jar.Manifest;
|
||||||
|
|
||||||
|
import org.springframework.boot.loader.jar.JarFile;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link Archive} implementation backed by a {@link JarFile}.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
public class JarFileArchive implements Archive {
|
||||||
|
|
||||||
|
private static final String UNPACK_MARKER = "UNPACK:";
|
||||||
|
|
||||||
|
private static final int BUFFER_SIZE = 32 * 1024;
|
||||||
|
|
||||||
|
private static final FileAttribute<?>[] NO_FILE_ATTRIBUTES = {};
|
||||||
|
|
||||||
|
private static final EnumSet<PosixFilePermission> DIRECTORY_PERMISSIONS = EnumSet.of(PosixFilePermission.OWNER_READ,
|
||||||
|
PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE);
|
||||||
|
|
||||||
|
private static final EnumSet<PosixFilePermission> FILE_PERMISSIONS = EnumSet.of(PosixFilePermission.OWNER_READ,
|
||||||
|
PosixFilePermission.OWNER_WRITE);
|
||||||
|
|
||||||
|
private final JarFile jarFile;
|
||||||
|
|
||||||
|
private URL url;
|
||||||
|
|
||||||
|
private Path tempUnpackDirectory;
|
||||||
|
|
||||||
|
public JarFileArchive(File file) throws IOException {
|
||||||
|
this(file, file.toURI().toURL());
|
||||||
|
}
|
||||||
|
|
||||||
|
public JarFileArchive(File file, URL url) throws IOException {
|
||||||
|
this(new JarFile(file));
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JarFileArchive(JarFile jarFile) {
|
||||||
|
this.jarFile = jarFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public URL getUrl() throws MalformedURLException {
|
||||||
|
if (this.url != null) {
|
||||||
|
return this.url;
|
||||||
|
}
|
||||||
|
return this.jarFile.getUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Manifest getManifest() throws IOException {
|
||||||
|
return this.jarFile.getManifest();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Iterator<Archive> getNestedArchives(EntryFilter searchFilter, EntryFilter includeFilter) throws IOException {
|
||||||
|
return new NestedArchiveIterator(this.jarFile.iterator(), searchFilter, includeFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Deprecated(since = "2.3.10", forRemoval = false)
|
||||||
|
public Iterator<Entry> iterator() {
|
||||||
|
return new EntryIterator(this.jarFile.iterator(), null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
this.jarFile.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Archive getNestedArchive(Entry entry) throws IOException {
|
||||||
|
JarEntry jarEntry = ((JarFileEntry) entry).getJarEntry();
|
||||||
|
if (jarEntry.getComment().startsWith(UNPACK_MARKER)) {
|
||||||
|
return getUnpackedNestedArchive(jarEntry);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
JarFile jarFile = this.jarFile.getNestedJarFile(jarEntry);
|
||||||
|
return new JarFileArchive(jarFile);
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
throw new IllegalStateException("Failed to get nested archive for entry " + entry.getName(), ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Archive getUnpackedNestedArchive(JarEntry jarEntry) throws IOException {
|
||||||
|
String name = jarEntry.getName();
|
||||||
|
if (name.lastIndexOf('/') != -1) {
|
||||||
|
name = name.substring(name.lastIndexOf('/') + 1);
|
||||||
|
}
|
||||||
|
Path path = getTempUnpackDirectory().resolve(name);
|
||||||
|
if (!Files.exists(path) || Files.size(path) != jarEntry.getSize()) {
|
||||||
|
unpack(jarEntry, path);
|
||||||
|
}
|
||||||
|
return new JarFileArchive(path.toFile(), path.toUri().toURL());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path getTempUnpackDirectory() {
|
||||||
|
if (this.tempUnpackDirectory == null) {
|
||||||
|
Path tempDirectory = Paths.get(System.getProperty("java.io.tmpdir"));
|
||||||
|
this.tempUnpackDirectory = createUnpackDirectory(tempDirectory);
|
||||||
|
}
|
||||||
|
return this.tempUnpackDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path createUnpackDirectory(Path parent) {
|
||||||
|
int attempts = 0;
|
||||||
|
while (attempts++ < 1000) {
|
||||||
|
String fileName = Paths.get(this.jarFile.getName()).getFileName().toString();
|
||||||
|
Path unpackDirectory = parent.resolve(fileName + "-spring-boot-libs-" + UUID.randomUUID());
|
||||||
|
try {
|
||||||
|
createDirectory(unpackDirectory);
|
||||||
|
return unpackDirectory;
|
||||||
|
}
|
||||||
|
catch (IOException ex) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Failed to create unpack directory in directory '" + parent + "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void unpack(JarEntry entry, Path path) throws IOException {
|
||||||
|
createFile(path);
|
||||||
|
path.toFile().deleteOnExit();
|
||||||
|
try (InputStream inputStream = this.jarFile.getInputStream(entry);
|
||||||
|
OutputStream outputStream = Files.newOutputStream(path, StandardOpenOption.WRITE,
|
||||||
|
StandardOpenOption.TRUNCATE_EXISTING)) {
|
||||||
|
byte[] buffer = new byte[BUFFER_SIZE];
|
||||||
|
int bytesRead;
|
||||||
|
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||||
|
outputStream.write(buffer, 0, bytesRead);
|
||||||
|
}
|
||||||
|
outputStream.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createDirectory(Path path) throws IOException {
|
||||||
|
Files.createDirectory(path, getFileAttributes(path.getFileSystem(), DIRECTORY_PERMISSIONS));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createFile(Path path) throws IOException {
|
||||||
|
Files.createFile(path, getFileAttributes(path.getFileSystem(), FILE_PERMISSIONS));
|
||||||
|
}
|
||||||
|
|
||||||
|
private FileAttribute<?>[] getFileAttributes(FileSystem fileSystem, EnumSet<PosixFilePermission> ownerReadWrite) {
|
||||||
|
if (!fileSystem.supportedFileAttributeViews().contains("posix")) {
|
||||||
|
return NO_FILE_ATTRIBUTES;
|
||||||
|
}
|
||||||
|
return new FileAttribute<?>[] { PosixFilePermissions.asFileAttribute(ownerReadWrite) };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
try {
|
||||||
|
return getUrl().toString();
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
return "jar archive";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract base class for iterator implementations.
|
||||||
|
*/
|
||||||
|
private abstract static class AbstractIterator<T> implements Iterator<T> {
|
||||||
|
|
||||||
|
private final Iterator<JarEntry> iterator;
|
||||||
|
|
||||||
|
private final EntryFilter searchFilter;
|
||||||
|
|
||||||
|
private final EntryFilter includeFilter;
|
||||||
|
|
||||||
|
private Entry current;
|
||||||
|
|
||||||
|
AbstractIterator(Iterator<JarEntry> iterator, EntryFilter searchFilter, EntryFilter includeFilter) {
|
||||||
|
this.iterator = iterator;
|
||||||
|
this.searchFilter = searchFilter;
|
||||||
|
this.includeFilter = includeFilter;
|
||||||
|
this.current = poll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasNext() {
|
||||||
|
return this.current != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public T next() {
|
||||||
|
T result = adapt(this.current);
|
||||||
|
this.current = poll();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Entry poll() {
|
||||||
|
while (this.iterator.hasNext()) {
|
||||||
|
JarFileEntry candidate = new JarFileEntry(this.iterator.next());
|
||||||
|
if ((this.searchFilter == null || this.searchFilter.matches(candidate))
|
||||||
|
&& (this.includeFilter == null || this.includeFilter.matches(candidate))) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract T adapt(Entry entry);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link Archive.Entry} iterator implementation backed by {@link JarEntry}.
|
||||||
|
*/
|
||||||
|
private static class EntryIterator extends AbstractIterator<Entry> {
|
||||||
|
|
||||||
|
EntryIterator(Iterator<JarEntry> iterator, EntryFilter searchFilter, EntryFilter includeFilter) {
|
||||||
|
super(iterator, searchFilter, includeFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Entry adapt(Entry entry) {
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nested {@link Archive} iterator implementation backed by {@link JarEntry}.
|
||||||
|
*/
|
||||||
|
private class NestedArchiveIterator extends AbstractIterator<Archive> {
|
||||||
|
|
||||||
|
NestedArchiveIterator(Iterator<JarEntry> iterator, EntryFilter searchFilter, EntryFilter includeFilter) {
|
||||||
|
super(iterator, searchFilter, includeFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Archive adapt(Entry entry) {
|
||||||
|
try {
|
||||||
|
return getNestedArchive(entry);
|
||||||
|
}
|
||||||
|
catch (IOException ex) {
|
||||||
|
throw new IllegalStateException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link Archive.Entry} implementation backed by a {@link JarEntry}.
|
||||||
|
*/
|
||||||
|
private static class JarFileEntry implements Entry {
|
||||||
|
|
||||||
|
private final JarEntry jarEntry;
|
||||||
|
|
||||||
|
JarFileEntry(JarEntry jarEntry) {
|
||||||
|
this.jarEntry = jarEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
JarEntry getJarEntry() {
|
||||||
|
return this.jarEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isDirectory() {
|
||||||
|
return this.jarEntry.isDirectory();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return this.jarEntry.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstraction over logical Archives be they backed by a JAR file or unpacked into a
|
||||||
|
* directory.
|
||||||
|
*
|
||||||
|
* @see org.springframework.boot.loader.archive.Archive
|
||||||
|
*/
|
||||||
|
package org.springframework.boot.loader.archive;
|
@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.data;
|
||||||
|
|
||||||
|
import java.io.EOFException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface that provides read-only random access to some underlying data.
|
||||||
|
* Implementations must allow concurrent reads in a thread-safe manner.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
public interface RandomAccessData {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an {@link InputStream} that can be used to read the underlying data. The
|
||||||
|
* caller is responsible close the underlying stream.
|
||||||
|
* @return a new input stream that can be used to read the underlying data.
|
||||||
|
* @throws IOException if the stream cannot be opened
|
||||||
|
*/
|
||||||
|
InputStream getInputStream() throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new {@link RandomAccessData} for a specific subsection of this data.
|
||||||
|
* @param offset the offset of the subsection
|
||||||
|
* @param length the length of the subsection
|
||||||
|
* @return the subsection data
|
||||||
|
*/
|
||||||
|
RandomAccessData getSubsection(long offset, long length);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads all the data and returns it as a byte array.
|
||||||
|
* @return the data
|
||||||
|
* @throws IOException if the data cannot be read
|
||||||
|
*/
|
||||||
|
byte[] read() throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the {@code length} bytes of data starting at the given {@code offset}.
|
||||||
|
* @param offset the offset from which data should be read
|
||||||
|
* @param length the number of bytes to be read
|
||||||
|
* @return the data
|
||||||
|
* @throws IOException if the data cannot be read
|
||||||
|
* @throws IndexOutOfBoundsException if offset is beyond the end of the file or
|
||||||
|
* subsection
|
||||||
|
* @throws EOFException if offset plus length is greater than the length of the file
|
||||||
|
* or subsection
|
||||||
|
*/
|
||||||
|
byte[] read(long offset, long length) throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the size of the data.
|
||||||
|
* @return the size
|
||||||
|
*/
|
||||||
|
long getSize();
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,262 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.data;
|
||||||
|
|
||||||
|
import java.io.EOFException;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.RandomAccessFile;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link RandomAccessData} implementation backed by a {@link RandomAccessFile}.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
public class RandomAccessDataFile implements RandomAccessData {
|
||||||
|
|
||||||
|
private final FileAccess fileAccess;
|
||||||
|
|
||||||
|
private final long offset;
|
||||||
|
|
||||||
|
private final long length;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link RandomAccessDataFile} backed by the specified file.
|
||||||
|
* @param file the underlying file
|
||||||
|
* @throws IllegalArgumentException if the file is null or does not exist
|
||||||
|
*/
|
||||||
|
public RandomAccessDataFile(File file) {
|
||||||
|
if (file == null) {
|
||||||
|
throw new IllegalArgumentException("File must not be null");
|
||||||
|
}
|
||||||
|
this.fileAccess = new FileAccess(file);
|
||||||
|
this.offset = 0L;
|
||||||
|
this.length = file.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private constructor used to create a {@link #getSubsection(long, long) subsection}.
|
||||||
|
* @param fileAccess provides access to the underlying file
|
||||||
|
* @param offset the offset of the section
|
||||||
|
* @param length the length of the section
|
||||||
|
*/
|
||||||
|
private RandomAccessDataFile(FileAccess fileAccess, long offset, long length) {
|
||||||
|
this.fileAccess = fileAccess;
|
||||||
|
this.offset = offset;
|
||||||
|
this.length = length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the underlying File.
|
||||||
|
* @return the underlying file
|
||||||
|
*/
|
||||||
|
public File getFile() {
|
||||||
|
return this.fileAccess.file;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream getInputStream() throws IOException {
|
||||||
|
return new DataInputStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RandomAccessData getSubsection(long offset, long length) {
|
||||||
|
if (offset < 0 || length < 0 || offset + length > this.length) {
|
||||||
|
throw new IndexOutOfBoundsException();
|
||||||
|
}
|
||||||
|
return new RandomAccessDataFile(this.fileAccess, this.offset + offset, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] read() throws IOException {
|
||||||
|
return read(0, this.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] read(long offset, long length) throws IOException {
|
||||||
|
if (offset > this.length) {
|
||||||
|
throw new IndexOutOfBoundsException();
|
||||||
|
}
|
||||||
|
if (offset + length > this.length) {
|
||||||
|
throw new EOFException();
|
||||||
|
}
|
||||||
|
byte[] bytes = new byte[(int) length];
|
||||||
|
read(bytes, offset, 0, bytes.length);
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int readByte(long position) throws IOException {
|
||||||
|
if (position >= this.length) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return this.fileAccess.readByte(this.offset + position);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int read(byte[] bytes, long position, int offset, int length) throws IOException {
|
||||||
|
if (position > this.length) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return this.fileAccess.read(bytes, this.offset + position, offset, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getSize() {
|
||||||
|
return this.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() throws IOException {
|
||||||
|
this.fileAccess.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link InputStream} implementation for the {@link RandomAccessDataFile}.
|
||||||
|
*/
|
||||||
|
private class DataInputStream extends InputStream {
|
||||||
|
|
||||||
|
private int position;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read() throws IOException {
|
||||||
|
int read = RandomAccessDataFile.this.readByte(this.position);
|
||||||
|
if (read > -1) {
|
||||||
|
moveOn(1);
|
||||||
|
}
|
||||||
|
return read;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(byte[] b) throws IOException {
|
||||||
|
return read(b, 0, (b != null) ? b.length : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(byte[] b, int off, int len) throws IOException {
|
||||||
|
if (b == null) {
|
||||||
|
throw new NullPointerException("Bytes must not be null");
|
||||||
|
}
|
||||||
|
return doRead(b, off, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform the actual read.
|
||||||
|
* @param b the bytes to read or {@code null} when reading a single byte
|
||||||
|
* @param off the offset of the byte array
|
||||||
|
* @param len the length of data to read
|
||||||
|
* @return the number of bytes read into {@code b} or the actual read byte if
|
||||||
|
* {@code b} is {@code null}. Returns -1 when the end of the stream is reached
|
||||||
|
* @throws IOException in case of I/O errors
|
||||||
|
*/
|
||||||
|
int doRead(byte[] b, int off, int len) throws IOException {
|
||||||
|
if (len == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int cappedLen = cap(len);
|
||||||
|
if (cappedLen <= 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return (int) moveOn(RandomAccessDataFile.this.read(b, this.position, off, cappedLen));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long skip(long n) throws IOException {
|
||||||
|
return (n <= 0) ? 0 : moveOn(cap(n));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int available() throws IOException {
|
||||||
|
return (int) RandomAccessDataFile.this.length - this.position;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cap the specified value such that it cannot exceed the number of bytes
|
||||||
|
* remaining.
|
||||||
|
* @param n the value to cap
|
||||||
|
* @return the capped value
|
||||||
|
*/
|
||||||
|
private int cap(long n) {
|
||||||
|
return (int) Math.min(RandomAccessDataFile.this.length - this.position, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the stream position forwards the specified amount.
|
||||||
|
* @param amount the amount to move
|
||||||
|
* @return the amount moved
|
||||||
|
*/
|
||||||
|
private long moveOn(int amount) {
|
||||||
|
this.position += amount;
|
||||||
|
return amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class FileAccess {
|
||||||
|
|
||||||
|
private final Object monitor = new Object();
|
||||||
|
|
||||||
|
private final File file;
|
||||||
|
|
||||||
|
private RandomAccessFile randomAccessFile;
|
||||||
|
|
||||||
|
private FileAccess(File file) {
|
||||||
|
this.file = file;
|
||||||
|
openIfNecessary();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int read(byte[] bytes, long position, int offset, int length) throws IOException {
|
||||||
|
synchronized (this.monitor) {
|
||||||
|
openIfNecessary();
|
||||||
|
this.randomAccessFile.seek(position);
|
||||||
|
return this.randomAccessFile.read(bytes, offset, length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openIfNecessary() {
|
||||||
|
if (this.randomAccessFile == null) {
|
||||||
|
try {
|
||||||
|
this.randomAccessFile = new RandomAccessFile(this.file, "r");
|
||||||
|
}
|
||||||
|
catch (FileNotFoundException ex) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
String.format("File %s must exist", this.file.getAbsolutePath()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void close() throws IOException {
|
||||||
|
synchronized (this.monitor) {
|
||||||
|
if (this.randomAccessFile != null) {
|
||||||
|
this.randomAccessFile.close();
|
||||||
|
this.randomAccessFile = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int readByte(long position) throws IOException {
|
||||||
|
synchronized (this.monitor) {
|
||||||
|
openIfNecessary();
|
||||||
|
this.randomAccessFile.seek(position);
|
||||||
|
return this.randomAccessFile.read();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Classes and interfaces to allow random access to a block of data.
|
||||||
|
*
|
||||||
|
* @see org.springframework.boot.loader.data.RandomAccessData
|
||||||
|
*/
|
||||||
|
package org.springframework.boot.loader.data;
|
@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.jar;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.security.Permission;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for extended variants of {@link java.util.jar.JarFile}.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
abstract class AbstractJarFile extends java.util.jar.JarFile {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link AbstractJarFile}.
|
||||||
|
* @param file the root jar file.
|
||||||
|
* @throws IOException on IO error
|
||||||
|
*/
|
||||||
|
AbstractJarFile(File file) throws IOException {
|
||||||
|
super(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a URL that can be used to access this JAR file. NOTE: the specified URL
|
||||||
|
* cannot be serialized and or cloned.
|
||||||
|
* @return the URL
|
||||||
|
* @throws MalformedURLException if the URL is malformed
|
||||||
|
*/
|
||||||
|
abstract URL getUrl() throws MalformedURLException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the {@link JarFileType} of this instance.
|
||||||
|
* @return the jar file type
|
||||||
|
*/
|
||||||
|
abstract JarFileType getType();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the security permission for this JAR.
|
||||||
|
* @return the security permission.
|
||||||
|
*/
|
||||||
|
abstract Permission getPermission();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an {@link InputStream} for the entire jar contents.
|
||||||
|
* @return the contents input stream
|
||||||
|
* @throws IOException on IO error
|
||||||
|
*/
|
||||||
|
abstract InputStream getInputStream() throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of a {@link JarFile}.
|
||||||
|
*/
|
||||||
|
enum JarFileType {
|
||||||
|
|
||||||
|
DIRECT, NESTED_DIRECTORY, NESTED_JAR
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,255 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.jar;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple wrapper around a byte array that represents an ASCII. Used for performance
|
||||||
|
* reasons to save constructing Strings for ZIP data.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
*/
|
||||||
|
final class AsciiBytes {
|
||||||
|
|
||||||
|
private static final String EMPTY_STRING = "";
|
||||||
|
|
||||||
|
private static final int[] INITIAL_BYTE_BITMASK = { 0x7F, 0x1F, 0x0F, 0x07 };
|
||||||
|
|
||||||
|
private static final int SUBSEQUENT_BYTE_BITMASK = 0x3F;
|
||||||
|
|
||||||
|
private final byte[] bytes;
|
||||||
|
|
||||||
|
private final int offset;
|
||||||
|
|
||||||
|
private final int length;
|
||||||
|
|
||||||
|
private String string;
|
||||||
|
|
||||||
|
private int hash;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link AsciiBytes} from the specified String.
|
||||||
|
* @param string the source string
|
||||||
|
*/
|
||||||
|
AsciiBytes(String string) {
|
||||||
|
this(string.getBytes(StandardCharsets.UTF_8));
|
||||||
|
this.string = string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link AsciiBytes} from the specified bytes. NOTE: underlying bytes
|
||||||
|
* are not expected to change.
|
||||||
|
* @param bytes the source bytes
|
||||||
|
*/
|
||||||
|
AsciiBytes(byte[] bytes) {
|
||||||
|
this(bytes, 0, bytes.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link AsciiBytes} from the specified bytes. NOTE: underlying bytes
|
||||||
|
* are not expected to change.
|
||||||
|
* @param bytes the source bytes
|
||||||
|
* @param offset the offset
|
||||||
|
* @param length the length
|
||||||
|
*/
|
||||||
|
AsciiBytes(byte[] bytes, int offset, int length) {
|
||||||
|
if (offset < 0 || length < 0 || (offset + length) > bytes.length) {
|
||||||
|
throw new IndexOutOfBoundsException();
|
||||||
|
}
|
||||||
|
this.bytes = bytes;
|
||||||
|
this.offset = offset;
|
||||||
|
this.length = length;
|
||||||
|
}
|
||||||
|
|
||||||
|
int length() {
|
||||||
|
return this.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean startsWith(AsciiBytes prefix) {
|
||||||
|
if (this == prefix) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (prefix.length > this.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < prefix.length; i++) {
|
||||||
|
if (this.bytes[i + this.offset] != prefix.bytes[i + prefix.offset]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean endsWith(AsciiBytes postfix) {
|
||||||
|
if (this == postfix) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (postfix.length > this.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < postfix.length; i++) {
|
||||||
|
if (this.bytes[this.offset + (this.length - 1) - i] != postfix.bytes[postfix.offset + (postfix.length - 1)
|
||||||
|
- i]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
AsciiBytes substring(int beginIndex) {
|
||||||
|
return substring(beginIndex, this.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
AsciiBytes substring(int beginIndex, int endIndex) {
|
||||||
|
int length = endIndex - beginIndex;
|
||||||
|
if (this.offset + length > this.bytes.length) {
|
||||||
|
throw new IndexOutOfBoundsException();
|
||||||
|
}
|
||||||
|
return new AsciiBytes(this.bytes, this.offset + beginIndex, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean matches(CharSequence name, char suffix) {
|
||||||
|
int charIndex = 0;
|
||||||
|
int nameLen = name.length();
|
||||||
|
int totalLen = nameLen + ((suffix != 0) ? 1 : 0);
|
||||||
|
for (int i = this.offset; i < this.offset + this.length; i++) {
|
||||||
|
int b = this.bytes[i];
|
||||||
|
int remainingUtfBytes = getNumberOfUtfBytes(b) - 1;
|
||||||
|
b &= INITIAL_BYTE_BITMASK[remainingUtfBytes];
|
||||||
|
for (int j = 0; j < remainingUtfBytes; j++) {
|
||||||
|
b = (b << 6) + (this.bytes[++i] & SUBSEQUENT_BYTE_BITMASK);
|
||||||
|
}
|
||||||
|
char c = getChar(name, suffix, charIndex++);
|
||||||
|
if (b <= 0xFFFF) {
|
||||||
|
if (c != b) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (c != ((b >> 0xA) + 0xD7C0)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
c = getChar(name, suffix, charIndex++);
|
||||||
|
if (c != ((b & 0x3FF) + 0xDC00)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return charIndex == totalLen;
|
||||||
|
}
|
||||||
|
|
||||||
|
private char getChar(CharSequence name, char suffix, int index) {
|
||||||
|
if (index < name.length()) {
|
||||||
|
return name.charAt(index);
|
||||||
|
}
|
||||||
|
if (index == name.length()) {
|
||||||
|
return suffix;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getNumberOfUtfBytes(int b) {
|
||||||
|
if ((b & 0x80) == 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int numberOfUtfBytes = 0;
|
||||||
|
while ((b & 0x80) != 0) {
|
||||||
|
b <<= 1;
|
||||||
|
numberOfUtfBytes++;
|
||||||
|
}
|
||||||
|
return numberOfUtfBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (obj == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this == obj) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (obj.getClass() == AsciiBytes.class) {
|
||||||
|
AsciiBytes other = (AsciiBytes) obj;
|
||||||
|
if (this.length == other.length) {
|
||||||
|
for (int i = 0; i < this.length; i++) {
|
||||||
|
if (this.bytes[this.offset + i] != other.bytes[other.offset + i]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
int hash = this.hash;
|
||||||
|
if (hash == 0 && this.bytes.length > 0) {
|
||||||
|
for (int i = this.offset; i < this.offset + this.length; i++) {
|
||||||
|
int b = this.bytes[i];
|
||||||
|
int remainingUtfBytes = getNumberOfUtfBytes(b) - 1;
|
||||||
|
b &= INITIAL_BYTE_BITMASK[remainingUtfBytes];
|
||||||
|
for (int j = 0; j < remainingUtfBytes; j++) {
|
||||||
|
b = (b << 6) + (this.bytes[++i] & SUBSEQUENT_BYTE_BITMASK);
|
||||||
|
}
|
||||||
|
if (b <= 0xFFFF) {
|
||||||
|
hash = 31 * hash + b;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
hash = 31 * hash + ((b >> 0xA) + 0xD7C0);
|
||||||
|
hash = 31 * hash + ((b & 0x3FF) + 0xDC00);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.hash = hash;
|
||||||
|
}
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
if (this.string == null) {
|
||||||
|
if (this.length == 0) {
|
||||||
|
this.string = EMPTY_STRING;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.string = new String(this.bytes, this.offset, this.length, StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.string;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String toString(byte[] bytes) {
|
||||||
|
return new String(bytes, StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int hashCode(CharSequence charSequence) {
|
||||||
|
// We're compatible with String's hashCode()
|
||||||
|
if (charSequence instanceof StringSequence) {
|
||||||
|
// ... but save making an unnecessary String for StringSequence
|
||||||
|
return charSequence.hashCode();
|
||||||
|
}
|
||||||
|
return charSequence.toString().hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
static int hashCode(int hash, char suffix) {
|
||||||
|
return (suffix != 0) ? (31 * hash + suffix) : hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.jar;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utilities for dealing with bytes from ZIP files.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
final class Bytes {
|
||||||
|
|
||||||
|
private Bytes() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static long littleEndianValue(byte[] bytes, int offset, int length) {
|
||||||
|
long value = 0;
|
||||||
|
for (int i = length - 1; i >= 0; i--) {
|
||||||
|
value = ((value << 8) | (bytes[offset + i] & 0xFF));
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,258 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.jar;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.springframework.boot.loader.data.RandomAccessData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A ZIP File "End of central directory record" (EOCD).
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Camille Vienot
|
||||||
|
* @see <a href="https://en.wikipedia.org/wiki/Zip_%28file_format%29">Zip File Format</a>
|
||||||
|
*/
|
||||||
|
class CentralDirectoryEndRecord {
|
||||||
|
|
||||||
|
private static final int MINIMUM_SIZE = 22;
|
||||||
|
|
||||||
|
private static final int MAXIMUM_COMMENT_LENGTH = 0xFFFF;
|
||||||
|
|
||||||
|
private static final int MAXIMUM_SIZE = MINIMUM_SIZE + MAXIMUM_COMMENT_LENGTH;
|
||||||
|
|
||||||
|
private static final int SIGNATURE = 0x06054b50;
|
||||||
|
|
||||||
|
private static final int COMMENT_LENGTH_OFFSET = 20;
|
||||||
|
|
||||||
|
private static final int READ_BLOCK_SIZE = 256;
|
||||||
|
|
||||||
|
private final Zip64End zip64End;
|
||||||
|
|
||||||
|
private byte[] block;
|
||||||
|
|
||||||
|
private int offset;
|
||||||
|
|
||||||
|
private int size;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link CentralDirectoryEndRecord} instance from the specified
|
||||||
|
* {@link RandomAccessData}, searching backwards from the end until a valid block is
|
||||||
|
* located.
|
||||||
|
* @param data the source data
|
||||||
|
* @throws IOException in case of I/O errors
|
||||||
|
*/
|
||||||
|
CentralDirectoryEndRecord(RandomAccessData data) throws IOException {
|
||||||
|
this.block = createBlockFromEndOfData(data, READ_BLOCK_SIZE);
|
||||||
|
this.size = MINIMUM_SIZE;
|
||||||
|
this.offset = this.block.length - this.size;
|
||||||
|
while (!isValid()) {
|
||||||
|
this.size++;
|
||||||
|
if (this.size > this.block.length) {
|
||||||
|
if (this.size >= MAXIMUM_SIZE || this.size > data.getSize()) {
|
||||||
|
throw new IOException(
|
||||||
|
"Unable to find ZIP central directory records after reading " + this.size + " bytes");
|
||||||
|
}
|
||||||
|
this.block = createBlockFromEndOfData(data, this.size + READ_BLOCK_SIZE);
|
||||||
|
}
|
||||||
|
this.offset = this.block.length - this.size;
|
||||||
|
}
|
||||||
|
long startOfCentralDirectoryEndRecord = data.getSize() - this.size;
|
||||||
|
Zip64Locator zip64Locator = Zip64Locator.find(data, startOfCentralDirectoryEndRecord);
|
||||||
|
this.zip64End = (zip64Locator != null) ? new Zip64End(data, zip64Locator) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] createBlockFromEndOfData(RandomAccessData data, int size) throws IOException {
|
||||||
|
int length = (int) Math.min(data.getSize(), size);
|
||||||
|
return data.read(data.getSize() - length, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isValid() {
|
||||||
|
if (this.block.length < MINIMUM_SIZE || Bytes.littleEndianValue(this.block, this.offset + 0, 4) != SIGNATURE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Total size must be the structure size + comment
|
||||||
|
long commentLength = Bytes.littleEndianValue(this.block, this.offset + COMMENT_LENGTH_OFFSET, 2);
|
||||||
|
return this.size == MINIMUM_SIZE + commentLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the location in the data that the archive actually starts. For most files
|
||||||
|
* the archive data will start at 0, however, it is possible to have prefixed bytes
|
||||||
|
* (often used for startup scripts) at the beginning of the data.
|
||||||
|
* @param data the source data
|
||||||
|
* @return the offset within the data where the archive begins
|
||||||
|
*/
|
||||||
|
long getStartOfArchive(RandomAccessData data) {
|
||||||
|
long length = Bytes.littleEndianValue(this.block, this.offset + 12, 4);
|
||||||
|
long specifiedOffset = (this.zip64End != null) ? this.zip64End.centralDirectoryOffset
|
||||||
|
: Bytes.littleEndianValue(this.block, this.offset + 16, 4);
|
||||||
|
long zip64EndSize = (this.zip64End != null) ? this.zip64End.getSize() : 0L;
|
||||||
|
int zip64LocSize = (this.zip64End != null) ? Zip64Locator.ZIP64_LOCSIZE : 0;
|
||||||
|
long actualOffset = data.getSize() - this.size - length - zip64EndSize - zip64LocSize;
|
||||||
|
return actualOffset - specifiedOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the bytes of the "Central directory" based on the offset indicated in this
|
||||||
|
* record.
|
||||||
|
* @param data the source data
|
||||||
|
* @return the central directory data
|
||||||
|
*/
|
||||||
|
RandomAccessData getCentralDirectory(RandomAccessData data) {
|
||||||
|
if (this.zip64End != null) {
|
||||||
|
return this.zip64End.getCentralDirectory(data);
|
||||||
|
}
|
||||||
|
long offset = Bytes.littleEndianValue(this.block, this.offset + 16, 4);
|
||||||
|
long length = Bytes.littleEndianValue(this.block, this.offset + 12, 4);
|
||||||
|
return data.getSubsection(offset, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the number of ZIP entries in the file.
|
||||||
|
* @return the number of records in the zip
|
||||||
|
*/
|
||||||
|
int getNumberOfRecords() {
|
||||||
|
if (this.zip64End != null) {
|
||||||
|
return this.zip64End.getNumberOfRecords();
|
||||||
|
}
|
||||||
|
long numberOfRecords = Bytes.littleEndianValue(this.block, this.offset + 10, 2);
|
||||||
|
return (int) numberOfRecords;
|
||||||
|
}
|
||||||
|
|
||||||
|
String getComment() {
|
||||||
|
int commentLength = (int) Bytes.littleEndianValue(this.block, this.offset + COMMENT_LENGTH_OFFSET, 2);
|
||||||
|
AsciiBytes comment = new AsciiBytes(this.block, this.offset + COMMENT_LENGTH_OFFSET + 2, commentLength);
|
||||||
|
return comment.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isZip64() {
|
||||||
|
return this.zip64End != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Zip64 end of central directory record.
|
||||||
|
*
|
||||||
|
* @see <a href="https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT">Chapter
|
||||||
|
* 4.3.14 of Zip64 specification</a>
|
||||||
|
*/
|
||||||
|
private static final class Zip64End {
|
||||||
|
|
||||||
|
private static final int ZIP64_ENDTOT = 32; // total number of entries
|
||||||
|
|
||||||
|
private static final int ZIP64_ENDSIZ = 40; // central directory size in bytes
|
||||||
|
|
||||||
|
private static final int ZIP64_ENDOFF = 48; // offset of first CEN header
|
||||||
|
|
||||||
|
private final Zip64Locator locator;
|
||||||
|
|
||||||
|
private final long centralDirectoryOffset;
|
||||||
|
|
||||||
|
private final long centralDirectoryLength;
|
||||||
|
|
||||||
|
private final int numberOfRecords;
|
||||||
|
|
||||||
|
private Zip64End(RandomAccessData data, Zip64Locator locator) throws IOException {
|
||||||
|
this.locator = locator;
|
||||||
|
byte[] block = data.read(locator.getZip64EndOffset(), 56);
|
||||||
|
this.centralDirectoryOffset = Bytes.littleEndianValue(block, ZIP64_ENDOFF, 8);
|
||||||
|
this.centralDirectoryLength = Bytes.littleEndianValue(block, ZIP64_ENDSIZ, 8);
|
||||||
|
this.numberOfRecords = (int) Bytes.littleEndianValue(block, ZIP64_ENDTOT, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the size of this zip 64 end of central directory record.
|
||||||
|
* @return size of this zip 64 end of central directory record
|
||||||
|
*/
|
||||||
|
private long getSize() {
|
||||||
|
return this.locator.getZip64EndSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the bytes of the "Central directory" based on the offset indicated in
|
||||||
|
* this record.
|
||||||
|
* @param data the source data
|
||||||
|
* @return the central directory data
|
||||||
|
*/
|
||||||
|
private RandomAccessData getCentralDirectory(RandomAccessData data) {
|
||||||
|
return data.getSubsection(this.centralDirectoryOffset, this.centralDirectoryLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the number of entries in the zip64 archive.
|
||||||
|
* @return the number of records in the zip
|
||||||
|
*/
|
||||||
|
private int getNumberOfRecords() {
|
||||||
|
return this.numberOfRecords;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Zip64 end of central directory locator.
|
||||||
|
*
|
||||||
|
* @see <a href="https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT">Chapter
|
||||||
|
* 4.3.15 of Zip64 specification</a>
|
||||||
|
*/
|
||||||
|
private static final class Zip64Locator {
|
||||||
|
|
||||||
|
static final int SIGNATURE = 0x07064b50;
|
||||||
|
|
||||||
|
static final int ZIP64_LOCSIZE = 20; // locator size
|
||||||
|
|
||||||
|
static final int ZIP64_LOCOFF = 8; // offset of zip64 end
|
||||||
|
|
||||||
|
private final long zip64EndOffset;
|
||||||
|
|
||||||
|
private final long offset;
|
||||||
|
|
||||||
|
private Zip64Locator(long offset, byte[] block) {
|
||||||
|
this.offset = offset;
|
||||||
|
this.zip64EndOffset = Bytes.littleEndianValue(block, ZIP64_LOCOFF, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the size of the zip 64 end record located by this zip64 end locator.
|
||||||
|
* @return size of the zip 64 end record located by this zip64 end locator
|
||||||
|
*/
|
||||||
|
private long getZip64EndSize() {
|
||||||
|
return this.offset - this.zip64EndOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the offset to locate {@link Zip64End}.
|
||||||
|
* @return offset of the Zip64 end of central directory record
|
||||||
|
*/
|
||||||
|
private long getZip64EndOffset() {
|
||||||
|
return this.zip64EndOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Zip64Locator find(RandomAccessData data, long centralDirectoryEndOffset) throws IOException {
|
||||||
|
long offset = centralDirectoryEndOffset - ZIP64_LOCSIZE;
|
||||||
|
if (offset >= 0) {
|
||||||
|
byte[] block = data.read(offset, ZIP64_LOCSIZE);
|
||||||
|
if (Bytes.littleEndianValue(block, 0, 4) == SIGNATURE) {
|
||||||
|
return new Zip64Locator(offset, block);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,222 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.jar;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.time.temporal.ChronoField;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.time.temporal.ValueRange;
|
||||||
|
|
||||||
|
import org.springframework.boot.loader.data.RandomAccessData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A ZIP File "Central directory file header record" (CDFH).
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Dmytro Nosan
|
||||||
|
* @see <a href="https://en.wikipedia.org/wiki/Zip_%28file_format%29">Zip File Format</a>
|
||||||
|
*/
|
||||||
|
|
||||||
|
final class CentralDirectoryFileHeader implements FileHeader {
|
||||||
|
|
||||||
|
private static final AsciiBytes SLASH = new AsciiBytes("/");
|
||||||
|
|
||||||
|
private static final byte[] NO_EXTRA = {};
|
||||||
|
|
||||||
|
private static final AsciiBytes NO_COMMENT = new AsciiBytes("");
|
||||||
|
|
||||||
|
private byte[] header;
|
||||||
|
|
||||||
|
private int headerOffset;
|
||||||
|
|
||||||
|
private AsciiBytes name;
|
||||||
|
|
||||||
|
private byte[] extra;
|
||||||
|
|
||||||
|
private AsciiBytes comment;
|
||||||
|
|
||||||
|
private long localHeaderOffset;
|
||||||
|
|
||||||
|
CentralDirectoryFileHeader() {
|
||||||
|
}
|
||||||
|
|
||||||
|
CentralDirectoryFileHeader(byte[] header, int headerOffset, AsciiBytes name, byte[] extra, AsciiBytes comment,
|
||||||
|
long localHeaderOffset) {
|
||||||
|
this.header = header;
|
||||||
|
this.headerOffset = headerOffset;
|
||||||
|
this.name = name;
|
||||||
|
this.extra = extra;
|
||||||
|
this.comment = comment;
|
||||||
|
this.localHeaderOffset = localHeaderOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
void load(byte[] data, int dataOffset, RandomAccessData variableData, long variableOffset, JarEntryFilter filter)
|
||||||
|
throws IOException {
|
||||||
|
// Load fixed part
|
||||||
|
this.header = data;
|
||||||
|
this.headerOffset = dataOffset;
|
||||||
|
long compressedSize = Bytes.littleEndianValue(data, dataOffset + 20, 4);
|
||||||
|
long uncompressedSize = Bytes.littleEndianValue(data, dataOffset + 24, 4);
|
||||||
|
long nameLength = Bytes.littleEndianValue(data, dataOffset + 28, 2);
|
||||||
|
long extraLength = Bytes.littleEndianValue(data, dataOffset + 30, 2);
|
||||||
|
long commentLength = Bytes.littleEndianValue(data, dataOffset + 32, 2);
|
||||||
|
long localHeaderOffset = Bytes.littleEndianValue(data, dataOffset + 42, 4);
|
||||||
|
// Load variable part
|
||||||
|
dataOffset += 46;
|
||||||
|
if (variableData != null) {
|
||||||
|
data = variableData.read(variableOffset + 46, nameLength + extraLength + commentLength);
|
||||||
|
dataOffset = 0;
|
||||||
|
}
|
||||||
|
this.name = new AsciiBytes(data, dataOffset, (int) nameLength);
|
||||||
|
if (filter != null) {
|
||||||
|
this.name = filter.apply(this.name);
|
||||||
|
}
|
||||||
|
this.extra = NO_EXTRA;
|
||||||
|
this.comment = NO_COMMENT;
|
||||||
|
if (extraLength > 0) {
|
||||||
|
this.extra = new byte[(int) extraLength];
|
||||||
|
System.arraycopy(data, (int) (dataOffset + nameLength), this.extra, 0, this.extra.length);
|
||||||
|
}
|
||||||
|
this.localHeaderOffset = getLocalHeaderOffset(compressedSize, uncompressedSize, localHeaderOffset, this.extra);
|
||||||
|
if (commentLength > 0) {
|
||||||
|
this.comment = new AsciiBytes(data, (int) (dataOffset + nameLength + extraLength), (int) commentLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private long getLocalHeaderOffset(long compressedSize, long uncompressedSize, long localHeaderOffset, byte[] extra)
|
||||||
|
throws IOException {
|
||||||
|
if (localHeaderOffset != 0xFFFFFFFFL) {
|
||||||
|
return localHeaderOffset;
|
||||||
|
}
|
||||||
|
int extraOffset = 0;
|
||||||
|
while (extraOffset < extra.length - 2) {
|
||||||
|
int id = (int) Bytes.littleEndianValue(extra, extraOffset, 2);
|
||||||
|
int length = (int) Bytes.littleEndianValue(extra, extraOffset, 2);
|
||||||
|
extraOffset += 4;
|
||||||
|
if (id == 1) {
|
||||||
|
int localHeaderExtraOffset = 0;
|
||||||
|
if (compressedSize == 0xFFFFFFFFL) {
|
||||||
|
localHeaderExtraOffset += 4;
|
||||||
|
}
|
||||||
|
if (uncompressedSize == 0xFFFFFFFFL) {
|
||||||
|
localHeaderExtraOffset += 4;
|
||||||
|
}
|
||||||
|
return Bytes.littleEndianValue(extra, extraOffset + localHeaderExtraOffset, 8);
|
||||||
|
}
|
||||||
|
extraOffset += length;
|
||||||
|
}
|
||||||
|
throw new IOException("Zip64 Extended Information Extra Field not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
AsciiBytes getName() {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasName(CharSequence name, char suffix) {
|
||||||
|
return this.name.matches(name, suffix);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isDirectory() {
|
||||||
|
return this.name.endsWith(SLASH);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getMethod() {
|
||||||
|
return (int) Bytes.littleEndianValue(this.header, this.headerOffset + 10, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
long getTime() {
|
||||||
|
long datetime = Bytes.littleEndianValue(this.header, this.headerOffset + 12, 4);
|
||||||
|
return decodeMsDosFormatDateTime(datetime);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode MS-DOS Date Time details. See <a href=
|
||||||
|
* "https://docs.microsoft.com/en-gb/windows/desktop/api/winbase/nf-winbase-dosdatetimetofiletime">
|
||||||
|
* Microsoft's documentation</a> for more details of the format.
|
||||||
|
* @param datetime the date and time
|
||||||
|
* @return the date and time as milliseconds since the epoch
|
||||||
|
*/
|
||||||
|
private long decodeMsDosFormatDateTime(long datetime) {
|
||||||
|
int year = getChronoValue(((datetime >> 25) & 0x7f) + 1980, ChronoField.YEAR);
|
||||||
|
int month = getChronoValue((datetime >> 21) & 0x0f, ChronoField.MONTH_OF_YEAR);
|
||||||
|
int day = getChronoValue((datetime >> 16) & 0x1f, ChronoField.DAY_OF_MONTH);
|
||||||
|
int hour = getChronoValue((datetime >> 11) & 0x1f, ChronoField.HOUR_OF_DAY);
|
||||||
|
int minute = getChronoValue((datetime >> 5) & 0x3f, ChronoField.MINUTE_OF_HOUR);
|
||||||
|
int second = getChronoValue((datetime << 1) & 0x3e, ChronoField.SECOND_OF_MINUTE);
|
||||||
|
return ZonedDateTime.of(year, month, day, hour, minute, second, 0, ZoneId.systemDefault())
|
||||||
|
.toInstant()
|
||||||
|
.truncatedTo(ChronoUnit.SECONDS)
|
||||||
|
.toEpochMilli();
|
||||||
|
}
|
||||||
|
|
||||||
|
long getCrc() {
|
||||||
|
return Bytes.littleEndianValue(this.header, this.headerOffset + 16, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getCompressedSize() {
|
||||||
|
return Bytes.littleEndianValue(this.header, this.headerOffset + 20, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getSize() {
|
||||||
|
return Bytes.littleEndianValue(this.header, this.headerOffset + 24, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] getExtra() {
|
||||||
|
return this.extra;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean hasExtra() {
|
||||||
|
return this.extra.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
AsciiBytes getComment() {
|
||||||
|
return this.comment;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getLocalHeaderOffset() {
|
||||||
|
return this.localHeaderOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CentralDirectoryFileHeader clone() {
|
||||||
|
byte[] header = new byte[46];
|
||||||
|
System.arraycopy(this.header, this.headerOffset, header, 0, header.length);
|
||||||
|
return new CentralDirectoryFileHeader(header, 0, this.name, header, this.comment, this.localHeaderOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
static CentralDirectoryFileHeader fromRandomAccessData(RandomAccessData data, long offset, JarEntryFilter filter)
|
||||||
|
throws IOException {
|
||||||
|
CentralDirectoryFileHeader fileHeader = new CentralDirectoryFileHeader();
|
||||||
|
byte[] bytes = data.read(offset, 46);
|
||||||
|
fileHeader.load(bytes, 0, data, offset, filter);
|
||||||
|
return fileHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getChronoValue(long value, ChronoField field) {
|
||||||
|
ValueRange range = field.range();
|
||||||
|
return Math.toIntExact(Math.min(Math.max(value, range.getMinimum()), range.getMaximum()));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,101 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.jar;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.boot.loader.data.RandomAccessData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the central directory from a JAR file.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @see CentralDirectoryVisitor
|
||||||
|
*/
|
||||||
|
class CentralDirectoryParser {
|
||||||
|
|
||||||
|
private static final int CENTRAL_DIRECTORY_HEADER_BASE_SIZE = 46;
|
||||||
|
|
||||||
|
private final List<CentralDirectoryVisitor> visitors = new ArrayList<>();
|
||||||
|
|
||||||
|
<T extends CentralDirectoryVisitor> T addVisitor(T visitor) {
|
||||||
|
this.visitors.add(visitor);
|
||||||
|
return visitor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the source data, triggering {@link CentralDirectoryVisitor visitors}.
|
||||||
|
* @param data the source data
|
||||||
|
* @param skipPrefixBytes if prefix bytes should be skipped
|
||||||
|
* @return the actual archive data without any prefix bytes
|
||||||
|
* @throws IOException on error
|
||||||
|
*/
|
||||||
|
RandomAccessData parse(RandomAccessData data, boolean skipPrefixBytes) throws IOException {
|
||||||
|
CentralDirectoryEndRecord endRecord = new CentralDirectoryEndRecord(data);
|
||||||
|
if (skipPrefixBytes) {
|
||||||
|
data = getArchiveData(endRecord, data);
|
||||||
|
}
|
||||||
|
RandomAccessData centralDirectoryData = endRecord.getCentralDirectory(data);
|
||||||
|
visitStart(endRecord, centralDirectoryData);
|
||||||
|
parseEntries(endRecord, centralDirectoryData);
|
||||||
|
visitEnd();
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parseEntries(CentralDirectoryEndRecord endRecord, RandomAccessData centralDirectoryData)
|
||||||
|
throws IOException {
|
||||||
|
byte[] bytes = centralDirectoryData.read(0, centralDirectoryData.getSize());
|
||||||
|
CentralDirectoryFileHeader fileHeader = new CentralDirectoryFileHeader();
|
||||||
|
int dataOffset = 0;
|
||||||
|
for (int i = 0; i < endRecord.getNumberOfRecords(); i++) {
|
||||||
|
fileHeader.load(bytes, dataOffset, null, 0, null);
|
||||||
|
visitFileHeader(dataOffset, fileHeader);
|
||||||
|
dataOffset += CENTRAL_DIRECTORY_HEADER_BASE_SIZE + fileHeader.getName().length()
|
||||||
|
+ fileHeader.getComment().length() + fileHeader.getExtra().length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private RandomAccessData getArchiveData(CentralDirectoryEndRecord endRecord, RandomAccessData data) {
|
||||||
|
long offset = endRecord.getStartOfArchive(data);
|
||||||
|
if (offset == 0) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
return data.getSubsection(offset, data.getSize() - offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void visitStart(CentralDirectoryEndRecord endRecord, RandomAccessData centralDirectoryData) {
|
||||||
|
for (CentralDirectoryVisitor visitor : this.visitors) {
|
||||||
|
visitor.visitStart(endRecord, centralDirectoryData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void visitFileHeader(long dataOffset, CentralDirectoryFileHeader fileHeader) {
|
||||||
|
for (CentralDirectoryVisitor visitor : this.visitors) {
|
||||||
|
visitor.visitFileHeader(fileHeader, dataOffset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void visitEnd() {
|
||||||
|
for (CentralDirectoryVisitor visitor : this.visitors) {
|
||||||
|
visitor.visitEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.jar;
|
||||||
|
|
||||||
|
import org.springframework.boot.loader.data.RandomAccessData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback visitor triggered by {@link CentralDirectoryParser}.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
interface CentralDirectoryVisitor {
|
||||||
|
|
||||||
|
void visitStart(CentralDirectoryEndRecord endRecord, RandomAccessData centralDirectoryData);
|
||||||
|
|
||||||
|
void visitFileHeader(CentralDirectoryFileHeader fileHeader, long dataOffset);
|
||||||
|
|
||||||
|
void visitEnd();
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.jar;
|
||||||
|
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A file header record that has been loaded from a Jar file.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @see JarEntry
|
||||||
|
* @see CentralDirectoryFileHeader
|
||||||
|
*/
|
||||||
|
interface FileHeader {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@code true} if the header has the given name.
|
||||||
|
* @param name the name to test
|
||||||
|
* @param suffix an additional suffix (or {@code 0})
|
||||||
|
* @return {@code true} if the header has the given name
|
||||||
|
*/
|
||||||
|
boolean hasName(CharSequence name, char suffix);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the offset of the load file header within the archive data.
|
||||||
|
* @return the local header offset
|
||||||
|
*/
|
||||||
|
long getLocalHeaderOffset();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the compressed size of the entry.
|
||||||
|
* @return the compressed size.
|
||||||
|
*/
|
||||||
|
long getCompressedSize();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the uncompressed size of the entry.
|
||||||
|
* @return the uncompressed size.
|
||||||
|
*/
|
||||||
|
long getSize();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the method used to compress the data.
|
||||||
|
* @return the zip compression method
|
||||||
|
* @see ZipEntry#STORED
|
||||||
|
* @see ZipEntry#DEFLATED
|
||||||
|
*/
|
||||||
|
int getMethod();
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,466 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.jar;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.ref.SoftReference;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.URLConnection;
|
||||||
|
import java.net.URLStreamHandler;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link URLStreamHandler} for Spring Boot loader {@link JarFile}s.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @since 1.0.0
|
||||||
|
* @see JarFile#registerUrlProtocolHandler()
|
||||||
|
*/
|
||||||
|
public class Handler extends URLStreamHandler {
|
||||||
|
|
||||||
|
// NOTE: in order to be found as a URL protocol handler, this class must be public,
|
||||||
|
// must be named Handler and must be in a package ending '.jar'
|
||||||
|
|
||||||
|
private static final String JAR_PROTOCOL = "jar:";
|
||||||
|
|
||||||
|
private static final String FILE_PROTOCOL = "file:";
|
||||||
|
|
||||||
|
private static final String TOMCAT_WARFILE_PROTOCOL = "war:file:";
|
||||||
|
|
||||||
|
private static final String SEPARATOR = "!/";
|
||||||
|
|
||||||
|
private static final Pattern SEPARATOR_PATTERN = Pattern.compile(SEPARATOR, Pattern.LITERAL);
|
||||||
|
|
||||||
|
private static final String CURRENT_DIR = "/./";
|
||||||
|
|
||||||
|
private static final Pattern CURRENT_DIR_PATTERN = Pattern.compile(CURRENT_DIR, Pattern.LITERAL);
|
||||||
|
|
||||||
|
private static final String PARENT_DIR = "/../";
|
||||||
|
|
||||||
|
private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs";
|
||||||
|
|
||||||
|
private static final String[] FALLBACK_HANDLERS = { "sun.net.www.protocol.jar.Handler" };
|
||||||
|
|
||||||
|
private static URL jarContextUrl;
|
||||||
|
|
||||||
|
private static SoftReference<Map<File, JarFile>> rootFileCache;
|
||||||
|
|
||||||
|
static {
|
||||||
|
rootFileCache = new SoftReference<>(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final JarFile jarFile;
|
||||||
|
|
||||||
|
private URLStreamHandler fallbackHandler;
|
||||||
|
|
||||||
|
public Handler() {
|
||||||
|
this(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Handler(JarFile jarFile) {
|
||||||
|
this.jarFile = jarFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected URLConnection openConnection(URL url) throws IOException {
|
||||||
|
if (this.jarFile != null && isUrlInJarFile(url, this.jarFile)) {
|
||||||
|
return JarURLConnection.get(url, this.jarFile);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JarURLConnection.get(url, getRootJarFileFromUrl(url));
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
return openFallbackConnection(url, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isUrlInJarFile(URL url, JarFile jarFile) throws MalformedURLException {
|
||||||
|
// Try the path first to save building a new url string each time
|
||||||
|
return url.getPath().startsWith(jarFile.getUrl().getPath())
|
||||||
|
&& url.toString().startsWith(jarFile.getUrlString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private URLConnection openFallbackConnection(URL url, Exception reason) throws IOException {
|
||||||
|
try {
|
||||||
|
URLConnection connection = openFallbackTomcatConnection(url);
|
||||||
|
connection = (connection != null) ? connection : openFallbackContextConnection(url);
|
||||||
|
return (connection != null) ? connection : openFallbackHandlerConnection(url);
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
if (reason instanceof IOException ioException) {
|
||||||
|
log(false, "Unable to open fallback handler", ex);
|
||||||
|
throw ioException;
|
||||||
|
}
|
||||||
|
log(true, "Unable to open fallback handler", ex);
|
||||||
|
if (reason instanceof RuntimeException runtimeException) {
|
||||||
|
throw runtimeException;
|
||||||
|
}
|
||||||
|
throw new IllegalStateException(reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to open a Tomcat formatted 'jar:war:file:...' URL. This method allows us to
|
||||||
|
* use our own nested JAR support to open the content rather than the logic in
|
||||||
|
* {@code sun.net.www.protocol.jar.URLJarFile} which will extract the nested jar to
|
||||||
|
* the temp folder to that its content can be accessed.
|
||||||
|
* @param url the URL to open
|
||||||
|
* @return a {@link URLConnection} or {@code null}
|
||||||
|
*/
|
||||||
|
private URLConnection openFallbackTomcatConnection(URL url) {
|
||||||
|
String file = url.getFile();
|
||||||
|
if (isTomcatWarUrl(file)) {
|
||||||
|
file = file.substring(TOMCAT_WARFILE_PROTOCOL.length());
|
||||||
|
file = file.replaceFirst("\\*/", "!/");
|
||||||
|
try {
|
||||||
|
URLConnection connection = openConnection(new URL("jar:file:" + file));
|
||||||
|
connection.getInputStream().close();
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
catch (IOException ex) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isTomcatWarUrl(String file) {
|
||||||
|
if (file.startsWith(TOMCAT_WARFILE_PROTOCOL) || !file.contains("*/")) {
|
||||||
|
try {
|
||||||
|
URLConnection connection = new URL(file).openConnection();
|
||||||
|
if (connection.getClass().getName().startsWith("org.apache.catalina")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to open a fallback connection by using a context URL captured before the
|
||||||
|
* jar handler was replaced with our own version. Since this method doesn't use
|
||||||
|
* reflection it won't trigger "illegal reflective access operation has occurred"
|
||||||
|
* warnings on Java 13+.
|
||||||
|
* @param url the URL to open
|
||||||
|
* @return a {@link URLConnection} or {@code null}
|
||||||
|
*/
|
||||||
|
private URLConnection openFallbackContextConnection(URL url) {
|
||||||
|
try {
|
||||||
|
if (jarContextUrl != null) {
|
||||||
|
return new URL(jarContextUrl, url.toExternalForm()).openConnection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to open a fallback connection by using reflection to access Java's default
|
||||||
|
* jar {@link URLStreamHandler}.
|
||||||
|
* @param url the URL to open
|
||||||
|
* @return the {@link URLConnection}
|
||||||
|
* @throws Exception if not connection could be opened
|
||||||
|
*/
|
||||||
|
private URLConnection openFallbackHandlerConnection(URL url) throws Exception {
|
||||||
|
URLStreamHandler fallbackHandler = getFallbackHandler();
|
||||||
|
return new URL(null, url.toExternalForm(), fallbackHandler).openConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
private URLStreamHandler getFallbackHandler() {
|
||||||
|
if (this.fallbackHandler != null) {
|
||||||
|
return this.fallbackHandler;
|
||||||
|
}
|
||||||
|
for (String handlerClassName : FALLBACK_HANDLERS) {
|
||||||
|
try {
|
||||||
|
Class<?> handlerClass = Class.forName(handlerClassName);
|
||||||
|
this.fallbackHandler = (URLStreamHandler) handlerClass.getDeclaredConstructor().newInstance();
|
||||||
|
return this.fallbackHandler;
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Unable to find fallback handler");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void log(boolean warning, String message, Exception cause) {
|
||||||
|
try {
|
||||||
|
Level level = warning ? Level.WARNING : Level.FINEST;
|
||||||
|
Logger.getLogger(getClass().getName()).log(level, message, cause);
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
if (warning) {
|
||||||
|
System.err.println("WARNING: " + message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void parseURL(URL context, String spec, int start, int limit) {
|
||||||
|
if (spec.regionMatches(true, 0, JAR_PROTOCOL, 0, JAR_PROTOCOL.length())) {
|
||||||
|
setFile(context, getFileFromSpec(spec.substring(start, limit)));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setFile(context, getFileFromContext(context, spec.substring(start, limit)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getFileFromSpec(String spec) {
|
||||||
|
int separatorIndex = spec.lastIndexOf("!/");
|
||||||
|
if (separatorIndex == -1) {
|
||||||
|
throw new IllegalArgumentException("No !/ in spec '" + spec + "'");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
new URL(spec.substring(0, separatorIndex));
|
||||||
|
return spec;
|
||||||
|
}
|
||||||
|
catch (MalformedURLException ex) {
|
||||||
|
throw new IllegalArgumentException("Invalid spec URL '" + spec + "'", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getFileFromContext(URL context, String spec) {
|
||||||
|
String file = context.getFile();
|
||||||
|
if (spec.startsWith("/")) {
|
||||||
|
return trimToJarRoot(file) + SEPARATOR + spec.substring(1);
|
||||||
|
}
|
||||||
|
if (file.endsWith("/")) {
|
||||||
|
return file + spec;
|
||||||
|
}
|
||||||
|
int lastSlashIndex = file.lastIndexOf('/');
|
||||||
|
if (lastSlashIndex == -1) {
|
||||||
|
throw new IllegalArgumentException("No / found in context URL's file '" + file + "'");
|
||||||
|
}
|
||||||
|
return file.substring(0, lastSlashIndex + 1) + spec;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String trimToJarRoot(String file) {
|
||||||
|
int lastSeparatorIndex = file.lastIndexOf(SEPARATOR);
|
||||||
|
if (lastSeparatorIndex == -1) {
|
||||||
|
throw new IllegalArgumentException("No !/ found in context URL's file '" + file + "'");
|
||||||
|
}
|
||||||
|
return file.substring(0, lastSeparatorIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setFile(URL context, String file) {
|
||||||
|
String path = normalize(file);
|
||||||
|
String query = null;
|
||||||
|
int queryIndex = path.lastIndexOf('?');
|
||||||
|
if (queryIndex != -1) {
|
||||||
|
query = path.substring(queryIndex + 1);
|
||||||
|
path = path.substring(0, queryIndex);
|
||||||
|
}
|
||||||
|
setURL(context, JAR_PROTOCOL, null, -1, null, null, path, query, context.getRef());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalize(String file) {
|
||||||
|
if (!file.contains(CURRENT_DIR) && !file.contains(PARENT_DIR)) {
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
int afterLastSeparatorIndex = file.lastIndexOf(SEPARATOR) + SEPARATOR.length();
|
||||||
|
String afterSeparator = file.substring(afterLastSeparatorIndex);
|
||||||
|
afterSeparator = replaceParentDir(afterSeparator);
|
||||||
|
afterSeparator = replaceCurrentDir(afterSeparator);
|
||||||
|
return file.substring(0, afterLastSeparatorIndex) + afterSeparator;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String replaceParentDir(String file) {
|
||||||
|
int parentDirIndex;
|
||||||
|
while ((parentDirIndex = file.indexOf(PARENT_DIR)) >= 0) {
|
||||||
|
int precedingSlashIndex = file.lastIndexOf('/', parentDirIndex - 1);
|
||||||
|
if (precedingSlashIndex >= 0) {
|
||||||
|
file = file.substring(0, precedingSlashIndex) + file.substring(parentDirIndex + 3);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
file = file.substring(parentDirIndex + 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String replaceCurrentDir(String file) {
|
||||||
|
return CURRENT_DIR_PATTERN.matcher(file).replaceAll("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int hashCode(URL u) {
|
||||||
|
return hashCode(u.getProtocol(), u.getFile());
|
||||||
|
}
|
||||||
|
|
||||||
|
private int hashCode(String protocol, String file) {
|
||||||
|
int result = (protocol != null) ? protocol.hashCode() : 0;
|
||||||
|
int separatorIndex = file.indexOf(SEPARATOR);
|
||||||
|
if (separatorIndex == -1) {
|
||||||
|
return result + file.hashCode();
|
||||||
|
}
|
||||||
|
String source = file.substring(0, separatorIndex);
|
||||||
|
String entry = canonicalize(file.substring(separatorIndex + 2));
|
||||||
|
try {
|
||||||
|
result += new URL(source).hashCode();
|
||||||
|
}
|
||||||
|
catch (MalformedURLException ex) {
|
||||||
|
result += source.hashCode();
|
||||||
|
}
|
||||||
|
result += entry.hashCode();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean sameFile(URL u1, URL u2) {
|
||||||
|
if (!u1.getProtocol().equals("jar") || !u2.getProtocol().equals("jar")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int separator1 = u1.getFile().indexOf(SEPARATOR);
|
||||||
|
int separator2 = u2.getFile().indexOf(SEPARATOR);
|
||||||
|
if (separator1 == -1 || separator2 == -1) {
|
||||||
|
return super.sameFile(u1, u2);
|
||||||
|
}
|
||||||
|
String nested1 = u1.getFile().substring(separator1 + SEPARATOR.length());
|
||||||
|
String nested2 = u2.getFile().substring(separator2 + SEPARATOR.length());
|
||||||
|
if (!nested1.equals(nested2)) {
|
||||||
|
String canonical1 = canonicalize(nested1);
|
||||||
|
String canonical2 = canonicalize(nested2);
|
||||||
|
if (!canonical1.equals(canonical2)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String root1 = u1.getFile().substring(0, separator1);
|
||||||
|
String root2 = u2.getFile().substring(0, separator2);
|
||||||
|
try {
|
||||||
|
return super.sameFile(new URL(root1), new URL(root2));
|
||||||
|
}
|
||||||
|
catch (MalformedURLException ex) {
|
||||||
|
// Continue
|
||||||
|
}
|
||||||
|
return super.sameFile(u1, u2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String canonicalize(String path) {
|
||||||
|
return SEPARATOR_PATTERN.matcher(path).replaceAll("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
public JarFile getRootJarFileFromUrl(URL url) throws IOException {
|
||||||
|
String spec = url.getFile();
|
||||||
|
int separatorIndex = spec.indexOf(SEPARATOR);
|
||||||
|
if (separatorIndex == -1) {
|
||||||
|
throw new MalformedURLException("Jar URL does not contain !/ separator");
|
||||||
|
}
|
||||||
|
String name = spec.substring(0, separatorIndex);
|
||||||
|
return getRootJarFile(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private JarFile getRootJarFile(String name) throws IOException {
|
||||||
|
try {
|
||||||
|
if (!name.startsWith(FILE_PROTOCOL)) {
|
||||||
|
throw new IllegalStateException("Not a file URL");
|
||||||
|
}
|
||||||
|
File file = new File(URI.create(name));
|
||||||
|
Map<File, JarFile> cache = rootFileCache.get();
|
||||||
|
JarFile result = (cache != null) ? cache.get(file) : null;
|
||||||
|
if (result == null) {
|
||||||
|
result = new JarFile(file);
|
||||||
|
addToRootFileCache(file, result);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
throw new IOException("Unable to open root Jar file '" + name + "'", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the given {@link JarFile} to the root file cache.
|
||||||
|
* @param sourceFile the source file to add
|
||||||
|
* @param jarFile the jar file.
|
||||||
|
*/
|
||||||
|
static void addToRootFileCache(File sourceFile, JarFile jarFile) {
|
||||||
|
Map<File, JarFile> cache = rootFileCache.get();
|
||||||
|
if (cache == null) {
|
||||||
|
cache = new ConcurrentHashMap<>();
|
||||||
|
rootFileCache = new SoftReference<>(cache);
|
||||||
|
}
|
||||||
|
cache.put(sourceFile, jarFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If possible, capture a URL that is configured with the original jar handler so that
|
||||||
|
* we can use it as a fallback context later. We can only do this if we know that we
|
||||||
|
* can reset the handlers after.
|
||||||
|
*/
|
||||||
|
static void captureJarContextUrl() {
|
||||||
|
if (canResetCachedUrlHandlers()) {
|
||||||
|
String handlers = System.getProperty(PROTOCOL_HANDLER);
|
||||||
|
try {
|
||||||
|
System.clearProperty(PROTOCOL_HANDLER);
|
||||||
|
try {
|
||||||
|
resetCachedUrlHandlers();
|
||||||
|
jarContextUrl = new URL("jar:file:context.jar!/");
|
||||||
|
URLConnection connection = jarContextUrl.openConnection();
|
||||||
|
if (connection instanceof JarURLConnection) {
|
||||||
|
jarContextUrl = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if (handlers == null) {
|
||||||
|
System.clearProperty(PROTOCOL_HANDLER);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
System.setProperty(PROTOCOL_HANDLER, handlers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resetCachedUrlHandlers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean canResetCachedUrlHandlers() {
|
||||||
|
try {
|
||||||
|
resetCachedUrlHandlers();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Error ex) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void resetCachedUrlHandlers() {
|
||||||
|
URL.setURLStreamHandlerFactory(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set if a generic static exception can be thrown when a URL cannot be connected.
|
||||||
|
* This optimization is used during class loading to save creating lots of exceptions
|
||||||
|
* which are then swallowed.
|
||||||
|
* @param useFastConnectionExceptions if fast connection exceptions can be used.
|
||||||
|
*/
|
||||||
|
public static void setUseFastConnectionExceptions(boolean useFastConnectionExceptions) {
|
||||||
|
JarURLConnection.setUseFastExceptions(useFastConnectionExceptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,120 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.jar;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.security.CodeSigner;
|
||||||
|
import java.security.cert.Certificate;
|
||||||
|
import java.util.jar.Attributes;
|
||||||
|
import java.util.jar.Manifest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extended variant of {@link java.util.jar.JarEntry} returned by {@link JarFile}s.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
*/
|
||||||
|
class JarEntry extends java.util.jar.JarEntry implements FileHeader {
|
||||||
|
|
||||||
|
private final int index;
|
||||||
|
|
||||||
|
private final AsciiBytes name;
|
||||||
|
|
||||||
|
private final AsciiBytes headerName;
|
||||||
|
|
||||||
|
private final JarFile jarFile;
|
||||||
|
|
||||||
|
private final long localHeaderOffset;
|
||||||
|
|
||||||
|
private volatile JarEntryCertification certification;
|
||||||
|
|
||||||
|
JarEntry(JarFile jarFile, int index, CentralDirectoryFileHeader header, AsciiBytes nameAlias) {
|
||||||
|
super((nameAlias != null) ? nameAlias.toString() : header.getName().toString());
|
||||||
|
this.index = index;
|
||||||
|
this.name = (nameAlias != null) ? nameAlias : header.getName();
|
||||||
|
this.headerName = header.getName();
|
||||||
|
this.jarFile = jarFile;
|
||||||
|
this.localHeaderOffset = header.getLocalHeaderOffset();
|
||||||
|
setCompressedSize(header.getCompressedSize());
|
||||||
|
setMethod(header.getMethod());
|
||||||
|
setCrc(header.getCrc());
|
||||||
|
setComment(header.getComment().toString());
|
||||||
|
setSize(header.getSize());
|
||||||
|
setTime(header.getTime());
|
||||||
|
if (header.hasExtra()) {
|
||||||
|
setExtra(header.getExtra());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int getIndex() {
|
||||||
|
return this.index;
|
||||||
|
}
|
||||||
|
|
||||||
|
AsciiBytes getAsciiBytesName() {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasName(CharSequence name, char suffix) {
|
||||||
|
return this.headerName.matches(name, suffix);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a {@link URL} for this {@link JarEntry}.
|
||||||
|
* @return the URL for the entry
|
||||||
|
* @throws MalformedURLException if the URL is not valid
|
||||||
|
*/
|
||||||
|
URL getUrl() throws MalformedURLException {
|
||||||
|
return new URL(this.jarFile.getUrl(), getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Attributes getAttributes() throws IOException {
|
||||||
|
Manifest manifest = this.jarFile.getManifest();
|
||||||
|
return (manifest != null) ? manifest.getAttributes(getName()) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Certificate[] getCertificates() {
|
||||||
|
return getCertification().getCertificates();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CodeSigner[] getCodeSigners() {
|
||||||
|
return getCertification().getCodeSigners();
|
||||||
|
}
|
||||||
|
|
||||||
|
private JarEntryCertification getCertification() {
|
||||||
|
if (!this.jarFile.isSigned()) {
|
||||||
|
return JarEntryCertification.NONE;
|
||||||
|
}
|
||||||
|
JarEntryCertification certification = this.certification;
|
||||||
|
if (certification == null) {
|
||||||
|
certification = this.jarFile.getCertification(this);
|
||||||
|
this.certification = certification;
|
||||||
|
}
|
||||||
|
return certification;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getLocalHeaderOffset() {
|
||||||
|
return this.localHeaderOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.jar;
|
||||||
|
|
||||||
|
import java.security.CodeSigner;
|
||||||
|
import java.security.cert.Certificate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link Certificate} and {@link CodeSigner} details for a {@link JarEntry} from a signed
|
||||||
|
* {@link JarFile}.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class JarEntryCertification {
|
||||||
|
|
||||||
|
static final JarEntryCertification NONE = new JarEntryCertification(null, null);
|
||||||
|
|
||||||
|
private final Certificate[] certificates;
|
||||||
|
|
||||||
|
private final CodeSigner[] codeSigners;
|
||||||
|
|
||||||
|
JarEntryCertification(Certificate[] certificates, CodeSigner[] codeSigners) {
|
||||||
|
this.certificates = certificates;
|
||||||
|
this.codeSigners = codeSigners;
|
||||||
|
}
|
||||||
|
|
||||||
|
Certificate[] getCertificates() {
|
||||||
|
return (this.certificates != null) ? this.certificates.clone() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
CodeSigner[] getCodeSigners() {
|
||||||
|
return (this.codeSigners != null) ? this.codeSigners.clone() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static JarEntryCertification from(java.util.jar.JarEntry certifiedEntry) {
|
||||||
|
Certificate[] certificates = (certifiedEntry != null) ? certifiedEntry.getCertificates() : null;
|
||||||
|
CodeSigner[] codeSigners = (certifiedEntry != null) ? certifiedEntry.getCodeSigners() : null;
|
||||||
|
if (certificates == null && codeSigners == null) {
|
||||||
|
return NONE;
|
||||||
|
}
|
||||||
|
return new JarEntryCertification(certificates, codeSigners);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.jar;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface that can be used to filter and optionally rename jar entries.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
interface JarEntryFilter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the jar entry filter.
|
||||||
|
* @param name the current entry name. This may be different that the original entry
|
||||||
|
* name if a previous filter has been applied
|
||||||
|
* @return the new name of the entry or {@code null} if the entry should not be
|
||||||
|
* included.
|
||||||
|
*/
|
||||||
|
AsciiBytes apply(AsciiBytes name);
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,475 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.jar;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FilePermission;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.lang.ref.SoftReference;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.URLStreamHandler;
|
||||||
|
import java.net.URLStreamHandlerFactory;
|
||||||
|
import java.security.Permission;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.Spliterator;
|
||||||
|
import java.util.Spliterators;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
import java.util.jar.Manifest;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
import java.util.stream.StreamSupport;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
|
||||||
|
import org.springframework.boot.loader.data.RandomAccessData;
|
||||||
|
import org.springframework.boot.loader.data.RandomAccessDataFile;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extended variant of {@link java.util.jar.JarFile} that behaves in the same way but
|
||||||
|
* offers the following additional functionality.
|
||||||
|
* <ul>
|
||||||
|
* <li>A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} based
|
||||||
|
* on any directory entry.</li>
|
||||||
|
* <li>A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} for
|
||||||
|
* embedded JAR files (as long as their entry is not compressed).</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
public class JarFile extends AbstractJarFile implements Iterable<java.util.jar.JarEntry> {
|
||||||
|
|
||||||
|
private static final String MANIFEST_NAME = "META-INF/MANIFEST.MF";
|
||||||
|
|
||||||
|
private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs";
|
||||||
|
|
||||||
|
private static final String HANDLERS_PACKAGE = "org.springframework.boot.loader";
|
||||||
|
|
||||||
|
private static final AsciiBytes META_INF = new AsciiBytes("META-INF/");
|
||||||
|
|
||||||
|
private static final AsciiBytes SIGNATURE_FILE_EXTENSION = new AsciiBytes(".SF");
|
||||||
|
|
||||||
|
private static final String READ_ACTION = "read";
|
||||||
|
|
||||||
|
private final RandomAccessDataFile rootFile;
|
||||||
|
|
||||||
|
private final String pathFromRoot;
|
||||||
|
|
||||||
|
private final RandomAccessData data;
|
||||||
|
|
||||||
|
private final JarFileType type;
|
||||||
|
|
||||||
|
private URL url;
|
||||||
|
|
||||||
|
private String urlString;
|
||||||
|
|
||||||
|
private final JarFileEntries entries;
|
||||||
|
|
||||||
|
private final Supplier<Manifest> manifestSupplier;
|
||||||
|
|
||||||
|
private SoftReference<Manifest> manifest;
|
||||||
|
|
||||||
|
private boolean signed;
|
||||||
|
|
||||||
|
private String comment;
|
||||||
|
|
||||||
|
private volatile boolean closed;
|
||||||
|
|
||||||
|
private volatile JarFileWrapper wrapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link JarFile} backed by the specified file.
|
||||||
|
* @param file the root jar file
|
||||||
|
* @throws IOException if the file cannot be read
|
||||||
|
*/
|
||||||
|
public JarFile(File file) throws IOException {
|
||||||
|
this(new RandomAccessDataFile(file));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link JarFile} backed by the specified file.
|
||||||
|
* @param file the root jar file
|
||||||
|
* @throws IOException if the file cannot be read
|
||||||
|
*/
|
||||||
|
JarFile(RandomAccessDataFile file) throws IOException {
|
||||||
|
this(file, "", file, JarFileType.DIRECT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private constructor used to create a new {@link JarFile} either directly or from a
|
||||||
|
* nested entry.
|
||||||
|
* @param rootFile the root jar file
|
||||||
|
* @param pathFromRoot the name of this file
|
||||||
|
* @param data the underlying data
|
||||||
|
* @param type the type of the jar file
|
||||||
|
* @throws IOException if the file cannot be read
|
||||||
|
*/
|
||||||
|
private JarFile(RandomAccessDataFile rootFile, String pathFromRoot, RandomAccessData data, JarFileType type)
|
||||||
|
throws IOException {
|
||||||
|
this(rootFile, pathFromRoot, data, null, type, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private JarFile(RandomAccessDataFile rootFile, String pathFromRoot, RandomAccessData data, JarEntryFilter filter,
|
||||||
|
JarFileType type, Supplier<Manifest> manifestSupplier) throws IOException {
|
||||||
|
super(rootFile.getFile());
|
||||||
|
super.close();
|
||||||
|
this.rootFile = rootFile;
|
||||||
|
this.pathFromRoot = pathFromRoot;
|
||||||
|
CentralDirectoryParser parser = new CentralDirectoryParser();
|
||||||
|
this.entries = parser.addVisitor(new JarFileEntries(this, filter));
|
||||||
|
this.type = type;
|
||||||
|
parser.addVisitor(centralDirectoryVisitor());
|
||||||
|
try {
|
||||||
|
this.data = parser.parse(data, filter == null);
|
||||||
|
}
|
||||||
|
catch (RuntimeException ex) {
|
||||||
|
try {
|
||||||
|
this.rootFile.close();
|
||||||
|
super.close();
|
||||||
|
}
|
||||||
|
catch (IOException ioex) {
|
||||||
|
}
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
this.manifestSupplier = (manifestSupplier != null) ? manifestSupplier : () -> {
|
||||||
|
try (InputStream inputStream = getInputStream(MANIFEST_NAME)) {
|
||||||
|
if (inputStream == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new Manifest(inputStream);
|
||||||
|
}
|
||||||
|
catch (IOException ex) {
|
||||||
|
throw new RuntimeException(ex);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private CentralDirectoryVisitor centralDirectoryVisitor() {
|
||||||
|
return new CentralDirectoryVisitor() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visitStart(CentralDirectoryEndRecord endRecord, RandomAccessData centralDirectoryData) {
|
||||||
|
JarFile.this.comment = endRecord.getComment();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visitFileHeader(CentralDirectoryFileHeader fileHeader, long dataOffset) {
|
||||||
|
AsciiBytes name = fileHeader.getName();
|
||||||
|
if (name.startsWith(META_INF) && name.endsWith(SIGNATURE_FILE_EXTENSION)) {
|
||||||
|
JarFile.this.signed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visitEnd() {
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
JarFileWrapper getWrapper() throws IOException {
|
||||||
|
JarFileWrapper wrapper = this.wrapper;
|
||||||
|
if (wrapper == null) {
|
||||||
|
wrapper = new JarFileWrapper(this);
|
||||||
|
this.wrapper = wrapper;
|
||||||
|
}
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
Permission getPermission() {
|
||||||
|
return new FilePermission(this.rootFile.getFile().getPath(), READ_ACTION);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final RandomAccessDataFile getRootJarFile() {
|
||||||
|
return this.rootFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
RandomAccessData getData() {
|
||||||
|
return this.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Manifest getManifest() throws IOException {
|
||||||
|
Manifest manifest = (this.manifest != null) ? this.manifest.get() : null;
|
||||||
|
if (manifest == null) {
|
||||||
|
try {
|
||||||
|
manifest = this.manifestSupplier.get();
|
||||||
|
}
|
||||||
|
catch (RuntimeException ex) {
|
||||||
|
throw new IOException(ex);
|
||||||
|
}
|
||||||
|
this.manifest = new SoftReference<>(manifest);
|
||||||
|
}
|
||||||
|
return manifest;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Enumeration<java.util.jar.JarEntry> entries() {
|
||||||
|
return new JarEntryEnumeration(this.entries.iterator());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Stream<java.util.jar.JarEntry> stream() {
|
||||||
|
Spliterator<java.util.jar.JarEntry> spliterator = Spliterators.spliterator(iterator(), size(),
|
||||||
|
Spliterator.ORDERED | Spliterator.DISTINCT | Spliterator.IMMUTABLE | Spliterator.NONNULL);
|
||||||
|
return StreamSupport.stream(spliterator, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an iterator for the contained entries.
|
||||||
|
* @since 2.3.0
|
||||||
|
* @see java.lang.Iterable#iterator()
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||||
|
public Iterator<java.util.jar.JarEntry> iterator() {
|
||||||
|
return (Iterator) this.entries.iterator(this::ensureOpen);
|
||||||
|
}
|
||||||
|
|
||||||
|
public JarEntry getJarEntry(CharSequence name) {
|
||||||
|
return this.entries.getEntry(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JarEntry getJarEntry(String name) {
|
||||||
|
return (JarEntry) getEntry(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean containsEntry(String name) {
|
||||||
|
return this.entries.containsEntry(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ZipEntry getEntry(String name) {
|
||||||
|
ensureOpen();
|
||||||
|
return this.entries.getEntry(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
InputStream getInputStream() throws IOException {
|
||||||
|
return this.data.getInputStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized InputStream getInputStream(ZipEntry entry) throws IOException {
|
||||||
|
ensureOpen();
|
||||||
|
if (entry instanceof JarEntry jarEntry) {
|
||||||
|
return this.entries.getInputStream(jarEntry);
|
||||||
|
}
|
||||||
|
return getInputStream((entry != null) ? entry.getName() : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
InputStream getInputStream(String name) throws IOException {
|
||||||
|
return this.entries.getInputStream(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a nested {@link JarFile} loaded from the specified entry.
|
||||||
|
* @param entry the zip entry
|
||||||
|
* @return a {@link JarFile} for the entry
|
||||||
|
* @throws IOException if the nested jar file cannot be read
|
||||||
|
*/
|
||||||
|
public synchronized JarFile getNestedJarFile(ZipEntry entry) throws IOException {
|
||||||
|
return getNestedJarFile((JarEntry) entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a nested {@link JarFile} loaded from the specified entry.
|
||||||
|
* @param entry the zip entry
|
||||||
|
* @return a {@link JarFile} for the entry
|
||||||
|
* @throws IOException if the nested jar file cannot be read
|
||||||
|
*/
|
||||||
|
public synchronized JarFile getNestedJarFile(JarEntry entry) throws IOException {
|
||||||
|
try {
|
||||||
|
return createJarFileFromEntry(entry);
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
throw new IOException("Unable to open nested jar file '" + entry.getName() + "'", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private JarFile createJarFileFromEntry(JarEntry entry) throws IOException {
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
return createJarFileFromDirectoryEntry(entry);
|
||||||
|
}
|
||||||
|
return createJarFileFromFileEntry(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
private JarFile createJarFileFromDirectoryEntry(JarEntry entry) throws IOException {
|
||||||
|
AsciiBytes name = entry.getAsciiBytesName();
|
||||||
|
JarEntryFilter filter = (candidate) -> {
|
||||||
|
if (candidate.startsWith(name) && !candidate.equals(name)) {
|
||||||
|
return candidate.substring(name.length());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
return new JarFile(this.rootFile, this.pathFromRoot + "!/" + entry.getName().substring(0, name.length() - 1),
|
||||||
|
this.data, filter, JarFileType.NESTED_DIRECTORY, this.manifestSupplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
private JarFile createJarFileFromFileEntry(JarEntry entry) throws IOException {
|
||||||
|
if (entry.getMethod() != ZipEntry.STORED) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Unable to open nested entry '" + entry.getName() + "'. It has been compressed and nested "
|
||||||
|
+ "jar files must be stored without compression. Please check the "
|
||||||
|
+ "mechanism used to create your executable jar file");
|
||||||
|
}
|
||||||
|
RandomAccessData entryData = this.entries.getEntryData(entry.getName());
|
||||||
|
return new JarFile(this.rootFile, this.pathFromRoot + "!/" + entry.getName(), entryData,
|
||||||
|
JarFileType.NESTED_JAR);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getComment() {
|
||||||
|
ensureOpen();
|
||||||
|
return this.comment;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int size() {
|
||||||
|
ensureOpen();
|
||||||
|
return this.entries.getSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
if (this.closed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
super.close();
|
||||||
|
if (this.type == JarFileType.DIRECT) {
|
||||||
|
this.rootFile.close();
|
||||||
|
}
|
||||||
|
this.closed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureOpen() {
|
||||||
|
if (this.closed) {
|
||||||
|
throw new IllegalStateException("zip file closed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isClosed() {
|
||||||
|
return this.closed;
|
||||||
|
}
|
||||||
|
|
||||||
|
String getUrlString() throws MalformedURLException {
|
||||||
|
if (this.urlString == null) {
|
||||||
|
this.urlString = getUrl().toString();
|
||||||
|
}
|
||||||
|
return this.urlString;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public URL getUrl() throws MalformedURLException {
|
||||||
|
if (this.url == null) {
|
||||||
|
String file = this.rootFile.getFile().toURI() + this.pathFromRoot + "!/";
|
||||||
|
file = file.replace("file:////", "file://"); // Fix UNC paths
|
||||||
|
this.url = new URL("jar", "", -1, file, new Handler(this));
|
||||||
|
}
|
||||||
|
return this.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return this.rootFile.getFile() + this.pathFromRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isSigned() {
|
||||||
|
return this.signed;
|
||||||
|
}
|
||||||
|
|
||||||
|
JarEntryCertification getCertification(JarEntry entry) {
|
||||||
|
try {
|
||||||
|
return this.entries.getCertification(entry);
|
||||||
|
}
|
||||||
|
catch (IOException ex) {
|
||||||
|
throw new IllegalStateException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearCache() {
|
||||||
|
this.entries.clearCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String getPathFromRoot() {
|
||||||
|
return this.pathFromRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
JarFileType getType() {
|
||||||
|
return this.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a {@literal 'java.protocol.handler.pkgs'} property so that a
|
||||||
|
* {@link URLStreamHandler} will be located to deal with jar URLs.
|
||||||
|
*/
|
||||||
|
public static void registerUrlProtocolHandler() {
|
||||||
|
Handler.captureJarContextUrl();
|
||||||
|
String handlers = System.getProperty(PROTOCOL_HANDLER, "");
|
||||||
|
System.setProperty(PROTOCOL_HANDLER,
|
||||||
|
((handlers == null || handlers.isEmpty()) ? HANDLERS_PACKAGE : handlers + "|" + HANDLERS_PACKAGE));
|
||||||
|
resetCachedUrlHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset any cached handlers just in case a jar protocol has already been used. We
|
||||||
|
* reset the handler by trying to set a null {@link URLStreamHandlerFactory} which
|
||||||
|
* should have no effect other than clearing the handlers cache.
|
||||||
|
*/
|
||||||
|
private static void resetCachedUrlHandlers() {
|
||||||
|
try {
|
||||||
|
URL.setURLStreamHandlerFactory(null);
|
||||||
|
}
|
||||||
|
catch (Error ex) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An {@link Enumeration} on {@linkplain java.util.jar.JarEntry jar entries}.
|
||||||
|
*/
|
||||||
|
private static class JarEntryEnumeration implements Enumeration<java.util.jar.JarEntry> {
|
||||||
|
|
||||||
|
private final Iterator<JarEntry> iterator;
|
||||||
|
|
||||||
|
JarEntryEnumeration(Iterator<JarEntry> iterator) {
|
||||||
|
this.iterator = iterator;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasMoreElements() {
|
||||||
|
return this.iterator.hasNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public java.util.jar.JarEntry nextElement() {
|
||||||
|
return this.iterator.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,491 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.jar;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
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.JarInputStream;
|
||||||
|
import java.util.jar.Manifest;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
|
||||||
|
import org.springframework.boot.loader.data.RandomAccessData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides access to entries from a {@link JarFile}. In order to reduce memory
|
||||||
|
* consumption entry details are stored using arrays. The {@code hashCodes} array stores
|
||||||
|
* the hash code of the entry name, the {@code centralDirectoryOffsets} provides the
|
||||||
|
* offset to the central directory record and {@code positions} provides the original
|
||||||
|
* order position of the entry. The arrays are stored in hashCode order so that a binary
|
||||||
|
* search can be used to find a name.
|
||||||
|
* <p>
|
||||||
|
* A typical Spring Boot application will have somewhere in the region of 10,500 entries
|
||||||
|
* which should consume about 122K.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
*/
|
||||||
|
class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> {
|
||||||
|
|
||||||
|
private static final Runnable NO_VALIDATION = () -> {
|
||||||
|
};
|
||||||
|
|
||||||
|
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 = Runtime.version().feature();
|
||||||
|
|
||||||
|
private static final long LOCAL_FILE_HEADER_SIZE = 30;
|
||||||
|
|
||||||
|
private static final char SLASH = '/';
|
||||||
|
|
||||||
|
private static final char NO_SUFFIX = 0;
|
||||||
|
|
||||||
|
protected static final int ENTRY_CACHE_SIZE = 25;
|
||||||
|
|
||||||
|
private final JarFile jarFile;
|
||||||
|
|
||||||
|
private final JarEntryFilter filter;
|
||||||
|
|
||||||
|
private RandomAccessData centralDirectoryData;
|
||||||
|
|
||||||
|
private int size;
|
||||||
|
|
||||||
|
private int[] hashCodes;
|
||||||
|
|
||||||
|
private Offsets centralDirectoryOffsets;
|
||||||
|
|
||||||
|
private int[] positions;
|
||||||
|
|
||||||
|
private Boolean multiReleaseJar;
|
||||||
|
|
||||||
|
private JarEntryCertification[] certifications;
|
||||||
|
|
||||||
|
private final Map<Integer, FileHeader> entriesCache = Collections
|
||||||
|
.synchronizedMap(new LinkedHashMap<>(16, 0.75f, true) {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean removeEldestEntry(Map.Entry<Integer, FileHeader> eldest) {
|
||||||
|
return size() >= ENTRY_CACHE_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
JarFileEntries(JarFile jarFile, JarEntryFilter filter) {
|
||||||
|
this.jarFile = jarFile;
|
||||||
|
this.filter = filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visitStart(CentralDirectoryEndRecord endRecord, RandomAccessData centralDirectoryData) {
|
||||||
|
int maxSize = endRecord.getNumberOfRecords();
|
||||||
|
this.centralDirectoryData = centralDirectoryData;
|
||||||
|
this.hashCodes = new int[maxSize];
|
||||||
|
this.centralDirectoryOffsets = Offsets.from(endRecord);
|
||||||
|
this.positions = new int[maxSize];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visitFileHeader(CentralDirectoryFileHeader fileHeader, long dataOffset) {
|
||||||
|
AsciiBytes name = applyFilter(fileHeader.getName());
|
||||||
|
if (name != null) {
|
||||||
|
add(name, dataOffset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void add(AsciiBytes name, long dataOffset) {
|
||||||
|
this.hashCodes[this.size] = name.hashCode();
|
||||||
|
this.centralDirectoryOffsets.set(this.size, dataOffset);
|
||||||
|
this.positions[this.size] = this.size;
|
||||||
|
this.size++;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visitEnd() {
|
||||||
|
sort(0, this.size - 1);
|
||||||
|
int[] positions = this.positions;
|
||||||
|
this.positions = new int[positions.length];
|
||||||
|
for (int i = 0; i < this.size; i++) {
|
||||||
|
this.positions[positions[i]] = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int getSize() {
|
||||||
|
return this.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sort(int left, int right) {
|
||||||
|
// Quick sort algorithm, uses hashCodes as the source but sorts all arrays
|
||||||
|
if (left < right) {
|
||||||
|
int pivot = this.hashCodes[left + (right - left) / 2];
|
||||||
|
int i = left;
|
||||||
|
int j = right;
|
||||||
|
while (i <= j) {
|
||||||
|
while (this.hashCodes[i] < pivot) {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
while (this.hashCodes[j] > pivot) {
|
||||||
|
j--;
|
||||||
|
}
|
||||||
|
if (i <= j) {
|
||||||
|
swap(i, j);
|
||||||
|
i++;
|
||||||
|
j--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (left < j) {
|
||||||
|
sort(left, j);
|
||||||
|
}
|
||||||
|
if (right > i) {
|
||||||
|
sort(i, right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void swap(int i, int j) {
|
||||||
|
swap(this.hashCodes, i, j);
|
||||||
|
this.centralDirectoryOffsets.swap(i, j);
|
||||||
|
swap(this.positions, i, j);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Iterator<JarEntry> iterator() {
|
||||||
|
return new EntryIterator(NO_VALIDATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
Iterator<JarEntry> iterator(Runnable validator) {
|
||||||
|
return new EntryIterator(validator);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean containsEntry(CharSequence name) {
|
||||||
|
return getEntry(name, FileHeader.class, true) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
JarEntry getEntry(CharSequence name) {
|
||||||
|
return getEntry(name, JarEntry.class, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
InputStream getInputStream(String name) throws IOException {
|
||||||
|
FileHeader entry = getEntry(name, FileHeader.class, false);
|
||||||
|
return getInputStream(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
InputStream getInputStream(FileHeader entry) throws IOException {
|
||||||
|
if (entry == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
InputStream inputStream = getEntryData(entry).getInputStream();
|
||||||
|
if (entry.getMethod() == ZipEntry.DEFLATED) {
|
||||||
|
inputStream = new ZipInflaterInputStream(inputStream, (int) entry.getSize());
|
||||||
|
}
|
||||||
|
return inputStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
RandomAccessData getEntryData(String name) throws IOException {
|
||||||
|
FileHeader entry = getEntry(name, FileHeader.class, false);
|
||||||
|
if (entry == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return getEntryData(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
private RandomAccessData getEntryData(FileHeader entry) throws IOException {
|
||||||
|
// aspectjrt-1.7.4.jar has a different ext bytes length in the
|
||||||
|
// local directory to the central directory. We need to re-read
|
||||||
|
// here to skip them
|
||||||
|
RandomAccessData data = this.jarFile.getData();
|
||||||
|
byte[] localHeader = data.read(entry.getLocalHeaderOffset(), LOCAL_FILE_HEADER_SIZE);
|
||||||
|
long nameLength = Bytes.littleEndianValue(localHeader, 26, 2);
|
||||||
|
long extraLength = Bytes.littleEndianValue(localHeader, 28, 2);
|
||||||
|
return data.getSubsection(entry.getLocalHeaderOffset() + LOCAL_FILE_HEADER_SIZE + nameLength + extraLength,
|
||||||
|
entry.getCompressedSize());
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T extends FileHeader> T getEntry(CharSequence name, Class<T> type, boolean cacheEntry) {
|
||||||
|
T entry = doGetEntry(name, type, cacheEntry, null);
|
||||||
|
if (!isMetaInfEntry(name) && isMultiReleaseJar()) {
|
||||||
|
int version = RUNTIME_VERSION;
|
||||||
|
AsciiBytes nameAlias = (entry instanceof JarEntry jarEntry) ? jarEntry.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);
|
||||||
|
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, nameAlias);
|
||||||
|
}
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T extends FileHeader> T getEntry(int hashCode, CharSequence name, char suffix, Class<T> 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, nameAlias);
|
||||||
|
if (entry.hasName(name, suffix)) {
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private <T extends FileHeader> T getEntry(int index, Class<T> type, boolean cacheEntry, AsciiBytes nameAlias) {
|
||||||
|
try {
|
||||||
|
long offset = this.centralDirectoryOffsets.get(index);
|
||||||
|
FileHeader cached = this.entriesCache.get(index);
|
||||||
|
FileHeader entry = (cached != null) ? cached
|
||||||
|
: CentralDirectoryFileHeader.fromRandomAccessData(this.centralDirectoryData, offset, this.filter);
|
||||||
|
if (CentralDirectoryFileHeader.class.equals(entry.getClass()) && type.equals(JarEntry.class)) {
|
||||||
|
entry = new JarEntry(this.jarFile, index, (CentralDirectoryFileHeader) entry, nameAlias);
|
||||||
|
}
|
||||||
|
if (cacheEntry && cached != entry) {
|
||||||
|
this.entriesCache.put(index, entry);
|
||||||
|
}
|
||||||
|
return (T) entry;
|
||||||
|
}
|
||||||
|
catch (IOException ex) {
|
||||||
|
throw new IllegalStateException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getFirstIndex(int hashCode) {
|
||||||
|
int index = Arrays.binarySearch(this.hashCodes, 0, this.size, hashCode);
|
||||||
|
if (index < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
while (index > 0 && this.hashCodes[index - 1] == hashCode) {
|
||||||
|
index--;
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearCache() {
|
||||||
|
this.entriesCache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private AsciiBytes applyFilter(AsciiBytes name) {
|
||||||
|
return (this.filter != null) ? this.filter.apply(name) : name;
|
||||||
|
}
|
||||||
|
|
||||||
|
JarEntryCertification getCertification(JarEntry entry) throws IOException {
|
||||||
|
JarEntryCertification[] certifications = this.certifications;
|
||||||
|
if (certifications == null) {
|
||||||
|
certifications = new JarEntryCertification[this.size];
|
||||||
|
// We fall back to use JarInputStream to obtain the certs. This isn't that
|
||||||
|
// fast, but hopefully doesn't happen too often.
|
||||||
|
try (JarInputStream certifiedJarStream = new JarInputStream(this.jarFile.getData().getInputStream())) {
|
||||||
|
java.util.jar.JarEntry certifiedEntry;
|
||||||
|
while ((certifiedEntry = certifiedJarStream.getNextJarEntry()) != null) {
|
||||||
|
// Entry must be closed to trigger a read and set entry certificates
|
||||||
|
certifiedJarStream.closeEntry();
|
||||||
|
int index = getEntryIndex(certifiedEntry.getName());
|
||||||
|
if (index != -1) {
|
||||||
|
certifications[index] = JarEntryCertification.from(certifiedEntry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.certifications = certifications;
|
||||||
|
}
|
||||||
|
JarEntryCertification certification = certifications[entry.getIndex()];
|
||||||
|
return (certification != null) ? certification : JarEntryCertification.NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getEntryIndex(CharSequence name) {
|
||||||
|
int hashCode = AsciiBytes.hashCode(name);
|
||||||
|
int index = getFirstIndex(hashCode);
|
||||||
|
while (index >= 0 && index < this.size && this.hashCodes[index] == hashCode) {
|
||||||
|
FileHeader candidate = getEntry(index, FileHeader.class, false, null);
|
||||||
|
if (candidate.hasName(name, NO_SUFFIX)) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void swap(int[] array, int i, int j) {
|
||||||
|
int temp = array[i];
|
||||||
|
array[i] = array[j];
|
||||||
|
array[j] = temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void swap(long[] array, int i, int j) {
|
||||||
|
long temp = array[i];
|
||||||
|
array[i] = array[j];
|
||||||
|
array[j] = temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterator for contained entries.
|
||||||
|
*/
|
||||||
|
private final class EntryIterator implements Iterator<JarEntry> {
|
||||||
|
|
||||||
|
private final Runnable validator;
|
||||||
|
|
||||||
|
private int index = 0;
|
||||||
|
|
||||||
|
private EntryIterator(Runnable validator) {
|
||||||
|
this.validator = validator;
|
||||||
|
validator.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasNext() {
|
||||||
|
this.validator.run();
|
||||||
|
return this.index < JarFileEntries.this.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JarEntry next() {
|
||||||
|
this.validator.run();
|
||||||
|
if (!hasNext()) {
|
||||||
|
throw new NoSuchElementException();
|
||||||
|
}
|
||||||
|
int entryIndex = JarFileEntries.this.positions[this.index];
|
||||||
|
this.index++;
|
||||||
|
return getEntry(entryIndex, JarEntry.class, false, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface to manage offsets to central directory records. Regular zip files are
|
||||||
|
* backed by an {@code int[]} based implementation, Zip64 files are backed by a
|
||||||
|
* {@code long[]} and will consume more memory.
|
||||||
|
*/
|
||||||
|
private interface Offsets {
|
||||||
|
|
||||||
|
void set(int index, long value);
|
||||||
|
|
||||||
|
long get(int index);
|
||||||
|
|
||||||
|
void swap(int i, int j);
|
||||||
|
|
||||||
|
static Offsets from(CentralDirectoryEndRecord endRecord) {
|
||||||
|
int size = endRecord.getNumberOfRecords();
|
||||||
|
return endRecord.isZip64() ? new Zip64Offsets(size) : new ZipOffsets(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link Offsets} implementation for regular zip files.
|
||||||
|
*/
|
||||||
|
private static final class ZipOffsets implements Offsets {
|
||||||
|
|
||||||
|
private final int[] offsets;
|
||||||
|
|
||||||
|
private ZipOffsets(int size) {
|
||||||
|
this.offsets = new int[size];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void swap(int i, int j) {
|
||||||
|
JarFileEntries.swap(this.offsets, i, j);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void set(int index, long value) {
|
||||||
|
this.offsets[index] = (int) value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long get(int index) {
|
||||||
|
return this.offsets[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link Offsets} implementation for zip64 files.
|
||||||
|
*/
|
||||||
|
private static final class Zip64Offsets implements Offsets {
|
||||||
|
|
||||||
|
private final long[] offsets;
|
||||||
|
|
||||||
|
private Zip64Offsets(int size) {
|
||||||
|
this.offsets = new long[size];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void swap(int i, int j) {
|
||||||
|
JarFileEntries.swap(this.offsets, i, j);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void set(int index, long value) {
|
||||||
|
this.offsets[index] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long get(int index) {
|
||||||
|
return this.offsets[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,126 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.jar;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.security.Permission;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.jar.JarEntry;
|
||||||
|
import java.util.jar.Manifest;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A wrapper used to create a copy of a {@link JarFile} so that it can be safely closed
|
||||||
|
* without closing the original.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class JarFileWrapper extends AbstractJarFile {
|
||||||
|
|
||||||
|
private final JarFile parent;
|
||||||
|
|
||||||
|
JarFileWrapper(JarFile parent) throws IOException {
|
||||||
|
super(parent.getRootJarFile().getFile());
|
||||||
|
this.parent = parent;
|
||||||
|
super.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
URL getUrl() throws MalformedURLException {
|
||||||
|
return this.parent.getUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
JarFileType getType() {
|
||||||
|
return this.parent.getType();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
Permission getPermission() {
|
||||||
|
return this.parent.getPermission();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Manifest getManifest() throws IOException {
|
||||||
|
return this.parent.getManifest();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Enumeration<JarEntry> entries() {
|
||||||
|
return this.parent.entries();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Stream<JarEntry> stream() {
|
||||||
|
return this.parent.stream();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JarEntry getJarEntry(String name) {
|
||||||
|
return this.parent.getJarEntry(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ZipEntry getEntry(String name) {
|
||||||
|
return this.parent.getEntry(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
InputStream getInputStream() throws IOException {
|
||||||
|
return this.parent.getInputStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized InputStream getInputStream(ZipEntry ze) throws IOException {
|
||||||
|
return this.parent.getInputStream(ze);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getComment() {
|
||||||
|
return this.parent.getComment();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int size() {
|
||||||
|
return this.parent.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return this.parent.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return this.parent.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
static JarFile unwrap(java.util.jar.JarFile jarFile) {
|
||||||
|
if (jarFile instanceof JarFile file) {
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
if (jarFile instanceof JarFileWrapper wrapper) {
|
||||||
|
return unwrap(wrapper.parent);
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Not a JarFile or Wrapper");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,393 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.jar;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.URLConnection;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.net.URLStreamHandler;
|
||||||
|
import java.security.Permission;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link java.net.JarURLConnection} used to support {@link JarFile#getUrl()}.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Rostyslav Dudka
|
||||||
|
*/
|
||||||
|
final class JarURLConnection extends java.net.JarURLConnection {
|
||||||
|
|
||||||
|
private static final ThreadLocal<Boolean> useFastExceptions = new ThreadLocal<>();
|
||||||
|
|
||||||
|
private static final FileNotFoundException FILE_NOT_FOUND_EXCEPTION = new FileNotFoundException(
|
||||||
|
"Jar file or entry not found");
|
||||||
|
|
||||||
|
private static final IllegalStateException NOT_FOUND_CONNECTION_EXCEPTION = new IllegalStateException(
|
||||||
|
FILE_NOT_FOUND_EXCEPTION);
|
||||||
|
|
||||||
|
private static final String SEPARATOR = "!/";
|
||||||
|
|
||||||
|
private static final URL EMPTY_JAR_URL;
|
||||||
|
|
||||||
|
static {
|
||||||
|
try {
|
||||||
|
EMPTY_JAR_URL = new URL("jar:", null, 0, "file:!/", new URLStreamHandler() {
|
||||||
|
@Override
|
||||||
|
protected URLConnection openConnection(URL u) throws IOException {
|
||||||
|
// Stub URLStreamHandler to prevent the wrong JAR Handler from being
|
||||||
|
// Instantiated and cached.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (MalformedURLException ex) {
|
||||||
|
throw new IllegalStateException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final JarEntryName EMPTY_JAR_ENTRY_NAME = new JarEntryName(new StringSequence(""));
|
||||||
|
|
||||||
|
private static final JarURLConnection NOT_FOUND_CONNECTION = JarURLConnection.notFound();
|
||||||
|
|
||||||
|
private final AbstractJarFile jarFile;
|
||||||
|
|
||||||
|
private Permission permission;
|
||||||
|
|
||||||
|
private URL jarFileUrl;
|
||||||
|
|
||||||
|
private final JarEntryName jarEntryName;
|
||||||
|
|
||||||
|
private java.util.jar.JarEntry jarEntry;
|
||||||
|
|
||||||
|
private JarURLConnection(URL url, AbstractJarFile jarFile, JarEntryName jarEntryName) throws IOException {
|
||||||
|
// What we pass to super is ultimately ignored
|
||||||
|
super(EMPTY_JAR_URL);
|
||||||
|
this.url = url;
|
||||||
|
this.jarFile = jarFile;
|
||||||
|
this.jarEntryName = jarEntryName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void connect() throws IOException {
|
||||||
|
if (this.jarFile == null) {
|
||||||
|
throw FILE_NOT_FOUND_EXCEPTION;
|
||||||
|
}
|
||||||
|
if (!this.jarEntryName.isEmpty() && this.jarEntry == null) {
|
||||||
|
this.jarEntry = this.jarFile.getJarEntry(getEntryName());
|
||||||
|
if (this.jarEntry == null) {
|
||||||
|
throwFileNotFound(this.jarEntryName, this.jarFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.connected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public java.util.jar.JarFile getJarFile() throws IOException {
|
||||||
|
connect();
|
||||||
|
return this.jarFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public URL getJarFileURL() {
|
||||||
|
if (this.jarFile == null) {
|
||||||
|
throw NOT_FOUND_CONNECTION_EXCEPTION;
|
||||||
|
}
|
||||||
|
if (this.jarFileUrl == null) {
|
||||||
|
this.jarFileUrl = buildJarFileUrl();
|
||||||
|
}
|
||||||
|
return this.jarFileUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
private URL buildJarFileUrl() {
|
||||||
|
try {
|
||||||
|
String spec = this.jarFile.getUrl().getFile();
|
||||||
|
if (spec.endsWith(SEPARATOR)) {
|
||||||
|
spec = spec.substring(0, spec.length() - SEPARATOR.length());
|
||||||
|
}
|
||||||
|
if (!spec.contains(SEPARATOR)) {
|
||||||
|
return new URL(spec);
|
||||||
|
}
|
||||||
|
return new URL("jar:" + spec);
|
||||||
|
}
|
||||||
|
catch (MalformedURLException ex) {
|
||||||
|
throw new IllegalStateException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public java.util.jar.JarEntry getJarEntry() throws IOException {
|
||||||
|
if (this.jarEntryName == null || this.jarEntryName.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
connect();
|
||||||
|
return this.jarEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getEntryName() {
|
||||||
|
if (this.jarFile == null) {
|
||||||
|
throw NOT_FOUND_CONNECTION_EXCEPTION;
|
||||||
|
}
|
||||||
|
return this.jarEntryName.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream getInputStream() throws IOException {
|
||||||
|
if (this.jarFile == null) {
|
||||||
|
throw FILE_NOT_FOUND_EXCEPTION;
|
||||||
|
}
|
||||||
|
if (this.jarEntryName.isEmpty() && this.jarFile.getType() == JarFile.JarFileType.DIRECT) {
|
||||||
|
throw new IOException("no entry name specified");
|
||||||
|
}
|
||||||
|
connect();
|
||||||
|
InputStream inputStream = (this.jarEntryName.isEmpty() ? this.jarFile.getInputStream()
|
||||||
|
: this.jarFile.getInputStream(this.jarEntry));
|
||||||
|
if (inputStream == null) {
|
||||||
|
throwFileNotFound(this.jarEntryName, this.jarFile);
|
||||||
|
}
|
||||||
|
return inputStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void throwFileNotFound(Object entry, AbstractJarFile jarFile) throws FileNotFoundException {
|
||||||
|
if (Boolean.TRUE.equals(useFastExceptions.get())) {
|
||||||
|
throw FILE_NOT_FOUND_EXCEPTION;
|
||||||
|
}
|
||||||
|
throw new FileNotFoundException("JAR entry " + entry + " not found in " + jarFile.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getContentLength() {
|
||||||
|
long length = getContentLengthLong();
|
||||||
|
if (length > Integer.MAX_VALUE) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return (int) length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getContentLengthLong() {
|
||||||
|
if (this.jarFile == null) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (this.jarEntryName.isEmpty()) {
|
||||||
|
return this.jarFile.size();
|
||||||
|
}
|
||||||
|
java.util.jar.JarEntry entry = getJarEntry();
|
||||||
|
return (entry != null) ? (int) entry.getSize() : -1;
|
||||||
|
}
|
||||||
|
catch (IOException ex) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getContent() throws IOException {
|
||||||
|
connect();
|
||||||
|
return this.jarEntryName.isEmpty() ? this.jarFile : super.getContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getContentType() {
|
||||||
|
return (this.jarEntryName != null) ? this.jarEntryName.getContentType() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Permission getPermission() throws IOException {
|
||||||
|
if (this.jarFile == null) {
|
||||||
|
throw FILE_NOT_FOUND_EXCEPTION;
|
||||||
|
}
|
||||||
|
if (this.permission == null) {
|
||||||
|
this.permission = this.jarFile.getPermission();
|
||||||
|
}
|
||||||
|
return this.permission;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getLastModified() {
|
||||||
|
if (this.jarFile == null || this.jarEntryName.isEmpty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
java.util.jar.JarEntry entry = getJarEntry();
|
||||||
|
return (entry != null) ? entry.getTime() : 0;
|
||||||
|
}
|
||||||
|
catch (IOException ex) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void setUseFastExceptions(boolean useFastExceptions) {
|
||||||
|
JarURLConnection.useFastExceptions.set(useFastExceptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
static JarURLConnection get(URL url, JarFile jarFile) throws IOException {
|
||||||
|
StringSequence spec = new StringSequence(url.getFile());
|
||||||
|
int index = indexOfRootSpec(spec, jarFile.getPathFromRoot());
|
||||||
|
if (index == -1) {
|
||||||
|
return (Boolean.TRUE.equals(useFastExceptions.get()) ? NOT_FOUND_CONNECTION
|
||||||
|
: new JarURLConnection(url, null, EMPTY_JAR_ENTRY_NAME));
|
||||||
|
}
|
||||||
|
int separator;
|
||||||
|
while ((separator = spec.indexOf(SEPARATOR, index)) > 0) {
|
||||||
|
JarEntryName entryName = JarEntryName.get(spec.subSequence(index, separator));
|
||||||
|
JarEntry jarEntry = jarFile.getJarEntry(entryName.toCharSequence());
|
||||||
|
if (jarEntry == null) {
|
||||||
|
return JarURLConnection.notFound(jarFile, entryName);
|
||||||
|
}
|
||||||
|
jarFile = jarFile.getNestedJarFile(jarEntry);
|
||||||
|
index = separator + SEPARATOR.length();
|
||||||
|
}
|
||||||
|
JarEntryName jarEntryName = JarEntryName.get(spec, index);
|
||||||
|
if (Boolean.TRUE.equals(useFastExceptions.get()) && !jarEntryName.isEmpty()
|
||||||
|
&& !jarFile.containsEntry(jarEntryName.toString())) {
|
||||||
|
return NOT_FOUND_CONNECTION;
|
||||||
|
}
|
||||||
|
return new JarURLConnection(url, jarFile.getWrapper(), jarEntryName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int indexOfRootSpec(StringSequence file, String pathFromRoot) {
|
||||||
|
int separatorIndex = file.indexOf(SEPARATOR);
|
||||||
|
if (separatorIndex < 0 || !file.startsWith(pathFromRoot, separatorIndex)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return separatorIndex + SEPARATOR.length() + pathFromRoot.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JarURLConnection notFound() {
|
||||||
|
try {
|
||||||
|
return notFound(null, null);
|
||||||
|
}
|
||||||
|
catch (IOException ex) {
|
||||||
|
throw new IllegalStateException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JarURLConnection notFound(JarFile jarFile, JarEntryName jarEntryName) throws IOException {
|
||||||
|
if (Boolean.TRUE.equals(useFastExceptions.get())) {
|
||||||
|
return NOT_FOUND_CONNECTION;
|
||||||
|
}
|
||||||
|
return new JarURLConnection(null, jarFile, jarEntryName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A JarEntryName parsed from a URL String.
|
||||||
|
*/
|
||||||
|
static class JarEntryName {
|
||||||
|
|
||||||
|
private final StringSequence name;
|
||||||
|
|
||||||
|
private String contentType;
|
||||||
|
|
||||||
|
JarEntryName(StringSequence spec) {
|
||||||
|
this.name = decode(spec);
|
||||||
|
}
|
||||||
|
|
||||||
|
private StringSequence decode(StringSequence source) {
|
||||||
|
if (source.isEmpty() || (source.indexOf('%') < 0)) {
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
ByteArrayOutputStream bos = new ByteArrayOutputStream(source.length());
|
||||||
|
write(source.toString(), bos);
|
||||||
|
// AsciiBytes is what is used to store the JarEntries so make it symmetric
|
||||||
|
return new StringSequence(AsciiBytes.toString(bos.toByteArray()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void write(String source, ByteArrayOutputStream outputStream) {
|
||||||
|
int length = source.length();
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
int c = source.charAt(i);
|
||||||
|
if (c > 127) {
|
||||||
|
try {
|
||||||
|
String encoded = URLEncoder.encode(String.valueOf((char) c), "UTF-8");
|
||||||
|
write(encoded, outputStream);
|
||||||
|
}
|
||||||
|
catch (UnsupportedEncodingException ex) {
|
||||||
|
throw new IllegalStateException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (c == '%') {
|
||||||
|
if ((i + 2) >= length) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Invalid encoded sequence \"" + source.substring(i) + "\"");
|
||||||
|
}
|
||||||
|
c = decodeEscapeSequence(source, i);
|
||||||
|
i += 2;
|
||||||
|
}
|
||||||
|
outputStream.write(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private char decodeEscapeSequence(String source, int i) {
|
||||||
|
int hi = Character.digit(source.charAt(i + 1), 16);
|
||||||
|
int lo = Character.digit(source.charAt(i + 2), 16);
|
||||||
|
if (hi == -1 || lo == -1) {
|
||||||
|
throw new IllegalArgumentException("Invalid encoded sequence \"" + source.substring(i) + "\"");
|
||||||
|
}
|
||||||
|
return ((char) ((hi << 4) + lo));
|
||||||
|
}
|
||||||
|
|
||||||
|
CharSequence toCharSequence() {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return this.name.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isEmpty() {
|
||||||
|
return this.name.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
String getContentType() {
|
||||||
|
if (this.contentType == null) {
|
||||||
|
this.contentType = deduceContentType();
|
||||||
|
}
|
||||||
|
return this.contentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String deduceContentType() {
|
||||||
|
// Guess the content type, don't bother with streams as mark is not supported
|
||||||
|
String type = isEmpty() ? "x-java/jar" : null;
|
||||||
|
type = (type != null) ? type : guessContentTypeFromName(toString());
|
||||||
|
type = (type != null) ? type : "content/unknown";
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
static JarEntryName get(StringSequence spec) {
|
||||||
|
return get(spec, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static JarEntryName get(StringSequence spec, int beginIndex) {
|
||||||
|
if (spec.length() <= beginIndex) {
|
||||||
|
return EMPTY_JAR_ENTRY_NAME;
|
||||||
|
}
|
||||||
|
return new JarEntryName(spec.subSequence(beginIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,157 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.jar;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link CharSequence} backed by a single shared {@link String}. Unlike a regular
|
||||||
|
* {@link String}, {@link #subSequence(int, int)} operations will not copy the underlying
|
||||||
|
* character array.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
final class StringSequence implements CharSequence {
|
||||||
|
|
||||||
|
private final String source;
|
||||||
|
|
||||||
|
private final int start;
|
||||||
|
|
||||||
|
private final int end;
|
||||||
|
|
||||||
|
private int hash;
|
||||||
|
|
||||||
|
StringSequence(String source) {
|
||||||
|
this(source, 0, (source != null) ? source.length() : -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
StringSequence(String source, int start, int end) {
|
||||||
|
Objects.requireNonNull(source, "Source must not be null");
|
||||||
|
if (start < 0) {
|
||||||
|
throw new StringIndexOutOfBoundsException(start);
|
||||||
|
}
|
||||||
|
if (end > source.length()) {
|
||||||
|
throw new StringIndexOutOfBoundsException(end);
|
||||||
|
}
|
||||||
|
this.source = source;
|
||||||
|
this.start = start;
|
||||||
|
this.end = end;
|
||||||
|
}
|
||||||
|
|
||||||
|
StringSequence subSequence(int start) {
|
||||||
|
return subSequence(start, length());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public StringSequence subSequence(int start, int end) {
|
||||||
|
int subSequenceStart = this.start + start;
|
||||||
|
int subSequenceEnd = this.start + end;
|
||||||
|
if (subSequenceStart > this.end) {
|
||||||
|
throw new StringIndexOutOfBoundsException(start);
|
||||||
|
}
|
||||||
|
if (subSequenceEnd > this.end) {
|
||||||
|
throw new StringIndexOutOfBoundsException(end);
|
||||||
|
}
|
||||||
|
if (start == 0 && subSequenceEnd == this.end) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
return new StringSequence(this.source, subSequenceStart, subSequenceEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@code true} if the sequence is empty. Public to be compatible with JDK 15.
|
||||||
|
* @return {@code true} if {@link #length()} is {@code 0}, otherwise {@code false}
|
||||||
|
*/
|
||||||
|
public boolean isEmpty() {
|
||||||
|
return length() == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int length() {
|
||||||
|
return this.end - this.start;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public char charAt(int index) {
|
||||||
|
return this.source.charAt(this.start + index);
|
||||||
|
}
|
||||||
|
|
||||||
|
int indexOf(char ch) {
|
||||||
|
return this.source.indexOf(ch, this.start) - this.start;
|
||||||
|
}
|
||||||
|
|
||||||
|
int indexOf(String str) {
|
||||||
|
return this.source.indexOf(str, this.start) - this.start;
|
||||||
|
}
|
||||||
|
|
||||||
|
int indexOf(String str, int fromIndex) {
|
||||||
|
return this.source.indexOf(str, this.start + fromIndex) - this.start;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean startsWith(String prefix) {
|
||||||
|
return startsWith(prefix, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean startsWith(String prefix, int offset) {
|
||||||
|
int prefixLength = prefix.length();
|
||||||
|
int length = length();
|
||||||
|
if (length - prefixLength - offset < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this.source.startsWith(prefix, this.start + offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (this == obj) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!(obj instanceof CharSequence other)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int n = length();
|
||||||
|
if (n != other.length()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int i = 0;
|
||||||
|
while (n-- != 0) {
|
||||||
|
if (charAt(i) != other.charAt(i)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
int hash = this.hash;
|
||||||
|
if (hash == 0 && length() > 0) {
|
||||||
|
for (int i = this.start; i < this.end; i++) {
|
||||||
|
hash = 31 * hash + this.source.charAt(i);
|
||||||
|
}
|
||||||
|
this.hash = hash;
|
||||||
|
}
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return this.source.substring(this.start, this.end);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.jar;
|
||||||
|
|
||||||
|
import java.io.EOFException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.zip.Inflater;
|
||||||
|
import java.util.zip.InflaterInputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link InflaterInputStream} that supports the writing of an extra "dummy" byte (which
|
||||||
|
* is required with JDK 6) and returns accurate available() results.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class ZipInflaterInputStream extends InflaterInputStream {
|
||||||
|
|
||||||
|
private int available;
|
||||||
|
|
||||||
|
private boolean extraBytesWritten;
|
||||||
|
|
||||||
|
ZipInflaterInputStream(InputStream inputStream, int size) {
|
||||||
|
super(inputStream, new Inflater(true), getInflaterBufferSize(size));
|
||||||
|
this.available = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int available() throws IOException {
|
||||||
|
if (this.available < 0) {
|
||||||
|
return super.available();
|
||||||
|
}
|
||||||
|
return this.available;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(byte[] b, int off, int len) throws IOException {
|
||||||
|
int result = super.read(b, off, len);
|
||||||
|
if (result != -1) {
|
||||||
|
this.available -= result;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
super.close();
|
||||||
|
this.inf.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void fill() throws IOException {
|
||||||
|
try {
|
||||||
|
super.fill();
|
||||||
|
}
|
||||||
|
catch (EOFException ex) {
|
||||||
|
if (this.extraBytesWritten) {
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
this.len = 1;
|
||||||
|
this.buf[0] = 0x0;
|
||||||
|
this.extraBytesWritten = true;
|
||||||
|
this.inf.setInput(this.buf, 0, this.len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getInflaterBufferSize(long size) {
|
||||||
|
size += 2; // inflater likes some space
|
||||||
|
size = (size > 65536) ? 8192 : size;
|
||||||
|
size = (size <= 0) ? 4096 : size;
|
||||||
|
return (int) size;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Support for loading and manipulating JAR/WAR files.
|
||||||
|
*/
|
||||||
|
package org.springframework.boot.loader.jar;
|
@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.jarmode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface registered in {@code spring.factories} to provides extended 'jarmode'
|
||||||
|
* support.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @since 2.3.0
|
||||||
|
*/
|
||||||
|
public interface JarMode {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns if this accepts and can run the given mode.
|
||||||
|
* @param mode the mode to check
|
||||||
|
* @return if this instance accepts the mode
|
||||||
|
*/
|
||||||
|
boolean accepts(String mode);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the jar in the given mode.
|
||||||
|
* @param mode the mode to use
|
||||||
|
* @param args any program arguments
|
||||||
|
*/
|
||||||
|
void run(String mode, String[] args);
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.jarmode;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.core.io.support.SpringFactoriesLoader;
|
||||||
|
import org.springframework.util.ClassUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delegate class used to launch the fat jar in a specific mode.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @since 2.3.0
|
||||||
|
*/
|
||||||
|
public final class JarModeLauncher {
|
||||||
|
|
||||||
|
static final String DISABLE_SYSTEM_EXIT = JarModeLauncher.class.getName() + ".DISABLE_SYSTEM_EXIT";
|
||||||
|
|
||||||
|
private JarModeLauncher() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
String mode = System.getProperty("jarmode");
|
||||||
|
List<JarMode> candidates = SpringFactoriesLoader.loadFactories(JarMode.class,
|
||||||
|
ClassUtils.getDefaultClassLoader());
|
||||||
|
for (JarMode candidate : candidates) {
|
||||||
|
if (candidate.accepts(mode)) {
|
||||||
|
candidate.run(mode, args);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
System.err.println("Unsupported jarmode '" + mode + "'");
|
||||||
|
if (!Boolean.getBoolean(DISABLE_SYSTEM_EXIT)) {
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.jarmode;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link JarMode} for testing.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class TestJarMode implements JarMode {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean accepts(String mode) {
|
||||||
|
return "test".equals(mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run(String mode, String[] args) {
|
||||||
|
System.out.println("running in " + mode + " jar mode " + Arrays.asList(args));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Support for launching the JAR using jarmode.
|
||||||
|
*
|
||||||
|
* @see org.springframework.boot.loader.jarmode.JarModeLauncher
|
||||||
|
*/
|
||||||
|
package org.springframework.boot.loader.jarmode;
|
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.launch;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repackaged {@link org.springframework.boot.loader.JarLauncher}.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @since 3.2.0
|
||||||
|
*/
|
||||||
|
public final class JarLauncher {
|
||||||
|
|
||||||
|
private JarLauncher() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
org.springframework.boot.loader.JarLauncher.main(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.launch;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repackaged {@link org.springframework.boot.loader.PropertiesLauncher}.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @since 3.2.0
|
||||||
|
*/
|
||||||
|
public final class PropertiesLauncher {
|
||||||
|
|
||||||
|
private PropertiesLauncher() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
org.springframework.boot.loader.PropertiesLauncher.main(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.launch;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repackaged {@link org.springframework.boot.loader.WarLauncher}.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @since 3.2.0
|
||||||
|
*/
|
||||||
|
public final class WarLauncher {
|
||||||
|
|
||||||
|
private WarLauncher() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
org.springframework.boot.loader.WarLauncher.main(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repackaged launcher classes.
|
||||||
|
*
|
||||||
|
* @see org.springframework.boot.loader.launch.JarLauncher
|
||||||
|
* @see org.springframework.boot.loader.launch.WarLauncher
|
||||||
|
*/
|
||||||
|
package org.springframework.boot.loader.launch;
|
@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* System that allows self-contained JAR/WAR archives to be launched using
|
||||||
|
* {@code java -jar}. Archives can include nested packaged dependency JARs (there is no
|
||||||
|
* need to create shade style jars) and are executed without unpacking. The only
|
||||||
|
* constraint is that nested JARs must be stored in the archive uncompressed.
|
||||||
|
*
|
||||||
|
* @see org.springframework.boot.loader.JarLauncher
|
||||||
|
* @see org.springframework.boot.loader.WarLauncher
|
||||||
|
*/
|
||||||
|
package org.springframework.boot.loader;
|
@ -0,0 +1,232 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.util;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Properties;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class for resolving placeholders in texts. Usually applied to file paths.
|
||||||
|
* <p>
|
||||||
|
* A text may contain {@code $ ...} placeholders, to be resolved as system properties:
|
||||||
|
* e.g. {@code $ user.dir}. Default values can be supplied using the ":" separator between
|
||||||
|
* key and value.
|
||||||
|
* <p>
|
||||||
|
* Adapted from Spring.
|
||||||
|
*
|
||||||
|
* @author Juergen Hoeller
|
||||||
|
* @author Rob Harrop
|
||||||
|
* @author Dave Syer
|
||||||
|
* @since 1.0.0
|
||||||
|
* @see System#getProperty(String)
|
||||||
|
*/
|
||||||
|
public abstract class SystemPropertyUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefix for system property placeholders: "${".
|
||||||
|
*/
|
||||||
|
public static final String PLACEHOLDER_PREFIX = "${";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Suffix for system property placeholders: "}".
|
||||||
|
*/
|
||||||
|
public static final String PLACEHOLDER_SUFFIX = "}";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Value separator for system property placeholders: ":".
|
||||||
|
*/
|
||||||
|
public static final String VALUE_SEPARATOR = ":";
|
||||||
|
|
||||||
|
private static final String SIMPLE_PREFIX = PLACEHOLDER_PREFIX.substring(1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve ${...} placeholders in the given text, replacing them with corresponding
|
||||||
|
* system property values.
|
||||||
|
* @param text the String to resolve
|
||||||
|
* @return the resolved String
|
||||||
|
* @throws IllegalArgumentException if there is an unresolvable placeholder
|
||||||
|
* @see #PLACEHOLDER_PREFIX
|
||||||
|
* @see #PLACEHOLDER_SUFFIX
|
||||||
|
*/
|
||||||
|
public static String resolvePlaceholders(String text) {
|
||||||
|
if (text == null) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
return parseStringValue(null, text, text, new HashSet<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve ${...} placeholders in the given text, replacing them with corresponding
|
||||||
|
* system property values.
|
||||||
|
* @param properties a properties instance to use in addition to System
|
||||||
|
* @param text the String to resolve
|
||||||
|
* @return the resolved String
|
||||||
|
* @throws IllegalArgumentException if there is an unresolvable placeholder
|
||||||
|
* @see #PLACEHOLDER_PREFIX
|
||||||
|
* @see #PLACEHOLDER_SUFFIX
|
||||||
|
*/
|
||||||
|
public static String resolvePlaceholders(Properties properties, String text) {
|
||||||
|
if (text == null) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
return parseStringValue(properties, text, text, new HashSet<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String parseStringValue(Properties properties, String value, String current,
|
||||||
|
Set<String> visitedPlaceholders) {
|
||||||
|
|
||||||
|
StringBuilder buf = new StringBuilder(current);
|
||||||
|
|
||||||
|
int startIndex = current.indexOf(PLACEHOLDER_PREFIX);
|
||||||
|
while (startIndex != -1) {
|
||||||
|
int endIndex = findPlaceholderEndIndex(buf, startIndex);
|
||||||
|
if (endIndex != -1) {
|
||||||
|
String placeholder = buf.substring(startIndex + PLACEHOLDER_PREFIX.length(), endIndex);
|
||||||
|
String originalPlaceholder = placeholder;
|
||||||
|
if (!visitedPlaceholders.add(originalPlaceholder)) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Circular placeholder reference '" + originalPlaceholder + "' in property definitions");
|
||||||
|
}
|
||||||
|
// Recursive invocation, parsing placeholders contained in the
|
||||||
|
// placeholder
|
||||||
|
// key.
|
||||||
|
placeholder = parseStringValue(properties, value, placeholder, visitedPlaceholders);
|
||||||
|
// Now obtain the value for the fully resolved key...
|
||||||
|
String propVal = resolvePlaceholder(properties, value, placeholder);
|
||||||
|
if (propVal == null) {
|
||||||
|
int separatorIndex = placeholder.indexOf(VALUE_SEPARATOR);
|
||||||
|
if (separatorIndex != -1) {
|
||||||
|
String actualPlaceholder = placeholder.substring(0, separatorIndex);
|
||||||
|
String defaultValue = placeholder.substring(separatorIndex + VALUE_SEPARATOR.length());
|
||||||
|
propVal = resolvePlaceholder(properties, value, actualPlaceholder);
|
||||||
|
if (propVal == null) {
|
||||||
|
propVal = defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (propVal != null) {
|
||||||
|
// Recursive invocation, parsing placeholders contained in the
|
||||||
|
// previously resolved placeholder value.
|
||||||
|
propVal = parseStringValue(properties, value, propVal, visitedPlaceholders);
|
||||||
|
buf.replace(startIndex, endIndex + PLACEHOLDER_SUFFIX.length(), propVal);
|
||||||
|
startIndex = buf.indexOf(PLACEHOLDER_PREFIX, startIndex + propVal.length());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Proceed with unprocessed value.
|
||||||
|
startIndex = buf.indexOf(PLACEHOLDER_PREFIX, endIndex + PLACEHOLDER_SUFFIX.length());
|
||||||
|
}
|
||||||
|
visitedPlaceholders.remove(originalPlaceholder);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
startIndex = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String resolvePlaceholder(Properties properties, String text, String placeholderName) {
|
||||||
|
String propVal = getProperty(placeholderName, null, text);
|
||||||
|
if (propVal != null) {
|
||||||
|
return propVal;
|
||||||
|
}
|
||||||
|
return (properties != null) ? properties.getProperty(placeholderName) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getProperty(String key) {
|
||||||
|
return getProperty(key, null, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getProperty(String key, String defaultValue) {
|
||||||
|
return getProperty(key, defaultValue, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search the System properties and environment variables for a value with the
|
||||||
|
* provided key. Environment variables in {@code UPPER_CASE} style are allowed where
|
||||||
|
* System properties would normally be {@code lower.case}.
|
||||||
|
* @param key the key to resolve
|
||||||
|
* @param defaultValue the default value
|
||||||
|
* @param text optional extra context for an error message if the key resolution fails
|
||||||
|
* (e.g. if System properties are not accessible)
|
||||||
|
* @return a static property value or null of not found
|
||||||
|
*/
|
||||||
|
public static String getProperty(String key, String defaultValue, String text) {
|
||||||
|
try {
|
||||||
|
String propVal = System.getProperty(key);
|
||||||
|
if (propVal == null) {
|
||||||
|
// Fall back to searching the system environment.
|
||||||
|
propVal = System.getenv(key);
|
||||||
|
}
|
||||||
|
if (propVal == null) {
|
||||||
|
// Try with underscores.
|
||||||
|
String name = key.replace('.', '_');
|
||||||
|
propVal = System.getenv(name);
|
||||||
|
}
|
||||||
|
if (propVal == null) {
|
||||||
|
// Try uppercase with underscores as well.
|
||||||
|
String name = key.toUpperCase(Locale.ENGLISH).replace('.', '_');
|
||||||
|
propVal = System.getenv(name);
|
||||||
|
}
|
||||||
|
if (propVal != null) {
|
||||||
|
return propVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Throwable ex) {
|
||||||
|
System.err.println("Could not resolve key '" + key + "' in '" + text
|
||||||
|
+ "' as system property or in environment: " + ex);
|
||||||
|
}
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int findPlaceholderEndIndex(CharSequence buf, int startIndex) {
|
||||||
|
int index = startIndex + PLACEHOLDER_PREFIX.length();
|
||||||
|
int withinNestedPlaceholder = 0;
|
||||||
|
while (index < buf.length()) {
|
||||||
|
if (substringMatch(buf, index, PLACEHOLDER_SUFFIX)) {
|
||||||
|
if (withinNestedPlaceholder > 0) {
|
||||||
|
withinNestedPlaceholder--;
|
||||||
|
index = index + PLACEHOLDER_SUFFIX.length();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (substringMatch(buf, index, SIMPLE_PREFIX)) {
|
||||||
|
withinNestedPlaceholder++;
|
||||||
|
index = index + SIMPLE_PREFIX.length();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean substringMatch(CharSequence str, int index, CharSequence substring) {
|
||||||
|
for (int j = 0; j < substring.length(); j++) {
|
||||||
|
int i = index + j;
|
||||||
|
if (i >= str.length() || str.charAt(i) != substring.charAt(j)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utilities used by Spring Boot's JAR loading.
|
||||||
|
*/
|
||||||
|
package org.springframework.boot.loader.util;
|
@ -0,0 +1,149 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.io.Writer;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.jar.JarEntry;
|
||||||
|
import java.util.jar.JarFile;
|
||||||
|
import java.util.jar.JarOutputStream;
|
||||||
|
import java.util.jar.Manifest;
|
||||||
|
import java.util.zip.CRC32;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import org.springframework.boot.loader.archive.Archive;
|
||||||
|
import org.springframework.util.FileCopyUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for testing {@link ExecutableArchiveLauncher} implementations.
|
||||||
|
*
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Madhura Bhave
|
||||||
|
* @author Scott Frederick
|
||||||
|
*/
|
||||||
|
public abstract class AbstractExecutableArchiveLauncherTests {
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
File tempDir;
|
||||||
|
|
||||||
|
protected File createJarArchive(String name, String entryPrefix) throws IOException {
|
||||||
|
return createJarArchive(name, entryPrefix, false, Collections.emptyList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("resource")
|
||||||
|
protected File createJarArchive(String name, String entryPrefix, boolean indexed, List<String> extraLibs)
|
||||||
|
throws IOException {
|
||||||
|
return createJarArchive(name, null, entryPrefix, indexed, extraLibs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("resource")
|
||||||
|
protected File createJarArchive(String name, Manifest manifest, String entryPrefix, boolean indexed,
|
||||||
|
List<String> extraLibs) throws IOException {
|
||||||
|
File archive = new File(this.tempDir, name);
|
||||||
|
JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(archive));
|
||||||
|
if (manifest != null) {
|
||||||
|
jarOutputStream.putNextEntry(new JarEntry("META-INF/"));
|
||||||
|
jarOutputStream.putNextEntry(new JarEntry("META-INF/MANIFEST.MF"));
|
||||||
|
manifest.write(jarOutputStream);
|
||||||
|
jarOutputStream.closeEntry();
|
||||||
|
}
|
||||||
|
jarOutputStream.putNextEntry(new JarEntry(entryPrefix + "/"));
|
||||||
|
jarOutputStream.putNextEntry(new JarEntry(entryPrefix + "/classes/"));
|
||||||
|
jarOutputStream.putNextEntry(new JarEntry(entryPrefix + "/lib/"));
|
||||||
|
if (indexed) {
|
||||||
|
jarOutputStream.putNextEntry(new JarEntry(entryPrefix + "/classpath.idx"));
|
||||||
|
Writer writer = new OutputStreamWriter(jarOutputStream, StandardCharsets.UTF_8);
|
||||||
|
writer.write("- \"" + entryPrefix + "/lib/foo.jar\"\n");
|
||||||
|
writer.write("- \"" + entryPrefix + "/lib/bar.jar\"\n");
|
||||||
|
writer.write("- \"" + entryPrefix + "/lib/baz.jar\"\n");
|
||||||
|
writer.flush();
|
||||||
|
jarOutputStream.closeEntry();
|
||||||
|
}
|
||||||
|
addNestedJars(entryPrefix, "/lib/foo.jar", jarOutputStream);
|
||||||
|
addNestedJars(entryPrefix, "/lib/bar.jar", jarOutputStream);
|
||||||
|
addNestedJars(entryPrefix, "/lib/baz.jar", jarOutputStream);
|
||||||
|
for (String lib : extraLibs) {
|
||||||
|
addNestedJars(entryPrefix, "/lib/" + lib, jarOutputStream);
|
||||||
|
}
|
||||||
|
jarOutputStream.close();
|
||||||
|
return archive;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addNestedJars(String entryPrefix, String lib, JarOutputStream jarOutputStream) throws IOException {
|
||||||
|
JarEntry libFoo = new JarEntry(entryPrefix + lib);
|
||||||
|
libFoo.setMethod(ZipEntry.STORED);
|
||||||
|
ByteArrayOutputStream fooJarStream = new ByteArrayOutputStream();
|
||||||
|
new JarOutputStream(fooJarStream).close();
|
||||||
|
libFoo.setSize(fooJarStream.size());
|
||||||
|
CRC32 crc32 = new CRC32();
|
||||||
|
crc32.update(fooJarStream.toByteArray());
|
||||||
|
libFoo.setCrc(crc32.getValue());
|
||||||
|
jarOutputStream.putNextEntry(libFoo);
|
||||||
|
jarOutputStream.write(fooJarStream.toByteArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected File explode(File archive) throws IOException {
|
||||||
|
File exploded = new File(this.tempDir, "exploded");
|
||||||
|
exploded.mkdirs();
|
||||||
|
JarFile jarFile = new JarFile(archive);
|
||||||
|
Enumeration<JarEntry> entries = jarFile.entries();
|
||||||
|
while (entries.hasMoreElements()) {
|
||||||
|
JarEntry entry = entries.nextElement();
|
||||||
|
File entryFile = new File(exploded, entry.getName());
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
entryFile.mkdirs();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
FileCopyUtils.copy(jarFile.getInputStream(entry), new FileOutputStream(entryFile));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jarFile.close();
|
||||||
|
return exploded;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Set<URL> getUrls(List<Archive> archives) throws MalformedURLException {
|
||||||
|
Set<URL> urls = new LinkedHashSet<>(archives.size());
|
||||||
|
for (Archive archive : archives) {
|
||||||
|
urls.add(archive.getUrl());
|
||||||
|
}
|
||||||
|
return urls;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final URL toUrl(File file) {
|
||||||
|
try {
|
||||||
|
return file.toURI().toURL();
|
||||||
|
}
|
||||||
|
catch (MalformedURLException ex) {
|
||||||
|
throw new IllegalStateException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,109 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link ClassPathIndexFile}.
|
||||||
|
*
|
||||||
|
* @author Madhura Bhave
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class ClassPathIndexFileTests {
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
File temp;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void loadIfPossibleWhenRootIsNotFileReturnsNull() {
|
||||||
|
assertThatIllegalArgumentException()
|
||||||
|
.isThrownBy(() -> ClassPathIndexFile.loadIfPossible(new URL("https://example.com/file"), "test.idx"))
|
||||||
|
.withMessage("URL does not reference a file");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void loadIfPossibleWhenRootDoesNotExistReturnsNull() throws Exception {
|
||||||
|
File root = new File(this.temp, "missing");
|
||||||
|
assertThat(ClassPathIndexFile.loadIfPossible(root.toURI().toURL(), "test.idx")).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void loadIfPossibleWhenRootIsDirectoryThrowsException() throws Exception {
|
||||||
|
File root = new File(this.temp, "directory");
|
||||||
|
root.mkdirs();
|
||||||
|
assertThat(ClassPathIndexFile.loadIfPossible(root.toURI().toURL(), "test.idx")).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void loadIfPossibleReturnsInstance() throws Exception {
|
||||||
|
ClassPathIndexFile indexFile = copyAndLoadTestIndexFile();
|
||||||
|
assertThat(indexFile).isNotNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sizeReturnsNumberOfLines() throws Exception {
|
||||||
|
ClassPathIndexFile indexFile = copyAndLoadTestIndexFile();
|
||||||
|
assertThat(indexFile.size()).isEqualTo(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getUrlsReturnsUrls() throws Exception {
|
||||||
|
ClassPathIndexFile indexFile = copyAndLoadTestIndexFile();
|
||||||
|
List<URL> urls = indexFile.getUrls();
|
||||||
|
List<File> expected = new ArrayList<>();
|
||||||
|
expected.add(new File(this.temp, "BOOT-INF/layers/one/lib/a.jar"));
|
||||||
|
expected.add(new File(this.temp, "BOOT-INF/layers/one/lib/b.jar"));
|
||||||
|
expected.add(new File(this.temp, "BOOT-INF/layers/one/lib/c.jar"));
|
||||||
|
expected.add(new File(this.temp, "BOOT-INF/layers/two/lib/d.jar"));
|
||||||
|
expected.add(new File(this.temp, "BOOT-INF/layers/two/lib/e.jar"));
|
||||||
|
assertThat(urls).containsExactly(expected.stream().map(this::toUrl).toArray(URL[]::new));
|
||||||
|
}
|
||||||
|
|
||||||
|
private URL toUrl(File file) {
|
||||||
|
try {
|
||||||
|
return file.toURI().toURL();
|
||||||
|
}
|
||||||
|
catch (MalformedURLException ex) {
|
||||||
|
throw new IllegalStateException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ClassPathIndexFile copyAndLoadTestIndexFile() throws IOException {
|
||||||
|
copyTestIndexFile();
|
||||||
|
ClassPathIndexFile indexFile = ClassPathIndexFile.loadIfPossible(this.temp.toURI().toURL(), "test.idx");
|
||||||
|
return indexFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void copyTestIndexFile() throws IOException {
|
||||||
|
Files.copy(getClass().getResourceAsStream("classpath-index-file.idx"),
|
||||||
|
new File(this.temp, "test.idx").toPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,154 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.URLClassLoader;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.jar.Attributes;
|
||||||
|
import java.util.jar.Attributes.Name;
|
||||||
|
import java.util.jar.Manifest;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.boot.loader.archive.Archive;
|
||||||
|
import org.springframework.boot.loader.archive.ExplodedArchive;
|
||||||
|
import org.springframework.boot.loader.archive.JarFileArchive;
|
||||||
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
import org.springframework.core.test.tools.SourceFile;
|
||||||
|
import org.springframework.core.test.tools.TestCompiler;
|
||||||
|
import org.springframework.util.FileCopyUtils;
|
||||||
|
import org.springframework.util.function.ThrowingConsumer;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link JarLauncher}.
|
||||||
|
*
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Madhura Bhave
|
||||||
|
*/
|
||||||
|
class JarLauncherTests extends AbstractExecutableArchiveLauncherTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void explodedJarHasOnlyBootInfClassesAndContentsOfBootInfLibOnClasspath() throws Exception {
|
||||||
|
File explodedRoot = explode(createJarArchive("archive.jar", "BOOT-INF"));
|
||||||
|
JarLauncher launcher = new JarLauncher(new ExplodedArchive(explodedRoot, true));
|
||||||
|
List<Archive> archives = new ArrayList<>();
|
||||||
|
launcher.getClassPathArchivesIterator().forEachRemaining(archives::add);
|
||||||
|
assertThat(getUrls(archives)).containsExactlyInAnyOrder(getExpectedFileUrls(explodedRoot));
|
||||||
|
for (Archive archive : archives) {
|
||||||
|
archive.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void archivedJarHasOnlyBootInfClassesAndContentsOfBootInfLibOnClasspath() throws Exception {
|
||||||
|
File jarRoot = createJarArchive("archive.jar", "BOOT-INF");
|
||||||
|
try (JarFileArchive archive = new JarFileArchive(jarRoot)) {
|
||||||
|
JarLauncher launcher = new JarLauncher(archive);
|
||||||
|
List<Archive> classPathArchives = new ArrayList<>();
|
||||||
|
launcher.getClassPathArchivesIterator().forEachRemaining(classPathArchives::add);
|
||||||
|
assertThat(classPathArchives).hasSize(4);
|
||||||
|
assertThat(getUrls(classPathArchives)).containsOnly(
|
||||||
|
new URL("jar:" + jarRoot.toURI().toURL() + "!/BOOT-INF/classes!/"),
|
||||||
|
new URL("jar:" + jarRoot.toURI().toURL() + "!/BOOT-INF/lib/foo.jar!/"),
|
||||||
|
new URL("jar:" + jarRoot.toURI().toURL() + "!/BOOT-INF/lib/bar.jar!/"),
|
||||||
|
new URL("jar:" + jarRoot.toURI().toURL() + "!/BOOT-INF/lib/baz.jar!/"));
|
||||||
|
for (Archive classPathArchive : classPathArchives) {
|
||||||
|
classPathArchive.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void explodedJarShouldPreserveClasspathOrderWhenIndexPresent() throws Exception {
|
||||||
|
File explodedRoot = explode(createJarArchive("archive.jar", "BOOT-INF", true, Collections.emptyList()));
|
||||||
|
JarLauncher launcher = new JarLauncher(new ExplodedArchive(explodedRoot, true));
|
||||||
|
Iterator<Archive> archives = launcher.getClassPathArchivesIterator();
|
||||||
|
URLClassLoader classLoader = (URLClassLoader) launcher.createClassLoader(archives);
|
||||||
|
URL[] urls = classLoader.getURLs();
|
||||||
|
assertThat(urls).containsExactly(getExpectedFileUrls(explodedRoot));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void jarFilesPresentInBootInfLibsAndNotInClasspathIndexShouldBeAddedAfterBootInfClasses() throws Exception {
|
||||||
|
ArrayList<String> extraLibs = new ArrayList<>(Arrays.asList("extra-1.jar", "extra-2.jar"));
|
||||||
|
File explodedRoot = explode(createJarArchive("archive.jar", "BOOT-INF", true, extraLibs));
|
||||||
|
JarLauncher launcher = new JarLauncher(new ExplodedArchive(explodedRoot, true));
|
||||||
|
Iterator<Archive> archives = launcher.getClassPathArchivesIterator();
|
||||||
|
URLClassLoader classLoader = (URLClassLoader) launcher.createClassLoader(archives);
|
||||||
|
URL[] urls = classLoader.getURLs();
|
||||||
|
List<File> expectedFiles = getExpectedFilesWithExtraLibs(explodedRoot);
|
||||||
|
URL[] expectedFileUrls = expectedFiles.stream().map(this::toUrl).toArray(URL[]::new);
|
||||||
|
assertThat(urls).containsExactly(expectedFileUrls);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void explodedJarDefinedPackagesIncludeManifestAttributes() {
|
||||||
|
Manifest manifest = new Manifest();
|
||||||
|
Attributes attributes = manifest.getMainAttributes();
|
||||||
|
attributes.put(Name.MANIFEST_VERSION, "1.0");
|
||||||
|
attributes.put(Name.IMPLEMENTATION_TITLE, "test");
|
||||||
|
SourceFile sourceFile = SourceFile.of("explodedsample/ExampleClass.java",
|
||||||
|
new ClassPathResource("explodedsample/ExampleClass.txt"));
|
||||||
|
TestCompiler.forSystem().compile(sourceFile, ThrowingConsumer.of((compiled) -> {
|
||||||
|
File explodedRoot = explode(
|
||||||
|
createJarArchive("archive.jar", manifest, "BOOT-INF", true, Collections.emptyList()));
|
||||||
|
File target = new File(explodedRoot, "BOOT-INF/classes/explodedsample/ExampleClass.class");
|
||||||
|
target.getParentFile().mkdirs();
|
||||||
|
FileCopyUtils.copy(compiled.getClassLoader().getResourceAsStream("explodedsample/ExampleClass.class"),
|
||||||
|
new FileOutputStream(target));
|
||||||
|
JarLauncher launcher = new JarLauncher(new ExplodedArchive(explodedRoot, true));
|
||||||
|
Iterator<Archive> archives = launcher.getClassPathArchivesIterator();
|
||||||
|
URLClassLoader classLoader = (URLClassLoader) launcher.createClassLoader(archives);
|
||||||
|
Class<?> loaded = classLoader.loadClass("explodedsample.ExampleClass");
|
||||||
|
assertThat(loaded.getPackage().getImplementationTitle()).isEqualTo("test");
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final URL[] getExpectedFileUrls(File explodedRoot) {
|
||||||
|
return getExpectedFiles(explodedRoot).stream().map(this::toUrl).toArray(URL[]::new);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final List<File> getExpectedFiles(File parent) {
|
||||||
|
List<File> expected = new ArrayList<>();
|
||||||
|
expected.add(new File(parent, "BOOT-INF/classes"));
|
||||||
|
expected.add(new File(parent, "BOOT-INF/lib/foo.jar"));
|
||||||
|
expected.add(new File(parent, "BOOT-INF/lib/bar.jar"));
|
||||||
|
expected.add(new File(parent, "BOOT-INF/lib/baz.jar"));
|
||||||
|
return expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final List<File> getExpectedFilesWithExtraLibs(File parent) {
|
||||||
|
List<File> expected = new ArrayList<>();
|
||||||
|
expected.add(new File(parent, "BOOT-INF/classes"));
|
||||||
|
expected.add(new File(parent, "BOOT-INF/lib/extra-1.jar"));
|
||||||
|
expected.add(new File(parent, "BOOT-INF/lib/extra-2.jar"));
|
||||||
|
expected.add(new File(parent, "BOOT-INF/lib/foo.jar"));
|
||||||
|
expected.add(new File(parent, "BOOT-INF/lib/bar.jar"));
|
||||||
|
expected.add(new File(parent, "BOOT-INF/lib/baz.jar"));
|
||||||
|
return expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,111 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.JarURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.URLConnection;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import org.springframework.boot.loader.jar.JarFile;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link LaunchedURLClassLoader}.
|
||||||
|
*
|
||||||
|
* @author Dave Syer
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("resource")
|
||||||
|
class LaunchedURLClassLoaderTests {
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
File tempDir;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolveResourceFromArchive() throws Exception {
|
||||||
|
LaunchedURLClassLoader loader = new LaunchedURLClassLoader(
|
||||||
|
new URL[] { new URL("jar:file:src/test/resources/jars/app.jar!/") }, getClass().getClassLoader());
|
||||||
|
assertThat(loader.getResource("demo/Application.java")).isNotNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolveResourcesFromArchive() throws Exception {
|
||||||
|
LaunchedURLClassLoader loader = new LaunchedURLClassLoader(
|
||||||
|
new URL[] { new URL("jar:file:src/test/resources/jars/app.jar!/") }, getClass().getClassLoader());
|
||||||
|
assertThat(loader.getResources("demo/Application.java").hasMoreElements()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolveRootPathFromArchive() throws Exception {
|
||||||
|
LaunchedURLClassLoader loader = new LaunchedURLClassLoader(
|
||||||
|
new URL[] { new URL("jar:file:src/test/resources/jars/app.jar!/") }, getClass().getClassLoader());
|
||||||
|
assertThat(loader.getResource("")).isNotNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolveRootResourcesFromArchive() throws Exception {
|
||||||
|
LaunchedURLClassLoader loader = new LaunchedURLClassLoader(
|
||||||
|
new URL[] { new URL("jar:file:src/test/resources/jars/app.jar!/") }, getClass().getClassLoader());
|
||||||
|
assertThat(loader.getResources("").hasMoreElements()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolveFromNested() throws Exception {
|
||||||
|
File file = new File(this.tempDir, "test.jar");
|
||||||
|
TestJarCreator.createTestJar(file);
|
||||||
|
try (JarFile jarFile = new JarFile(file)) {
|
||||||
|
URL url = jarFile.getUrl();
|
||||||
|
try (LaunchedURLClassLoader loader = new LaunchedURLClassLoader(new URL[] { url }, null)) {
|
||||||
|
URL resource = loader.getResource("nested.jar!/3.dat");
|
||||||
|
assertThat(resource).hasToString(url + "nested.jar!/3.dat");
|
||||||
|
try (InputStream input = resource.openConnection().getInputStream()) {
|
||||||
|
assertThat(input.read()).isEqualTo(3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolveFromNestedWhileThreadIsInterrupted() throws Exception {
|
||||||
|
File file = new File(this.tempDir, "test.jar");
|
||||||
|
TestJarCreator.createTestJar(file);
|
||||||
|
try (JarFile jarFile = new JarFile(file)) {
|
||||||
|
URL url = jarFile.getUrl();
|
||||||
|
try (LaunchedURLClassLoader loader = new LaunchedURLClassLoader(new URL[] { url }, null)) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
URL resource = loader.getResource("nested.jar!/3.dat");
|
||||||
|
assertThat(resource).hasToString(url + "nested.jar!/3.dat");
|
||||||
|
URLConnection connection = resource.openConnection();
|
||||||
|
try (InputStream input = connection.getInputStream()) {
|
||||||
|
assertThat(input.read()).isEqualTo(3);
|
||||||
|
}
|
||||||
|
((JarURLConnection) connection).getJarFile().close();
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
Thread.interrupted();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,433 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.ref.SoftReference;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.URLClassLoader;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.jar.Attributes;
|
||||||
|
import java.util.jar.Manifest;
|
||||||
|
|
||||||
|
import org.assertj.core.api.Condition;
|
||||||
|
import org.awaitility.Awaitility;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import org.springframework.boot.loader.archive.Archive;
|
||||||
|
import org.springframework.boot.loader.archive.ExplodedArchive;
|
||||||
|
import org.springframework.boot.loader.archive.JarFileArchive;
|
||||||
|
import org.springframework.boot.loader.jar.Handler;
|
||||||
|
import org.springframework.boot.loader.jar.JarFile;
|
||||||
|
import org.springframework.boot.testsupport.system.CapturedOutput;
|
||||||
|
import org.springframework.boot.testsupport.system.OutputCaptureExtension;
|
||||||
|
import org.springframework.core.io.FileSystemResource;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
import org.springframework.util.FileCopyUtils;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||||
|
import static org.hamcrest.Matchers.containsString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link PropertiesLauncher}.
|
||||||
|
*
|
||||||
|
* @author Dave Syer
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
*/
|
||||||
|
@ExtendWith(OutputCaptureExtension.class)
|
||||||
|
class PropertiesLauncherTests {
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
File tempDir;
|
||||||
|
|
||||||
|
private PropertiesLauncher launcher;
|
||||||
|
|
||||||
|
private ClassLoader contextClassLoader;
|
||||||
|
|
||||||
|
private CapturedOutput output;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setup(CapturedOutput capturedOutput) throws Exception {
|
||||||
|
this.contextClassLoader = Thread.currentThread().getContextClassLoader();
|
||||||
|
clearHandlerCache();
|
||||||
|
System.setProperty("loader.home", new File("src/test/resources").getAbsolutePath());
|
||||||
|
this.output = capturedOutput;
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void close() throws Exception {
|
||||||
|
Thread.currentThread().setContextClassLoader(this.contextClassLoader);
|
||||||
|
System.clearProperty("loader.home");
|
||||||
|
System.clearProperty("loader.path");
|
||||||
|
System.clearProperty("loader.main");
|
||||||
|
System.clearProperty("loader.config.name");
|
||||||
|
System.clearProperty("loader.config.location");
|
||||||
|
System.clearProperty("loader.system");
|
||||||
|
System.clearProperty("loader.classLoader");
|
||||||
|
clearHandlerCache();
|
||||||
|
if (this.launcher != null) {
|
||||||
|
this.launcher.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private void clearHandlerCache() throws Exception {
|
||||||
|
Map<File, JarFile> rootFileCache = ((SoftReference<Map<File, JarFile>>) ReflectionTestUtils
|
||||||
|
.getField(Handler.class, "rootFileCache")).get();
|
||||||
|
if (rootFileCache != null) {
|
||||||
|
for (JarFile rootJarFile : rootFileCache.values()) {
|
||||||
|
rootJarFile.close();
|
||||||
|
}
|
||||||
|
rootFileCache.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDefaultHome() {
|
||||||
|
System.clearProperty("loader.home");
|
||||||
|
this.launcher = new PropertiesLauncher();
|
||||||
|
assertThat(this.launcher.getHomeDirectory()).isEqualTo(new File(System.getProperty("user.dir")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testAlternateHome() throws Exception {
|
||||||
|
System.setProperty("loader.home", "src/test/resources/home");
|
||||||
|
this.launcher = new PropertiesLauncher();
|
||||||
|
assertThat(this.launcher.getHomeDirectory()).isEqualTo(new File(System.getProperty("loader.home")));
|
||||||
|
assertThat(this.launcher.getMainClass()).isEqualTo("demo.HomeApplication");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testNonExistentHome() {
|
||||||
|
System.setProperty("loader.home", "src/test/resources/nonexistent");
|
||||||
|
assertThatIllegalStateException().isThrownBy(PropertiesLauncher::new)
|
||||||
|
.withMessageContaining("Invalid source directory")
|
||||||
|
.withCauseInstanceOf(IllegalArgumentException.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testUserSpecifiedMain() throws Exception {
|
||||||
|
this.launcher = new PropertiesLauncher();
|
||||||
|
assertThat(this.launcher.getMainClass()).isEqualTo("demo.Application");
|
||||||
|
assertThat(System.getProperty("loader.main")).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testUserSpecifiedConfigName() throws Exception {
|
||||||
|
System.setProperty("loader.config.name", "foo");
|
||||||
|
this.launcher = new PropertiesLauncher();
|
||||||
|
assertThat(this.launcher.getMainClass()).isEqualTo("my.Application");
|
||||||
|
assertThat(ReflectionTestUtils.getField(this.launcher, "paths")).hasToString("[etc/]");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testRootOfClasspathFirst() throws Exception {
|
||||||
|
System.setProperty("loader.config.name", "bar");
|
||||||
|
this.launcher = new PropertiesLauncher();
|
||||||
|
assertThat(this.launcher.getMainClass()).isEqualTo("my.BarApplication");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testUserSpecifiedDotPath() {
|
||||||
|
System.setProperty("loader.path", ".");
|
||||||
|
this.launcher = new PropertiesLauncher();
|
||||||
|
assertThat(ReflectionTestUtils.getField(this.launcher, "paths")).hasToString("[.]");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testUserSpecifiedSlashPath() throws Exception {
|
||||||
|
System.setProperty("loader.path", "jars/");
|
||||||
|
this.launcher = new PropertiesLauncher();
|
||||||
|
assertThat(ReflectionTestUtils.getField(this.launcher, "paths")).hasToString("[jars/]");
|
||||||
|
List<Archive> archives = new ArrayList<>();
|
||||||
|
this.launcher.getClassPathArchivesIterator().forEachRemaining(archives::add);
|
||||||
|
assertThat(archives).areExactly(1, endingWith("app.jar"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testUserSpecifiedWildcardPath() throws Exception {
|
||||||
|
System.setProperty("loader.path", "jars/*");
|
||||||
|
System.setProperty("loader.main", "demo.Application");
|
||||||
|
this.launcher = new PropertiesLauncher();
|
||||||
|
assertThat(ReflectionTestUtils.getField(this.launcher, "paths")).hasToString("[jars/]");
|
||||||
|
this.launcher.launch(new String[0]);
|
||||||
|
waitFor("Hello World");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testUserSpecifiedJarPath() throws Exception {
|
||||||
|
System.setProperty("loader.path", "jars/app.jar");
|
||||||
|
System.setProperty("loader.main", "demo.Application");
|
||||||
|
this.launcher = new PropertiesLauncher();
|
||||||
|
assertThat(ReflectionTestUtils.getField(this.launcher, "paths")).hasToString("[jars/app.jar]");
|
||||||
|
this.launcher.launch(new String[0]);
|
||||||
|
waitFor("Hello World");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testUserSpecifiedRootOfJarPath() throws Exception {
|
||||||
|
System.setProperty("loader.path", "jar:file:./src/test/resources/nested-jars/app.jar!/");
|
||||||
|
this.launcher = new PropertiesLauncher();
|
||||||
|
assertThat(ReflectionTestUtils.getField(this.launcher, "paths"))
|
||||||
|
.hasToString("[jar:file:./src/test/resources/nested-jars/app.jar!/]");
|
||||||
|
List<Archive> archives = new ArrayList<>();
|
||||||
|
this.launcher.getClassPathArchivesIterator().forEachRemaining(archives::add);
|
||||||
|
assertThat(archives).areExactly(1, endingWith("foo.jar!/"));
|
||||||
|
assertThat(archives).areExactly(1, endingWith("app.jar"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testUserSpecifiedRootOfJarPathWithDot() throws Exception {
|
||||||
|
System.setProperty("loader.path", "nested-jars/app.jar!/./");
|
||||||
|
this.launcher = new PropertiesLauncher();
|
||||||
|
List<Archive> archives = new ArrayList<>();
|
||||||
|
this.launcher.getClassPathArchivesIterator().forEachRemaining(archives::add);
|
||||||
|
assertThat(archives).areExactly(1, endingWith("foo.jar!/"));
|
||||||
|
assertThat(archives).areExactly(1, endingWith("app.jar"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testUserSpecifiedRootOfJarPathWithDotAndJarPrefix() throws Exception {
|
||||||
|
System.setProperty("loader.path", "jar:file:./src/test/resources/nested-jars/app.jar!/./");
|
||||||
|
this.launcher = new PropertiesLauncher();
|
||||||
|
List<Archive> archives = new ArrayList<>();
|
||||||
|
this.launcher.getClassPathArchivesIterator().forEachRemaining(archives::add);
|
||||||
|
assertThat(archives).areExactly(1, endingWith("foo.jar!/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testUserSpecifiedJarFileWithNestedArchives() throws Exception {
|
||||||
|
System.setProperty("loader.path", "nested-jars/app.jar");
|
||||||
|
System.setProperty("loader.main", "demo.Application");
|
||||||
|
this.launcher = new PropertiesLauncher();
|
||||||
|
List<Archive> archives = new ArrayList<>();
|
||||||
|
this.launcher.getClassPathArchivesIterator().forEachRemaining(archives::add);
|
||||||
|
assertThat(archives).areExactly(1, endingWith("foo.jar!/"));
|
||||||
|
assertThat(archives).areExactly(1, endingWith("app.jar"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testUserSpecifiedNestedJarPath() throws Exception {
|
||||||
|
System.setProperty("loader.path", "nested-jars/nested-jar-app.jar!/BOOT-INF/classes/");
|
||||||
|
System.setProperty("loader.main", "demo.Application");
|
||||||
|
this.launcher = new PropertiesLauncher();
|
||||||
|
assertThat(ReflectionTestUtils.getField(this.launcher, "paths"))
|
||||||
|
.hasToString("[nested-jars/nested-jar-app.jar!/BOOT-INF/classes/]");
|
||||||
|
this.launcher.launch(new String[0]);
|
||||||
|
waitFor("Hello World");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testUserSpecifiedDirectoryContainingJarFileWithNestedArchives() throws Exception {
|
||||||
|
System.setProperty("loader.path", "nested-jars");
|
||||||
|
System.setProperty("loader.main", "demo.Application");
|
||||||
|
this.launcher = new PropertiesLauncher();
|
||||||
|
this.launcher.launch(new String[0]);
|
||||||
|
waitFor("Hello World");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testUserSpecifiedJarPathWithDot() throws Exception {
|
||||||
|
System.setProperty("loader.path", "./jars/app.jar");
|
||||||
|
System.setProperty("loader.main", "demo.Application");
|
||||||
|
this.launcher = new PropertiesLauncher();
|
||||||
|
assertThat(ReflectionTestUtils.getField(this.launcher, "paths")).hasToString("[jars/app.jar]");
|
||||||
|
this.launcher.launch(new String[0]);
|
||||||
|
waitFor("Hello World");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testUserSpecifiedClassLoader() throws Exception {
|
||||||
|
System.setProperty("loader.path", "jars/app.jar");
|
||||||
|
System.setProperty("loader.classLoader", URLClassLoader.class.getName());
|
||||||
|
this.launcher = new PropertiesLauncher();
|
||||||
|
assertThat(ReflectionTestUtils.getField(this.launcher, "paths")).hasToString("[jars/app.jar]");
|
||||||
|
this.launcher.launch(new String[0]);
|
||||||
|
waitFor("Hello World");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testUserSpecifiedClassPathOrder() throws Exception {
|
||||||
|
System.setProperty("loader.path", "more-jars/app.jar,jars/app.jar");
|
||||||
|
System.setProperty("loader.classLoader", URLClassLoader.class.getName());
|
||||||
|
this.launcher = new PropertiesLauncher();
|
||||||
|
assertThat(ReflectionTestUtils.getField(this.launcher, "paths"))
|
||||||
|
.hasToString("[more-jars/app.jar, jars/app.jar]");
|
||||||
|
this.launcher.launch(new String[0]);
|
||||||
|
waitFor("Hello Other World");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCustomClassLoaderCreation() throws Exception {
|
||||||
|
System.setProperty("loader.classLoader", TestLoader.class.getName());
|
||||||
|
this.launcher = new PropertiesLauncher();
|
||||||
|
ClassLoader loader = this.launcher.createClassLoader(archives());
|
||||||
|
assertThat(loader).isNotNull();
|
||||||
|
assertThat(loader.getClass().getName()).isEqualTo(TestLoader.class.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Iterator<Archive> archives() throws Exception {
|
||||||
|
List<Archive> archives = new ArrayList<>();
|
||||||
|
String path = System.getProperty("java.class.path");
|
||||||
|
for (String url : path.split(File.pathSeparator)) {
|
||||||
|
Archive archive = archive(url);
|
||||||
|
if (archive != null) {
|
||||||
|
archives.add(archive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return archives.iterator();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Archive archive(String url) throws IOException {
|
||||||
|
File file = new FileSystemResource(url).getFile();
|
||||||
|
if (!file.exists()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (url.endsWith(".jar")) {
|
||||||
|
return new JarFileArchive(file);
|
||||||
|
}
|
||||||
|
return new ExplodedArchive(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testUserSpecifiedConfigPathWins() throws Exception {
|
||||||
|
System.setProperty("loader.config.name", "foo");
|
||||||
|
System.setProperty("loader.config.location", "classpath:bar.properties");
|
||||||
|
this.launcher = new PropertiesLauncher();
|
||||||
|
assertThat(this.launcher.getMainClass()).isEqualTo("my.BarApplication");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSystemPropertySpecifiedMain() throws Exception {
|
||||||
|
System.setProperty("loader.main", "foo.Bar");
|
||||||
|
this.launcher = new PropertiesLauncher();
|
||||||
|
assertThat(this.launcher.getMainClass()).isEqualTo("foo.Bar");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSystemPropertiesSet() {
|
||||||
|
System.setProperty("loader.system", "true");
|
||||||
|
new PropertiesLauncher();
|
||||||
|
assertThat(System.getProperty("loader.main")).isEqualTo("demo.Application");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testArgsEnhanced() throws Exception {
|
||||||
|
System.setProperty("loader.args", "foo");
|
||||||
|
this.launcher = new PropertiesLauncher();
|
||||||
|
assertThat(Arrays.asList(this.launcher.getArgs("bar"))).hasToString("[foo, bar]");
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Test
|
||||||
|
void testLoadPathCustomizedUsingManifest() throws Exception {
|
||||||
|
System.setProperty("loader.home", this.tempDir.getAbsolutePath());
|
||||||
|
Manifest manifest = new Manifest();
|
||||||
|
manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
|
||||||
|
manifest.getMainAttributes().putValue("Loader-Path", "/foo.jar, /bar");
|
||||||
|
File manifestFile = new File(this.tempDir, "META-INF/MANIFEST.MF");
|
||||||
|
manifestFile.getParentFile().mkdirs();
|
||||||
|
try (FileOutputStream manifestStream = new FileOutputStream(manifestFile)) {
|
||||||
|
manifest.write(manifestStream);
|
||||||
|
}
|
||||||
|
this.launcher = new PropertiesLauncher();
|
||||||
|
assertThat((List<String>) ReflectionTestUtils.getField(this.launcher, "paths")).containsExactly("/foo.jar",
|
||||||
|
"/bar/");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testManifestWithPlaceholders() throws Exception {
|
||||||
|
System.setProperty("loader.home", "src/test/resources/placeholders");
|
||||||
|
this.launcher = new PropertiesLauncher();
|
||||||
|
assertThat(this.launcher.getMainClass()).isEqualTo("demo.FooApplication");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void encodedFileUrlLoaderPathIsHandledCorrectly() throws Exception {
|
||||||
|
File loaderPath = new File(this.tempDir, "loader path");
|
||||||
|
loaderPath.mkdir();
|
||||||
|
System.setProperty("loader.path", loaderPath.toURI().toURL().toString());
|
||||||
|
this.launcher = new PropertiesLauncher();
|
||||||
|
List<Archive> archives = new ArrayList<>();
|
||||||
|
this.launcher.getClassPathArchivesIterator().forEachRemaining(archives::add);
|
||||||
|
assertThat(archives).hasSize(1);
|
||||||
|
File archiveRoot = (File) ReflectionTestUtils.getField(archives.get(0), "root");
|
||||||
|
assertThat(archiveRoot).isEqualTo(loaderPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test // gh-21575
|
||||||
|
void loadResourceFromJarFile() throws Exception {
|
||||||
|
File jarFile = new File(this.tempDir, "app.jar");
|
||||||
|
TestJarCreator.createTestJar(jarFile);
|
||||||
|
System.setProperty("loader.home", this.tempDir.getAbsolutePath());
|
||||||
|
System.setProperty("loader.path", "app.jar");
|
||||||
|
this.launcher = new PropertiesLauncher();
|
||||||
|
try {
|
||||||
|
this.launcher.launch(new String[0]);
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
// Expected ClassNotFoundException
|
||||||
|
LaunchedURLClassLoader classLoader = (LaunchedURLClassLoader) Thread.currentThread()
|
||||||
|
.getContextClassLoader();
|
||||||
|
classLoader.close();
|
||||||
|
}
|
||||||
|
URL resource = new URL("jar:" + jarFile.toURI() + "!/nested.jar!/3.dat");
|
||||||
|
byte[] bytes = FileCopyUtils.copyToByteArray(resource.openStream());
|
||||||
|
assertThat(bytes).isNotEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void waitFor(String value) {
|
||||||
|
Awaitility.waitAtMost(Duration.ofSeconds(5)).until(this.output::toString, containsString(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Condition<Archive> endingWith(String value) {
|
||||||
|
return new Condition<>() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean matches(Archive archive) {
|
||||||
|
return archive.toString().endsWith(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static class TestLoader extends URLClassLoader {
|
||||||
|
|
||||||
|
TestLoader(ClassLoader parent) {
|
||||||
|
super(new URL[0], parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Class<?> findClass(String name) throws ClassNotFoundException {
|
||||||
|
return super.findClass(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,151 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.jar.Attributes;
|
||||||
|
import java.util.jar.JarEntry;
|
||||||
|
import java.util.jar.JarOutputStream;
|
||||||
|
import java.util.jar.Manifest;
|
||||||
|
import java.util.zip.CRC32;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a simple test jar.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
public abstract class TestJarCreator {
|
||||||
|
|
||||||
|
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 = BASE_VERSION;
|
||||||
|
}
|
||||||
|
RUNTIME_VERSION = version;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void createTestJar(File file) throws Exception {
|
||||||
|
createTestJar(file, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void createTestJar(File file, boolean unpackNested) throws Exception {
|
||||||
|
FileOutputStream fileOutputStream = new FileOutputStream(file);
|
||||||
|
try (JarOutputStream jarOutputStream = new JarOutputStream(fileOutputStream)) {
|
||||||
|
jarOutputStream.setComment("outer");
|
||||||
|
writeManifest(jarOutputStream, "j1");
|
||||||
|
writeEntry(jarOutputStream, "1.dat", 1);
|
||||||
|
writeEntry(jarOutputStream, "2.dat", 2);
|
||||||
|
writeDirEntry(jarOutputStream, "d/");
|
||||||
|
writeEntry(jarOutputStream, "d/9.dat", 9);
|
||||||
|
writeDirEntry(jarOutputStream, "special/");
|
||||||
|
writeEntry(jarOutputStream, "special/\u00EB.dat", '\u00EB');
|
||||||
|
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(multiRelease);
|
||||||
|
nestedEntry.setSize(nestedJarData.length);
|
||||||
|
nestedEntry.setCompressedSize(nestedJarData.length);
|
||||||
|
if (unpackNested) {
|
||||||
|
nestedEntry.setComment("UNPACK:0000000000000000000000000000000000000000");
|
||||||
|
}
|
||||||
|
CRC32 crc32 = new CRC32();
|
||||||
|
crc32.update(nestedJarData);
|
||||||
|
nestedEntry.setCrc(crc32.getValue());
|
||||||
|
nestedEntry.setMethod(ZipEntry.STORED);
|
||||||
|
jarOutputStream.putNextEntry(nestedEntry);
|
||||||
|
jarOutputStream.write(nestedJarData);
|
||||||
|
jarOutputStream.closeEntry();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] getNestedJarData(boolean multiRelease) throws Exception {
|
||||||
|
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
|
||||||
|
JarOutputStream jarOutputStream = new JarOutputStream(byteArrayOutputStream);
|
||||||
|
jarOutputStream.setComment("nested");
|
||||||
|
writeManifest(jarOutputStream, "j2", multiRelease);
|
||||||
|
if (multiRelease) {
|
||||||
|
writeEntry(jarOutputStream, "multi-release.dat", BASE_VERSION);
|
||||||
|
writeEntry(jarOutputStream, String.format("META-INF/versions/%d/multi-release.dat", RUNTIME_VERSION),
|
||||||
|
RUNTIME_VERSION);
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeDirEntry(JarOutputStream jarOutputStream, String name) throws IOException {
|
||||||
|
jarOutputStream.putNextEntry(new JarEntry(name));
|
||||||
|
jarOutputStream.closeEntry();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeEntry(JarOutputStream jarOutputStream, String name, int data) throws IOException {
|
||||||
|
jarOutputStream.putNextEntry(new JarEntry(name));
|
||||||
|
jarOutputStream.write(new byte[] { (byte) data });
|
||||||
|
jarOutputStream.closeEntry();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,121 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.URLClassLoader;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.boot.loader.archive.Archive;
|
||||||
|
import org.springframework.boot.loader.archive.ExplodedArchive;
|
||||||
|
import org.springframework.boot.loader.archive.JarFileArchive;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link WarLauncher}.
|
||||||
|
*
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Scott Frederick
|
||||||
|
*/
|
||||||
|
class WarLauncherTests extends AbstractExecutableArchiveLauncherTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void explodedWarHasOnlyWebInfClassesAndContentsOfWebInfLibOnClasspath() throws Exception {
|
||||||
|
File explodedRoot = explode(createJarArchive("archive.war", "WEB-INF"));
|
||||||
|
WarLauncher launcher = new WarLauncher(new ExplodedArchive(explodedRoot, true));
|
||||||
|
List<Archive> archives = new ArrayList<>();
|
||||||
|
launcher.getClassPathArchivesIterator().forEachRemaining(archives::add);
|
||||||
|
assertThat(getUrls(archives)).containsExactlyInAnyOrder(getExpectedFileUrls(explodedRoot));
|
||||||
|
for (Archive archive : archives) {
|
||||||
|
archive.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void archivedWarHasOnlyWebInfClassesAndContentsOfWebInfLibOnClasspath() throws Exception {
|
||||||
|
File jarRoot = createJarArchive("archive.war", "WEB-INF");
|
||||||
|
try (JarFileArchive archive = new JarFileArchive(jarRoot)) {
|
||||||
|
WarLauncher launcher = new WarLauncher(archive);
|
||||||
|
List<Archive> classPathArchives = new ArrayList<>();
|
||||||
|
launcher.getClassPathArchivesIterator().forEachRemaining(classPathArchives::add);
|
||||||
|
assertThat(getUrls(classPathArchives)).containsOnly(
|
||||||
|
new URL("jar:" + jarRoot.toURI().toURL() + "!/WEB-INF/classes!/"),
|
||||||
|
new URL("jar:" + jarRoot.toURI().toURL() + "!/WEB-INF/lib/foo.jar!/"),
|
||||||
|
new URL("jar:" + jarRoot.toURI().toURL() + "!/WEB-INF/lib/bar.jar!/"),
|
||||||
|
new URL("jar:" + jarRoot.toURI().toURL() + "!/WEB-INF/lib/baz.jar!/"));
|
||||||
|
for (Archive classPathArchive : classPathArchives) {
|
||||||
|
classPathArchive.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void explodedWarShouldPreserveClasspathOrderWhenIndexPresent() throws Exception {
|
||||||
|
File explodedRoot = explode(createJarArchive("archive.war", "WEB-INF", true, Collections.emptyList()));
|
||||||
|
WarLauncher launcher = new WarLauncher(new ExplodedArchive(explodedRoot, true));
|
||||||
|
Iterator<Archive> archives = launcher.getClassPathArchivesIterator();
|
||||||
|
URLClassLoader classLoader = (URLClassLoader) launcher.createClassLoader(archives);
|
||||||
|
URL[] urls = classLoader.getURLs();
|
||||||
|
assertThat(urls).containsExactly(getExpectedFileUrls(explodedRoot));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void warFilesPresentInWebInfLibsAndNotInClasspathIndexShouldBeAddedAfterWebInfClasses() throws Exception {
|
||||||
|
ArrayList<String> extraLibs = new ArrayList<>(Arrays.asList("extra-1.jar", "extra-2.jar"));
|
||||||
|
File explodedRoot = explode(createJarArchive("archive.war", "WEB-INF", true, extraLibs));
|
||||||
|
WarLauncher launcher = new WarLauncher(new ExplodedArchive(explodedRoot, true));
|
||||||
|
Iterator<Archive> archives = launcher.getClassPathArchivesIterator();
|
||||||
|
URLClassLoader classLoader = (URLClassLoader) launcher.createClassLoader(archives);
|
||||||
|
URL[] urls = classLoader.getURLs();
|
||||||
|
List<File> expectedFiles = getExpectedFilesWithExtraLibs(explodedRoot);
|
||||||
|
URL[] expectedFileUrls = expectedFiles.stream().map(this::toUrl).toArray(URL[]::new);
|
||||||
|
assertThat(urls).containsExactly(expectedFileUrls);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final URL[] getExpectedFileUrls(File explodedRoot) {
|
||||||
|
return getExpectedFiles(explodedRoot).stream().map(this::toUrl).toArray(URL[]::new);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final List<File> getExpectedFiles(File parent) {
|
||||||
|
List<File> expected = new ArrayList<>();
|
||||||
|
expected.add(new File(parent, "WEB-INF/classes"));
|
||||||
|
expected.add(new File(parent, "WEB-INF/lib/foo.jar"));
|
||||||
|
expected.add(new File(parent, "WEB-INF/lib/bar.jar"));
|
||||||
|
expected.add(new File(parent, "WEB-INF/lib/baz.jar"));
|
||||||
|
return expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final List<File> getExpectedFilesWithExtraLibs(File parent) {
|
||||||
|
List<File> expected = new ArrayList<>();
|
||||||
|
expected.add(new File(parent, "WEB-INF/classes"));
|
||||||
|
expected.add(new File(parent, "WEB-INF/lib/extra-1.jar"));
|
||||||
|
expected.add(new File(parent, "WEB-INF/lib/extra-2.jar"));
|
||||||
|
expected.add(new File(parent, "WEB-INF/lib/foo.jar"));
|
||||||
|
expected.add(new File(parent, "WEB-INF/lib/bar.jar"));
|
||||||
|
expected.add(new File(parent, "WEB-INF/lib/baz.jar"));
|
||||||
|
return expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,189 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.archive;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.URLClassLoader;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.jar.JarEntry;
|
||||||
|
import java.util.jar.JarFile;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import org.springframework.boot.loader.TestJarCreator;
|
||||||
|
import org.springframework.boot.loader.archive.Archive.Entry;
|
||||||
|
import org.springframework.util.FileCopyUtils;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link ExplodedArchive}.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @author Dave Syer
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
*/
|
||||||
|
class ExplodedArchiveTests {
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
File tempDir;
|
||||||
|
|
||||||
|
private File rootDirectory;
|
||||||
|
|
||||||
|
private ExplodedArchive archive;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setup() throws Exception {
|
||||||
|
createArchive();
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() throws Exception {
|
||||||
|
if (this.archive != null) {
|
||||||
|
this.archive.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createArchive() throws Exception {
|
||||||
|
createArchive(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createArchive(String directoryName) throws Exception {
|
||||||
|
File file = new File(this.tempDir, "test.jar");
|
||||||
|
TestJarCreator.createTestJar(file);
|
||||||
|
this.rootDirectory = (StringUtils.hasText(directoryName) ? new File(this.tempDir, directoryName)
|
||||||
|
: new File(this.tempDir, UUID.randomUUID().toString()));
|
||||||
|
JarFile jarFile = new JarFile(file);
|
||||||
|
Enumeration<JarEntry> entries = jarFile.entries();
|
||||||
|
while (entries.hasMoreElements()) {
|
||||||
|
JarEntry entry = entries.nextElement();
|
||||||
|
File destination = new File(this.rootDirectory.getAbsolutePath() + File.separator + entry.getName());
|
||||||
|
destination.getParentFile().mkdirs();
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
destination.mkdir();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
FileCopyUtils.copy(jarFile.getInputStream(entry), new FileOutputStream(destination));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.archive = new ExplodedArchive(this.rootDirectory);
|
||||||
|
jarFile.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getManifest() throws Exception {
|
||||||
|
assertThat(this.archive.getManifest().getMainAttributes().getValue("Built-By")).isEqualTo("j1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getEntries() {
|
||||||
|
Map<String, Archive.Entry> entries = getEntriesMap(this.archive);
|
||||||
|
assertThat(entries).hasSize(12);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getUrl() throws Exception {
|
||||||
|
assertThat(this.archive.getUrl()).isEqualTo(this.rootDirectory.toURI().toURL());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getUrlWithSpaceInPath() throws Exception {
|
||||||
|
createArchive("spaces in the name");
|
||||||
|
assertThat(this.archive.getUrl()).isEqualTo(this.rootDirectory.toURI().toURL());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getNestedArchive() throws Exception {
|
||||||
|
Entry entry = getEntriesMap(this.archive).get("nested.jar");
|
||||||
|
Archive nested = this.archive.getNestedArchive(entry);
|
||||||
|
assertThat(nested.getUrl()).hasToString(this.rootDirectory.toURI() + "nested.jar");
|
||||||
|
nested.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void nestedDirArchive() throws Exception {
|
||||||
|
Entry entry = getEntriesMap(this.archive).get("d/");
|
||||||
|
Archive nested = this.archive.getNestedArchive(entry);
|
||||||
|
Map<String, Entry> nestedEntries = getEntriesMap(nested);
|
||||||
|
assertThat(nestedEntries).hasSize(1);
|
||||||
|
assertThat(nested.getUrl()).hasToString("file:" + this.rootDirectory.toURI().getPath() + "d/");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getNonRecursiveEntriesForRoot() throws Exception {
|
||||||
|
try (ExplodedArchive explodedArchive = new ExplodedArchive(new File("/"), false)) {
|
||||||
|
Map<String, Archive.Entry> entries = getEntriesMap(explodedArchive);
|
||||||
|
assertThat(entries).hasSizeGreaterThan(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getNonRecursiveManifest() throws Exception {
|
||||||
|
try (ExplodedArchive explodedArchive = new ExplodedArchive(new File("src/test/resources/root"))) {
|
||||||
|
assertThat(explodedArchive.getManifest()).isNotNull();
|
||||||
|
Map<String, Archive.Entry> entries = getEntriesMap(explodedArchive);
|
||||||
|
assertThat(entries).hasSize(4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getNonRecursiveManifestEvenIfNonRecursive() throws Exception {
|
||||||
|
try (ExplodedArchive explodedArchive = new ExplodedArchive(new File("src/test/resources/root"), false)) {
|
||||||
|
assertThat(explodedArchive.getManifest()).isNotNull();
|
||||||
|
Map<String, Archive.Entry> entries = getEntriesMap(explodedArchive);
|
||||||
|
assertThat(entries).hasSize(3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getResourceAsStream() throws Exception {
|
||||||
|
try (ExplodedArchive explodedArchive = new ExplodedArchive(new File("src/test/resources/root"))) {
|
||||||
|
assertThat(explodedArchive.getManifest()).isNotNull();
|
||||||
|
URLClassLoader loader = new URLClassLoader(new URL[] { explodedArchive.getUrl() });
|
||||||
|
assertThat(loader.getResourceAsStream("META-INF/spring/application.xml")).isNotNull();
|
||||||
|
loader.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getResourceAsStreamNonRecursive() throws Exception {
|
||||||
|
try (ExplodedArchive explodedArchive = new ExplodedArchive(new File("src/test/resources/root"), false)) {
|
||||||
|
assertThat(explodedArchive.getManifest()).isNotNull();
|
||||||
|
URLClassLoader loader = new URLClassLoader(new URL[] { explodedArchive.getUrl() });
|
||||||
|
assertThat(loader.getResourceAsStream("META-INF/spring/application.xml")).isNotNull();
|
||||||
|
loader.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Archive.Entry> getEntriesMap(Archive archive) {
|
||||||
|
Map<String, Archive.Entry> entries = new HashMap<>();
|
||||||
|
for (Archive.Entry entry : archive) {
|
||||||
|
entries.put(entry.getName(), entry);
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,207 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.archive;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.jar.JarEntry;
|
||||||
|
import java.util.jar.JarOutputStream;
|
||||||
|
import java.util.zip.CRC32;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import org.springframework.boot.loader.TestJarCreator;
|
||||||
|
import org.springframework.boot.loader.archive.Archive.Entry;
|
||||||
|
import org.springframework.boot.loader.jar.JarFile;
|
||||||
|
import org.springframework.util.FileCopyUtils;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link JarFileArchive}.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Camille Vienot
|
||||||
|
*/
|
||||||
|
class JarFileArchiveTests {
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
File tempDir;
|
||||||
|
|
||||||
|
private File rootJarFile;
|
||||||
|
|
||||||
|
private JarFileArchive archive;
|
||||||
|
|
||||||
|
private String rootJarFileUrl;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setup() throws Exception {
|
||||||
|
setup(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() throws Exception {
|
||||||
|
this.archive.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setup(boolean unpackNested) throws Exception {
|
||||||
|
this.rootJarFile = new File(this.tempDir, "root.jar");
|
||||||
|
this.rootJarFileUrl = this.rootJarFile.toURI().toString();
|
||||||
|
TestJarCreator.createTestJar(this.rootJarFile, unpackNested);
|
||||||
|
if (this.archive != null) {
|
||||||
|
this.archive.close();
|
||||||
|
}
|
||||||
|
this.archive = new JarFileArchive(this.rootJarFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getManifest() throws Exception {
|
||||||
|
assertThat(this.archive.getManifest().getMainAttributes().getValue("Built-By")).isEqualTo("j1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getEntries() {
|
||||||
|
Map<String, Archive.Entry> entries = getEntriesMap(this.archive);
|
||||||
|
assertThat(entries).hasSize(12);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getUrl() throws Exception {
|
||||||
|
URL url = this.archive.getUrl();
|
||||||
|
assertThat(url).hasToString(this.rootJarFileUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getNestedArchive() throws Exception {
|
||||||
|
Entry entry = getEntriesMap(this.archive).get("nested.jar");
|
||||||
|
try (Archive nested = this.archive.getNestedArchive(entry)) {
|
||||||
|
assertThat(nested.getUrl()).hasToString("jar:" + this.rootJarFileUrl + "!/nested.jar!/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getNestedUnpackedArchive() throws Exception {
|
||||||
|
setup(true);
|
||||||
|
Entry entry = getEntriesMap(this.archive).get("nested.jar");
|
||||||
|
try (Archive nested = this.archive.getNestedArchive(entry)) {
|
||||||
|
assertThat(nested.getUrl().toString()).startsWith("file:");
|
||||||
|
assertThat(nested.getUrl().toString()).endsWith("/nested.jar");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void unpackedLocationsAreUniquePerArchive() throws Exception {
|
||||||
|
setup(true);
|
||||||
|
Entry entry = getEntriesMap(this.archive).get("nested.jar");
|
||||||
|
URL firstNestedUrl;
|
||||||
|
try (Archive firstNested = this.archive.getNestedArchive(entry)) {
|
||||||
|
firstNestedUrl = firstNested.getUrl();
|
||||||
|
}
|
||||||
|
this.archive.close();
|
||||||
|
setup(true);
|
||||||
|
entry = getEntriesMap(this.archive).get("nested.jar");
|
||||||
|
try (Archive secondNested = this.archive.getNestedArchive(entry)) {
|
||||||
|
URL secondNestedUrl = secondNested.getUrl();
|
||||||
|
assertThat(secondNestedUrl).isNotEqualTo(firstNestedUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void unpackedLocationsFromSameArchiveShareSameParent() throws Exception {
|
||||||
|
setup(true);
|
||||||
|
try (Archive nestedArchive = this.archive.getNestedArchive(getEntriesMap(this.archive).get("nested.jar"));
|
||||||
|
Archive anotherNestedArchive = this.archive
|
||||||
|
.getNestedArchive(getEntriesMap(this.archive).get("another-nested.jar"))) {
|
||||||
|
File nested = new File(nestedArchive.getUrl().toURI());
|
||||||
|
File anotherNested = new File(anotherNestedArchive.getUrl().toURI());
|
||||||
|
assertThat(nested).hasParent(anotherNested.getParent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void filesInZip64ArchivesAreAllListed() throws IOException {
|
||||||
|
File file = new File(this.tempDir, "test.jar");
|
||||||
|
FileCopyUtils.copy(writeZip64Jar(), file);
|
||||||
|
try (JarFileArchive zip64Archive = new JarFileArchive(file)) {
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
Iterator<Entry> entries = zip64Archive.iterator();
|
||||||
|
for (int i = 0; i < 65537; i++) {
|
||||||
|
assertThat(entries.hasNext()).as(i + "nth file is present").isTrue();
|
||||||
|
entries.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void nestedZip64ArchivesAreHandledGracefully() throws Exception {
|
||||||
|
File file = new File(this.tempDir, "test.jar");
|
||||||
|
try (JarOutputStream output = new JarOutputStream(new FileOutputStream(file))) {
|
||||||
|
JarEntry zip64JarEntry = new JarEntry("nested/zip64.jar");
|
||||||
|
output.putNextEntry(zip64JarEntry);
|
||||||
|
byte[] zip64JarData = writeZip64Jar();
|
||||||
|
zip64JarEntry.setSize(zip64JarData.length);
|
||||||
|
zip64JarEntry.setCompressedSize(zip64JarData.length);
|
||||||
|
zip64JarEntry.setMethod(ZipEntry.STORED);
|
||||||
|
CRC32 crc32 = new CRC32();
|
||||||
|
crc32.update(zip64JarData);
|
||||||
|
zip64JarEntry.setCrc(crc32.getValue());
|
||||||
|
output.write(zip64JarData);
|
||||||
|
output.closeEntry();
|
||||||
|
}
|
||||||
|
try (JarFile jarFile = new JarFile(file)) {
|
||||||
|
ZipEntry nestedEntry = jarFile.getEntry("nested/zip64.jar");
|
||||||
|
try (JarFile nestedJarFile = jarFile.getNestedJarFile(nestedEntry)) {
|
||||||
|
Iterator<JarEntry> iterator = nestedJarFile.iterator();
|
||||||
|
for (int i = 0; i < 65537; i++) {
|
||||||
|
assertThat(iterator.hasNext()).as(i + "nth file is present").isTrue();
|
||||||
|
iterator.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] writeZip64Jar() throws IOException {
|
||||||
|
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||||
|
try (JarOutputStream jarOutput = new JarOutputStream(bytes)) {
|
||||||
|
for (int i = 0; i < 65537; i++) {
|
||||||
|
jarOutput.putNextEntry(new JarEntry(i + ".dat"));
|
||||||
|
jarOutput.closeEntry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bytes.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Archive.Entry> getEntriesMap(Archive archive) {
|
||||||
|
Map<String, Archive.Entry> entries = new HashMap<>();
|
||||||
|
for (Archive.Entry entry : archive) {
|
||||||
|
entries.put(entry.getName(), entry);
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,300 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.data;
|
||||||
|
|
||||||
|
import java.io.EOFException;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatNullPointerException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link RandomAccessDataFile}.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
*/
|
||||||
|
class RandomAccessDataFileTests {
|
||||||
|
|
||||||
|
private static final byte[] BYTES;
|
||||||
|
|
||||||
|
static {
|
||||||
|
BYTES = new byte[256];
|
||||||
|
for (int i = 0; i < BYTES.length; i++) {
|
||||||
|
BYTES[i] = (byte) i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private File tempFile;
|
||||||
|
|
||||||
|
private RandomAccessDataFile file;
|
||||||
|
|
||||||
|
private InputStream inputStream;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setup(@TempDir File tempDir) throws Exception {
|
||||||
|
this.tempFile = new File(tempDir, "tempFile");
|
||||||
|
FileOutputStream outputStream = new FileOutputStream(this.tempFile);
|
||||||
|
outputStream.write(BYTES);
|
||||||
|
outputStream.close();
|
||||||
|
this.file = new RandomAccessDataFile(this.tempFile);
|
||||||
|
this.inputStream = this.file.getInputStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void cleanup() throws Exception {
|
||||||
|
this.inputStream.close();
|
||||||
|
this.file.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fileNotNull() {
|
||||||
|
assertThatIllegalArgumentException().isThrownBy(() -> new RandomAccessDataFile(null))
|
||||||
|
.withMessageContaining("File must not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fileExists() {
|
||||||
|
File file = new File("/does/not/exist");
|
||||||
|
assertThatIllegalArgumentException().isThrownBy(() -> new RandomAccessDataFile(file))
|
||||||
|
.withMessageContaining(String.format("File %s must exist", file.getAbsolutePath()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void readWithOffsetAndLengthShouldRead() throws Exception {
|
||||||
|
byte[] read = this.file.read(2, 3);
|
||||||
|
assertThat(read).isEqualTo(new byte[] { 2, 3, 4 });
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void readWhenOffsetIsBeyondEOFShouldThrowException() {
|
||||||
|
assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> this.file.read(257, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void readWhenOffsetIsBeyondEndOfSubsectionShouldThrowException() {
|
||||||
|
RandomAccessData subsection = this.file.getSubsection(0, 10);
|
||||||
|
assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> subsection.read(11, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void readWhenOffsetPlusLengthGreaterThanEOFShouldThrowException() {
|
||||||
|
assertThatExceptionOfType(EOFException.class).isThrownBy(() -> this.file.read(256, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void readWhenOffsetPlusLengthGreaterThanEndOfSubsectionShouldThrowException() {
|
||||||
|
RandomAccessData subsection = this.file.getSubsection(0, 10);
|
||||||
|
assertThatExceptionOfType(EOFException.class).isThrownBy(() -> subsection.read(10, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void inputStreamRead() throws Exception {
|
||||||
|
for (int i = 0; i <= 255; i++) {
|
||||||
|
assertThat(this.inputStream.read()).isEqualTo(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void inputStreamReadNullBytes() {
|
||||||
|
assertThatNullPointerException().isThrownBy(() -> this.inputStream.read(null))
|
||||||
|
.withMessage("Bytes must not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void inputStreamReadNullBytesWithOffset() {
|
||||||
|
assertThatNullPointerException().isThrownBy(() -> this.inputStream.read(null, 0, 1))
|
||||||
|
.withMessage("Bytes must not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void inputStreamReadBytes() throws Exception {
|
||||||
|
byte[] b = new byte[256];
|
||||||
|
int amountRead = this.inputStream.read(b);
|
||||||
|
assertThat(b).isEqualTo(BYTES);
|
||||||
|
assertThat(amountRead).isEqualTo(256);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void inputStreamReadOffsetBytes() throws Exception {
|
||||||
|
byte[] b = new byte[7];
|
||||||
|
this.inputStream.skip(1);
|
||||||
|
int amountRead = this.inputStream.read(b, 2, 3);
|
||||||
|
assertThat(b).isEqualTo(new byte[] { 0, 0, 1, 2, 3, 0, 0 });
|
||||||
|
assertThat(amountRead).isEqualTo(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void inputStreamReadMoreBytesThanAvailable() throws Exception {
|
||||||
|
byte[] b = new byte[257];
|
||||||
|
int amountRead = this.inputStream.read(b);
|
||||||
|
assertThat(b).startsWith(BYTES);
|
||||||
|
assertThat(amountRead).isEqualTo(256);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void inputStreamReadPastEnd() throws Exception {
|
||||||
|
this.inputStream.skip(255);
|
||||||
|
assertThat(this.inputStream.read()).isEqualTo(0xFF);
|
||||||
|
assertThat(this.inputStream.read()).isEqualTo(-1);
|
||||||
|
assertThat(this.inputStream.read()).isEqualTo(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void inputStreamReadZeroLength() throws Exception {
|
||||||
|
byte[] b = new byte[] { 0x0F };
|
||||||
|
int amountRead = this.inputStream.read(b, 0, 0);
|
||||||
|
assertThat(b).isEqualTo(new byte[] { 0x0F });
|
||||||
|
assertThat(amountRead).isZero();
|
||||||
|
assertThat(this.inputStream.read()).isZero();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void inputStreamSkip() throws Exception {
|
||||||
|
long amountSkipped = this.inputStream.skip(4);
|
||||||
|
assertThat(this.inputStream.read()).isEqualTo(4);
|
||||||
|
assertThat(amountSkipped).isEqualTo(4L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void inputStreamSkipMoreThanAvailable() throws Exception {
|
||||||
|
long amountSkipped = this.inputStream.skip(257);
|
||||||
|
assertThat(this.inputStream.read()).isEqualTo(-1);
|
||||||
|
assertThat(amountSkipped).isEqualTo(256L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void inputStreamSkipPastEnd() throws Exception {
|
||||||
|
this.inputStream.skip(256);
|
||||||
|
long amountSkipped = this.inputStream.skip(1);
|
||||||
|
assertThat(amountSkipped).isZero();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void inputStreamAvailable() throws Exception {
|
||||||
|
assertThat(this.inputStream.available()).isEqualTo(256);
|
||||||
|
this.inputStream.skip(56);
|
||||||
|
assertThat(this.inputStream.available()).isEqualTo(200);
|
||||||
|
this.inputStream.skip(200);
|
||||||
|
assertThat(this.inputStream.available()).isZero();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void subsectionNegativeOffset() {
|
||||||
|
assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> this.file.getSubsection(-1, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void subsectionNegativeLength() {
|
||||||
|
assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> this.file.getSubsection(0, -1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void subsectionZeroLength() throws Exception {
|
||||||
|
RandomAccessData subsection = this.file.getSubsection(0, 0);
|
||||||
|
assertThat(subsection.getInputStream().read()).isEqualTo(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void subsectionTooBig() {
|
||||||
|
this.file.getSubsection(0, 256);
|
||||||
|
assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> this.file.getSubsection(0, 257));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void subsectionTooBigWithOffset() {
|
||||||
|
this.file.getSubsection(1, 255);
|
||||||
|
assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> this.file.getSubsection(1, 256));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void subsection() throws Exception {
|
||||||
|
RandomAccessData subsection = this.file.getSubsection(1, 1);
|
||||||
|
assertThat(subsection.getInputStream().read()).isOne();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void inputStreamReadPastSubsection() throws Exception {
|
||||||
|
RandomAccessData subsection = this.file.getSubsection(1, 2);
|
||||||
|
InputStream inputStream = subsection.getInputStream();
|
||||||
|
assertThat(inputStream.read()).isOne();
|
||||||
|
assertThat(inputStream.read()).isEqualTo(2);
|
||||||
|
assertThat(inputStream.read()).isEqualTo(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void inputStreamReadBytesPastSubsection() throws Exception {
|
||||||
|
RandomAccessData subsection = this.file.getSubsection(1, 2);
|
||||||
|
InputStream inputStream = subsection.getInputStream();
|
||||||
|
byte[] b = new byte[3];
|
||||||
|
int amountRead = inputStream.read(b);
|
||||||
|
assertThat(b).isEqualTo(new byte[] { 1, 2, 0 });
|
||||||
|
assertThat(amountRead).isEqualTo(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void inputStreamSkipPastSubsection() throws Exception {
|
||||||
|
RandomAccessData subsection = this.file.getSubsection(1, 2);
|
||||||
|
InputStream inputStream = subsection.getInputStream();
|
||||||
|
assertThat(inputStream.skip(3)).isEqualTo(2L);
|
||||||
|
assertThat(inputStream.read()).isEqualTo(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void inputStreamSkipNegative() throws Exception {
|
||||||
|
assertThat(this.inputStream.skip(-1)).isZero();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getFile() {
|
||||||
|
assertThat(this.file.getFile()).isEqualTo(this.tempFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void concurrentReads() throws Exception {
|
||||||
|
ExecutorService executorService = Executors.newFixedThreadPool(20);
|
||||||
|
List<Future<Boolean>> results = new ArrayList<>();
|
||||||
|
for (int i = 0; i < 100; i++) {
|
||||||
|
results.add(executorService.submit(() -> {
|
||||||
|
InputStream subsectionInputStream = RandomAccessDataFileTests.this.file.getSubsection(0, 256)
|
||||||
|
.getInputStream();
|
||||||
|
byte[] b = new byte[256];
|
||||||
|
subsectionInputStream.read(b);
|
||||||
|
return Arrays.equals(b, BYTES);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
for (Future<Boolean> future : results) {
|
||||||
|
assertThat(future.get()).isTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,196 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.jar;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link AsciiBytes}.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
*/
|
||||||
|
class AsciiBytesTests {
|
||||||
|
|
||||||
|
private static final char NO_SUFFIX = 0;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createFromBytes() {
|
||||||
|
AsciiBytes bytes = new AsciiBytes(new byte[] { 65, 66 });
|
||||||
|
assertThat(bytes).hasToString("AB");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createFromBytesWithOffset() {
|
||||||
|
AsciiBytes bytes = new AsciiBytes(new byte[] { 65, 66, 67, 68 }, 1, 2);
|
||||||
|
assertThat(bytes).hasToString("BC");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createFromString() {
|
||||||
|
AsciiBytes bytes = new AsciiBytes("AB");
|
||||||
|
assertThat(bytes).hasToString("AB");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void length() {
|
||||||
|
AsciiBytes b1 = new AsciiBytes(new byte[] { 65, 66 });
|
||||||
|
AsciiBytes b2 = new AsciiBytes(new byte[] { 65, 66, 67, 68 }, 1, 2);
|
||||||
|
assertThat(b1.length()).isEqualTo(2);
|
||||||
|
assertThat(b2.length()).isEqualTo(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void startWith() {
|
||||||
|
AsciiBytes abc = new AsciiBytes(new byte[] { 65, 66, 67 });
|
||||||
|
AsciiBytes ab = new AsciiBytes(new byte[] { 65, 66 });
|
||||||
|
AsciiBytes bc = new AsciiBytes(new byte[] { 65, 66, 67 }, 1, 2);
|
||||||
|
AsciiBytes abcd = new AsciiBytes(new byte[] { 65, 66, 67, 68 });
|
||||||
|
assertThat(abc.startsWith(abc)).isTrue();
|
||||||
|
assertThat(abc.startsWith(ab)).isTrue();
|
||||||
|
assertThat(abc.startsWith(bc)).isFalse();
|
||||||
|
assertThat(abc.startsWith(abcd)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void endsWith() {
|
||||||
|
AsciiBytes abc = new AsciiBytes(new byte[] { 65, 66, 67 });
|
||||||
|
AsciiBytes bc = new AsciiBytes(new byte[] { 65, 66, 67 }, 1, 2);
|
||||||
|
AsciiBytes ab = new AsciiBytes(new byte[] { 65, 66 });
|
||||||
|
AsciiBytes aabc = new AsciiBytes(new byte[] { 65, 65, 66, 67 });
|
||||||
|
assertThat(abc.endsWith(abc)).isTrue();
|
||||||
|
assertThat(abc.endsWith(bc)).isTrue();
|
||||||
|
assertThat(abc.endsWith(ab)).isFalse();
|
||||||
|
assertThat(abc.endsWith(aabc)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void substringFromBeingIndex() {
|
||||||
|
AsciiBytes abcd = new AsciiBytes(new byte[] { 65, 66, 67, 68 });
|
||||||
|
assertThat(abcd.substring(0)).hasToString("ABCD");
|
||||||
|
assertThat(abcd.substring(1)).hasToString("BCD");
|
||||||
|
assertThat(abcd.substring(2)).hasToString("CD");
|
||||||
|
assertThat(abcd.substring(3)).hasToString("D");
|
||||||
|
assertThat(abcd.substring(4).toString()).isEmpty();
|
||||||
|
assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> abcd.substring(5));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void substring() {
|
||||||
|
AsciiBytes abcd = new AsciiBytes(new byte[] { 65, 66, 67, 68 });
|
||||||
|
assertThat(abcd.substring(0, 4)).hasToString("ABCD");
|
||||||
|
assertThat(abcd.substring(1, 3)).hasToString("BC");
|
||||||
|
assertThat(abcd.substring(3, 4)).hasToString("D");
|
||||||
|
assertThat(abcd.substring(3, 3).toString()).isEmpty();
|
||||||
|
assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> abcd.substring(3, 5));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void hashCodeAndEquals() {
|
||||||
|
AsciiBytes abcd = new AsciiBytes(new byte[] { 65, 66, 67, 68 });
|
||||||
|
AsciiBytes bc = new AsciiBytes(new byte[] { 66, 67 });
|
||||||
|
AsciiBytes bc_substring = new AsciiBytes(new byte[] { 65, 66, 67, 68 }).substring(1, 3);
|
||||||
|
AsciiBytes bc_string = new AsciiBytes("BC");
|
||||||
|
assertThat(bc).hasSameHashCodeAs(bc);
|
||||||
|
assertThat(bc).hasSameHashCodeAs(bc_substring);
|
||||||
|
assertThat(bc).hasSameHashCodeAs(bc_string);
|
||||||
|
assertThat(bc).isEqualTo(bc);
|
||||||
|
assertThat(bc).isEqualTo(bc_substring);
|
||||||
|
assertThat(bc).isEqualTo(bc_string);
|
||||||
|
assertThat(bc.hashCode()).isNotEqualTo(abcd.hashCode());
|
||||||
|
assertThat(bc).isNotEqualTo(abcd);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void hashCodeSameAsString() {
|
||||||
|
hashCodeSameAsString("abcABC123xyz!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void hashCodeSameAsStringWithSpecial() {
|
||||||
|
hashCodeSameAsString("special/\u00EB.dat");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void hashCodeSameAsStringWithCyrillicCharacters() {
|
||||||
|
hashCodeSameAsString("\u0432\u0435\u0441\u043D\u0430");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void hashCodeSameAsStringWithEmoji() {
|
||||||
|
hashCodeSameAsString("\ud83d\udca9");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hashCodeSameAsString(String input) {
|
||||||
|
assertThat(new AsciiBytes(input)).hasSameHashCodeAs(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void matchesSameAsString() {
|
||||||
|
matchesSameAsString("abcABC123xyz!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void matchesSameAsStringWithSpecial() {
|
||||||
|
matchesSameAsString("special/\u00EB.dat");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void matchesSameAsStringWithCyrillicCharacters() {
|
||||||
|
matchesSameAsString("\u0432\u0435\u0441\u043D\u0430");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void matchesDifferentLengths() {
|
||||||
|
assertThat(new AsciiBytes("abc").matches("ab", NO_SUFFIX)).isFalse();
|
||||||
|
assertThat(new AsciiBytes("abc").matches("abcd", NO_SUFFIX)).isFalse();
|
||||||
|
assertThat(new AsciiBytes("abc").matches("abc", NO_SUFFIX)).isTrue();
|
||||||
|
assertThat(new AsciiBytes("abc").matches("a", 'b')).isFalse();
|
||||||
|
assertThat(new AsciiBytes("abc").matches("abc", 'd')).isFalse();
|
||||||
|
assertThat(new AsciiBytes("abc").matches("ab", 'c')).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void matchesSuffix() {
|
||||||
|
assertThat(new AsciiBytes("ab").matches("a", 'b')).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void matchesSameAsStringWithEmoji() {
|
||||||
|
matchesSameAsString("\ud83d\udca9");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void hashCodeFromInstanceMatchesHashCodeFromString() {
|
||||||
|
String name = "fonts/宋体/simsun.ttf";
|
||||||
|
assertThat(new AsciiBytes(name).hashCode()).isEqualTo(AsciiBytes.hashCode(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void instanceCreatedFromCharSequenceMatchesSameCharSequence() {
|
||||||
|
String name = "fonts/宋体/simsun.ttf";
|
||||||
|
assertThat(new AsciiBytes(name).matches(name, NO_SUFFIX)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void matchesSameAsString(String input) {
|
||||||
|
assertThat(new AsciiBytes(input).matches(input, NO_SUFFIX)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,139 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.jar;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import org.springframework.boot.loader.TestJarCreator;
|
||||||
|
import org.springframework.boot.loader.data.RandomAccessData;
|
||||||
|
import org.springframework.boot.loader.data.RandomAccessDataFile;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link CentralDirectoryParser}.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class CentralDirectoryParserTests {
|
||||||
|
|
||||||
|
private File jarFile;
|
||||||
|
|
||||||
|
private RandomAccessDataFile jarData;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setup(@TempDir File tempDir) throws Exception {
|
||||||
|
this.jarFile = new File(tempDir, "test.jar");
|
||||||
|
TestJarCreator.createTestJar(this.jarFile);
|
||||||
|
this.jarData = new RandomAccessDataFile(this.jarFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() throws IOException {
|
||||||
|
this.jarData.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void visitsInOrder() throws Exception {
|
||||||
|
MockCentralDirectoryVisitor visitor = new MockCentralDirectoryVisitor();
|
||||||
|
CentralDirectoryParser parser = new CentralDirectoryParser();
|
||||||
|
parser.addVisitor(visitor);
|
||||||
|
parser.parse(this.jarData, false);
|
||||||
|
List<String> invocations = visitor.getInvocations();
|
||||||
|
assertThat(invocations).startsWith("visitStart").endsWith("visitEnd").contains("visitFileHeader");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void visitRecords() throws Exception {
|
||||||
|
Collector collector = new Collector();
|
||||||
|
CentralDirectoryParser parser = new CentralDirectoryParser();
|
||||||
|
parser.addVisitor(collector);
|
||||||
|
parser.parse(this.jarData, false);
|
||||||
|
Iterator<CentralDirectoryFileHeader> headers = collector.getHeaders().iterator();
|
||||||
|
assertThat(headers.next().getName()).hasToString("META-INF/");
|
||||||
|
assertThat(headers.next().getName()).hasToString("META-INF/MANIFEST.MF");
|
||||||
|
assertThat(headers.next().getName()).hasToString("1.dat");
|
||||||
|
assertThat(headers.next().getName()).hasToString("2.dat");
|
||||||
|
assertThat(headers.next().getName()).hasToString("d/");
|
||||||
|
assertThat(headers.next().getName()).hasToString("d/9.dat");
|
||||||
|
assertThat(headers.next().getName()).hasToString("special/");
|
||||||
|
assertThat(headers.next().getName()).hasToString("special/\u00EB.dat");
|
||||||
|
assertThat(headers.next().getName()).hasToString("nested.jar");
|
||||||
|
assertThat(headers.next().getName()).hasToString("another-nested.jar");
|
||||||
|
assertThat(headers.next().getName()).hasToString("space nested.jar");
|
||||||
|
assertThat(headers.next().getName()).hasToString("multi-release.jar");
|
||||||
|
assertThat(headers.hasNext()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Collector implements CentralDirectoryVisitor {
|
||||||
|
|
||||||
|
private final List<CentralDirectoryFileHeader> headers = new ArrayList<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visitStart(CentralDirectoryEndRecord endRecord, RandomAccessData centralDirectoryData) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visitFileHeader(CentralDirectoryFileHeader fileHeader, long dataOffset) {
|
||||||
|
this.headers.add(fileHeader.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visitEnd() {
|
||||||
|
}
|
||||||
|
|
||||||
|
List<CentralDirectoryFileHeader> getHeaders() {
|
||||||
|
return this.headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static class MockCentralDirectoryVisitor implements CentralDirectoryVisitor {
|
||||||
|
|
||||||
|
private final List<String> invocations = new ArrayList<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visitStart(CentralDirectoryEndRecord endRecord, RandomAccessData centralDirectoryData) {
|
||||||
|
this.invocations.add("visitStart");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visitFileHeader(CentralDirectoryFileHeader fileHeader, long dataOffset) {
|
||||||
|
this.invocations.add("visitFileHeader");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visitEnd() {
|
||||||
|
this.invocations.add("visitEnd");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> getInvocations() {
|
||||||
|
return this.invocations;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,210 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.jar;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.URLConnection;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import org.springframework.boot.loader.TestJarCreator;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link Handler}.
|
||||||
|
*
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
*/
|
||||||
|
@ExtendWith(JarUrlProtocolHandler.class)
|
||||||
|
class HandlerTests {
|
||||||
|
|
||||||
|
private final Handler handler = new Handler();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void parseUrlWithJarRootContextAndAbsoluteSpecThatUsesContext() throws MalformedURLException {
|
||||||
|
String spec = "/entry.txt";
|
||||||
|
URL context = createUrl("file:example.jar!/");
|
||||||
|
this.handler.parseURL(context, spec, 0, spec.length());
|
||||||
|
assertThat(context.toExternalForm()).isEqualTo("jar:file:example.jar!/entry.txt");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void parseUrlWithDirectoryEntryContextAndAbsoluteSpecThatUsesContext() throws MalformedURLException {
|
||||||
|
String spec = "/entry.txt";
|
||||||
|
URL context = createUrl("file:example.jar!/dir/");
|
||||||
|
this.handler.parseURL(context, spec, 0, spec.length());
|
||||||
|
assertThat(context.toExternalForm()).isEqualTo("jar:file:example.jar!/entry.txt");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void parseUrlWithJarRootContextAndRelativeSpecThatUsesContext() throws MalformedURLException {
|
||||||
|
String spec = "entry.txt";
|
||||||
|
URL context = createUrl("file:example.jar!/");
|
||||||
|
this.handler.parseURL(context, spec, 0, spec.length());
|
||||||
|
assertThat(context.toExternalForm()).isEqualTo("jar:file:example.jar!/entry.txt");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void parseUrlWithDirectoryEntryContextAndRelativeSpecThatUsesContext() throws MalformedURLException {
|
||||||
|
String spec = "entry.txt";
|
||||||
|
URL context = createUrl("file:example.jar!/dir/");
|
||||||
|
this.handler.parseURL(context, spec, 0, spec.length());
|
||||||
|
assertThat(context.toExternalForm()).isEqualTo("jar:file:example.jar!/dir/entry.txt");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void parseUrlWithFileEntryContextAndRelativeSpecThatUsesContext() throws MalformedURLException {
|
||||||
|
String spec = "entry.txt";
|
||||||
|
URL context = createUrl("file:example.jar!/dir/file");
|
||||||
|
this.handler.parseURL(context, spec, 0, spec.length());
|
||||||
|
assertThat(context.toExternalForm()).isEqualTo("jar:file:example.jar!/dir/entry.txt");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void parseUrlWithSpecThatIgnoresContext() throws MalformedURLException {
|
||||||
|
JarFile.registerUrlProtocolHandler();
|
||||||
|
String spec = "jar:file:/other.jar!/nested!/entry.txt";
|
||||||
|
URL context = createUrl("file:example.jar!/dir/file");
|
||||||
|
this.handler.parseURL(context, spec, 0, spec.length());
|
||||||
|
assertThat(context.toExternalForm()).isEqualTo("jar:jar:file:/other.jar!/nested!/entry.txt");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sameFileReturnsFalseForUrlsWithDifferentProtocols() throws MalformedURLException {
|
||||||
|
assertThat(this.handler.sameFile(new URL("jar:file:foo.jar!/content.txt"), new URL("file:/foo.jar"))).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sameFileReturnsFalseForDifferentFileInSameJar() throws MalformedURLException {
|
||||||
|
assertThat(this.handler.sameFile(new URL("jar:file:foo.jar!/the/path/to/the/first/content.txt"),
|
||||||
|
new URL("jar:file:/foo.jar!/content.txt")))
|
||||||
|
.isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sameFileReturnsFalseForSameFileInDifferentJars() throws MalformedURLException {
|
||||||
|
assertThat(this.handler.sameFile(new URL("jar:file:/the/path/to/the/first.jar!/content.txt"),
|
||||||
|
new URL("jar:file:/second.jar!/content.txt")))
|
||||||
|
.isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sameFileReturnsTrueForSameFileInSameJar() throws MalformedURLException {
|
||||||
|
assertThat(this.handler.sameFile(new URL("jar:file:/the/path/to/the/first.jar!/content.txt"),
|
||||||
|
new URL("jar:file:/the/path/to/the/first.jar!/content.txt")))
|
||||||
|
.isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sameFileReturnsTrueForUrlsThatReferenceSameFileViaNestedArchiveAndFromRootOfJar()
|
||||||
|
throws MalformedURLException {
|
||||||
|
assertThat(this.handler.sameFile(new URL("jar:file:/test.jar!/BOOT-INF/classes!/foo.txt"),
|
||||||
|
new URL("jar:file:/test.jar!/BOOT-INF/classes/foo.txt")))
|
||||||
|
.isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void hashCodesAreEqualForUrlsThatReferenceSameFileViaNestedArchiveAndFromRootOfJar() throws MalformedURLException {
|
||||||
|
assertThat(this.handler.hashCode(new URL("jar:file:/test.jar!/BOOT-INF/classes!/foo.txt")))
|
||||||
|
.isEqualTo(this.handler.hashCode(new URL("jar:file:/test.jar!/BOOT-INF/classes/foo.txt")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void urlWithSpecReferencingParentDirectory() throws MalformedURLException {
|
||||||
|
assertStandardAndCustomHandlerUrlsAreEqual("file:/test.jar!/BOOT-INF/classes!/xsd/directoryA/a.xsd",
|
||||||
|
"../directoryB/c/d/e.xsd");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void urlWithSpecReferencingAncestorDirectoryOutsideJarStopsAtJarRoot() throws MalformedURLException {
|
||||||
|
assertStandardAndCustomHandlerUrlsAreEqual("file:/test.jar!/BOOT-INF/classes!/xsd/directoryA/a.xsd",
|
||||||
|
"../../../../../../directoryB/b.xsd");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void urlWithSpecReferencingCurrentDirectory() throws MalformedURLException {
|
||||||
|
assertStandardAndCustomHandlerUrlsAreEqual("file:/test.jar!/BOOT-INF/classes!/xsd/directoryA/a.xsd",
|
||||||
|
"./directoryB/c/d/e.xsd");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void urlWithRef() throws MalformedURLException {
|
||||||
|
assertStandardAndCustomHandlerUrlsAreEqual("file:/test.jar!/BOOT-INF/classes", "!/foo.txt#alpha");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void urlWithQuery() throws MalformedURLException {
|
||||||
|
assertStandardAndCustomHandlerUrlsAreEqual("file:/test.jar!/BOOT-INF/classes", "!/foo.txt?alpha");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fallbackToJdksJarUrlStreamHandler(@TempDir File tempDir) throws Exception {
|
||||||
|
File testJar = new File(tempDir, "test.jar");
|
||||||
|
TestJarCreator.createTestJar(testJar);
|
||||||
|
URLConnection connection = new URL(null, "jar:" + testJar.toURI().toURL() + "!/nested.jar!/", this.handler)
|
||||||
|
.openConnection();
|
||||||
|
assertThat(connection).isInstanceOf(JarURLConnection.class);
|
||||||
|
((JarURLConnection) connection).getJarFile().close();
|
||||||
|
URLConnection jdkConnection = new URL(null, "jar:file:" + testJar.toURI().toURL() + "!/nested.jar!/",
|
||||||
|
this.handler)
|
||||||
|
.openConnection();
|
||||||
|
assertThat(jdkConnection).isNotInstanceOf(JarURLConnection.class);
|
||||||
|
assertThat(jdkConnection.getClass().getName()).endsWith(".JarURLConnection");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void whenJarHasAPlusInItsPathConnectionJarFileMatchesOriginalJarFile(@TempDir File tempDir) throws Exception {
|
||||||
|
File testJar = new File(tempDir, "t+e+s+t.jar");
|
||||||
|
TestJarCreator.createTestJar(testJar);
|
||||||
|
URL url = new URL(null, "jar:" + testJar.toURI().toURL() + "!/nested.jar!/3.dat", this.handler);
|
||||||
|
JarURLConnection connection = (JarURLConnection) url.openConnection();
|
||||||
|
try (JarFile jarFile = JarFileWrapper.unwrap(connection.getJarFile())) {
|
||||||
|
assertThat(jarFile.getRootJarFile().getFile()).isEqualTo(testJar);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void whenJarHasASpaceInItsPathConnectionJarFileMatchesOriginalJarFile(@TempDir File tempDir) throws Exception {
|
||||||
|
File testJar = new File(tempDir, "t e s t.jar");
|
||||||
|
TestJarCreator.createTestJar(testJar);
|
||||||
|
URL url = new URL(null, "jar:" + testJar.toURI().toURL() + "!/nested.jar!/3.dat", this.handler);
|
||||||
|
JarURLConnection connection = (JarURLConnection) url.openConnection();
|
||||||
|
try (JarFile jarFile = JarFileWrapper.unwrap(connection.getJarFile())) {
|
||||||
|
assertThat(jarFile.getRootJarFile().getFile()).isEqualTo(testJar);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertStandardAndCustomHandlerUrlsAreEqual(String context, String spec) throws MalformedURLException {
|
||||||
|
URL standardUrl = new URL(new URL("jar:" + context), spec);
|
||||||
|
URL customHandlerUrl = new URL(new URL("jar", null, -1, context, this.handler), spec);
|
||||||
|
assertThat(customHandlerUrl).hasToString(standardUrl.toString());
|
||||||
|
assertThat(customHandlerUrl.getFile()).isEqualTo(standardUrl.getFile());
|
||||||
|
assertThat(customHandlerUrl.getPath()).isEqualTo(standardUrl.getPath());
|
||||||
|
assertThat(customHandlerUrl.getQuery()).isEqualTo(standardUrl.getQuery());
|
||||||
|
assertThat(customHandlerUrl.getRef()).isEqualTo(standardUrl.getRef());
|
||||||
|
}
|
||||||
|
|
||||||
|
private URL createUrl(String file) throws MalformedURLException {
|
||||||
|
return new URL("jar", null, -1, file, this.handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,736 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.jar;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.FilePermission;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.URLClassLoader;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.attribute.FileTime;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.jar.JarEntry;
|
||||||
|
import java.util.jar.JarInputStream;
|
||||||
|
import java.util.jar.JarOutputStream;
|
||||||
|
import java.util.jar.Manifest;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
import java.util.zip.CRC32;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipFile;
|
||||||
|
|
||||||
|
import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.Assumptions;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import org.springframework.boot.loader.TestJarCreator;
|
||||||
|
import org.springframework.boot.loader.data.RandomAccessDataFile;
|
||||||
|
import org.springframework.util.FileCopyUtils;
|
||||||
|
import org.springframework.util.StopWatch;
|
||||||
|
import org.springframework.util.StreamUtils;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatIOException;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||||
|
import static org.mockito.BDDMockito.then;
|
||||||
|
import static org.mockito.Mockito.spy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link JarFile}.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @author Martin Lau
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Madhura Bhave
|
||||||
|
*/
|
||||||
|
@ExtendWith(JarUrlProtocolHandler.class)
|
||||||
|
class JarFileTests {
|
||||||
|
|
||||||
|
private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs";
|
||||||
|
|
||||||
|
private static final String HANDLERS_PACKAGE = "org.springframework.boot.loader";
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
File tempDir;
|
||||||
|
|
||||||
|
private File rootJarFile;
|
||||||
|
|
||||||
|
private JarFile jarFile;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setup() throws Exception {
|
||||||
|
this.rootJarFile = new File(this.tempDir, "root.jar");
|
||||||
|
TestJarCreator.createTestJar(this.rootJarFile);
|
||||||
|
this.jarFile = new JarFile(this.rootJarFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() throws Exception {
|
||||||
|
this.jarFile.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void jdkJarFile() throws Exception {
|
||||||
|
// Sanity checks to see how the default jar file operates
|
||||||
|
java.util.jar.JarFile jarFile = new java.util.jar.JarFile(this.rootJarFile);
|
||||||
|
assertThat(jarFile.getComment()).isEqualTo("outer");
|
||||||
|
Enumeration<java.util.jar.JarEntry> entries = jarFile.entries();
|
||||||
|
assertThat(entries.nextElement().getName()).isEqualTo("META-INF/");
|
||||||
|
assertThat(entries.nextElement().getName()).isEqualTo("META-INF/MANIFEST.MF");
|
||||||
|
assertThat(entries.nextElement().getName()).isEqualTo("1.dat");
|
||||||
|
assertThat(entries.nextElement().getName()).isEqualTo("2.dat");
|
||||||
|
assertThat(entries.nextElement().getName()).isEqualTo("d/");
|
||||||
|
assertThat(entries.nextElement().getName()).isEqualTo("d/9.dat");
|
||||||
|
assertThat(entries.nextElement().getName()).isEqualTo("special/");
|
||||||
|
assertThat(entries.nextElement().getName()).isEqualTo("special/\u00EB.dat");
|
||||||
|
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 });
|
||||||
|
assertThat(urlClassLoader.getResource("special/\u00EB.dat")).isNotNull();
|
||||||
|
assertThat(urlClassLoader.getResource("d/9.dat")).isNotNull();
|
||||||
|
urlClassLoader.close();
|
||||||
|
jarFile.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createFromFile() throws Exception {
|
||||||
|
JarFile jarFile = new JarFile(this.rootJarFile);
|
||||||
|
assertThat(jarFile.getName()).isNotNull();
|
||||||
|
jarFile.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getManifest() throws Exception {
|
||||||
|
assertThat(this.jarFile.getManifest().getMainAttributes().getValue("Built-By")).isEqualTo("j1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getManifestEntry() throws Exception {
|
||||||
|
ZipEntry entry = this.jarFile.getJarEntry("META-INF/MANIFEST.MF");
|
||||||
|
Manifest manifest = new Manifest(this.jarFile.getInputStream(entry));
|
||||||
|
assertThat(manifest.getMainAttributes().getValue("Built-By")).isEqualTo("j1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getEntries() {
|
||||||
|
Enumeration<java.util.jar.JarEntry> entries = this.jarFile.entries();
|
||||||
|
assertThat(entries.nextElement().getName()).isEqualTo("META-INF/");
|
||||||
|
assertThat(entries.nextElement().getName()).isEqualTo("META-INF/MANIFEST.MF");
|
||||||
|
assertThat(entries.nextElement().getName()).isEqualTo("1.dat");
|
||||||
|
assertThat(entries.nextElement().getName()).isEqualTo("2.dat");
|
||||||
|
assertThat(entries.nextElement().getName()).isEqualTo("d/");
|
||||||
|
assertThat(entries.nextElement().getName()).isEqualTo("d/9.dat");
|
||||||
|
assertThat(entries.nextElement().getName()).isEqualTo("special/");
|
||||||
|
assertThat(entries.nextElement().getName()).isEqualTo("special/\u00EB.dat");
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getSpecialResourceViaClassLoader() throws Exception {
|
||||||
|
URLClassLoader urlClassLoader = new URLClassLoader(new URL[] { this.jarFile.getUrl() });
|
||||||
|
assertThat(urlClassLoader.getResource("special/\u00EB.dat")).isNotNull();
|
||||||
|
urlClassLoader.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getJarEntry() {
|
||||||
|
java.util.jar.JarEntry entry = this.jarFile.getJarEntry("1.dat");
|
||||||
|
assertThat(entry).isNotNull();
|
||||||
|
assertThat(entry.getName()).isEqualTo("1.dat");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getJarEntryWhenClosed() throws Exception {
|
||||||
|
this.jarFile.close();
|
||||||
|
assertThatZipFileClosedIsThrownBy(() -> this.jarFile.getJarEntry("1.dat"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getInputStream() throws Exception {
|
||||||
|
InputStream inputStream = this.jarFile.getInputStream(this.jarFile.getEntry("1.dat"));
|
||||||
|
assertThat(inputStream.available()).isOne();
|
||||||
|
assertThat(inputStream.read()).isOne();
|
||||||
|
assertThat(inputStream.available()).isZero();
|
||||||
|
assertThat(inputStream.read()).isEqualTo(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getInputStreamWhenClosed() throws Exception {
|
||||||
|
ZipEntry entry = this.jarFile.getEntry("1.dat");
|
||||||
|
this.jarFile.close();
|
||||||
|
assertThatZipFileClosedIsThrownBy(() -> this.jarFile.getInputStream(entry));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getComment() {
|
||||||
|
assertThat(this.jarFile.getComment()).isEqualTo("outer");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getCommentWhenClosed() throws Exception {
|
||||||
|
this.jarFile.close();
|
||||||
|
assertThatZipFileClosedIsThrownBy(() -> this.jarFile.getComment());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getName() {
|
||||||
|
assertThat(this.jarFile.getName()).isEqualTo(this.rootJarFile.getPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void size() throws Exception {
|
||||||
|
try (ZipFile zip = new ZipFile(this.rootJarFile)) {
|
||||||
|
assertThat(this.jarFile).hasSize(zip.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sizeWhenClosed() throws Exception {
|
||||||
|
this.jarFile.close();
|
||||||
|
assertThatZipFileClosedIsThrownBy(() -> this.jarFile.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getEntryTime() throws Exception {
|
||||||
|
java.util.jar.JarFile jdkJarFile = new java.util.jar.JarFile(this.rootJarFile);
|
||||||
|
assertThat(this.jarFile.getEntry("META-INF/MANIFEST.MF").getTime())
|
||||||
|
.isEqualTo(jdkJarFile.getEntry("META-INF/MANIFEST.MF").getTime());
|
||||||
|
jdkJarFile.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void close() throws Exception {
|
||||||
|
RandomAccessDataFile randomAccessDataFile = spy(new RandomAccessDataFile(this.rootJarFile));
|
||||||
|
JarFile jarFile = new JarFile(randomAccessDataFile);
|
||||||
|
jarFile.close();
|
||||||
|
then(randomAccessDataFile).should().close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getUrl() throws Exception {
|
||||||
|
URL url = this.jarFile.getUrl();
|
||||||
|
assertThat(url).hasToString("jar:" + this.rootJarFile.toURI() + "!/");
|
||||||
|
JarURLConnection jarURLConnection = (JarURLConnection) url.openConnection();
|
||||||
|
assertThat(JarFileWrapper.unwrap(jarURLConnection.getJarFile())).isSameAs(this.jarFile);
|
||||||
|
assertThat(jarURLConnection.getJarEntry()).isNull();
|
||||||
|
assertThat(jarURLConnection.getContentLength()).isGreaterThan(1);
|
||||||
|
assertThat(JarFileWrapper.unwrap((java.util.jar.JarFile) jarURLConnection.getContent())).isSameAs(this.jarFile);
|
||||||
|
assertThat(jarURLConnection.getContentType()).isEqualTo("x-java/jar");
|
||||||
|
assertThat(jarURLConnection.getJarFileURL().toURI()).isEqualTo(this.rootJarFile.toURI());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createEntryUrl() throws Exception {
|
||||||
|
URL url = new URL(this.jarFile.getUrl(), "1.dat");
|
||||||
|
assertThat(url).hasToString("jar:" + this.rootJarFile.toURI() + "!/1.dat");
|
||||||
|
JarURLConnection jarURLConnection = (JarURLConnection) url.openConnection();
|
||||||
|
assertThat(JarFileWrapper.unwrap(jarURLConnection.getJarFile())).isSameAs(this.jarFile);
|
||||||
|
assertThat(jarURLConnection.getJarEntry()).isSameAs(this.jarFile.getJarEntry("1.dat"));
|
||||||
|
assertThat(jarURLConnection.getContentLength()).isOne();
|
||||||
|
assertThat(jarURLConnection.getContent()).isInstanceOf(InputStream.class);
|
||||||
|
assertThat(jarURLConnection.getContentType()).isEqualTo("content/unknown");
|
||||||
|
assertThat(jarURLConnection.getPermission()).isInstanceOf(FilePermission.class);
|
||||||
|
FilePermission permission = (FilePermission) jarURLConnection.getPermission();
|
||||||
|
assertThat(permission.getActions()).isEqualTo("read");
|
||||||
|
assertThat(permission.getName()).isEqualTo(this.rootJarFile.getPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getMissingEntryUrl() throws Exception {
|
||||||
|
URL url = new URL(this.jarFile.getUrl(), "missing.dat");
|
||||||
|
assertThat(url).hasToString("jar:" + this.rootJarFile.toURI() + "!/missing.dat");
|
||||||
|
assertThatExceptionOfType(FileNotFoundException.class)
|
||||||
|
.isThrownBy(((JarURLConnection) url.openConnection())::getJarEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getUrlStream() throws Exception {
|
||||||
|
URL url = this.jarFile.getUrl();
|
||||||
|
url.openConnection();
|
||||||
|
assertThatIOException().isThrownBy(url::openStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getEntryUrlStream() throws Exception {
|
||||||
|
URL url = new URL(this.jarFile.getUrl(), "1.dat");
|
||||||
|
url.openConnection();
|
||||||
|
try (InputStream stream = url.openStream()) {
|
||||||
|
assertThat(stream.read()).isOne();
|
||||||
|
assertThat(stream.read()).isEqualTo(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getNestedJarFile() throws Exception {
|
||||||
|
try (JarFile nestedJarFile = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) {
|
||||||
|
assertThat(nestedJarFile.getComment()).isEqualTo("nested");
|
||||||
|
Enumeration<java.util.jar.JarEntry> entries = nestedJarFile.entries();
|
||||||
|
assertThat(entries.nextElement().getName()).isEqualTo("META-INF/");
|
||||||
|
assertThat(entries.nextElement().getName()).isEqualTo("META-INF/MANIFEST.MF");
|
||||||
|
assertThat(entries.nextElement().getName()).isEqualTo("3.dat");
|
||||||
|
assertThat(entries.nextElement().getName()).isEqualTo("4.dat");
|
||||||
|
assertThat(entries.nextElement().getName()).isEqualTo("\u00E4.dat");
|
||||||
|
assertThat(entries.hasMoreElements()).isFalse();
|
||||||
|
|
||||||
|
InputStream inputStream = nestedJarFile.getInputStream(nestedJarFile.getEntry("3.dat"));
|
||||||
|
assertThat(inputStream.read()).isEqualTo(3);
|
||||||
|
assertThat(inputStream.read()).isEqualTo(-1);
|
||||||
|
|
||||||
|
URL url = nestedJarFile.getUrl();
|
||||||
|
assertThat(url).hasToString("jar:" + this.rootJarFile.toURI() + "!/nested.jar!/");
|
||||||
|
JarURLConnection conn = (JarURLConnection) url.openConnection();
|
||||||
|
assertThat(JarFileWrapper.unwrap(conn.getJarFile())).isSameAs(nestedJarFile);
|
||||||
|
assertThat(conn.getJarFileURL()).hasToString("jar:" + this.rootJarFile.toURI() + "!/nested.jar");
|
||||||
|
assertThat(conn.getInputStream()).isNotNull();
|
||||||
|
JarInputStream jarInputStream = new JarInputStream(conn.getInputStream());
|
||||||
|
assertThat(jarInputStream.getNextJarEntry().getName()).isEqualTo("3.dat");
|
||||||
|
assertThat(jarInputStream.getNextJarEntry().getName()).isEqualTo("4.dat");
|
||||||
|
assertThat(jarInputStream.getNextJarEntry().getName()).isEqualTo("\u00E4.dat");
|
||||||
|
jarInputStream.close();
|
||||||
|
assertThat(conn.getPermission()).isInstanceOf(FilePermission.class);
|
||||||
|
FilePermission permission = (FilePermission) conn.getPermission();
|
||||||
|
assertThat(permission.getActions()).isEqualTo("read");
|
||||||
|
assertThat(permission.getName()).isEqualTo(this.rootJarFile.getPath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getNestedJarDirectory() throws Exception {
|
||||||
|
try (JarFile nestedJarFile = this.jarFile.getNestedJarFile(this.jarFile.getEntry("d/"))) {
|
||||||
|
Enumeration<java.util.jar.JarEntry> entries = nestedJarFile.entries();
|
||||||
|
assertThat(entries.nextElement().getName()).isEqualTo("9.dat");
|
||||||
|
assertThat(entries.hasMoreElements()).isFalse();
|
||||||
|
|
||||||
|
try (InputStream inputStream = nestedJarFile.getInputStream(nestedJarFile.getEntry("9.dat"))) {
|
||||||
|
assertThat(inputStream.read()).isEqualTo(9);
|
||||||
|
assertThat(inputStream.read()).isEqualTo(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
URL url = nestedJarFile.getUrl();
|
||||||
|
assertThat(url).hasToString("jar:" + this.rootJarFile.toURI() + "!/d!/");
|
||||||
|
JarURLConnection connection = (JarURLConnection) url.openConnection();
|
||||||
|
assertThat(JarFileWrapper.unwrap(connection.getJarFile())).isSameAs(nestedJarFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getNestedJarEntryUrl() throws Exception {
|
||||||
|
try (JarFile nestedJarFile = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) {
|
||||||
|
URL url = nestedJarFile.getJarEntry("3.dat").getUrl();
|
||||||
|
assertThat(url).hasToString("jar:" + this.rootJarFile.toURI() + "!/nested.jar!/3.dat");
|
||||||
|
try (InputStream inputStream = url.openStream()) {
|
||||||
|
assertThat(inputStream).isNotNull();
|
||||||
|
assertThat(inputStream.read()).isEqualTo(3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createUrlFromString() throws Exception {
|
||||||
|
String spec = "jar:" + this.rootJarFile.toURI() + "!/nested.jar!/3.dat";
|
||||||
|
URL url = new URL(spec);
|
||||||
|
assertThat(url).hasToString(spec);
|
||||||
|
JarURLConnection connection = (JarURLConnection) url.openConnection();
|
||||||
|
try (InputStream inputStream = connection.getInputStream()) {
|
||||||
|
assertThat(inputStream).isNotNull();
|
||||||
|
assertThat(inputStream.read()).isEqualTo(3);
|
||||||
|
assertThat(connection.getURL()).hasToString(spec);
|
||||||
|
assertThat(connection.getJarFileURL()).hasToString("jar:" + this.rootJarFile.toURI() + "!/nested.jar");
|
||||||
|
assertThat(connection.getEntryName()).isEqualTo("3.dat");
|
||||||
|
connection.getJarFile().close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createNonNestedUrlFromString() throws Exception {
|
||||||
|
nonNestedJarFileFromString("jar:" + this.rootJarFile.toURI() + "!/2.dat");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createNonNestedUrlFromPathString() throws Exception {
|
||||||
|
nonNestedJarFileFromString("jar:" + this.rootJarFile.toPath().toUri() + "!/2.dat");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void nonNestedJarFileFromString(String spec) throws Exception {
|
||||||
|
JarFile.registerUrlProtocolHandler();
|
||||||
|
URL url = new URL(spec);
|
||||||
|
assertThat(url).hasToString(spec);
|
||||||
|
JarURLConnection connection = (JarURLConnection) url.openConnection();
|
||||||
|
try (InputStream inputStream = connection.getInputStream()) {
|
||||||
|
assertThat(inputStream).isNotNull();
|
||||||
|
assertThat(inputStream.read()).isEqualTo(2);
|
||||||
|
assertThat(connection.getURL()).hasToString(spec);
|
||||||
|
assertThat(connection.getJarFileURL().toURI()).isEqualTo(this.rootJarFile.toURI());
|
||||||
|
assertThat(connection.getEntryName()).isEqualTo("2.dat");
|
||||||
|
}
|
||||||
|
connection.getJarFile().close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getDirectoryInputStream() throws Exception {
|
||||||
|
InputStream inputStream = this.jarFile.getInputStream(this.jarFile.getEntry("d/"));
|
||||||
|
assertThat(inputStream).isNotNull();
|
||||||
|
assertThat(inputStream.read()).isEqualTo(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getDirectoryInputStreamWithoutSlash() throws Exception {
|
||||||
|
InputStream inputStream = this.jarFile.getInputStream(this.jarFile.getEntry("d"));
|
||||||
|
assertThat(inputStream).isNotNull();
|
||||||
|
assertThat(inputStream.read()).isEqualTo(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sensibleToString() throws Exception {
|
||||||
|
assertThat(this.jarFile).hasToString(this.rootJarFile.getPath());
|
||||||
|
try (JarFile nested = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) {
|
||||||
|
assertThat(nested).hasToString(this.rootJarFile.getPath() + "!/nested.jar");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void verifySignedJar() throws Exception {
|
||||||
|
File signedJarFile = getSignedJarFile();
|
||||||
|
assertThat(signedJarFile).exists();
|
||||||
|
try (java.util.jar.JarFile expected = new java.util.jar.JarFile(signedJarFile)) {
|
||||||
|
try (JarFile actual = new JarFile(signedJarFile)) {
|
||||||
|
StopWatch stopWatch = new StopWatch();
|
||||||
|
Enumeration<JarEntry> actualEntries = actual.entries();
|
||||||
|
while (actualEntries.hasMoreElements()) {
|
||||||
|
JarEntry actualEntry = actualEntries.nextElement();
|
||||||
|
java.util.jar.JarEntry expectedEntry = expected.getJarEntry(actualEntry.getName());
|
||||||
|
StreamUtils.drain(expected.getInputStream(expectedEntry));
|
||||||
|
if (!actualEntry.getName().equals("META-INF/MANIFEST.MF")) {
|
||||||
|
assertThat(actualEntry.getCertificates()).as(actualEntry.getName())
|
||||||
|
.isEqualTo(expectedEntry.getCertificates());
|
||||||
|
assertThat(actualEntry.getCodeSigners()).as(actualEntry.getName())
|
||||||
|
.isEqualTo(expectedEntry.getCodeSigners());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertThat(stopWatch.getTotalTimeSeconds()).isLessThan(3.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private File getSignedJarFile() {
|
||||||
|
String[] entries = System.getProperty("java.class.path").split(System.getProperty("path.separator"));
|
||||||
|
for (String entry : entries) {
|
||||||
|
if (entry.contains("bcprov")) {
|
||||||
|
return new File(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void jarFileWithScriptAtTheStart() throws Exception {
|
||||||
|
File file = new File(this.tempDir, "test.jar");
|
||||||
|
InputStream sourceJarContent = new FileInputStream(this.rootJarFile);
|
||||||
|
FileOutputStream outputStream = new FileOutputStream(file);
|
||||||
|
StreamUtils.copy("#/bin/bash", Charset.defaultCharset(), outputStream);
|
||||||
|
FileCopyUtils.copy(sourceJarContent, outputStream);
|
||||||
|
this.rootJarFile = file;
|
||||||
|
this.jarFile.close();
|
||||||
|
this.jarFile = new JarFile(file);
|
||||||
|
// Call some other tests to verify
|
||||||
|
getEntries();
|
||||||
|
getNestedJarFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void cannotLoadMissingJar() throws Exception {
|
||||||
|
// relates to gh-1070
|
||||||
|
try (JarFile nestedJarFile = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) {
|
||||||
|
URL nestedUrl = nestedJarFile.getUrl();
|
||||||
|
URL url = new URL(nestedUrl, nestedJarFile.getUrl() + "missing.jar!/3.dat");
|
||||||
|
assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(url.openConnection()::getInputStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void registerUrlProtocolHandlerWithNoExistingRegistration() {
|
||||||
|
String original = System.getProperty(PROTOCOL_HANDLER);
|
||||||
|
try {
|
||||||
|
System.clearProperty(PROTOCOL_HANDLER);
|
||||||
|
JarFile.registerUrlProtocolHandler();
|
||||||
|
String protocolHandler = System.getProperty(PROTOCOL_HANDLER);
|
||||||
|
assertThat(protocolHandler).isEqualTo(HANDLERS_PACKAGE);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if (original == null) {
|
||||||
|
System.clearProperty(PROTOCOL_HANDLER);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
System.setProperty(PROTOCOL_HANDLER, original);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void registerUrlProtocolHandlerAddsToExistingRegistration() {
|
||||||
|
String original = System.getProperty(PROTOCOL_HANDLER);
|
||||||
|
try {
|
||||||
|
System.setProperty(PROTOCOL_HANDLER, "com.example");
|
||||||
|
JarFile.registerUrlProtocolHandler();
|
||||||
|
String protocolHandler = System.getProperty(PROTOCOL_HANDLER);
|
||||||
|
assertThat(protocolHandler).isEqualTo("com.example|" + HANDLERS_PACKAGE);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if (original == null) {
|
||||||
|
System.clearProperty(PROTOCOL_HANDLER);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
System.setProperty(PROTOCOL_HANDLER, original);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void jarFileCanBeDeletedOnceItHasBeenClosed() throws Exception {
|
||||||
|
File jar = new File(this.tempDir, "test.jar");
|
||||||
|
TestJarCreator.createTestJar(jar);
|
||||||
|
JarFile jf = new JarFile(jar);
|
||||||
|
jf.close();
|
||||||
|
assertThat(jar.delete()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createUrlFromStringWithContextWhenNotFound() throws Exception {
|
||||||
|
// gh-12483
|
||||||
|
JarURLConnection.setUseFastExceptions(true);
|
||||||
|
try {
|
||||||
|
try (JarFile nested = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) {
|
||||||
|
URL context = nested.getUrl();
|
||||||
|
new URL(context, "jar:" + this.rootJarFile.toURI() + "!/nested.jar!/3.dat").openConnection()
|
||||||
|
.getInputStream()
|
||||||
|
.close();
|
||||||
|
assertThatExceptionOfType(FileNotFoundException.class)
|
||||||
|
.isThrownBy(new URL(context, "jar:" + this.rootJarFile.toURI() + "!/no.dat")
|
||||||
|
.openConnection()::getInputStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
JarURLConnection.setUseFastExceptions(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void multiReleaseEntry() throws Exception {
|
||||||
|
try (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()).isOne();
|
||||||
|
assertThat(inputStream.read()).isEqualTo(Runtime.version().feature());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void zip64JarThatExceedsZipEntryLimitCanBeRead() throws Exception {
|
||||||
|
File zip64Jar = new File(this.tempDir, "zip64.jar");
|
||||||
|
FileCopyUtils.copy(zip64Jar(), zip64Jar);
|
||||||
|
try (JarFile zip64JarFile = new JarFile(zip64Jar)) {
|
||||||
|
List<JarEntry> entries = Collections.list(zip64JarFile.entries());
|
||||||
|
assertThat(entries).hasSize(65537);
|
||||||
|
for (int i = 0; i < entries.size(); i++) {
|
||||||
|
JarEntry entry = entries.get(i);
|
||||||
|
InputStream entryInput = zip64JarFile.getInputStream(entry);
|
||||||
|
assertThat(entryInput).hasContent("Entry " + (i + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void zip64JarThatExceedsZipSizeLimitCanBeRead() throws Exception {
|
||||||
|
Assumptions.assumeTrue(this.tempDir.getFreeSpace() > 6 * 1024 * 1024 * 1024, "Insufficient disk space");
|
||||||
|
File zip64Jar = new File(this.tempDir, "zip64.jar");
|
||||||
|
File entry = new File(this.tempDir, "entry.dat");
|
||||||
|
CRC32 crc32 = new CRC32();
|
||||||
|
try (FileOutputStream entryOut = new FileOutputStream(entry)) {
|
||||||
|
byte[] data = new byte[1024 * 1024];
|
||||||
|
new Random().nextBytes(data);
|
||||||
|
for (int i = 0; i < 1024; i++) {
|
||||||
|
entryOut.write(data);
|
||||||
|
crc32.update(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try (JarOutputStream jarOutput = new JarOutputStream(new FileOutputStream(zip64Jar))) {
|
||||||
|
for (int i = 0; i < 6; i++) {
|
||||||
|
JarEntry storedEntry = new JarEntry("huge-" + i);
|
||||||
|
storedEntry.setSize(entry.length());
|
||||||
|
storedEntry.setCompressedSize(entry.length());
|
||||||
|
storedEntry.setCrc(crc32.getValue());
|
||||||
|
storedEntry.setMethod(ZipEntry.STORED);
|
||||||
|
jarOutput.putNextEntry(storedEntry);
|
||||||
|
try (FileInputStream entryIn = new FileInputStream(entry)) {
|
||||||
|
StreamUtils.copy(entryIn, jarOutput);
|
||||||
|
}
|
||||||
|
jarOutput.closeEntry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try (JarFile zip64JarFile = new JarFile(zip64Jar)) {
|
||||||
|
assertThat(Collections.list(zip64JarFile.entries())).hasSize(6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void nestedZip64JarCanBeRead() throws Exception {
|
||||||
|
File outer = new File(this.tempDir, "outer.jar");
|
||||||
|
try (JarOutputStream jarOutput = new JarOutputStream(new FileOutputStream(outer))) {
|
||||||
|
JarEntry nestedEntry = new JarEntry("nested-zip64.jar");
|
||||||
|
byte[] contents = zip64Jar();
|
||||||
|
nestedEntry.setSize(contents.length);
|
||||||
|
nestedEntry.setCompressedSize(contents.length);
|
||||||
|
CRC32 crc32 = new CRC32();
|
||||||
|
crc32.update(contents);
|
||||||
|
nestedEntry.setCrc(crc32.getValue());
|
||||||
|
nestedEntry.setMethod(ZipEntry.STORED);
|
||||||
|
jarOutput.putNextEntry(nestedEntry);
|
||||||
|
jarOutput.write(contents);
|
||||||
|
jarOutput.closeEntry();
|
||||||
|
}
|
||||||
|
try (JarFile outerJarFile = new JarFile(outer)) {
|
||||||
|
try (JarFile nestedZip64JarFile = outerJarFile
|
||||||
|
.getNestedJarFile(outerJarFile.getJarEntry("nested-zip64.jar"))) {
|
||||||
|
List<JarEntry> entries = Collections.list(nestedZip64JarFile.entries());
|
||||||
|
assertThat(entries).hasSize(65537);
|
||||||
|
for (int i = 0; i < entries.size(); i++) {
|
||||||
|
JarEntry entry = entries.get(i);
|
||||||
|
InputStream entryInput = nestedZip64JarFile.getInputStream(entry);
|
||||||
|
assertThat(entryInput).hasContent("Entry " + (i + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] zip64Jar() throws IOException {
|
||||||
|
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||||
|
JarOutputStream jarOutput = new JarOutputStream(bytes);
|
||||||
|
for (int i = 0; i < 65537; i++) {
|
||||||
|
jarOutput.putNextEntry(new JarEntry(i + ".dat"));
|
||||||
|
jarOutput.write(("Entry " + (i + 1)).getBytes(StandardCharsets.UTF_8));
|
||||||
|
jarOutput.closeEntry();
|
||||||
|
}
|
||||||
|
jarOutput.close();
|
||||||
|
return bytes.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void jarFileEntryWithEpochTimeOfZeroShouldNotFail() throws Exception {
|
||||||
|
File file = createJarFileWithEpochTimeOfZero();
|
||||||
|
try (JarFile jar = new JarFile(file)) {
|
||||||
|
Enumeration<java.util.jar.JarEntry> entries = jar.entries();
|
||||||
|
JarEntry entry = entries.nextElement();
|
||||||
|
assertThat(entry.getLastModifiedTime().toInstant()).isEqualTo(Instant.EPOCH);
|
||||||
|
assertThat(entry.getName()).isEqualTo("1.dat");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private File createJarFileWithEpochTimeOfZero() throws Exception {
|
||||||
|
File jarFile = new File(this.tempDir, "temp.jar");
|
||||||
|
FileOutputStream fileOutputStream = new FileOutputStream(jarFile);
|
||||||
|
String comment = "outer";
|
||||||
|
try (JarOutputStream jarOutputStream = new JarOutputStream(fileOutputStream)) {
|
||||||
|
jarOutputStream.setComment(comment);
|
||||||
|
JarEntry entry = new JarEntry("1.dat");
|
||||||
|
entry.setLastModifiedTime(FileTime.from(Instant.EPOCH));
|
||||||
|
jarOutputStream.putNextEntry(entry);
|
||||||
|
jarOutputStream.write(new byte[] { (byte) 1 });
|
||||||
|
jarOutputStream.closeEntry();
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] data = Files.readAllBytes(jarFile.toPath());
|
||||||
|
int headerPosition = data.length - ZipFile.ENDHDR - comment.getBytes().length;
|
||||||
|
int centralHeaderPosition = (int) Bytes.littleEndianValue(data, headerPosition + ZipFile.ENDOFF, 1);
|
||||||
|
int localHeaderPosition = (int) Bytes.littleEndianValue(data, centralHeaderPosition + ZipFile.CENOFF, 1);
|
||||||
|
writeTimeBlock(data, centralHeaderPosition + ZipFile.CENTIM, 0);
|
||||||
|
writeTimeBlock(data, localHeaderPosition + ZipFile.LOCTIM, 0);
|
||||||
|
|
||||||
|
File jar = new File(this.tempDir, "zerotimed.jar");
|
||||||
|
Files.write(jar.toPath(), data);
|
||||||
|
return jar;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeTimeBlock(byte[] data, int pos, int value) {
|
||||||
|
data[pos] = (byte) (value & 0xff);
|
||||||
|
data[pos + 1] = (byte) ((value >> 8) & 0xff);
|
||||||
|
data[pos + 2] = (byte) ((value >> 16) & 0xff);
|
||||||
|
data[pos + 3] = (byte) ((value >> 24) & 0xff);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void iterator() {
|
||||||
|
Iterator<JarEntry> iterator = this.jarFile.iterator();
|
||||||
|
List<String> names = new ArrayList<>();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
names.add(iterator.next().getName());
|
||||||
|
}
|
||||||
|
assertThat(names).hasSize(12).contains("1.dat");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void iteratorWhenClosed() throws IOException {
|
||||||
|
this.jarFile.close();
|
||||||
|
assertThatZipFileClosedIsThrownBy(() -> this.jarFile.iterator());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void iteratorWhenClosedLater() throws IOException {
|
||||||
|
Iterator<JarEntry> iterator = this.jarFile.iterator();
|
||||||
|
iterator.next();
|
||||||
|
this.jarFile.close();
|
||||||
|
assertThatZipFileClosedIsThrownBy(() -> iterator.hasNext());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void stream() {
|
||||||
|
Stream<String> stream = this.jarFile.stream().map(JarEntry::getName);
|
||||||
|
assertThat(stream).hasSize(12).contains("1.dat");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertThatZipFileClosedIsThrownBy(ThrowingCallable throwingCallable) {
|
||||||
|
assertThatIllegalStateException().isThrownBy(throwingCallable).withMessage("zip file closed");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,281 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.jar;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.security.Permission;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.jar.JarOutputStream;
|
||||||
|
import java.util.jar.Manifest;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import org.springframework.boot.loader.jar.JarFileWrapperTests.SpyJarFile.Call;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link JarFileWrapper}.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class JarFileWrapperTests {
|
||||||
|
|
||||||
|
private SpyJarFile parent;
|
||||||
|
|
||||||
|
private JarFileWrapper wrapper;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setup(@TempDir File temp) throws Exception {
|
||||||
|
this.parent = new SpyJarFile(createTempJar(temp));
|
||||||
|
this.wrapper = new JarFileWrapper(this.parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void cleanup() throws Exception {
|
||||||
|
this.parent.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private File createTempJar(File temp) throws IOException {
|
||||||
|
File file = new File(temp, "temp.jar");
|
||||||
|
new JarOutputStream(new FileOutputStream(file)).close();
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getUrlDelegatesToParent() throws MalformedURLException {
|
||||||
|
this.wrapper.getUrl();
|
||||||
|
this.parent.verify(Call.GET_URL);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getTypeDelegatesToParent() {
|
||||||
|
this.wrapper.getType();
|
||||||
|
this.parent.verify(Call.GET_TYPE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getPermissionDelegatesToParent() {
|
||||||
|
this.wrapper.getPermission();
|
||||||
|
this.parent.verify(Call.GET_PERMISSION);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getManifestDelegatesToParent() throws IOException {
|
||||||
|
this.wrapper.getManifest();
|
||||||
|
this.parent.verify(Call.GET_MANIFEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void entriesDelegatesToParent() {
|
||||||
|
this.wrapper.entries();
|
||||||
|
this.parent.verify(Call.ENTRIES);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getJarEntryDelegatesToParent() {
|
||||||
|
this.wrapper.getJarEntry("test");
|
||||||
|
this.parent.verify(Call.GET_JAR_ENTRY);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getEntryDelegatesToParent() {
|
||||||
|
this.wrapper.getEntry("test");
|
||||||
|
this.parent.verify(Call.GET_ENTRY);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getInputStreamDelegatesToParent() throws IOException {
|
||||||
|
this.wrapper.getInputStream();
|
||||||
|
this.parent.verify(Call.GET_INPUT_STREAM);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getEntryInputStreamDelegatesToParent() throws IOException {
|
||||||
|
ZipEntry entry = new ZipEntry("test");
|
||||||
|
this.wrapper.getInputStream(entry);
|
||||||
|
this.parent.verify(Call.GET_ENTRY_INPUT_STREAM);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getCommentDelegatesToParent() {
|
||||||
|
this.wrapper.getComment();
|
||||||
|
this.parent.verify(Call.GET_COMMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sizeDelegatesToParent() {
|
||||||
|
this.wrapper.size();
|
||||||
|
this.parent.verify(Call.SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void toStringDelegatesToParent() {
|
||||||
|
assertThat(this.wrapper.toString()).endsWith("temp.jar");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test // gh-22991
|
||||||
|
void wrapperMustNotImplementClose() {
|
||||||
|
// If the wrapper overrides close then on Java 11 a FinalizableResource
|
||||||
|
// instance will be used to perform cleanup. This can result in a lot
|
||||||
|
// of additional memory being used since cleanup only occurs when the
|
||||||
|
// finalizer thread runs. See gh-22991
|
||||||
|
assertThatExceptionOfType(NoSuchMethodException.class)
|
||||||
|
.isThrownBy(() -> JarFileWrapper.class.getDeclaredMethod("close"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void streamDelegatesToParent() {
|
||||||
|
this.wrapper.stream();
|
||||||
|
this.parent.verify(Call.STREAM);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link JarFile} that we can spy (even on Java 11+)
|
||||||
|
*/
|
||||||
|
static class SpyJarFile extends JarFile {
|
||||||
|
|
||||||
|
private final Set<Call> calls = EnumSet.noneOf(Call.class);
|
||||||
|
|
||||||
|
SpyJarFile(File file) throws IOException {
|
||||||
|
super(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
Permission getPermission() {
|
||||||
|
mark(Call.GET_PERMISSION);
|
||||||
|
return super.getPermission();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Manifest getManifest() throws IOException {
|
||||||
|
mark(Call.GET_MANIFEST);
|
||||||
|
return super.getManifest();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Enumeration<java.util.jar.JarEntry> entries() {
|
||||||
|
mark(Call.ENTRIES);
|
||||||
|
return super.entries();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Stream<java.util.jar.JarEntry> stream() {
|
||||||
|
mark(Call.STREAM);
|
||||||
|
return super.stream();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JarEntry getJarEntry(String name) {
|
||||||
|
mark(Call.GET_JAR_ENTRY);
|
||||||
|
return super.getJarEntry(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ZipEntry getEntry(String name) {
|
||||||
|
mark(Call.GET_ENTRY);
|
||||||
|
return super.getEntry(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
InputStream getInputStream() throws IOException {
|
||||||
|
mark(Call.GET_INPUT_STREAM);
|
||||||
|
return super.getInputStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
InputStream getInputStream(String name) throws IOException {
|
||||||
|
mark(Call.GET_ENTRY_INPUT_STREAM);
|
||||||
|
return super.getInputStream(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getComment() {
|
||||||
|
mark(Call.GET_COMMENT);
|
||||||
|
return super.getComment();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int size() {
|
||||||
|
mark(Call.SIZE);
|
||||||
|
return super.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public URL getUrl() throws MalformedURLException {
|
||||||
|
mark(Call.GET_URL);
|
||||||
|
return super.getUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
JarFileType getType() {
|
||||||
|
mark(Call.GET_TYPE);
|
||||||
|
return super.getType();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void mark(Call call) {
|
||||||
|
this.calls.add(call);
|
||||||
|
}
|
||||||
|
|
||||||
|
void verify(Call call) {
|
||||||
|
assertThat(call).matches(this.calls::contains);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Call {
|
||||||
|
|
||||||
|
GET_URL,
|
||||||
|
|
||||||
|
GET_TYPE,
|
||||||
|
|
||||||
|
GET_PERMISSION,
|
||||||
|
|
||||||
|
GET_MANIFEST,
|
||||||
|
|
||||||
|
ENTRIES,
|
||||||
|
|
||||||
|
GET_JAR_ENTRY,
|
||||||
|
|
||||||
|
GET_ENTRY,
|
||||||
|
|
||||||
|
GET_INPUT_STREAM,
|
||||||
|
|
||||||
|
GET_ENTRY_INPUT_STREAM,
|
||||||
|
|
||||||
|
GET_COMMENT,
|
||||||
|
|
||||||
|
SIZE,
|
||||||
|
|
||||||
|
STREAM
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,246 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.jar;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.jar.JarEntry;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import org.springframework.boot.loader.TestJarCreator;
|
||||||
|
import org.springframework.boot.loader.jar.JarURLConnection.JarEntryName;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link JarURLConnection}.
|
||||||
|
*
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @author Rostyslav Dudka
|
||||||
|
*/
|
||||||
|
class JarURLConnectionTests {
|
||||||
|
|
||||||
|
private File rootJarFile;
|
||||||
|
|
||||||
|
private JarFile jarFile;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setup(@TempDir File tempDir) throws Exception {
|
||||||
|
this.rootJarFile = new File(tempDir, "root.jar");
|
||||||
|
TestJarCreator.createTestJar(this.rootJarFile);
|
||||||
|
this.jarFile = new JarFile(this.rootJarFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() throws Exception {
|
||||||
|
this.jarFile.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void connectionToRootUsingAbsoluteUrl() throws Exception {
|
||||||
|
URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/");
|
||||||
|
Object content = JarURLConnection.get(url, this.jarFile).getContent();
|
||||||
|
assertThat(JarFileWrapper.unwrap((java.util.jar.JarFile) content)).isSameAs(this.jarFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void connectionToRootUsingRelativeUrl() throws Exception {
|
||||||
|
URL url = new URL("jar:file:" + getRelativePath() + "!/");
|
||||||
|
Object content = JarURLConnection.get(url, this.jarFile).getContent();
|
||||||
|
assertThat(JarFileWrapper.unwrap((java.util.jar.JarFile) content)).isSameAs(this.jarFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void connectionToEntryUsingAbsoluteUrl() throws Exception {
|
||||||
|
URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/1.dat");
|
||||||
|
try (InputStream input = JarURLConnection.get(url, this.jarFile).getInputStream()) {
|
||||||
|
assertThat(input).hasBinaryContent(new byte[] { 1 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void connectionToEntryUsingRelativeUrl() throws Exception {
|
||||||
|
URL url = new URL("jar:file:" + getRelativePath() + "!/1.dat");
|
||||||
|
try (InputStream input = JarURLConnection.get(url, this.jarFile).getInputStream()) {
|
||||||
|
assertThat(input).hasBinaryContent(new byte[] { 1 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void connectionToEntryUsingAbsoluteUrlWithFileColonSlashSlashPrefix() throws Exception {
|
||||||
|
URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/1.dat");
|
||||||
|
try (InputStream input = JarURLConnection.get(url, this.jarFile).getInputStream()) {
|
||||||
|
assertThat(input).hasBinaryContent(new byte[] { 1 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void connectionToEntryUsingAbsoluteUrlForNestedEntry() throws Exception {
|
||||||
|
URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/nested.jar!/3.dat");
|
||||||
|
JarURLConnection connection = JarURLConnection.get(url, this.jarFile);
|
||||||
|
try (InputStream input = connection.getInputStream()) {
|
||||||
|
assertThat(input).hasBinaryContent(new byte[] { 3 });
|
||||||
|
}
|
||||||
|
connection.getJarFile().close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void connectionToEntryUsingRelativeUrlForNestedEntry() throws Exception {
|
||||||
|
URL url = new URL("jar:file:" + getRelativePath() + "!/nested.jar!/3.dat");
|
||||||
|
JarURLConnection connection = JarURLConnection.get(url, this.jarFile);
|
||||||
|
try (InputStream input = connection.getInputStream()) {
|
||||||
|
assertThat(input).hasBinaryContent(new byte[] { 3 });
|
||||||
|
}
|
||||||
|
connection.getJarFile().close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void connectionToEntryUsingAbsoluteUrlForEntryFromNestedJarFile() throws Exception {
|
||||||
|
URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/nested.jar!/3.dat");
|
||||||
|
try (JarFile nested = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) {
|
||||||
|
try (InputStream input = JarURLConnection.get(url, nested).getInputStream()) {
|
||||||
|
assertThat(input).hasBinaryContent(new byte[] { 3 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void connectionToEntryUsingRelativeUrlForEntryFromNestedJarFile() throws Exception {
|
||||||
|
URL url = new URL("jar:file:" + getRelativePath() + "!/nested.jar!/3.dat");
|
||||||
|
try (JarFile nested = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) {
|
||||||
|
try (InputStream input = JarURLConnection.get(url, nested).getInputStream()) {
|
||||||
|
assertThat(input).hasBinaryContent(new byte[] { 3 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void connectionToEntryInNestedJarFromUrlThatUsesExistingUrlAsContext() throws Exception {
|
||||||
|
URL url = new URL(new URL("jar", null, -1, this.rootJarFile.toURI().toURL() + "!/nested.jar!/", new Handler()),
|
||||||
|
"/3.dat");
|
||||||
|
try (JarFile nested = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) {
|
||||||
|
try (InputStream input = JarURLConnection.get(url, nested).getInputStream()) {
|
||||||
|
assertThat(input).hasBinaryContent(new byte[] { 3 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void connectionToEntryWithSpaceNestedEntry() throws Exception {
|
||||||
|
URL url = new URL("jar:file:" + getRelativePath() + "!/space nested.jar!/3.dat");
|
||||||
|
JarURLConnection connection = JarURLConnection.get(url, this.jarFile);
|
||||||
|
try (InputStream input = connection.getInputStream()) {
|
||||||
|
assertThat(input).hasBinaryContent(new byte[] { 3 });
|
||||||
|
}
|
||||||
|
connection.getJarFile().close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void connectionToEntryWithEncodedSpaceNestedEntry() throws Exception {
|
||||||
|
URL url = new URL("jar:file:" + getRelativePath() + "!/space%20nested.jar!/3.dat");
|
||||||
|
JarURLConnection connection = JarURLConnection.get(url, this.jarFile);
|
||||||
|
try (InputStream input = connection.getInputStream()) {
|
||||||
|
assertThat(input).hasBinaryContent(new byte[] { 3 });
|
||||||
|
}
|
||||||
|
connection.getJarFile().close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void connectionToEntryUsingWrongAbsoluteUrlForEntryFromNestedJarFile() throws Exception {
|
||||||
|
URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/w.jar!/3.dat");
|
||||||
|
try (JarFile nested = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) {
|
||||||
|
assertThatExceptionOfType(FileNotFoundException.class)
|
||||||
|
.isThrownBy(JarURLConnection.get(url, nested)::getInputStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getContentLengthReturnsLengthOfUnderlyingEntry() throws Exception {
|
||||||
|
URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/nested.jar!/3.dat");
|
||||||
|
try (JarFile nested = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) {
|
||||||
|
JarURLConnection connection = JarURLConnection.get(url, nested);
|
||||||
|
assertThat(connection.getContentLength()).isOne();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getContentLengthLongReturnsLengthOfUnderlyingEntry() throws Exception {
|
||||||
|
URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/nested.jar!/3.dat");
|
||||||
|
try (JarFile nested = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) {
|
||||||
|
JarURLConnection connection = JarURLConnection.get(url, nested);
|
||||||
|
assertThat(connection.getContentLengthLong()).isOne();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getLastModifiedReturnsLastModifiedTimeOfJarEntry() throws Exception {
|
||||||
|
URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/1.dat");
|
||||||
|
JarURLConnection connection = JarURLConnection.get(url, this.jarFile);
|
||||||
|
assertThat(connection.getLastModified()).isEqualTo(connection.getJarEntry().getTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void entriesCanBeStreamedFromJarFileOfConnection() throws Exception {
|
||||||
|
URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/");
|
||||||
|
JarURLConnection connection = JarURLConnection.get(url, this.jarFile);
|
||||||
|
List<String> entryNames = connection.getJarFile().stream().map(JarEntry::getName).toList();
|
||||||
|
assertThat(entryNames).hasSize(12);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void jarEntryBasicName() {
|
||||||
|
assertThat(new JarEntryName(new StringSequence("a/b/C.class"))).hasToString("a/b/C.class");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void jarEntryNameWithSingleByteEncodedCharacters() {
|
||||||
|
assertThat(new JarEntryName(new StringSequence("%61/%62/%43.class"))).hasToString("a/b/C.class");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void jarEntryNameWithDoubleByteEncodedCharacters() {
|
||||||
|
assertThat(new JarEntryName(new StringSequence("%c3%a1/b/C.class"))).hasToString("\u00e1/b/C.class");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void jarEntryNameWithMixtureOfEncodedAndUnencodedDoubleByteCharacters() {
|
||||||
|
assertThat(new JarEntryName(new StringSequence("%c3%a1/b/\u00c7.class"))).hasToString("\u00e1/b/\u00c7.class");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void openConnectionCanBeClosedWithoutClosingSourceJar() throws Exception {
|
||||||
|
URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/");
|
||||||
|
JarURLConnection connection = JarURLConnection.get(url, this.jarFile);
|
||||||
|
java.util.jar.JarFile connectionJarFile = connection.getJarFile();
|
||||||
|
connectionJarFile.close();
|
||||||
|
assertThat(this.jarFile.isClosed()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getRelativePath() {
|
||||||
|
return this.rootJarFile.getPath().replace('\\', '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.jar;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.lang.ref.SoftReference;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.extension.AfterEachCallback;
|
||||||
|
import org.junit.jupiter.api.extension.BeforeEachCallback;
|
||||||
|
import org.junit.jupiter.api.extension.Extension;
|
||||||
|
import org.junit.jupiter.api.extension.ExtensionContext;
|
||||||
|
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JUnit 5 {@link Extension} for tests that interact with Spring Boot's {@link Handler}
|
||||||
|
* for {@code jar:} URLs. Ensures that the handler is registered prior to test execution
|
||||||
|
* and cleans up the handler's root file cache afterwards.
|
||||||
|
*
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
*/
|
||||||
|
class JarUrlProtocolHandler implements BeforeEachCallback, AfterEachCallback {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeEach(ExtensionContext context) throws Exception {
|
||||||
|
JarFile.registerUrlProtocolHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public void afterEach(ExtensionContext context) throws Exception {
|
||||||
|
Map<File, JarFile> rootFileCache = ((SoftReference<Map<File, JarFile>>) ReflectionTestUtils
|
||||||
|
.getField(Handler.class, "rootFileCache")).get();
|
||||||
|
if (rootFileCache != null) {
|
||||||
|
for (JarFile rootJarFile : rootFileCache.values()) {
|
||||||
|
rootJarFile.close();
|
||||||
|
}
|
||||||
|
rootFileCache.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,220 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.jar;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatNullPointerException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link StringSequence}.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class StringSequenceTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createWhenSourceIsNullShouldThrowException() {
|
||||||
|
assertThatNullPointerException().isThrownBy(() -> new StringSequence(null))
|
||||||
|
.withMessage("Source must not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createWithIndexWhenSourceIsNullShouldThrowException() {
|
||||||
|
assertThatNullPointerException().isThrownBy(() -> new StringSequence(null, 0, 0))
|
||||||
|
.withMessage("Source must not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createWhenStartIsLessThanZeroShouldThrowException() {
|
||||||
|
assertThatExceptionOfType(StringIndexOutOfBoundsException.class)
|
||||||
|
.isThrownBy(() -> new StringSequence("x", -1, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createWhenEndIsGreaterThanLengthShouldThrowException() {
|
||||||
|
assertThatExceptionOfType(StringIndexOutOfBoundsException.class)
|
||||||
|
.isThrownBy(() -> new StringSequence("x", 0, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createFromString() {
|
||||||
|
assertThat(new StringSequence("test")).hasToString("test");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void subSequenceWithJustStartShouldReturnSubSequence() {
|
||||||
|
assertThat(new StringSequence("smiles").subSequence(1)).hasToString("miles");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void subSequenceShouldReturnSubSequence() {
|
||||||
|
assertThat(new StringSequence("hamburger").subSequence(4, 8)).hasToString("urge");
|
||||||
|
assertThat(new StringSequence("smiles").subSequence(1, 5)).hasToString("mile");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void subSequenceWhenCalledMultipleTimesShouldReturnSubSequence() {
|
||||||
|
assertThat(new StringSequence("hamburger").subSequence(4, 8).subSequence(1, 3)).hasToString("rg");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void subSequenceWhenEndPastExistingEndShouldThrowException() {
|
||||||
|
StringSequence sequence = new StringSequence("abcde").subSequence(1, 4);
|
||||||
|
assertThat(sequence).hasToString("bcd");
|
||||||
|
assertThat(sequence.subSequence(2, 3)).hasToString("d");
|
||||||
|
assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> sequence.subSequence(3, 4));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void subSequenceWhenStartPastExistingEndShouldThrowException() {
|
||||||
|
StringSequence sequence = new StringSequence("abcde").subSequence(1, 4);
|
||||||
|
assertThat(sequence).hasToString("bcd");
|
||||||
|
assertThat(sequence.subSequence(2, 3)).hasToString("d");
|
||||||
|
assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> sequence.subSequence(4, 3));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isEmptyWhenEmptyShouldReturnTrue() {
|
||||||
|
assertThat(new StringSequence("").isEmpty()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isEmptyWhenNotEmptyShouldReturnFalse() {
|
||||||
|
assertThat(new StringSequence("x").isEmpty()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void lengthShouldReturnLength() {
|
||||||
|
StringSequence sequence = new StringSequence("hamburger");
|
||||||
|
assertThat(sequence).hasSize(9);
|
||||||
|
assertThat(sequence.subSequence(4, 8)).hasSize(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void charAtShouldReturnChar() {
|
||||||
|
StringSequence sequence = new StringSequence("hamburger");
|
||||||
|
assertThat(sequence.charAt(0)).isEqualTo('h');
|
||||||
|
assertThat(sequence.charAt(1)).isEqualTo('a');
|
||||||
|
assertThat(sequence.subSequence(4, 8).charAt(0)).isEqualTo('u');
|
||||||
|
assertThat(sequence.subSequence(4, 8).charAt(1)).isEqualTo('r');
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void indexOfCharShouldReturnIndexOf() {
|
||||||
|
StringSequence sequence = new StringSequence("aabbaacc");
|
||||||
|
assertThat(sequence.indexOf('a')).isZero();
|
||||||
|
assertThat(sequence.indexOf('b')).isEqualTo(2);
|
||||||
|
assertThat(sequence.subSequence(2).indexOf('a')).isEqualTo(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void indexOfStringShouldReturnIndexOf() {
|
||||||
|
StringSequence sequence = new StringSequence("aabbaacc");
|
||||||
|
assertThat(sequence.indexOf('a')).isZero();
|
||||||
|
assertThat(sequence.indexOf('b')).isEqualTo(2);
|
||||||
|
assertThat(sequence.subSequence(2).indexOf('a')).isEqualTo(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void indexOfStringFromIndexShouldReturnIndexOf() {
|
||||||
|
StringSequence sequence = new StringSequence("aabbaacc");
|
||||||
|
assertThat(sequence.indexOf("a", 2)).isEqualTo(4);
|
||||||
|
assertThat(sequence.indexOf("b", 3)).isEqualTo(3);
|
||||||
|
assertThat(sequence.subSequence(2).indexOf("a", 3)).isEqualTo(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void hashCodeShouldBeSameAsString() {
|
||||||
|
assertThat(new StringSequence("hamburger")).hasSameHashCodeAs("hamburger");
|
||||||
|
assertThat(new StringSequence("hamburger").subSequence(4, 8)).hasSameHashCodeAs("urge");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void equalsWhenSameContentShouldMatch() {
|
||||||
|
StringSequence a = new StringSequence("hamburger").subSequence(4, 8);
|
||||||
|
StringSequence b = new StringSequence("urge");
|
||||||
|
StringSequence c = new StringSequence("urgh");
|
||||||
|
assertThat(a).isEqualTo(b).isNotEqualTo(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void notEqualsWhenSequencesOfDifferentLength() {
|
||||||
|
StringSequence a = new StringSequence("abcd");
|
||||||
|
StringSequence b = new StringSequence("ef");
|
||||||
|
assertThat(a).isNotEqualTo(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void startsWithWhenExactMatch() {
|
||||||
|
assertThat(new StringSequence("abc").startsWith("abc")).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void startsWithWhenLongerAndStartsWith() {
|
||||||
|
assertThat(new StringSequence("abcd").startsWith("abc")).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void startsWithWhenLongerAndDoesNotStartWith() {
|
||||||
|
assertThat(new StringSequence("abcd").startsWith("abx")).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void startsWithWhenShorterAndDoesNotStartWith() {
|
||||||
|
assertThat(new StringSequence("ab").startsWith("abc")).isFalse();
|
||||||
|
assertThat(new StringSequence("ab").startsWith("c")).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void startsWithOffsetWhenExactMatch() {
|
||||||
|
assertThat(new StringSequence("xabc").startsWith("abc", 1)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void startsWithOffsetWhenLongerAndStartsWith() {
|
||||||
|
assertThat(new StringSequence("xabcd").startsWith("abc", 1)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void startsWithOffsetWhenLongerAndDoesNotStartWith() {
|
||||||
|
assertThat(new StringSequence("xabcd").startsWith("abx", 1)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void startsWithOffsetWhenShorterAndDoesNotStartWith() {
|
||||||
|
assertThat(new StringSequence("xab").startsWith("abc", 1)).isFalse();
|
||||||
|
assertThat(new StringSequence("xab").startsWith("c", 1)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void startsWithOnSubstringTailWhenMatch() {
|
||||||
|
StringSequence subSequence = new StringSequence("xabc").subSequence(1);
|
||||||
|
assertThat(subSequence.startsWith("abc")).isTrue();
|
||||||
|
assertThat(subSequence.startsWith("abcd")).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void startsWithOnSubstringMiddleWhenMatch() {
|
||||||
|
StringSequence subSequence = new StringSequence("xabc").subSequence(1, 3);
|
||||||
|
assertThat(subSequence.startsWith("ab")).isTrue();
|
||||||
|
assertThat(subSequence.startsWith("abc")).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.jarmode;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Iterator;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
|
||||||
|
import org.springframework.boot.loader.Launcher;
|
||||||
|
import org.springframework.boot.loader.archive.Archive;
|
||||||
|
import org.springframework.boot.testsupport.system.CapturedOutput;
|
||||||
|
import org.springframework.boot.testsupport.system.OutputCaptureExtension;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link Launcher} with jar mode support.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
@ExtendWith(OutputCaptureExtension.class)
|
||||||
|
class LauncherJarModeTests {
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setup() {
|
||||||
|
System.setProperty(JarModeLauncher.DISABLE_SYSTEM_EXIT, "true");
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void cleanup() {
|
||||||
|
System.clearProperty("jarmode");
|
||||||
|
System.clearProperty(JarModeLauncher.DISABLE_SYSTEM_EXIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void launchWhenJarModePropertyIsSetLaunchesJarMode(CapturedOutput out) throws Exception {
|
||||||
|
System.setProperty("jarmode", "test");
|
||||||
|
new TestLauncher().launch(new String[] { "boot" });
|
||||||
|
assertThat(out).contains("running in test jar mode [boot]");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void launchWhenJarModePropertyIsNotAcceptedThrowsException(CapturedOutput out) throws Exception {
|
||||||
|
System.setProperty("jarmode", "idontexist");
|
||||||
|
new TestLauncher().launch(new String[] { "boot" });
|
||||||
|
assertThat(out).contains("Unsupported jarmode 'idontexist'");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class TestLauncher extends Launcher {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getMainClass() throws Exception {
|
||||||
|
throw new IllegalStateException("Should not be called");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Iterator<Archive> getClassPathArchivesIterator() throws Exception {
|
||||||
|
return Collections.emptyIterator();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void launch(String[] args) throws Exception {
|
||||||
|
super.launch(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.util;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link SystemPropertyUtils}.
|
||||||
|
*
|
||||||
|
* @author Dave Syer
|
||||||
|
*/
|
||||||
|
class SystemPropertyUtilsTests {
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void init() {
|
||||||
|
System.setProperty("foo", "bar");
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void close() {
|
||||||
|
System.clearProperty("foo");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testVanillaPlaceholder() {
|
||||||
|
assertThat(SystemPropertyUtils.resolvePlaceholders("${foo}")).isEqualTo("bar");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDefaultValue() {
|
||||||
|
assertThat(SystemPropertyUtils.resolvePlaceholders("${bar:foo}")).isEqualTo("foo");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testNestedPlaceholder() {
|
||||||
|
assertThat(SystemPropertyUtils.resolvePlaceholders("${bar:${spam:foo}}")).isEqualTo("foo");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testEnvVar() {
|
||||||
|
assertThat(SystemPropertyUtils.getProperty("lang")).isEqualTo(System.getenv("LANG"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
loader.main: demo.Application
|
@ -0,0 +1 @@
|
|||||||
|
loader.main: my.BootInfBarApplication
|
@ -0,0 +1,3 @@
|
|||||||
|
foo: Application
|
||||||
|
loader.main: my.${foo}
|
||||||
|
loader.path: etc
|
@ -0,0 +1 @@
|
|||||||
|
loader.main: demo.Application
|
@ -0,0 +1,3 @@
|
|||||||
|
# Jar Modes
|
||||||
|
org.springframework.boot.loader.jarmode.JarMode=\
|
||||||
|
org.springframework.boot.loader.jarmode.TestJarMode
|
@ -0,0 +1 @@
|
|||||||
|
loader.main: my.BarApplication
|
@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* 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 explodedsample;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example class used to test class loading.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
public class ExampleClass {
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
loader.main: demo.HomeApplication
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,5 @@
|
|||||||
|
- "BOOT-INF/layers/one/lib/a.jar"
|
||||||
|
- "BOOT-INF/layers/one/lib/b.jar"
|
||||||
|
- "BOOT-INF/layers/one/lib/c.jar"
|
||||||
|
- "BOOT-INF/layers/two/lib/d.jar"
|
||||||
|
- "BOOT-INF/layers/two/lib/e.jar"
|
@ -0,0 +1,2 @@
|
|||||||
|
Manifest-Version: 1.0
|
||||||
|
Start-Class: ${foo.main}
|
@ -0,0 +1 @@
|
|||||||
|
foo.main: demo.FooApplication
|
@ -0,0 +1 @@
|
|||||||
|
|
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
|
||||||
|
|
||||||
|
</beans>
|
@ -0,0 +1,44 @@
|
|||||||
|
plugins {
|
||||||
|
id "java"
|
||||||
|
id "org.springframework.boot.conventions"
|
||||||
|
id "org.springframework.boot.integration-test"
|
||||||
|
}
|
||||||
|
|
||||||
|
description = "Spring Boot Loader Integration Tests"
|
||||||
|
|
||||||
|
configurations {
|
||||||
|
app
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
app project(path: ":spring-boot-project:spring-boot-dependencies", configuration: "mavenRepository")
|
||||||
|
app project(path: ":spring-boot-project:spring-boot-tools:spring-boot-gradle-plugin", configuration: "mavenRepository")
|
||||||
|
app project(path: ":spring-boot-project:spring-boot-starters:spring-boot-starter-web", configuration: "mavenRepository")
|
||||||
|
|
||||||
|
intTestImplementation(enforcedPlatform(project(":spring-boot-project:spring-boot-parent")))
|
||||||
|
intTestImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support"))
|
||||||
|
intTestImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test"))
|
||||||
|
intTestImplementation("org.testcontainers:junit-jupiter")
|
||||||
|
intTestImplementation("org.testcontainers:testcontainers")
|
||||||
|
}
|
||||||
|
|
||||||
|
task syncMavenRepository(type: Sync) {
|
||||||
|
from configurations.app
|
||||||
|
into "${buildDir}/int-test-maven-repository"
|
||||||
|
}
|
||||||
|
|
||||||
|
task syncAppSource(type: org.springframework.boot.build.SyncAppSource) {
|
||||||
|
sourceDirectory = file("spring-boot-loader-tests-app")
|
||||||
|
destinationDirectory = file("${buildDir}/spring-boot-loader-tests-app")
|
||||||
|
}
|
||||||
|
|
||||||
|
task buildApp(type: GradleBuild) {
|
||||||
|
dependsOn syncAppSource, syncMavenRepository
|
||||||
|
dir = "${buildDir}/spring-boot-loader-tests-app"
|
||||||
|
startParameter.buildCacheEnabled = false
|
||||||
|
tasks = ["build"]
|
||||||
|
}
|
||||||
|
|
||||||
|
intTest {
|
||||||
|
dependsOn buildApp
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
plugins {
|
||||||
|
id "java"
|
||||||
|
id "org.springframework.boot"
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: "io.spring.dependency-management"
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
maven { url "file:${rootDir}/../int-test-maven-repository"}
|
||||||
|
mavenCentral()
|
||||||
|
maven { url "https://repo.spring.io/snapshot" }
|
||||||
|
maven { url "https://repo.spring.io/milestone" }
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||||
|
implementation("org.webjars:jquery:3.5.0")
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
maven { url "file:${rootDir}/../int-test-maven-repository"}
|
||||||
|
mavenCentral()
|
||||||
|
maven { url "https://repo.spring.io/snapshot" }
|
||||||
|
maven { url "https://repo.spring.io/milestone" }
|
||||||
|
}
|
||||||
|
resolutionStrategy {
|
||||||
|
eachPlugin {
|
||||||
|
if (requested.id.id == "org.springframework.boot") {
|
||||||
|
useModule "org.springframework.boot:spring-boot-gradle-plugin:${requested.version}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.loaderapp;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.net.JarURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import jakarta.servlet.ServletContext;
|
||||||
|
|
||||||
|
import org.springframework.boot.CommandLineRunner;
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.util.FileCopyUtils;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
public class LoaderTestApplication {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public CommandLineRunner commandLineRunner(ServletContext servletContext) {
|
||||||
|
return (args) -> {
|
||||||
|
File temp = new File(System.getProperty("java.io.tmpdir"));
|
||||||
|
URL resourceUrl = servletContext.getResource("webjars/jquery/3.5.0/jquery.js");
|
||||||
|
JarURLConnection connection = (JarURLConnection) resourceUrl.openConnection();
|
||||||
|
String jarName = connection.getJarFile().getName();
|
||||||
|
System.out.println(">>>>> jar file " + jarName);
|
||||||
|
if(jarName.contains(temp.getAbsolutePath())) {
|
||||||
|
System.out.println(">>>>> jar written to temp");
|
||||||
|
}
|
||||||
|
byte[] resourceContent = FileCopyUtils.copyToByteArray(resourceUrl.openStream());
|
||||||
|
URL directUrl = new URL(resourceUrl.toExternalForm());
|
||||||
|
byte[] directContent = FileCopyUtils.copyToByteArray(directUrl.openStream());
|
||||||
|
String message = (!Arrays.equals(resourceContent, directContent)) ? "NO MATCH"
|
||||||
|
: directContent.length + " BYTES";
|
||||||
|
System.out.println(">>>>> " + message + " from " + resourceUrl);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(LoaderTestApplication.class, args).close();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,139 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
|
import org.testcontainers.containers.GenericContainer;
|
||||||
|
import org.testcontainers.containers.output.ToStringConsumer;
|
||||||
|
import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy;
|
||||||
|
import org.testcontainers.images.builder.ImageFromDockerfile;
|
||||||
|
import org.testcontainers.utility.DockerImageName;
|
||||||
|
import org.testcontainers.utility.MountableFile;
|
||||||
|
|
||||||
|
import org.springframework.boot.system.JavaVersion;
|
||||||
|
import org.springframework.boot.testsupport.testcontainers.DisabledIfDockerUnavailable;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration tests loader that supports fat jars.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
*/
|
||||||
|
@DisabledIfDockerUnavailable
|
||||||
|
class LoaderIntegrationTests {
|
||||||
|
|
||||||
|
private final ToStringConsumer output = new ToStringConsumer();
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("javaRuntimes")
|
||||||
|
void readUrlsWithoutWarning(JavaRuntime javaRuntime) {
|
||||||
|
try (GenericContainer<?> container = createContainer(javaRuntime)) {
|
||||||
|
container.start();
|
||||||
|
System.out.println(this.output.toUtf8String());
|
||||||
|
assertThat(this.output.toUtf8String()).contains(">>>>> 287649 BYTES from")
|
||||||
|
.doesNotContain("WARNING:")
|
||||||
|
.doesNotContain("illegal")
|
||||||
|
.doesNotContain("jar written to temp");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private GenericContainer<?> createContainer(JavaRuntime javaRuntime) {
|
||||||
|
return javaRuntime.getContainer()
|
||||||
|
.withLogConsumer(this.output)
|
||||||
|
.withCopyFileToContainer(MountableFile.forHostPath(findApplication().toPath()), "/app.jar")
|
||||||
|
.withStartupCheckStrategy(new OneShotStartupCheckStrategy().withTimeout(Duration.ofMinutes(5)))
|
||||||
|
.withCommand("java", "-jar", "app.jar");
|
||||||
|
}
|
||||||
|
|
||||||
|
private File findApplication() {
|
||||||
|
String name = String.format("build/%1$s/build/libs/%1$s.jar", "spring-boot-loader-tests-app");
|
||||||
|
File jar = new File(name);
|
||||||
|
Assert.state(jar.isFile(), () -> "Could not find " + name + ". Have you built it?");
|
||||||
|
return jar;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Stream<JavaRuntime> javaRuntimes() {
|
||||||
|
List<JavaRuntime> javaRuntimes = new ArrayList<>();
|
||||||
|
javaRuntimes.add(JavaRuntime.openJdk(JavaVersion.SEVENTEEN));
|
||||||
|
javaRuntimes.add(JavaRuntime.openJdk(JavaVersion.TWENTY));
|
||||||
|
javaRuntimes.add(JavaRuntime.oracleJdk17());
|
||||||
|
javaRuntimes.add(JavaRuntime.openJdkEarlyAccess(JavaVersion.TWENTY_ONE));
|
||||||
|
return javaRuntimes.stream().filter(JavaRuntime::isCompatible);
|
||||||
|
}
|
||||||
|
|
||||||
|
static final class JavaRuntime {
|
||||||
|
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
private final JavaVersion version;
|
||||||
|
|
||||||
|
private final Supplier<GenericContainer<?>> container;
|
||||||
|
|
||||||
|
private JavaRuntime(String name, JavaVersion version, Supplier<GenericContainer<?>> container) {
|
||||||
|
this.name = name;
|
||||||
|
this.version = version;
|
||||||
|
this.container = container;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isCompatible() {
|
||||||
|
return this.version.isEqualOrNewerThan(JavaVersion.getJavaVersion());
|
||||||
|
}
|
||||||
|
|
||||||
|
GenericContainer<?> getContainer() {
|
||||||
|
return this.container.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
static JavaRuntime openJdkEarlyAccess(JavaVersion version) {
|
||||||
|
String imageVersion = version.toString();
|
||||||
|
DockerImageName image = DockerImageName.parse("openjdk:%s-ea-jdk".formatted(imageVersion));
|
||||||
|
return new JavaRuntime("OpenJDK Early Access " + imageVersion, version,
|
||||||
|
() -> new GenericContainer<>(image));
|
||||||
|
}
|
||||||
|
|
||||||
|
static JavaRuntime openJdk(JavaVersion version) {
|
||||||
|
String imageVersion = version.toString();
|
||||||
|
DockerImageName image = DockerImageName.parse("bellsoft/liberica-openjdk-debian:" + imageVersion);
|
||||||
|
return new JavaRuntime("OpenJDK " + imageVersion, version, () -> new GenericContainer<>(image));
|
||||||
|
}
|
||||||
|
|
||||||
|
static JavaRuntime oracleJdk17() {
|
||||||
|
String arch = System.getProperty("os.arch");
|
||||||
|
String dockerFile = ("aarch64".equals(arch)) ? "Dockerfile-aarch64" : "Dockerfile";
|
||||||
|
ImageFromDockerfile image = new ImageFromDockerfile("spring-boot-loader/oracle-jdk-17")
|
||||||
|
.withFileFromFile("Dockerfile", new File("src/intTest/resources/conf/oracle-jdk-17/" + dockerFile));
|
||||||
|
return new JavaRuntime("Oracle JDK 17", JavaVersion.SEVENTEEN, () -> new GenericContainer<>(image));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
FROM ubuntu:jammy-20230624
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y software-properties-common curl && \
|
||||||
|
mkdir -p /opt/oraclejdk && \
|
||||||
|
cd /opt/oraclejdk && \
|
||||||
|
curl -L https://download.oracle.com/java/17/latest/jdk-17_linux-x64_bin.tar.gz | tar zx --strip-components=1
|
||||||
|
ENV JAVA_HOME /opt/oraclejdk
|
||||||
|
ENV PATH $JAVA_HOME/bin:$PATH
|
@ -0,0 +1,8 @@
|
|||||||
|
FROM ubuntu:jammy-20230624
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y software-properties-common curl && \
|
||||||
|
mkdir -p /opt/oraclejdk && \
|
||||||
|
cd /opt/oraclejdk && \
|
||||||
|
curl -L https://download.oracle.com/java/17/archive/jdk-17.0.8_linux-aarch64_bin.tar.gz | tar zx --strip-components=1
|
||||||
|
ENV JAVA_HOME /opt/oraclejdk
|
||||||
|
ENV PATH $JAVA_HOME/bin:$PATH
|
@ -0,0 +1,5 @@
|
|||||||
|
This folder contains a Dockerfile that will create an Oracle JDK instance for use in integration tests.
|
||||||
|
The resulting Docker image should not be published.
|
||||||
|
|
||||||
|
Oracle JDK is subject to the https://www.oracle.com/downloads/licenses/no-fee-license.html["Oracle No-Fee Terms and Conditions" License (NFTC)] license.
|
||||||
|
We are specifically using the unmodified JDK for the purposes of developing and testing.
|
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<configuration>
|
||||||
|
<include resource="org/springframework/boot/logging/logback/base.xml"/>
|
||||||
|
</configuration>
|
Loading…
Reference in New Issue