Skip to content

Commit b9ebedd

Browse files
chris-evolvedbinaryadamretter
authored andcommitted
[feature] Implement the fn-path() method.
1 parent 00addd8 commit b9ebedd

File tree

2 files changed

+341
-1
lines changed

2 files changed

+341
-1
lines changed

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

Lines changed: 195 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,36 @@
1+
/*
2+
* eXist-db Open Source Native XML Database
3+
* Copyright (C) 2001 The eXist-db Authors
4+
*
5+
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+
123
package org.exist.xquery.functions.fn;
224

25+
import org.exist.Namespaces;
26+
import org.exist.dom.INode;
327
import org.exist.dom.QName;
428
import org.exist.xquery.*;
529
import org.exist.xquery.value.*;
30+
import org.w3c.dom.*;
31+
32+
import java.util.LinkedList;
33+
import java.util.List;
634

735
import static org.exist.xquery.FunctionDSL.functionSignature;
836

@@ -27,6 +55,172 @@ public FunPath(final XQueryContext context, final FunctionSignature signature) {
2755

2856
@Override
2957
public Sequence eval(final Sequence[] args, final Sequence contextSequence) throws XPathException {
30-
return Sequence.EMPTY_SEQUENCE;
58+
final Sequence result;
59+
final Sequence sequence;
60+
61+
// The behavior of the function if the argument is omitted is exactly
62+
// the same as if the context item (.) had been passed as the argument.
63+
if (getArgumentCount() == 0) {
64+
if (contextSequence != null) {
65+
sequence = contextSequence;
66+
} else {
67+
sequence = Sequence.EMPTY_SEQUENCE;
68+
}
69+
} else {
70+
sequence = args[0];
71+
}
72+
73+
if (sequence.isEmpty()) {
74+
// If $arg is the empty sequence, the function returns the empty sequence.
75+
result = Sequence.EMPTY_SEQUENCE;
76+
} else {
77+
final Item item = sequence.itemAt(0);
78+
if (item.getType() == Type.DOCUMENT) {
79+
// If $arg is a document node, the function returns the string "/".
80+
result = new StringValue("/");
81+
} else if (item.getType() == Type.ELEMENT ||
82+
item.getType() == Type.ATTRIBUTE ||
83+
item.getType() == Type.TEXT ) {
84+
// For an element node, Q{uri}local[position], where uri is the
85+
// namespace URI of the node name or the empty string if the
86+
// node is in no namespace, local is the local part of the node
87+
// name, and position is an integer representing the position
88+
// of the selected node among its like-named siblings.
89+
final NodeValue nodeValue = (NodeValue) item;
90+
final Node node = nodeValue.getNode();
91+
final LinkedList<String> pathValues = new LinkedList<>();
92+
getPathValues(node, pathValues);
93+
if ((node.getOwnerDocument() == null ||
94+
node.getOwnerDocument().getDocumentElement() == null ||
95+
(node.getOwnerDocument() instanceof org.exist.dom.memtree.DocumentImpl &&
96+
!((org.exist.dom.memtree.DocumentImpl) node.getOwnerDocument()).isExplicitlyCreated()))) {
97+
// This string is prefixed by "Q{http://www.w3.org/2005/xpath-functions}root()"
98+
// if the root node is not a document node.
99+
pathValues.remove(0);
100+
result = new StringValue(String.format("Q{%s}root()", Namespaces.XPATH_FUNCTIONS_NS) + String.join("", pathValues));
101+
} else {
102+
result = new StringValue(String.join("", pathValues));
103+
}
104+
} else {
105+
// If the context item is not a node, type error [err:XPTY0004].
106+
throw new XPathException(this, ErrorCodes.XPTY0004, "Item is not a document or node; got '" + item + "'", sequence);
107+
}
108+
}
109+
return result;
110+
}
111+
112+
/**
113+
* Gets the position of a specified node among its like-named siblings.
114+
*
115+
* @param node the node whose position to get
116+
* @return the position of the specified node, or zero if this method
117+
* failed to determine the position of the specified node
118+
*/
119+
private static int getNodePosition(final Node node) {
120+
int position = 1;
121+
Node siblingNode = node.getPreviousSibling();
122+
while (siblingNode != null) {
123+
if (siblingNode != null && siblingNode.getNodeName().equals(node.getNodeName())) {
124+
++position;
125+
}
126+
siblingNode = siblingNode.getPreviousSibling();
127+
}
128+
return position;
129+
}
130+
131+
/**
132+
* Gets the path values of a specified node.
133+
*
134+
* @param node the node whose path values to get
135+
* @param values the path values
136+
*/
137+
private static void getPathValues(Node node, final List<String> values) {
138+
// get the owner element, if the specified node is an attribute node
139+
Node attributeNode;
140+
if (node.getNodeType() == Type.ATTRIBUTE) {
141+
attributeNode = node;
142+
node = ((Attr) node).getOwnerElement();
143+
} else {
144+
attributeNode = null;
145+
}
146+
147+
final StringBuilder value = new StringBuilder();
148+
final StringBuilder attributeValue = new StringBuilder();
149+
if (node.getNodeType() == Type.TEXT) {
150+
// For a text node: text()[position] where position is an integer
151+
// representing the position of the selected node among its text
152+
// node siblings
153+
final int nodePosition = getNodePosition(node);
154+
if (nodePosition > 0) {
155+
value.append(String.format("/text()[%d]", nodePosition));
156+
}
157+
} else if (node.getNodeType() == Type.COMMENT) {
158+
// For a comment node: comment()[position] where position is an
159+
// integer representing the position of the selected node among
160+
// its comment node siblings.
161+
final int nodePosition = getNodePosition(node);
162+
if (nodePosition > 0) {
163+
value.append(String.format("/comment()[%d]", nodePosition));
164+
}
165+
} else if (node.getNodeType() == Type.PROCESSING_INSTRUCTION) {
166+
// For a processing-instruction node: processing-instruction(local)[position]
167+
// where local is the name of the processing instruction node and position is
168+
// an integer representing the position of the selected node among its
169+
// like-named processing-instruction node siblings.
170+
final int nodePosition = getNodePosition(node);
171+
if (nodePosition > 0) {
172+
value.append(String.format("/processing-instruction(%s)[%d]", node.getNodeName(), nodePosition));
173+
}
174+
} else if (node.getNodeType() == Type.NAMESPACE) {
175+
// For a namespace node: If the namespace node has a name: namespace::prefix,
176+
// where prefix is the local part of the name of the namespace node
177+
// (which represents the namespace prefix). If the namespace node
178+
// has no name (that is, it represents the default namespace):
179+
// namespace::*[Q{http://www.w3.org/2005/xpath-functions}local-name()=""]
180+
if (node.getNamespaceURI() != null) {
181+
value.append(String.format("namespace::{%s}", node.getLocalName()));
182+
} else {
183+
value.append("namespace::*[Q{http://www.w3.org/2005/xpath-functions}local-name()=\"\"]");
184+
}
185+
} else if (node.getLocalName() != null) {
186+
// For an element node, Q{uri}local[position], where uri is the
187+
// namespace URI of the node name or the empty string if the
188+
// node is in no namespace, local is the local part of the node
189+
// name, and position is an integer representing the position
190+
// of the selected node among its like-named siblings.
191+
final int nodePosition = getNodePosition(node);
192+
value.append((node.getOwnerDocument() != null && node.getOwnerDocument().getDocumentElement() != null) ? "/Q" : "Q");
193+
value.append(((INode) node).getQName().toURIQualifiedName());
194+
if (nodePosition > 0) {
195+
value.append(String.format("[%d]", nodePosition));
196+
}
197+
}
198+
199+
// For an attribute node, if the node is in no namespace,
200+
// @local, where local is the local part of the node name.
201+
// Otherwise, @Q{uri}local, where uri is the namespace URI of
202+
// the node name, and local is the local part of the node name.
203+
if (attributeNode != null) {
204+
attributeValue.append('/');
205+
if (attributeNode.getNamespaceURI() != null) {
206+
attributeValue.append(String.format("@Q{%s}", attributeNode.getNamespaceURI()));
207+
} else {
208+
attributeValue.append('@');
209+
}
210+
attributeValue.append(attributeNode.getLocalName());
211+
node = attributeNode;
212+
}
213+
214+
if (node.getParentNode() != null) {
215+
getPathValues(node.getParentNode(), values);
216+
}
217+
218+
if (value.toString().length() > 0) {
219+
values.add(value.toString());
220+
}
221+
222+
if (attributeValue.toString().length() > 0) {
223+
values.add(attributeValue.toString());
224+
}
31225
}
32226
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
(:
2+
: eXist-db Open Source Native XML Database
3+
: Copyright (C) 2001 The eXist-db Authors
4+
:
5+
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+
xquery version "3.1";
23+
24+
module namespace fnp="http://exist-db.org/xquery/test/function_path";
25+
26+
declare namespace test="http://exist-db.org/xquery/xqsuite";
27+
28+
declare
29+
%test:assertEmpty
30+
function fnp:empty() {
31+
fn:path()
32+
};
33+
34+
declare
35+
%test:assertEquals("/Q{http://example.com/one}p[1]/@author")
36+
function fnp:author() {
37+
let $e := document {
38+
<p xmlns="http://example.com/one" xml:lang="de" author="Friedrich von Schiller">
39+
Freude, schoner Gotterfunken,<br/>
40+
Tochter aus Elysium,<br/>
41+
Wir betreten feuertrunken,<br/>
42+
Himmlische, dein Heiligtum.</p>
43+
}
44+
return fn:path($e/*:p/@author)
45+
};
46+
47+
declare
48+
%test:assertEquals("/Q{http://example.com/one}p[1]/Q{http://example.com/one}br[2]")
49+
function fnp:br() {
50+
let $e := document {
51+
<p xmlns="http://example.com/one" xml:lang="de" author="Friedrich von Schiller">
52+
Freude, schoner Gotterfunken,<br/>
53+
Tochter aus Elysium,<br/>
54+
Wir betreten feuertrunken,<br/>
55+
Himmlische, dein Heiligtum.</p>
56+
}
57+
return fn:path($e/*:p/*:br[2])
58+
};
59+
60+
declare
61+
%test:assertEquals("/")
62+
function fnp:document() {
63+
let $e := document {
64+
<p xmlns="http://example.com/one" xml:lang="de" author="Friedrich von Schiller">
65+
Freude, schoner Gotterfunken,<br/>
66+
Tochter aus Elysium,<br/>
67+
Wir betreten feuertrunken,<br/>
68+
Himmlische, dein Heiligtum.</p>
69+
}
70+
return fn:path($e)
71+
};
72+
73+
declare
74+
%test:assertEquals("Q{http://www.w3.org/2005/xpath-functions}root()/Q{}empnr[1]")
75+
function fnp:empnr() {
76+
let $e := <employee xml:id="ID21256">
77+
<empnr>E21256</empnr>
78+
<first>John</first>
79+
<last>Brown</last>
80+
</employee>
81+
82+
return fn:path($e/empnr)
83+
};
84+
85+
declare
86+
%test:assertEquals("Q{http://www.w3.org/2005/xpath-functions}root()/@Q{http://www.w3.org/XML/1998/namespace}id")
87+
function fnp:id() {
88+
let $e := <employee xml:id="ID21256">
89+
<empnr>E21256</empnr>
90+
<first>John</first>
91+
<last>Brown</last>
92+
</employee>
93+
94+
return fn:path($e/@xml:id)
95+
};
96+
97+
declare
98+
%test:assertEquals("/Q{http://example.com/one}p[1]/@Q{http://www.w3.org/XML/1998/namespace}lang")
99+
function fnp:lang() {
100+
let $e := document {
101+
<p xmlns="http://example.com/one" xml:lang="de" author="Friedrich von Schiller">
102+
Freude, schoner Gotterfunken,<br/>
103+
Tochter aus Elysium,<br/>
104+
Wir betreten feuertrunken,<br/>
105+
Himmlische, dein Heiligtum.</p>
106+
}
107+
return fn:path($e/*:p/@xml:lang)
108+
};
109+
110+
declare
111+
%test:assertEquals("/Q{http://example.com/one}p[1]")
112+
function fnp:p() {
113+
let $e := document {
114+
<p xmlns="http://example.com/one" xml:lang="de" author="Friedrich von Schiller">
115+
Freude, schoner Gotterfunken,<br/>
116+
Tochter aus Elysium,<br/>
117+
Wir betreten feuertrunken,<br/>
118+
Himmlische, dein Heiligtum.</p>
119+
}
120+
return fn:path($e/*:p)
121+
};
122+
123+
declare
124+
%test:assertEquals("Q{http://www.w3.org/2005/xpath-functions}root()")
125+
function fnp:root() {
126+
let $e := <employee xml:id="ID21256">
127+
<empnr>E21256</empnr>
128+
<first>John</first>
129+
<last>Brown</last>
130+
</employee>
131+
132+
return fn:path($e)
133+
};
134+
135+
declare
136+
%test:assertEquals("/Q{http://example.com/one}p[1]/text()[2]")
137+
function fnp:text() {
138+
let $e := document {
139+
<p xmlns="http://example.com/one" xml:lang="de" author="Friedrich von Schiller">
140+
Freude, schoner Gotterfunken,<br/>
141+
Tochter aus Elysium,<br/>
142+
Wir betreten feuertrunken,<br/>
143+
Himmlische, dein Heiligtum.</p>
144+
}
145+
return fn:path($e//text()[starts-with(normalize-space(), 'Tochter')])
146+
};

0 commit comments

Comments
 (0)