Allow `new URL(String)` with nested JARs
Update JarFile to allow the custom registration of a JAR `URLStreamHandler` that allows `jar:` URLs to be constructed from Strings. This removes the previous requirement that all nested JAR URLs be created with a 'context'. To supported nested JARs the `java.protocol.handler.pkgs` system property is changed so that our custom URLHandler is picked for 'jar' protocols in preference to the Java default. Fixes gh-269
This commit is contained in:
parent
c1f8fd2bac
commit
01550fcec6
|
|
@ -162,25 +162,6 @@ $ java org.springframework.boot.loader.JarLauncher
|
||||||
There are a number of restrictions that you need to consider when working with a Spring
|
There are a number of restrictions that you need to consider when working with a Spring
|
||||||
Boot Loader packaged application.
|
Boot Loader packaged application.
|
||||||
|
|
||||||
### URLs
|
|
||||||
URLs for nested jar entries intentionally look and behave like standard jar URLs,
|
|
||||||
You cannot, however, directly create a nested jar URL from a string:
|
|
||||||
|
|
||||||
```
|
|
||||||
URL url = classLoader.getResoure("/a/b.txt");
|
|
||||||
String s = url.toString(); // In the form 'jar:file:/file.jar!/nested.jar!/a/b.txt'
|
|
||||||
new URL(s); // This will fail
|
|
||||||
```
|
|
||||||
|
|
||||||
If you need to obtain URL using a String, ensure that you always provide a context URL
|
|
||||||
to the constructor. This will ensure that the custom `URLStreamHandler` used to support
|
|
||||||
nested jars is used.
|
|
||||||
|
|
||||||
```
|
|
||||||
URL url = classLoader.getResoure("/a");
|
|
||||||
new URL(url, "b.txt");
|
|
||||||
```
|
|
||||||
|
|
||||||
### Zip entry compression
|
### Zip entry compression
|
||||||
The `ZipEntry` for a nested jar must be saved using the `ZipEntry.STORED` method. This
|
The `ZipEntry` for a nested jar must be saved using the `ZipEntry.STORED` method. This
|
||||||
is required so that we can seek directly to individual content within the nested jar.
|
is required so that we can seek directly to individual content within the nested jar.
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2012-2013 the original author or authors.
|
* Copyright 2012-2014 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -23,6 +23,7 @@ import java.util.List;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
import org.springframework.boot.loader.archive.Archive;
|
import org.springframework.boot.loader.archive.Archive;
|
||||||
|
import org.springframework.boot.loader.jar.JarFile;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base class for launchers that can start an application with a fully configured
|
* Base class for launchers that can start an application with a fully configured
|
||||||
|
|
@ -49,6 +50,7 @@ public abstract class Launcher {
|
||||||
*/
|
*/
|
||||||
protected void launch(String[] args) {
|
protected void launch(String[] args) {
|
||||||
try {
|
try {
|
||||||
|
JarFile.registerUrlProtocolHandler();
|
||||||
ClassLoader classLoader = createClassLoader(getClassPathArchives());
|
ClassLoader classLoader = createClassLoader(getClassPathArchives());
|
||||||
launch(args, getMainClass(), classLoader);
|
launch(args, getMainClass(), classLoader);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2014 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
|
||||||
|
*
|
||||||
|
* http://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.net.MalformedURLException;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.URLConnection;
|
||||||
|
import java.net.URLStreamHandler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link URLStreamHandler} for Spring Boot loader {@link JarFile}s.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @see JarFile#registerUrlProtocolHandler()
|
||||||
|
*/
|
||||||
|
public class Handler extends URLStreamHandler {
|
||||||
|
|
||||||
|
// NOTE: in order to be found as a URL protocol hander, this class must be public,
|
||||||
|
// must be named Handler and must be in a package ending '.jar'
|
||||||
|
|
||||||
|
private static final String SEPARATOR = JarURLConnection.SEPARATOR;
|
||||||
|
|
||||||
|
private final JarFile jarFile;
|
||||||
|
|
||||||
|
public Handler() {
|
||||||
|
this(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Handler(JarFile jarFile) {
|
||||||
|
this.jarFile = jarFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected URLConnection openConnection(URL url) throws IOException {
|
||||||
|
JarFile jarFile = (this.jarFile != null ? this.jarFile : getJarFileFromUrl(url));
|
||||||
|
return new JarURLConnection(url, jarFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
public JarFile getJarFileFromUrl(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");
|
||||||
|
}
|
||||||
|
|
||||||
|
JarFile jar = null;
|
||||||
|
while (separatorIndex != -1) {
|
||||||
|
String name = spec.substring(0, separatorIndex);
|
||||||
|
jar = (jar == null ? getRootJarFile(name) : getNestedJarFile(jar, name));
|
||||||
|
spec = spec.substring(separatorIndex + SEPARATOR.length());
|
||||||
|
separatorIndex = spec.indexOf(SEPARATOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
return jar;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JarFile getRootJarFile(String name) throws IOException {
|
||||||
|
try {
|
||||||
|
return new JarFile(new File(new URL(name).toURI()));
|
||||||
|
}
|
||||||
|
catch (URISyntaxException ex) {
|
||||||
|
throw new IOException("Unable to open root Jar file '" + name + "'", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private JarFile getNestedJarFile(JarFile jarFile, String name) throws IOException {
|
||||||
|
JarEntry jarEntry = jarFile.getJarEntry(name);
|
||||||
|
if (jarEntry == null) {
|
||||||
|
throw new IOException("Unable to find nested jar '" + name + "' from '"
|
||||||
|
+ jarFile + "'");
|
||||||
|
}
|
||||||
|
return jarFile.getNestedJarFile(jarEntry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2012-2013 the original author or authors.
|
* Copyright 2012-2014 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -17,6 +17,8 @@
|
||||||
package org.springframework.boot.loader.jar;
|
package org.springframework.boot.loader.jar;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
import java.security.CodeSigner;
|
import java.security.CodeSigner;
|
||||||
import java.security.cert.Certificate;
|
import java.security.cert.Certificate;
|
||||||
import java.util.jar.Attributes;
|
import java.util.jar.Attributes;
|
||||||
|
|
@ -47,6 +49,13 @@ public class JarEntry extends java.util.jar.JarEntry {
|
||||||
return this.source;
|
return this.source;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a {@link URL} for this {@link JarEntry}.
|
||||||
|
*/
|
||||||
|
public URL getUrl() throws MalformedURLException {
|
||||||
|
return new URL(this.source.getSource().getUrl(), getName());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Attributes getAttributes() throws IOException {
|
public Attributes getAttributes() throws IOException {
|
||||||
Manifest manifest = this.source.getSource().getManifest();
|
Manifest manifest = this.source.getSource().getManifest();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2012-2013 the original author or authors.
|
* Copyright 2012-2014 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -22,6 +22,8 @@ import java.io.InputStream;
|
||||||
import java.lang.ref.SoftReference;
|
import java.lang.ref.SoftReference;
|
||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
import java.net.URLStreamHandler;
|
||||||
|
import java.net.URLStreamHandlerFactory;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Enumeration;
|
import java.util.Enumeration;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
|
@ -63,6 +65,10 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
|
||||||
|
|
||||||
private static final AsciiBytes SIGNATURE_FILE_EXTENSION = new AsciiBytes(".SF");
|
private static final AsciiBytes SIGNATURE_FILE_EXTENSION = new AsciiBytes(".SF");
|
||||||
|
|
||||||
|
private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs";
|
||||||
|
|
||||||
|
private static final String HANDLERS_PACKAGE = "org.springframework.boot.loader";
|
||||||
|
|
||||||
private final RandomAccessDataFile rootFile;
|
private final RandomAccessDataFile rootFile;
|
||||||
|
|
||||||
private final RandomAccessData data;
|
private final RandomAccessData data;
|
||||||
|
|
@ -380,8 +386,32 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
|
||||||
* @throws MalformedURLException
|
* @throws MalformedURLException
|
||||||
*/
|
*/
|
||||||
public URL getUrl() throws MalformedURLException {
|
public URL getUrl() throws MalformedURLException {
|
||||||
JarURLStreamHandler handler = new JarURLStreamHandler(this);
|
Handler handler = new Handler(this);
|
||||||
return new URL("jar", "", -1, "file:" + getName() + "!/", handler);
|
return new URL("jar", "", -1, "file:" + getName() + "!/", handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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() {
|
||||||
|
String handlers = System.getProperty(PROTOCOL_HANDLER);
|
||||||
|
System.setProperty(PROTOCOL_HANDLER, ("".equals(handlers) ? HANDLERS_PACKAGE
|
||||||
|
: handlers + "|" + HANDLERS_PACKAGE));
|
||||||
|
resetCachedUrlHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset any cached handers 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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2012-2013 the original author or authors.
|
* Copyright 2012-2014 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -29,9 +29,11 @@ import java.net.URL;
|
||||||
*/
|
*/
|
||||||
class JarURLConnection extends java.net.JarURLConnection {
|
class JarURLConnection extends java.net.JarURLConnection {
|
||||||
|
|
||||||
private static final String JAR_URL_POSTFIX = "!/";
|
static final String PROTOCOL = "jar";
|
||||||
|
|
||||||
private static final String JAR_URL_PREFIX = "jar:file:";
|
static final String SEPARATOR = "!/";
|
||||||
|
|
||||||
|
private static final String PREFIX = PROTOCOL + ":" + "file:";
|
||||||
|
|
||||||
private final JarFile jarFile;
|
private final JarFile jarFile;
|
||||||
|
|
||||||
|
|
@ -46,9 +48,10 @@ class JarURLConnection extends java.net.JarURLConnection {
|
||||||
this.jarFile = jarFile;
|
this.jarFile = jarFile;
|
||||||
|
|
||||||
String spec = url.getFile();
|
String spec = url.getFile();
|
||||||
int separator = spec.lastIndexOf(JAR_URL_POSTFIX);
|
int separator = spec.lastIndexOf(SEPARATOR);
|
||||||
if (separator == -1) {
|
if (separator == -1) {
|
||||||
throw new MalformedURLException("no !/ found in url spec:" + spec);
|
throw new MalformedURLException("no " + SEPARATOR + " found in url spec:"
|
||||||
|
+ spec);
|
||||||
}
|
}
|
||||||
if (separator + 2 != spec.length()) {
|
if (separator + 2 != spec.length()) {
|
||||||
this.jarEntryName = spec.substring(separator + 2);
|
this.jarEntryName = spec.substring(separator + 2);
|
||||||
|
|
@ -122,11 +125,11 @@ class JarURLConnection extends java.net.JarURLConnection {
|
||||||
|
|
||||||
private static String buildRootUrl(JarFile jarFile) {
|
private static String buildRootUrl(JarFile jarFile) {
|
||||||
String path = jarFile.getRootJarFile().getFile().getPath();
|
String path = jarFile.getRootJarFile().getFile().getPath();
|
||||||
StringBuilder builder = new StringBuilder(JAR_URL_PREFIX.length() + path.length()
|
StringBuilder builder = new StringBuilder(PREFIX.length() + path.length()
|
||||||
+ JAR_URL_POSTFIX.length());
|
+ SEPARATOR.length());
|
||||||
builder.append(JAR_URL_PREFIX);
|
builder.append(PREFIX);
|
||||||
builder.append(path);
|
builder.append(path);
|
||||||
builder.append(JAR_URL_POSTFIX);
|
builder.append(SEPARATOR);
|
||||||
return builder.toString();
|
return builder.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2012-2013 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
|
|
||||||
*
|
|
||||||
* http://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.URL;
|
|
||||||
import java.net.URLConnection;
|
|
||||||
import java.net.URLStreamHandler;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@link URLStreamHandler} used to support {@link JarFile#getUrl()}.
|
|
||||||
*
|
|
||||||
* @author Phillip Webb
|
|
||||||
*/
|
|
||||||
class JarURLStreamHandler extends URLStreamHandler {
|
|
||||||
|
|
||||||
private final JarFile jarFile;
|
|
||||||
|
|
||||||
public JarURLStreamHandler(JarFile jarFile) {
|
|
||||||
this.jarFile = jarFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected URLConnection openConnection(URL url) throws IOException {
|
|
||||||
return new JarURLConnection(url, this.jarFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2012-2013 the original author or authors.
|
* Copyright 2012-2014 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -51,7 +51,7 @@ import static org.mockito.Mockito.verify;
|
||||||
*
|
*
|
||||||
* @author Phillip Webb
|
* @author Phillip Webb
|
||||||
*/
|
*/
|
||||||
public class RandomAccessJarFileTests {
|
public class JarFileTests {
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
public ExpectedException thrown = ExpectedException.none();
|
public ExpectedException thrown = ExpectedException.none();
|
||||||
|
|
@ -153,7 +153,7 @@ public class RandomAccessJarFileTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getEntryUrl() throws Exception {
|
public void createEntryUrl() throws Exception {
|
||||||
URL url = new URL(this.jarFile.getUrl(), "1.dat");
|
URL url = new URL(this.jarFile.getUrl(), "1.dat");
|
||||||
assertThat(url.toString(), equalTo("jar:file:" + this.rootJarFile.getPath()
|
assertThat(url.toString(), equalTo("jar:file:" + this.rootJarFile.getPath()
|
||||||
+ "!/1.dat"));
|
+ "!/1.dat"));
|
||||||
|
|
@ -237,6 +237,29 @@ public class RandomAccessJarFileTests {
|
||||||
sameInstance(nestedJarFile));
|
sameInstance(nestedJarFile));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getNestJarEntryUrl() throws Exception {
|
||||||
|
JarFile nestedJarFile = this.jarFile.getNestedJarFile(this.jarFile
|
||||||
|
.getEntry("nested.jar"));
|
||||||
|
URL url = nestedJarFile.getJarEntry("3.dat").getUrl();
|
||||||
|
assertThat(url.toString(), equalTo("jar:file:" + this.rootJarFile.getPath()
|
||||||
|
+ "!/nested.jar!/3.dat"));
|
||||||
|
InputStream inputStream = url.openStream();
|
||||||
|
assertThat(inputStream, notNullValue());
|
||||||
|
assertThat(inputStream.read(), equalTo(3));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void createUrlFromString() throws Exception {
|
||||||
|
JarFile.registerUrlProtocolHandler();
|
||||||
|
String spec = "jar:file:" + this.rootJarFile.getPath() + "!/nested.jar!/3.dat";
|
||||||
|
URL url = new URL(spec);
|
||||||
|
assertThat(url.toString(), equalTo(spec));
|
||||||
|
InputStream inputStream = url.openStream();
|
||||||
|
assertThat(inputStream, notNullValue());
|
||||||
|
assertThat(inputStream.read(), equalTo(3));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getDirectoryInputStream() throws Exception {
|
public void getDirectoryInputStream() throws Exception {
|
||||||
InputStream inputStream = this.jarFile
|
InputStream inputStream = this.jarFile
|
||||||
Loading…
Reference in New Issue