Skip to content

Commit

Permalink
Merge pull request #4402 from evolvedbinary/feature/fn-path#0,#1
Browse files Browse the repository at this point in the history
  • Loading branch information
dizzzz authored Jun 17, 2022
2 parents 4fa59fd + ec80946 commit ba90070
Show file tree
Hide file tree
Showing 10 changed files with 731 additions and 87 deletions.
11 changes: 9 additions & 2 deletions exist-core/src/main/java/org/exist/dom/memtree/AttrImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,11 @@ public void setTextContent(final String textContent) throws DOMException {

@Override
public Element getOwnerElement() {
return (Element) document.getNode(document.attrParent[nodeNumber]);
final Node node = document.getNode(document.attrParent[nodeNumber]);
if (node != null && node.getNodeType() == Node.ELEMENT_NODE) {
return (Element) node;
}
return null;
}

@Override
Expand All @@ -149,7 +153,10 @@ public void selectAncestors(boolean includeSelf, NodeTest test, Sequence result)
if (test.matches(this)) {
result.add(this);
}
((NodeImpl)getOwnerElement()).selectAncestors(true, test, result);
final ElementImpl ownerElement = (ElementImpl) getOwnerElement();
if (ownerElement != null) {
ownerElement.selectAncestors(true, test, result);
}
}

@Override
Expand Down
16 changes: 13 additions & 3 deletions exist-core/src/main/java/org/exist/dom/memtree/DocumentImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,10 @@ public long getDocId() {
return docId;
}

public boolean isExplicitlyCreated() {
return explicitlyCreated;
}

