@ -22,6 +22,8 @@ import java.io.IOException;
import java.io.InputStream ;
import java.io.InputStream ;
import java.net.MalformedURLException ;
import java.net.MalformedURLException ;
import java.net.URL ;
import java.net.URL ;
import java.net.URLConnection ;
import java.net.URLStreamHandler ;
import java.util.jar.Manifest ;
import java.util.jar.Manifest ;
import org.springframework.boot.loader.util.AsciiBytes ;
import org.springframework.boot.loader.util.AsciiBytes ;
@ -33,51 +35,73 @@ import org.springframework.boot.loader.util.AsciiBytes;
* /
* /
class JarURLConnection extends java . net . JarURLConnection {
class JarURLConnection extends java . net . JarURLConnection {
static final String PROTOCOL = "jar" ;
private static final FileNotFoundException FILE_NOT_FOUND_EXCEPTION = new FileNotFoundException ( ) ;
static final String SEPARATOR = "!/" ;
private static final String SEPARATOR = "!/" ;
private static final String PREFIX = PROTOCOL + ":" + "file:" ;
private static final URL EMPTY_JAR_URL ;
private final JarFile jarFile ;
static {
try {
EMPTY_JAR_URL = new URL ( "jar:" , null , 0 , "file:!/" , new URLStreamHandler ( ) {
@Override
protected URLConnection openConnection ( URL u ) throws IOException {
// Stub URLStreamHandler to prevent the wrong JAR Handler from being
// Instantiated and cached.
return null ;
}
} ) ;
}
catch ( MalformedURLException ex ) {
throw new IllegalStateException ( ex ) ;
}
}
private JarEntryData jarEntryData ;
private static final JarEntryName EMPTY_JAR_ENTRY_NAME = new JarEntryName ( "" ) ;
private String jarEntryName ;
private static ThreadLocal < Boolean > useFastExceptions = new ThreadLocal < Boolean > ( ) ;
private String contentType ;
private final String jarFileUrlSpec ;
private final JarFile jarFile ;
private JarEntryData jarEntryData ;
private URL jarFileUrl ;
private URL jarFileUrl ;
private JarEntryName jarEntryName ;
protected JarURLConnection ( URL url , JarFile jarFile ) throws MalformedURLException {
protected JarURLConnection ( URL url , JarFile jarFile ) throws MalformedURLException {
super ( new URL ( buildRootUrl ( jarFile ) ) ) ;
// What we pass to super is ultimately ignored
super ( EMPTY_JAR_URL ) ;
this . url = url ;
this . url = url ;
this . jarFile = jarFile ;
this . jarFile = jarFile ;
String spec = url . getFile ( ) ;
String spec = url . getFile ( ) ;
int separator = spec . lastIndexOf ( SEPARATOR ) ;
int separator = spec . lastIndexOf ( SEPARATOR ) ;
if ( separator = = - 1 ) {
if ( separator = = - 1 ) {
throw new MalformedURLException ( "no " + SEPARATOR + " found in url spec:"
throw new MalformedURLException ( "no " + SEPARATOR + " found in url spec:"
+ spec ) ;
+ spec ) ;
}
}
if ( separator + 2 ! = spec . length ( ) ) {
this. jarFileUrlSpec = spec . substring ( 0 , separator ) ;
this . jarEntryName = decod e( spec . substring ( separator + 2 ) ) ;
this . jarEntryName = getJarEntryNam e( spec . substring ( separator + 2 ) ) ;
}
}
String container = spec . substring ( 0 , separator ) ;
private JarEntryName getJarEntryName ( String spec ) {
if ( container . indexOf ( SEPARATOR ) = = - 1 ) {
if ( spec . length ( ) = = 0 ) {
this . jarFileUrl = new URL ( container ) ;
return EMPTY_JAR_ENTRY_NAME ;
}
else {
this . jarFileUrl = new URL ( "jar:" + container ) ;
}
}
return new JarEntryName ( spec ) ;
}
}
@Override
@Override
public void connect ( ) throws IOException {
public void connect ( ) throws IOException {
if ( this . jarEntryName ! = null ) {
if ( ! this . jarEntryName . isEmpty ( ) ) {
this . jarEntryData = this . jarFile . getJarEntryData ( this . jarEntryName ) ;
this . jarEntryData = this . jarFile . getJarEntryData ( this . jarEntryName
. asAsciiBytes ( ) ) ;
if ( this . jarEntryData = = null ) {
if ( this . jarEntryData = = null ) {
if ( Boolean . TRUE . equals ( useFastExceptions . get ( ) ) ) {
throw FILE_NOT_FOUND_EXCEPTION ;
}
throw new FileNotFoundException ( "JAR entry " + this . jarEntryName
throw new FileNotFoundException ( "JAR entry " + this . jarEntryName
+ " not found in " + this . jarFile . getName ( ) ) ;
+ " not found in " + this . jarFile . getName ( ) ) ;
}
}
@ -103,9 +127,24 @@ class JarURLConnection extends java.net.JarURLConnection {
@Override
@Override
public URL getJarFileURL ( ) {
public URL getJarFileURL ( ) {
if ( this . jarFileUrl = = null ) {
this . jarFileUrl = buildJarFileUrl ( ) ;
}
return this . jarFileUrl ;
return this . jarFileUrl ;
}
}
private URL buildJarFileUrl ( ) {
try {
if ( this . jarFileUrlSpec . indexOf ( SEPARATOR ) = = - 1 ) {
return new URL ( this . jarFileUrlSpec ) ;
}
return new URL ( "jar:" + this . jarFileUrlSpec ) ;
}
catch ( MalformedURLException ex ) {
throw new IllegalStateException ( ex ) ;
}
}
@Override
@Override
public JarEntry getJarEntry ( ) throws IOException {
public JarEntry getJarEntry ( ) throws IOException {
connect ( ) ;
connect ( ) ;
@ -114,13 +153,13 @@ class JarURLConnection extends java.net.JarURLConnection {
@Override
@Override
public String getEntryName ( ) {
public String getEntryName ( ) {
return this . jarEntryName ;
return this . jarEntryName .toString ( ) ;
}
}
@Override
@Override
public InputStream getInputStream ( ) throws IOException {
public InputStream getInputStream ( ) throws IOException {
connect ( ) ;
connect ( ) ;
if ( this . jarEntryName = = null ) {
if ( this . jarEntryName . isEmpty ( ) ) {
throw new IOException ( "no entry name specified" ) ;
throw new IOException ( "no entry name specified" ) ;
}
}
return this . jarEntryData . getInputStream ( ) ;
return this . jarEntryData . getInputStream ( ) ;
@ -130,8 +169,10 @@ class JarURLConnection extends java.net.JarURLConnection {
public int getContentLength ( ) {
public int getContentLength ( ) {
try {
try {
connect ( ) ;
connect ( ) ;
return this . jarEntryData = = null ? this . jarFile . size ( ) : this . jarEntryData
if ( this . jarEntryData ! = null ) {
. getSize ( ) ;
return this . jarEntryData . getSize ( ) ;
}
return this . jarFile . size ( ) ;
}
}
catch ( IOException ex ) {
catch ( IOException ex ) {
return - 1 ;
return - 1 ;
@ -146,32 +187,30 @@ class JarURLConnection extends java.net.JarURLConnection {
@Override
@Override
public String getContentType ( ) {
public String getContentType ( ) {
if ( this . contentType = = null ) {
return this . jarEntryName . getContentType ( ) ;
// Guess the content type, don't bother with steams as mark is not
// supported
this . contentType = ( this . jarEntryName = = null ? "x-java/jar" : null ) ;
this . contentType = ( this . contentType = = null ? guessContentTypeFromName ( this . jarEntryName )
: this . contentType ) ;
this . contentType = ( this . contentType = = null ? "content/unknown"
: this . contentType ) ;
}
}
return this . contentType ;
static void setUseFastExceptions ( boolean useFastExceptions ) {
JarURLConnection . useFastExceptions . set ( useFastExceptions ) ;
}
}
private static String buildRootUrl ( JarFile jarFile ) {
/ * *
String path = jarFile . getRootJarFile ( ) . getFile ( ) . getPath ( ) ;
* A JarEntryName parsed from a URL String .
StringBuilder builder = new StringBuilder ( PREFIX . length ( ) + path . length ( )
* /
+ SEPARATOR . length ( ) ) ;
private static class JarEntryName {
builder . append ( PREFIX ) ;
builder . append ( path ) ;
private final AsciiBytes name ;
builder . append ( SEPARATOR ) ;
return builder . toString ( ) ;
private String contentType ;
public JarEntryName ( String spec ) {
this . name = decode ( spec ) ;
}
}
private static String decode ( String source ) {
private AsciiBytes decode ( String source ) {
int length = source . length ( ) ;
int length = ( source = = null ? 0 : source . length ( ) ) ;
if ( ( length = = 0 ) | | ( source . indexOf ( '%' ) < 0 ) ) {
if ( ( length = = 0 ) | | ( source . indexOf ( '%' ) < 0 ) ) {
return source;
return new AsciiBytes( source) ;
}
}
ByteArrayOutputStream bos = new ByteArrayOutputStream ( length ) ;
ByteArrayOutputStream bos = new ByteArrayOutputStream ( length ) ;
for ( int i = 0 ; i < length ; i + + ) {
for ( int i = 0 ; i < length ; i + + ) {
@ -187,11 +226,10 @@ class JarURLConnection extends java.net.JarURLConnection {
bos . write ( ch ) ;
bos . write ( ch ) ;
}
}
// AsciiBytes is what is used to store the JarEntries so make it symmetric
// AsciiBytes is what is used to store the JarEntries so make it symmetric
return new AsciiBytes ( bos . toByteArray ( ) ) . toString ( ) ;
return new AsciiBytes ( bos . toByteArray ( ) ) ;
}
}
private static char decodeEscapeSequence ( String source , int i ) {
private char decodeEscapeSequence ( String source , int i ) {
int hi = Character . digit ( source . charAt ( i + 1 ) , 16 ) ;
int hi = Character . digit ( source . charAt ( i + 1 ) , 16 ) ;
int lo = Character . digit ( source . charAt ( i + 2 ) , 16 ) ;
int lo = Character . digit ( source . charAt ( i + 2 ) , 16 ) ;
if ( hi = = - 1 | | lo = = - 1 ) {
if ( hi = = - 1 | | lo = = - 1 ) {
@ -200,4 +238,35 @@ class JarURLConnection extends java.net.JarURLConnection {
}
}
return ( ( char ) ( ( hi < < 4 ) + lo ) ) ;
return ( ( char ) ( ( hi < < 4 ) + lo ) ) ;
}
}
@Override
public String toString ( ) {
return this . name . toString ( ) ;
}
public AsciiBytes asAsciiBytes ( ) {
return this . name ;
}
public boolean isEmpty ( ) {
return this . name . length ( ) = = 0 ;
}
public String getContentType ( ) {
if ( this . contentType = = null ) {
this . contentType = deduceContentType ( ) ;
}
return this . contentType ;
}
private String deduceContentType ( ) {
// Guess the content type, don't bother with streams as mark is not supported
String type = ( isEmpty ( ) ? "x-java/jar" : null ) ;
type = ( type ! = null ? type : guessContentTypeFromName ( toString ( ) ) ) ;
type = ( type ! = null ? type : "content/unknown" ) ;
return type ;
}
}
}
}