ORIGINAL DRAFT
XML documents often need to be bundled in groups, either to facilitate delivery or to logically cluster them for use by end-users. Java provides an excellent interface to ZIP format files that present themselves as ideal containers for groups of files. This month we’ll be developing a resource manager that allows us to access different resource types within arbitrary containers. We’ll implement a default container based on the ZIP file format, but the infrastructure is designed for flexibility, so you can implement your own if you like.
To demonstrate different resource utilization, we’ll define something called a ResourceLoader, which can take responsibility for turning a stream, found in a ResourceContainer, into a suitable Object. Because we’re primarily interested in XML resources, we’ll implement a couple of resource loaders to handle both DOM and JDOM objects. We’ll also build an image resource loader to show how easy it is to use this mechanism for other kinds of resources.
Our design relies on two interfaces that help uncouple the mechanics of our infrastructure from the objects being manipulated. We define a ResourceContainer like as:
public interface ResourceContainer
{
public InputStream getInputStream(
String name) throws IOException;
}
The getInputStream method returns a resource stream by name. The naming convention is left up to the container. In the case of our default implementation, the ZipResourceContainer, the name is a file path within the ZIP container. The calling program is expected to close the stream.
The second interface we’ll implement makes it possible to handle different kinds of resources in a unified way, leaving most of the work to the underlying infrastructure. The ResourceLoader interface looks like this:
public interface ResourceLoader
{
public Class type();
public boolean cache();
public Object load(InputStream input)
throws ResourceException;
}
Note that a resource loader defines the data type it gets associated with by using the type method. The cache method defines whether the resource is cacheable. In some cases, you’ll want caching to keep accessed objects in memory to avoid excess loading time. You can prime the cache by loading resource you don’t need until later if you like. In cases where the resource is likely to be large and you want to conserve memory, you can use this mechanism to make sure it gets reloaded each time it’s needed.
The ResourceException extends Exception and uses the Java 1.4 chained exception mechanism to report underlying problems. If you need to work with older Java versions you’ll have to change the constructor, but this implementation should be otherwise compatible with any Java version.
As you can see in Figure 1, we have a ZipResourceContainer implementation of the ResourceContainer interface and three implementations for the ResourceLoader interface, dealing with Image, DOM and JDOM resources. We’ll take a closer look at the ResourceManager, given its central role in our solution, and briefly glance at both the DOM and JDOM resource loaders to show how easy it is to implement your own resource loaders.
ResourceManager class (Listing 1) has two constructors. The first is a utility constructor provided for convenience when using the default implementations from this article. It uses the ZipResourceContainer and expects a single File argument, then registers each of the ResourceLoader types we implement. The second constructor is intended to be more flexible, using a ResourceContainer argument to set the container, making no assumptions about any registered resource loaders.
The addResourceLoader method lets you add ResourceLoader instances. We use a Map object (a HashMap instance), to associate the Class type associated with a loader to the loader instance. Because of the way a HashMap works, this means that registering a ResourceLoader instance repeatedly or registering one with an already registered type will have no ill effect other than replacing the previous entry in the table. The getResourceLoader method is used to fetch a ResouceLoader by name. If no entry exists in the table, the method returns a null value.
The loadResource method is key to the infrastructure and actually uses the ResourceLoader to fetch an object. The container reference is used to fetch the necessary input stream by name. We catch exceptions generically and throw a ResourceException if anything goes wrong. Note that we also make sure the input stream is closed before returning the object.
The getResource method is key to using the ResourceManager to get objects from a resource container. It can return any object and handles caching when appropriate. We first fetch the resource loader by type and then check the cache and return the object if it’s already been saved. Otherwise, we load the resource and cache it if the loader specifies caching is permitted.
The rest of the methods are there for convenience and make assumptions about the resource loaders available. If you want to handle additional types, the process primarily involves adding ResourceLoader instances. You can optionally extend the ResourceManager to add convenience methods like this if you like.
Listing 2 shows the DOMResourceLoader class. We use the JAXP (Java API for XML Processing) interface that’s now a standard part of Java 1.4. For older versions, you’ll need to get the JAXP jar file from Sun and a suitable XML parser, such as Xerces. If you use Java 1.4 the code requires no additional libraries.
The DOMResourceLoader creates a reference to a DocumentBuilder object by working with the DocumentBuilderFactory. We’ll use this in the load method. The type method returns a Document object. Fully qualified, this is actually the org.w3c.xml.Document type because we include the package above. You’ll see that the Document type in the JDOMResourceLoader looks the same but actually references a different class. The cache method returns false to avoid caching DOM objects. It’s up to you to change this if you disagree with the policy.
The load method does most of the work by using the builder reference created in the constructor to retrieve a DOM instance from an input stream. We don’t close the input stream here because the ResourceManager handles that for us. All we need to do is handle potential problems; in this case by catching exceptions and returning them with an explanation. Notice that this pattern of catching exceptions and cascading the stack trace information into a more informative exception instance is what chained exceptions were designed for, thus helping programmers report problems at suitable abstraction levels.
The JDOMResourceLoader (Listing 3) is similar, but uses the JDOM library instead of a standard DOM, primarily for convenience. JDOM specifies a DOMBuilder to build Document instances, so we create an instance in the constructor and reference it in the load method. Otherwise, the rest of the code is virtually identical to the DOMResourceLoader, though using a different Document class (as pointed out earlier).
Accessing XML documents from different sources is already possible using existing infrastructures, but the notion of collecting resources of different types in container files is also common requirement. Given how easy it is to work with ZIP files in Java, this article is meant to demonstrate both a simple solution that accommodates XML, as well as other objects, and represents a good use of interfaces to maximize design flexibility. I hope these ideas serve you well.
Listing 1
import java.io.*;
import java.awt.*;
import java.util.*;
public class ResourceManager
{
protected ResourceContainer container;
protected Map loaders = new HashMap();
protected Map cache = new HashMap();
public ResourceManager(File zipFile)
throws ResourceException
{
try
{
container = new ZipResourceContainer(zipFile);
addResourceLoader(new ImageResourceLoader());
addResourceLoader(new DOMResourceLoader());
addResourceLoader(new JDOMResourceLoader());
}
catch (Exception e)
{
throw new ResourceException(
"Unable to initialize default resource manager", e);
}
}
public ResourceManager(ResourceContainer container)
{
this.container = container;
}
public void addResourceLoader(ResourceLoader loader)
{
loaders.put(loader.type(), loader);
}
public ResourceLoader getResourceLoader(Class type)
{
return (ResourceLoader)loaders.get(type);
}
protected Object loadResource(
ResourceLoader loader, Class type, String name)
throws ResourceException
{
try
{
InputStream input = container.getInputStream(name);
Object resource = loader.load(input);
input.close();
return resource;
}
catch (Exception e)
{
throw new ResourceException(
"Unable to load '" + name + "' resource", e);
}
}
public Object getResource(Class type, String name)
throws ResourceException
{
ResourceLoader loader = getResourceLoader(type);
if (cache.containsKey(name))
{
return cache.get(name);
}
else
{
Object resource = loadResource(loader, type, name);
if (loader.cache())
{
cache.put(name, resource);
}
return resource;
}
}
public Image getImage(String name)
throws ResourceException
{
return (Image)getResource(Image.class, name);
}
public org.w3c.dom.Document getDOM(String name)
throws ResourceException
{
return (org.w3c.dom.Document)getResource(
org.w3c.dom.Document.class, name);
}
public org.jdom.Document getJDOM(String name)
throws ResourceException
{
return (org.jdom.Document)getResource(
org.jdom.Document.class, name);
}
}
Listing 2
import java.io.*;
import javax.xml.parsers.*;
import org.w3c.dom.*;
public class DOMResourceLoader
implements ResourceLoader
{
protected DocumentBuilder builder;
public DOMResourceLoader()
throws ParserConfigurationException
{
DocumentBuilderFactory factory =
DocumentBuilderFactory.newInstance();
builder = factory.newDocumentBuilder();
}
public Class type()
{
return Document.class;
}
public boolean cache()
{
return false;
}
public Object load(InputStream input)
throws ResourceException
{
try
{
return builder.parse(input);
}
catch (Exception e)
{
throw new ResourceException(
"Unable to load XML Document resource", e);
}
}
}
Listing 3
import java.io.*;
import org.jdom.*;
import org.jdom.input.*;
public class JDOMResourceLoader
implements ResourceLoader
{
protected DOMBuilder builder;
public JDOMResourceLoader()
{
builder = new DOMBuilder();
}
public Class type()
{
return Document.class;
}
public boolean cache()
{
return false;
}
public Object load(InputStream input)
throws ResourceException
{
try
{
return builder.build(input);
}
catch (Exception e)
{
throw new ResourceException(
"Unable to load XML Document resource", e);
}
}
}