public int addNode(final short kind, final short level, final QName qname) {
if(nodeKind == null) {
init();
Expand Down Expand Up @@ -1056,7 +1060,9 @@ protected void copyTo(NodeImpl node, final DocumentBuilderReceiver receiver, fin
nextNode = (NodeImpl) node.getFirstChild();
}
while(nextNode == null) {
copyEndNode(node, receiver);
if (node != null) {
copyEndNode(node, receiver);
}
if((top != null) && (top.nodeNumber == node.nodeNumber)) {
break;
}
Expand All @@ -1065,7 +1071,9 @@ protected void copyTo(NodeImpl node, final DocumentBuilderReceiver receiver, fin
if(nextNode == null) {
node = (NodeImpl) node.getParentNode();
if((node == null) || ((top != null) && (top.nodeNumber == node.nodeNumber))) {
copyEndNode(node, receiver);
if (node != null) {
copyEndNode(node, receiver);
}
break;
}
}
Expand Down Expand Up @@ -1304,7 +1312,9 @@ public void streamTo(final Serializer serializer, NodeImpl node, final Receiver
if(nextNode == null) {
node = (NodeImpl) node.getParentNode();
if((node == null) || ((top != null) && (top.nodeNumber == node.nodeNumber))) {
endNode(node, receiver);
if (node != null) {
endNode(node, receiver);
}
break;
}
}
Expand Down
16 changes: 13 additions & 3 deletions exist-core/src/main/java/org/exist/dom/memtree/NodeImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -270,13 +270,23 @@ public short getNodeType() {
@Override
public Node getParentNode() {
int next = document.next[nodeNumber];
while(next > nodeNumber) {
while (next > nodeNumber) {
next = document.next[next];
}
if(next < 0) {
if (next < 0) {
return null;
}
return document.getNode(next);
final NodeImpl parent = document.getNode(next);
if (parent.getNodeType() == DOCUMENT_NODE && !((DocumentImpl) parent).isExplicitlyCreated()) {
/*
All nodes in the MemTree will return an Owner document due to how the MemTree is implemented,
however the explicitlyCreated flag tells us whether there "really" was a Document Node or not.
See https://github.com/eXist-db/exist/issues/1463
*/
return null;
} else {
return parent;
}
}

public Node selectParentNode() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public void endDocument() throws SAXException {

@Override
public void startDocument() throws SAXException {
builder.startDocument();
builder.startDocument(true);
if(replaceAttributeFlag) {
builder.setReplaceAttributeFlag(replaceAttributeFlag);
}
Expand Down
2 changes: 1 addition & 1 deletion exist-core/src/main/java/org/exist/xquery/Function.java
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ private Expression checkArgumentType(Expression argument, @Nullable final Sequen
//Because () is seen as a node
(argType.getCardinality().isSuperCardinalityOrEqualOf(Cardinality.EMPTY_SEQUENCE) && returnType == Type.NODE))) {
LOG.debug(ExpressionDumper.dump(argument));
throw new XPathException(this, Messages.getMessage(Error.FUNC_PARAM_TYPE_STATIC,
throw new XPathException(this, ErrorCodes.XPTY0004, Messages.getMessage(Error.FUNC_PARAM_TYPE_STATIC,
String.valueOf(argPosition), mySignature, argType.toString(), Type.getTypeName(returnType)));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ public class FnModule extends AbstractInternalModule {
new FunctionDef(FunNumber.signatures[1], FunNumber.class),
new FunctionDef(FunOneOrMore.signature, FunOneOrMore.class),
new FunctionDef(FnOuterMost.FNS_OUTERMOST, FnOuterMost.class),
new FunctionDef(FunPath.FS_PATH_SIGNATURES[0], FunPath.class),
new FunctionDef(FunPath.FS_PATH_SIGNATURES[1], FunPath.class),
new FunctionDef(FunPosition.signature, FunPosition.class),
new FunctionDef(FunQName.signature, FunQName.class),
new FunctionDef(FunRemove.signature, FunRemove.class),
Expand Down
228 changes: 228 additions & 0 deletions exist-core/src/main/java/org/exist/xquery/functions/fn/FunPath.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
/*
* eXist-db Open Source Native XML Database
* Copyright (C) 2001 The eXist-db Authors
*
* info@exist-db.org
* http://www.exist-db.org
*
* This library 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.1 of the License, or (at your option) any later version.
*
* This library 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 library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/

package org.exist.xquery.functions.fn;

import org.exist.Namespaces;
import org.exist.dom.INode;
import org.exist.dom.QName;
import org.exist.xquery.*;
import org.exist.xquery.value.*;
import org.w3c.dom.*;

import javax.annotation.Nullable;
import java.util.LinkedList;
import java.util.List;

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

public class FunPath extends BasicFunction {

private static final QName FN_PATH_NAME = new QName("path", Function.BUILTIN_FUNCTION_NS);
private static final String FN_PATH_DESCRIPTION =
"Returns a path expression that can be used to select the supplied node " +
"relative to the root of its containing document.";
private static final FunctionReturnSequenceType FN_PATH_RETURN = new FunctionReturnSequenceType(
Type.STRING, Cardinality.ZERO_OR_ONE, "The path expression, or any empty sequence");

public static final FunctionSignature[] FS_PATH_SIGNATURES = {
functionSignature(FunPath.FN_PATH_NAME, FunPath.FN_PATH_DESCRIPTION, FunPath.FN_PATH_RETURN),
functionSignature(FunPath.FN_PATH_NAME, FunPath.FN_PATH_DESCRIPTION, FunPath.FN_PATH_RETURN,
new FunctionParameterSequenceType("node", Type.NODE, Cardinality.ZERO_OR_ONE, "The node for which to calculate a path expression"))
};

public FunPath(final XQueryContext context, final FunctionSignature signature) {
super(context, signature);
}

@Override
public Sequence eval(final Sequence[] args, final Sequence contextSequence) throws XPathException {
final Sequence result;
final Sequence sequence;

// The behavior of the function if the argument is omitted is exactly
// the same as if the context item (.) had been passed as the argument.
if (getArgumentCount() == 0) {
if (contextSequence != null) {
sequence = contextSequence;
} else {
sequence = Sequence.EMPTY_SEQUENCE;
}
} else {
sequence = args[0];
}

if (sequence.isEmpty()) {
// If $arg is the empty sequence, the function returns the empty sequence.
result = Sequence.EMPTY_SEQUENCE;
} else {
final Item item = sequence.itemAt(0);
if (item.getType() == Type.DOCUMENT) {
// If $arg is a document node, the function returns the string "/".
result = new StringValue("/");
} else if (Type.subTypeOf(item.getType(), Type.NODE)) {
// For an element node, Q{uri}local[position], where uri is the
// namespace URI of the node name or the empty string if the
// node is in no namespace, local is the local part of the node
// name, and position is an integer representing the position
// of the selected node among its like-named siblings.
final NodeValue nodeValue = (NodeValue) item;
final Node node = nodeValue.getNode();
final LinkedList<String> pathValues = new LinkedList<>();
getPathValues(node, pathValues);
if ((node.getOwnerDocument() == null ||
node.getOwnerDocument().getDocumentElement() == null ||
(node.getOwnerDocument() instanceof org.exist.dom.memtree.DocumentImpl &&
!((org.exist.dom.memtree.DocumentImpl) node.getOwnerDocument()).isExplicitlyCreated()))) {
// This string is prefixed by "Q{http://www.w3.org/2005/xpath-functions}root()"
// if the root node is not a document node.
pathValues.remove(0);
result = new StringValue(String.format("Q{%s}root()", Namespaces.XPATH_FUNCTIONS_NS) + String.join("", pathValues));
} else {
result = new StringValue(String.join("", pathValues));
}
} else {
// If the context item is not a node, type error [err:XPTY0004].
throw new XPathException(this, ErrorCodes.XPTY0004, "Item is not a document or node; got '" + Type.getTypeName(item.getType()) + "'", sequence);
}
}
return result;
}

/**
* Gets the position of a specified node among its like-named siblings.
*
* @param node the node whose position to get
* @return the position of the specified node, or zero if this method
* failed to determine the position of the specified node
*/
private static int getNodePosition(final Node node) {
int position = 1;
Node siblingNode = node.getPreviousSibling();
while (siblingNode != null) {
if (siblingNode.getNodeName().equals(node.getNodeName())) {
++position;
}
siblingNode = siblingNode.getPreviousSibling();
}
return position;
}

/**
* Gets the path values of a specified node.
*
* @param node the node whose path values to get
* @param values the path values
*/
private static void getPathValues(final Node node, final List<String> values) {
@Nullable Node parent = node.getParentNode();

final StringBuilder value = new StringBuilder();

switch (node.getNodeType()) {
case Node.ATTRIBUTE_NODE:
// For an attribute node, if the node is in no namespace,
// @local, where local is the local part of the node name.
// Otherwise, @Q{uri}local, where uri is the namespace URI of
// the node name, and local is the local part of the node name.
value.append('/');
if (node.getNamespaceURI() != null) {
value.append(String.format("@Q{%s}", node.getNamespaceURI()));
} else {
value.append('@');
}
value.append(node.getLocalName());

// attributes have an owner element - not a parent node!
parent = ((Attr) node).getOwnerElement();
break;

case Node.TEXT_NODE:
// For a text node: text()[position] where position is an integer
// representing the position of the selected node among its text
// node siblings
final int textNodePosition = getNodePosition(node);
if (textNodePosition > 0) {
value.append(String.format("/text()[%d]", textNodePosition));
}
break;

case Node.COMMENT_NODE:
// For a comment node: comment()[position] where position is an
// integer representing the position of the selected node among
// its comment node siblings.
final int commentNodePosition = getNodePosition(node);
if (commentNodePosition > 0) {
value.append(String.format("/comment()[%d]", commentNodePosition));
}
break;

case Node.PROCESSING_INSTRUCTION_NODE:
// For a processing-instruction node: processing-instruction(local)[position]
// where local is the name of the processing instruction node and position is
// an integer representing the position of the selected node among its
// like-named processing-instruction node siblings.
int processingInstructionNodePosition = getNodePosition(node);
if (processingInstructionNodePosition > 0) {
value.append(String.format("/processing-instruction(%s)[%d]", node.getNodeName(), processingInstructionNodePosition));
}
break;

case INode.NAMESPACE_NODE:
// For a namespace node: If the namespace node has a name: namespace::prefix,
// where prefix is the local part of the name of the namespace node
// (which represents the namespace prefix). If the namespace node
// has no name (that is, it represents the default namespace):
// namespace::*[Q{http://www.w3.org/2005/xpath-functions}local-name()=""]
if (node.getNamespaceURI() != null) {
value.append(String.format("namespace::{%s}", node.getLocalName()));
} else {
value.append("namespace::*[Q{http://www.w3.org/2005/xpath-functions}local-name()=\"\"]");
}
break;

default:
if (node.getLocalName() != null) {
// For an element node, Q{uri}local[position], where uri is the
// namespace URI of the node name or the empty string if the
// node is in no namespace, local is the local part of the node
// name, and position is an integer representing the position
// of the selected node among its like-named siblings.
final int nodePosition = getNodePosition(node);
value.append((node.getOwnerDocument() != null && node.getOwnerDocument().getDocumentElement() != null) ? "/Q" : "Q");
value.append(((INode) node).getQName().toURIQualifiedName());
if (nodePosition > 0) {
value.append(String.format("[%d]", nodePosition));
}
}
break;
}

if (parent != null) {
getPathValues(parent, values);
}

if (value.toString().length() > 0) {
values.add(value.toString());
}
}
}
Loading

0 comments on commit ba90070

Please sign in to comment.