package org.exist.fluent; import java.io.IOException; import java.util.*; import javax.xml.datatype.*; import org.exist.collections.*; import org.exist.collections.triggers.*; import org.exist.collections.triggers.Trigger; import org.exist.dom.*; import org.exist.storage.io.VariableByteOutputStream; import org.exist.xquery.XPathException; import org.exist.xquery.value.*; import org.w3c.dom.*; /** * A node in the database. Nodes are most often contained in XML documents, but can also * be transient in-memory nodes created by a query. * * @author <a href="mailto:piotr@ideanest.com">Piotr Kaminski</a> */ public class Node extends Item { private XMLDocument document; final StaleMarker staleMarker = new StaleMarker(); private Node() {} Node(org.exist.xquery.value.NodeValue item, NamespaceMap namespaceBindings, Database db) { super(item, namespaceBindings, db); if (item instanceof NodeProxy) { NodeProxy proxy = (NodeProxy) item; String docPath = proxy.getDocument().getURI().getCollectionPath(); staleMarker.track(docPath.substring(0, docPath.lastIndexOf('/'))); // folder staleMarker.track(docPath); // document staleMarker.track(docPath + "#" + proxy.getNodeId()); // node } } @Override Sequence convertToSequence() { staleMarker.check(); return super.convertToSequence(); } public boolean extant() { return !staleMarker.stale(); } org.w3c.dom.Node getDOMNode() { staleMarker.check(); try { org.w3c.dom.Node domNode = ((NodeValue) item).getNode(); if (domNode == null) throw new DatabaseException("unable to load node data"); return domNode; } catch (org.exist.util.sanity.AssertFailure e) { throw new DatabaseException(e); } } /** * Return this node. * * @return this node */ @Override public Node node() { return this; } @Override public Comparable<Object> comparableValue() { throw new DatabaseException("nodes are not comparable"); } /** * Return whether this node represents the same node in the database as the given object. */ @Override public boolean equals(Object o) { if (!(o instanceof Node)) return false; Node that = (Node) o; if (item == that.item) return true; if (this.item instanceof NodeProxy && that.item instanceof NodeProxy) { try { return ((NodeProxy) this.item).equals((NodeProxy) that.item); } catch (XPathException e) { // fall through to return false below } } return false; } /** * Warning: computing a node's hash code is surprisingly expensive, and the value is not cached. * You should not use nodes in situations where they might get hashed. */ @Override public int hashCode() { return computeHashCode(); } private int computeHashCode() { if (item instanceof NodeProxy) { NodeProxy proxy = (NodeProxy) item; VariableByteOutputStream buf = new VariableByteOutputStream(); try { proxy.getNodeId().write(buf); } catch (IOException e) { throw new RuntimeException("unable to serialize node's id to compute hashCode", e); } return proxy.getDocument().getURI().hashCode() ^ Arrays.hashCode(buf.toByteArray()); } else { return item.hashCode(); } } /** * Return the namespace bindings in force in the scope of this node. Only works on nodes * that are XML elements. Namespaces reserved by the XML spec, and implicitly in scope * for all XML elements, are not reported. * * @return the namespace bindings in force for this node */ public NamespaceMap inScopeNamespaces() { NamespaceMap namespaceMap = new NamespaceMap(); for (Iterator<String> it = query().all( "for $prefix in in-scope-prefixes($_1) return ($prefix, namespace-uri-for-prefix($prefix, $_1))", this).values().iterator(); it.hasNext(); ) { String prefix = it.next(), namespace = it.next(); if (!NamespaceMap.isReservedPrefix(prefix)) namespaceMap.put(prefix, namespace); } return namespaceMap; } /** * Compare the order of two nodes in a document. * * @param node the node to compare this one to * @return node 0 if this node is the same as the given node, a value less than 0 if it precedes the * given node in the document, and a value great than 0 if it follows the given node in the document * @throws DatabaseException if this node and the given one are not in the same document */ public int compareDocumentOrderTo(Node node) { if (this.item == node.item) return 0; NodeValue nv1 = (NodeValue) this.item, nv2 = (NodeValue) node.item; if (nv1.getImplementationType() != nv2.getImplementationType()) throw new DatabaseException("can't compare different node types, since they can never be in the same document"); if (nv1.getImplementationType() == NodeValue.PERSISTENT_NODE) { NodeProxy n1 = (NodeProxy) item, n2 = (NodeProxy) node.item; if (n1.getDocument().getDocId() != n2.getDocument().getDocId()) throw new DatabaseException("can't compare document order of nodes in disparate documents: this node is in " + document() + " and the argument node in " + node.document()); if (n1.getNodeId().equals(n2.getNodeId())) return 0; try { return n1.before(n2, false) ? -1 : +1; } catch (XPathException e) { throw new DatabaseException("unable to compare nodes", e); } } else if (nv1.getImplementationType() == NodeValue.IN_MEMORY_NODE) { org.exist.memtree.NodeImpl n1 = (org.exist.memtree.NodeImpl) nv1, n2 = (org.exist.memtree.NodeImpl) nv2; if (n1.getDocument() != n2.getDocument()) throw new DatabaseException("can't compare document order of in-memory nodes created separately"); try { return n1.before(n2, false) ? -1 : +1; } catch (XPathException e) { throw new DatabaseException("unable to compare nodes", e); } } else { throw new DatabaseException("unknown node implementation type: " + nv1.getImplementationType()); } } /** * Return the document to which this node belongs. * * @return the document to which this node belongs * @throws UnsupportedOperationException if this node does not belong to a document */ public XMLDocument document() { staleMarker.check(); if (document == null) try { document = Document.newInstance(((NodeProxy) item).getDocument(), this).xml(); } catch (ClassCastException e) { throw new UnsupportedOperationException("node is not part of a document in the database"); } return document; } /** * Return a builder that will append elements to this node's children. The builder will return the * appended node if a single node was appended, otherwise <code>null</code>. * * @return a builder that will append nodes to this node */ public ElementBuilder<Node> append() { staleMarker.check(); // do an early check to fail-fast, we'll check again on completion try { final StoredNode node = (StoredNode) getDOMNode(); return new ElementBuilder<Node>(namespaceBindings, true, new ElementBuilder.CompletedCallback<Node>() { public Node completed(org.w3c.dom.Node[] nodes) { Transaction tx = db.requireTransactionWithBroker(); try { tx.lockWrite(node.getDocument()); DocumentTrigger trigger = fireTriggerBefore(tx); node.appendChildren(tx.tx, toNodeList(nodes), 0); StoredNode result = (StoredNode) node.getLastChild(); touchDefragAndFireTriggerAfter(tx, trigger); tx.commit(); if (result == null) return null; NodeProxy proxy = new NodeProxy((DocumentImpl) result.getOwnerDocument(), result.getNodeId(), result.getNodeType(), result.getInternalAddress()); return new Node(proxy, namespaceBindings.extend(), db); } catch (DOMException e) { throw new DatabaseException(e); } catch (TriggerException e) { throw new DatabaseException("append aborted by listener", e); } finally { tx.abortIfIncomplete(); } } }); } catch (ClassCastException e) { if (getDOMNode() instanceof org.exist.memtree.NodeImpl) { throw new UnsupportedOperationException("appends to in-memory nodes are not supported"); } else { throw new UnsupportedOperationException("cannot append to a " + Type.getTypeName(item.getType())); } } } /** * Delete this node from its parent. This can delete an element from a document, * or an attribute from an element, etc. Trying to delete the root element of a * document will delete the document instead. If the node cannot be found, assume * it's already been deleted and return silently. */ public void delete() { org.w3c.dom.Node child; try { child = getDOMNode(); } catch (DatabaseException e) { return; } NodeImpl parent = (NodeImpl) child.getParentNode(); if (child instanceof org.w3c.dom.Document || parent instanceof org.w3c.dom.Document) { document().delete(); } else if (parent == null) { throw new DatabaseException("cannot delete node with no parent"); } else { Transaction tx = db.requireTransactionWithBroker(); try { if (parent instanceof StoredNode) tx.lockWrite(((StoredNode) parent).getDocument()); DocumentTrigger trigger = fireTriggerBefore(tx); parent.removeChild(tx.tx, child); touchDefragAndFireTriggerAfter(tx, trigger); tx.commit(); } catch (DOMException e) { throw new DatabaseException(e); } catch (TriggerException e) { throw new DatabaseException("delete aborted by listener", e); } finally { tx.abortIfIncomplete(); } } } /** * Return the name of this node, in the "prefix:localName" form. * * @return the name of this node */ public String name() { return getDOMNode().getNodeName(); } /** * Return the qualified name of this node, including its namespace URI, local name and prefix. * * @return the qname of this node */ public QName qname() { org.w3c.dom.Node node = getDOMNode(); String localName = node.getLocalName(); if (localName == null) localName = node.getNodeName(); return new QName(node.getNamespaceURI(), localName, node.getPrefix()); } /** * Return a builder that will replace this node. The builder returns <code>null</code>. * * @return a builder that will replace this node * @throws UnsupportedOperationException if the node does not have a parent */ public ElementBuilder<?> replace() { // TODO: right now, can only replace an element; what about other nodes? // TODO: right now, can only replace with a single node, investigate multiple replace try { final NodeImpl oldNode = (NodeImpl) getDOMNode(); if (oldNode.getParentNode() == null) throw new UnsupportedOperationException("cannot replace a " + Type.getTypeName(item.getType()) + " with no parent"); if (oldNode.getParentNode().getNodeType() == org.w3c.dom.Node.DOCUMENT_NODE) return document().folder().documents().build(Name.overwrite(document().name())); return new ElementBuilder<Object>(namespaceBindings, false, new ElementBuilder.CompletedCallback<Object>() { public Object completed(org.w3c.dom.Node[] nodes) { assert nodes.length == 1; Transaction tx = db.requireTransactionWithBroker(); try { DocumentImpl doc = (DocumentImpl) oldNode.getOwnerDocument(); tx.lockWrite(doc); DocumentTrigger trigger = fireTriggerBefore(tx); ((NodeImpl) oldNode.getParentNode()).replaceChild(tx.tx, nodes[0], oldNode); touchDefragAndFireTriggerAfter(tx, trigger); tx.commit(); // no point in returning the old node; we'd rather return the newly inserted one, // but it's not easily available return null; } catch (DOMException e) { throw new DatabaseException(e); } catch (TriggerException e) { throw new DatabaseException("append aborted by listener", e); } finally { tx.abortIfIncomplete(); } } }); } catch (ClassCastException e) { if (getDOMNode() instanceof org.exist.memtree.NodeImpl) { throw new UnsupportedOperationException("replacement of in-memory nodes is not supported"); } else { throw new UnsupportedOperationException("cannot replace a " + Type.getTypeName(item.getType())); } } } /** * Return a builder for updating the attribute values of this element. * * @return an attribute builder for this element * @throws UnsupportedOperationException if this node is not an element */ public AttributeBuilder update() { try { final ElementImpl elem = (ElementImpl) getDOMNode(); return new AttributeBuilder(elem, namespaceBindings, new AttributeBuilder.CompletedCallback() { public void completed(NodeList removeList, NodeList addList) { Transaction tx = db.requireTransactionWithBroker(); try { DocumentImpl doc = (DocumentImpl) elem.getOwnerDocument(); tx.lockWrite(doc); DocumentTrigger trigger = fireTriggerBefore(tx); elem.removeAppendAttributes(tx.tx, removeList, addList); touchDefragAndFireTriggerAfter(tx, trigger); tx.commit(); } catch (TriggerException e) { throw new DatabaseException("append aborted by listener", e); } finally { tx.abortIfIncomplete(); } } }); } catch (ClassCastException e) { if (getDOMNode() instanceof org.exist.memtree.ElementImpl) { throw new UnsupportedOperationException("updates on in-memory nodes are not supported"); } else { throw new UnsupportedOperationException("cannot update attributes on a " + Type.getTypeName(item.getType())); } } } private DocumentTrigger fireTriggerBefore(Transaction tx) throws TriggerException { if (!(item instanceof NodeProxy)) return null; DocumentImpl docimpl = ((NodeProxy) item).getDocument(); try { CollectionConfiguration config = docimpl.getCollection().getConfiguration(tx.broker); if (config == null) return null; DocumentTrigger trigger = (DocumentTrigger) config.newTrigger(Trigger.UPDATE_DOCUMENT_EVENT, tx.broker, docimpl.getCollection()); if (trigger == null) return null; trigger.prepare(Trigger.UPDATE_DOCUMENT_EVENT, tx.broker, tx.tx, docimpl.getURI(), docimpl); return trigger; } catch (CollectionConfigurationException e) { throw new DatabaseException(e); } } private void touchDefragAndFireTriggerAfter(Transaction tx, DocumentTrigger trigger) { DocumentImpl doc = ((NodeProxy) item).getDocument(); doc.getMetadata().setLastModified(System.currentTimeMillis()); tx.broker.storeXMLResource(tx.tx, doc); if (item instanceof NodeProxy) Database.queueDefrag(((NodeProxy) item).getDocument()); if (trigger == null) return; DocumentImpl docimpl = ((NodeProxy) item).getDocument(); trigger.finish(Trigger.UPDATE_DOCUMENT_EVENT, tx.broker, tx.tx, docimpl.getURI(), docimpl); } static NodeList toNodeList(final org.w3c.dom.Node[] nodes) { return new NodeList() { public int getLength() {return nodes.length;} public org.w3c.dom.Node item(int index) {return nodes[index];} }; } /** * A null node, used as a placeholder where an actual <code>null</code> would be inappropriate. */ @SuppressWarnings("hiding") static final Node NULL = new Node() { @Override public ElementBuilder<Node> append() {throw new UnsupportedOperationException("cannot append to a null resource");} @Override public void delete() {} @Override public XMLDocument document() {throw new UnsupportedOperationException("null resource does not have a document");} @Override public String name() {throw new UnsupportedOperationException("null resource does not have a name");} @Override public QName qname() {throw new UnsupportedOperationException("null resource does not have a qname");} @Override public ElementBuilder<?> replace() {throw new UnsupportedOperationException("cannot replace a null resource");} @Override public AttributeBuilder update() {throw new UnsupportedOperationException("cannot update a null resource");} @Override public boolean booleanValue() {return Item.NULL.booleanValue();} @Override public double doubleValue() {return Item.NULL.doubleValue();} @Override public int intValue() {return Item.NULL.intValue();} @Override public long longValue() {return Item.NULL.longValue();} @Override public Duration durationValue() {return Item.NULL.durationValue();} @Override public XMLGregorianCalendar dateTimeValue() {return Item.NULL.dateTimeValue();} @Override public Date instantValue() {return Item.NULL.instantValue();} @Override public Node node() {return Item.NULL.node();} @Override public boolean extant() {return Item.NULL.extant();} @Override public QueryService query() {return Item.NULL.query();} @Override public String value() {return Item.NULL.value();} @Override public String valueWithDefault(String defaultValue) {return Item.NULL.value();} @Override Sequence convertToSequence() {return Item.NULL.convertToSequence();} @Override public String toString() { return "NULL Node";} }; }