Skip to content

Commit 608dddb

Browse files
committed
[feature] Add support for setting XQuery external variables of type array(*) via the eXist-db REST API
1 parent 4d13de3 commit 608dddb

File tree

5 files changed

+446
-211
lines changed

5 files changed

+446
-211
lines changed

exist-core/src/main/java/org/exist/http/RESTServer.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1534,10 +1534,10 @@ private void declareExternalAndXQJVariables(final XQueryContext context,
15341534
}
15351535

15361536
// get serialized sequence
1537-
final NodeImpl value = variable.getFirstChild(new NameTest(Type.ELEMENT, Marshaller.ROOT_ELEMENT_QNAME));
1537+
final NodeImpl value = variable.getFirstChild(new NameTest(Type.ELEMENT, Marshaller.SEQUENCE_ELEMENT_QNAME));
15381538
final Sequence sequence;
15391539
try {
1540-
sequence = value == null ? Sequence.EMPTY_SEQUENCE : Marshaller.demarshall(value);
1540+
sequence = value == null ? Sequence.EMPTY_SEQUENCE : Marshaller.demarshall(context, value);
15411541
} catch (final XMLStreamException xe) {
15421542
throw new XPathException((Expression) null, xe.toString());
15431543
}

exist-core/src/main/java/org/exist/xqj/Marshaller.java

Lines changed: 131 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@
5252
import org.exist.xquery.Expression;
5353
import org.exist.xquery.NameTest;
5454
import org.exist.xquery.XPathException;
55+
import org.exist.xquery.XQueryContext;
56+
import org.exist.xquery.functions.array.ArrayType;
5557
import org.exist.xquery.value.*;
5658
import org.w3c.dom.Comment;
5759
import org.w3c.dom.Document;
@@ -74,6 +76,8 @@
7476
import javax.xml.xquery.XQItemType;
7577
import java.io.Reader;
7678
import java.io.StringReader;
79+
import java.util.ArrayList;
80+
import java.util.List;
7781
import java.util.Properties;
7882

