Skip to content

Commit 1884599

Browse files
authored
Merge pull request #5029 from eclipse-vertx/custom-jar-file-resolver-4.x
File resolver supports custom jar URL connection
2 parents 3f3a6b2 + 8620d31 commit 1884599

File tree

5 files changed

+210
-67
lines changed

5 files changed

+210
-67
lines changed

src/main/java/io/vertx/core/file/impl/FileResolverImpl.java

Lines changed: 98 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,22 @@
1414
import io.netty.util.internal.PlatformDependent;
1515
import io.vertx.core.VertxException;
1616
import io.vertx.core.file.FileSystemOptions;
17+
import io.vertx.core.impl.Utils;
1718
import io.vertx.core.spi.file.FileResolver;
1819

19-
import java.io.Closeable;
2020
import java.io.File;
2121
import java.io.IOException;
2222
import java.io.InputStream;
23+
import java.net.JarURLConnection;
2324
import java.net.URL;
25+
import java.nio.file.FileVisitResult;
26+
import java.nio.file.Files;
27+
import java.nio.file.Path;
28+
import java.nio.file.SimpleFileVisitor;
29+
import java.nio.file.attribute.BasicFileAttributes;
30+
import java.util.ArrayList;
2431
import java.util.Enumeration;
32+
import java.util.List;
2533
import java.util.zip.ZipEntry;
2634
import java.util.zip.ZipFile;
2735

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

