Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,4 @@ dependency-reduced-pom.xml
.vscode

# OS specific files
.DS_Store

.DS_Store
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,23 @@
import net.sf.saxon.type.BuiltInAtomicType;
import org.exist.dom.QName;
import org.exist.dom.memtree.DocumentImpl;
import org.exist.dom.persistent.NodeProxy;
import org.exist.xquery.ErrorCodes;
import org.exist.xquery.XPathException;
import org.exist.xquery.functions.array.ArrayType;
import org.exist.xquery.functions.fn.FnTransform;
import org.exist.xquery.functions.map.AbstractMapType;
import org.exist.xquery.value.*;
import org.w3c.dom.Document;
import org.w3c.dom.Node;

import io.lacuna.bifurcan.IEntry;

import javax.xml.transform.dom.DOMSource;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* Type conversion to and from Saxon
Expand Down Expand Up @@ -119,11 +125,18 @@ static net.sf.saxon.s9api.QName of(final QNameValue qName) {
}

XdmValue of(final Item item) throws XPathException {
if (item instanceof NodeProxy) {
return ofNode(((NodeProxy) item).getNode());
}
Comment thread
reinhapa marked this conversation as resolved.
final int itemType = item.getType();
if (Type.subTypeOf(itemType, Type.ATOMIC)) {
return ofAtomic((AtomicValue) item);
} else if (Type.subTypeOf(itemType, Type.NODE)) {
return ofNode((Node) item);
} else if (Type.subTypeOf(itemType, Type.MAP)) {
return ofMap((AbstractMapType) item);
} else if (Type.subTypeOf(itemType, Type.ARRAY)) {
return ofArray((ArrayType) item);
}
throw new XPathException(ErrorCodes.XPTY0004,
"Item " + item + " of type " + Type.getTypeName(itemType) + COULD_NOT_BE_CONVERTED + "XdmValue");
Expand All @@ -140,14 +153,12 @@ static private XdmValue ofAtomic(final AtomicValue atomicValue) throws XPathExce
} else if (Type.subTypeOf(itemType, Type.STRING)) {
return XdmValue.makeValue(((StringValue) atomicValue).getStringValue());
}

throw new XPathException(ErrorCodes.XPTY0004,
"Atomic value " + atomicValue + " of type " + Type.getTypeName(itemType) +
COULD_NOT_BE_CONVERTED + "XdmValue");
}

