Java Tip 83: Use filters to access resources in Java archives

Roll your own zip filters and cache resources to create application-specific views on Java archives

Starting with JDK 1.1, archive files were used to bundle together such arbitrary Java resources as class files, image files, and sound files. They are now used to shrink-wrap logically related resources that collectively define applications (for example, Web applications), libraries (Java 2 platform), beans (EJB), and applets.

The java.util.jar and java.util.zip packages let Java applications programmatically access archive files. With the diverse set of resource types that an archive file can store, there is a need for better tools that read, query, and load these resources from Java applications. The figure below illustrates the classes and interfaces that I implement in this article, and which provide a code foundation for such tools.

Classes and interfaces implemented in this article

John Mitchell, Arthur Choi, and Todd Sundsted have demonstrated how to extract data from archive files in previous JavaWorld articles (see the Resources section below for links). For the purposes of this article, I'm assuming that you're familiar with these basic concepts. I'll start by explaining what a Java archive filter is and walk through some example source code. Next, I'll implement the JarClassTable class. JarClassTable demonstrates how you can combine archive filters with a simple caching system to create simple, easy-to-define views of resources in Java archive files. You'll see how easy this all is in Java.

What is a Java archive filter?

An archive file is simply a zip file with entries of various types. You want to be able to load archive files of varying sizes and apply filters to them. The JDK provides ways in which you can access and load the complete contents of an archive, but does not supply code with which you can access specific resources within the file. In order to do that, or create application-defined views of zip files, you'll need an archive filter.

You implement archive filters in much the same way that you would pass a java.io.FileFilter to the list() method of java.io.File in order to get specific files in a directory. In the following example, SuffixZipEntryFilter, a zip filter, is used by JarInfo to provide a view of an archive file. The full source code for SuffixZipEntryFilter and JarInfo is packaged in the jar file in Resources.

ZipEntryFilter classFilter = new SuffixZipEntryFilter(".class");
JarInfo jinfo              = new JarInfo("yourarchive.jar", classFilter);
System.out.println( jinfo );

The code above displays all entries within yourarchive.jar whose zip entry names end with .class. Like java.io.FileFilter, ZipEntryFilter is an interface that has a single accept() method; however, it filters on ZipEntry, not java.io.File.

public interface ZipEntryFilter
{
  public boolean accept( ZipEntry ze );   
}

SuffixZipEntryFilter implements ZipEntryFilter, placing the filter logic inside the accept() method.

public class SuffixZipEntryFilter implements ZipEntryFilter
{    
  private String fSuffix;
 
  public SuffixZipEntryFilter(String suffix)
  {
    fSuffix = suffix;
  }
  public boolean accept( ZipEntry ze )
  {
    if( ze == null || fSuffix == null )
      return false;         
    return ze.getName().endsWith(fSuffix);
  }  
}

JarInfo uses zip entry filters to weed out unacceptable resources. The following example demonstrates how JarInfo's constructor takes both a zip file and a zip entry filter to determine which zip entries it should extract from the archive:

public JarInfo(String fileName, ZipEntryFilter filter)
    throws JarInfoException
{
  if( fileName == null )
    throw new JarInfoException("supplied filename to JarInfo was null");
  if( filter == null )
    throw new JarInfoException("supplied filter to JarInfo was null");
  fEntries        = new Hashtable();      
  ZipFile zipFile = null;
    
  try
  {
    // create a ZipFile for the supplied 'fileName'
    zipFile = new ZipFile(fileName);      
      
    // cycle through the header information with the zipFile
    Enumeration e = zipFile.entries(); 
    while( e.hasMoreElements() )
    {
      ZipEntry ze = (ZipEntry)e.nextElement();
       
      // apply filter and store zip entry if accepted
      if( filter.accept(ze) == true )
        fEntries.put( ze.getName(), ze ); 
    }
  }
  catch( ZipException zipe )
  {
    throw new JarInfoException( "Zip format problems reading " + fileName 
                                + "\n" + zipe.getMessage() );
  }
  catch( IOException ioe )
  {
    throw new JarInfoException( ioe.getMessage() );
  }
  catch( SecurityException se )
  {
    throw new JarInfoException( "Security problems reading " + fileName 
                                + "\n" + se.getMessage() );
  }
  finally
  {
    if( zipFile != null )
      try { zipFile.close(); } catch(IOException ioe) {}
  }    
}

If you want JarInfo to extract and store all zip entries in an archive, just pass it a filter whose accept() method always returns true.

The ZipEntryFilter interface provides a simple, extensible mechanism by which developers can implement zip entry filters. JarInfo takes these filters and applies them to create a lightweight view of a Java archive. In the next section, I'll demonstrate how to use JarInfo to create a simple cache for class resources.

Using JarInfo

