/**
* 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.oxf.common.OXFException;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Implements a streamable subset of XPath.
*
* This implementation requires that all searched XPath expressions be set beforehand.
*
* The class can be used as a regular XMLReceiver, or can use a callback to start reading the
* input SAX stream.
*
* Right now, only the first result asked will be streamed. All other results will be stored in SAX
* stores. This is not optimal, but since there are no coroutines in Java, it is hard to implement
* the more efficient way, which is to store only what is strictly required. In the best case
* scenario, if the matches occur in the document in the order asked, everything will be streamed
* (at least, in single node mode).
*
* TODO:
* o Do we need to build a stack of prefix mappings?
* o Implement other expressions!
* o Handle single node vs. all nodes
* o Handle duplicate expressions (e.g. /a/b/c added several times)
*/
public class XPathXMLReceiver implements XMLReceiver {
private Map expressions;
private List expressionsList;
private Expression[] expressionsArray;
private boolean canStream = true;
private Runnable readInputCallback;
private boolean readStarted;
private Expression searchedExpression;
private XMLReceiver output;
private static class Expression {
public Expression(String xpathExpression) {
this.xpathExpression = xpathExpression;
}
public String xpathExpression;
public ContentHandler expressionContentHandler;
public SAXStore result;
}
public XPathXMLReceiver() {
}
public boolean addExpresssion(String xpathExpression, boolean nodeSet) {
if (!supportsExpression(xpathExpression))
canStream = false;
if (expressions == null) {
expressions = new HashMap();
expressionsList = new ArrayList();
}
// TODO: Detect duplicate expressions?
// Create expression
Expression expression = new Expression(xpathExpression);
expression.expressionContentHandler = getExpressionContentHandler(expression);
expressions.put(xpathExpression, expression);
expressionsList.add(expression);
return canStream;
}
public boolean containsExpression(String xpathExpression) {
return expressions.containsKey(xpathExpression);
}
public void setReadInputCallback(Runnable readInputCallback) {
this.readInputCallback = readInputCallback;
}
public void selectContentHandler(String xpathExpression, XMLReceiver xmlReceiver) throws SAXException {
if (!canStream)
throw new OXFException("Cannot stream");
Expression expression = (Expression) expressions.get(xpathExpression);
if (expression == null)
throw new OXFException("Undefined expression: " + xpathExpression);
// Check if the expression has already been computed
if (expression.result != null) {
expression.result.replay(xmlReceiver);
return;
}
// Start the read if needed
if (!readStarted) {
if (readInputCallback == null)
throw new OXFException("Read not started and no read callback specified");
searchedExpression = expression;
output = xmlReceiver;
readInputCallback.run();
// At this point, all the stream is read
searchedExpression = null;
output = null;
}
// Well, if we had coroutines, we could continue the search from here. Instead, we have to
// compute everything before. If we get here, it means we got an empty result.
}
public static boolean supportsExpression(String xpathExpression) {
return getExpressionId(xpathExpression) != -1;
}
private static int getExpressionId(String xpathExpression) {
// Add other expressions here when implemented
// Identity transformation
if ("/*".equals(xpathExpression)) // should also match on "/" ? can also match on /abc, /abc[1], etc.
return 1;
return -1;
}
private ContentHandler getExpressionContentHandler(Expression expression) {
int expressionId = getExpressionId(expression.xpathExpression);
switch (expressionId) {
case 1:
return new Handler1(expression);
// case 2:
// return new Handler2(expression);
// Etc.
default:
return null;
}
}
public void characters(char ch[], int start, int length) throws SAXException {
for (int i = 0; i < expressionsArray.length; i++)
expressionsArray[i].expressionContentHandler.characters(ch, start, length);
}
public void endDocument() throws SAXException {
for (int i = 0; i < expressionsArray.length; i++)
expressionsArray[i].expressionContentHandler.endDocument();
}
public void endElement(String namespaceURI, String localName, String qName) throws SAXException {
for (int i = 0; i < expressionsArray.length; i++)
expressionsArray[i].expressionContentHandler.endElement(namespaceURI, localName, qName);
}
public void endPrefixMapping(String prefix) throws SAXException {
for (int i = 0; i < expressionsArray.length; i++)
expressionsArray[i].expressionContentHandler.endPrefixMapping(prefix);
}
public void ignorableWhitespace(char ch[], int start, int length) throws SAXException {
for (int i = 0; i < expressionsArray.length; i++)
expressionsArray[i].expressionContentHandler.ignorableWhitespace(ch, start, length);
}
public void processingInstruction(String target, String data) throws SAXException {
for (int i = 0; i < expressionsArray.length; i++)
expressionsArray[i].expressionContentHandler.processingInstruction(target, data);
}
public void setDocumentLocator(Locator locator) {
}
public void skippedEntity(String name) throws SAXException {
for (int i = 0; i < expressionsArray.length; i++)
expressionsArray[i].expressionContentHandler.skippedEntity(name);
}
public void startDocument() throws SAXException {
// Setup expression content handlers
expressionsArray = new Expression[expressionsList.size()];
expressionsList.toArray(expressionsArray);
readStarted = true;
for (int i = 0; i < expressionsArray.length; i++)
expressionsArray[i].expressionContentHandler.startDocument();
}
public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException {
for (int i = 0; i < expressionsArray.length; i++)
expressionsArray[i].expressionContentHandler.startElement(namespaceURI, localName, qName, atts);
}
public void startPrefixMapping(String prefix, String uri) throws SAXException {
for (int i = 0; i < expressionsArray.length; i++)
expressionsArray[i].expressionContentHandler.startPrefixMapping(prefix, uri);
}
private static abstract class ExpressionSearchHandler extends ForwardingXMLReceiver {
protected Expression expresssion;
protected ExpressionSearchHandler(Expression expresssion) {
this.expresssion = expresssion;
}
}
/**
* The root element.
*/
private class Handler1 extends ExpressionSearchHandler {
private int level = 0;
public Handler1(Expression expresssion) {
super(expresssion);
}
public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException {
level++;
if (level == 1) {
// Found!
setForward(true);
if (searchedExpression == expresssion) {
// Output directly
setXMLReceiver(output);
} else {
// Store result
expresssion.result = new SAXStore();
setXMLReceiver(expresssion.result);
}
}
super.startElement(namespaceURI, localName, qName, atts);
}
public void endElement(String namespaceURI, String localName, String qName) throws SAXException {
level--;
super.endElement(namespaceURI, localName, qName);
if (level == 0)
setForward(false);
}
}
public void startDTD(String name, String publicId, String systemId) throws SAXException {
// Ignore
}
public void endDTD() throws SAXException {
// Ignore
}
public void startEntity(String name) throws SAXException {
// Ignore
}
public void endEntity(String name) throws SAXException {
// Ignore
}
public void startCDATA() throws SAXException {
// Ignore
}
public void endCDATA() throws SAXException {
// Ignore
}
public void comment(char[] ch, int start, int length) throws SAXException {
// Ignore
}
// private class Handler2 extends ExpressionSearchHandler {
//
// public Handler2(Expression expresssion) {
// super(expresssion);
// }
//
// public void characters(char ch[], int start, int length) throws SAXException {
// output.characters(ch, start, length);
// }
//
// public void endDocument() throws SAXException {
// output.endDocument();
// }
//
// public void endElement(String namespaceURI, String localName, String qName) throws SAXException {
// output.endElement(namespaceURI, localName, qName);
// }
//
// public void endPrefixMapping(String prefix) throws SAXException {
// output.endPrefixMapping(prefix);
// }
//
// public void ignorableWhitespace(char ch[], int start, int length) throws SAXException {
// output.ignorableWhitespace(ch, start, length);
// }
//
// public void processingInstruction(String target, String data) throws SAXException {
// output.processingInstruction(target, data);
// }
//
// public void setDocumentLocator(Locator locator) {
// }
//
// public void skippedEntity(String name) throws SAXException {
// output.skippedEntity(name);
// }
//
// public void startDocument() throws SAXException {
// output.startDocument();
// }
//
// public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException {
// output.startElement(namespaceURI, localName, qName, atts);
// }
//
// public void startPrefixMapping(String prefix, String uri) throws SAXException {
// output.startPrefixMapping(prefix, uri);
// }
// }
}