Multihandler adapters

The callback interfaces in SAX are reminiscent of the callback interfaces in the AWT like ActionListener and MouseListener. However, there’s one crucial difference in the patterns followed by the SAX callback interfaces and the AWT callback interfaces. The AWT allows you to register multiple listener objects with any one component, whereas SAX limits you to just one listener of a certain type per parser. Filters allow you to parse a document through several listeners in turn. However, sometimes you want to install multiple handlers which are not filters.

For example, the DocBook XML source for the book you’re reading now is parsed multiple times to produce HTML, XHTML, PDF, and several other output formats. Each time a new output format is required, the parser has to reparse the document. However this is really wasted effort. It’s the same document. If it’s well-formed and valid for HTML output, then it’s well-formed and valid for PDF and XHTML output. There’s no need to run the checks anew for each separate output document as long as the input is the same. It makes sense to attach multiple handlers to the parser to avoid wasting time by unnecessarily reparsing the same document.

A properly designed filter can dispatch its events to multiple handlers. The trick is that rather than storing a single ContentHandler/DTDHandler/ErrorHandler the filter stores a list of each. Then each callback method iterates through one of the lists, invoking the method on each of the registered listeners in turn.

When you change the architecture of a class in a subclass as I’m doing here, the naming conventions of the superclass class often don’t make perfect sense in the subclass. In this case, it’s not clear what the various setFooHandler() methods should do. Do they add a new handler to the list? If so what happens when null is passed (the idiom for removing a handler from an XMLReader)? Or should we provide new addFooHandler() and removeFooHandler() methods? And which object from the list should getFooHandler() return? There’s no obvious answer to these questions. Indeed, I changed my mind several times while designing this class and writing this chapter. Here, I chose to keep the interface as similar to the superclass’s as possible. Thus the three setFooHandler() methods add a new handler without replacing the existing handler. However, if null is passed, the entire list is cleared. There’s no easy way to remove a handler or retrieve or delete a specific one from the list. The getFooHandler() method simply returns the first handler in the list. This is not the most flexible design, but it does keep the interface the same as the interface of the superclass. For example, this is how content handlers are stored, added, and removed:

  private List contentHandlers = new ArrayList(2);
    
  public void setContentHandler(ContentHandler handler) {
    
    if (handler == null) {
      contentHandlers.clear(); 
    }
    contentHandlers.add(handler); 
    
  }

  public ContentHandler getContentHandler() {
    if (contentHandlers.isEmpty()) return null;
    return (ContentHandler) contentHandlers.get(0);
  }

However, other approaches are certainly workable.

However the list is filled, the startElement() method would iterate through the list like this:

  public void startElement(String namespaceURI, String localName,
   String qualifiedName, Attributes atts) throws SAXException {    
   
    Iterator handlers = contentHandlers.iterator();
    while(handlers.hasNext()) {
      ContentHandler handler = (ContentHandler) handlers.next();
      handler.startElement(namespaceURI, localName, 
       qualifiedName, atts);
    }
    
  }

The other handlers and methods are implemented similarly. Example 8.15 shows the complete class.

Example 8.15. Attaching multiple handlers of the same type to a single parser

import org.xml.sax.*;
import org.xml.sax.helpers.XMLFilterImpl;
import java.util.*;


public class MultiHandlerAdapter extends XMLFilterImpl {

  public MultiHandlerAdapter(XMLReader parent) {
    super(parent);
  }

  List contentHandlers = new ArrayList(2);
  List dtdHandlers = new ArrayList(2);
  List errorHandlers = new ArrayList(2);
  
  public void setContentHandler(ContentHandler handler) {
    
    if (handler == null) {
      contentHandlers.clear(); 
    }
    contentHandlers.add(handler); 
    
  }

  // There's no good way to handle this within the XMLReader
  // interface. I just pick the first in the list. 
  public ContentHandler getContentHandler() {
    if (contentHandlers.isEmpty()) return null;
    return (ContentHandler) contentHandlers.get(0);
  }
  
  public void setDTDHandler(DTDHandler handler) {
    
    if (handler == null) {
      dtdHandlers.clear(); 
    }
    dtdHandlers.add(handler); 
    
  }

  public DTDHandler getDTDHandler() {
    if (dtdHandlers.isEmpty()) return null;
    return (DTDHandler) dtdHandlers.get(0);
  }
  
  public void setErrorHandler(ErrorHandler handler) {
    
    if (handler == null) {
      errorHandlers.clear(); 
    }
    errorHandlers.add(handler); 
    
  }

  public ErrorHandler getErrorHandler() {
    if (errorHandlers.isEmpty()) return null;
    return (ErrorHandler) errorHandlers.get(0);
  }
  
  // ContentHandler implementation
  public void startElement(String namespaceURI, String localName,
   String qualifiedName, Attributes atts) throws SAXException {    
   
    Iterator handlers = contentHandlers.iterator();
    while(handlers.hasNext()) {
      ContentHandler handler = (ContentHandler) handlers.next();
      handler.startElement(namespaceURI, localName, 
       qualifiedName, atts);
    }
    
  }  

  public void endElement(String namespaceURI, String localName,
   String qualifiedName) throws SAXException {    
   
    Iterator handlers = contentHandlers.iterator();
    while(handlers.hasNext()) {
      ContentHandler handler = (ContentHandler) handlers.next();
      handler.endElement(namespaceURI, localName, 
       qualifiedName);
    }
    
  }  