JarInfo provides you with the header information for filtered resources, but not the actual data. The next step is to load the data from a ZipEntry and make it available to your applications. The pros and cons of caching now come into play. Does the cost of storing data for these resources in memory outweigh the cost of repeatedly loading them from an archive file? This decision is dependent on the application. In our example, JarClassTable implements a simple cache for the bytes of Java class files loaded from a named archive file. JarClassTable uses JarInfo to provide a convenient, class-only view of an archive by filtering all resources that end in .class:

  public JarClassTable(String zipFile) throws JarInfoException
  {    
    // creates a JarInfo object that contains zip entries for Java Classes only
    fInfo         = new JarInfo( zipFile, new SuffixZipEntryFilter(kCLASS_SUFFIX) );        
    
    fClassTable   = new Hashtable();     // maps class names to class bytes
    fZipTable     = new Hashtable();     // maps class names to ZipEntry
    fZipFileName  = zipFile;         // holds copy of zip file name
    init();
  }

getClassBytes() is the only public method in JarClassTable. For a named class, it retrieves the bytes that make up the class file. getClassBytes() first consults the cache to see if it already has the required class bytes. If so, these are returned; otherwise, the bytes for the named class are fetched from the archive file using the loadBytes() method. This method is well commented in the source code; a full explanation of loading bytes from archive files is given in Mitchell and Choi's article, "Java Tip 49: How to extract Java resources from jar and zip archives" (see Resources).

  public byte[] getClassBytes( String className ) throws JarInfoException
  {
    if( className == null )
      return null;
      
    // first check to see if bytes for this class are cached
    byte[] b = (byte[])fClassTable.get(className);
    
    // if not
    if( b == null )
    {
      // get classes bytes from archive and cache them
      if( (b = loadBytes(className)) != null )
        fClassTable.put( className, b );      
      else
        throw new JarInfoException("Unable to load class bytes for -> " 
                   + className);
    }
    return b;
  }

Incidentally, the

init()

method in

JarClassTable

creates a hashtable of zip entries keyed by their class name. It does this by converting the zip entry name,

/com/ack/jar/JarClassTable.class

, into its appropriate Java class name,

com.ack.jar.JarClassTable

. You may prefer to do this name mangling as calls are made to

getClassBytes()

instead of creating such a hashtable.

Using JarClassTable

So far, we have created a general-purpose utility class, JarInfo, that lets you effortlessly create your own views of an archive file by implementing filters. We then implemented JarClassTable, which made direct use of JarInfo to provide a load-on-demand cache of class bytes. However, JarClassTable only lets you retrieve the bytes for a class. The following code demonstrates how the customized JarInfoClassLoader uses JarClassTable to load classes from an archive file and make them available as Class objects within Java applications.

public class JarInfoClassLoader extends SecureClassLoader
{ 
  private JarClassTable fClassTable;
  
  public JarInfoClassLoader(String zipFile, ClassLoader cl) throws JarInfoException
  {        
    super( cl );    
    fClassTable = new JarClassTable(zipFile);    
  }

Java 2 uses the delegation model for class loading. Under this model, loading a class is attempted first by the parent ClassLoader and then by JarInfoClassLoader .

The only other method in JarInfoClassLoader is findClass(), which is implemented as follows:

  public Class findClass( String className ) throws ClassNotFoundException
  {    
    // get the bytes for a class from the class table
    byte[] b = null;
    
    try
    {
      b = fClassTable.getClassBytes(className);
    }
    catch(JarInfoException jie) {}
    
    // and if there are some, use the inherited method defineClass
    // to convert these bytes into an instance of a Class
    if( b != null )
      return defineClass(className, b, 0, b.length);
      
    // otherwise if there is no such class      
    return null;      
  }  

findClass() also participates in the ClassLoader delegation model, and will only be called if all efforts by the Java Virtual Machine to find the class in question have failed. Putting this all together, you can now use JarInfoClassLoader to load classes from an archive that does not appear in the application's CLASSPATH or Java extension directory.

Using JarInfoClassLoader

The test harness, TestApp, demonstrates how to use JarInfoClassLoader:

  // create a jar class loader to read from kJARNAME
  JarInfoClassLoader jcl = new JarInfoClassLoader(kJARNAME);        
   
  // and load a class from that class loader
  Class c = jcl.loadClass("com.ack.jar.TestApp");    
    
  // display that class
  System.out.println( c );

Note that this version of JarInfoClassLoader's constructor defaults to using the primordial ClassLoader as the parent ClassLoader.

Conclusion

You have seen how the contents of archive files are simple to access from within Java applications. Using these classes, you can now create your own filters for selecting resources within jar files based on size, creation date, image type, and so on. You can also build your own archive caches for images and sounds that make use of JarInfo just as JarClassTable did. Finally, you can extend JarInfoClassLoader to handle multiple jar files and accept gar URLs (look in the java.util.jar package).

Cleveland Gibbon currently works for a large investment bank in its enterprise application infrastructure group. Previously, he worked as an independent contractor, mentoring organizations in object technology, C++, Java, and Internet development. His PhD focused on defining and evaluating object-oriented design heuristics for educating developers in common problems and solutions for software design. Cleveland has programmed in Java since 1995, and focuses on enterprise application integration using such key technologies as EJB, COM, CORBA, and XML. You can find out more about him and his Java work at http://www.acknowledge.co.uk.

Learn more about this topic

Related: