/* * eXist Open Source Native XML Database * Copyright (C) 2001-06 The eXist Project * 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$ */ /* * Some modifications Copyright (C) 2004 Luigi P. Bai * finder@users.sf.net * Licensed as above under the LGPL. * */ package org.exist.xupdate; import antlr.RecognitionException; import antlr.TokenStreamException; import antlr.collections.AST; import org.apache.log4j.Logger; import org.exist.Namespaces; import org.exist.dom.DocumentSet; import org.exist.dom.NodeListImpl; import org.exist.dom.NodeSetHelper; import org.exist.security.xacml.AccessContext; import org.exist.security.xacml.NullAccessContextException; import org.exist.storage.DBBroker; import org.exist.util.Configuration; import org.exist.util.FastStringBuffer; import org.exist.xquery.AnalyzeContextInfo; import org.exist.xquery.Constants; import org.exist.xquery.PathExpr; import org.exist.xquery.XPathException; import org.exist.xquery.XQueryContext; import org.exist.xquery.parser.XQueryLexer; import org.exist.xquery.parser.XQueryParser; import org.exist.xquery.parser.XQueryTreeParser; import org.exist.xquery.value.Item; 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.Attr; import org.w3c.dom.Comment; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.ProcessingInstruction; import org.w3c.dom.Text; import org.xml.sax.Attributes; import org.xml.sax.ContentHandler; import org.xml.sax.InputSource; import org.xml.sax.Locator; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; import org.xml.sax.ext.LexicalHandler; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import java.io.IOException; import java.io.StringReader; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Stack; import java.util.TreeMap; /** * Main class to pre-process an XUpdate request. XUpdateProcessor * will parse the request via SAX and compile it into a set of * {@link Modification} objects as returned by the {@link #parse(org.xml.sax.InputSource)} * method. The modifications can then be executed via {@link Modification#process(org.exist.storage.txn.Txn)}. * * @author Wolfgang Meier * */ public class XUpdateProcessor implements ContentHandler, LexicalHandler { public static final String MODIFICATIONS = "modifications"; // Modifications public static final String INSERT_AFTER = "insert-after"; public static final String INSERT_BEFORE = "insert-before"; public static final String REPLACE = "replace"; public static final String RENAME = "rename"; public static final String REMOVE = "remove"; public static final String APPEND = "append"; public static final String UPDATE = "update"; // node constructors public static final String COMMENT = "comment"; public static final String PROCESSING_INSTRUCTION = "processing-instruction"; public static final String TEXT = "text"; public static final String ATTRIBUTE = "attribute"; public static final String ELEMENT = "element"; public static final String VALUE_OF = "value-of"; public static final String VARIABLE = "variable"; public static final String IF = "if"; public final static String XUPDATE_NS = "http://www.xmldb.org/xupdate"; private final static Logger LOG = Logger.getLogger(XUpdateProcessor.class); /** * NodeList to keep track of created document fragments within * the currently processed XUpdate modification. */ private NodeListImpl contents = null; // Flags needed during SAX processing private boolean inModification = false; private boolean inAttribute = false; /** * Whitespace preservation: the XUpdate processor * will honour xml:space attribute settings. */ private boolean preserveWhitespace = false; private boolean preserveWhitespaceTemp = false; /** * Stack to maintain xml:space settings. The items on the * stack are strings, containing either "default" or "preserve". */ private Stack spaceStack = null; /** * The modification we are currently processing. */ private Modification modification = null; /** The DocumentBuilder used to create new nodes */ private DocumentBuilder builder; /** The Document object used to create new nodes */ private Document doc; /** The current element stack. Contains the last elements processed. */ private Stack stack = new Stack(); /** The last node that has been created */ private Node currentNode = null; /** DBBroker for this instance */ private DBBroker broker; /** The set of documents to which this XUpdate might apply. */ private DocumentSet documentSet; /** * The final list of modifications. All modifications encountered * within the XUpdate will be added to this list. The final list * will be returned to the caller. */ private List modifications = new ArrayList(); /** Temporary string buffer used for collecting text chunks */ private FastStringBuffer charBuf = new FastStringBuffer(64); // Environment /** Contains all variables declared via xupdate:variable. * Maps variable QName to the Sequence returned by * evaluating the variable expression. */ private Map variables = new TreeMap(); /** * Keeps track of namespaces declared within the XUpdate. */ private Map namespaces = new HashMap(10); /** * Stack used to track conditionals. */ private Stack conditionals = new Stack(); private AccessContext accessCtx; /** * Constructor for XUpdateProcessor. */ public XUpdateProcessor(DBBroker broker, DocumentSet docs, AccessContext accessCtx) throws ParserConfigurationException { if(accessCtx == null) throw new NullAccessContextException(); this.accessCtx = accessCtx; DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); factory.setValidating(false); this.builder = factory.newDocumentBuilder(); this.broker = broker; this.documentSet = docs; //namespaces.put("xml", Namespaces.XML_NS); //TODO : move this to a dedicated configure() method. if (broker != null) { Configuration config = broker.getConfiguration(); Boolean temp; if ((temp = (Boolean) config.getProperty("indexer.preserve-whitespace-mixed-content")) != null) preserveWhitespaceTemp = temp.booleanValue(); } } private XUpdateProcessor() throws ParserConfigurationException { this(null, null, null); } public void setBroker(DBBroker broker) { this.broker = broker; } public void setDocumentSet(DocumentSet docs) { this.documentSet = docs; } /** * Parse the input source into a set of modifications. * * @param is * @return an array of type Modification * @throws ParserConfigurationException * @throws IOException * @throws SAXException */ public Modification[] parse(InputSource is) throws ParserConfigurationException, IOException, SAXException { XMLReader reader = broker.getBrokerPool().getParserPool().borrowXMLReader(); try { reader.setProperty(Namespaces.SAX_LEXICAL_HANDLER, this); reader.setFeature(Namespaces.SAX_NAMESPACES, true); reader.setFeature(Namespaces.SAX_NAMESPACES_PREFIXES, false); reader.setContentHandler(this); reader.parse(is); Modification mods[] = new Modification[modifications.size()]; return (Modification[]) modifications.toArray(mods); } finally { broker.getBrokerPool().getParserPool().returnXMLReader(reader); } } /** * @see org.xml.sax.ContentHandler#setDocumentLocator(org.xml.sax.Locator) */ public void setDocumentLocator(Locator locator) { } /** * @see org.xml.sax.ContentHandler#startDocument() */ public void startDocument() throws SAXException { // The default... this.preserveWhitespace = preserveWhitespaceTemp; this.spaceStack = new Stack(); this.spaceStack.push("default"); } /** * @see org.xml.sax.ContentHandler#endDocument() */ public void endDocument() throws SAXException { } /** * @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); } /** * @see org.xml.sax.ContentHandler#endPrefixMapping(java.lang.String) */ public void endPrefixMapping(String prefix) throws SAXException { namespaces.remove(prefix); } /** * @see org.xml.sax.ContentHandler#startElement(java.lang.String, java.lang.String, java.lang.String, org.xml.sax.Attributes) */ public void startElement( String namespaceURI, String localName, String qName, Attributes atts) throws SAXException { // save accumulated character content if (inModification && charBuf.length() > 0) { // String normalized = charBuf.toString(); final String normalized = preserveWhitespace ? charBuf.toString() : charBuf.getNormalizedString(FastStringBuffer.SUPPRESS_BOTH); if (normalized.length() > 0) { Text text = doc.createTextNode(charBuf.toString()); if (stack.isEmpty()) { //LOG.debug("appending text to fragment: " + text.getData()); contents.add(text); } else { Element last = (Element) stack.peek(); last.appendChild(text); } } charBuf.setLength(0); } if (namespaceURI.equals(XUPDATE_NS)) { String select = null; if (localName.equals(MODIFICATIONS)) { startModifications(atts); return; } else if (localName.equals(VARIABLE)) { // variable declaration startVariableDecl(atts); return; } else if (IF.equals(localName)) { if (inModification) throw new SAXException("xupdate:if is not allowed inside a modification"); select = atts.getValue("test"); Conditional cond = new Conditional(broker, documentSet, select, namespaces, variables); cond.setAccessContext(accessCtx); conditionals.push(cond); return; } else if (VALUE_OF.equals(localName)) { if(!inModification) throw new SAXException("xupdate:value-of is not allowed outside a modification"); } else if (APPEND.equals(localName) || INSERT_BEFORE.equals(localName) || INSERT_AFTER.equals(localName) || REMOVE.equals(localName) || RENAME.equals(localName) || UPDATE.equals(localName) || REPLACE.equals(localName)) { if (inModification) throw new SAXException("nested modifications are not allowed"); select = atts.getValue("select"); if (select == null) throw new SAXException( localName + " requires a select attribute"); doc = builder.newDocument(); contents = new NodeListImpl(); inModification = true; } else if ( (ELEMENT.equals(localName) || ATTRIBUTE.equals(localName) || TEXT.equals(localName) || PROCESSING_INSTRUCTION.equals(localName) || COMMENT.equals(localName))) { if(!inModification) throw new SAXException( "creation elements are only allowed inside " + "a modification"); charBuf.setLength(0); } else throw new SAXException("Unknown XUpdate element: " + qName); // start a new modification section if (APPEND.equals(localName)) { String child = atts.getValue("child"); modification = new Append(broker, documentSet, select, child, namespaces, variables); } else if (UPDATE.equals(localName)) modification = new Update(broker, documentSet, select, namespaces, variables); else if (INSERT_BEFORE.equals(localName)) modification = new Insert(broker, documentSet, select, Insert.INSERT_BEFORE, namespaces, variables); else if (INSERT_AFTER.equals(localName)) modification = new Insert(broker, documentSet, select, Insert.INSERT_AFTER, namespaces, variables); else if (REMOVE.equals(localName)) modification = new Remove(broker, documentSet, select, namespaces, variables); else if (RENAME.equals(localName)) modification = new Rename(broker, documentSet, select, namespaces, variables); else if (REPLACE.equals(localName)) modification = new Replace(broker, documentSet, select, namespaces, variables); // process commands for node creation else if (ELEMENT.equals(localName)) { String name = atts.getValue("name"); if (name == null) throw new SAXException("element requires a name attribute"); int p = name.indexOf(':'); String namespace = null; String prefix = ""; if (p != Constants.STRING_NOT_FOUND) { prefix = name.substring(0, p); if (name.length() == p + 1) throw new SAXException( "illegal prefix in qname: " + name); name = name.substring(p + 1); namespace = atts.getValue("namespace"); if(namespace == null) namespace = (String) namespaces.get(prefix); if (namespace == null) { throw new SAXException( "no namespace defined for prefix " + prefix); } } Element elem; if (namespace != null && namespace.length() > 0) { elem = doc.createElementNS(namespace, name); elem.setPrefix(prefix); } else elem = doc.createElement(name); if (stack.isEmpty()) { contents.add(elem); } else { Element last = (Element) stack.peek(); last.appendChild(elem); } this.setWhitespaceHandling((Element) stack.push(elem)); } else if (ATTRIBUTE.equals(localName)) { String name = atts.getValue("name"); if (name == null) throw new SAXException("attribute requires a name attribute"); int p = name.indexOf(':'); String namespace = null; if (p != Constants.STRING_NOT_FOUND) { String prefix = name.substring(0, p); if (name.length() == p + 1) throw new SAXException( "illegal prefix in qname: " + name); namespace = atts.getValue("namespace"); if(namespace == null) namespace = (String) namespaces.get(prefix); if (namespace == null) throw new SAXException( "no namespace defined for prefix " + prefix); } Attr attrib = namespace != null && namespace.length() > 0 ? doc.createAttributeNS(namespace, name) : doc.createAttribute(name); if (stack.isEmpty()) { for(int i = 0; i < contents.getLength(); i++) { Node n = contents.item(i); String ns = n.getNamespaceURI(); String nname = ns == null ? n.getNodeName() : n.getLocalName(); if(ns == null) ns = ""; // check for duplicate attributes if(n.getNodeType() == Node.ATTRIBUTE_NODE && nname.equals(name) && ns.equals(namespace)) throw new SAXException("The attribute " + attrib.getNodeName() + " cannot be specified twice"); } contents.add(attrib); } else { Element last = (Element) stack.peek(); if(namespace != null && last.hasAttributeNS(namespace, name) || namespace == null && last.hasAttribute(name)) throw new SAXException("The attribute " + attrib.getNodeName() + " cannot be specified " + "twice on the same element"); if (namespace != null) last.setAttributeNodeNS(attrib); else last.setAttributeNode(attrib); } inAttribute = true; currentNode = attrib; // process value-of } else if (VALUE_OF.equals(localName)) { select = atts.getValue("select"); if (select == null) throw new SAXException("value-of requires a select attribute"); Sequence seq = processQuery(select); if (LOG.isDebugEnabled()) LOG.debug("Found " + seq.getItemCount() + " items for value-of"); Item item; try { for (SequenceIterator i = seq.iterate(); i.hasNext();) { item = i.nextItem(); if(Type.subTypeOf(item.getType(), Type.NODE)) { Node node = NodeSetHelper.copyNode(doc, ((NodeValue)item).getNode()); if (stack.isEmpty()) contents.add(node); else { Element last = (Element) stack.peek(); last.appendChild(node); } } else { String value = item.getStringValue(); characters(value.toCharArray(), 0, value.length()); } } } catch (XPathException e) { throw new SAXException(e.getMessage(), e); } } } else if (inModification) { Element elem = namespaceURI != null && namespaceURI.length() > 0 ? doc.createElementNS(namespaceURI, qName) : doc.createElement(qName); Attr a; for (int i = 0; i < atts.getLength(); i++) { String name = atts.getQName(i); String nsURI = atts.getURI(i); if (name.startsWith("xmlns")) { // Why are these showing up? They are supposed to be stripped out? } else { a = nsURI != null ? doc.createAttributeNS(nsURI, name) : doc.createAttribute(name); a.setValue(atts.getValue(i)); if (nsURI != null) elem.setAttributeNodeNS(a); else elem.setAttributeNode(a); } } if (stack.isEmpty()) { contents.add(elem); } else { Element last = (Element) stack.peek(); last.appendChild(elem); } this.setWhitespaceHandling((Element) stack.push(elem)); } } private void startVariableDecl(Attributes atts) throws SAXException { String select = atts.getValue("select"); if (select == null) throw new SAXException("variable declaration requires a select attribute"); String name = atts.getValue("name"); if (name == null) throw new SAXException("variable declarations requires a name attribute"); createVariable(name, select); } private void startModifications(Attributes atts) throws SAXException { String version = atts.getValue("version"); if (version == null) throw new SAXException( "version attribute is required for " + "element modifications"); if (!version.equals("1.0")) throw new SAXException( "Version " + version + " of XUpdate " + "not supported."); } /** * @see org.xml.sax.ContentHandler#endElement(java.lang.String, java.lang.String, java.lang.String) */ public void endElement(String namespaceURI, String localName, String qName) throws SAXException { if (inModification && charBuf.length() > 0) { final String normalized = preserveWhitespace ? charBuf.toString() : charBuf.getNormalizedString(FastStringBuffer.SUPPRESS_BOTH); if (normalized.length() > 0) { Text text = doc.createTextNode(charBuf.toString()); if (stack.isEmpty()) { contents.add(text); } else { Element last = (Element) stack.peek(); last.appendChild(text); } } charBuf.setLength(0); } if (XUPDATE_NS.equals(namespaceURI)) { if (IF.equals(localName)) { Conditional cond = (Conditional) conditionals.pop(); modifications.add(cond); } else if (localName.equals(ELEMENT)) { this.resetWhitespaceHandling((Element) stack.pop()); } else if (localName.equals(ATTRIBUTE)) { inAttribute = false; } else if (localName.equals(APPEND) || localName.equals(UPDATE) || localName.equals(REMOVE) || localName.equals(RENAME) || localName.equals(REPLACE) || localName.equals(INSERT_BEFORE) || localName.equals(INSERT_AFTER)) { inModification = false; modification.setContent(contents); modification.setAccessContext(accessCtx); if(!conditionals.isEmpty()) { Conditional cond = (Conditional) conditionals.peek(); cond.addModification(modification); } else { modifications.add(modification); } modification = null; } } else if (inModification) { this.resetWhitespaceHandling((Element) stack.pop()); } } /** * @see org.xml.sax.ContentHandler#characters(char[], int, int) */ public void characters(char[] ch, int start, int length) throws SAXException { if (inModification) { if (inAttribute) { Attr attr = (Attr)currentNode; String val = attr.getValue(); if(val == null) val = new String(ch, start, length); else val += new String(ch, start, length); attr.setValue(val); } else { charBuf.append(ch, start, length); } } } /** * @see org.xml.sax.ContentHandler#ignorableWhitespace(char[], int, int) */ public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException { if (this.preserveWhitespace) { if (this.inModification) { if (this.inAttribute) { Attr attr = (Attr) this.currentNode; String val = attr.getValue(); if(val == null) val = new String(ch, start, length); else val += new String(ch, start, length); attr.setValue(val); } else { this.charBuf.append(ch, start, length); } } } } private void setWhitespaceHandling(Element e) { String wsSetting = e.getAttributeNS(Namespaces.XML_NS, "space"); if ("preserve".equals(wsSetting)) { this.spaceStack.push(wsSetting); this.preserveWhitespace = true; } else if ("default".equals(wsSetting)) { this.spaceStack.push(wsSetting); this.preserveWhitespace = preserveWhitespaceTemp; } // Otherwise, don't change what's currently in effect! } private void resetWhitespaceHandling(Element e) { String wsSetting = e.getAttributeNS(Namespaces.XML_NS, "space"); if ("preserve".equals(wsSetting) || "default".equals(wsSetting)) { // Since an opinion was expressed, restore what was previously set: this.spaceStack.pop(); if (0 == this.spaceStack.size()) { // This is the default... this.preserveWhitespace = preserveWhitespaceTemp; } else { this.preserveWhitespace = ("preserve".equals(this.spaceStack.peek())); } } } /** * @see org.xml.sax.ContentHandler#processingInstruction(java.lang.String, java.lang.String) */ public void processingInstruction(String target, String data) throws SAXException { if (inModification && charBuf.length() > 0) { final String normalized = charBuf.getNormalizedString(FastStringBuffer.SUPPRESS_BOTH); if (normalized.length() > 0) { Text text = doc.createTextNode(normalized); if (stack.isEmpty()) { if (LOG.isDebugEnabled()) LOG.debug("appending text to fragment: " + text.getData()); contents.add(text); } else { Element last = (Element) stack.peek(); last.appendChild(text); } } charBuf.setLength(0); } if (inModification) { ProcessingInstruction pi = doc.createProcessingInstruction(target, data); if (stack.isEmpty()) { contents.add(pi); } else { Element last = (Element) stack.peek(); last.appendChild(pi); } } } /** * @see org.xml.sax.ContentHandler#skippedEntity(java.lang.String) */ public void skippedEntity(String name) throws SAXException { } private void createVariable(String name, String select) throws SAXException { if (LOG.isDebugEnabled()) LOG.debug("creating variable " + name + " as " + select); Sequence result = processQuery(select); if (LOG.isDebugEnabled()) LOG.debug("found " + result.getItemCount() + " for variable " + name); variables.put(name, result); } private Sequence processQuery(String select) throws SAXException { XQueryContext context = null; try { context = new XQueryContext(broker, accessCtx); context.setStaticallyKnownDocuments(documentSet); Map.Entry entry; for (Iterator i = namespaces.entrySet().iterator(); i.hasNext();) { entry = (Map.Entry) i.next(); context.declareNamespace( (String) entry.getKey(), (String) entry.getValue()); } for (Iterator i = variables.entrySet().iterator(); i.hasNext(); ) { entry = (Map.Entry) i.next(); context.declareVariable(entry.getKey().toString(), entry.getValue()); } // TODO(pkaminsk2): why replicate XQuery.compile here? XQueryLexer lexer = new XQueryLexer(context, new StringReader(select)); XQueryParser parser = new XQueryParser(lexer); XQueryTreeParser treeParser = new XQueryTreeParser(context); parser.xpath(); if (parser.foundErrors()) { throw new SAXException(parser.getErrorMessage()); } AST ast = parser.getAST(); if (LOG.isDebugEnabled()) LOG.debug("generated AST: " + ast.toStringTree()); PathExpr expr = new PathExpr(context); treeParser.xpath(ast, expr); if (treeParser.foundErrors()) { throw new SAXException(treeParser.getErrorMessage()); } expr.analyze(new AnalyzeContextInfo()); Sequence seq = expr.eval(null, null); return seq; } catch (RecognitionException e) { LOG.warn("error while creating variable", e); throw new SAXException(e); } catch (TokenStreamException e) { LOG.warn("error while creating variable", e); throw new SAXException(e); } catch (XPathException e) { throw new SAXException(e); } finally { if (context != null) context.reset(false); } } /* (non-Javadoc) * @see org.xml.sax.ext.LexicalHandler#comment(char[], int, int) */ public void comment(char[] ch, int start, int length) throws SAXException { if (inModification && charBuf.length() > 0) { final String normalized = charBuf.getNormalizedString(FastStringBuffer.SUPPRESS_BOTH); if (normalized.length() > 0) { Text text = doc.createTextNode(normalized); if (stack.isEmpty()) { //LOG.debug("appending text to fragment: " + text.getData()); contents.add(text); } else { Element last = (Element) stack.peek(); last.appendChild(text); } } charBuf.setLength(0); } if (inModification) { Comment comment = doc.createComment(new String(ch, start, length)); if (stack.isEmpty()) { contents.add(comment); } else { Element last = (Element) stack.peek(); last.appendChild(comment); } } } /* (non-Javadoc) * @see org.xml.sax.ext.LexicalHandler#endCDATA() */ public void endCDATA() throws SAXException { } /* (non-Javadoc) * @see org.xml.sax.ext.LexicalHandler#endDTD() */ public void endDTD() throws SAXException { } /* (non-Javadoc) * @see org.xml.sax.ext.LexicalHandler#endEntity(java.lang.String) */ public void endEntity(String name) throws SAXException { } /* (non-Javadoc) * @see org.xml.sax.ext.LexicalHandler#startCDATA() */ public void startCDATA() throws SAXException { } /* (non-Javadoc) * @see org.xml.sax.ext.LexicalHandler#startDTD(java.lang.String, java.lang.String, java.lang.String) */ public void startDTD(String name, String publicId, String systemId) throws SAXException { } /* (non-Javadoc) * @see org.xml.sax.ext.LexicalHandler#startEntity(java.lang.String) */ public void startEntity(String name) throws SAXException { } public void reset() { this.preserveWhitespace = false; this.spaceStack = null; this.inModification = false; this.inAttribute = false; this.modification = null; this.doc = null; this.contents = null; this.stack.clear(); this.currentNode = null; this.broker = null; this.documentSet = null; this.modifications.clear(); this.charBuf = new FastStringBuffer(64); this.variables.clear(); this.namespaces.clear(); this.conditionals.clear(); //this.namespaces.put("xml", Namespaces.XML_NS); } }