7983
import static org.exist.util.StringUtil.nullIfEmpty;
@@ -106,7 +110,7 @@ public class Marshaller {
106110

107111
private final static String ATTR_NAME = "name";
108112

109-
public final static QName ROOT_ELEMENT_QNAME = new QName(SEQ_ELEMENT, NAMESPACE, PREFIX);
113+
public final static QName SEQUENCE_ELEMENT_QNAME = new QName(SEQ_ELEMENT, NAMESPACE, PREFIX);
110114

111115
/**
112116
* Marshall a sequence in an xml based string representation.
@@ -253,135 +257,154 @@ public static Sequence demarshall(DBBroker broker, XMLStreamReader parser) throw
253257
return result;
254258
}
255259

256-
public static Sequence demarshall(final NodeImpl node) throws XMLStreamException, XPathException {
260+
public static Sequence demarshall(final XQueryContext context, final NodeImpl node) throws XMLStreamException, XPathException {
261+
return demarshallSequence(context, node);
262+
}
263+
264+
private static Sequence demarshallSequence(final XQueryContext context, final NodeImpl node) throws XMLStreamException, XPathException {
257265
final String ns = node.getNamespaceURI();
258266
if (ns == null || !NAMESPACE.equals(ns)) {
259-
throw new XMLStreamException("Root element is not in the correct namespace. Expected: " + NAMESPACE);
267+
throw new XMLStreamException("Sequence element is not in the correct namespace. Expected: " + NAMESPACE);
260268
}
261269
if (!SEQ_ELEMENT.equals(node.getLocalName())) {
262-
throw new XMLStreamException("Root element should be a " + SEQ_ELEMENT_QNAME);
270+
throw new XMLStreamException("Element should be a " + SEQ_ELEMENT_QNAME);
263271
}
272+
273+
return demarshallValues(context, node);
274+
}
275+
276+
private static Sequence demarshallValues(final XQueryContext context, final NodeImpl node) throws XMLStreamException, XPathException {
264277
final ValueSequence result = new ValueSequence();
265-
final InMemoryNodeSet values = new InMemoryNodeSet();
266-
node.selectChildren(new NameTest(Type.ELEMENT, VALUE_QNAME), values);
267-
for (final SequenceIterator i = values.iterate(); i.hasNext();) {
268-
final ElementImpl sxValue = (ElementImpl) i.nextItem();
269-
270-
int type = Type.ITEM;
271-
final String typeName = sxValue.getAttribute(ATTR_TYPE);
272-
if (!typeName.isEmpty()) {
273-
type = Type.getType(typeName);
274-
}
278+
final InMemoryNodeSet sxValues = new InMemoryNodeSet();
279+
node.selectChildren(new NameTest(Type.ELEMENT, VALUE_QNAME), sxValues);
280+
for (final SequenceIterator itSxValue = sxValues.iterate(); itSxValue.hasNext();) {
281+
final ElementImpl sxValue = (ElementImpl) itSxValue.nextItem();
282+
final Item item = demarshallValue(context, sxValue);
283+
result.add(item);
284+
}
285+
return result;
286+
}
287+
288+
private static Item demarshallValue(final XQueryContext context, final ElementImpl sxValue) throws XMLStreamException, XPathException {
289+
int type = Type.ITEM;
290+
final String typeName = sxValue.getAttribute(ATTR_TYPE);
291+
if (!typeName.isEmpty()) {
292+
type = Type.getType(typeName);
293+
}
294+
295+
@Nullable String attrNameString = null;
296+
if (sxValue instanceof Element) {
297+
attrNameString = nullIfEmpty(sxValue.getAttribute(ATTR_NAME));
298+
}
275299

276-
// TODO(AR) coerce the item type to the type desired for the variable binding
300+
final InMemoryNodeSet sxSequences = new InMemoryNodeSet();
301+
sxValue.selectChildren(new NameTest(Type.ELEMENT, SEQUENCE_ELEMENT_QNAME), sxSequences);
277302

278-
@Nullable String attrNameString = null;
279-
if (sxValue instanceof Element) {
280-
attrNameString = nullIfEmpty(sxValue.getAttribute(ATTR_NAME));
303+
Node item = sxValue.getFirstChild();
304+
305+
if (type == Type.ATTRIBUTE || (type == Type.ITEM && attrNameString != null)) {
306+
if (attrNameString.isEmpty()) {
307+
throw new XMLStreamException("sx:value must contain a name attribute if type is " + typeName);
281308
}
282-
Node item = sxValue.getFirstChild();
283309

284-
if (type == Type.ATTRIBUTE || (type == Type.ITEM && attrNameString != null)) {
285-
if (attrNameString.isEmpty()) {
286-
throw new XMLStreamException("sx:value must contain a name attribute if type is " + typeName);
310+
final String attrPrefix;
311+
final String attrNamespace;
312+
final String attrLocalName;
313+
final int colonSep = attrNameString.indexOf(':');
314+
if (colonSep > -1) {
315+
attrPrefix = attrNameString.substring(0, colonSep);
316+
attrNamespace = item.lookupNamespaceURI(attrPrefix);
317+
if (attrNamespace == null) {
318+
throw new XMLStreamException("sx:value's name attribute contains the QName prefix '" + attrPrefix + "' for which the namespace has not been declared if type is " + typeName);
287319
}
320+
attrLocalName = attrNameString.substring(colonSep + 1);
321+
} else {
322+
attrPrefix = XMLConstants.DEFAULT_NS_PREFIX;
323+
attrNamespace = XMLConstants.NULL_NS_URI;
324+
attrLocalName = attrNameString;
325+
}
288326

289-
final String attrPrefix;
290-
final String attrNamespace;
291-
final String attrLocalName;
292-
final int colonSep = attrNameString.indexOf(':');
293-
if (colonSep > -1) {
294-
attrPrefix = attrNameString.substring(0, colonSep);
295-
attrNamespace = item.lookupNamespaceURI(attrPrefix);
296-
if (attrNamespace == null) {
297-
throw new XMLStreamException("sx:value's name attribute contains the QName prefix '" + attrPrefix + "' for which the namespace has not been declared if type is " + typeName);
327+
final QName attrName = new QName(attrLocalName, attrNamespace, attrPrefix, ElementValue.ATTRIBUTE);
328+
final MemTreeBuilder builder = new MemTreeBuilder(context);
329+
builder.startDocument();
330+
final int attrNodeNumber = builder.addAttribute(attrName, sxValue.getTextContent());
331+
builder.endDocument();
332+
final AttrImpl attr = (AttrImpl) builder.getDocument().getAttribute(attrNodeNumber);
333+
return attr;
334+
335+
} else if (Type.subTypeOf(type, Type.NODE)) {
336+
337+
switch (type) {
338+
case Type.ELEMENT:
339+
if (item instanceof Document) {
340+
return (ElementImpl) ((DocumentImpl) item).getDocumentElement();
341+
} else if (!(item instanceof Element)) {
342+
throw new XMLStreamException("sx:value should only contain an Element if type is " + typeName);
343+
} else {
344+
return (ElementImpl) item;
298345
}
299-
attrLocalName = attrNameString.substring(colonSep + 1);
300-
} else {
301-
attrPrefix = XMLConstants.DEFAULT_NS_PREFIX;
302-
attrNamespace = XMLConstants.NULL_NS_URI;
303-
attrLocalName = attrNameString;
304-
}
305346

306-
final QName attrName = new QName(attrLocalName, attrNamespace, attrPrefix, ElementValue.ATTRIBUTE);
307-
final MemTreeBuilder builder = new MemTreeBuilder();
308-
builder.startDocument();
309-
final int attrNodeNumber = builder.addAttribute(attrName, sxValue.getTextContent());
310-
builder.endDocument();
311-
final AttrImpl attr = (AttrImpl) builder.getDocument().getAttribute(attrNodeNumber);
312-
result.add(attr);
313-
314-
} else if (Type.subTypeOf(type, Type.NODE)) {
315-
316-
switch (type) {
317-
case Type.ELEMENT:
318-
if (item instanceof Document) {
319-
result.add((ElementImpl) ((DocumentImpl) item).getDocumentElement());
320-
} else if (!(item instanceof Element)) {
321-
throw new XMLStreamException("sx:value should only contain an Element if type is " + typeName);
322-
} else {
323-
result.add((ElementImpl) item);
324-
}
325-
break;
326-
327-
case Type.COMMENT:
328-
if (!(item instanceof Comment)) {
329-
throw new XMLStreamException("sx:value should only contain a Comment node if type is " + typeName);
330-
}
331-
result.add((CommentImpl) item);
332-
break;
347+
case Type.COMMENT:
348+
if (!(item instanceof Comment)) {
349+
throw new XMLStreamException("sx:value should only contain a Comment node if type is " + typeName);
350+
}
351+
return (CommentImpl) item;
333352

334-
case Type.PROCESSING_INSTRUCTION:
335-
if (!(item instanceof ProcessingInstruction)) {
336-
throw new XMLStreamException("sx:value should only contain a Processing Instruction node if type is " + typeName);
337-
}
338-
result.add((ProcessingInstructionImpl) item);
339-
break;
353+
case Type.PROCESSING_INSTRUCTION:
354+
if (!(item instanceof ProcessingInstruction)) {
355+
throw new XMLStreamException("sx:value should only contain a Processing Instruction node if type is " + typeName);
356+
}
357+
return (ProcessingInstructionImpl) item;
340358

341-
case Type.TEXT:
342-
if (!(item instanceof Text)) {
343-
throw new XMLStreamException("sx:value should only contain a Text node if type is " + typeName);
344-
}
345-
result.add((TextImpl) item);
346-
break;
347-
348-
case Type.DOCUMENT:
349-
default:
350-
if (item instanceof Document || item instanceof Element) {
351-
final DocumentBuilderReceiver receiver = new DocumentBuilderReceiver(((NodeImpl) item).getExpression());
352-
try {
353-
receiver.startDocument();
354-
((NodeImpl) item).copyTo(null, receiver);
355-
receiver.endDocument();
356-
} catch (final SAXException e) {
357-
throw new XPathException(item != null ? ((NodeImpl) item).getExpression() : null, "Error while demarshalling node: " + e.getMessage(), e);
358-
}
359-
result.add((NodeImpl) receiver.getDocument());
360-
} else {
361-
throw new XMLStreamException("sx:value should only contain a Node if type is " + typeName);
359+
case Type.TEXT:
360+
if (!(item instanceof Text)) {
361+
throw new XMLStreamException("sx:value should only contain a Text node if type is " + typeName);
362+
}
363+
return (TextImpl) item;
364+
365+
case Type.DOCUMENT:
366+
default:
367+
if (item instanceof Document || item instanceof Element) {
368+
final DocumentBuilderReceiver receiver = new DocumentBuilderReceiver(((NodeImpl) item).getExpression());
369+
try {
370+
receiver.startDocument();
371+
((NodeImpl) item).copyTo(null, receiver);
372+
receiver.endDocument();
373+
} catch (final SAXException e) {
374+
throw new XPathException(item != null ? ((NodeImpl) item).getExpression() : null, "Error while demarshalling node: " + e.getMessage(), e);
362375
}
363-
break;
364-
}
376+
return (NodeImpl) receiver.getDocument();
377+
} else {
378+
throw new XMLStreamException("sx:value should only contain a Node if type is " + typeName);
379+
}
380+
}
365381

366-
} else if (type == Type.ITEM && !(item instanceof Text)) {
367-
// item() type requested and we have been given a node which is not a text() node
368-
result.add((NodeImpl) item);
382+
} else if (type == Type.ITEM && !(item instanceof Text)) {
383+
// item() type requested and we have been given a node which is not a text() node
384+
return (NodeImpl) item;
385+
386+
} else if (type == Type.ARRAY_ITEM || (type == Type.ITEM && !sxSequences.isEmpty())) {
387+
// array(*) type
388+
final List<Sequence> arrayValues = new ArrayList<>();
389+
for (final SequenceIterator itSxSequence = sxSequences.iterate(); itSxSequence.hasNext();) {
390+
final ElementImpl sxSequence = (ElementImpl) itSxSequence.nextItem();
391+
final Sequence arrayValue = demarshallSequence(context, sxSequence);
392+
arrayValues.add(arrayValue);
393+
}
394+
return new ArrayType(context, arrayValues);
369395

370-
} else {
371-
// specific non-node type or text()
372-
final StringBuilder data = new StringBuilder();
373-
while (item != null) {
374-
if (!(item.getNodeType() == Node.TEXT_NODE || item.getNodeType() == Node.CDATA_SECTION_NODE)) {
375-
throw new XMLStreamException("sx:value should only contain text if type is " + typeName);
376-
}
377-
data.append(item.getNodeValue());
378-
item = item.getNextSibling();
396+
} else {
397+
// specific non-node type or text()
398+
final StringBuilder data = new StringBuilder();
399+
while (item != null) {
400+
if (!(item.getNodeType() == Node.TEXT_NODE || item.getNodeType() == Node.CDATA_SECTION_NODE)) {
401+
throw new XMLStreamException("sx:value should only contain text if type is " + typeName);
379402
}
380-
result.add(new StringValue(data.toString()).convertTo(type));
403+
data.append(item.getNodeValue());
404+
item = item.getNextSibling();
381405
}
406+
return new StringValue(data.toString()).convertTo(type);
382407
}
383-
384-
return result;
385408
}
386409

387410
public static Item streamToDOM(XMLStreamReader parser, XQItemType type) throws XMLStreamException, XQException {

exist-core/src/main/xsd/rest-serialized-sequence.xsd

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,28 @@
2424
-->
2525
<xs:schema
2626
xmlns:xs="http://www.w3.org/2001/XMLSchema"
27+
xmlns:sx="http://exist-db.org/xquery/types/serialized"
2728
targetNamespace="http://exist-db.org/xquery/types/serialized"
2829
elementFormDefault="qualified">
29-
30+
3031
<xs:element name="sequence">
3132
<xs:complexType>
3233
<xs:sequence>
33-
<xs:element name="value" minOccurs="0" maxOccurs="unbounded">
34-
<xs:complexType mixed="true">
35-
<xs:attribute name="type" type="xs:string" default="item()"/>
36-
<xs:attribute name="name" type="xs:string" use="optional"/>
37-
</xs:complexType>
38-
</xs:element>
34+
<xs:element ref="sx:value" minOccurs="0" maxOccurs="unbounded"/>
3935
</xs:sequence>
4036
</xs:complexType>
4137
</xs:element>
42-
38+
39+
<xs:element name="value">
40+
<xs:complexType mixed="true">
41+
<xs:sequence>
42+
<xs:choice minOccurs="0">
43+
<xs:any processContents="skip"/>
44+
</xs:choice>
45+
</xs:sequence>
46+
<xs:attribute name="type" type="xs:string" default="item()"/>
47+
<xs:attribute name="name" type="xs:string" use="optional"/>
48+
</xs:complexType>
49+
</xs:element>
50+
4351
</xs:schema>

0 commit comments

Comments
 (0)