@ -24,12 +24,8 @@ 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 ;
import java.util.Iterator ;
import java.util.List ;
import java.util.Map ;
import java.util.jar.JarInputStream ;
import java.util.jar.Manifest ;
import java.util.zip.ZipEntry ;
@ -37,36 +33,30 @@ import java.util.zip.ZipEntry;
import org.springframework.boot.loader.data.RandomAccessData ;
import org.springframework.boot.loader.data.RandomAccessData.ResourceAccess ;
import org.springframework.boot.loader.data.RandomAccessDataFile ;
import org.springframework.boot.loader.util.AsciiBytes ;
/ * *
* Extended variant of { @link java . util . jar . JarFile } that behaves in the same way but
* offers the following additional functionality .
* < ul >
* < li > New filtered files can be { @link # getFilteredJarFile ( JarEntryFilter . . . ) created }
* from existing files . < / li >
* < li > A nested { @link JarFile } can be { @link # getNestedJarFile ( ZipEntry ) obtained } based
* on any directory entry . < / li >
* < li > A nested { @link JarFile } can be { @link # getNestedJarFile ( ZipEntry ) obtained } for
* embedded JAR files ( as long as their entry is not compressed ) . < / li >
* < li > Entry data can be accessed as { @link RandomAccessData } . < / li >
* < / ul >
*
* @author Phillip Webb
* /
public class JarFile extends java . util . jar . JarFile implements Iterable < JarEntryData > {
public class JarFile extends java . util . jar . JarFile {
private static final AsciiBytes META_INF = new AsciiBytes ( "META-INF/" ) ;
private static final AsciiBytes MANIFEST_MF = new AsciiBytes ( "META-INF/MANIFEST.MF" ) ;
private static final AsciiBytes SIGNATURE_FILE_EXTENSION = new AsciiBytes ( ".SF" ) ;
private static final String MANIFEST_NAME = "META-INF/MANIFEST.MF" ;
private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs" ;
private static final String HANDLERS_PACKAGE = "org.springframework.boot.loader" ;
private static final AsciiBytes SLASH = new AsciiBytes ( "/" ) ;
private static final AsciiBytes META_INF = new AsciiBytes ( "META-INF/" ) ;
private static final AsciiBytes SIGNATURE_FILE_EXTENSION = new AsciiBytes ( ".SF" ) ;
private final RandomAccessDataFile rootFile ;
@ -74,17 +64,13 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
private final RandomAccessData data ;
private final List < JarEntryData > entries ;
private SoftReference < Map < AsciiBytes , JarEntryData > > entriesByName ;
private boolean signed ;
private URL url ;
private Jar EntryData manifestEntry ;
private JarFileEntries entries ;
private SoftReference < Manifest > manifest ;
private URL url ;
private boolean signed ;
/ * *
* Create a new { @link JarFile } backed by the specified file .
@ -114,85 +100,43 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
* /
private JarFile ( RandomAccessDataFile rootFile , String pathFromRoot ,
RandomAccessData data ) throws IOException {
super ( rootFile . getFile ( ) ) ;
CentralDirectoryEndRecord endRecord = new CentralDirectoryEndRecord ( data ) ;
this . rootFile = rootFile ;
this . pathFromRoot = pathFromRoot ;
this . data = getArchiveData ( endRecord , data ) ;
this . entries = loadJarEntries ( endRecord ) ;
this ( rootFile , pathFromRoot , data , null ) ;
}
private JarFile ( RandomAccessDataFile rootFile , String pathFromRoot ,
RandomAccessData data , List < JarEntryData > entries , JarEntryFilter . . . filters )
throws IOException {
RandomAccessData data , JarEntryFilter filter ) throws IOException {
super ( rootFile . getFile ( ) ) ;
this . rootFile = rootFile ;
this . pathFromRoot = pathFromRoot ;
this . data = data ;
this . entries = filterEntries ( entries , filters ) ;
CentralDirectoryParser parser = new CentralDirectoryParser ( ) ;
this . entries = parser . addVistor ( new JarFileEntries ( this , filter ) ) ;
parser . addVistor ( centralDirectoryVistor ( ) ) ;
this . data = parser . parse ( data , filter = = null ) ;
}
private RandomAccessData getArchiveData ( CentralDirectoryEndRecord endRecord ,
RandomAccessData data ) {
long offset = endRecord . getStartOfArchive ( data ) ;
if ( offset = = 0 ) {
return data ;
}
return data . getSubsection ( offset , data . getSize ( ) - offset ) ;
}
private CentralDirectoryVistor centralDirectoryVistor ( ) {
return new CentralDirectoryVistor ( ) {
private List < JarEntryData > loadJarEntries ( CentralDirectoryEndRecord endRecord )
throws IOException {
RandomAccessData centralDirectory = endRecord . getCentralDirectory ( this . data ) ;
int numberOfRecords = endRecord . getNumberOfRecords ( ) ;
List < JarEntryData > entries = new ArrayList < JarEntryData > ( numberOfRecords ) ;
InputStream inputStream = centralDirectory . getInputStream ( ResourceAccess . ONCE ) ;
try {
JarEntryData entry = JarEntryData . fromInputStream ( this , inputStream ) ;
while ( entry ! = null ) {
entries . add ( entry ) ;
processEntry ( entry ) ;
entry = JarEntryData . fromInputStream ( this , inputStream ) ;
}
}
finally {
inputStream . close ( ) ;
}
return entries ;
@Override
public void visitStart ( CentralDirectoryEndRecord endRecord ,
RandomAccessData centralDirectoryData ) {
}
private List < JarEntryData > filterEntries ( List < JarEntryData > entries ,
JarEntryFilter [ ] filters ) {
List < JarEntryData > filteredEntries = new ArrayList < JarEntryData > ( entries . size ( ) ) ;
for ( JarEntryData entry : entries ) {
AsciiBytes name = entry . getName ( ) ;
for ( JarEntryFilter filter : filters ) {
name = ( filter = = null | | name = = null ? name
: filter . apply ( name , entry ) ) ;
}
if ( name ! = null ) {
JarEntryData filteredCopy = entry . createFilteredCopy ( this , name ) ;
filteredEntries . add ( filteredCopy ) ;
processEntry ( filteredCopy ) ;
}
@Override
public void visitFileHeader ( CentralDirectoryFileHeader fileHeader ,
int dataOffset ) {
AsciiBytes name = fileHeader . getName ( ) ;
if ( name . startsWith ( META_INF )
& & name . endsWith ( SIGNATURE_FILE_EXTENSION ) ) {
JarFile . this . signed = true ;
}
return filteredEntries ;
}
private void processEntry ( JarEntryData entry ) {
AsciiBytes name = entry . getName ( ) ;
if ( name . startsWith ( META_INF ) ) {
processMetaInfEntry ( name , entry ) ;
}
@Override
public void visitEnd ( ) {
}
private void processMetaInfEntry ( AsciiBytes name , JarEntryData entry ) {
if ( name . equals ( MANIFEST_MF ) ) {
this . manifestEntry = entry ;
}
if ( name . endsWith ( SIGNATURE_FILE_EXTENSION ) ) {
this . signed = true ;
}
} ;
}
protected final RandomAccessDataFile getRootJarFile ( ) {
@ -205,12 +149,12 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
@Override
public Manifest getManifest ( ) throws IOException {
if ( this . manifestEntry = = null ) {
return null ;
}
Manifest manifest = ( this . manifest = = null ? null : this . manifest . get ( ) ) ;
if ( manifest = = null ) {
InputStream inputStream = this . manifestEntry . getInputStream ( ) ;
InputStream inputStream = getInputStream ( MANIFEST_NAME , ResourceAccess . ONCE ) ;
if ( inputStream = = null ) {
return null ;
}
try {
manifest = new Manifest ( inputStream ) ;
}
@ -224,7 +168,7 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
@Override
public Enumeration < java . util . jar . JarEntry > entries ( ) {
final Iterator < JarEntry Data> iterator = iterator( ) ;
final Iterator < JarEntry > iterator = this . entries . iterator( ) ;
return new Enumeration < java . util . jar . JarEntry > ( ) {
@Override
@ -234,14 +178,10 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
@Override
public java . util . jar . JarEntry nextElement ( ) {
return iterator . next ( ) . asJarEntry ( ) ;
}
} ;
return iterator . next ( ) ;
}
@Override
public Iterator < JarEntryData > iterator ( ) {
return this . entries . iterator ( ) ;
} ;
}
@Override
@ -251,72 +191,24 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
@Override
public ZipEntry getEntry ( String name ) {
JarEntryData jarEntryData = getJarEntryData ( name ) ;
return ( jarEntryData = = null ? null : jarEntryData . asJarEntry ( ) ) ;
return this . entries . getEntry ( name ) ;
}
public JarEntryData getJarEntryData ( String name ) {
if ( name = = null ) {
return null ;
}
return getJarEntryData ( new AsciiBytes ( name ) ) ;
}
public JarEntryData getJarEntryData ( AsciiBytes name ) {
if ( name = = null ) {
return null ;
}
Map < AsciiBytes , JarEntryData > entriesByName = ( this . entriesByName = = null ? null
: this . entriesByName . get ( ) ) ;
if ( entriesByName = = null ) {
entriesByName = new HashMap < AsciiBytes , JarEntryData > ( ) ;
for ( JarEntryData entry : this . entries ) {
entriesByName . put ( entry . getName ( ) , entry ) ;
}
this . entriesByName = new SoftReference < Map < AsciiBytes , JarEntryData > > (
entriesByName ) ;
}
JarEntryData entryData = entriesByName . get ( name ) ;
if ( entryData = = null & & ! name . endsWith ( SLASH ) ) {
entryData = entriesByName . get ( name . append ( SLASH ) ) ;
}
return entryData ;
}
boolean isSigned ( ) {
return this . signed ;
@Override
public synchronized InputStream getInputStream ( ZipEntry ze ) throws IOException {
return getInputStream ( ze , ResourceAccess . PER_READ ) ;
}
void setupEntryCertificates ( ) {
// Fallback to JarInputStream to obtain certificates, not fast but hopefully not
// happening that often.
try {
JarInputStream inputStream = new JarInputStream (
getData ( ) . getInputStream ( ResourceAccess . ONCE ) ) ;
try {
java . util . jar . JarEntry entry = inputStream . getNextJarEntry ( ) ;
while ( entry ! = null ) {
inputStream . closeEntry ( ) ;
JarEntry jarEntry = getJarEntry ( entry . getName ( ) ) ;
if ( jarEntry ! = null ) {
jarEntry . setupCertificates ( entry ) ;
}
entry = inputStream . getNextJarEntry ( ) ;
}
}
finally {
inputStream . close ( ) ;
}
}
catch ( IOException ex ) {
throw new IllegalStateException ( ex ) ;
public InputStream getInputStream ( ZipEntry ze , ResourceAccess access )
throws IOException {
if ( ze instanceof JarEntry ) {
return this . entries . getInputStream ( ( JarEntry ) ze , access ) ;
}
return getInputStream ( ze = = null ? null : ze . getName ( ) , access ) ;
}
@Override
public synchronized InputStream getInputStream ( ZipEntry ze ) throws IOException {
return getContainedEntry ( ze ) . getSource ( ) . getInputStream ( ) ;
InputStream getInputStream ( String name , ResourceAccess access ) throws IOException {
return this . entries . getInputStream ( name , access ) ;
}
/ * *
@ -327,42 +219,37 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
* /
public synchronized JarFile getNestedJarFile ( final ZipEntry entry )
throws IOException {
return getNestedJarFile ( getContainedEntry ( entry ) . getSource ( ) ) ;
return getNestedJarFile ( ( JarEntry ) entry ) ;
}
/ * *
* Return a nested { @link JarFile } loaded from the specified entry .
* @param sourc eE ntry the zip entry
* @param entry the zip entry
* @return a { @link JarFile } for the entry
* @throws IOException if the nested jar file cannot be read
* /
public synchronized JarFile getNestedJarFile ( JarEntryData sourceEntry )
throws IOException {
public synchronized JarFile getNestedJarFile ( JarEntry entry ) throws IOException {
try {
if ( sourceEntry . nestedJar = = null ) {
sourceEntry . nestedJar = createJarFileFromEntry ( sourceEntry ) ;
}
return sourceEntry . nestedJar ;
return createJarFileFromEntry ( entry ) ;
}
catch ( IOException ex ) {
throw new IOException (
"Unable to open nested jar file '" + sourc eE ntry. getName ( ) + "'" , ex ) ;
"Unable to open nested jar file '" + entry. getName ( ) + "'" , ex ) ;
}
}
private JarFile createJarFileFromEntry ( JarEntry Data sourc eE ntry) throws IOException {
if ( sourc eE ntry. isDirectory ( ) ) {
return createJarFileFromDirectoryEntry ( sourc eE ntry) ;
private JarFile createJarFileFromEntry ( JarEntry entry) throws IOException {
if ( entry. isDirectory ( ) ) {
return createJarFileFromDirectoryEntry ( entry) ;
}
return createJarFileFromFileEntry ( sourc eE ntry) ;
return createJarFileFromFileEntry ( entry) ;
}
private JarFile createJarFileFromDirectoryEntry ( JarEntryData sourceEntry )
throws IOException {
final AsciiBytes sourceName = sourceEntry . getName ( ) ;
private JarFile createJarFileFromDirectoryEntry ( JarEntry entry ) throws IOException {
final AsciiBytes sourceName = new AsciiBytes ( entry . getName ( ) ) ;
JarEntryFilter filter = new JarEntryFilter ( ) {
@Override
public AsciiBytes apply ( AsciiBytes name , JarEntryData entryData ) {
public AsciiBytes apply ( AsciiBytes name ) {
if ( name . startsWith ( sourceName ) & & ! name . equals ( sourceName ) ) {
return name . substring ( sourceName . length ( ) ) ;
}
@ -371,40 +258,20 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
} ;
return new JarFile ( this . rootFile ,
this . pathFromRoot + "!/"
+ sourc eE ntry. getName ( ) . substring ( 0 , sourceName . length ( ) - 1 ) ,
this . data , this . entries , filter ) ;
+ entry. getName ( ) . substring ( 0 , sourceName . length ( ) - 1 ) ,
this . data , filter ) ;
}
private JarFile createJarFileFromFileEntry ( JarEntryData sourceEntry )
throws IOException {
if ( sourceEntry . getMethod ( ) ! = ZipEntry . STORED ) {
private JarFile createJarFileFromFileEntry ( JarEntry entry ) throws IOException {
if ( entry . getMethod ( ) ! = ZipEntry . STORED ) {
throw new IllegalStateException ( "Unable to open nested entry '"
+ sourc eE ntry. getName ( ) + "'. It has been compressed and nested "
+ entry. getName ( ) + "'. It has been compressed and nested "
+ "jar files must be stored without compression. Please check the "
+ "mechanism used to create your executable jar file" ) ;
}
return new JarFile ( this . rootFile ,
this . pathFromRoot + "!/" + sourceEntry . getName ( ) , sourceEntry . getData ( ) ) ;
}
/ * *
* Return a new jar based on the filtered contents of this file .
* @param filters the set of jar entry filters to be applied
* @return a filtered { @link JarFile }
* @throws IOException if the jar file cannot be read
* /
public synchronized JarFile getFilteredJarFile ( JarEntryFilter . . . filters )
throws IOException {
return new JarFile ( this . rootFile , this . pathFromRoot , this . data , this . entries ,
filters ) ;
}
private JarEntry getContainedEntry ( ZipEntry zipEntry ) throws IOException {
if ( zipEntry instanceof JarEntry
& & ( ( JarEntry ) zipEntry ) . getSource ( ) . getSource ( ) = = this ) {
return ( JarEntry ) zipEntry ;
}
throw new IllegalArgumentException ( "ZipEntry must be contained in this file" ) ;
RandomAccessData entryData = this . entries . getEntryData ( entry . getName ( ) ) ;
return new JarFile ( this . rootFile , this . pathFromRoot + "!/" + entry . getName ( ) ,
entryData ) ;
}
@Override
@ -444,6 +311,42 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
return this . rootFile . getFile ( ) + path ;
}
boolean isSigned ( ) {
return this . signed ;
}
void setupEntryCertificates ( JarEntry entry ) {
// Fallback to JarInputStream to obtain certificates, not fast but hopefully not
// happening that often.
try {
JarInputStream inputStream = new JarInputStream (
getData ( ) . getInputStream ( ResourceAccess . ONCE ) ) ;
try {
java . util . jar . JarEntry certEntry = inputStream . getNextJarEntry ( ) ;
while ( certEntry ! = null ) {
inputStream . closeEntry ( ) ;
if ( entry . getName ( ) . equals ( certEntry . getName ( ) ) ) {
setCertificates ( entry , certEntry ) ;
}
setCertificates ( getJarEntry ( certEntry . getName ( ) ) , certEntry ) ;
certEntry = inputStream . getNextJarEntry ( ) ;
}
}
finally {
inputStream . close ( ) ;
}
}
catch ( IOException ex ) {
throw new IllegalStateException ( ex ) ;
}
}
private void setCertificates ( JarEntry entry , java . util . jar . JarEntry certEntry ) {
if ( entry ! = null ) {
entry . setCertificates ( certEntry ) ;
}
}
/ * *
* Register a { @literal ' java . protocol . handler . pkgs ' } property so that a
* { @link URLStreamHandler } will be located to deal with jar URLs .