  public void characters(char[] text, int start, int length)
   throws SAXException {
   
    Iterator handlers = contentHandlers.iterator();
    while(handlers.hasNext()) {
      ContentHandler handler = (ContentHandler) handlers.next();
      handler.characters(text, start, length);
    }
    
  }
  
  public void ignorableWhitespace(char[] text, int start, 
   int length) throws SAXException {
   
    Iterator handlers = contentHandlers.iterator();
    while(handlers.hasNext()) {
      ContentHandler handler = (ContentHandler) handlers.next();
      handler.ignorableWhitespace(text, start, length); 
    }
  }
  
  public void processingInstruction(String target, String data)
   throws SAXException {
   
    Iterator handlers = contentHandlers.iterator();
    while(handlers.hasNext()) {
      ContentHandler handler = (ContentHandler) handlers.next();
      handler.processingInstruction(target, data); 
    }
  }
  
  public void skippedEntity(String name)
   throws SAXException {
   
    Iterator handlers = contentHandlers.iterator();
    while(handlers.hasNext()) {
      ContentHandler handler = (ContentHandler) handlers.next();
      handler.skippedEntity(name); 
    }
  }

  public void startPrefixMapping(String prefix, String uri)
   throws SAXException {
   
    Iterator handlers = contentHandlers.iterator();
    while(handlers.hasNext()) {
      ContentHandler handler = (ContentHandler) handlers.next();
      handler.startPrefixMapping(prefix, uri); 
    }
    
  }
  
  public void endPrefixMapping(String prefix) 
   throws SAXException {
     
    Iterator handlers = contentHandlers.iterator();
    while(handlers.hasNext()) {
      ContentHandler handler = (ContentHandler) handlers.next();
      handler.endPrefixMapping(prefix); 
    }
    
  }

  public void startDocument() throws SAXException {

    Iterator handlers = contentHandlers.iterator();
    while(handlers.hasNext()) {
      ContentHandler handler = (ContentHandler) handlers.next();
      handler.startDocument();
    }
    
  }

  public void endDocument() throws SAXException {
    
    Iterator handlers = contentHandlers.iterator();
    while(handlers.hasNext()) {
      ContentHandler handler = (ContentHandler) handlers.next();
      handler.endDocument();
    }
    
  }
    
  public void setDocumentLocator(Locator locator) {
    
    Iterator handlers = contentHandlers.iterator();
    while(handlers.hasNext()) {
      ContentHandler handler = (ContentHandler) handlers.next();
      handler.setDocumentLocator(locator); 
    }
    
  }  

  // DTDHandler implementation
  public void notationDecl(String name, String publicID, 
   String systemID) throws SAXException {
     
    Iterator handlers = dtdHandlers.iterator();
    while(handlers.hasNext()) {
      DTDHandler handler = (DTDHandler) handlers.next();
      handler.notationDecl(name, publicID, systemID); 
    }
    
  }
   
  public void unparsedEntityDecl(String name, String publicID, 
   String systemID, String notationName) throws SAXException {
     
    Iterator handlers = dtdHandlers.iterator();
    while(handlers.hasNext()) {
      DTDHandler handler = (DTDHandler) handlers.next();
      handler.unparsedEntityDecl(name, publicID, systemID, 
       notationName); 
    }
      
  }
  
  // ErrorHandler implementation
  public void warning(SAXParseException exception)
   throws SAXException {
     
    Iterator handlers = errorHandlers.iterator();
    while(handlers.hasNext()) {
      ErrorHandler handler = (ErrorHandler) handlers.next();
      handler.warning(exception); 
    }
          
  }
  
  public void error(SAXParseException exception)
   throws SAXException {
     
    Iterator handlers = errorHandlers.iterator();
    while(handlers.hasNext()) {
      ErrorHandler handler = (ErrorHandler) handlers.next();
      handler.error(exception); 
    }
          
  }
  
  public void fatalError(SAXParseException exception)
   throws SAXException {
     
    Iterator handlers = errorHandlers.iterator();
    while(handlers.hasNext()) {
      ErrorHandler handler = (ErrorHandler) handlers.next();
      handler.fatalError(exception); 
    }
          
  }
  
}

Note

MultiHandlerAdapter is a little on the long side. It contains a lot of duplicated code. This is definitely a case where some space could be saved by using templates.

MultiHandlerAdapter only provides the three main callback interfaces: ContentHandler, DTDHandler, and ErrorHandler. I chose not to provide EntityResolver because having more than one of those responding to the same request doesn’t make sense. Each entity must be provided exactly once, not two or three or seventeen times. Potentially, you could register multiple entity resolvers and then iterate through them in sequence until one found the entity you were looking for. However, since order would be very significant in that case, a proper API is much trickier to design.

I also chose not to implement the property based handlers, LexicalHandler and DeclHandler. First, not all parsers support them. Secondly, they are not part of the standard XMLFilterImpl class. Most importantly, adding and removing these objects solely by setting and getting features and properties would be very messy. If you need multiples of one of them, it seems simpler to implement the multi-way dispatching in a LexicalHandler/DeclHandler implementation rather than directly in the filter. A similar scheme could work for ContentHandler, DTDHandler, and ErrorHandler. However, here the filter approach seems the most natural.


Copyright 2001, 2002 Elliotte Rusty Haroldelharo@metalab.unc.eduLast Modified December 02, 2001
Up To Cafe con Leche