/** * Copyright 2011 meltmedia * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.xchain.framework.sax; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xml.sax.Attributes; import org.xml.sax.ContentHandler; import org.xml.sax.DTDHandler; import org.xml.sax.Locator; import org.xml.sax.SAXException; import org.xml.sax.ext.LexicalHandler; import org.xml.sax.helpers.AttributesImpl; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.LinkedList; import java.util.LinkedHashMap; import java.util.Map; import java.util.Iterator; import java.util.Set; import javax.xml.namespace.QName; import org.xchain.framework.sax.util.NamespaceContext; /** * This handler provides caching and translation services for command based sax sources. The following features are implemented: * * 1) Attributes can be assigned after a startElement event by calling attribute( String namespace, String localName, String qName ); * 2) The handler can be set into comment mode, which will translate all calls into comments. * 3) The handler can be flushed, to force all outstanding events to the content handler. This will be a partial flush when in comment mode, since * comments are passed in one call, instead of many. * Modes: * 1) Standard mode. * 2) Element building mode. * 2.1) Attribute building mode. * 3) Comment building mode. * * Element building is terminated by: * 1) A text event (characters or ignorable whitespace.) * 2) Another start element event. * 3) A processing instruction. * 4) An end element event. * * Attribute building is terminated by: * 1) An end attribute event. * * Comment building is terminated by: * 1) an end comment event. * * @author Mike Moulton * @author Devon Tackett * @author Christian Trimble * @author Jason Rose * @author Josh Kennedy */ public abstract class CommandHandler implements ContentHandler, DTDHandler, LexicalHandler { public static Logger log = LoggerFactory.getLogger(CommandHandler.class); /** The buffer used to redirect character events. */ protected StringBuilder redirectBuilder = new StringBuilder(); /** The depth of redirection elements. */ protected int redirectDepth = 0; /** The next element that will be output. */ protected Element nextElement = null; /** The namespace context for this handler. */ protected PrefixMappingContext inputNamespaceContext = new PrefixMappingContext(); /** The namespace context for the output handler. */ protected PrefixMappingContext outputNamespaceContext = new PrefixMappingContext(); /** The exclude result prefix context. */ protected LinkedList<PrefixMappingContext> excludeNamespaceContextStack = new LinkedList<PrefixMappingContext>(); /** The stack of elements from the root element to the current output element. */ protected LinkedList<Element> elementStack = new LinkedList<Element>(); /** The map of prefix mappings for the next startElement event. */ protected HashMap<String, String> nextPrefixMapping = new HashMap<String, String>(); public abstract ContentHandler contentHandler(); public abstract DTDHandler dtdHandler(); public abstract LexicalHandler lexicalHandler(); public void setDocumentLocator(Locator locator) { contentHandler().setDocumentLocator(locator); } public void startDocument() throws SAXException { excludeNamespaceContextStack.addFirst(new PrefixMappingContext()); contentHandler().startDocument(); } public void endDocument() throws SAXException { contentHandler().endDocument(); excludeNamespaceContextStack.removeFirst(); } public void startPrefixMapping(String prefix, String uri) throws SAXException { nextPrefixMapping.put(prefix, uri); } public void endPrefixMapping(String prefix) throws SAXException { inputNamespaceContext.endPrefixMapping(prefix); } public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException { if( redirectCharacters() ) { redirectBuilder.append(ch, start, length); } else { contentHandler().ignorableWhitespace(ch, start, length); } } public void processingInstruction(String target, String data) throws SAXException { if( !redirectCharacters() ) { // pass the processing instruction through. contentHandler().processingInstruction(target, data); } } public void skippedEntity(String name) throws SAXException { if( !redirectCharacters() ) { contentHandler().skippedEntity(name); } } public void notationDecl(String name, String publicId, String systemId) throws SAXException { if( !redirectCharacters() ) { fireOutstandingEvents(); dtdHandler().notationDecl(name, publicId, systemId); } } public void unparsedEntityDecl(String name, String publicId, String systemId, String notationName) throws SAXException { if( !redirectCharacters() ) { fireOutstandingEvents(); dtdHandler().unparsedEntityDecl(name, publicId, systemId, notationName); } } /** * Sends text to the content handler. This will flush any outstanding events and then send * a characters(char[], int, int) event to the content handler. */ public void characters(char[] ch, int offset, int length ) throws SAXException { if( redirectCharacters() ) { redirectBuilder.append(ch, offset, length); } else { // fire any events that are waiting. fireOutstandingEvents(); // send the characters through. contentHandler().characters(ch, 0, length); } } /** * Sets up a start element event that will be sent to the content handler. When the simple handler detects that the * creation of an element is complete, then start prefix mapping events and a start element event will be fired. */ public void startElement(String namespace, String localName, String qName, Attributes attributes) throws SAXException { if( !redirectCharacters() ) { fireOutstandingEvents(); // CHANGE: push the next element. // add the prefix mappings that need to be added to this element. // clear the current prefix mappings. // update the input namespace context. for( Map.Entry<String, String> prefixMapping : nextPrefixMapping.entrySet() ) { inputNamespaceContext.startPrefixMapping(prefixMapping.getKey(), prefixMapping.getValue()); } nextElement = new Element(namespace, localName, qName); // copy all of the prefix mappings that were found for this element. nextElement.getPrefixMappings().putAll(nextPrefixMapping); nextPrefixMapping.clear(); for( int i = 0; i < attributes.getLength(); i++ ) { nextElement.addAttribute( attributes.getURI(i), attributes.getLocalName(i), attributes.getQName(i), attributes.getValue(i) ); } elementStack.addFirst(nextElement); } } /** * Sends the end element event to the content handler. */ public void endElement(String namespace, String localName, String qName) throws SAXException { if( !redirectCharacters() ) { // fires outstanding events. fireOutstandingEvents(); // remove the first element from the stack. Element element = elementStack.removeFirst(); // make sure that this is the correct element. // TODO: add sanity checks. // end the element. contentHandler().endElement(element.getNamespace(), element.getLocalName(), element.getQName()); for( String prefix : element.getPrefixMappings().keySet() ) { contentHandler().endPrefixMapping(prefix); outputNamespaceContext.endPrefixMapping(prefix); } } } public void startAttribute( String namespace, String localName, String qName ) throws SAXException { if( !redirectCharacters() ) { startRedirectCharacters(); } redirectDepth++; if( redirectDepth == 1 && nextElement != null ) { // add any missing prefix mappings to the next element. for( Map.Entry<String, String> prefixMapping : nextPrefixMapping.entrySet() ) { String currentMapping = nextElement.getPrefixMappings().get(prefixMapping.getKey()); if( currentMapping == null ) { currentMapping = outputNamespaceContext.lookUpNamespaceUri(prefixMapping.getKey()); } if( currentMapping == null || currentMapping.equals(prefixMapping.getValue()) ) { inputNamespaceContext.startPrefixMapping(prefixMapping.getKey(), prefixMapping.getValue()); elementStack.getFirst().getPrefixMappings().put(prefixMapping.getKey(), prefixMapping.getValue()); } else { throw new SAXException("Cannot map prefix '"+prefixMapping.getKey()+"' to '"+prefixMapping.getValue()+"' because it is already mapped to '"+currentMapping+"'."); } } } else { for( Map.Entry<String, String> prefixMapping : nextPrefixMapping.entrySet() ) { inputNamespaceContext.startPrefixMapping(prefixMapping.getKey(), prefixMapping.getValue()); } } nextPrefixMapping.clear(); } public void endAttribute( String namespace, String localName, String qName ) throws SAXException { redirectDepth--; if( !redirectCharacters() ) { // if there is an element to add this attribute to, then add it. if( nextElement != null ) { nextElement.addAttribute(namespace, localName, qName, redirectBuilder.toString()); } endRedirectCharacters(); } } public void endAttribute( String qName ) throws SAXException { String[] qNameSplit = qName.split(":", 2); String prefix = qNameSplit.length == 2 ? qNameSplit[0] : null; String localName = qNameSplit.length == 2 ? qNameSplit[1] : qNameSplit[0]; endAttribute( inputNamespaceContext.lookUpNamespaceUri(prefix), localName, qName ); } /** * Starts a new context for removing result prefixes. */ public void startExcludeResultPrefixContext() throws SAXException { excludeNamespaceContextStack.addFirst(new PrefixMappingContext()); } /** * Ends the current context for removing result prefixes. */ public void endExcludeResultPrefixContext() throws SAXException { excludeNamespaceContextStack.removeFirst(); } public void startExcludeResultPrefix( String prefix ) throws SAXException { String namespace = null; // try to get the namespace from the next prefix mapping, since this happens before outstanding events // are fired. if( "#all".equals(prefix) ) { namespace = "#all"; } else if( nextPrefixMapping.containsKey(prefix) ) { namespace = nextPrefixMapping.get(prefix); } else { namespace = inputNamespaceContext.lookUpNamespaceUri(prefix); } if( namespace == null ) { throw new SAXException("The prefix '"+prefix+"' was excluded, but is not defined in the namespace context."); } excludeNamespaceContextStack.getFirst().startPrefixMapping(prefix, namespace); } public void endExcludeResultPrefix( String prefix ) throws SAXException { excludeNamespaceContextStack.getFirst().endPrefixMapping(prefix); } public void startDTD(String name, String publicId, String systemId) throws SAXException { if( !redirectCharacters() ) { LexicalHandler lexicalHandler = lexicalHandler(); if( lexicalHandler != null ) { lexicalHandler.startDTD(name, publicId, systemId); } } } public void endDTD() throws SAXException { if( !redirectCharacters() ) { LexicalHandler lexicalHandler = lexicalHandler(); if( lexicalHandler != null ) { lexicalHandler.endDTD(); } } } public void startEntity(String name) throws SAXException { if( !redirectCharacters() ) { LexicalHandler lexicalHandler = lexicalHandler(); if( lexicalHandler != null ) { lexicalHandler.startEntity(name); } } } public void endEntity(String name) throws SAXException { if( !redirectCharacters() ) { LexicalHandler lexicalHandler = lexicalHandler(); if( lexicalHandler != null ) { lexicalHandler.endEntity(name); } } } public void startCDATA() throws SAXException { if( !redirectCharacters() ) { LexicalHandler lexicalHandler = lexicalHandler(); if( lexicalHandler != null ) { lexicalHandler.startCDATA(); } } } public void endCDATA() throws SAXException { if( !redirectCharacters() ) { LexicalHandler lexicalHandler = lexicalHandler(); if( lexicalHandler != null ) { lexicalHandler.endCDATA(); } } } /** * Adds a comment to this handler. If the handler is in comment mode, then an escaped comment will be added to the sax stream, * otherwise a standard comment is passed on. * * @param comment the character array containing the comment. * @param offset the offset into the array to start the characters. * @param length the length of the comment starting at the offset. */ public void comment( char[] comment, int offset, int length ) throws SAXException { if( log.isDebugEnabled() ) { log.debug("comment('"+(new StringBuilder().append(comment, offset, length).toString())+"') called."); } if( !redirectCharacters() ) { // fire any events that are waiting. fireOutstandingEvents(); // get the lexical handler. LexicalHandler lexicalHandler = lexicalHandler(); // only send the event if the lexical handler is defined. if( lexicalHandler != null ) { lexicalHandler.comment(comment, offset, length); } } } /** * Adds a comment to this handler. This method turns the comment string into a character array then calls comment(char[], int, int). * * @param comment the text of the comment. */ public void comment( String comment ) throws SAXException { if( comment != null ) { char[] commentArray = comment.toCharArray(); comment(commentArray, 0, commentArray.length); } } /** * Starts a comment section. While in a comment section, all events are translated into a comment, to be passed on once the * comment section ends. */ public void startComment() throws SAXException { if( !redirectCharacters() ) { // flush any outstanding events. fireOutstandingEvents(); // start the redirection of characters. startRedirectCharacters(); } // increment the depth. redirectDepth++; } /** * Stops translating all events as a single comment and passes the comment along. */ public void endComment() throws SAXException { redirectDepth--; if( !redirectCharacters() ) { char[] comment = redirectBuilder.toString().toCharArray(); LexicalHandler lexicalHandler = lexicalHandler(); if( lexicalHandler != null ) { lexicalHandler.comment(comment, 0, comment.length); } endRedirectCharacters(); } } private boolean redirectCharacters() { return redirectDepth > 0; } private void startRedirectCharacters() { redirectBuilder.delete(0, redirectBuilder.length()); } private void endRedirectCharacters() { redirectBuilder.delete(0, redirectBuilder.length()); } /** * Fires any element events that have been cached by this content handler. */ private void fireOutstandingEvents() throws SAXException { if( log.isDebugEnabled() ) { log.debug("fireOutstandingEvents() called."); } // if there is a next element. if( nextElement != null ) { // the set of all of the namespaces that are required by this element. Map<String, String> requiredPrefixMappings = new HashMap<String, String>(); requiredPrefixMappings.put(parsePrefix(nextElement.getQName()), nextElement.getNamespace()); // create the set of attributes for this element. AttributesImpl attributes = new AttributesImpl(); Iterator<Attribute> attributeIterator = nextElement.getAttributeMap().values().iterator(); while( attributeIterator.hasNext() ) { Attribute attribute = (Attribute)attributeIterator.next(); attributes.addAttribute(attribute.getNamespace(), attribute.getLocalName(), attribute.getQName(), "CDATA", attribute.getValue()); if( attribute.getNamespace() != null && !"".equals(attribute.getNamespace()) ) { requiredPrefixMappings.put(parsePrefix(attribute.getQName()), attribute.getNamespace()); } } // ASSERT: the attributes for this element are all constructed. // ASSERT: we know all of the required prefix mappings for this element. // ASSERT: the requiredPrefixMappings is a subset of nextElement.getPrefixMappings() // remove the required mappings from the nextElement, we will put them back when the trimming of the prefixes is done. nextElement.getPrefixMappings().keySet().removeAll(requiredPrefixMappings.keySet()); // remove all of the mappings that will have no effect on the output document. removeUnneededMappings(outputNamespaceContext, nextElement.getPrefixMappings()); removeUnneededMappings(outputNamespaceContext, requiredPrefixMappings); // remove all excluded prefix mappings from the namespace mappings left in nextElement.getPrefixMappings(). These are the namespaces that are // not required right now. if( !excludeNamespaceContextStack.isEmpty() ) { PrefixMappingContext excludeResultContext = excludeNamespaceContextStack.getFirst(); if( excludeResultContext.contains("#all", "#all") ) { nextElement.getPrefixMappings().clear(); } else { Iterator<Map.Entry<String, String>> mappingIterator = nextElement.getPrefixMappings().entrySet().iterator(); while( mappingIterator.hasNext() ) { Map.Entry<String, String> entry = mappingIterator.next(); if( excludeResultContext.contains(entry.getKey(), entry.getValue()) ) { mappingIterator.remove(); } } } } // add all of the required prefix mappings back. nextElement.getPrefixMappings().putAll(requiredPrefixMappings); // ASSERT: all prefixes that must be output by this element are now in nextElement.getPrefixMappings(). // output all of the namespace prefixes. for( Map.Entry<String, String> prefixMapping : nextElement.getPrefixMappings().entrySet() ) { contentHandler().startPrefixMapping(prefixMapping.getKey(), prefixMapping.getValue()); outputNamespaceContext.startPrefixMapping(prefixMapping.getKey(), prefixMapping.getValue()); } // ASSERT: the prefix mappings are now all defined for the context of this element. // fire the start event for the element. contentHandler().startElement(nextElement.getNamespace(), nextElement.getLocalName(), nextElement.getQName(), attributes ); // ASSERT: the start element has been fired for this element. if( log.isDebugEnabled() ) { log.debug("Adding element '"+nextElement.getQName()+"' to the node stack."); } // clear the next element. nextElement = null; } } /** * Removes mappings from the map prefixMappings that are already defined in prefixMappingContext. * * @param prefixMappingContext the prefix mapping context that will be checked for mappings. * @param prefixMappings the prefix map that will have unneeded mappings removed. */ private static void removeUnneededMappings( PrefixMappingContext prefixMappingContext, Map<String, String> prefixMappings ) { Iterator<Map.Entry<String, String>> prefixMappingIterator = prefixMappings.entrySet().iterator(); while( prefixMappingIterator.hasNext() ) { Map.Entry<String, String> prefixMappingEntry = prefixMappingIterator.next(); if( prefixMappingContext.contains(prefixMappingEntry.getKey(), prefixMappingEntry.getValue()) ) { prefixMappingIterator.remove(); } } } /** * Parses a prefix from a QName. If the qName does not have a prefix, then "" is returned, otherwise the prefix portion of the QName is returned. * * @param qName the QName to parse. * @return the prefix part of the QName, or the empty string if there is not a prefix in the QName. */ private static String parsePrefix( String qName ) { String qNameSplit[] = qName.split(":", 2); return qNameSplit.length == 2 ? qNameSplit[0] : ""; } public static class Node { protected String qName = null; protected String namespace = null; protected String localName = null; public Node( String namespace, String localName, String qName ) { this.qName = qName; this.namespace = namespace; this.localName = localName; } public String getQName() { return this.qName; } public String getNamespace() { return this.namespace; } public String getLocalName() { return this.localName; } } public static class Element extends Node { protected Map<String, String> prefixMapping = new HashMap<String, String>(); protected Map<QName, Attribute> attributeMap = new LinkedHashMap<QName, Attribute>(); public Element( String namespace, String localName, String qName ) { super(namespace, localName, qName); } public Map<QName, Attribute> getAttributeMap() { return this.attributeMap; } public void addAttribute( String namespace, String localName, String qName, String value ) { attributeMap.put(new QName(namespace, localName), new Attribute(namespace, localName, qName, value)); } public Map<String, String> getPrefixMappings() { return this.prefixMapping; } } public static class Attribute extends Node { protected String value = null; public Attribute( String namespace, String localName, String qName, String value ) { super( namespace, localName, qName ); this.value = value; } public String getValue() { return this.value; } } }