/* * eXist Open Source Native XML Database * Copyright (C) 2001-2007 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; import org.apache.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.ExtArrayNodeSet}) remove duplicates when sorting the set. */ public abstract class AbstractNodeSet extends AbstractSequence implements NodeSet { protected final static Logger LOG = Logger.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; protected boolean hasTextIndex = false; protected boolean hasMixedContent = false; private boolean isCached = false; private boolean processInReverseOrder = false; protected AbstractNodeSet() { 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. * */ public abstract NodeSetIterator iterator(); /* (non-Javadoc) * @see org.exist.xquery.value.Sequence#iterate() */ public abstract SequenceIterator iterate() throws XPathException; /* (non-Javadoc) * @see org.exist.xquery.value.Sequence#unorderedIterator() */ public abstract SequenceIterator unorderedIterator() throws XPathException; /* (non-Javadoc) * @see org.exist.xquery.value.Sequence#getItemType() */ public int getItemType() { return Type.NODE; } /** * Check if this node set contains a node matching the document and * node-id of the given NodeProxy object. * * @param proxy */ public abstract boolean contains(NodeProxy proxy); /** * Add a new proxy object to the node set. Please note: node set * implementations may allow duplicates. * * @param proxy */ public abstract void add(NodeProxy proxy); /** * 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 */ public void add(NodeProxy proxy, int sizeHint) { add(proxy); } /** * Add a sequence item to the node set. The item has to be * a subtype of node. */ public void add(Item item) throws XPathException { if (!Type.subTypeOf(item.getType(), Type.NODE)) throw new XPathException("item has wrong type"); add((NodeProxy) item); } /** * Add all items from the given sequence to the node set. All items * have to be a subtype of node. * * @param other * @throws XPathException */ public void addAll(Sequence other) throws XPathException { if (!other.isEmpty() && !Type.subTypeOf(other.getItemType(), Type.NODE)) throw new XPathException("sequence argument is not a node sequence"); for (SequenceIterator i = other.iterate(); i.hasNext();) { add(i.nextItem()); } } /** * Add all nodes from the given node set. * * @param other */ public abstract void addAll(NodeSet other); /** * Return the number of nodes contained in this node set. */ public abstract int getLength(); public void setIsCached(boolean cached) { isCached = cached; } public boolean isCached() { return isCached; } /* (non-Javadoc) * @see org.exist.xquery.value.Sequence#removeDuplicates() */ public void removeDuplicates() { // all instances of NodeSet will automatically remove duplicates // upon a call to getLength() or iterate() } public abstract Node item(int pos); /** * Get the node at position pos within this node set. * @param pos */ public abstract NodeProxy get(int pos); /** * Get a node from this node set matching the document and node id of * the given NodeProxy. * * @param p */ public abstract NodeProxy get(NodeProxy p); public DocumentSet getDocumentSet() { MutableDocumentSet ds = new DefaultDocumentSet(); NodeProxy p; for(Iterator i = iterator(); i.hasNext(); ) { p = (NodeProxy)i.next(); ds.add(p.getDocument()); } return ds; } public Iterator getCollectionIterator() { return new CollectionIterator(); } /** * Check if any child nodes are found within this node set for a given * set of potential parent nodes. * * 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 */ public NodeSet selectParentChild(NodeSet al, 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. * * 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. */ public NodeSet selectParentChild(NodeSet al, int mode, int contextId) { return NodeSetHelper.selectParentChild(this, al, mode, contextId); } /** * Check if any descendant nodes are found within this node set for a given * set of potential ancestor nodes. * * 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. * */ public NodeSet selectAncestorDescendant(NodeSet al, int mode, boolean includeSelf, int contextId, boolean copyMatches) { return NodeSetHelper.selectAncestorDescendant(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 */ public NodeSet selectAncestors(NodeSet descendants, boolean includeSelf, int contextId) { return NodeSetHelper.selectAncestors(this, descendants, includeSelf, contextId); } public NodeSet selectFollowing(NodeSet fl, int contextId) throws XPathException { return NodeSetHelper.selectFollowing(fl, this); } public NodeSet selectFollowing(NodeSet following, int position, int contextId) throws XPathException { throw new UnsupportedOperationException(); } public NodeSet selectPreceding(NodeSet pl, int contextId) throws XPathException { return NodeSetHelper.selectPreceding(pl, this); } public NodeSet selectPreceding(NodeSet preceding, int nth, int contextId) throws XPathException, UnsupportedOperationException { throw new UnsupportedOperationException(); } /** * 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. */ public NodeSet selectPrecedingSiblings(NodeSet siblings, int contextId) { return NodeSetHelper.selectPrecedingSiblings(this, siblings, contextId); } public NodeSet selectFollowingSiblings(NodeSet siblings, int contextId) { return NodeSetHelper.selectFollowingSiblings(this, siblings, contextId); } public NodeSet directSelectAttribute(DBBroker broker, org.exist.xquery.NodeTest qname, int contextId) { return NodeSetHelper.directSelectAttributes(broker, this, qname, contextId); } public NodeProxy parentWithChild(DocumentImpl doc, NodeId nodeId, boolean directParent, 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. * * If directParent is true, only immediate ancestors (parents) are considered. * Otherwise the method will call itself recursively for all the node's * parents. * * If includeSelf is true, the method returns also true if * the node itself is contained in the node set. */ public NodeProxy parentWithChild(NodeProxy proxy, boolean directParent, boolean includeSelf, int level) { return parentWithChild(proxy.getDocument(), 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 */ public NodeSet getParents(int contextId) { NodeSet parents = new NewArrayNodeSet(); NodeProxy parent = null; for (Iterator i = iterator(); i.hasNext();) { NodeProxy current = (NodeProxy) i.next(); NodeId parentID = current.getNodeId().getParentId(); //Filter out the temporary nodes wrapper element // Moved the parentID != NodeId.DOCUMENT_NODE test inside for /a/parent::node() to work // correctly. // Added the needed test parentID != null, detected in org.exist.xquery.OptimizerTest // "//node()[parent::mods:title &= 'ethnic']" which caused an NPE here // since I dont know when. /ljo if (parentID != null && !(parentID.getTreeLevel() == 1 && current.getDocument().getCollection().isTempCollection())) { if (parent == null || parent.getDocument().getDocId() != current.getDocument().getDocId() || !parent.getNodeId().equals(parentID)) { if (parentID != NodeId.DOCUMENT_NODE) { parent = new NodeProxy(current.getDocument(), parentID, Node.ELEMENT_NODE, StoredNode.UNKNOWN_NODE_IMPL_ADDRESS); } else { parent = new NodeProxy(current.getDocument(), 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 */ public NodeSet getAncestors(int contextId, boolean includeSelf) { ExtArrayNodeSet ancestors = new ExtArrayNodeSet(); for (Iterator i = iterator(); i.hasNext();) { NodeProxy current = (NodeProxy) 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.getDocument().getCollection().isTempCollection())) { NodeProxy parent = new NodeProxy(current.getDocument(), 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 */ public int getSizeHint(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 */ public NodeSet intersection(NodeSet other) { AVLTreeNodeSet r = new AVLTreeNodeSet(); NodeProxy l, p; for (Iterator i = iterator(); i.hasNext();) { l = (NodeProxy) i.next(); if ((p = other.get(l)) != null) { l.addMatches(p); r.add(l); } } // for (Iterator i = other.iterator(); i.hasNext();) { // l = (NodeProxy) i.next(); // if (contains(l)) { // if ((p = r.get(l)) != null) { // p.addMatches(l); // } else // r.add(l); // } // } return r; } public NodeSet deepIntersection(NodeSet other) { //ExtArrayNodeSet r = new ExtArrayNodeSet(); AVLTreeNodeSet r = new AVLTreeNodeSet(); NodeProxy l, p, q; for (Iterator i = iterator(); i.hasNext();) { l = (NodeProxy) 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 (Iterator i = other.iterator(); i.hasNext();) { l = (NodeProxy) 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; } public NodeSet except(NodeSet other) { AVLTreeNodeSet r = new AVLTreeNodeSet(); NodeProxy l; for (Iterator i = iterator(); i.hasNext();) { l = (NodeProxy) i.next(); if (!other.contains(l)) { r.add(l); } } return r; } public NodeSet filterDocuments(NodeSet otherSet) { DocumentSet docs = otherSet.getDocumentSet(); NodeSet newSet = new NewArrayNodeSet(); for (Iterator i = iterator(); i.hasNext();) { NodeProxy p = (NodeProxy) i.next(); if (docs.contains(p.getDocument().getDocId())) newSet.add(p); } return newSet; } public void setProcessInReverseOrder(boolean inReverseOrder) { processInReverseOrder = inReverseOrder; } 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(NodeSet other) { NewArrayNodeSet result = new NewArrayNodeSet(); result.addAll(other); NodeProxy p, c; for (Iterator i = iterator(); i.hasNext();) { p = (NodeProxy) i.next(); if (other.contains(p)) { c = other.get(p); if(c != 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. */ public NodeSet getContextNodes(int contextId) { NodeProxy current, context; ContextItem contextNode; NewArrayNodeSet result = new NewArrayNodeSet(); DocumentImpl lastDoc = null; for (Iterator i = iterator(); i.hasNext();) { current = (NodeProxy) i.next(); contextNode = current.getContext(); while (contextNode != null) { if (contextNode.getContextId() == contextId) { context = contextNode.getNode(); context.addMatches(current); //if (!result.contains(context)) { if (Expression.NO_CONTEXT_ID != contextId) context.addContextNode(contextId, context); if (lastDoc != null && lastDoc.getDocId() != context.getDocument().getDocId()) { lastDoc = context.getDocument(); result.add(context, getSizeHint(lastDoc)); } else result.add(context); //} } contextNode = contextNode.getNextDirect(); } } return result; } /** * Always returns this. * * @see org.exist.xquery.value.Sequence#toNodeSet() */ public NodeSet toNodeSet() throws XPathException { return this; } public MemoryNodeSet toMemNodeSet() throws XPathException { return null; } /* (non-Javadoc) * @see org.exist.dom.NodeSet#getState() */ public int getState() { return 1; } /* (non-Javadoc) * @see org.exist.dom.NodeSet#hasChanged(int) */ public boolean hasChanged(int previousState) { return false; } /** * If all nodes in this set have an index, returns the common * supertype 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 */ public int getIndexType() { //Is the index type initialized ? if (indexType == Type.ANY_TYPE) { hasTextIndex = true; hasMixedContent = true; for (Iterator i = iterator(); i.hasNext();) { NodeProxy node = (NodeProxy) i.next(); if (node.getDocument().getCollection().isTempCollection()) { //Temporary nodes return default values indexType = Type.ITEM; hasTextIndex = false; hasMixedContent = false; break; } 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; } if(!node.hasTextIndex()) { hasTextIndex = false; } if(!node.hasMixedContent()) { hasMixedContent = false; } } } return indexType; } public boolean hasTextIndex() { if(indexType == Type.ANY_TYPE) { getIndexType(); // int type; // NodeProxy p; // for (Iterator i = iterator(); i.hasNext();) { // p = (NodeProxy) i.next(); // hasTextIndex = p.hasTextIndex(); // if(!hasTextIndex) // break; // } } return hasTextIndex; } public boolean hasMixedContent() { if(indexType == Type.ANY_TYPE) { getIndexType(); } return hasMixedContent; } public void clearContext(int contextId) throws XPathException { NodeProxy p; for (Iterator i = iterator(); i.hasNext(); ) { p = (NodeProxy) i.next(); p.clearContext(contextId); } } public void nodeMoved(NodeId oldNodeId, StoredNode newNode) { NodeProxy p = get((DocumentImpl)newNode.getOwnerDocument(), oldNodeId); if (p != null) p.nodeMoved(oldNodeId, newNode); } /* (non-Javadoc) * @see org.exist.xquery.value.AbstractSequence#isPersistentSet() */ public boolean isPersistentSet() { // node sets are always persistent return true; } public String toString() { StringBuilder result = new StringBuilder(); result.append("NodeSet("); for (int i = 0 ; i < getLength() ; i++) { if(i > 0) result.append(", "); NodeProxy p = get(i); result.append("[").append(p.getDocument().getDocId()).append(":").append(p.getNodeId()).append("]"); } result.append(")"); return result.toString(); } private class CollectionIterator implements Iterator { Collection nextCollection = null; NodeSetIterator nodeIterator = iterator(); CollectionIterator() { if (nodeIterator.hasNext()) { NodeProxy p = (NodeProxy) nodeIterator.next(); nextCollection = p.getDocument().getCollection(); } } public boolean hasNext() { return nextCollection != null; } public Object next() { Collection oldCollection = nextCollection; nextCollection = null; while (nodeIterator.hasNext()) { NodeProxy p = (NodeProxy) nodeIterator.next(); if (!p.getDocument().getCollection().equals(oldCollection)) { nextCollection = p.getDocument().getCollection(); break; } } return oldCollection; } public void remove() { // not needed throw new IllegalStateException(); } } }