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
|
||||
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
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
* 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 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
|
||||
|
|
@ -49,6 +50,7 @@ public abstract class Launcher {
|
|||
*/
|
||||
protected void launch(String[] args) {
|
||||
try {
|
||||
JarFile.registerUrlProtocolHandler();
|
||||
ClassLoader classLoader = createClassLoader(getClassPathArchives());
|
||||
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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
|
@ -17,6 +17,8 @@
|
|||
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;
|
||||
|
|
@ -47,6 +49,13 @@ public class JarEntry extends java.util.jar.JarEntry {
|
|||
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
|
||||
public Attributes getAttributes() throws IOException {
|
||||
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");
|
||||
* 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.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLStreamHandler;
|
||||
import java.net.URLStreamHandlerFactory;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Enumeration;
|
||||
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 String PROTOCOL_HANDLER = "java.protocol.handler.pkgs";
|
||||
|
||||
private static final String HANDLERS_PACKAGE = "org.springframework.boot.loader";
|
||||
|
||||
private final RandomAccessDataFile rootFile;
|
||||
|
||||
private final RandomAccessData data;
|
||||
|
|
@ -380,8 +386,32 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
|
|||
* @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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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");
|
||||
* 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 {
|
||||
|
||||
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;
|
||||
|
||||
|
|
@ -46,9 +48,10 @@ class JarURLConnection extends java.net.JarURLConnection {
|
|||
this.jarFile = jarFile;
|
||||
|
||||
String spec = url.getFile();
|
||||
int separator = spec.lastIndexOf(JAR_URL_POSTFIX);
|
||||
int separator = spec.lastIndexOf(SEPARATOR);
|
||||
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()) {
|
||||
this.jarEntryName = spec.substring(separator + 2);
|
||||
|
|
@ -122,11 +125,11 @@ class JarURLConnection extends java.net.JarURLConnection {
|
|||
|
||||
private static String buildRootUrl(JarFile jarFile) {
|
||||
String path = jarFile.getRootJarFile().getFile().getPath();
|
||||
StringBuilder builder = new StringBuilder(JAR_URL_PREFIX.length() + path.length()
|
||||
+ JAR_URL_POSTFIX.length());
|
||||
builder.append(JAR_URL_PREFIX);
|
||||
StringBuilder builder = new StringBuilder(PREFIX.length() + path.length()
|
||||
+ SEPARATOR.length());
|
||||
builder.append(PREFIX);
|
||||
builder.append(path);
|
||||
builder.append(JAR_URL_POSTFIX);
|
||||
builder.append(SEPARATOR);
|
||||
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");
|
||||
* 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
|
||||
*/
|
||||
public class RandomAccessJarFileTests {
|
||||
public class JarFileTests {
|
||||
|
||||
@Rule
|
||||
public ExpectedException thrown = ExpectedException.none();
|
||||
|
|
@ -153,7 +153,7 @@ public class RandomAccessJarFileTests {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void getEntryUrl() throws Exception {
|
||||
public void createEntryUrl() throws Exception {
|
||||
URL url = new URL(this.jarFile.getUrl(), "1.dat");
|
||||
assertThat(url.toString(), equalTo("jar:file:" + this.rootJarFile.getPath()
|
||||
+ "!/1.dat"));
|
||||
|
|
@ -237,6 +237,29 @@ public class RandomAccessJarFileTests {
|
|||
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
|
||||
public void getDirectoryInputStream() throws Exception {
|
||||
InputStream inputStream = this.jarFile
|
||||
Loading…
Reference in New Issue