Skip to content

Commit 824ac46

Browse files
authored
Merge pull request eXist-db#4485 from evolvedbinary/feature/fn-element-with-id#1,-#2
[feature] Implement the fn:element-with-id function
2 parents fa3970b + e72832e commit 824ac46

8 files changed

Lines changed: 527 additions & 6 deletions

File tree

exist-core/src/main/java/org/exist/Namespaces.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@ public interface Namespaces {
7474
/** QName representing xml:space */
7575
QName XML_SPACE_QNAME = new QName("space", XML_NS, "xml");
7676

77+
/** QName representing xs:id */
78+
QName XS_ID_QNAME = new QName("ID", XML_NS, "xs");
79+
80+
/** QName representing xsi:type */
81+
QName XSI_TYPE_QNAME = new QName("type", XML_NS, "xsi");
82+
7783
String SOAP_ENVELOPE = "http://schemas.xmlsoap.org/soap/envelope/";
7884

7985
//SAXfeatures / properties : move toadedicated package

exist-core/src/main/java/org/exist/dom/memtree/DocumentImpl.java

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949

5050
import javax.xml.XMLConstants;
5151
import java.util.Arrays;
52+
import java.util.Objects;
5253
import java.util.concurrent.atomic.AtomicLong;
5354

5455
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -706,21 +707,49 @@ public void selectDescendantAttributes(final NodeTest test, final Sequence resul
706707
}
707708
}
708709

710+
/**
711+
* Gets a specified node of this document.
712+
*
713+
* @param id the ID of the node to select
714+
* @return the specified node of this document, or null if this document
715+
* does not have the specified node
716+
*/
709717
public NodeImpl selectById(final String id) {
718+
return selectById(id, false);
719+
}
720+
721+
/**
722+
* Gets a specified node of this document.
723+
*
724+
* @param id the ID of the node to select
725+
* @param typeConsidered if true, this method should consider node
726+
* type attributes (i.e. <code>xsi:type="xs:ID"</code>);
727+
* if false, this method should not consider
728+
* node type attributes
729+
* @return the specified node of this document, or null if this document
730+
* does not have the specified node
731+
*/
732+
public NodeImpl selectById(final String id, final boolean typeConsidered) {
710733
if(size == 1) {
711734
return null;
712735
}
713736
expand();
714737
final ElementImpl root = (ElementImpl) getDocumentElement();
715-
if(hasIdAttribute(root.getNodeNumber(), id)) {
738+
if (hasIdAttribute(root.getNodeNumber(), id)) {
716739
return root;
717740
}
718741
final int treeLevel = this.treeLevel[root.getNodeNumber()];
719742
int nextNode = root.getNodeNumber();
720743
while((++nextNode < document.size) && (document.treeLevel[nextNode] > treeLevel)) {
721-
if((document.nodeKind[nextNode] == Node.ELEMENT_NODE) &&
722-
hasIdAttribute(nextNode, id)) {
723-
return getNode(nextNode);
744+
if (document.nodeKind[nextNode] == Node.ELEMENT_NODE) {
745+
if (hasIdAttribute(nextNode, id)) {
746+
return getNode(nextNode);
747+
} else if (hasIdTypeAttribute(nextNode, id)) {
748+
return typeConsidered ? (NodeImpl) getNode(nextNode).getParentNode() : getNode(nextNode);
749+
} else if (getNode(nextNode).getNodeName().equalsIgnoreCase("id") &&
750+
getNode(nextNode).getStringValue().equals(id)) {
751+
return typeConsidered ? (NodeImpl) getNode(nextNode).getParentNode() : getNode(nextNode);
752+
}
724753
}
725754
}
726755
return null;
@@ -754,7 +783,25 @@ private boolean hasIdAttribute(final int nodeNumber, final String id) {
754783
if(-1 < attr) {
755784
while((attr < document.nextAttr) && (document.attrParent[attr] == nodeNumber)) {
756785
if((document.attrType[attr] == AttrImpl.ATTR_ID_TYPE) &&
757-
id.equals(document.attrValue[attr])) {
786+
id.equals(document.attrValue[attr])) {
787+
return true;
788+
} else if (document.attrName[attr].getLocalPart().equals("id") &&
789+
Objects.equals(document.attrValue[attr], id)) {
790+
return true;
791+
}
792+
++attr;
793+
}
794+
}
795+
return false;
796+
}
797+
798+
private boolean hasIdTypeAttribute(final int nodeNumber, final String id) {
799+
int attr = document.alpha[nodeNumber];
800+
if(-1 < attr) {
801+
while((attr < document.nextAttr) && (document.attrParent[attr] == nodeNumber)) {
802+
if (document.attrName[attr].getStringValue().equals(Namespaces.XSI_TYPE_QNAME.getStringValue()) &&
803+
document.attrValue[attr].equals(Namespaces.XS_ID_QNAME.getStringValue()) &&
804+
document.getNode(nodeNumber).getStringValue().equals(id)) {
758805
return true;
759806
}
760807
++attr;

exist-core/src/main/java/org/exist/dom/memtree/ElementImpl.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,4 +707,16 @@ public Node appendChild(final Node newChild) throws DOMException {
707707

708708
throw unsupported();
709709
}
710+
711+
public String getAttributeValue(final String name) {
712+
int attr = 0;
713+
while (attr < document.nextAttr) {
714+
final QName attrQName = document.attrName[attr];
715+
if (attrQName.getStringValue().equals(name)) {
716+
return document.attrValue[attr];
717+
}
718+
++attr;
719+
}
720+
return null;
721+
}
710722
}

exist-core/src/main/java/org/exist/xquery/functions/fn/FnModule.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ public class FnModule extends AbstractInternalModule {
7676
new FunctionDef(FunDocAvailable.signature, FunDocAvailable.class),
7777
new FunctionDef(FunDocumentURI.FS_DOCUMENT_URI_0, FunDocumentURI.class),
7878
new FunctionDef(FunDocumentURI.FS_DOCUMENT_URI_1, FunDocumentURI.class),
79+
new FunctionDef(FunElementWithId.FS_ELEMENT_WITH_ID_SIGNATURES[0], FunElementWithId.class),
80+
new FunctionDef(FunElementWithId.FS_ELEMENT_WITH_ID_SIGNATURES[1], FunElementWithId.class),
7981
new FunctionDef(FunEmpty.signature, FunEmpty.class),
8082
new FunctionDef(FunEncodeForURI.signature, FunEncodeForURI.class),
8183
new FunctionDef(FunEndsWith.signatures[0], FunEndsWith.class),
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
/*
2+
* eXist-db Open Source Native XML Database
3+
* Copyright (C) 2001 The eXist-db Authors
4+
*
5+
* info@exist-db.org
6+
* http://www.exist-db.org
7+
*
8+
* This library is free software; you can redistribute it and/or
9+
* modify it under the terms of the GNU Lesser General Public
10+
* License as published by the Free Software Foundation; either
11+
* version 2.1 of the License, or (at your option) any later version.
12+
*
13+
* This library is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16+
* Lesser General Public License for more details.
17+
*
18+
* You should have received a copy of the GNU Lesser General Public
19+
* License along with this library; if not, write to the Free Software
20+
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21+
*/
22+
package org.exist.xquery.functions.fn;
23+
24+
import org.exist.dom.memtree.DocumentImpl;
25+
import org.exist.dom.memtree.NodeImpl;
26+
import org.exist.dom.persistent.*;
27+
import org.exist.util.XMLNames;
28+
import org.exist.xquery.*;
29+
import org.exist.xquery.Constants.Comparison;
30+
import org.exist.xquery.value.*;
31+
import org.w3c.dom.Node;
32+
33+
import java.util.Set;
34+
import java.util.StringTokenizer;
35+
import java.util.TreeSet;
36+
37+
import static org.exist.xquery.FunctionDSL.*;
38+
import static org.exist.xquery.functions.fn.FnModule.functionSignatures;
39+
40+
public class FunElementWithId extends BasicFunction {
41+
private static final String FN_NAME = "element-with-id";
42+
private static final String FN_DESCRIPTION =
43+
"Returns the sequence of element nodes that have an ID value " +
44+
"matching the value of one or more of the IDREF values supplied in $idrefs. " +
45+
"If none is matching or $idrefs is the empty sequence, returns the empty sequence.";
46+
private static final FunctionReturnSequenceType FN_RETURN = returnsOptMany(Type.STRING, "the elements with IDs matching IDREFs from $idref-sequence");
47+
private static final FunctionParameterSequenceType PARAM_ID_REFS_STRING = optManyParam("idrefs", Type.STRING, "The IDREF sequence");
48+
public static final FunctionSignature[] FS_ELEMENT_WITH_ID_SIGNATURES = functionSignatures(
49+
FN_NAME,
50+
FN_DESCRIPTION,
51+
FN_RETURN,
52+
arities(
53+
arity(),
54+
arity(PARAM_ID_REFS_STRING)
55+
)
56+
);
57+
58+
public FunElementWithId(final XQueryContext context, final FunctionSignature signature) {
59+
super(context, signature);
60+
}
61+
62+
@Override
63+
public Sequence eval(final Sequence[] args, Sequence contextSequence) throws XPathException {
64+
if (getArgumentCount() < 1) {
65+
throw new XPathException(this, ErrorCodes.XPST0017, "function element-with-id requires one argument");
66+
}
67+
68+
final Sequence result;
69+
boolean processInMem = false;
70+
final Expression arg = getArgument(0);
71+
final Sequence idval = arg.eval(contextSequence);
72+
73+
if (idval.isEmpty() || (getArgumentCount() == 1 && contextSequence != null && contextSequence.isEmpty())) {
74+
result = Sequence.EMPTY_SEQUENCE;
75+
} else {
76+
String nextId;
77+
DocumentSet docs = null;
78+
if (getArgumentCount() == 2) {
79+
final Sequence nodes = getArgument(1).eval(contextSequence);
80+
if (nodes.isEmpty()) {
81+
throw new XPathException(this, ErrorCodes.XPDY0002, "XPDY0002: no node or context item for fn:id", nodes);
82+
} else if (!Type.subTypeOf(nodes.itemAt(0).getType(), Type.NODE)) {
83+
throw new XPathException(this, ErrorCodes.XPTY0004, "XPTY0004: fn:id() argument is not a node", nodes);
84+
}
85+
NodeValue node = (NodeValue)nodes.itemAt(0);
86+
if (node.getImplementationType() == NodeValue.IN_MEMORY_NODE) {
87+
processInMem = true;
88+
} else {
89+
MutableDocumentSet ndocs = new DefaultDocumentSet();
90+
ndocs.add(((NodeProxy)node).getOwnerDocument());
91+
docs = ndocs;
92+
}
93+
contextSequence = node;
94+
} else if (contextSequence == null) {
95+
throw new XPathException(this, ErrorCodes.XPDY0002, "No context item specified");
96+
} else if(!Type.subTypeOf(contextSequence.getItemType(), Type.NODE)) {
97+
throw new XPathException(this, ErrorCodes.XPTY0004, "Context item is not a node", contextSequence);
98+
} else {
99+
if (contextSequence.isPersistentSet()) {
100+
docs = contextSequence.toNodeSet().getDocumentSet();
101+
} else {
102+
processInMem = true;
103+
}
104+
}
105+
106+
if (processInMem) {
107+
result = new ValueSequence();
108+
} else {
109+
result = new ExtArrayNodeSet();
110+
}
111+
112+
for(final SequenceIterator i = idval.iterate(); i.hasNext(); ) {
113+
nextId = i.nextItem().getStringValue();
114+
if (!nextId.isEmpty()) {
115+
if (nextId.indexOf(' ') != Constants.STRING_NOT_FOUND) {
116+
final StringTokenizer tok = new StringTokenizer(nextId, " ");
117+
while (tok.hasMoreTokens()) {
118+
nextId = tok.nextToken();
119+
if (XMLNames.isNCName(nextId)) {
120+
if (processInMem) {
121+
getId(result, contextSequence, nextId);
122+
} else {
123+
getId((NodeSet) result, docs, nextId);
124+
}
125+
}
126+
}
127+
} else {
128+
if (XMLNames.isNCName(nextId)) {
129+
if (processInMem) {
130+
getId(result, contextSequence, nextId);
131+
} else {
132+
getId((NodeSet) result, docs, nextId);
133+
}
134+
}
135+
}
136+
}
137+
}
138+
}
139+
result.removeDuplicates();
140+
if (context.getProfiler().isEnabled()) {
141+
context.getProfiler().end(this, "", result);
142+
}
143+
144+
return result;
145+
}
146+
147+
private void getId(final NodeSet result, final DocumentSet docs, final String id) throws XPathException {
148+
final NodeSet attribs = context.getBroker().getValueIndex().find(context.getWatchDog(), Comparison.EQ, docs, null, -1, null, new StringValue(id, Type.ID));
149+
NodeProxy p;
150+
for (final NodeProxy n : attribs) {
151+
p = new NodeProxy(n.getOwnerDocument(), n.getNodeId().getParentId(), Node.ELEMENT_NODE);
152+
result.add(p);
153+
}
154+
}
155+
156+
private void getId(final Sequence result, final Sequence seq, final String id) throws XPathException {
157+
final Set<DocumentImpl> visitedDocs = new TreeSet<>();
158+
for (final SequenceIterator i = seq.iterate(); i.hasNext(); ) {
159+
final NodeImpl v = (NodeImpl) i.nextItem();
160+
final DocumentImpl doc = v.getNodeType() == Node.DOCUMENT_NODE ? (DocumentImpl)v : v.getOwnerDocument();
161+
162+
if (doc != null && !visitedDocs.contains(doc)) {
163+
final NodeImpl elem = doc.selectById(id, true);
164+
if (elem != null) {
165+
result.add(elem);
166+
}
167+
visitedDocs.add(doc);
168+
}
169+
}
170+
}
171+
172+
@Override
173+
public int getDependencies() {
174+
return getArgument(0).getDependencies();
175+
}
176+
}

exist-core/src/main/java/org/exist/xquery/functions/fn/FunId.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ public Sequence eval(Sequence contextSequence, Item contextItem) throws XPathExc
127127
contextSequence = node;
128128
} else if (contextSequence == null) {
129129
logger.error("{} No context item specified", ErrorCodes.XPDY0002);
130-
throw new XPathException(this, ErrorCodes.XPDY0002, "No context item specified");
130+
throw new XPathException(this, ErrorCodes.XPTY0004, "No context item specified");
131131
} else if(!Type.subTypeOf(contextSequence.getItemType(), Type.NODE)) {
132132
logger.error("{} Context item is not a node", ErrorCodes.XPTY0004);
133133
throw new XPathException(this, ErrorCodes.XPTY0004, "Context item is not a node", contextSequence);

0 commit comments

Comments
 (0)