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
pull/276/head
Phillip Webb 11 years ago
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…
Cancel
Save