private XdmValue ofNode(final Node node) throws XPathException {

final DocumentBuilder sourceBuilder = newDocumentBuilder();
try {
if (node instanceof DocumentImpl) {
Expand All @@ -167,6 +178,25 @@ private XdmValue ofNode(final Node node) throws XPathException {
}
}

private XdmValue ofMap(final AbstractMapType map) throws XPathException {
Map<XdmAtomicValue, XdmValue> xdmMap = new HashMap<XdmAtomicValue, XdmValue>();
for (IEntry<AtomicValue, Sequence> entry : map) {
XdmAtomicValue key = (XdmAtomicValue) ofAtomic(entry.key());
XdmValue value = of(entry.value());
xdmMap.put(key, value);
}
return new XdmMap(xdmMap);
}

private XdmValue ofArray(final ArrayType array) throws XPathException {
int size = array.getSize();
XdmValue[] members = new XdmValue[size];
for (int i = 0; i < size; ++i) {
members[i] = of(array.get(i));
}
return new XdmArray(members);
}

XdmValue[] of(final ArrayType values) throws XPathException {
final int size = values.getSize();
final XdmValue[] result = new XdmValue[size];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,10 @@ public Sequence eval(final Sequence[] args, final Sequence contextSequence) thro
final Transform.TemplateInvocation invocation = new Transform.TemplateInvocation(
options, sourceNode, delivery, xslt30Transformer, resultDocuments);
return invocation.invoke();
} catch (final SaxonApiException | UncheckedXPathException e) {
throw originalXPathException("Could not transform input: ", e, ErrorCodes.FOXT0003);
} catch (final SaxonApiException e) {
throw originalXPathException("Could not transform with "+options.xsltSource._1+" line "+e.getLineNumber()+": ", e, ErrorCodes.FOXT0003);
} catch (final UncheckedXPathException e) {
throw originalXPathException("Could not transform with "+options.xsltSource._1+" line "+e.getXPathException().getLocationAsString()+": ", e, ErrorCodes.FOXT0003);
}

} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

import net.sf.saxon.s9api.XdmNode;
import org.exist.xquery.value.NodeValue;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Node;

Expand Down Expand Up @@ -64,20 +65,41 @@ static StringBuilder pathTo(final Node node) {
static List<Integer> treeIndex(final Node node) {
final Node parent = node.getParentNode();
if (parent == null) {
return new ArrayList<>();
final List<Integer> index = new ArrayList<>();
// The root element always index 0 within the document node.
// Some node implementations (e.g., org.exist.dom.memtree.NodeImpl) do not always have an associated document.
// In this case, the nodeIndex must get an extra 0 index to be valid for xdmDocument.
if (! (node instanceof Document)) {
index.add(0);
}
return index;
}
final List<Integer> index = treeIndex(parent);
Node sibling = node.getPreviousSibling();
Node sibling = previousSiblingNotAttribute(node);
int position = 0;
while (sibling != null) {
position += 1;
sibling = sibling.getPreviousSibling();
sibling = previousSiblingNotAttribute(sibling);
}
index.add(position);

return index;
}

/**
* A org.exist.dom.persistent.StoredNode returns attributes of an element as previous siblings of the element's children.
* This is not compatible with the way xdmNodeAtIndex works, so we need to compensate for this.
* @param node
* @return the previous sibling of `node` that is not an attribute.
*/
private static Node previousSiblingNotAttribute(Node node) {
Node sibling = node.getPreviousSibling();
if (sibling instanceof Attr) {
return null;
}
return sibling;
}

static XdmNode xdmNodeAtIndex(final XdmNode xdmNode, final List<Integer> index) {
if (index.isEmpty()) {
return xdmNode;
Expand Down
126 changes: 126 additions & 0 deletions exist-core/src/test/xquery/xquery3/transform/fnTransform5882.xqm
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
(:
: 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
:)
xquery version "3.1";

module namespace testTransform="http://exist-db.org/xquery/test/function_transform";
import module namespace xmldb="http://exist-db.org/xquery/xmldb";

declare namespace test="http://exist-db.org/xquery/xqsuite";
declare namespace xsl="http://www.w3.org/1999/XSL/Transform";

(:~
: Regression test for PR 5882: fn:transform with MemTree nodes.
: A bare element constructor (element root { ... }) creates a MemTree node whose
: getParentNode() returns null (see issue #1463). We use initial-match-selection
: because that path calls Convert.ofNode/treeIndex. Without the TreeUtils fix:
: - For root: treeIndex returns [], xdmNodeAtIndex returns doc (wrong but works)
: - For second child: treeIndex returns [1] (missing leading 0 for root), doc has
: only one child, so xdmNodeAtIndex(doc, [1]) returns null, causing failure.
: @see https://github.com/eXist-db/exist/pull/5882
:)
declare variable $testTransform:transform-5882-simple-xsl := <xsl:stylesheet version='1.0'>
<xsl:template match='*'>
<out><xsl:value-of select='.'/></out>
</xsl:template>
</xsl:stylesheet>;

declare variable $testTransform:transform-5882-map-array-xsl := <xsl:stylesheet version='3.0'>
<xsl:param name='m' as='map(*)'/>
<xsl:param name='a' as='array(*)'/>
<xsl:template name='main'>
<out>
<xsl:attribute name='map-val' select='$m("k")'/>
<xsl:attribute name='array-val' select='$a(2)'/>
</out>
</xsl:template>
</xsl:stylesheet>;

declare variable $testTransform:transform-5882-stored-doc := <root id="attr"><child>stored-val</child></root>;

declare variable $testTransform:transform-5882-coll := "/db/fn-transform-5882-stored";

declare
%test:setUp
function testTransform:setup() {
xmldb:create-collection("/db", "fn-transform-5882-stored"),
xmldb:store($testTransform:transform-5882-coll, "doc.xml", $testTransform:transform-5882-stored-doc)
};

declare
%test:tearDown
function testTransform:tearDown() {
xmldb:remove($testTransform:transform-5882-coll)
};

(:~
: Select the second child of a MemTree root. Without the fix, treeIndex(b) = [1]
: (missing index 0 for root), but doc has only one child; xdmNodeAtIndex returns null.
:)
declare
%test:assertEquals('<out>second</out>')
function testTransform:memtree-second-child() {
let $source := element root { <a>first</a>, <b>second</b> }
let $selection := $source/b
let $result := (fn:transform(map{
"initial-match-selection": $selection,
"stylesheet-node": $testTransform:transform-5882-simple-xsl
}))?output
return $result
};

(:~
: PR 5882 adds Convert.ofMap/ofArray for stylesheet params. Without it, map/array
: params cause XPTY0004. XSLT 3.0 required for map(*) and array(*) param types.
: @see https://github.com/eXist-db/exist/pull/5882
:)
declare
%test:assertEquals('v', 'y')
function testTransform:stylesheet-params-map-array() {
let $result := fn:transform(map{
"stylesheet-node": $testTransform:transform-5882-map-array-xsl,
"initial-template": QName('','main'),
"stylesheet-params": map {
QName('','m'): map{'k':'v'},
QName('','a'): ['x','y','z']
}
})?output
return
($result/out/@map-val/string(), $result/out/@array-val/string())

};

(:~
: PR 5882 fixes TreeUtils.previousSiblingNotAttribute for StoredNode: attributes
: appear as previous siblings of element children. Store a doc with attrs, select
: the child element as initial-match-selection.
: @see https://github.com/eXist-db/exist/pull/5882
:)
declare
%test:assertEquals('<out>stored-val</out>')
function testTransform:stored-doc-with-attributes() {
let $selection := doc($testTransform:transform-5882-coll || "/doc.xml")/root/child
let $result := (fn:transform(map{
"initial-match-selection": $selection,
"stylesheet-node": $testTransform:transform-5882-simple-xsl
}))?output
return $result
};
Loading