Package org.exist.fluent

Source Code of org.exist.fluent.Node

package org.exist.fluent;

import java.io.IOException;
import java.util.*;

import javax.xml.datatype.*;

import org.exist.collections.Collection;
import org.exist.collections.triggers.*;
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(db, 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();
    Collection col = docimpl.getCollection();
   
    DocumentTrigger trigger = new DocumentTriggers(tx.broker, null, col, col.getConfiguration(tx.broker));
     
    trigger.beforeUpdateDocument(tx.broker, tx.tx, docimpl);

    return trigger;
  }
 
  private void touchDefragAndFireTriggerAfter(Transaction tx, DocumentTrigger trigger) throws TriggerException {
    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.afterUpdateDocument(tx.broker, tx.tx, 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.
   */
  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";}
  };

}
TOP

Related Classes of org.exist.fluent.Node

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.