/*
* eXist Open Source Native XML Database
* Copyright (C) 2001-04 Wolfgang M. Meier (wolfgang@exist-db.org)
* and others (see http://exist-db.org)
*
* 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
* 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.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*
* $Id$
*/
package org.exist.storage.serializers;
import org.apache.log4j.Logger;
import org.exist.dom.BinaryDocument;
import org.exist.dom.DocumentImpl;
import org.exist.dom.QName;
import org.exist.dom.StoredNode;
import org.exist.memtree.SAXAdapter;
import org.exist.security.Permission;
import org.exist.security.PermissionDeniedException;
import org.exist.security.xacml.AccessContext;
import org.exist.source.DBSource;
import org.exist.source.Source;
import org.exist.source.StringSource;
import org.exist.storage.XQueryPool;
import org.exist.util.serializer.AttrList;
import org.exist.util.serializer.Receiver;
import org.exist.xmldb.XmldbURI;
import org.exist.xquery.CompiledXQuery;
import org.exist.xquery.Constants;
import org.exist.xquery.Expression;
import org.exist.xquery.XPathException;
import org.exist.xquery.XQuery;
import org.exist.xquery.XQueryContext;
import org.exist.xquery.functions.request.RequestModule;
import org.exist.xquery.functions.response.ResponseModule;
import org.exist.xquery.functions.session.SessionModule;
import org.exist.xquery.util.ExpressionDumper;
import org.exist.xquery.value.NodeValue;
import org.exist.xquery.value.Sequence;
import org.exist.xquery.value.SequenceIterator;
import org.exist.xquery.value.Type;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLConnection;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.StringTokenizer;
/**
* A filter that listens for XInclude elements in the stream
* of events generated by the {@link org.exist.storage.serializers.Serializer}.
*
* XInclude elements are expanded at the position where they were found.
*/
public class XIncludeFilter implements Receiver {
private final static Logger LOG = Logger.getLogger(XIncludeFilter.class);
public final static String XINCLUDE_NS = "http://www.w3.org/2001/XInclude";
private final static QName HREF_ATTRIB = new QName("href", "");
private static final QName XPOINTER_ATTRIB = new QName("xpointer", "");
private static final String XI_INCLUDE = "include";
private static final String XI_FALLBACK = "fallback";
private static class ResourceError extends Exception {
private ResourceError(String message, Throwable cause) {
super(message, cause);
}
private ResourceError(String message) {
super(message);
}
}
private Receiver receiver;
private Serializer serializer;
private DocumentImpl document = null;
private String moduleLoadPath = null;
private HashMap namespaces = new HashMap(10);
private boolean inFallback = false;
private ResourceError error = null;
public XIncludeFilter(Serializer serializer, Receiver receiver) {
this.receiver = receiver;
this.serializer = serializer;
}
public XIncludeFilter(Serializer serializer) {
this(serializer, null);
}
public void setReceiver(Receiver handler) {
this.receiver = handler;
}
public Receiver getReceiver() {
return receiver;
}
public void setDocument(DocumentImpl doc) {
this.document = doc;
this.inFallback = false;
this.error = null;
}
public void setModuleLoadPath(String path) {
this.moduleLoadPath = path;
}
/* (non-Javadoc)
* @see org.exist.util.serializer.Receiver#characters(java.lang.CharSequence)
*/
public void characters(CharSequence seq) throws SAXException {
if (!inFallback || error != null)
receiver.characters(seq);
}
/* (non-Javadoc)
* @see org.exist.util.serializer.Receiver#comment(char[], int, int)
*/
public void comment(char[] ch, int start, int length) throws SAXException {
if (!inFallback || error != null)
receiver.comment(ch, start, length);
}
/**
* @see org.xml.sax.ContentHandler#endDocument()
*/
public void endDocument() throws SAXException {
receiver.endDocument();
}
/**
* @see org.exist.util.serializer.Receiver#endElement(org.exist.dom.QName)
*/
public void endElement(QName qname) throws SAXException {
if (XINCLUDE_NS.equals(qname.getNamespaceURI())) {
if (XI_FALLBACK.equals(qname.getLocalName())) {
inFallback = false;
// clear error
error = null;
} else if (XI_INCLUDE.equals(qname.getLocalName()) && error != null) {
// found an error, but there was no fallback element.
// throw the exception now
Exception e = error;
error = null;
throw new SAXException(e.getMessage(), e);
}
} else if (!inFallback || error != null)
receiver.endElement(qname);
}
public void endPrefixMapping(String prefix) throws SAXException {
namespaces.remove(prefix);
receiver.endPrefixMapping(prefix);
}
/**
* @see org.xml.sax.ContentHandler#processingInstruction(java.lang.String, java.lang.String)
*/
public void processingInstruction(String target, String data) throws SAXException {
if (!inFallback || error != null)
receiver.processingInstruction(target, data);
}
/**
* @see org.exist.util.serializer.Receiver#cdataSection(char[], int, int)
*/
public void cdataSection(char[] ch, int start, int len) throws SAXException {
if (!inFallback || error != null)
receiver.cdataSection(ch, start, len);
}
/**
* @see org.xml.sax.ContentHandler#startDocument()
*/
public void startDocument() throws SAXException {
receiver.startDocument();
}
/* (non-Javadoc)
* @see org.exist.util.serializer.Receiver#attribute(org.exist.dom.QName, java.lang.String)
*/
public void attribute(QName qname, String value) throws SAXException {
if (!inFallback || error != null)
receiver.attribute(qname, value);
}
/* (non-Javadoc)
* @see org.exist.util.serializer.Receiver#startElement(org.exist.dom.QName, org.exist.util.serializer.AttrList)
*/
public void startElement(QName qname, AttrList attribs) throws SAXException {
if (qname.getNamespaceURI() != null && qname.getNamespaceURI().equals(XINCLUDE_NS)) {
if (qname.getLocalName().equals(XI_INCLUDE)) {
if (LOG.isDebugEnabled())
LOG.debug("processing include ...");
try {
processXInclude(attribs.getValue(HREF_ATTRIB), attribs.getValue(XPOINTER_ATTRIB));
} catch (ResourceError resourceError) {
if (LOG.isDebugEnabled())
LOG.debug(resourceError.getMessage(), resourceError);
error = resourceError;
}
} else if (qname.getLocalName().equals(XI_FALLBACK)) {
inFallback = true;
}
} else if (!inFallback || error != null) {
//LOG.debug("start: " + qName);
receiver.startElement(qname, attribs);
}
}
public void documentType(String name, String publicId, String systemId)
throws SAXException {
receiver.documentType(name, publicId, systemId);
}
public void highlightText(CharSequence seq) {
// not supported with this receiver
}
protected void processXInclude(String href, String xpointer) throws SAXException, ResourceError {
if(href == null)
throw new SAXException("No href attribute found in XInclude include element");
// save some settings
DocumentImpl prevDoc = document;
boolean createContainerElements = serializer.createContainerElements;
serializer.createContainerElements = false;
//The following comments are the basis for possible external documents
XmldbURI docUri = null;
try {
docUri = XmldbURI.xmldbUriFor(href);
/*
if(!stylesheetUri.toCollectionPathURI().equals(stylesheetUri)) {
externalUri = stylesheetUri.getXmldbURI();
}
*/
} catch (URISyntaxException e) {
//could be an external URI!
}
// parse the href attribute
LOG.debug("found href=\"" + href + "\"");
//String xpointer = null;
//String docName = href;
Map params = null;
DocumentImpl doc = null;
org.exist.memtree.DocumentImpl memtreeDoc = null;
boolean xqueryDoc = false;
if (docUri != null) {
String fragment = docUri.getFragment();
if (!(fragment == null || fragment.length() == 0))
throw new SAXException("Fragment identifiers must not be used in an xinclude href attribute. To specify an " +
"xpointer, use the xpointer attribute.");
// extract possible parameters in the URI
params = null;
String paramStr = docUri.getQuery();
if (paramStr != null) {
params = processParameters(paramStr);
// strip query part
docUri = XmldbURI.create(docUri.getRawCollectionPath());
}
// if docName has no collection specified, assume
// current collection
// Patch 1520454 start
if (!docUri.isAbsolute() && document != null) {
String base = document.getCollection().getURI() + "/";
String child = "./" + docUri.toString();
URI baseUri = URI.create(base);
URI childUri = URI.create(child);
URI uri = baseUri.resolve(childUri);
docUri = XmldbURI.create(uri);
}
// Patch 1520454 end
// retrieve the document
doc = null;
try {
doc = (DocumentImpl) serializer.broker.getXMLResource(docUri);
if(doc != null && !doc.getPermissions().validate(serializer.broker.getUser(), Permission.READ))
throw new ResourceError("Permission denied to read xincluded resource");
} catch (PermissionDeniedException e) {
LOG.warn("permission denied", e);
throw new ResourceError("Permission denied to read xincluded resource", e);
}
/* Check if the document is a stored XQuery */
if (doc != null && doc.getResourceType() == DocumentImpl.BINARY_FILE) {
xqueryDoc = "application/xquery".equals(doc.getMetadata().getMimeType());
}
}
// The document could not be found: check if it points to an external resource
if (docUri == null || (doc == null && !docUri.isAbsolute())) {
try {
URI externalUri = new URI(href);
String scheme = externalUri.getScheme();
// If the URI has no scheme is specified,
// we have to check if it is a relative path, and if yes, try to
// interpret it relative to the moduleLoadPath property of the current
// XQuery context.
if (scheme == null && moduleLoadPath != null) {
String path = externalUri.getSchemeSpecificPart();
File f = new File(path);
if (!f.isAbsolute()) {
if (moduleLoadPath.startsWith(XmldbURI.XMLDB_URI_PREFIX)) {
XmldbURI parentUri = XmldbURI.create(moduleLoadPath);
docUri = parentUri.append(path);
try {
doc = (DocumentImpl) serializer.broker.getXMLResource(docUri);
if(doc != null && !doc.getPermissions().validate(serializer.broker.getUser(), Permission.READ))
throw new ResourceError("Permission denied to read xincluded resource");
} catch (PermissionDeniedException e) {
LOG.warn("permission denied", e);
throw new ResourceError("Permission denied to read xincluded resource", e);
}
} else {
f = new File(moduleLoadPath, path);
externalUri = f.toURI();
}
}
}
if (doc == null)
memtreeDoc = parseExternal(externalUri);
} catch (IOException e) {
throw new ResourceError("XInclude: failed to read document at URI: " + href +
": " + e.getMessage(), e);
} catch (PermissionDeniedException e) {
throw new ResourceError("XInclude: failed to read document at URI: " + href +
": " + e.getMessage(), e);
} catch (ParserConfigurationException e) {
throw new ResourceError("XInclude: failed to read document at URI: " + href +
": " + e.getMessage(), e);
} catch (URISyntaxException e) {
throw new ResourceError("XInclude: failed to read document at URI: " + href +
": " + e.getMessage(), e);
}
}
/* if document has not been found and xpointer is
* null, throw an exception. If xpointer != null
* we retry below and interpret docName as
* a collection.
*/
if (doc == null && memtreeDoc == null && xpointer == null)
throw new ResourceError("document " + docUri + " not found");
if (xpointer == null && !xqueryDoc) {
// no xpointer found - just serialize the doc
if (memtreeDoc == null)
serializer.serializeToReceiver(doc, false);
else
serializer.serializeToReceiver(memtreeDoc, false);
} else {
// process the xpointer or the stored XQuery
try {
Source source;
if (xpointer == null)
source = new DBSource(serializer.broker, (BinaryDocument) doc, true);
else {
xpointer = checkNamespaces(xpointer);
source = new StringSource(xpointer);
}
XQuery xquery = serializer.broker.getXQueryService();
XQueryPool pool = xquery.getXQueryPool();
XQueryContext context;
CompiledXQuery compiled = pool.borrowCompiledXQuery(serializer.broker, source);
if (compiled != null)
context = compiled.getContext();
else
context = xquery.newContext(AccessContext.XINCLUDE);
context.declareNamespaces(namespaces);
context.declareNamespace("xinclude", XINCLUDE_NS);
//setup the http context if known
if(serializer.httpContext != null)
{
if(serializer.httpContext.getRequest() != null)
context.declareVariable(RequestModule.PREFIX + ":request", serializer.httpContext.getRequest());
if(serializer.httpContext.getResponse() != null)
context.declareVariable(ResponseModule.PREFIX + ":response", serializer.httpContext.getResponse());
if(serializer.httpContext.getSession() != null)
context.declareVariable(SessionModule.PREFIX + ":session", serializer.httpContext.getSession());
}
//TODO: change these to putting the XmldbURI in, but we need to warn users!
if(document!=null){
context.declareVariable("xinclude:current-doc", document.getFileURI().toString());
context.declareVariable("xinclude:current-collection", document.getCollection().getURI().toString());
}
if (xpointer != null) {
if(doc != null)
context.setStaticallyKnownDocuments(new XmldbURI[] { doc.getURI() } );
else if (docUri != null)
context.setStaticallyKnownDocuments(new XmldbURI[] { docUri });
}
// pass parameters as variables
if (params != null) {
for (Iterator i = params.entrySet().iterator(); i.hasNext(); ) {
Map.Entry entry = (Map.Entry) i.next();
context.declareVariable(entry.getKey().toString(), entry.getValue());
}
}
if(compiled == null) {
try {
compiled = xquery.compile(context, source, xpointer != null);
} catch (IOException e) {
throw new SAXException("I/O error while reading query for xinclude: " + e.getMessage(), e);
}
}
LOG.info("xpointer query: " + ExpressionDumper.dump((Expression) compiled));
Sequence contextSeq = null;
if (memtreeDoc != null)
contextSeq = memtreeDoc;
Sequence seq = xquery.execute(compiled, contextSeq);
if(Type.subTypeOf(seq.getItemType(), Type.NODE)) {
if (LOG.isDebugEnabled())
LOG.debug("xpointer found: " + seq.getItemCount());
NodeValue node;
for (SequenceIterator i = seq.iterate(); i.hasNext();) {
node = (NodeValue) i.nextItem();
serializer.serializeToReceiver(node, false);
}
} else {
String val;
for (int i = 0; i < seq.getItemCount(); i++) {
val = seq.itemAt(i).getStringValue();
characters(val);
}
}
} catch (XPathException e) {
LOG.warn("xpointer error", e);
throw new SAXException("Error while processing XInclude expression: " + e.getMessage(), e);
}
}
// restore settings
document = prevDoc;
serializer.createContainerElements = createContainerElements;
}
private org.exist.memtree.DocumentImpl parseExternal(URI externalUri) throws IOException, ResourceError, PermissionDeniedException, ParserConfigurationException, SAXException {
URLConnection con = externalUri.toURL().openConnection();
if(con instanceof HttpURLConnection)
{
HttpURLConnection httpConnection = (HttpURLConnection)con;
if(httpConnection.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND)
{
// Special case: '404'
throw new ResourceError("XInclude: no document found at URI: " + externalUri.toString());
}
else if(httpConnection.getResponseCode() != HttpURLConnection.HTTP_OK)
{
//TODO : return another type
throw new PermissionDeniedException("Server returned code " + httpConnection.getResponseCode());
}
}
// we use eXist's in-memory DOM implementation
SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setNamespaceAware(true);
InputSource src = new InputSource(con.getInputStream());
SAXParser parser = factory.newSAXParser();
XMLReader reader = parser.getXMLReader();
SAXAdapter adapter = new SAXAdapter();
reader.setContentHandler(adapter);
reader.parse(src);
org.exist.memtree.DocumentImpl doc =
(org.exist.memtree.DocumentImpl)adapter.getDocument();
doc.setDocumentURI(externalUri.toString());
return doc;
}
/**
* @see org.xml.sax.ContentHandler#startPrefixMapping(java.lang.String, java.lang.String)
*/
public void startPrefixMapping(String prefix, String uri) throws SAXException {
namespaces.put(prefix, uri);
receiver.startPrefixMapping(prefix, uri);
}
/**
* Process xmlns() schema. We process these here, because namespace mappings should
* already been known when parsing the xpointer() expression.
*
* @param xpointer
* @return
* @throws XPathException
*/
private String checkNamespaces(String xpointer) throws XPathException {
int p0;
while((p0 = xpointer.indexOf("xmlns(")) != Constants.STRING_NOT_FOUND) {
if(p0 < 0)
return xpointer;
int p1 = xpointer.indexOf(')', p0 + 6);
if(p1 < 0)
throw new XPathException("expected ) for xmlns()");
String mapping = xpointer.substring(p0 + 6, p1);
xpointer = xpointer.substring(0, p0) + xpointer.substring(p1 + 1);
StringTokenizer tok = new StringTokenizer(mapping, "= \t\n");
if(tok.countTokens() < 2)
throw new XPathException("expected prefix=namespace mapping in " + mapping);
String prefix = tok.nextToken();
String namespaceURI = tok.nextToken();
namespaces.put(prefix, namespaceURI);
}
return xpointer;
}
protected HashMap processParameters(String args) {
HashMap parameters = new HashMap();
String param;
String value;
int start = 0;
int end = 0;
int l = args.length();
while ((start < l) && (end < l)) {
while ((end < l) && (args.charAt(end++) != '='))
;
if (end == l)
break;
param = args.substring(start, end - 1);
start = end;
while ((end < l) && (args.charAt(end++) != '&'))
;
if (end == l)
value = args.substring(start);
else
value = args.substring(start, end - 1);
start = end;
try {
param = URLDecoder.decode(param, "UTF-8");
value = URLDecoder.decode(value, "UTF-8");
LOG.debug("parameter: " + param + " = " + value);
parameters.put(param, value);
} catch (UnsupportedEncodingException e) {
LOG.warn(e.getMessage(), e);
}
}
return parameters;
}
public void setCurrentNode(StoredNode node) {
//ignored
}
public Document getDocument() {
//ignored
return null;
}
}