package li.earth.urchin.twic.xml; import java.util.List; import java.util.ArrayList; import java.util.Set; import java.util.LinkedHashSet; import java.util.Map; import java.util.LinkedHashMap; import java.util.Collections; import java.util.Arrays; import java.io.InputStream; import java.io.Reader; import java.io.IOException; import org.xml.sax.ContentHandler; import org.xml.sax.Attributes; import org.xml.sax.XMLReader; import org.xml.sax.Locator; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.helpers.XMLReaderFactory; public class XMLEventGenerator implements ContentHandler { private static final boolean DEFAULT_TRIM_LEADING_WHITESPACE = true; private final XMLReader parser; private final boolean trimLeadingWhitespace; private final Set listeners; private List path; private List unmodifiablePath; private Map attrs; private StringBuilder sb; private boolean elementContent; public XMLEventGenerator(boolean trimLeadingWhitespace) throws SAXException { parser = XMLReaderFactory.createXMLReader(); // TODO: set options to ignore namespaces or whatever, and/or expose an interface to set parser options parser.setContentHandler(this); this.trimLeadingWhitespace = trimLeadingWhitespace; listeners = new LinkedHashSet(); } public XMLEventGenerator() throws SAXException { this(DEFAULT_TRIM_LEADING_WHITESPACE); } public XMLEventGenerator(boolean trimLeadingWhitespace, XMLListener... listeners) throws SAXException { this(trimLeadingWhitespace); this.listeners.addAll(Arrays.asList(listeners)); } public XMLEventGenerator(XMLListener... listeners) throws SAXException { this(DEFAULT_TRIM_LEADING_WHITESPACE, listeners); } public void addListener(XMLListener listener) { listeners.add(listener); } public void removeListener(XMLListener listener) { listeners.remove(listener); } public void parse(InputSource in) throws IOException, SAXException { parser.parse(in); } public void parse(Reader in) throws IOException, SAXException { parse(new InputSource(in)); } public void parse(InputStream in) throws IOException, SAXException { parse(new InputSource(in)); } public void parse(String uri) throws IOException, SAXException { parse(new InputSource(uri)); } public void startDocument() throws SAXException { if (path != null) throw new SAXException("startDocument called when a document is already started"); path = new ArrayList(); unmodifiablePath = Collections.unmodifiableList(path); sb = new StringBuilder(); fireStartDocument(); } public void endDocument() throws SAXException { if (path == null) throw new SAXException("endDocument called when a document has not been started"); if (!path.isEmpty()) throw new SAXException("endDocument called when elements have not been ended"); path = null; unmodifiablePath = null; sb = null; fireEndDocument(); } public void startElement(String uri, String localname, String qname, Attributes attrs) throws SAXException { if (sb.length() != 0) throw new SAXException(new IllegalStateException("cannot start an element when there is already character data " + path)); assert attrs != null; if ((!path.isEmpty()) && (this.attrs != null)) fireStartContainer(); path.add(qname); this.attrs = toMap(attrs); elementContent = false; } private Map toMap(Attributes attrs) { Map map = new LinkedHashMap(attrs.getLength()); for (int i = 0; i < attrs.getLength(); ++i) { map.put(attrs.getQName(i), attrs.getValue(i)); } return map; } public void characters(char[] buf, int off, int len) throws SAXException { if (trimLeadingWhitespace && (sb.length() == 0)) { int end; for (end = off + len; Character.isWhitespace(buf[off]) && (off < end); ++off); len = end - off; } if (len == 0) return; if (elementContent) throw new SAXException(new IllegalStateException("cannot add character data when there is already element item")); sb.append(buf, off, len); } public void endElement(String uri, String localname, String qname) throws SAXException { if (elementContent) { fireEndContainer(); } else { fireContent(); sb.setLength(0); } path.remove(path.size() - 1); attrs = null; elementContent = true; } private void fireStartDocument() throws SAXException { for (XMLListener listener: listeners) { try { listener.startDocument(); } catch (RuntimeException e) { throw new SAXException("a listener threw a runtime exception", e); } } } private void fireEndDocument() throws SAXException { for (XMLListener listener: listeners) { try { listener.endDocument(); } catch (RuntimeException e) { throw new SAXException("a listener threw a runtime exception", e); } } } private void fireStartContainer() throws SAXException { assert attrs != null; for (XMLListener listener: listeners) { try { listener.startContainer(unmodifiablePath, attrs); } catch (RuntimeException e) { throw new SAXException("a listener threw a runtime exception", e); } } } private void fireContent() throws SAXException { String text = sb.toString(); assert attrs != null; assert text != null; for (XMLListener listener: listeners) { try { listener.item(unmodifiablePath, attrs, text); } catch (RuntimeException e) { throw new SAXException("a listener threw a runtime exception", e); } } } private void fireEndContainer() throws SAXException { for (XMLListener listener: listeners) { try { listener.endContainer(unmodifiablePath); } catch (RuntimeException e) { throw new SAXException("a listener threw a runtime exception", e); } } } public void setDocumentLocator(Locator loc) {} public void startPrefixMapping(String prefix, String uri) {} public void endPrefixMapping(String prefix) throws SAXException {} public void ignorableWhitespace(char[] buf, int off, int len) {} public void processingInstruction(String target, String data) {} public void skippedEntity(String name) {} }