/*
* eXist Open Source Native XML Database
* Copyright (C) 2001-06 Wolfgang M. Meier
* wolfgang@exist-db.org
* http://exist.sourceforge.net
*
* 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.dom.memtree;
import org.apache.xml.utils.XMLChar;
import org.apache.xml.utils.XML11Char;
import org.exist.collections.Collection;
import org.exist.dom.INode;
import org.exist.dom.QName;
import org.exist.dom.persistent.DocumentSet;
import org.exist.dom.persistent.EmptyNodeSet;
import org.exist.dom.persistent.NodeHandle;
import org.exist.dom.persistent.NodeSet;
import org.exist.numbering.NodeId;
import org.exist.storage.DBBroker;
import org.exist.storage.serializers.Serializer;
import org.exist.util.serializer.Receiver;
import org.exist.xquery.Constants;
import org.exist.xquery.NodeTest;
import org.exist.xquery.XQueryContext;
import org.exist.xquery.XPathException;
import org.exist.xquery.value.AtomicValue;
import org.exist.xquery.Cardinality;
import org.exist.xquery.value.Item;
import org.exist.xquery.value.MemoryNodeSet;
import org.exist.xquery.value.NodeValue;
import org.exist.xquery.value.Sequence;
import org.exist.xquery.value.SequenceIterator;
import org.exist.xquery.value.StringValue;
import org.exist.xquery.value.Type;
import org.exist.xquery.value.UntypedAtomicValue;
import org.exist.xquery.value.ValueSequence;
import org.xml.sax.ContentHandler;
import org.w3c.dom.DOMException;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.UserDataHandler;
import org.xml.sax.SAXException;
import org.xml.sax.ext.LexicalHandler;
import javax.xml.XMLConstants;
import java.util.Iterator;
import java.util.Properties;
public abstract class NodeImpl<T extends NodeImpl> implements INode<DocumentImpl, T>, NodeValue {
public static final short REFERENCE_NODE = 100;
public static final short NAMESPACE_NODE = 101;
protected int nodeNumber;
protected DocumentImpl document;
public NodeImpl(final DocumentImpl doc, final int nodeNumber) {
this.document = doc;
this.nodeNumber = nodeNumber;
}
public int getNodeNumber() {
return nodeNumber;
}
@Override
public int getImplementationType() {
return NodeValue.IN_MEMORY_NODE;
}
@Override
public DocumentSet getDocumentSet() {
return DocumentSet.EMPTY_DOCUMENT_SET;
}
@Override
public Iterator<Collection> getCollectionIterator() {
return EmptyNodeSet.EMPTY_COLLECTION_ITERATOR;
}
@Override
public Node getNode() {
return this;
}
@Override
public final QName getQName() {
switch(getNodeType()) {
case Node.ATTRIBUTE_NODE:
return document.attrName[nodeNumber];
case Node.ELEMENT_NODE:
case Node.PROCESSING_INSTRUCTION_NODE:
return document.nodeName[nodeNumber];
case NodeImpl.NAMESPACE_NODE:
return document.namespaceCode[nodeNumber];
case Node.DOCUMENT_NODE:
return QName.EMPTY_QNAME;
case Node.COMMENT_NODE:
return QName.EMPTY_QNAME;
case Node.TEXT_NODE:
return QName.EMPTY_QNAME;
case Node.CDATA_SECTION_NODE:
return QName.EMPTY_QNAME;
default:
return null;
}
}
@Override
public final void setQName(final QName qname) {
switch(getNodeType()) {
case Node.ATTRIBUTE_NODE:
document.attrName[nodeNumber] = qname;
break;
case Node.ELEMENT_NODE:
case Node.PROCESSING_INSTRUCTION_NODE:
document.nodeName[nodeNumber] = qname;
break;
case NodeImpl.NAMESPACE_NODE:
document.namespaceCode[nodeNumber] = qname;
break;
}
}
@Override
public final String getNodeName() {
switch(getType()) {
case Type.DOCUMENT:
return "#document";
case Type.ELEMENT:
case Type.ATTRIBUTE:
case Type.PROCESSING_INSTRUCTION:
case Type.NAMESPACE:
return getQName().getStringValue();
case Type.TEXT:
return "#text";
case Type.COMMENT:
return "#comment";
case Type.CDATA_SECTION:
return "#cdata-section";
default:
return "#unknown";
}
}
@Override
public String getLocalName() {
switch(getNodeType()) {
case Node.ELEMENT_NODE:
case Node.ATTRIBUTE_NODE:
return getQName().getLocalPart();
case Node.PROCESSING_INSTRUCTION_NODE:
final QName qname = getQName();
return qname != null ? qname.getLocalPart() : null;
default:
return null;
}
}
@Override
public String getNamespaceURI() {
switch(getNodeType()) {
case Node.ELEMENT_NODE:
case Node.ATTRIBUTE_NODE:
return getQName().getNamespaceURI();
case NodeImpl.NAMESPACE_NODE:
return XMLConstants.XMLNS_ATTRIBUTE_NS_URI;
default:
return null;
}
}
@Override
public final String getPrefix() {
switch(getNodeType()) {
case Node.ELEMENT_NODE:
case Node.ATTRIBUTE_NODE:
case NodeImpl.NAMESPACE_NODE:
return getQName().getPrefix();
default:
return null;
}
}
@Override
public void setPrefix(final String prefix) throws DOMException {
if(prefix == null) {
return;
} else if(getOwnerDocument().getXmlVersion().equals("1.0") && !XMLChar.isValidNCName(prefix)) {
throw new DOMException(DOMException.INVALID_CHARACTER_ERR, "Prefix '" + prefix + "' in XML 1.0 contains invalid characters");
} else if(getOwnerDocument().getXmlVersion().equals("1.1") && !XML11Char.isXML11ValidNCName(prefix)) {
throw new DOMException(DOMException.INVALID_CHARACTER_ERR, "Prefix '" + prefix + "' in XML 1.1 contains invalid characters");
} else if(getNamespaceURI() == null) {
throw new DOMException(DOMException.NAMESPACE_ERR, "Cannot set prefix when namespace is null");
} else if(prefix.equals(XMLConstants.XML_NS_PREFIX) && !getNamespaceURI().equals(XMLConstants.XML_NS_URI)) {
throw new DOMException(DOMException.NAMESPACE_ERR, "Prefix '" + XMLConstants.XML_NS_PREFIX + "' is invalid for namespace '" + getNamespaceURI() + "'");
} else if(getNodeType() == Node.ATTRIBUTE_NODE && prefix.equals(XMLConstants.XMLNS_ATTRIBUTE) && !getNamespaceURI().equals(XMLConstants.XMLNS_ATTRIBUTE_NS_URI)) {
throw new DOMException(DOMException.NAMESPACE_ERR, "Prefix '" + XMLConstants.XMLNS_ATTRIBUTE + "' is invalid for namespace '" + getNamespaceURI() + "'");
} else if(getNodeType() == Node.ELEMENT_NODE || getNodeType() == Node.ATTRIBUTE_NODE) {
final QName qname = getQName();
setQName(new QName(qname.getLocalPart(), qname.getNamespaceURI(), prefix, qname.getNameType()));
}
}
@Override
public NodeId getNodeId() {
expand();
return document.nodeId[nodeNumber];
}
public void expand() throws DOMException {
document.expand();
}
public void deepCopy() throws DOMException {
final DocumentImpl newDoc = document.expandRefs(this);
if(newDoc != document) {
// we received a new document
this.nodeNumber = 1;
this.document = newDoc;
}
}
@Override
public String getNodeValue() throws DOMException {
throw unsupported();
}
@Override
public void setNodeValue(final String nodeValue) throws DOMException {
throw unsupported();
}
@Override
public short getNodeType() {
//Workaround for fn:string-length(fn:node-name(document {""}))
if(this.document == null) {
return Node.DOCUMENT_NODE;
}
return document.nodeKind[nodeNumber];
}
@Override
public Node getParentNode() {
int next = document.next[nodeNumber];
while(next > nodeNumber) {
next = document.next[next];
}
if(next < 0) {
return null;
}
return document.getNode(next);
}
public Node selectParentNode() {
// as getParentNode() but doesn't return the document itself
if(nodeNumber == 0) {
return null;
}
int next = document.next[nodeNumber];
while(next > nodeNumber) {
next = document.next[next];
}
if(next < 0) { //Is this even possible ?
return null;
}
if(next == 0) {
return this.document.explicitlyCreated ? this.document : null;
}
return document.getNode(next);
}
@Override
public void addContextNode(final int contextId, final NodeValue node) {
throw unsupported();
}
@Override
public boolean equals(final Object other) {
if(!(other instanceof NodeImpl)) {
return false;
}
final NodeImpl o = (NodeImpl) other;
return document == o.document && nodeNumber == o.nodeNumber &&
getNodeType() == o.getNodeType();
}
@Override
public boolean equals(final NodeValue other) throws XPathException {
if(other.getImplementationType() != NodeValue.IN_MEMORY_NODE) {
return false;
}
final NodeImpl o = (NodeImpl) other;
return document == o.document && nodeNumber == o.nodeNumber &&
getNodeType() == o.getNodeType();
}
@Override
public boolean after(final NodeValue other, final boolean isFollowing) throws XPathException {
if(other.getImplementationType() != NodeValue.IN_MEMORY_NODE) {
throw new XPathException("cannot compare persistent node with in-memory node");
}
return nodeNumber > ((NodeImpl) other).nodeNumber;
}
@Override
public boolean before(final NodeValue other, final boolean isPreceding) throws XPathException {
if(other.getImplementationType() != NodeValue.IN_MEMORY_NODE) {
throw new XPathException("cannot compare persistent node with in-memory node");
}
return nodeNumber < ((NodeImpl)other).nodeNumber;
}
@Override
public int compareTo(final NodeImpl other) {
if(other.document == document) {
if(nodeNumber == other.nodeNumber && getNodeType() == other.getNodeType()) {
return Constants.EQUAL;
} else if(nodeNumber < other.nodeNumber) {
return Constants.INFERIOR;
} else {
return Constants.SUPERIOR;
}
} else if(document.docId < other.document.docId) {
return Constants.INFERIOR;
} else {
return Constants.SUPERIOR;
}
}
@Override
public Sequence tail() throws XPathException {
return Sequence.EMPTY_SEQUENCE;
}
@Override
public NodeList getChildNodes() {
throw unsupported();
}
@Override
public Node getFirstChild() {
throw unsupported();
}
@Override
public Node getLastChild() {
throw unsupported();
}
@Override
public Node getPreviousSibling() {
if(nodeNumber == 0) {
return null;
}
final int parent = document.getParentNodeFor(nodeNumber);
int nextNode = document.getFirstChildFor(parent);
while((nextNode >= parent) && (nextNode < nodeNumber)) {
final int following = document.next[nextNode];
if(following == nodeNumber) {
return document.getNode(nextNode);
}
nextNode = following;
}
return null;
}
@Override
public Node getNextSibling() {
final int nextNr = document.next[nodeNumber];
return nextNr < nodeNumber ? null : document.getNode(nextNr);
}
@Override
public NamedNodeMap getAttributes() {
throw unsupported();
}
@Override
public DocumentImpl getOwnerDocument() {
return document;
}
@Override
public Node insertBefore(final Node newChild, final Node refChild) throws DOMException {
throw unsupported();
}
@Override
public Node replaceChild(final Node newChild, final Node oldChild) throws DOMException {
throw unsupported();
}
@Override
public Node removeChild(final Node oldChild) throws DOMException {
throw unsupported();
}
@Override
public Node appendChild(final Node newChild) throws DOMException {
throw unsupported();
}
@Override
public boolean hasChildNodes() {
return false;
}
@Override
public Node cloneNode(final boolean deep) {
throw unsupported();
}
@Override
public void normalize() {
}
@Override
public boolean isSupported(final String feature, final String version) {
throw unsupported();
}
@Override
public boolean hasAttributes() {
throw unsupported();
}
@Override
public int getType() {
//Workaround for fn:string-length(fn:node-name(document {""}))
if(this.document == null) {
return Type.DOCUMENT;
}
switch(getNodeType()) {
case Node.DOCUMENT_NODE:
return Type.DOCUMENT;
case Node.COMMENT_NODE:
return Type.COMMENT;
case Node.PROCESSING_INSTRUCTION_NODE:
return Type.PROCESSING_INSTRUCTION;
case Node.ELEMENT_NODE:
return Type.ELEMENT;
case Node.ATTRIBUTE_NODE:
return Type.ATTRIBUTE;
case Node.TEXT_NODE:
return Type.TEXT;
case Node.CDATA_SECTION_NODE:
return Type.CDATA_SECTION;
default:
return Type.NODE;
}
}
@Override
public String getStringValue() {
final int level = document.treeLevel[nodeNumber];
int next = nodeNumber + 1;
int startOffset = 0;
int len = -1;
while(next < document.size && document.treeLevel[next] > level) {
if(
(document.nodeKind[next] == Node.TEXT_NODE)
|| (document.nodeKind[next] == Node.CDATA_SECTION_NODE)
|| (document.nodeKind[next] == Node.PROCESSING_INSTRUCTION_NODE)
) {
if(len < 0) {
startOffset = document.alpha[next];
len = document.alphaLen[next];
} else {
len += document.alphaLen[next];
}
} else {
return getStringValueSlow();
}
++next;
}
return len < 0 ? "" : new String(document.characters, startOffset, len);
}
private String getStringValueSlow() {
final int level = document.treeLevel[nodeNumber];
StringBuilder buf = null;
int next = nodeNumber + 1;
while(next < document.size && document.treeLevel[next] > level) {
switch(document.nodeKind[next]) {
case Node.TEXT_NODE:
case Node.CDATA_SECTION_NODE:
case Node.PROCESSING_INSTRUCTION_NODE: {
if(buf == null) {
buf = new StringBuilder();
}
buf.append(document.characters, document.alpha[next], document.alphaLen[next]);
break;
}
case REFERENCE_NODE: {
if(buf == null) {
buf = new StringBuilder();
}
buf.append(document.references[document.alpha[next]].getStringValue());
break;
}
}
++next;
}
return ((buf == null) ? "" : buf.toString());
}
@Override
public Sequence toSequence() {
return this;
}
@Override
public AtomicValue convertTo(final int requiredType) throws XPathException {
return UntypedAtomicValue.convertTo(null, getStringValue(), requiredType);
}
@Override
public AtomicValue atomize() throws XPathException {
return new UntypedAtomicValue(getStringValue());
}
@Override
public boolean isEmpty() {
return false;
}
@Override
public boolean hasOne() {
return true;
}
@Override
public boolean hasMany() {
return false;
}
@Override
public void add(final Item item) throws XPathException {
throw unsupported();
}
@Override
public void addAll(final Sequence other) throws XPathException {
throw unsupported();
}
@Override
public int getItemType() {
return Type.NODE;
}
@Override
public SequenceIterator iterate() throws XPathException {
return new SingleNodeIterator(this);
}
@Override
public SequenceIterator unorderedIterator() {
return new SingleNodeIterator(this);
}
@Override
public int getItemCount() {
return 1;
}
@Override
public int getCardinality() {
return Cardinality.EXACTLY_ONE;
}
@Override
public Item itemAt(final int pos) {
return pos == 0 ? this : null;
}
@Override
public boolean effectiveBooleanValue() throws XPathException {
//A node evaluates to true()
return true;
}
@Override
public NodeSet toNodeSet() throws XPathException {
final ValueSequence seq = new ValueSequence();
seq.add(this);
return seq.toNodeSet();
}
@Override
public MemoryNodeSet toMemNodeSet() throws XPathException {
return new ValueSequence(this).toMemNodeSet();
}
@Override
public void toSAX(final DBBroker broker, final ContentHandler handler, final Properties properties)
throws SAXException {
final Serializer serializer = broker.getSerializer();
serializer.reset();
serializer.setProperty(Serializer.GENERATE_DOC_EVENTS, "false");
if(properties != null) {
serializer.setProperties(properties);
}
if(handler instanceof LexicalHandler) {
serializer.setSAXHandlers(handler, (LexicalHandler) handler);
} else {
serializer.setSAXHandlers(handler, null);
}
serializer.toSAX(this);
}
@Override
public void copyTo(final DBBroker broker, final DocumentBuilderReceiver receiver) throws SAXException {
//Null test for document nodes
if(document != null) {
document.copyTo(this, receiver);
}
}
public void streamTo(final Serializer serializer, final Receiver receiver) throws SAXException {
//Null test for document nodes
if(document != null) {
document.streamTo(serializer, this, receiver);
}
}
@Override
public int conversionPreference(final Class<?> javaClass) {
final int preference;
if(javaClass.isAssignableFrom(NodeImpl.class)) {
preference = 0;
} else if(javaClass.isAssignableFrom(Node.class)) {
preference = 1;
} else if((javaClass == String.class) || (javaClass == CharSequence.class)) {
preference = 2;
} else if((javaClass == Character.class) || (javaClass == char.class)) {
preference = 2;
} else if((javaClass == Double.class) || (javaClass == double.class)) {
preference = 10;
} else if((javaClass == Float.class) || (javaClass == float.class)) {
preference = 11;
} else if((javaClass == Long.class) || (javaClass == long.class)) {
preference = 12;
} else if((javaClass == Integer.class) || (javaClass == int.class)) {
preference = 13;
} else if((javaClass == Short.class) || (javaClass == short.class)) {
preference = 14;
} else if((javaClass == Byte.class) || (javaClass == byte.class)) {
preference = 15;
} else if((javaClass == Boolean.class) || (javaClass == boolean.class)) {
preference = 16;
} else if(javaClass == Object.class) {
preference = 20;
} else {
preference = Integer.MAX_VALUE;
}
return preference;
}
@Override
public <T> T toJavaObject(final Class<T> target) throws XPathException {
if(target.isAssignableFrom(NodeImpl.class) || target.isAssignableFrom(Node.class) || target == Object.class) {
return (T) this;
} else {
final StringValue v = new StringValue(getStringValue());
return v.toJavaObject(target);
}
}
@Override
public void setSelfAsContext(final int contextId) {
throw unsupported();
}
@Override
public boolean isCached() {
// always return false
return false;
}
@Override
public void setIsCached(final boolean cached) {
throw unsupported();
}
@Override
public void removeDuplicates() {
}
@Override
public String getBaseURI() {
return null;
}
@Override
public void destroy(final XQueryContext context, final Sequence contextSequence) {
}
public abstract void selectAttributes(final NodeTest test, final Sequence result) throws XPathException;
public abstract void selectDescendantAttributes(final NodeTest test, final Sequence result) throws XPathException;
public abstract void selectChildren(final NodeTest test, final Sequence result) throws XPathException;
public void selectDescendants(final boolean includeSelf, final NodeTest test, final Sequence result)
throws XPathException {
if(includeSelf && test.matches(this)) {
result.add(this);
}
}
public void selectAncestors(final boolean includeSelf, final NodeTest test, final Sequence result)
throws XPathException {
if(nodeNumber < 1) {
return;
}
if(includeSelf) {
final NodeImpl n = document.getNode(nodeNumber);
if(test.matches(n)) {
result.add(n);
}
}
int nextNode = document.getParentNodeFor(nodeNumber);
while(nextNode > 0) {
final NodeImpl n = document.getNode(nextNode);
if(test.matches(n)) {
result.add(n);
}
nextNode = document.getParentNodeFor(nextNode);
}
}
public void selectPrecedingSiblings(final NodeTest test, final Sequence result)
throws XPathException {
final int parent = document.getParentNodeFor(nodeNumber);
int nextNode = document.getFirstChildFor(parent);
while((nextNode >= parent) && (nextNode < nodeNumber)) {
final NodeImpl n = document.getNode(nextNode);
if(test.matches(n)) {
result.add(n);
}
nextNode = document.next[nextNode];
}
}
public void selectPreceding(final NodeTest test, final Sequence result, final int position)
throws XPathException {
final NodeId myNodeId = getNodeId();
int count = 0;
for(int i = nodeNumber - 1; i > 0; i--) {
final NodeImpl n = document.getNode(i);
if(!myNodeId.isDescendantOf(n.getNodeId()) && test.matches(n)) {
if((position < 0) || (++count == position)) {
result.add(n);
}
if(count == position) {
break;
}
}
}
}
public void selectFollowingSiblings(final NodeTest test, final Sequence result)
throws XPathException {
final int parent = document.getParentNodeFor(nodeNumber);
if(parent == 0) {
// parent is the document node
if(getNodeType() == Node.ELEMENT_NODE) {
return;
}
NodeImpl next = (NodeImpl) getNextSibling();
while(next != null) {
if(test.matches(next)) {
result.add(next);
}
if(next.getNodeType() == Node.ELEMENT_NODE) {
break;
}
next = (NodeImpl) next.getNextSibling();
}
} else {
int nextNode = document.getFirstChildFor(parent);
while(nextNode > parent) {
final NodeImpl n = document.getNode(nextNode);
if((nextNode > nodeNumber) && test.matches(n)) {
result.add(n);
}
nextNode = document.next[nextNode];
}
}
}
public void selectFollowing(final NodeTest test, final Sequence result, final int position)
throws XPathException {
final int parent = document.getParentNodeFor(nodeNumber);
if(parent == 0) {
// parent is the document node
if(getNodeType() == Node.ELEMENT_NODE) {
return;
}
NodeImpl next = (NodeImpl) getNextSibling();
while(next != null) {
if(test.matches(next)) {
next.selectDescendants(true, test, result);
}
if(next.getNodeType() == Node.ELEMENT_NODE) {
break;
}
next = (NodeImpl) next.getNextSibling();
}
} else {
final NodeId myNodeId = getNodeId();
int count = 0;
int nextNode = nodeNumber + 1;
while(nextNode < document.size) {
final NodeImpl n = document.getNode(nextNode);
if(!n.getNodeId().isDescendantOf(myNodeId) && test.matches(n)) {
if((position < 0) || (++count == position)) {
result.add(n);
}
if(count == position) {
break;
}
}
nextNode++;
}
}
}
public boolean matchAttributes(final NodeTest test) {
// do nothing
return false;
}
public boolean matchDescendantAttributes(final NodeTest test) throws XPathException {
// do nothing
return false;
}
public boolean matchChildren(final NodeTest test) throws XPathException {
return false;
}
public boolean matchDescendants(final boolean includeSelf, final NodeTest test) throws XPathException {
return includeSelf && test.matches(this);
}
@Override
public short compareDocumentPosition(final Node other) throws DOMException {
throw unsupported();
}
@Override
public String getTextContent() throws DOMException {
throw unsupported();
}
@Override
public void setTextContent(final String textContent) throws DOMException {
throw unsupported();
}
@Override
public boolean isSameNode(final Node other) {
throw unsupported();
}
@Override
public String lookupPrefix(final String namespaceURI) {
throw unsupported();
}
@Override
public boolean isDefaultNamespace(final String namespaceURI) {
throw unsupported();
}
@Override
public String lookupNamespaceURI(final String prefix) {
throw unsupported();
}
@Override
public boolean isEqualNode(final Node arg) {
throw unsupported();
}
@Override
public Object getFeature(final String feature, final String version) {
throw unsupported();
}
@Override
public Object setUserData(final String key, final Object data, final UserDataHandler handler) {
throw unsupported();
}
@Override
public Object getUserData(final String key) {
throw unsupported();
}
@Override
public boolean isPersistentSet() {
return false;
}
@Override
public void nodeMoved(final NodeId oldNodeId, final NodeHandle newNode) {
}
@Override
public void clearContext(final int contextId) {
}
@Override
public int getState() {
return 0;
}
@Override
public boolean isCacheable() {
return true;
}
@Override
public boolean hasChanged(final int previousState) {
return false;
}
private UnsupportedOperationException unsupported() {
return new UnsupportedOperationException("Operation is unsupported on node type: " + this.getNodeType());
}
private final static class SingleNodeIterator implements SequenceIterator {
NodeImpl node;
public SingleNodeIterator(final NodeImpl node) {
this.node = node;
}
@Override
public boolean hasNext() {
return node != null;
}
@Override
public Item nextItem() {
final NodeImpl next = node;
node = null;
return next;
}
}
}