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:
Phillip Webb 2014-01-25 21:05:06 -08:00
parent c1f8fd2bac
commit 01550fcec6
8 changed files with 176 additions and 76 deletions

View File

@ -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.

View File

@ -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);
} }

View File

@ -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);
}
}

View File

@ -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();

View File

@ -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
}
}
} }

View File

@ -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();
} }

View File

@ -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);
}
}

View File

@ -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