278-
private File unpackFromJarURL(URL url, String fileName, ClassLoader cl) {
279-
ZipFile zip = null;
280-
try {
281-
String path = url.getPath();
282-
int idx1 = -1, idx2 = -1;
283-
for (int i = path.length() - 1; i > 4; ) {
284-
if (path.charAt(i) == '!' && (path.startsWith(".jar", i - 4) || path.startsWith(".zip", i - 4) || path.startsWith(".war", i - 4))) {
285-
if (idx1 == -1) {
286-
idx1 = i;
287-
i -= 4;
288-
continue;
289-
} else {
290-
idx2 = i;
291-
break;
292-
}
293-
}
294-
i--;
295-
}
296-
if (idx2 == -1) {
297-
File file = new File(decodeURIComponent(path.substring(5, idx1), false));
298-
zip = new ZipFile(file);
286+
/**
287+
* Parse the list of entries of a URL assuming the URL is a jar URL.
288+
*
289+
* <ul>
290+
* <li>when the URL is a nested file within the archive, the list is the jar entry</li>
291+
* <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>
292+
* <li>and so on.</li>
293+
* </ul>
294+
*
295+
* @param url the URL
296+
* @return the list of entries
297+
*/
298+
private List<String> listOfEntries(URL url) {
299+
String path = url.getPath();
300+
List<String> list = new ArrayList<>();
301+
int last = path.length();
302+
for (int i = path.length() - 2; i >= 0;) {
303+
if (path.charAt(i) == '!' && path.charAt(i + 1) == '/') {
304+
list.add(path.substring(2 + i, last));
305+
last = i;
306+
i -= 2;
299307
} else {
300-
String s = path.substring(idx2 + 2, idx1);
301-
File file = resolveFile(s);
302-
zip = new ZipFile(file);
308+
i--;
303309
}
310+
}
311+
return list;
312+
}
304313

305-
String inJarPath = path.substring(idx1 + 2);
306-
StringBuilder prefixBuilder = new StringBuilder();
307-
int first = 0;
308-
int second;
309-
int len = JAR_URL_SEP.length();
310-
while ((second = inJarPath.indexOf(JAR_URL_SEP, first)) >= 0) {
311-
prefixBuilder.append(inJarPath, first, second).append("/");
312-
first = second + len;
313-
}
314-
String prefix = prefixBuilder.toString();
315-
Enumeration<? extends ZipEntry> entries = zip.entries();
316-
String prefixCheck = prefix.isEmpty() ? fileName : prefix + fileName;
317-
while (entries.hasMoreElements()) {
318-
ZipEntry entry = entries.nextElement();
319-
String name = entry.getName();
320-
if (name.startsWith(prefixCheck)) {
321-
String p = prefix.isEmpty() ? name : name.substring(prefix.length());
322-
if (name.endsWith("/")) {
323-
// Directory
324-
cache.cacheDir(p);
325-
} else {
326-
try (InputStream is = zip.getInputStream(entry)) {
327-
cache.cacheFile(p, is, !enableCaching);
314+
private File unpackFromJarURL(URL url, String fileName, ClassLoader cl) {
315+
try {
316+
List<String> listOfEntries = listOfEntries(url);
317+
switch (listOfEntries.size()) {
318+
case 1:
319+
JarURLConnection conn = (JarURLConnection) url.openConnection();
320+
try (ZipFile zip = conn.getJarFile()) {
321+
extractFilesFromJarFile(zip, fileName);
322+
}
323+
break;
324+
case 2:
325+
URL nestedURL = cl.getResource(listOfEntries.get(1));
326+
if (nestedURL != null && nestedURL.getProtocol().equals("jar")) {
327+
File root = unpackFromJarURL(nestedURL, listOfEntries.get(1), cl);
328+
if (root.isDirectory()) {
329+
// jar:file:/path/to/nesting.jar!/xxx-inf/classes
330+
// we need to unpack xxx-inf/classes and then copy the content as is in the cache
331+
Path path = root.toPath();
332+
Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
333+
@Override
334+
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
335+
Path relative = path.relativize(dir);
336+
cache.cacheDir(relative.toString());
337+
return FileVisitResult.CONTINUE;
338+
}
339+
@Override
340+
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
341+
Path relative = path.relativize(file);
342+
cache.cacheFile(relative.toString(), file.toFile(), false);
343+
return FileVisitResult.CONTINUE;
344+
}
345+
});
346+
} else {
347+
// jar:file:/path/to/nesting.jar!/path/to/nested.jar
348+
try (ZipFile zip = new ZipFile(root)) {
349+
extractFilesFromJarFile(zip, fileName);
350+
}
328351
}
352+
} else {
353+
throw new VertxException("Unexpected nested url : " + nestedURL);
329354
}
330-
331-
}
355+
break;
356+
default:
357+
throw new VertxException("Nesting more than two levels is not supported");
332358
}
333359
} catch (IOException e) {
334360
throw new VertxException(FileSystemImpl.getFileAccessErrorMessage("unpack", url.toString()), e);
335-
} finally {
336-
closeQuietly(zip);
337361
}
338-
339362
return cache.getFile(fileName);
340363
}
341364

342-
private void closeQuietly(Closeable zip) {
343-
if (zip != null) {
344-
try {
345-
zip.close();
346-
} catch (IOException e) {
347-
// Ignored.
365+
/**
366+
* Extract a subset of the entries to the cache.
367+
*/
368+
private void extractFilesFromJarFile(ZipFile zip, String entryFilter) throws IOException {
369+
Enumeration<? extends ZipEntry> entries = zip.entries();
370+
while (entries.hasMoreElements()) {
371+
ZipEntry entry = entries.nextElement();
372+
String name = entry.getName();
373+
int len = name.length();
374+
if (len == 0) {
375+
return;
376+
}
377+
if (name.charAt(len - 1) != ' ' || !Utils.isWindows()) {
378+
if (name.startsWith(entryFilter)) {
379+
if (name.charAt(len - 1) == '/') {
380+
cache.cacheDir(name);
381+
} else {
382+
try (InputStream is = zip.getInputStream(entry)) {
383+
cache.cacheFile(name, is, !enableCaching);
384+
}
385+
}
386+
}
348387
}
349388
}
350389
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright (c) 2011-2023 Contributors to the Eclipse Foundation
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License 2.0 which is available at
6+
* http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
7+
* which is available at https://www.apache.org/licenses/LICENSE-2.0.
8+
*
9+
* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
10+
*/
11+
12+
package io.vertx.core.file;
13+
14+
import io.vertx.test.core.TestUtils;
15+
import org.junit.Assert;
16+
17+
import java.io.File;
18+
import java.io.IOException;
19+
import java.net.*;
20+
import java.nio.file.Files;
21+
import java.util.jar.JarEntry;
22+
import java.util.jar.JarFile;
23+
import java.util.jar.JarOutputStream;
24+
25+
/**
26+
* 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
27+
*
28+
* what we are trying to resolve:
29+
* - BOOT-INF/classes -> jar:file:/path/to/file.jar!/BOOT-INF/classes/
30+
* - webroot/hello.txt -> jar:nested:/path/to/file.jar/!BOOT-INF/classes/!/webroot/hello.txt
31+
*/
32+
public class CustomJarFileResolverTest extends FileResolverTestBase {
33+
34+
static File getFiles(File baseDir) throws Exception {
35+
File file = Files.createTempFile(TestUtils.MAVEN_TARGET_DIR.toPath(), "", "files.custom").toFile();
36+
Assert.assertTrue(file.delete());
37+
return ZipFileResolverTest.getFiles(
38+
baseDir,
39+
file,
40+
out -> {
41+
try {
42+
return new JarOutputStream(out);
43+
} catch (IOException e) {
44+
throw new AssertionError(e);
45+
}
46+
}, JarEntry::new);
47+
}
48+
49+
@Override
50+
protected ClassLoader resourcesLoader(File baseDir) throws Exception {
51+
File files = getFiles(baseDir);
52+
return new ClassLoader() {
53+
@Override
54+
public URL getResource(String name) {
55+
try {
56+
try (JarFile jf = new JarFile(files)) {
57+
if (jf.getJarEntry(name) == null) {
58+
return super.getResource(name);
59+
}
60+
}
61+
return new URL("jar", "null" , -1, "custom:/whatever!/" + name, new URLStreamHandler() {
62+
@Override
63+
protected URLConnection openConnection(URL u) throws IOException {
64+
// Use file protocol here on purpose otherwise we would need to register the protocol
65+
return new JarURLConnection(new URL("jar:file:/whatever!/" + name)) {
66+
@Override
67+
public JarFile getJarFile() throws IOException {
68+
return new JarFile(files);
69+
}
70+
@Override
71+
public void connect() throws IOException {
72+
}
73+
};
74+
}
75+
});
76+
} catch (Exception e) {
77+
return null;
78+
}
79+
}
80+
};
81+
}
82+
}

src/test/java/io/vertx/core/file/NestedJarFileResolverTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,11 @@ public URL getResource(String name) {
4747
} else if (name.startsWith("webroot")) {
4848
return new URL("jar:" + webrootURL + "!/lib/nested.jar!/" + name.substring(7));
4949
} else if (name.equals("afile.html")) {
50-
return new URL("jar:" + webrootURL + "!/lib/nested.jar!afile.html/");
50+
return new URL("jar:" + webrootURL + "!/lib/nested.jar!/afile.html");
5151
} else if (name.equals("afile with spaces.html")) {
52-
return new URL("jar:" + webrootURL + "!/lib/nested.jar!afile with spaces.html/");
52+
return new URL("jar:" + webrootURL + "!/lib/nested.jar!/afile with spaces.html");
5353
} else if (name.equals("afilewithspaceatend ")) {
54-
return new URL("jar:" + webrootURL + "!/lib/nested.jar!afilewithspaceatend /");
54+
return new URL("jar:" + webrootURL + "!/lib/nested.jar!/afilewithspaceatend ");
5555
}
5656
} catch (MalformedURLException e) {
5757
throw new AssertionError(e);

src/test/java/io/vertx/core/file/NestedRootJarResolverTest.java

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,24 @@
1212
package io.vertx.core.file;
1313

1414
import io.vertx.test.core.TestUtils;
15+
import junit.framework.AssertionFailedError;
1516
import org.junit.Assert;
1617

1718
import java.io.File;
19+
import java.io.IOException;
20+
import java.io.OutputStream;
1821
import java.net.MalformedURLException;
1922
import java.net.URL;
2023
import java.nio.file.Files;
24+
import java.util.function.Function;
2125
import java.util.zip.ZipEntry;
2226
import java.util.zip.ZipOutputStream;
2327

2428
/**
29+
* what we are trying to resolve:
30+
* - webroot/hello.txt -> jar:file:/path/to/file.jar!/BOOT-INF/classes!/webroot/hello.txt
31+
* - BOOT-INF/classes -> jar:file:/path/to/file.jar!/BOOT-INF/classes
32+
*
2533
* @author Thomas Segismont
2634
*/
2735
public class NestedRootJarResolverTest extends FileResolverTestBase {
@@ -30,13 +38,27 @@ public class NestedRootJarResolverTest extends FileResolverTestBase {
3038
protected ClassLoader resourcesLoader(File baseDir) throws Exception {
3139
File nestedFiles = Files.createTempFile(TestUtils.MAVEN_TARGET_DIR.toPath(), "", "nestedroot.jar").toFile();
3240
Assert.assertTrue(nestedFiles.delete());
33-
ZipFileResolverTest.getFiles(baseDir, nestedFiles, ZipOutputStream::new, name -> new ZipEntry("nested-inf/classes/" + name));
41+
ZipFileResolverTest.getFiles(baseDir, nestedFiles, new Function<OutputStream, ZipOutputStream>() {
42+
@Override
43+
public ZipOutputStream apply(OutputStream outputStream) {
44+
ZipOutputStream zip = new ZipOutputStream(outputStream);
45+
try {
46+
zip.putNextEntry(new ZipEntry("nested-inf/classes/"));
47+
zip.closeEntry();
48+
} catch (IOException e) {
49+
throw new AssertionFailedError();
50+
}
51+
return zip;
52+
}
53+
}, name -> new ZipEntry("nested-inf/classes/" + name));
3454
URL webrootURL = nestedFiles.toURI().toURL();
3555
return new ClassLoader(Thread.currentThread().getContextClassLoader()) {
3656
@Override
3757
public URL getResource(String name) {
3858
try {
39-
if (name.startsWith("webroot")) {
59+
if (name.equals("nested-inf/classes")) {
60+
return new URL("jar:" + webrootURL + "!/nested-inf/classes");
61+
} else if (name.startsWith("webroot")) {
4062
return new URL("jar:" + webrootURL + "!/nested-inf/classes!/" + name);
4163
} else if (name.equals("afile.html") || name.equals("afile with spaces.html") || name.equals("afilewithspaceatend ")) {
4264
return new URL("jar:" + webrootURL + "!/nested-inf/classes!/" + name);

src/test/java/io/vertx/core/file/NestedZipFileResolverTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,11 @@ public URL getResource(String name) {
4747
} else if (name.startsWith("webroot")) {
4848
return new URL("jar:" + webrootURL + "!/lib/nested.zip!/" + name.substring(7));
4949
} else if (name.equals("afile.html")) {
50-
return new URL("jar:" + webrootURL + "!/lib/nested.zip!afile.html/");
50+
return new URL("jar:" + webrootURL + "!/lib/nested.zip!/afile.html");
5151
} else if (name.equals("afile with spaces.html")) {
52-
return new URL("jar:" + webrootURL + "!/lib/nested.zip!afile with spaces.html/");
52+
return new URL("jar:" + webrootURL + "!/lib/nested.zip!/afile with spaces.html");
5353
} else if (name.equals("afilewithspaceatend ")) {
54-
return new URL("jar:" + webrootURL + "!/lib/nested.zip!afilewithspaceatend /");
54+
return new URL("jar:" + webrootURL + "!/lib/nested.zip!/afilewithspaceatend ");
5555
}
5656
} catch (MalformedURLException e) {
5757
throw new AssertionError(e);

0 commit comments

Comments
 (0)