/**
* Copyright (C) 2010 Orbeon, Inc.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU Lesser General Public License as published by the Free Software Foundation; either version
* 2.1 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
*
* The full text of the license is available at http://www.gnu.org/copyleft/lesser.html
*/
package org.orbeon.oxf.xml;
import org.orbeon.dom.Element;
import org.orbeon.oxf.common.OrbeonLocationException;
import org.orbeon.oxf.xml.dom4j.Dom4jUtils;
import org.orbeon.oxf.xml.dom4j.LocationData;
import org.xml.sax.Attributes;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;
import scala.Tuple2;
import java.util.*;
/**
* This is the controller for the handlers system.
*
* The handler controller:
*
* - keeps a list of element handlers
* - reacts to a stream of SAX events
* - calls handlers when needed
* - handles repeated content
*
* TODO: Should use pools of handlers to reduce memory consumption?
*/
public class ElementHandlerController implements ElementHandlerContext, XMLReceiver {
private Object elementHandlerContext;
private DeferredXMLReceiver output;
private final Map<String, List<HandlerMatcher>> handlerMatchers = new HashMap<String, List<HandlerMatcher>>();
private final Map<String, String> uriHandlers = new HashMap<String, String>();
private final List<HandlerMatcher> customMatchers = new ArrayList<HandlerMatcher>();
private final Stack<HandlerInfo> handlerInfos = new Stack<HandlerInfo>();
private HandlerInfo currentHandlerInfo;
private boolean isFillingUpSAXStore;
private final NamespaceContext namespaceContext = new NamespaceContext();
private OutputLocator locator;
private int level = 0;
// Class.forName is expensive, so we cache mappings
private static Map<String, Class<ElementHandler>> classNameToHandlerClass = new HashMap<String, Class<ElementHandler>>();
/**
* Register a handler. The handler can match on a URI + localname + custom matcher, URI + localname, or on URI only
* in that order.
*
* @param handlerClassName class name for the handler
* @param uri URI of the element that triggers the handler
* @param localname local name of the element that triggers the handler, or null if match on URI only
* @param matcher matcher on attributes, or null
*/
public void registerHandler(String handlerClassName, String uri, String localname, Matcher matcher) {
if (localname != null) {
// Match on URI + localname and optionally custom matcher
final String key = XMLUtils.buildExplodedQName(uri, localname);
List<HandlerMatcher> handlerMatchers = this.handlerMatchers.get(key);
if (handlerMatchers == null) {
handlerMatchers = new ArrayList<HandlerMatcher>();
this.handlerMatchers.put(key, handlerMatchers);
}
handlerMatchers.add(new HandlerMatcher(handlerClassName, matcher != null ? matcher : ALL_MATCHER));
} else {
// Match on URI only
uriHandlers.put(uri, handlerClassName);
}
}
public void registerHandler(String handlerClassName, Matcher matcher) {
customMatchers.add(new HandlerMatcher(handlerClassName, matcher));
}
public void setElementHandlerContext(Object elementHandlerContext) {
this.elementHandlerContext = elementHandlerContext;
}
public DeferredXMLReceiver getOutput() {
return output;
}
public void setOutput(DeferredXMLReceiver output) {
this.output = output;
}
public NamespaceContext getNamespaceContext() {
return namespaceContext;
}
public void startDocument() throws SAXException {
try {
output.startDocument();
} catch (Exception e) {
throw OrbeonLocationException.wrapException(e, LocationData.createIfPresent(locator));
}
}
public void endDocument() throws SAXException {
try {
output.endDocument();
} catch (Exception e) {
throw OrbeonLocationException.wrapException(e, LocationData.createIfPresent(locator));
}
}
public void startElement(String uri, String localname, String qName, Attributes attributes) throws SAXException {
try {
// Increment level before, so that if callees like start() and startElement() use us, the level is correct
level++;
namespaceContext.startElement();
if (isFillingUpSAXStore) {
// Fill-up SAXStore
currentHandlerInfo.saxStore.startElement(uri, localname, qName, attributes);
} else if (currentHandlerInfo != null && !currentHandlerInfo.elementHandler.isForwarding()) {
// The current handler doesn't want forwarding
// Just ignore content
} else {
// Look for a new handler
final String explodedQName = XMLUtils.buildExplodedQName(uri, localname);
final HandlerInfo handlerInfo = getHandler(uri, explodedQName, attributes);
if (handlerInfo != null) {
// New handler found
final ElementHandler elementHandler = handlerInfo.elementHandler;
elementHandler.setContext(elementHandlerContext);
// Push current handler
currentHandlerInfo = handlerInfo;
handlerInfos.push(currentHandlerInfo);
if (elementHandler.isRepeating()) {
// Repeating handler will process its body later
isFillingUpSAXStore = true;
} else {
// Non-repeating handler processes its body immediately
// Signal init/start to current handler
elementHandler.init(uri, localname, qName, attributes, handlerInfo.matched);
elementHandler.start(uri, localname, qName, attributes);
}
} else {
// New handler not found, send to output
output.startElement(uri, localname, qName, attributes);
}
}
} catch (Exception e) {
throw OrbeonLocationException.wrapException(e, LocationData.createIfPresent(locator));
}
}
public void endElement(String uri, String localname, String qName) throws SAXException {
try {
if (currentHandlerInfo != null && currentHandlerInfo.level == level) {
// End of current handler
if (isFillingUpSAXStore) {
// Was filling-up SAXStore
isFillingUpSAXStore = false;
// Process body once
currentHandlerInfo.elementHandler.init(uri, localname, qName, currentHandlerInfo.attributes, currentHandlerInfo.matched);
currentHandlerInfo.elementHandler.start(uri, localname, qName, currentHandlerInfo.attributes);
currentHandlerInfo.elementHandler.end(uri, localname, qName);
} else {
// Signal end to current handler
currentHandlerInfo.elementHandler.end(uri, localname, qName);
}
// Pop current handler
handlerInfos.pop();
currentHandlerInfo = ((handlerInfos.size() > 0) ? handlerInfos.peek() : null);
} else if (isFillingUpSAXStore) {
// Fill-up SAXStore
currentHandlerInfo.saxStore.endElement(uri, localname, qName);
} else if (currentHandlerInfo != null && !currentHandlerInfo.elementHandler.isForwarding()) {
// The current handler doesn't want forwarding
// Just ignore content
} else {
// Just forward
output.endElement(uri, localname, qName);
}
namespaceContext.endElement();
level--;
} catch (Exception e) {
throw OrbeonLocationException.wrapException(e, LocationData.createIfPresent(locator));
}
}
/**
* A repeated handler may call this 1 or more times to start handling the captured body.
*
* @throws SAXException
*/
public void repeatBody() throws SAXException {
// Replay content of current SAXStore
final int beforeLocatorCount = (this.locator != null) ? this.locator.size() : 0;
currentHandlerInfo.saxStore.replay(this);
final int afterLocatorCount = (this.locator != null) ? this.locator.size() : 0;
if (beforeLocatorCount != afterLocatorCount) {
// This means that the SAXStore replay called setDocumentLocator()
assert afterLocatorCount == beforeLocatorCount + 1 : "incorrect locator stack state";
this.locator.pop();
}
}
/**
* A handler may call this to start providing new dynamic content to process.
*/
public void startBody() {
// Just push null so that the contents is not subject to the isForwarding() test.
handlerInfos.push(null);
currentHandlerInfo = null;
}
/**
* A handler may call this to end providing new dynamic content to process.
*/
public void endBody() {
handlerInfos.pop();
currentHandlerInfo = ((handlerInfos.size() > 0) ? handlerInfos.peek() : null);
}
public void characters(char[] chars, int start, int length) throws SAXException {
try {
if (isFillingUpSAXStore) {
// Fill-up SAXStore
currentHandlerInfo.saxStore.characters(chars, start, length);
} else if (currentHandlerInfo != null && !currentHandlerInfo.elementHandler.isForwarding()) {
// The current handler doesn't want forwarding
// Just ignore content
} else {
// Send to output
output.characters(chars, start, length);
}
} catch (Exception e) {
throw OrbeonLocationException.wrapException(e, LocationData.createIfPresent(locator));
}
}
public void startPrefixMapping(String prefix, String uri) throws SAXException {
try {
if (isFillingUpSAXStore) {
// Fill-up SAXStore
currentHandlerInfo.saxStore.startPrefixMapping(prefix, uri);
} else if (currentHandlerInfo != null && !currentHandlerInfo.elementHandler.isForwarding()) {
// The current handler doesn't want forwarding
// Just ignore content
} else {
// Update global NamespaceContext
namespaceContext.startPrefixMapping(prefix, uri);
// Send to output
output.startPrefixMapping(prefix, uri);
}
} catch (Exception e) {
throw OrbeonLocationException.wrapException(e, LocationData.createIfPresent(locator));
}
}
public void endPrefixMapping(String s) throws SAXException {
try {
if (isFillingUpSAXStore) {
// Fill-up SAXStore
currentHandlerInfo.saxStore.endPrefixMapping(s);
} else if (currentHandlerInfo != null && !currentHandlerInfo.elementHandler.isForwarding()) {
// The current handler doesn't want forwarding
// Just ignore content
} else {
// Send to output
output.endPrefixMapping(s);
}
} catch (Exception e) {
throw OrbeonLocationException.wrapException(e, LocationData.createIfPresent(locator));
}
}
public void ignorableWhitespace(char ch[], int start, int length) throws SAXException {
try {
if (isFillingUpSAXStore) {
// Fill-up SAXStore
currentHandlerInfo.saxStore.ignorableWhitespace(ch, start, length);
} else if (currentHandlerInfo != null && !currentHandlerInfo.elementHandler.isForwarding()) {
// The current handler doesn't want forwarding
// Just ignore content
} else {
// Send to output
output.ignorableWhitespace(ch, start, length);
}
} catch (Exception e) {
throw OrbeonLocationException.wrapException(e, LocationData.createIfPresent(locator));
}
}
public void processingInstruction(String target, String data) throws SAXException {
try {
if (isFillingUpSAXStore) {
// Fill-up SAXStore
currentHandlerInfo.saxStore.processingInstruction(target, data);
} else if (currentHandlerInfo != null && !currentHandlerInfo.elementHandler.isForwarding()) {
// The current handler doesn't want forwarding
// Just ignore content
} else {
// Send to output
output.processingInstruction(target, data);
}
} catch (Exception e) {
throw OrbeonLocationException.wrapException(e, LocationData.createIfPresent(locator));
}
}
public void skippedEntity(String name) throws SAXException {
try {
if (isFillingUpSAXStore) {
// Fill-up SAXStore
currentHandlerInfo.saxStore.skippedEntity(name);
} else if (currentHandlerInfo != null && !currentHandlerInfo.elementHandler.isForwarding()) {
// The current handler doesn't want forwarding
// Just ignore content
} else {
// Send to output
output.skippedEntity(name);
}
} catch (Exception e) {
throw OrbeonLocationException.wrapException(e, LocationData.createIfPresent(locator));
}
}
public void setDocumentLocator(Locator locator) {
// NOTE: This is called by the outer caller. Then it can be called by repeat or component body replay, which
// recursively hit this controller. The outer caller may or may not call setDocumentLocator() once. If there is
// one, repeat body replay recursively calls setDocumentLocator(), which is pushed on the stack, and then popped
// after the repeat body has been entirely replayed.
if (locator != null) {
if (this.locator == null) {
// This is likely the source's initial setDocumentLocator() call
// Use our own locator
this.locator = new OutputLocator();
this.locator.push(locator);
// We don't forward this (anyway nobody is listening initially)
} else {
// This is a repeat or component body replay (otherwise it's a bug)
// Push the SAXStore's locator
this.locator.push(locator);
// But don't forward this! SAX prevents calls to setDocumentLocator() mid-course. Our own locator will do the job.
}
}
}
public void startDTD(String name, String publicId, String systemId) throws SAXException {
try {
if (isFillingUpSAXStore) {
// Fill-up SAXStore
currentHandlerInfo.saxStore.startDTD(name, publicId, systemId);
} else if (currentHandlerInfo != null && !currentHandlerInfo.elementHandler.isForwarding()) {
// The current handler doesn't want forwarding
// Just ignore content
} else {
// Send to output
output.startDTD(name, publicId, systemId);
}
} catch (Exception e) {
throw OrbeonLocationException.wrapException(e, LocationData.createIfPresent(locator));
}
}
public void endDTD() throws SAXException {
try {
if (isFillingUpSAXStore) {
// Fill-up SAXStore
currentHandlerInfo.saxStore.endDTD();
} else if (currentHandlerInfo != null && !currentHandlerInfo.elementHandler.isForwarding()) {
// The current handler doesn't want forwarding
// Just ignore content
} else {
// Send to output
output.endDTD();
}
} catch (Exception e) {
throw OrbeonLocationException.wrapException(e, LocationData.createIfPresent(locator));
}
}
public void startEntity(String name) throws SAXException {
try {
if (isFillingUpSAXStore) {
// Fill-up SAXStore
currentHandlerInfo.saxStore.startEntity(name);
} else if (currentHandlerInfo != null && !currentHandlerInfo.elementHandler.isForwarding()) {
// The current handler doesn't want forwarding
// Just ignore content
} else {
// Send to output
output.startEntity(name);
}
} catch (Exception e) {
throw OrbeonLocationException.wrapException(e, LocationData.createIfPresent(locator));
}
}
public void endEntity(String name) throws SAXException {
try {
if (isFillingUpSAXStore) {
// Fill-up SAXStore
currentHandlerInfo.saxStore.endEntity(name);
} else if (currentHandlerInfo != null && !currentHandlerInfo.elementHandler.isForwarding()) {
// The current handler doesn't want forwarding
// Just ignore content
} else {
// Send to output
output.endEntity(name);
}
} catch (Exception e) {
throw OrbeonLocationException.wrapException(e, LocationData.createIfPresent(locator));
}
}
public void startCDATA() throws SAXException {
try {
if (isFillingUpSAXStore) {
// Fill-up SAXStore
currentHandlerInfo.saxStore.startCDATA();
} else if (currentHandlerInfo != null && !currentHandlerInfo.elementHandler.isForwarding()) {
// The current handler doesn't want forwarding
// Just ignore content
} else {
// Send to output
output.startCDATA();
}
} catch (Exception e) {
throw OrbeonLocationException.wrapException(e, LocationData.createIfPresent(locator));
}
}
public void endCDATA() throws SAXException {
try {
if (isFillingUpSAXStore) {
// Fill-up SAXStore
currentHandlerInfo.saxStore.endCDATA();
} else if (currentHandlerInfo != null && !currentHandlerInfo.elementHandler.isForwarding()) {
// The current handler doesn't want forwarding
// Just ignore content
} else {
// Send to output
output.endCDATA();
}
} catch (Exception e) {
throw OrbeonLocationException.wrapException(e, LocationData.createIfPresent(locator));
}
}
public void comment(char[] ch, int start, int length) throws SAXException {
try {
if (isFillingUpSAXStore) {
// Fill-up SAXStore
currentHandlerInfo.saxStore.comment(ch, start, length);
} else if (currentHandlerInfo != null && !currentHandlerInfo.elementHandler.isForwarding()) {
// The current handler doesn't want forwarding
// Just ignore content
} else {
// Send to output
output.comment(ch, start, length);
}
} catch (Exception e) {
throw OrbeonLocationException.wrapException(e, LocationData.createIfPresent(locator));
}
}
public Locator getLocator() {
return locator;
}
/**
* Get an ElementHandler based on a dom4j element.
*
* @param element element
* @return handler if found
*/
public Tuple2<ElementHandler, Object> getHandler(Element element) {
final HandlerInfo handlerInfo =
getHandler(
element.getNamespaceURI(),
XMLUtils.buildExplodedQName(element.getNamespaceURI(), element.getName()),
Dom4jUtils.getSAXAttributes(element)
);
return (handlerInfo != null)
? new scala.Tuple2<ElementHandler, Object>(handlerInfo.elementHandler, handlerInfo.matched)
: null;
}
private HandlerInfo getHandler(String uri, String explodedQName, Attributes attributes) {
// 1: Try custom matchers
{
final HandlerInfo handlerInfo = runMatchers(customMatchers, explodedQName, attributes);
if (handlerInfo != null)
return handlerInfo;
}
// 2: Try full matchers
final List<HandlerMatcher> handlerMatchers = this.handlerMatchers.get(explodedQName);
if (handlerMatchers != null) {
final HandlerInfo handlerInfo = runMatchers(handlerMatchers, explodedQName, attributes);
if (handlerInfo != null)
return handlerInfo;
}
// 3: Try URI-based handler
final String uriHandlerClassName = uriHandlers.get(uri);
if (uriHandlerClassName != null) {
final ElementHandler elementHandler = getHandlerByClassName(uriHandlerClassName);
return new HandlerInfo(level, explodedQName, elementHandler, attributes, null, this.locator);
}
return null;
}
private HandlerInfo runMatchers(List<HandlerMatcher> matchers, String explodedQName, Attributes attributes) {
for (HandlerMatcher handlerMatcher: matchers) {
final Object matched = handlerMatcher.matcher.match(attributes, elementHandlerContext);
if (matched != null) {
final ElementHandler elementHandler = getHandlerByClassName(handlerMatcher.handlerClassName);
return new HandlerInfo(level, explodedQName, elementHandler, attributes, matched, this.locator);
}
}
return null;
}
@SuppressWarnings("unchecked")
private ElementHandler getHandlerByClassName(String handlerClassName) {
Class<ElementHandler> handlerClass = classNameToHandlerClass.get(handlerClassName);
if (handlerClass == null) {
try {
handlerClass = (Class<ElementHandler>) Class.forName(handlerClassName);
classNameToHandlerClass.put(handlerClassName, handlerClass);
} catch (ClassNotFoundException e) {
throw OrbeonLocationException.wrapException(e, LocationData.createIfPresent(locator));
}
}
try {
return handlerClass.newInstance();
} catch (Exception e) {
throw OrbeonLocationException.wrapException(e, LocationData.createIfPresent(locator));
}
}
private static class HandlerInfo {
public final int level;
public final String explodedQName;
public final ElementHandler elementHandler;
public final Attributes attributes;
public final Object matched;
public final SAXStore saxStore;
public HandlerInfo(int level, String explodedQName, ElementHandler elementHandler, Attributes attributes, Object matched, Locator locator) {
this.level = level;
this.explodedQName = explodedQName;
this.elementHandler = elementHandler;
this.attributes = elementHandler.isRepeating() ? new AttributesImpl(attributes) : null; // NOTE: could keep attributes if needed
this.matched = matched;
this.saxStore = elementHandler.isRepeating() ? new SAXStore() : null;
// Set initial locator so that SAXStore can obtain location data if any
if (this.saxStore != null && locator != null)
this.saxStore.setDocumentLocator(locator);
}
}
public interface Matcher<T> {
T match(Attributes attributes, Object handlerContext);
}
private final Matcher ALL_MATCHER = new Matcher<Boolean>() {
public Boolean match(Attributes attributes, Object handlerContext) {
return Boolean.TRUE;
}
};
private static class HandlerMatcher {
public String handlerClassName;
public Matcher matcher;
private HandlerMatcher(String handlerClassName, Matcher matcher) {
this.handlerClassName = handlerClassName;
this.matcher = matcher;
}
}
}