Skip to content

Commit

Permalink
Merge pull request #5029 from eclipse-vertx/custom-jar-file-resolver-4.x
Browse files Browse the repository at this point in the history
File resolver supports custom jar URL connection
  • Loading branch information
vietj authored Dec 12, 2023
2 parents 3f3a6b2 + 8620d31 commit 1884599
Show file tree
Hide file tree
Showing 5 changed files with 210 additions and 67 deletions.
157 changes: 98 additions & 59 deletions src/main/java/io/vertx/core/file/impl/FileResolverImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,22 @@
import io.netty.util.internal.PlatformDependent;
import io.vertx.core.VertxException;
import io.vertx.core.file.FileSystemOptions;
import io.vertx.core.impl.Utils;
import io.vertx.core.spi.file.FileResolver;

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.JarURLConnection;
import java.net.URL;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

Expand Down Expand Up @@ -275,76 +283,107 @@ private File unpackFromFileURL(URL url, String fileName, ClassLoader cl) {
return cacheFile;
}

private File unpackFromJarURL(URL url, String fileName, ClassLoader cl) {
ZipFile zip = null;
try {
String path = url.getPath();
int idx1 = -1, idx2 = -1;
for (int i = path.length() - 1; i > 4; ) {
if (path.charAt(i) == '!' && (path.startsWith(".jar", i - 4) || path.startsWith(".zip", i - 4) || path.startsWith(".war", i - 4))) {
if (idx1 == -1) {
idx1 = i;
i -= 4;
continue;
} else {
idx2 = i;
break;
}
}
i--;
}
if (idx2 == -1) {
File file = new File(decodeURIComponent(path.substring(5, idx1), false));
zip = new ZipFile(file);
/**
* Parse the list of entries of a URL assuming the URL is a jar URL.
*
* <ul>
* <li>when the URL is a nested file within the archive, the list is the jar entry</li>
* <li>when the URL is a nested file within a nested file within the archive, the list is jar entry followed by the jar entry of the nested archive</li>
* <li>and so on.</li>
* </ul>
*
* @param url the URL
* @return the list of entries
*/
private List<String> listOfEntries(URL url) {
String path = url.getPath();
List<String> list = new ArrayList<>();
int last = path.length();
for (int i = path.length() - 2; i >= 0;) {
if (path.charAt(i) == '!' && path.charAt(i + 1) == '/') {
list.add(path.substring(2 + i, last));
last = i;
i -= 2;
} else {
String s = path.substring(idx2 + 2, idx1);
File file = resolveFile(s);
zip = new ZipFile(file);
i--;
}
}
return list;
}

String inJarPath = path.substring(idx1 + 2);
StringBuilder prefixBuilder = new StringBuilder();
int first = 0;
int second;
int len = JAR_URL_SEP.length();
while ((second = inJarPath.indexOf(JAR_URL_SEP, first)) >= 0) {
prefixBuilder.append(inJarPath, first, second).append("/");
first = second + len;
}
String prefix = prefixBuilder.toString();
Enumeration<? extends ZipEntry> entries = zip.entries();
String prefixCheck = prefix.isEmpty() ? fileName : prefix + fileName;
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
String name = entry.getName();
if (name.startsWith(prefixCheck)) {
String p = prefix.isEmpty() ? name : name.substring(prefix.length());
if (name.endsWith("/")) {
// Directory
cache.cacheDir(p);
} else {
try (InputStream is = zip.getInputStream(entry)) {
cache.cacheFile(p, is, !enableCaching);
private File unpackFromJarURL(URL url, String fileName, ClassLoader cl) {
try {
List<String> listOfEntries = listOfEntries(url);
switch (listOfEntries.size()) {
case 1:
JarURLConnection conn = (JarURLConnection) url.openConnection();
try (ZipFile zip = conn.getJarFile()) {
extractFilesFromJarFile(zip, fileName);
}
break;
case 2:
URL nestedURL = cl.getResource(listOfEntries.get(1));
if (nestedURL != null && nestedURL.getProtocol().equals("jar")) {
File root = unpackFromJarURL(nestedURL, listOfEntries.get(1), cl);
if (root.isDirectory()) {
// jar:file:/path/to/nesting.jar!/xxx-inf/classes
// we need to unpack xxx-inf/classes and then copy the content as is in the cache
Path path = root.toPath();
Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
Path relative = path.relativize(dir);
cache.cacheDir(relative.toString());
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Path relative = path.relativize(file);
cache.cacheFile(relative.toString(), file.toFile(), false);
return FileVisitResult.CONTINUE;
}
});
} else {
// jar:file:/path/to/nesting.jar!/path/to/nested.jar
try (ZipFile zip = new ZipFile(root)) {
extractFilesFromJarFile(zip, fileName);
}
}
} else {
throw new VertxException("Unexpected nested url : " + nestedURL);
}

}
break;
default:
throw new VertxException("Nesting more than two levels is not supported");
}
} catch (IOException e) {
throw new VertxException(FileSystemImpl.getFileAccessErrorMessage("unpack", url.toString()), e);
} finally {
closeQuietly(zip);
}

return cache.getFile(fileName);
}

private void closeQuietly(Closeable zip) {
if (zip != null) {
try {
zip.close();
} catch (IOException e) {
// Ignored.
/**
* Extract a subset of the entries to the cache.
*/
private void extractFilesFromJarFile(ZipFile zip, String entryFilter) throws IOException {
Enumeration<? extends ZipEntry> entries = zip.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
String name = entry.getName();
int len = name.length();
if (len == 0) {
return;
}
if (name.charAt(len - 1) != ' ' || !Utils.isWindows()) {
if (name.startsWith(entryFilter)) {
if (name.charAt(len - 1) == '/') {
cache.cacheDir(name);
} else {
try (InputStream is = zip.getInputStream(entry)) {
cache.cacheFile(name, is, !enableCaching);
}
}
}
}
}
}
Expand Down
82 changes: 82 additions & 0 deletions src/test/java/io/vertx/core/file/CustomJarFileResolverTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright (c) 2011-2023 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
* which is available at https://www.apache.org/licenses/LICENSE-2.0.
*
* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
*/

package io.vertx.core.file;

import io.vertx.test.core.TestUtils;
import org.junit.Assert;

import java.io.File;
import java.io.IOException;
import java.net.*;
import java.nio.file.Files;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;

/**
* Custom jar file resolution test, à la spring boot nested URLs: https://docs.spring.io/spring-boot/docs/current/reference/html/executable-jar.html#appendix.executable-jar.nested-jars
*
* what we are trying to resolve:
* - BOOT-INF/classes -> jar:file:/path/to/file.jar!/BOOT-INF/classes/
* - webroot/hello.txt -> jar:nested:/path/to/file.jar/!BOOT-INF/classes/!/webroot/hello.txt
*/
public class CustomJarFileResolverTest extends FileResolverTestBase {

static File getFiles(File baseDir) throws Exception {
File file = Files.createTempFile(TestUtils.MAVEN_TARGET_DIR.toPath(), "", "files.custom").toFile();
Assert.assertTrue(file.delete());
return ZipFileResolverTest.getFiles(
baseDir,
file,
out -> {
try {
return new JarOutputStream(out);
} catch (IOException e) {
throw new AssertionError(e);
}
}, JarEntry::new);
}

@Override
protected ClassLoader resourcesLoader(File baseDir) throws Exception {
File files = getFiles(baseDir);
return new ClassLoader() {
@Override
public URL getResource(String name) {
try {
try (JarFile jf = new JarFile(files)) {
if (jf.getJarEntry(name) == null) {
return super.getResource(name);
}
}
return new URL("jar", "null" , -1, "custom:/whatever!/" + name, new URLStreamHandler() {
@Override
protected URLConnection openConnection(URL u) throws IOException {
// Use file protocol here on purpose otherwise we would need to register the protocol
return new JarURLConnection(new URL("jar:file:/whatever!/" + name)) {
@Override
public JarFile getJarFile() throws IOException {
return new JarFile(files);
}
@Override
public void connect() throws IOException {
}
};
}
});
} catch (Exception e) {
return null;
}
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@ public URL getResource(String name) {
} else if (name.startsWith("webroot")) {
return new URL("jar:" + webrootURL + "!/lib/nested.jar!/" + name.substring(7));
} else if (name.equals("afile.html")) {
return new URL("jar:" + webrootURL + "!/lib/nested.jar!afile.html/");
return new URL("jar:" + webrootURL + "!/lib/nested.jar!/afile.html");
} else if (name.equals("afile with spaces.html")) {
return new URL("jar:" + webrootURL + "!/lib/nested.jar!afile with spaces.html/");
return new URL("jar:" + webrootURL + "!/lib/nested.jar!/afile with spaces.html");
} else if (name.equals("afilewithspaceatend ")) {
return new URL("jar:" + webrootURL + "!/lib/nested.jar!afilewithspaceatend /");
return new URL("jar:" + webrootURL + "!/lib/nested.jar!/afilewithspaceatend ");
}
} catch (MalformedURLException e) {
throw new AssertionError(e);
Expand Down
26 changes: 24 additions & 2 deletions src/test/java/io/vertx/core/file/NestedRootJarResolverTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,24 @@
package io.vertx.core.file;

import io.vertx.test.core.TestUtils;
import junit.framework.AssertionFailedError;
import org.junit.Assert;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Files;
import java.util.function.Function;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
* what we are trying to resolve:
* - webroot/hello.txt -> jar:file:/path/to/file.jar!/BOOT-INF/classes!/webroot/hello.txt
* - BOOT-INF/classes -> jar:file:/path/to/file.jar!/BOOT-INF/classes
*
* @author Thomas Segismont
*/
public class NestedRootJarResolverTest extends FileResolverTestBase {
Expand All @@ -30,13 +38,27 @@ public class NestedRootJarResolverTest extends FileResolverTestBase {
protected ClassLoader resourcesLoader(File baseDir) throws Exception {
File nestedFiles = Files.createTempFile(TestUtils.MAVEN_TARGET_DIR.toPath(), "", "nestedroot.jar").toFile();
Assert.assertTrue(nestedFiles.delete());
ZipFileResolverTest.getFiles(baseDir, nestedFiles, ZipOutputStream::new, name -> new ZipEntry("nested-inf/classes/" + name));
ZipFileResolverTest.getFiles(baseDir, nestedFiles, new Function<OutputStream, ZipOutputStream>() {
@Override
public ZipOutputStream apply(OutputStream outputStream) {
ZipOutputStream zip = new ZipOutputStream(outputStream);
try {
zip.putNextEntry(new ZipEntry("nested-inf/classes/"));
zip.closeEntry();
} catch (IOException e) {
throw new AssertionFailedError();
}
return zip;
}
}, name -> new ZipEntry("nested-inf/classes/" + name));
URL webrootURL = nestedFiles.toURI().toURL();
return new ClassLoader(Thread.currentThread().getContextClassLoader()) {
@Override
public URL getResource(String name) {
try {
if (name.startsWith("webroot")) {
if (name.equals("nested-inf/classes")) {
return new URL("jar:" + webrootURL + "!/nested-inf/classes");
} else if (name.startsWith("webroot")) {
return new URL("jar:" + webrootURL + "!/nested-inf/classes!/" + name);
} else if (name.equals("afile.html") || name.equals("afile with spaces.html") || name.equals("afilewithspaceatend ")) {
return new URL("jar:" + webrootURL + "!/nested-inf/classes!/" + name);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@ public URL getResource(String name) {
} else if (name.startsWith("webroot")) {
return new URL("jar:" + webrootURL + "!/lib/nested.zip!/" + name.substring(7));
} else if (name.equals("afile.html")) {
return new URL("jar:" + webrootURL + "!/lib/nested.zip!afile.html/");
return new URL("jar:" + webrootURL + "!/lib/nested.zip!/afile.html");
} else if (name.equals("afile with spaces.html")) {
return new URL("jar:" + webrootURL + "!/lib/nested.zip!afile with spaces.html/");
return new URL("jar:" + webrootURL + "!/lib/nested.zip!/afile with spaces.html");
} else if (name.equals("afilewithspaceatend ")) {
return new URL("jar:" + webrootURL + "!/lib/nested.zip!afilewithspaceatend /");
return new URL("jar:" + webrootURL + "!/lib/nested.zip!/afilewithspaceatend ");
}
} catch (MalformedURLException e) {
throw new AssertionError(e);
Expand Down

0 comments on commit 1884599

Please sign in to comment.