/*
* eXist Open Source Native XML Database
* Copyright (C) 2001-2014 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* $Id$
*/
package org.exist.dom.persistent;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.exist.collections.Collection;
import org.exist.numbering.NodeId;
import org.exist.storage.DBBroker;
import org.exist.xquery.Constants;
import org.exist.xquery.Expression;
import org.exist.xquery.XPathException;
import org.exist.xquery.value.AbstractSequence;
import org.exist.xquery.value.Item;
import org.exist.xquery.value.MemoryNodeSet;
import org.exist.xquery.value.Sequence;
import org.exist.xquery.value.SequenceIterator;
import org.exist.xquery.value.Type;
import org.w3c.dom.Node;
import java.util.Iterator;
/**
* Abstract base class for all node set implementations. A node set is a special type of sequence,
* which contains only nodes. Class NodeSet thus implements the {@link org.exist.xquery.value.Sequence}
* as well as the DOM {@link org.w3c.dom.NodeList} interfaces.
*
* Please note that a node set may or may not contain duplicate nodes. Some implementations
* (e.g. {@link org.exist.dom.persistent.ExtArrayNodeSet}) remove duplicates when sorting the set.
*/
public abstract class AbstractNodeSet extends AbstractSequence implements NodeSet {
protected static final Logger LOG = LogManager.getLogger(AbstractNodeSet.class);
// indicates the type of an optional value index that may have
// been defined on the nodes in this set.
protected int indexType = Type.ANY_TYPE;
private boolean isCached = false;
private boolean processInReverseOrder = false;
private boolean trackMatches = true;
protected AbstractNodeSet() {
this.isEmpty = true;
}
/**
* Return an iterator on the nodes in this list. The iterator returns nodes
* according to the internal ordering of nodes (i.e. level first), not in document-
* order.
*/
@Override
public abstract NodeSetIterator iterator();
@Override
public int getItemType() {
return Type.NODE;
}
/**
* Add a sequence item to the node set. The item has to be
* a subtype of node.
*/
@Override
public void add(final Item item) throws XPathException {
if(!Type.subTypeOf(item.getType(), Type.NODE)) {
throw new XPathException("item has wrong type");
}
add((NodeProxy) item);
}
/**
* Add a proxy object to the node set. The sizeHint parameter
* gives a hint about the number of items to be expected for the
* current document.
*
* @param proxy
* @param sizeHint
*/
@Override
public void add(final NodeProxy proxy, final int sizeHint) {
add(proxy);
}
/**
* Add all items from the given sequence to the node set. All items
* have to be a subtype of node.
*
* @param other
* @throws XPathException
*/
@Override
public void addAll(final Sequence other) throws XPathException {
if(!other.isEmpty() && !Type.subTypeOf(other.getItemType(), Type.NODE)) {
throw new XPathException("sequence argument is not a node sequence");
}
if(Type.subTypeOf(other.getItemType(), Type.NODE)) {
addAll((NodeSet) other);
}
for(final SequenceIterator i = other.iterate(); i.hasNext(); ) {
add(i.nextItem());
}
}
@Override
public NodeSet copy() {
final NewArrayNodeSet set = new NewArrayNodeSet();
set.addAll(this);
return set;
}
@Override
public void setIsCached(final boolean cached) {
isCached = cached;
}
@Override
public boolean isCached() {
return isCached;
}
@Override
public void removeDuplicates() {
// all instances of NodeSet will automatically remove duplicates
// upon a call to getLength() or iterate()
}
@Override
public DocumentSet getDocumentSet() {
final MutableDocumentSet ds = new DefaultDocumentSet();
for(final Iterator<NodeProxy> i = iterator(); i.hasNext(); ) {
ds.add(i.next().getOwnerDocument());
}
return ds;
}
@Override
public Iterator<Collection> getCollectionIterator() {
return new CollectionIterator();
}
/**
* Check if any child nodes are found within this node set for a given
* set of potential parent nodes.
* <p/>
* If mode is {@link #DESCENDANT}, the returned node set will contain
* all child nodes found in this node set for each parent node. If mode is
* {@link #ANCESTOR}, the returned set will contain those parent nodes,
* for which children have been found.
*
* @param al a node set containing potential parent nodes
* @param mode selection mode
*/
@Override
public NodeSet selectParentChild(final NodeSet al, final int mode) {
return selectParentChild(al, mode, Expression.NO_CONTEXT_ID);
}
/**
* Check if any child nodes are found within this node set for a given
* set of potential ancestor nodes.
* <p/>
* If mode is {@link #DESCENDANT}, the returned node set will contain
* all child nodes found in this node set for each parent node. If mode is
* {@link #ANCESTOR}, the returned set will contain those parent nodes,
* for which children have been found.
*
* @param al a node set containing potential parent nodes
* @param mode selection mode
* @param contextId used to track context nodes when evaluating predicate
* expressions. If contextId != {@link Expression#NO_CONTEXT_ID}, the current context
* will be added to each result of the of the selection.
*/
@Override
public NodeSet selectParentChild(final NodeSet al, final int mode, final int contextId) {
return NodeSetHelper.selectParentChild(this, al, mode, contextId);
}
@Override
public boolean matchParentChild(final NodeSet al, final int mode, final int contextId) {
return NodeSetHelper.matchParentChild(this, al, mode, contextId);
}
/**
* Check if any descendant nodes are found within this node set for a given
* set of potential ancestor nodes.
* <p/>
* If mode is {@link #DESCENDANT}, the returned node set will contain
* all descendant nodes found in this node set for each ancestor. If mode is
* {@link #ANCESTOR}, the returned set will contain those ancestor nodes,
* for which descendants have been found.
*
* @param al a node set containing potential parent nodes
* @param mode selection mode
* @param includeSelf if true, check if the ancestor node itself is contained in
* the set of descendant nodes (descendant-or-self axis)
* @param contextId used to track context nodes when evaluating predicate
* expressions. If contextId != {@link Expression#NO_CONTEXT_ID}, the current context
* will be added to each result of the selection.
*/
@Override
public NodeSet selectAncestorDescendant(final NodeSet al, final int mode, final boolean includeSelf,
final int contextId, final boolean copyMatches) {
return NodeSetHelper.selectAncestorDescendant(this, al, mode, includeSelf, contextId);
}
@Override
public boolean matchAncestorDescendant(final NodeSet al, final int mode, final boolean includeSelf,
final int contextId, final boolean copyMatches) {
return NodeSetHelper.matchAncestorDescendant(this, al, mode, includeSelf, contextId);
}
/**
* For a given set of potential ancestor nodes, return all ancestors
* having descendants in this node set.
*
* @param descendants node set containing potential ancestors
* @param includeSelf if true, check if the ancestor node itself is contained
* in this node set (ancestor-or-self axis)
* @param contextId
*/
@Override
public NodeSet selectAncestors(final NodeSet descendants, boolean includeSelf, int contextId) {
return NodeSetHelper.selectAncestors(this, descendants, includeSelf, contextId);
}
public boolean matchAncestors(final NodeSet descendants, final boolean includeSelf, final int contextId) {
return NodeSetHelper.matchAncestors(this, descendants, includeSelf, contextId);
}
@Override
public NodeSet selectFollowing(final NodeSet fl, final int contextId) throws XPathException {
return NodeSetHelper.selectFollowing(fl, this);
}
@Override
public NodeSet selectFollowing(final NodeSet following, final int position, final int contextId) throws XPathException {
throw new UnsupportedOperationException();
}
@Override
public NodeSet selectPreceding(final NodeSet pl, final int contextId) throws XPathException {
return NodeSetHelper.selectPreceding(pl, this);
}
@Override
public NodeSet selectPreceding(final NodeSet preceding, final int nth, final int contextId) throws XPathException, UnsupportedOperationException {
throw new UnsupportedOperationException("selectPreceding is not implemented on AbstractNodeSet");
}
/**
* Select all nodes from the passed node set, which
* are preceding or following siblings of the nodes in
* this set. If mode is {@link #FOLLOWING}, only nodes following
* the context node are selected. {@link #PRECEDING} selects
* preceding nodes.
*
* @param siblings a node set containing potential siblings
* @param contextId used to track context nodes when evaluating predicate
* expressions. If contextId != {@link Expression#NO_CONTEXT_ID}, the current context
* will be added to each result of the of the selection.
*/
@Override
public NodeSet selectPrecedingSiblings(final NodeSet siblings, final int contextId) {
return NodeSetHelper.selectPrecedingSiblings(this, siblings, contextId);
}
@Override
public NodeSet selectFollowingSiblings(final NodeSet siblings, final int contextId) {
return NodeSetHelper.selectFollowingSiblings(this, siblings, contextId);
}
@Override
public NodeSet directSelectAttribute(final DBBroker broker, final org.exist.xquery.NodeTest qname, final int contextId) {
return NodeSetHelper.directSelectAttributes(broker, this, qname, contextId);
}
@Override
public boolean directMatchAttribute(final DBBroker broker, final org.exist.xquery.NodeTest qname, final int contextId) {
return NodeSetHelper.directMatchAttributes(broker, this, qname, contextId);
}
@Override
public NodeProxy parentWithChild(final DocumentImpl doc, NodeId nodeId, final boolean directParent, final boolean includeSelf) {
NodeProxy temp = get(doc, nodeId);
if(includeSelf && temp != null) {
return temp;
}
nodeId = nodeId.getParentId();
while(nodeId != null) {
temp = get(doc, nodeId);
if(temp != null) {
return temp;
} else if(directParent) {
return null;
}
nodeId = nodeId.getParentId();
}
return null;
}
/**
* Check if the given node has an ancestor contained in this node set
* and return the ancestor found.
* <p/>
* If directParent is true, only immediate ancestors (parents) are considered.
* Otherwise the method will call itself recursively for all the node's
* parents.
* <p/>
* If includeSelf is true, the method returns also true if
* the node itself is contained in the node set.
*/
@Override
public NodeProxy parentWithChild(final NodeProxy proxy, final boolean directParent, final boolean includeSelf, final int level) {
return parentWithChild(proxy.getOwnerDocument(), proxy.getNodeId(), directParent, includeSelf);
}
/**
* Return a new node set containing the parent nodes of all nodes in the
* current set.
*
* @param contextId an <code>int</code> value
* @return a <code>NodeSet</code> value
*/
@Override
public NodeSet getParents(final int contextId) {
final NodeSet parents = new NewArrayNodeSet();
NodeProxy parent = null;
for(final Iterator<NodeProxy> i = iterator(); i.hasNext(); ) {
final NodeProxy current = i.next();
final NodeId parentID = current.getNodeId().getParentId();
if(parentID != null && !(parentID.getTreeLevel() == 1 &&
current.getOwnerDocument().getCollection().isTempCollection())) {
if(parent == null || parent.getOwnerDocument().getDocId() !=
current.getOwnerDocument().getDocId() || !parent.getNodeId().equals(parentID)) {
if(parentID != NodeId.DOCUMENT_NODE) {
parent = new NodeProxy(current.getOwnerDocument(), parentID, Node.ELEMENT_NODE,
StoredNode.UNKNOWN_NODE_IMPL_ADDRESS);
} else {
parent = new NodeProxy(current.getOwnerDocument(), parentID, Node.DOCUMENT_NODE,
StoredNode.UNKNOWN_NODE_IMPL_ADDRESS);
}
}
if(Expression.NO_CONTEXT_ID != contextId) {
parent.addContextNode(contextId, current);
} else {
parent.copyContext(current);
}
parent.addMatches(current);
parents.add(parent);
}
}
return parents;
}
/**
* The method <code>getAncestors</code>
*
* @param contextId an <code>int</code> value
* @param includeSelf a <code>boolean</code> value
* @return a <code>NodeSet</code> value
*/
@Override
public NodeSet getAncestors(final int contextId, final boolean includeSelf) {
final ExtArrayNodeSet ancestors = new ExtArrayNodeSet();
for(final Iterator<NodeProxy> i = iterator(); i.hasNext(); ) {
final NodeProxy current = i.next();
if(includeSelf) {
if(Expression.NO_CONTEXT_ID != contextId) {
current.addContextNode(contextId, current);
}
ancestors.add(current);
}
NodeId parentID = current.getNodeId().getParentId();
while(parentID != null) {
//Filter out the temporary nodes wrapper element
if(parentID != NodeId.DOCUMENT_NODE &&
!(parentID.getTreeLevel() == 1 && current.getOwnerDocument().getCollection().isTempCollection())) {
final NodeProxy parent = new NodeProxy(current.getOwnerDocument(), parentID, Node.ELEMENT_NODE);
if(Expression.NO_CONTEXT_ID != contextId) {
parent.addContextNode(contextId, current);
} else {
parent.copyContext(current);
}
ancestors.add(parent);
}
parentID = parentID.getParentId();
}
}
ancestors.mergeDuplicates();
return ancestors;
}
/**
* Get a hint about how many nodes in this node set belong to the
* specified document. This is just used for allocating new node sets.
* The information does not need to be exact. -1 is returned if the
* size cannot be determined (the default).
*
* @param doc
*/
@Override
public int getSizeHint(final DocumentImpl doc) {
return Constants.NO_SIZE_HINT;
}
/**
* Return a new node set, which represents the intersection of the current
* node set with the given node set.
*
* @param other
*/
@Override
public NodeSet intersection(final NodeSet other) {
final AVLTreeNodeSet r = new AVLTreeNodeSet();
NodeProxy l, p;
for(final Iterator<NodeProxy> i = iterator(); i.hasNext(); ) {
l = i.next();
if((p = other.get(l)) != null) {
l.addMatches(p);
r.add(l);
}
}
return r;
}
@Override
public NodeSet deepIntersection(final NodeSet other) {
final AVLTreeNodeSet r = new AVLTreeNodeSet();
NodeProxy l, p, q;
for(final Iterator<NodeProxy> i = iterator(); i.hasNext(); ) {
l = i.next();
if((p = other.parentWithChild(l, false, true, NodeProxy.UNKNOWN_NODE_LEVEL)) != null) {
if(p.getNodeId().equals(l.getNodeId())) {
p.addMatches(l);
}
r.add(p);
}
}
for(final Iterator<NodeProxy> i = other.iterator(); i.hasNext(); ) {
l = i.next();
if((q = parentWithChild(l, false, true, NodeProxy.UNKNOWN_NODE_LEVEL)) != null) {
if((p = r.get(q)) != null) {
p.addMatches(l);
} else {
r.add(l);
}
}
}
return r;
}
@Override
public NodeSet except(final NodeSet other) {
final AVLTreeNodeSet r = new AVLTreeNodeSet();
NodeProxy l;
for(final Iterator<NodeProxy> i = iterator(); i.hasNext(); ) {
l = i.next();
if(!other.contains(l)) {
r.add(l);
}
}
return r;
}
@Override
public NodeSet filterDocuments(final NodeSet otherSet) {
final DocumentSet docs = otherSet.getDocumentSet();
final NodeSet newSet = new NewArrayNodeSet();
for(final Iterator<NodeProxy> i = iterator(); i.hasNext(); ) {
final NodeProxy p = i.next();
if(docs.contains(p.getOwnerDocument().getDocId())) {
newSet.add(p);
}
}
return newSet;
}
@Override
public void setProcessInReverseOrder(final boolean inReverseOrder) {
processInReverseOrder = inReverseOrder;
}
@Override
public boolean getProcessInReverseOrder() {
return processInReverseOrder;
}
/**
* Return a new node set which represents the union of the
* current node set and the given node set.
*
* @param other
*/
public NodeSet union(final NodeSet other) {
if(isEmpty()) {
return other;
} else if(other.isEmpty()) {
return this;
} else {
final NewArrayNodeSet result = new NewArrayNodeSet();
result.addAll(other);
NodeProxy p, c;
for(final Iterator<NodeProxy> i = iterator(); i.hasNext(); ) {
p = i.next();
if((c = other.get(p)) != null) {
c.addMatches(p);
} else {
result.add(p);
}
}
return result;
}
}
/**
* Returns all context nodes associated with the nodes in
* this node set.
*
* @param contextId used to track context nodes when evaluating predicate
* expressions. If contextId != {@link Expression#NO_CONTEXT_ID}, the current context
* will be added to each result of the of the selection.
*/
@Override
public NodeSet getContextNodes(final int contextId) {
NodeProxy current, context;
ContextItem contextNode;
final NewArrayNodeSet result = new NewArrayNodeSet();
DocumentImpl lastDoc = null;
for(final Iterator<NodeProxy> i = iterator(); i.hasNext(); ) {
current = i.next();
contextNode = current.getContext();
while(contextNode != null) {
if(contextNode.getContextId() == contextId) {
context = contextNode.getNode();
context.addMatches(current);
if(Expression.NO_CONTEXT_ID != contextId) {
context.addContextNode(contextId, context);
}
if(lastDoc != null && lastDoc.getDocId() != context.getOwnerDocument().getDocId()) {
lastDoc = context.getOwnerDocument();
result.add(context, getSizeHint(lastDoc));
} else {
result.add(context);
}
}
contextNode = contextNode.getNextDirect();
}
}
return result;
}
/**
* Always returns this.
*
* @see org.exist.xquery.value.Sequence#toNodeSet()
*/
@Override
public NodeSet toNodeSet() throws XPathException {
return this;
}
public MemoryNodeSet toMemNodeSet() throws XPathException {
return null;
}
@Override
public int getState() {
return 1;
}
@Override
public boolean hasChanged(final int previousState) {
return false;
}
@Override
public void setTrackMatches(final boolean track) {
this.trackMatches = track;
}
@Override
public boolean getTrackMatches() {
return trackMatches;
}
/**
* If all nodes in this set have an index, returns the common
* super type used to build the index, e.g. xs:integer or xs:string.
* If the nodes have different index types or no node has been indexed,
* returns {@link Type#ITEM}.
*
* @see org.exist.xquery.GeneralComparison
* @see org.exist.xquery.ValueComparison
*/
@Override
public int getIndexType() {
//Is the index type initialized ?
if(indexType == Type.ANY_TYPE) {
for(final Iterator<NodeProxy> i = iterator(); i.hasNext(); ) {
final NodeProxy node = i.next();
if(node.getOwnerDocument().getCollection().isTempCollection()) {
//Temporary nodes return default values
indexType = Type.ITEM;
break;
}
final int nodeIndexType = node.getIndexType();
//Refine type
//TODO : use common subtype
if(indexType == Type.ANY_TYPE) {
indexType = nodeIndexType;
} else {
//Broaden type
//TODO : use common supertype
if(indexType != nodeIndexType) {
indexType = Type.ITEM;
}
}
}
}
return indexType;
}
@Override
public void clearContext(final int contextId) throws XPathException {
NodeProxy p;
for(final Iterator<NodeProxy> i = iterator(); i.hasNext(); ) {
p = i.next();
p.clearContext(contextId);
}
}
@Override
public void nodeMoved(final NodeId oldNodeId, final NodeHandle newNode) {
final NodeProxy p = get(newNode.getOwnerDocument(), oldNodeId);
if(p != null) {
p.nodeMoved(oldNodeId, newNode);
}
}
@Override
public boolean isPersistentSet() {
// node sets are always persistent
return true;
}
@Override
public String toString() {
final StringBuilder result = new StringBuilder();
result.append("NodeSet(");
for(int i = 0; i < getLength(); i++) {
if(i > 0) {
result.append(", ");
}
final NodeProxy p = get(i);
result.append("[").append(p.getOwnerDocument().getDocId()).append(":").append(p.getNodeId()).append("]");
}
result.append(")");
return result.toString();
}
private final class CollectionIterator implements Iterator<Collection> {
private Collection nextCollection = null;
private final NodeSetIterator nodeIterator = iterator();
private CollectionIterator() {
if(nodeIterator.hasNext()) {
final NodeProxy p = nodeIterator.next();
nextCollection = p.getOwnerDocument().getCollection();
}
}
@Override
public final boolean hasNext() {
return nextCollection != null;
}
@Override
public final Collection next() {
final Collection oldCollection = nextCollection;
nextCollection = null;
while(nodeIterator.hasNext()) {
final NodeProxy p = nodeIterator.next();
if(!p.getOwnerDocument().getCollection().equals(oldCollection)) {
nextCollection = p.getOwnerDocument().getCollection();
break;
}
}
return oldCollection;
}
@Override
public final void remove() {
throw new UnsupportedOperationException("remove is not implemented for CollectionIterator");
}
}
}