/* * 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.exist.numbering.NodeId; import org.exist.storage.DBBroker; import org.exist.xquery.Constants; import org.exist.xquery.Expression; import org.exist.xquery.NodeTest; import org.exist.xquery.XPathException; import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.dom.Text; /** * Collection of static methods operating on node sets. * * @author wolf */ public final class NodeSetHelper { private NodeSetHelper() { //Utility class of static methods } /** * For two given sets of potential parent and child nodes, find those nodes * from the child set that actually have parents in the parent set, i.e. the * parent-child relationship is true. * <p/> * The method returns either the matching descendant or ancestor nodes, * depending on the mode constant. * <p/> * If mode is {@link NodeSet#DESCENDANT}, the returned node set will contain all * child nodes found in this node set for each parent node. If mode is * {@link NodeSet#ANCESTOR}, the returned set will contain those parent nodes, for * which children have been found. * * @param dl A node set containing potential child nodes * @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 selection. */ public static NodeSet selectParentChild(final NodeSet dl, final NodeSet al, final int mode, final int contextId) { final ExtArrayNodeSet result = new ExtArrayNodeSet(); DocumentImpl lastDoc = null; switch(mode) { case NodeSet.DESCENDANT: for(final NodeProxy child : dl) { int sizeHint = Constants.NO_SIZE_HINT; if(lastDoc == null || child.getOwnerDocument() != lastDoc) { lastDoc = child.getOwnerDocument(); sizeHint = dl.getSizeHint(lastDoc); } final NodeProxy parent = al.parentWithChild(child, true, false, NodeProxy.UNKNOWN_NODE_LEVEL); if(parent != null) { if(Expression.NO_CONTEXT_ID != contextId) { child.deepCopyContext(parent, contextId); } else { child.copyContext(parent); } result.add(child, sizeHint); } } break; case NodeSet.ANCESTOR: for(final NodeProxy child : dl) { int sizeHint = Constants.NO_SIZE_HINT; if(lastDoc == null || child.getOwnerDocument() != lastDoc) { lastDoc = child.getOwnerDocument(); sizeHint = al.getSizeHint(lastDoc); } final NodeProxy parent = al.parentWithChild(child, true, false, NodeProxy.UNKNOWN_NODE_LEVEL); if(parent != null) { if(Expression.NO_CONTEXT_ID != contextId) { parent.deepCopyContext(child, contextId); } else { parent.copyContext(child); } parent.addMatches(child); result.add(parent, sizeHint); } } break; default: throw new IllegalArgumentException("Bad 'mode' argument"); } result.sort(); return result; } public static boolean matchParentChild(final NodeSet dl, final NodeSet al, final int mode, final int contextId) { DocumentImpl lastDoc = null; switch(mode) { case NodeSet.DESCENDANT: for(final NodeProxy child : dl) { if(lastDoc == null || child.getOwnerDocument() != lastDoc) { lastDoc = child.getOwnerDocument(); } final NodeProxy parent = al.parentWithChild(child, true, false, NodeProxy.UNKNOWN_NODE_LEVEL); if(parent != null) { return true; } } break; case NodeSet.ANCESTOR: for(final NodeProxy child : dl) { if(lastDoc == null || child.getOwnerDocument() != lastDoc) { lastDoc = child.getOwnerDocument(); } final NodeProxy parent = al.parentWithChild(child, true, false, NodeProxy.UNKNOWN_NODE_LEVEL); if(parent != null) { return true; } } break; default: throw new IllegalArgumentException("Bad 'mode' argument"); } return false; } /** * For two given sets of potential ancestor and descendant nodes, find those * nodes from the descendant set that actually have ancestors in the * ancestor set, i.e. the ancestor-descendant relationship is true. * <p/> * The method returns either the matching descendant or ancestor nodes, * depending on the mode constant. * <p/> * If mode is {@link NodeSet#DESCENDANT}, the returned node set will contain all * descendant nodes found in this node set for each ancestor. If mode is * {@link NodeSet#ANCESTOR}, the returned set will contain those ancestor nodes, * for which descendants have been found. * * @param dl A node set containing potential descendant nodes * @param al A node set containing potential ancestor 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 static NodeSet selectAncestorDescendant(final NodeSet dl, final NodeSet al, final int mode, final boolean includeSelf, final int contextId) { final ExtArrayNodeSet result = new ExtArrayNodeSet(); DocumentImpl lastDoc = null; switch(mode) { case NodeSet.DESCENDANT: for(final NodeProxy descendant : dl) { int sizeHint = Constants.NO_SIZE_HINT; // get a size hint for every new document encountered if(lastDoc == null || descendant.getOwnerDocument() != lastDoc) { lastDoc = descendant.getOwnerDocument(); sizeHint = dl.getSizeHint(lastDoc); } final NodeProxy ancestor = al.parentWithChild(descendant.getOwnerDocument(), descendant.getNodeId(), false, includeSelf); if(ancestor != null) { if(Expression.NO_CONTEXT_ID != contextId) { descendant.addContextNode(contextId, ancestor); } else { descendant.copyContext(ancestor); } result.add(descendant, sizeHint); } } break; case NodeSet.ANCESTOR: for(final NodeProxy descendant : dl) { int sizeHint = Constants.NO_SIZE_HINT; // get a size hint for every new document encountered if(lastDoc == null || descendant.getOwnerDocument() != lastDoc) { lastDoc = descendant.getOwnerDocument(); sizeHint = al.getSizeHint(lastDoc); } final NodeProxy ancestor = al.parentWithChild(descendant.getOwnerDocument(), descendant.getNodeId(), false, includeSelf); if(ancestor != null) { if(Expression.NO_CONTEXT_ID != contextId) { ancestor.addContextNode(contextId, descendant); } else { ancestor.copyContext(descendant); } result.add(ancestor, sizeHint); } } break; default: throw new IllegalArgumentException("Bad 'mode' argument"); } return result; } public static boolean matchAncestorDescendant(final NodeSet dl, final NodeSet al, final int mode, final boolean includeSelf, final int contextId) { final ExtArrayNodeSet result = new ExtArrayNodeSet(); DocumentImpl lastDoc = null; switch(mode) { case NodeSet.DESCENDANT: for(final NodeProxy descendant : dl) { int sizeHint = Constants.NO_SIZE_HINT; // get a size hint for every new document encountered if(lastDoc == null || descendant.getOwnerDocument() != lastDoc) { lastDoc = descendant.getOwnerDocument(); sizeHint = dl.getSizeHint(lastDoc); } final NodeProxy ancestor = al.parentWithChild(descendant.getOwnerDocument(), descendant.getNodeId(), false, includeSelf); if(ancestor != null) { if(Expression.NO_CONTEXT_ID != contextId) { descendant.addContextNode(contextId, ancestor); } else { descendant.copyContext(ancestor); } result.add(descendant, sizeHint); return true; } } break; case NodeSet.ANCESTOR: for(final NodeProxy descendant : dl) { int sizeHint = Constants.NO_SIZE_HINT; // get a size hint for every new document encountered if(lastDoc == null || descendant.getOwnerDocument() != lastDoc) { lastDoc = descendant.getOwnerDocument(); sizeHint = al.getSizeHint(lastDoc); } final NodeProxy ancestor = al.parentWithChild(descendant.getOwnerDocument(), descendant.getNodeId(), false, includeSelf); if(ancestor != null) { if(Expression.NO_CONTEXT_ID != contextId) { ancestor.addContextNode(contextId, descendant); } else { ancestor.copyContext(descendant); } result.add(ancestor, sizeHint); return true; } } break; default: throw new IllegalArgumentException("Bad 'mode' argument"); } return false; } /** * For two sets of potential ancestor and descendant nodes, return all the * real ancestors having a descendant in the descendant set. * * @param al Node set containing potential ancestors * @param dl Node set containing potential descendants * @param includeSelf If true, check if the ancestor node itself * is contained in this node set (ancestor-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 of the * selection. */ public static NodeSet selectAncestors(final NodeSet al, final NodeSet dl, final boolean includeSelf, final int contextId) { final NodeSet result = new NewArrayNodeSet(); for(final NodeProxy descendant : dl) { final NodeSet ancestors = ancestorsForChild(al, descendant, false, includeSelf); for(final NodeProxy ancestor : ancestors) { if(ancestor != null) { final NodeProxy temp = result.get(ancestor); if(temp == null) { if(Expression.IGNORE_CONTEXT != contextId) { if(Expression.NO_CONTEXT_ID != contextId) { ancestor.addContextNode(contextId, descendant); } else { ancestor.copyContext(descendant); } } ancestor.addMatches(descendant); result.add(ancestor); } else if(Expression.NO_CONTEXT_ID != contextId) { temp.addContextNode(contextId, descendant); } } } } return result; } public static boolean matchAncestors(final NodeSet al, final NodeSet dl, final boolean includeSelf, final int contextId) { final NodeSet result = new NewArrayNodeSet(); for(final NodeProxy descendant : dl) { final NodeSet ancestors = ancestorsForChild(al, descendant, false, includeSelf); for(final NodeProxy ancestor : ancestors) { if(ancestor != null) { final NodeProxy temp = result.get(ancestor); if(temp == null) { if(Expression.IGNORE_CONTEXT != contextId) { if(Expression.NO_CONTEXT_ID != contextId) { ancestor.addContextNode(contextId, descendant); } else { ancestor.copyContext(descendant); } } ancestor.addMatches(descendant); result.add(ancestor); return true; } else if(Expression.NO_CONTEXT_ID != contextId) { temp.addContextNode(contextId, descendant); } } } } return false; } /** * Return all nodes contained in the node set that are ancestors of the node * p. */ private static NodeSet ancestorsForChild(final NodeSet ancestors, final NodeProxy child, final boolean directParent, final boolean includeSelf) { final NodeSet result = new NewArrayNodeSet(); NodeId nodeId = child.getNodeId(); NodeProxy temp = ancestors.get(child.getOwnerDocument(), nodeId); if(includeSelf && temp != null) { result.add(temp); } while(nodeId != null && nodeId != NodeId.DOCUMENT_NODE) { nodeId = nodeId.getParentId(); temp = ancestors.get(child.getOwnerDocument(), nodeId); if(temp != null) { result.add(temp); } else if(directParent) { return result; } } return result; } /** * Select all nodes from the passed set of potential siblings, which are * preceding siblings of the nodes in the other set. * * @param candidates The node set to check * @param references 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 static NodeSet selectPrecedingSiblings(final NodeSet candidates, final NodeSet references, final int contextId) { if(references.isEmpty() || candidates.isEmpty()) { return NodeSet.EMPTY_SET; } final NodeSet result = new ExtArrayNodeSet(); final NodeSetIterator iReferences = references.iterator(); final NodeSetIterator iCandidates = candidates.iterator(); NodeProxy reference = iReferences.next(); NodeProxy candidate = iCandidates.next(); NodeProxy firstCandidate = null; while(true) { // first, try to find nodes belonging to the same doc if(reference.getOwnerDocument().getDocId() < candidate.getOwnerDocument().getDocId()) { firstCandidate = null; if(iReferences.hasNext()) { reference = iReferences.next(); } else { break; } } else if(reference.getOwnerDocument().getDocId() > candidate.getOwnerDocument().getDocId()) { firstCandidate = null; if(iCandidates.hasNext()) { candidate =iCandidates.next(); } else { break; } } else { // same document: check if the nodes have the same parent int cmp = candidate.getNodeId().getParentId().compareTo(reference.getNodeId().getParentId()); if(cmp > 0 && candidate.getNodeId().getTreeLevel() <= reference.getNodeId().getTreeLevel()) { // wrong parent: proceed firstCandidate = null; if(iReferences.hasNext()) { reference = iReferences.next(); } else { break; } } else if(cmp < 0 || (cmp > 0 && candidate.getNodeId().getTreeLevel() >= reference.getNodeId().getTreeLevel())) { // wrong parent: proceed firstCandidate = null; if(iCandidates.hasNext()) { candidate = iCandidates.next(); } else { break; } } else { if(firstCandidate == null) { firstCandidate = candidate; } // found two nodes with the same parent // now, compare the ids: a node is a following sibling // if its id is greater than the id of the other node cmp = candidate.getNodeId().compareTo(reference.getNodeId()); if(cmp < 0) { // found a preceding sibling final NodeProxy t = result.get(candidate); if(t == null) { if(Expression.IGNORE_CONTEXT != contextId) { if(Expression.NO_CONTEXT_ID == contextId) { candidate.copyContext(reference); } else { candidate.addContextNode(contextId, reference); } } result.add(candidate); } else if(contextId > Expression.NO_CONTEXT_ID) { t.addContextNode(contextId, reference); } if(iCandidates.hasNext()) { candidate = iCandidates.next(); } else { break; } } else if(cmp > 0) { // found a following sibling if(iCandidates.hasNext()) { candidate = iCandidates.next(); } else { break; } // equal nodes: proceed with next node } else { if(iReferences.hasNext()) { reference = iReferences.next(); iCandidates.setPosition(firstCandidate); candidate = iCandidates.next(); } else { break; } } } } } return result; } /** * Select all nodes from the passed set of potential siblings, which are * following siblings of the nodes in the other set. * * @param candidates The node set to check * @param references 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 static NodeSet selectFollowingSiblings(final NodeSet candidates, final NodeSet references, final int contextId) { if(references.isEmpty() || candidates.isEmpty()) { return NodeSet.EMPTY_SET; } final NodeSet result = new ExtArrayNodeSet(); final NodeSetIterator iReferences = references.iterator(); final NodeSetIterator iCandidates = candidates.iterator(); NodeProxy reference = iReferences.next(); NodeProxy candidate = iCandidates.next(); NodeProxy firstCandidate = null; // TODO : review : don't care about preceding siblings while(true) { // first, try to find nodes belonging to the same doc if(reference.getOwnerDocument().getDocId() < candidate.getOwnerDocument().getDocId()) { firstCandidate = null; if(iReferences.hasNext()) { reference = iReferences.next(); } else { break; } } else if(reference.getOwnerDocument().getDocId() > candidate.getOwnerDocument().getDocId()) { firstCandidate = null; if(iCandidates.hasNext()) { candidate = iCandidates.next(); } else { break; } } else { // same document: check if the nodes have the same parent int cmp = candidate.getNodeId().getParentId().compareTo(reference.getNodeId().getParentId()); if(cmp > 0 && candidate.getNodeId().getTreeLevel() <= reference.getNodeId().getTreeLevel()) { //Do not proceed to the next "parent" if the candidate is a descendant // wrong parent: proceed firstCandidate = null; if(iReferences.hasNext()) { reference = iReferences.next(); } else { break; } } else if(cmp < 0 || (cmp > 0 && candidate.getNodeId().getTreeLevel() >= reference.getNodeId().getTreeLevel())) { // wrong parent: proceed firstCandidate = null; if(iCandidates.hasNext()) { candidate = iCandidates.next(); } else { break; } } else { if(firstCandidate == null) { firstCandidate = candidate; } cmp = candidate.getNodeId().compareTo(reference.getNodeId()); // found two nodes with the same parent // now, compare the ids: a node is a following sibling // if its id is greater than the id of the other node if(cmp < 0) { // found a preceding sibling if(iCandidates.hasNext()) { candidate = iCandidates.next(); } else { break; } } else if(cmp > 0) { // found a following sibling final NodeProxy t = result.get(candidate); if(t == null) { if(Expression.IGNORE_CONTEXT != contextId) { if(Expression.NO_CONTEXT_ID == contextId) { candidate.copyContext(reference); } else { candidate.addContextNode(contextId, reference); } } result.add(candidate); } else { t.addContextNode(contextId, reference); } result.add(candidate); if(iCandidates.hasNext()) { candidate = iCandidates.next(); } else if(iReferences.hasNext()) { reference = iReferences.next(); iCandidates.setPosition(firstCandidate); candidate = iCandidates.next(); } else { break; } // equal nodes: proceed with next node } else { if(iCandidates.hasNext()) { candidate = iCandidates.next(); } else { break; } } } } } return result; } /** * TODO: doesn't work!!! */ public static NodeSet selectPreceding(final NodeSet references, final NodeSet candidates) throws XPathException { if(candidates.isEmpty() || references.isEmpty()) { return NodeSet.EMPTY_SET; } final NodeSet result = new NewArrayNodeSet(); for(final NodeProxy reference : references) { for(final NodeProxy candidate : candidates) { if(candidate.before(reference, true)) { // TODO : add transverse context candidate.addContextNode(Expression.NO_CONTEXT_ID, reference); result.add(candidate); } } } return result; } /** * TODO: doesn't work!!! */ public static NodeSet selectFollowing(final NodeSet references, final NodeSet candidates) throws XPathException { if(candidates.isEmpty() || references.isEmpty()) { return NodeSet.EMPTY_SET; } final NodeSet result = new ExtArrayNodeSet(); for(final NodeProxy reference : references) { for(final NodeProxy candidate : candidates) { if(candidate.after(reference, true)) { // TODO : add transverse context candidate.addContextNode(Expression.NO_CONTEXT_ID, reference); result.add(candidate); } } } return result; } public static NodeSet directSelectAttributes(final DBBroker broker, final NodeSet candidates, final NodeTest test, final int contextId) { if(candidates.isEmpty()) { return NodeSet.EMPTY_SET; } final NodeSet result = new ExtArrayNodeSet(); for(final NodeProxy candidate : candidates) { result.addAll(candidate.directSelectAttribute(broker, test, contextId)); } return result; } public static boolean directMatchAttributes(final DBBroker broker, final NodeSet candidates, final NodeTest test, final int contextId) { if(candidates.isEmpty()) { return false; } for(final NodeProxy candidate : candidates) { if(candidate.directMatchAttribute(broker, test, contextId)) { return true; } } return false; } public static final void copyChildren(final Document newDoc, final Node node, final Node newNode) { final NodeList children = node.getChildNodes(); Node newChild; for(int i = 0; i < children.getLength(); i++) { final Node child = children.item(i); if(child == null) { continue; } switch(child.getNodeType()) { case Node.ELEMENT_NODE: { newChild = copyNode(newDoc, child); newNode.appendChild(newChild); break; } case Node.ATTRIBUTE_NODE: { newChild = copyNode(newDoc, child); ((Element) newNode).setAttributeNode((Attr) newChild); break; } case Node.TEXT_NODE: { newChild = copyNode(newDoc, child); newNode.appendChild(newChild); break; } // TODO : error for any other one -pb } } } public static final Node copyNode(final Document newDoc, final Node node) { final Node newNode; switch(node.getNodeType()) { case Node.ELEMENT_NODE: newNode = newDoc.createElementNS(node.getNamespaceURI(), node.getNodeName()); copyChildren(newDoc, node, newNode); break; case Node.TEXT_NODE: newNode = newDoc.createTextNode(((Text) node).getData()); break; case Node.ATTRIBUTE_NODE: newNode = newDoc.createAttributeNS(node.getNamespaceURI(), node.getNodeName()); ((Attr) newNode).setValue(((Attr) node).getValue()); break; default: // TODO : error ? -pb newNode = null; } return newNode; } }