From f8f032cbb1870d0001d40eb5a230e7361ce55bf6 Mon Sep 17 00:00:00 2001 From: Adam Retter Date: Tue, 28 Apr 2026 15:37:52 +0200 Subject: [PATCH 01/18] [ignore] Code cleanup --- .../src/main/java/org/exist/Namespaces.java | 1 + .../main/java/org/exist/http/RESTServer.java | 21 +++++----- .../exist/storage/serializers/Serializer.java | 9 +++-- .../org/exist/test/ExistEmbeddedServer.java | 2 +- .../main/java/org/exist/util/Collations.java | 1 + .../main/java/org/exist/xqj/Marshaller.java | 39 +++++++++++-------- .../java/org/exist/xquery/XQueryContext.java | 4 +- .../exist/xquery/EmbeddedBinariesTest.java | 2 +- .../modules/file/EmbeddedBinariesTest.java | 2 +- 9 files changed, 45 insertions(+), 36 deletions(-) diff --git a/exist-core/src/main/java/org/exist/Namespaces.java b/exist-core/src/main/java/org/exist/Namespaces.java index 187f8d38cd..4a284ffb9a 100644 --- a/exist-core/src/main/java/org/exist/Namespaces.java +++ b/exist-core/src/main/java/org/exist/Namespaces.java @@ -61,6 +61,7 @@ public interface Namespaces { String DTD_NS = XMLConstants.XML_DTD_NS_URI; String SCHEMA_NS = XMLConstants.W3C_XML_SCHEMA_NS_URI; + String SCHEMA_NS_PREFIX = "xs"; String SCHEMA_DATATYPES_NS = "http://www.w3.org/2001/XMLSchema-datatypes"; String SCHEMA_INSTANCE_NS = XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI; diff --git a/exist-core/src/main/java/org/exist/http/RESTServer.java b/exist-core/src/main/java/org/exist/http/RESTServer.java index 8254b7743d..1412d23e66 100644 --- a/exist-core/src/main/java/org/exist/http/RESTServer.java +++ b/exist-core/src/main/java/org/exist/http/RESTServer.java @@ -1481,7 +1481,7 @@ private HttpRequestWrapper declareVariables(final XQueryContext context, final ResponseWrapper respw = new HttpResponseWrapper(response); context.setHttpContext(new XQueryContext.HttpContext(reqw, respw)); - //enable EXQuery Request Module (if present) + // enable EXQuery Request Module (if present) try { if(xqueryContextExqueryRequestAttribute != null && cstrHttpServletRequestAdapter != null) { final HttpRequest exqueryRequestAdapter = cstrHttpServletRequestAdapter.apply(request, () -> (String)context.getBroker().getConfiguration().getProperty(Configuration.BINARY_CACHE_CLASS_PROPERTY)); @@ -2073,10 +2073,10 @@ protected void writeCollection(final HttpServletResponse response, serializer.setOutput(writer, defaultProperties); serializer.startDocument(); - serializer.startPrefixMapping("exist", Namespaces.EXIST_NS); + serializer.startPrefixMapping(Namespaces.EXIST_NS_PREFIX, Namespaces.EXIST_NS); if (wrap) { - serializer.startElement(Namespaces.EXIST_NS, "result", "exist:result", null); + serializer.startElement(Namespaces.EXIST_NS, "result", Namespaces.EXIST_NS_PREFIX + ":result", null); } final AttributesImpl attrs = new AttributesImpl(); @@ -2096,8 +2096,7 @@ protected void writeCollection(final HttpServletResponse response, addPermissionAttributes(attrs, collection.getPermissionsNoLock()); - serializer.startElement(Namespaces.EXIST_NS, "collection", - "exist:collection", attrs); + serializer.startElement(Namespaces.EXIST_NS, "collection", Namespaces.EXIST_NS_PREFIX + ":collection", attrs); for (final Iterator i = collection.collectionIterator(broker); i.hasNext();) { final XmldbURI child = i.next(); @@ -2120,8 +2119,8 @@ protected void writeCollection(final HttpServletResponse response, } addPermissionAttributes(attrs, childCollection.getPermissionsNoLock()); - serializer.startElement(Namespaces.EXIST_NS, "collection", "exist:collection", attrs); - serializer.endElement(Namespaces.EXIST_NS, "collection", "exist:collection"); + serializer.startElement(Namespaces.EXIST_NS, "collection", Namespaces.EXIST_NS_PREFIX + ":collection", attrs); + serializer.endElement(Namespaces.EXIST_NS, "collection", Namespaces.EXIST_NS_PREFIX + ":collection"); } } @@ -2158,15 +2157,15 @@ protected void writeCollection(final HttpServletResponse response, } addPermissionAttributes(attrs, doc.getPermissions()); - serializer.startElement(Namespaces.EXIST_NS, "resource", "exist:resource", attrs); - serializer.endElement(Namespaces.EXIST_NS, "resource", "exist:resource"); + serializer.startElement(Namespaces.EXIST_NS, "resource", Namespaces.EXIST_NS_PREFIX + ":resource", attrs); + serializer.endElement(Namespaces.EXIST_NS, "resource", Namespaces.EXIST_NS_PREFIX + ":resource"); } } - serializer.endElement(Namespaces.EXIST_NS, "collection", "exist:collection"); + serializer.endElement(Namespaces.EXIST_NS, "collection", Namespaces.EXIST_NS_PREFIX + ":collection"); if (wrap) { - serializer.endElement(Namespaces.EXIST_NS, "result", "exist:result"); + serializer.endElement(Namespaces.EXIST_NS, "result", Namespaces.EXIST_NS_PREFIX + ":result"); } serializer.endDocument(); diff --git a/exist-core/src/main/java/org/exist/storage/serializers/Serializer.java b/exist-core/src/main/java/org/exist/storage/serializers/Serializer.java index 478d1b38e7..6852600c70 100644 --- a/exist-core/src/main/java/org/exist/storage/serializers/Serializer.java +++ b/exist-core/src/main/java/org/exist/storage/serializers/Serializer.java @@ -52,6 +52,7 @@ import java.util.*; import javax.annotation.Nullable; +import javax.xml.XMLConstants; import javax.xml.transform.OutputKeys; import javax.xml.transform.Source; import javax.xml.transform.Templates; @@ -1091,7 +1092,7 @@ public void toSAX(final Sequence seq, int start, final int count, final boolean documentStarted = true; } if (wrap) { - receiver.startPrefixMapping("exist", Namespaces.EXIST_NS); + receiver.startPrefixMapping(Namespaces.EXIST_NS_PREFIX, Namespaces.EXIST_NS); receiver.startElement(ELEM_RESULT_QNAME, attrs); } @@ -1106,7 +1107,7 @@ public void toSAX(final Sequence seq, int start, final int count, final boolean if (wrap) { receiver.endElement(ELEM_RESULT_QNAME); - receiver.endPrefixMapping("exist"); + receiver.endPrefixMapping(Namespaces.EXIST_NS_PREFIX); } receiver.endDocument(); } @@ -1178,7 +1179,7 @@ public void toSAX(final Item item, final boolean wrap, final boolean typed) thro } if (wrap) { - receiver.startPrefixMapping("exist", Namespaces.EXIST_NS); + receiver.startPrefixMapping(Namespaces.EXIST_NS_PREFIX, Namespaces.EXIST_NS); receiver.startElement(ELEM_RESULT_QNAME, attrs); } @@ -1186,7 +1187,7 @@ public void toSAX(final Item item, final boolean wrap, final boolean typed) thro if (wrap) { receiver.endElement(ELEM_RESULT_QNAME); - receiver.endPrefixMapping("exist"); + receiver.endPrefixMapping(Namespaces.EXIST_NS_PREFIX); } receiver.endDocument(); diff --git a/exist-core/src/main/java/org/exist/test/ExistEmbeddedServer.java b/exist-core/src/main/java/org/exist/test/ExistEmbeddedServer.java index 108a6d4be6..325e24561f 100644 --- a/exist-core/src/main/java/org/exist/test/ExistEmbeddedServer.java +++ b/exist-core/src/main/java/org/exist/test/ExistEmbeddedServer.java @@ -94,7 +94,7 @@ public ExistEmbeddedServer(final String instanceName, final Path configFile, fin this(instanceName, configFile, configProperties, false, false); } - public ExistEmbeddedServer(@Nullable final String instanceName, @Nullable final Path configFile, @Nullable final Properties configProperties, @Nullable final boolean disableAutoDeploy, @Nullable final boolean useTemporaryStorage) { + public ExistEmbeddedServer(@Nullable final String instanceName, @Nullable final Path configFile, @Nullable final Properties configProperties, final boolean disableAutoDeploy, final boolean useTemporaryStorage) { this.instanceName = Optional.ofNullable(instanceName); this.configFile = Optional.ofNullable(configFile); this.configProperties = Optional.ofNullable(configProperties); diff --git a/exist-core/src/main/java/org/exist/util/Collations.java b/exist-core/src/main/java/org/exist/util/Collations.java index e4a00820dd..cb65b5f446 100644 --- a/exist-core/src/main/java/org/exist/util/Collations.java +++ b/exist-core/src/main/java/org/exist/util/Collations.java @@ -309,6 +309,7 @@ public class Collations { } else if (uri.startsWith("java:")) { // java class specified: this should be a subclass of // com.ibm.icu.text.RuleBasedCollator + // TODO(AR) RuleBasedCollator is a final class - we should only need to sub-class Collator.class final String uriClassName = uri.substring("java:".length()); try { final Class collatorClass = Class.forName(uriClassName); diff --git a/exist-core/src/main/java/org/exist/xqj/Marshaller.java b/exist-core/src/main/java/org/exist/xqj/Marshaller.java index cb277e6c3e..e6e9410810 100644 --- a/exist-core/src/main/java/org/exist/xqj/Marshaller.java +++ b/exist-core/src/main/java/org/exist/xqj/Marshaller.java @@ -100,15 +100,13 @@ public class Marshaller { public final static String NAMESPACE = "http://exist-db.org/xquery/types/serialized"; public final static String PREFIX = "sx"; - private final static Properties DEFAULT_OUTPUT_PROPERTIES = new Properties(); private final static String VALUE_ELEMENT = "value"; - private final static String VALUE_ELEMENT_QNAME = PREFIX + ":value"; - private final static QName VALUE_QNAME = new QName(VALUE_ELEMENT, NAMESPACE, PREFIX); + private final static String VALUE_ELEMENT_PREFIXED_NAME = PREFIX + ":value"; private final static String SEQ_ELEMENT = "sequence"; - private final static String SEQ_ELEMENT_QNAME = PREFIX + ":sequence"; + private final static String SEQ_ELEMENT_PREFIXED_NAME = PREFIX + ":sequence"; private final static String ATTR_TYPE = "type"; private final static String ATTR_ITEM_TYPE = "item-type"; @@ -116,6 +114,7 @@ public class Marshaller { private final static String ATTR_NAME = "name"; public final static QName SEQUENCE_ELEMENT_QNAME = new QName(SEQ_ELEMENT, NAMESPACE, PREFIX); + public final static QName VALUE_ELEMENT_QNAME = new QName(VALUE_ELEMENT, NAMESPACE, PREFIX); public final static QName ENTRY_ELEMENT_QNAME = new QName("entry", NAMESPACE, PREFIX); public final static QName KEY_ELEMENT_QNAME = new QName("key", NAMESPACE, PREFIX); @@ -133,11 +132,11 @@ public static void marshall(final DBBroker broker, final Sequence seq, final Con throws XPathException, SAXException { final AttributesImpl attrs = new AttributesImpl(); attrs.addAttribute("", ATTR_ITEM_TYPE, ATTR_ITEM_TYPE, "CDATA", Type.getTypeName(seq.getItemType())); - handler.startElement(NAMESPACE, SEQ_ELEMENT, SEQ_ELEMENT_QNAME, attrs); + handler.startElement(NAMESPACE, SEQ_ELEMENT, SEQ_ELEMENT_PREFIXED_NAME, attrs); for (final SequenceIterator i = seq.iterate(); i.hasNext(); ) { marshallItem(broker, i.nextItem(), handler); } - handler.endElement(NAMESPACE, SEQ_ELEMENT, SEQ_ELEMENT_QNAME); + handler.endElement(NAMESPACE, SEQ_ELEMENT, SEQ_ELEMENT_PREFIXED_NAME); } @@ -157,12 +156,12 @@ public static void marshall(final DBBroker broker, final Sequence seq, final int final ContentHandler handler) throws XPathException, SAXException { final AttributesImpl attrs = new AttributesImpl(); attrs.addAttribute("", ATTR_ITEM_TYPE, ATTR_ITEM_TYPE, "CDATA", Type.getTypeName(seq.getItemType())); - handler.startElement(NAMESPACE, SEQ_ELEMENT, SEQ_ELEMENT_QNAME, attrs); + handler.startElement(NAMESPACE, SEQ_ELEMENT, SEQ_ELEMENT_PREFIXED_NAME, attrs); for (int i = start; i < howmany && i < seq.getItemCount(); i++ ) { marshallItem(broker, seq.itemAt(i), handler); } - handler.endElement(NAMESPACE, SEQ_ELEMENT, SEQ_ELEMENT_QNAME); + handler.endElement(NAMESPACE, SEQ_ELEMENT, SEQ_ELEMENT_PREFIXED_NAME); } /** @@ -202,15 +201,15 @@ public static void marshallItem(final DBBroker broker, final Item item, final Co } attrs.addAttribute("", ATTR_TYPE, ATTR_TYPE, "CDATA", Type.getTypeName(type)); if (Type.subTypeOf(item.getType(), Type.NODE)) { - handler.startElement(NAMESPACE, VALUE_ELEMENT, VALUE_ELEMENT_QNAME, attrs); + handler.startElement(NAMESPACE, VALUE_ELEMENT, VALUE_ELEMENT_PREFIXED_NAME, attrs); final NodeValue nv = (NodeValue) item; nv.toSAX(broker, handler, outputProperties); - handler.endElement(NAMESPACE, VALUE_ELEMENT, VALUE_ELEMENT_QNAME); + handler.endElement(NAMESPACE, VALUE_ELEMENT, VALUE_ELEMENT_PREFIXED_NAME); } else { - handler.startElement(NAMESPACE, VALUE_ELEMENT, VALUE_ELEMENT_QNAME, attrs); + handler.startElement(NAMESPACE, VALUE_ELEMENT, VALUE_ELEMENT_PREFIXED_NAME, attrs); final String value = item.getStringValue(); handler.characters(value.toCharArray(), 0, value.length()); - handler.endElement(NAMESPACE, VALUE_ELEMENT, VALUE_ELEMENT_QNAME); + handler.endElement(NAMESPACE, VALUE_ELEMENT, VALUE_ELEMENT_PREFIXED_NAME); } } @@ -246,11 +245,13 @@ public static Sequence demarshall(final XMLStreamReader parser) throws XMLStream while (event != XMLStreamConstants.START_ELEMENT) { event = parser.next(); } + if (!NAMESPACE.equals(parser.getNamespaceURI())) { throw new XMLStreamException("Root element is not in the correct namespace. Expected: " + NAMESPACE); } + if (!SEQ_ELEMENT.equals(parser.getLocalName())) { - throw new XMLStreamException("Root element should be a " + SEQ_ELEMENT_QNAME); + throw new XMLStreamException("Root element should be a " + SEQ_ELEMENT_PREFIXED_NAME); } final ValueSequence result = new ValueSequence(); while ((event = parser.next()) != XMLStreamConstants.END_DOCUMENT) { @@ -280,12 +281,15 @@ public static Sequence demarshall(final XMLStreamReader parser) throws XMLStream result.add(item); } break; + case XMLStreamConstants.END_ELEMENT : - if (NAMESPACE.equals(parser.getNamespaceURI()) && SEQ_ELEMENT.equals(parser.getLocalName())) - {return result;} + if (NAMESPACE.equals(parser.getNamespaceURI()) && SEQ_ELEMENT.equals(parser.getLocalName())) { + return result; + } break; } } + return result; } @@ -299,7 +303,7 @@ private static Sequence demarshallSequence(final XQueryContext context, final No throw new XMLStreamException("Sequence element is not in the correct namespace. Expected: " + NAMESPACE); } if (!SEQ_ELEMENT.equals(node.getLocalName())) { - throw new XMLStreamException("Element should be a " + SEQ_ELEMENT_QNAME); + throw new XMLStreamException("Element should be a " + SEQ_ELEMENT_PREFIXED_NAME); } return demarshallValues(context, node); @@ -308,7 +312,8 @@ private static Sequence demarshallSequence(final XQueryContext context, final No private static Sequence demarshallValues(final XQueryContext context, final NodeImpl node) throws XMLStreamException, XPathException { final ValueSequence result = new ValueSequence(); final InMemoryNodeSet sxValues = new InMemoryNodeSet(); - node.selectChildren(new NameTest(Type.ELEMENT, VALUE_QNAME), sxValues); + node.selectChildren(new NameTest(Type.ELEMENT, VALUE_ELEMENT_QNAME), sxValues); + for (final SequenceIterator itSxValue = sxValues.iterate(); itSxValue.hasNext();) { final ElementImpl sxValue = (ElementImpl) itSxValue.nextItem(); final Item item = demarshallValue(context, sxValue); diff --git a/exist-core/src/main/java/org/exist/xquery/XQueryContext.java b/exist-core/src/main/java/org/exist/xquery/XQueryContext.java index d8eddd3266..5502cbd1f6 100644 --- a/exist-core/src/main/java/org/exist/xquery/XQueryContext.java +++ b/exist-core/src/main/java/org/exist/xquery/XQueryContext.java @@ -1234,6 +1234,7 @@ public DocumentSet getStaticallyKnownDocuments() throws XPathException { staticDocuments = protectedDocuments.toDocumentSet(); return staticDocuments; } + final MutableDocumentSet ndocs = new DefaultDocumentSet(40); if (staticDocumentPaths == null) { @@ -1272,7 +1273,8 @@ public DocumentSet getStaticallyKnownDocuments() throws XPathException { } } } - staticDocuments = ndocs; + + this.staticDocuments = ndocs; return staticDocuments; } diff --git a/exist-core/src/test/java/org/exist/xquery/EmbeddedBinariesTest.java b/exist-core/src/test/java/org/exist/xquery/EmbeddedBinariesTest.java index 72805ae64e..6d7eb28784 100644 --- a/exist-core/src/test/java/org/exist/xquery/EmbeddedBinariesTest.java +++ b/exist-core/src/test/java/org/exist/xquery/EmbeddedBinariesTest.java @@ -143,7 +143,7 @@ protected QueryResultAccessor executeXQuery(final String consumer2E.accept(results); } finally { - //TODO(AR) performing #runCleanupTasks causes the stream to be closed, so if we do so before we are finished with the results, serialization fails. + // NOTE(AR) performing #runCleanupTasks causes the stream to be closed, so if we do so before we are finished with the results, serialization fails. if (fContext != null) { fContext.runCleanupTasks(); } diff --git a/extensions/modules/file/src/test/java/org/exist/xquery/modules/file/EmbeddedBinariesTest.java b/extensions/modules/file/src/test/java/org/exist/xquery/modules/file/EmbeddedBinariesTest.java index e51cdd042c..cfc3b21fba 100644 --- a/extensions/modules/file/src/test/java/org/exist/xquery/modules/file/EmbeddedBinariesTest.java +++ b/extensions/modules/file/src/test/java/org/exist/xquery/modules/file/EmbeddedBinariesTest.java @@ -147,7 +147,7 @@ protected QueryResultAccessor executeXQuery(final String consumer2E.accept(results); } finally { - //TODO(AR) performing #runCleanupTasks causes the stream to be closed, so if we do so before we are finished with the results, serialization fails. + // NOTE(AR) performing #runCleanupTasks causes the stream to be closed, so if we do so before we are finished with the results, serialization fails. if (fContext != null) { fContext.runCleanupTasks(); } From 7115082df897efb2e64404ca5f76ed855067cce2 Mon Sep 17 00:00:00 2001 From: Adam Retter Date: Tue, 28 Apr 2026 15:41:43 +0200 Subject: [PATCH 02/18] [bugfix] Correct the wrapping in the Serializer so that it outputs the correct execution time of the query --- .../src/main/java/org/exist/storage/serializers/Serializer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exist-core/src/main/java/org/exist/storage/serializers/Serializer.java b/exist-core/src/main/java/org/exist/storage/serializers/Serializer.java index 6852600c70..5f47ca3bef 100644 --- a/exist-core/src/main/java/org/exist/storage/serializers/Serializer.java +++ b/exist-core/src/main/java/org/exist/storage/serializers/Serializer.java @@ -1085,7 +1085,7 @@ public void toSAX(final Sequence seq, int start, final int count, final boolean attrs.addAttribute(ATTR_SESSION_ID, outputProperties.getProperty(PROPERTY_SESSION_ID)); } attrs.addAttribute(ATTR_COMPILATION_TIME_QNAME, Long.toString(compilationTime)); - attrs.addAttribute(ATTR_EXECUTION_TIME_QNAME, Long.toString(compilationTime)); + attrs.addAttribute(ATTR_EXECUTION_TIME_QNAME, Long.toString(executionTime)); if (!documentStarted) { receiver.startDocument(); From bf5a8e3588b75117158d3203fffe781f07fb6a9f Mon Sep 17 00:00:00 2001 From: Adam Retter Date: Tue, 28 Apr 2026 15:51:58 +0200 Subject: [PATCH 03/18] [bugfix] Do not modify namespaces when processing an HTTP POST of an XML exist:query document --- .../main/java/org/exist/http/RESTServer.java | 72 +-- .../exist/http/RESTExternalVariableTest.java | 457 +++++++++++++++--- 2 files changed, 409 insertions(+), 120 deletions(-) diff --git a/exist-core/src/main/java/org/exist/http/RESTServer.java b/exist-core/src/main/java/org/exist/http/RESTServer.java index 1412d23e66..438dc0f209 100644 --- a/exist-core/src/main/java/org/exist/http/RESTServer.java +++ b/exist-core/src/main/java/org/exist/http/RESTServer.java @@ -107,7 +107,6 @@ import org.xml.sax.SAXParseException; import org.xml.sax.XMLReader; import org.xml.sax.helpers.AttributesImpl; -import org.xml.sax.helpers.XMLFilterImpl; import xyz.elemental.mediatype.MediaType; import xyz.elemental.mediatype.MediaTypeResolver; @@ -339,13 +338,10 @@ public void doGet(final DBBroker broker, final Txn transaction, final HttpServle } } final String _var = getParameter(request, Variables); - List /**/ namespaces = null; ElementImpl variables = null; try { if (_var != null) { - final NamespaceExtractor nsExtractor = new NamespaceExtractor(); - variables = parseXML(broker.getBrokerPool(), _var, nsExtractor); - namespaces = nsExtractor.getNamespaces(); + variables = parseXML(broker.getBrokerPool(), _var); } } catch (final SAXException e) { final XPathException x = new XPathException(variables != null ? variables.getExpression() : null, e.toString()); @@ -439,7 +435,7 @@ public void doGet(final DBBroker broker, final Txn transaction, final HttpServle if (query != null) { // query parameter specified, search method does all the rest of the work try { - search(broker, transaction, query, path, namespaces, variables, howmany, start, typed, outputProperties, + search(broker, transaction, query, path, null, variables, howmany, start, typed, outputProperties, wrap, cache, request, response); } catch (final XPathException e) { @@ -796,8 +792,7 @@ public void doPost(final DBBroker broker, final Txn transaction, final HttpServl try { final String content = getRequestContent(request); - final NamespaceExtractor nsExtractor = new NamespaceExtractor(); - final ElementImpl root = parseXML(broker.getBrokerPool(), content, nsExtractor); + final ElementImpl root = parseXML(broker.getBrokerPool(), content); final String rootNS = root.getNamespaceURI(); if (rootNS != null && rootNS.equals(Namespaces.EXIST_NS)) { @@ -905,7 +900,7 @@ public void doPost(final DBBroker broker, final Txn transaction, final HttpServl if (query != null) { try { - search(broker, transaction, query, path, nsExtractor.getNamespaces(), variables, + search(broker, transaction, query, path, null, variables, howmany, start, typed, outputProperties, enclose, cache, request, response); } catch (final XPathException e) { @@ -1002,22 +997,19 @@ public void doPost(final DBBroker broker, final Txn transaction, final HttpServl } } - private ElementImpl parseXML(final BrokerPool pool, final String content, - final NamespaceExtractor nsExtractor) - throws SAXException, IOException { + private ElementImpl parseXML(final BrokerPool pool, final String content) throws SAXException, IOException { final InputSource src = new InputSource(new StringReader(content)); final XMLReaderPool parserPool = pool.getParserPool(); XMLReader reader = null; try { reader = parserPool.borrowXMLReader(); final SAXAdapter adapter = new SAXAdapter((Expression) null); - nsExtractor.setContentHandler(adapter); + + reader.setContentHandler(adapter); reader.setProperty(Namespaces.SAX_LEXICAL_HANDLER, adapter); - nsExtractor.setParent(reader); - nsExtractor.parse(src); + reader.parse(src); final Document doc = adapter.getDocument(); - return (ElementImpl) doc.getDocumentElement(); } finally { if (reader != null) { @@ -1026,42 +1018,14 @@ private ElementImpl parseXML(final BrokerPool pool, final String content, } } - private class NamespaceExtractor extends XMLFilterImpl { - - final List namespaces = new ArrayList<>(); - - @Override - public void startPrefixMapping(final String prefix, final String uri) - throws SAXException { - if (!Namespaces.EXIST_NS.equals(uri)) { - final Namespace ns = new Namespace(prefix, uri); - namespaces.add(ns); - } - super.startPrefixMapping(prefix, uri); - } - - public List getNamespaces() { - return namespaces; - } - } - public static class Namespace { - - private final String prefix; - private final String uri; + final String prefix; + final String uri; public Namespace(final String prefix, final String uri) { this.prefix = prefix; this.uri = uri; } - - public String getPrefix() { - return prefix; - } - - public String getUri() { - return uri; - } } /** @@ -1347,11 +1311,11 @@ private String getRequestContent(final HttpServletRequest request) throws IOExce * @throws XPathException if the XQuery raises an error */ protected void search(final DBBroker broker, final Txn transaction, final String query, - final String path, final List namespaces, - final ElementImpl variables, final int howmany, final int start, - final boolean typed, final Properties outputProperties, - final boolean wrap, final boolean cache, - final HttpServletRequest request, + final String path, @Nullable final List namespaces, + @Nullable final ElementImpl variables, final int howmany, + final int start, final boolean typed, + final Properties outputProperties, final boolean wrap, + final boolean cache, final HttpServletRequest request, final HttpServletResponse response) throws BadRequestException, PermissionDeniedException, XPathException { @@ -1453,15 +1417,13 @@ protected void search(final DBBroker broker, final Txn transaction, final String } } - private void declareNamespaces(final XQueryContext context, - final List namespaces) throws XPathException { - + private void declareNamespaces(final XQueryContext context, @Nullable final List namespaces) throws XPathException { if (namespaces == null) { return; } for (final Namespace ns : namespaces) { - context.declareNamespace(ns.getPrefix(), ns.getUri()); + context.declareNamespace(ns.prefix, ns.uri); } } diff --git a/exist-core/src/test/java/org/exist/http/RESTExternalVariableTest.java b/exist-core/src/test/java/org/exist/http/RESTExternalVariableTest.java index 22de9afe1c..ab3f7bf1b7 100644 --- a/exist-core/src/test/java/org/exist/http/RESTExternalVariableTest.java +++ b/exist-core/src/test/java/org/exist/http/RESTExternalVariableTest.java @@ -21,7 +21,7 @@ package org.exist.http; import com.evolvedbinary.j8fu.tuple.Tuple2; -import com.googlecode.junittoolbox.ParallelRunner; +import com.googlecode.junittoolbox.ParallelParameterized; import org.apache.commons.codec.binary.Base64; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; @@ -39,7 +39,9 @@ import org.junit.ClassRule; import org.junit.Test; import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; import org.w3c.dom.Attr; +import org.w3c.dom.Node; import org.xmlunit.diff.DefaultNodeMatcher; import org.xmlunit.diff.ElementSelectors; import org.xmlunit.matchers.CompareMatcher; @@ -50,6 +52,7 @@ import javax.xml.namespace.QName; import java.io.IOException; import java.io.InputStream; +import java.util.Arrays; import java.util.Map; import static com.evolvedbinary.j8fu.tuple.Tuple.Tuple; @@ -66,15 +69,30 @@ import static org.exist.http.RESTExternalVariableTest.UntypedNamedValueRep.value; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; +import static org.xmlunit.matchers.HasXPathMatcher.hasXPath; /** * See: [BUG] The JavaDoc comments for variable in the REST API are inconsistent with the implementation. * * @author data() { + return Arrays.asList(new Object[][] { + { "xmlns-prefixed-ns", true }, + { "xmlns-default-ns", false } + }); + } + + @Parameterized.Parameter + public String testTypeName; + + @Parameterized.Parameter(value = 1) + public boolean useXmlnsPrefixes; + @ClassRule public static final ExistWebServer existWebServer = new ExistWebServer(true, false, true, true); @@ -316,13 +334,23 @@ public void queryPostWithExternalVariableStringzSuppliedUntypeds() throws IOExce @Test public void queryPostWithExternalVariableUntypedSuppliedUntypedElementValue() throws IOException { - final ExternalVariableValueRep externalVariable = value("world"); + final ExternalVariableValueRep externalVariable; + if (useXmlnsPrefixes) { + externalVariable = value("world"); + } else { + externalVariable = value("world"); + } queryPostWithExternalVariable(HttpStatus.OK_200, null, externalVariable); } @Test public void queryPostWithExternalVariableUntypedSuppliedElement() throws IOException { - final ExternalVariableValueRep externalVariable = value(Type.ELEMENT, "world"); + final ExternalVariableValueRep externalVariable; + if (useXmlnsPrefixes) { + externalVariable = value(Type.ELEMENT, "world"); + } else { + externalVariable = value(Type.ELEMENT, "world"); + } queryPostWithExternalVariable(HttpStatus.OK_200, null, externalVariable); } @@ -339,31 +367,56 @@ public void queryPostWithExternalVariableElementSuppliedEmpty() throws IOExcepti @Test public void queryPostWithExternalVariableElementSuppliedElement() throws IOException { - final ExternalVariableValueRep externalVariable = value(Type.ELEMENT, "world"); + final ExternalVariableValueRep externalVariable; + if (useXmlnsPrefixes) { + externalVariable = value(Type.ELEMENT, "world"); + } else { + externalVariable = value(Type.ELEMENT, "world"); + } queryPostWithExternalVariable(HttpStatus.OK_200, "element()", externalVariable); } @Test public void queryPostWithExternalVariableElementSuppliedElements() throws IOException { - final ExternalVariableValueRep[] externalVariable = new ExternalVariableValueRep[] { value("world"), value("see you soon") }; + final ExternalVariableValueRep[] externalVariable; + if (useXmlnsPrefixes) { + externalVariable = new ExternalVariableValueRep[] { value("world"), value("see you soon") }; + } else { + externalVariable = new ExternalVariableValueRep[] { value("world"), value("see you soon") }; + } queryPostWithExternalVariable(HttpStatus.BAD_REQUEST_400, "element()", externalVariable); } @Test public void queryPostWithExternalVariableElementSuppliedUntyped() throws IOException { - final ExternalVariableValueRep externalVariable = value("world"); + final ExternalVariableValueRep externalVariable; + if (useXmlnsPrefixes) { + externalVariable = value("world"); + } else { + externalVariable = value("world"); + } queryPostWithExternalVariable(HttpStatus.OK_200, "element()", externalVariable); } @Test public void queryPostWithExternalVariableUntypedSuppliedUntypedElements() throws IOException { - final ExternalVariableValueRep[] externalVariable = new ExternalVariableValueRep[] { value("world"), value("see you soon") }; + final ExternalVariableValueRep[] externalVariable; + if (useXmlnsPrefixes) { + externalVariable = new ExternalVariableValueRep[] { value("world"), value("see you soon") }; + } else { + externalVariable = new ExternalVariableValueRep[] { value("world"), value("see you soon") }; + } queryPostWithExternalVariable(HttpStatus.OK_200, null, externalVariable); } @Test public void queryPostWithExternalVariableUntypedSuppliedElements() throws IOException { - final ExternalVariableValueRep[] externalVariable = new ExternalVariableValueRep[] { value(Type.ELEMENT, "world"), value(Type.ELEMENT, "see you soon") }; + final ExternalVariableValueRep[] externalVariable; + if (useXmlnsPrefixes) { + externalVariable = new ExternalVariableValueRep[] { value(Type.ELEMENT, "world"), value(Type.ELEMENT, "see you soon") }; + } else { + externalVariable = new ExternalVariableValueRep[] { value(Type.ELEMENT, "world"), value(Type.ELEMENT, "see you soon") }; + } queryPostWithExternalVariable(HttpStatus.OK_200, null, externalVariable); } @@ -380,19 +433,34 @@ public void queryPostWithExternalVariableOptElementSuppliedEmpty() throws IOExce @Test public void queryPostWithExternalVariableOptElementSuppliedElement() throws IOException { - final ExternalVariableValueRep externalVariable = value(Type.ELEMENT, "world"); + final ExternalVariableValueRep externalVariable; + if (useXmlnsPrefixes) { + externalVariable = value(Type.ELEMENT, "world"); + } else { + externalVariable = value(Type.ELEMENT, "world"); + } queryPostWithExternalVariable(HttpStatus.OK_200, "element()?", externalVariable); } @Test public void queryPostWithExternalVariableOptElementSuppliedElements() throws IOException { - final ExternalVariableValueRep[] externalVariable = new ExternalVariableValueRep[] { value(Type.ELEMENT, "world"), value(Type.ELEMENT, "see you soon") }; + final ExternalVariableValueRep[] externalVariable; + if (useXmlnsPrefixes) { + externalVariable = new ExternalVariableValueRep[] { value(Type.ELEMENT, "world"), value(Type.ELEMENT, "see you soon") }; + } else { + externalVariable = new ExternalVariableValueRep[] { value(Type.ELEMENT, "world"), value(Type.ELEMENT, "see you soon") }; + } queryPostWithExternalVariable(HttpStatus.BAD_REQUEST_400, "element()?", externalVariable); } @Test public void queryPostWithExternalVariableOptElementSuppliedUntyped() throws IOException { - final ExternalVariableValueRep externalVariable = value("world"); + final ExternalVariableValueRep externalVariable; + if (useXmlnsPrefixes) { + externalVariable = value("world"); + } else { + externalVariable = value("world"); + } queryPostWithExternalVariable(HttpStatus.OK_200, "element()?", externalVariable); } @@ -409,25 +477,45 @@ public void queryPostWithExternalVariableElementsSuppliedEmpty() throws IOExcept @Test public void queryPostWithExternalVariableElementsSuppliedElement() throws IOException { - final ExternalVariableValueRep externalVariable = value(Type.ELEMENT, "world"); + final ExternalVariableValueRep externalVariable; + if (useXmlnsPrefixes) { + externalVariable = value(Type.ELEMENT, "world"); + } else { + externalVariable = value(Type.ELEMENT, "world"); + } queryPostWithExternalVariable(HttpStatus.OK_200, "element()+", externalVariable); } @Test public void queryPostWithExternalVariableElementsSuppliedElements() throws IOException { - final ExternalVariableValueRep[] externalVariable = new ExternalVariableValueRep[] { value(Type.ELEMENT, "world"), value(Type.ELEMENT, "see you soon") }; + final ExternalVariableValueRep[] externalVariable; + if (useXmlnsPrefixes) { + externalVariable = new ExternalVariableValueRep[] { value(Type.ELEMENT, "world"), value(Type.ELEMENT, "see you soon") }; + } else { + externalVariable = new ExternalVariableValueRep[] { value(Type.ELEMENT, "world"), value(Type.ELEMENT, "see you soon") }; + } queryPostWithExternalVariable(HttpStatus.OK_200, "element()+", externalVariable); } @Test public void queryPostWithExternalVariableElementsSuppliedUntyped() throws IOException { - final ExternalVariableValueRep[] externalVariable = new ExternalVariableValueRep[] { value("world") }; + final ExternalVariableValueRep[] externalVariable; + if (useXmlnsPrefixes) { + externalVariable = new ExternalVariableValueRep[] { value("world") }; + } else { + externalVariable = new ExternalVariableValueRep[] { value("world") }; + } queryPostWithExternalVariable(HttpStatus.OK_200, "element()+", externalVariable); } @Test public void queryPostWithExternalVariableElementsSuppliedUntypeds() throws IOException { - final ExternalVariableValueRep[] externalVariable = new ExternalVariableValueRep[] { value("world"), value("see you soon") }; + final ExternalVariableValueRep[] externalVariable; + if (useXmlnsPrefixes) { + externalVariable = new ExternalVariableValueRep[] { value("world"), value("see you soon") }; + } else { + externalVariable = new ExternalVariableValueRep[] { value("world"), value("see you soon") }; + } queryPostWithExternalVariable(HttpStatus.OK_200, "element()+", externalVariable); } @@ -444,37 +532,67 @@ public void queryPostWithExternalVariableElementzSuppliedEmpty() throws IOExcept @Test public void queryPostWithExternalVariableElementzSuppliedElement() throws IOException { - final ExternalVariableValueRep externalVariable = value(Type.ELEMENT, "world"); + final ExternalVariableValueRep externalVariable; + if (useXmlnsPrefixes) { + externalVariable = value(Type.ELEMENT, "world"); + } else { + externalVariable = value(Type.ELEMENT, "world"); + } queryPostWithExternalVariable(HttpStatus.OK_200, "element()*", externalVariable); } @Test public void queryPostWithExternalVariableElementzSuppliedElements() throws IOException { - final ExternalVariableValueRep[] externalVariable = new ExternalVariableValueRep[] { value(Type.ELEMENT, "world"), value(Type.ELEMENT, "see you soon") }; + final ExternalVariableValueRep[] externalVariable; + if (useXmlnsPrefixes) { + externalVariable = new ExternalVariableValueRep[] { value(Type.ELEMENT, "world"), value(Type.ELEMENT, "see you soon") }; + } else { + externalVariable = new ExternalVariableValueRep[] { value(Type.ELEMENT, "world"), value(Type.ELEMENT, "see you soon") }; + } queryPostWithExternalVariable(HttpStatus.OK_200, "element()*", externalVariable); } @Test public void queryPostWithExternalVariableElementszSuppliedUntyped() throws IOException { - final ExternalVariableValueRep[] externalVariable = new ExternalVariableValueRep[] { value("world") }; + final ExternalVariableValueRep[] externalVariable; + if (useXmlnsPrefixes) { + externalVariable = new ExternalVariableValueRep[] { value("world") }; + } else { + externalVariable = new ExternalVariableValueRep[] { value("world") }; + } queryPostWithExternalVariable(HttpStatus.OK_200, "element()*", externalVariable); } @Test public void queryPostWithExternalVariableElementzSuppliedUntypeds() throws IOException { - final ExternalVariableValueRep[] externalVariable = new ExternalVariableValueRep[] { value("world"), value("see you soon") }; + final ExternalVariableValueRep[] externalVariable; + if (useXmlnsPrefixes) { + externalVariable = new ExternalVariableValueRep[] { value("world"), value("see you soon") }; + } else { + externalVariable = new ExternalVariableValueRep[] { value("world"), value("see you soon") }; + } queryPostWithExternalVariable(HttpStatus.OK_200, "element()*", externalVariable); } @Test public void queryPostWithExternalVariableUntypedSuppliedUntypedDocument() throws IOException { - final ExternalVariableValueRep externalVariable = value("world"); + final ExternalVariableValueRep externalVariable; + if (useXmlnsPrefixes) { + externalVariable = value("world"); + } else { + externalVariable = value("world"); + } queryPostWithExternalVariable(HttpStatus.OK_200, null, externalVariable); } @Test public void queryPostWithExternalVariableUntypedSuppliedDocument() throws IOException { - final ExternalVariableValueRep externalVariable = value(Type.DOCUMENT, "world"); + final ExternalVariableValueRep externalVariable; + if (useXmlnsPrefixes) { + externalVariable = value(Type.DOCUMENT, "world"); + } else { + externalVariable = value(Type.DOCUMENT, "world"); + } queryPostWithExternalVariable(HttpStatus.OK_200, null, externalVariable); } @@ -491,32 +609,57 @@ public void queryPostWithExternalVariableDocumentSuppliedEmpty() throws IOExcept @Test public void queryPostWithExternalVariableDocumentSuppliedDocument() throws IOException { - final ExternalVariableValueRep externalVariable = value(Type.DOCUMENT, "world"); + final ExternalVariableValueRep externalVariable; + if (useXmlnsPrefixes) { + externalVariable = value(Type.DOCUMENT, "world"); + } else { + externalVariable = value(Type.DOCUMENT, "world"); + } queryPostWithExternalVariable(HttpStatus.OK_200, "document-node()", externalVariable); } @Test public void queryPostWithExternalVariableDocumentSuppliedDocuments() throws IOException { - final ExternalVariableValueRep[] externalVariable = new ExternalVariableValueRep[] { value("world"), value("see you soon") }; + final ExternalVariableValueRep[] externalVariable; + if (useXmlnsPrefixes) { + externalVariable = new ExternalVariableValueRep[] { value("world"), value("see you soon") }; + } else { + externalVariable = new ExternalVariableValueRep[] { value("world"), value("see you soon") }; + } queryPostWithExternalVariable(HttpStatus.BAD_REQUEST_400, "document-node()", externalVariable); } @Test public void queryPostWithExternalVariableDocumentSuppliedUntyped() throws IOException { - final ExternalVariableValueRep externalVariable = value("world"); + final ExternalVariableValueRep externalVariable; + if (useXmlnsPrefixes) { + externalVariable = value("world"); + } else { + externalVariable = value("world"); + } final String expectedResponseError = "/db/test/test.xmlerr:XPTY0004 Invalid type for variable $local:my-variable. Expected document-node(), got element()"; queryPostWithExternalVariable(Tuple(HttpStatus.BAD_REQUEST_400, expectedResponseError), "document-node()", externalVariable); } @Test public void queryPostWithExternalVariableUntypedSuppliedUntypedDocuments() throws IOException { - final ExternalVariableValueRep[] externalVariable = new ExternalVariableValueRep[] { value("world"), value("see you soon") }; + final ExternalVariableValueRep[] externalVariable; + if (useXmlnsPrefixes) { + externalVariable = new ExternalVariableValueRep[] { value("world"), value("see you soon") }; + } else { + externalVariable = new ExternalVariableValueRep[] { value("world"), value("see you soon") }; + } queryPostWithExternalVariable(HttpStatus.OK_200, null, externalVariable); } @Test public void queryPostWithExternalVariableUntypedSuppliedDocuments() throws IOException { - final ExternalVariableValueRep[] externalVariable = new ExternalVariableValueRep[] { value(Type.DOCUMENT, "world"), value(Type.DOCUMENT, "see you soon") }; + final ExternalVariableValueRep[] externalVariable; + if (useXmlnsPrefixes) { + externalVariable = new ExternalVariableValueRep[] { value(Type.DOCUMENT, "world"), value(Type.DOCUMENT, "see you soon") }; + } else { + externalVariable = new ExternalVariableValueRep[] { value(Type.DOCUMENT, "world"), value(Type.DOCUMENT, "see you soon") }; + } queryPostWithExternalVariable(HttpStatus.OK_200, null, externalVariable); } @@ -533,19 +676,34 @@ public void queryPostWithExternalVariableOptDocumentSuppliedEmpty() throws IOExc @Test public void queryPostWithExternalVariableOptDocumentSuppliedDocument() throws IOException { - final ExternalVariableValueRep externalVariable = value(Type.DOCUMENT, "world"); + final ExternalVariableValueRep externalVariable; + if (useXmlnsPrefixes) { + externalVariable = value(Type.DOCUMENT, "world"); + } else { + externalVariable = value(Type.DOCUMENT, "world"); + } queryPostWithExternalVariable(HttpStatus.OK_200, "document-node()?", externalVariable); } @Test public void queryPostWithExternalVariableOptDocumentSuppliedDocuments() throws IOException { - final ExternalVariableValueRep[] externalVariable = new ExternalVariableValueRep[] { value(Type.DOCUMENT, "world"), value(Type.DOCUMENT, "see you soon") }; + final ExternalVariableValueRep[] externalVariable; + if (useXmlnsPrefixes) { + externalVariable = new ExternalVariableValueRep[] { value(Type.DOCUMENT, "world"), value(Type.DOCUMENT, "see you soon") }; + } else { + externalVariable = new ExternalVariableValueRep[] { value(Type.DOCUMENT, "world"), value(Type.DOCUMENT, "see you soon") }; + } queryPostWithExternalVariable(HttpStatus.BAD_REQUEST_400, "document-node()?", externalVariable); } @Test public void queryPostWithExternalVariableOptDocumentSuppliedUntyped() throws IOException { - final ExternalVariableValueRep externalVariable = value("world"); + final ExternalVariableValueRep externalVariable; + if (useXmlnsPrefixes) { + externalVariable = value("world"); + } else { + externalVariable = value("world"); + } final String expectedResponseError = "/db/test/test.xmlerr:XPTY0004 Invalid type for variable $local:my-variable. Expected document-node(), got element()"; queryPostWithExternalVariable(Tuple(HttpStatus.BAD_REQUEST_400, expectedResponseError), "document-node()?", externalVariable); } @@ -563,26 +721,46 @@ public void queryPostWithExternalVariableDocumentsSuppliedEmpty() throws IOExcep @Test public void queryPostWithExternalVariableDocumentsSuppliedDocument() throws IOException { - final ExternalVariableValueRep externalVariable = value(Type.DOCUMENT, "world"); + final ExternalVariableValueRep externalVariable; + if (useXmlnsPrefixes) { + externalVariable = value(Type.DOCUMENT, "world"); + } else { + externalVariable = value(Type.DOCUMENT, "world"); + } queryPostWithExternalVariable(HttpStatus.OK_200, "document-node()+", externalVariable); } @Test public void queryPostWithExternalVariableDocumentsSuppliedDocuments() throws IOException { - final ExternalVariableValueRep[] externalVariable = new ExternalVariableValueRep[] { value(Type.DOCUMENT, "world"), value(Type.DOCUMENT, "see you soon") }; + final ExternalVariableValueRep[] externalVariable; + if (useXmlnsPrefixes) { + externalVariable = new ExternalVariableValueRep[] { value(Type.DOCUMENT, "world"), value(Type.DOCUMENT, "see you soon") }; + } else { + externalVariable = new ExternalVariableValueRep[] { value(Type.DOCUMENT, "world"), value(Type.DOCUMENT, "see you soon") }; + } queryPostWithExternalVariable(HttpStatus.OK_200, "document-node()+", externalVariable); } @Test public void queryPostWithExternalVariableDocumentsSuppliedUntyped() throws IOException { - final ExternalVariableValueRep[] externalVariable = new ExternalVariableValueRep[] { value("world") }; + final ExternalVariableValueRep[] externalVariable; + if (useXmlnsPrefixes) { + externalVariable = new ExternalVariableValueRep[] { value("world") }; + } else { + externalVariable = new ExternalVariableValueRep[] { value("world") }; + } final String expectedResponseError = "/db/test/test.xmlerr:XPTY0004 Invalid type for variable $local:my-variable. Expected document-node(), got element()"; queryPostWithExternalVariable(Tuple(HttpStatus.BAD_REQUEST_400, expectedResponseError), "document-node()+", externalVariable); } @Test public void queryPostWithExternalVariableDocumentsSuppliedUntypeds() throws IOException { - final ExternalVariableValueRep[] externalVariable = new ExternalVariableValueRep[] { value("world"), value("see you soon") }; + final ExternalVariableValueRep[] externalVariable; + if (useXmlnsPrefixes) { + externalVariable = new ExternalVariableValueRep[] { value("world"), value("see you soon") }; + } else { + externalVariable = new ExternalVariableValueRep[] { value("world"), value("see you soon") }; + } final String expectedResponseError = "/db/test/test.xmlerr:XPTY0004 Invalid type for variable $local:my-variable. Expected document-node(), got element()"; queryPostWithExternalVariable(Tuple(HttpStatus.BAD_REQUEST_400, expectedResponseError), "document-node()+", externalVariable); } @@ -600,26 +778,46 @@ public void queryPostWithExternalVariableDocumentzSuppliedEmpty() throws IOExcep @Test public void queryPostWithExternalVariableDocumentzSuppliedDocument() throws IOException { - final ExternalVariableValueRep externalVariable = value(Type.DOCUMENT, "world"); + final ExternalVariableValueRep externalVariable; + if (useXmlnsPrefixes) { + externalVariable = value(Type.DOCUMENT, "world"); + } else { + externalVariable = value(Type.DOCUMENT, "world"); + } queryPostWithExternalVariable(HttpStatus.OK_200, "document-node()*", externalVariable); } @Test public void queryPostWithExternalVariableDocumentzSuppliedDocuments() throws IOException { - final ExternalVariableValueRep[] externalVariable = new ExternalVariableValueRep[] { value(Type.DOCUMENT, "world"), value(Type.DOCUMENT, "see you soon") }; + final ExternalVariableValueRep[] externalVariable; + if (useXmlnsPrefixes) { + externalVariable = new ExternalVariableValueRep[] { value(Type.DOCUMENT, "world"), value(Type.DOCUMENT, "see you soon") }; + } else { + externalVariable = new ExternalVariableValueRep[] { value(Type.DOCUMENT, "world"), value(Type.DOCUMENT, "see you soon") }; + } queryPostWithExternalVariable(HttpStatus.OK_200, "document-node()*", externalVariable); } @Test public void queryPostWithExternalVariableDocumentszSuppliedUntyped() throws IOException { - final ExternalVariableValueRep[] externalVariable = new ExternalVariableValueRep[] { value("world") }; + final ExternalVariableValueRep[] externalVariable; + if (useXmlnsPrefixes) { + externalVariable = new ExternalVariableValueRep[] { value("world") }; + } else { + externalVariable = new ExternalVariableValueRep[] { value("world") }; + } final String expectedResponseError = "/db/test/test.xmlerr:XPTY0004 Invalid type for variable $local:my-variable. Expected document-node(), got element()"; queryPostWithExternalVariable(Tuple(HttpStatus.BAD_REQUEST_400, expectedResponseError), "document-node()*", externalVariable); } @Test public void queryPostWithExternalVariableDocumentzSuppliedUntypeds() throws IOException { - final ExternalVariableValueRep[] externalVariable = new ExternalVariableValueRep[] { value("world"), value("see you soon") }; + final ExternalVariableValueRep[] externalVariable; + if (useXmlnsPrefixes) { + externalVariable = new ExternalVariableValueRep[] { value("world"), value("see you soon") }; + } else { + externalVariable = new ExternalVariableValueRep[] { value("world"), value("see you soon") }; + } final String expectedResponseError = "/db/test/test.xmlerr:XPTY0004 Invalid type for variable $local:my-variable. Expected document-node(), got element()"; queryPostWithExternalVariable(Tuple(HttpStatus.BAD_REQUEST_400, expectedResponseError), "document-node()*", externalVariable); } @@ -1622,6 +1820,75 @@ public void queryPostWithExternalVariableMapzSuppliedUntypeds() throws IOExcepti queryPostWithExternalVariable(HttpStatus.OK_200, "map(*)*", externalVariable); } + @Test + public void queryPostWrappedTypedWithExternalVariableStringConstructElement() throws IOException { + queryPostWithExternalVariableStringConstructElement(true, true); + } + + @Test + public void queryPostWrappedNotTypedWithExternalVariableStringConstructElement() throws IOException { + queryPostWithExternalVariableStringConstructElement(true, false); + } + + @Test + public void queryPostNotWrappedTypedWithExternalVariableStringConstructElement() throws IOException { + queryPostWithExternalVariableStringConstructElement(false, true); + } + + @Test + public void queryPostNotWrappedNotTypedWithExternalVariableStringConstructElement() throws IOException { + queryPostWithExternalVariableStringConstructElement(false, false); + } + + private void queryPostWithExternalVariableStringConstructElement(final boolean wrap, final boolean typed) throws IOException { + final String query; + if (useXmlnsPrefixes) { + query = "\n" + + "\t\n" + + "\t\t\n" + + "\t\t\t\n" + + "\t\t\t\tmy-variable\n" + + "\t\t\t\n" + + "\t\t\t\n" + + "\t\t\t\tgreeting" + + "\t\t\t\n" + + "\t\t\n" + + "\t\n" + + "\t\n" + + "\n"; + } else { + query = "\n" + + "\t\n" + + "\t\t\n" + + "\t\t\t\n" + + "\t\t\t\tmy-variable\n" + + "\t\t\t\n" + + "\t\t\t\n" + + "\t\t\t\tgreeting" + + "\t\t\t\n" + + "\t\t\n" + + "\t\n" + + "\t\n" + + "\n"; + } + + final HttpResponse response = doPostWithAuth(getResourceUri(), query); + final int resultStatusCode = response.getStatusLine() + .getStatusCode(); + + assertEquals("Server returned response code: " + resultStatusCode, HttpStatus.OK_200, resultStatusCode); + + //final String actual = "Hello, world."; + final String actual = readResponse(response.getEntity()); + assertThat(actual, hasXPath("//greeting[namespace-uri() = ''][text() = 'Hello, world.']").withNamespaceContext(NS_CONTEXT)); + } + private void queryPostWithExternalVariable(final int expectedResponseCode, @Nullable final String xqExternalVariableType, final ExternalVariableValueRep... externalVariableSequence) throws IOException { queryPostWithExternalVariable(Tuple(expectedResponseCode, null), externalVariableSequence, xqExternalVariableType, externalVariableSequence); } @@ -1791,38 +2058,73 @@ private static boolean isMap(final int xdmType, final ExternalVariableValueRep e return xdmType == Type.MAP_ITEM || (xdmType == Type.ITEM && externalVariableValueRep instanceof MapRep); } - private static String buildQueryExternalVariable(@Nullable final String xqExternalVariableType, @Nullable final ExternalVariableValueRep... externalVariableSequence) { + private String buildQueryExternalVariable(@Nullable final String xqExternalVariableType, @Nullable final ExternalVariableValueRep... externalVariableSequence) { final StringBuilder builder = new StringBuilder(); - builder.append("\n"); - if (externalVariableSequence!= null) { - builder.append("\t\n"); - builder.append("\t\t\n"); - builder.append("\t\t\tlocalmy-variable\n"); - buildQueryExternalVariableSequence(builder, 3, externalVariableSequence); - builder.append("\t\t\n"); - builder.append("\t\n"); - } + if (useXmlnsPrefixes) { + builder.append("\n"); - builder.append("\t\n"); + builder.append("\t\t\n"); + builder.append("\t\t\tlocalmy-variable\n"); + buildQueryExternalVariableSequence(builder, 3, externalVariableSequence); + builder.append("\t\t\n"); + builder.append("\t\n"); + } + + builder.append("\t\n"); + builder.append("\n"); + + } else { + builder.append("\n"); + + if (externalVariableSequence != null) { + builder.append("\t\n"); + builder.append("\t\t\n"); + builder.append("\t\t\tlocalmy-variable\n"); + buildQueryExternalVariableSequence(builder, 3, externalVariableSequence); + builder.append("\t\t\n"); + builder.append("\t\n"); + } + + builder.append("\t\n"); + builder.append("\n"); } - builder.append(" external;\n"); - builder.append("$local:my-variable\n"); - builder.append("\t]]>\n"); - builder.append("\n"); return builder.toString(); } private static final char[] INDENTS = { '\t', '\t', '\t', '\t', '\t', '\t', '\t', '\t', '\t', '\t', '\t', '\t', '\t', '\t', '\t', '\t' }; - private static void buildQueryExternalVariableSequence(final StringBuilder builder, final int indentCount, final ExternalVariableValueRep... externalVariableSequence) { - builder.append(INDENTS, 0, indentCount).append("\n"); + private void buildQueryExternalVariableSequence(final StringBuilder builder, final int indentCount, final ExternalVariableValueRep... externalVariableSequence) { + if (useXmlnsPrefixes) { + builder.append(INDENTS, 0, indentCount).append("\n"); + } else { + builder.append(INDENTS, 0, indentCount).append("\n"); + } + for (final ExternalVariableValueRep externalVariableSequenceItem : externalVariableSequence) { - builder.append(INDENTS, 0, indentCount + 1).append("\n"); - builder.append(INDENTS, 0, indentCount + 3).append("\n"); + builder.append(INDENTS, 0, indentCount + 3).append("\n"); + builder.append(INDENTS, 0, indentCount + 3).append("').append(key.getContent()).append("\n"); + if (useXmlnsPrefixes) { + builder.append('>').append(key.getContent()).append("\n"); + } else { + builder.append('>').append(key.getContent()).append("\n"); + } + buildQueryExternalVariableSequence(builder, indentCount + 3, entryRep.value.values); - builder.append(INDENTS, 0, indentCount + 2).append("\n"); + + if (useXmlnsPrefixes) { + builder.append(INDENTS, 0, indentCount + 2).append("\n"); + } else { + builder.append(INDENTS, 0, indentCount + 2).append("\n"); + } } builder.append(INDENTS, 0, indentCount + 1); @@ -1862,9 +2180,18 @@ private static void buildQueryExternalVariableSequence(final StringBuilder build builder.append(((ValueRep) externalVariableSequenceItem).getContent()); } - builder.append("\n"); + if (useXmlnsPrefixes) { + builder.append("\n"); + } else { + builder.append("\n"); + } + } + + if (useXmlnsPrefixes) { + builder.append(INDENTS, 0, indentCount).append("\n"); + } else { + builder.append(INDENTS, 0, indentCount).append("\n"); } - builder.append(INDENTS, 0, indentCount).append("\n"); } private static String getServerUri() { From b9d550fc35b2f85ad458b59b03f6371ac6830b9b Mon Sep 17 00:00:00 2001 From: Adam Retter Date: Tue, 28 Apr 2026 16:03:27 +0200 Subject: [PATCH 04/18] [refactor] fn:collection can only accept a single URI --- exist-core/pom.xml | 2 ++ .../xquery/functions/fn/ExtCollection.java | 25 ++++++++-------- .../functions/xmldb/FunXCollection.java | 29 +++++++++++++++++-- 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/exist-core/pom.xml b/exist-core/pom.xml index 22f801fbbe..e4478cb11a 100644 --- a/exist-core/pom.xml +++ b/exist-core/pom.xml @@ -1415,6 +1415,7 @@ src/test/java/org/exist/xquery/functions/validate/JingXsdTest.java src/main/java/org/exist/xquery/functions/validation/Jaxp.java src/test/java/org/exist/xquery/functions/xmldb/DbStore2Test.java + src/main/java/org/exist/xquery/functions/xmldb/FunXCollection.java src/main/java/org/exist/xquery/functions/xmldb/XMLDBGetMimeType.java src/main/java/org/exist/xquery/functions/xmldb/XMLDBLoadFromPattern.java src/main/java/org/exist/xquery/functions/xmldb/XMLDBModule.java @@ -2249,6 +2250,7 @@ src/main/java/org/exist/xquery/functions/validation/Jaxp.java src/test/java/org/exist/xquery/functions/xmldb/AbstractXMLDBTest.java src/test/java/org/exist/xquery/functions/xmldb/DbStore2Test.java + src/main/java/org/exist/xquery/functions/xmldb/FunXCollection.java src/test/java/org/exist/xquery/functions/xmldb/XMLDBAuthenticateTest.java src/main/java/org/exist/xquery/functions/xmldb/XMLDBGetMimeType.java src/main/java/org/exist/xquery/functions/xmldb/XMLDBLoadFromPattern.java diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/ExtCollection.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/ExtCollection.java index 2922d352fd..5436c9e584 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/ExtCollection.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/ExtCollection.java @@ -62,6 +62,7 @@ import org.exist.xquery.functions.xmldb.XMLDBModule; import org.exist.xquery.value.*; +import javax.annotation.Nullable; import java.net.URI; import java.net.URISyntaxException; import java.util.Iterator; @@ -101,27 +102,23 @@ public ExtCollection(final XQueryContext context, final FunctionSignature signat @Override public Sequence eval(final Sequence[] args, final Sequence contextSequence) throws XPathException { - final URI collectionUri; + @Nullable final URI collectionUri; if (args.length == 0 || args[0].isEmpty()) { collectionUri = null; } else { collectionUri = asUri(args[0].itemAt(0).getStringValue()); } - return getCollectionItems(new URI[] { collectionUri }); + return getCollectionItems(collectionUri); } - protected Sequence getCollectionItems(final URI[] collectionUris) throws XPathException { - if (collectionUris == null) { + protected Sequence getCollectionItems(@Nullable final URI collectionUri) throws XPathException { + if (collectionUri == null) { // no collection-uri(s) return getDefaultCollectionItems(); } - final Sequence result = new ValueSequence(); - for (final URI collectionUri : collectionUris) { - getCollectionItems(collectionUri, result); - } - return result; + return getCollectionUriItems(collectionUri); } private Sequence getDefaultCollectionItems() throws XPathException { @@ -139,15 +136,15 @@ private Sequence getDefaultCollectionItems() throws XPathException { } } - private void getCollectionItems(final URI collectionUri, final Sequence items) throws XPathException { - final Sequence dynamicCollection = context.getDynamicallyAvailableCollection(collectionUri.toString()); + private Sequence getCollectionUriItems(final URI collectionUri) throws XPathException { + @Nullable final Sequence dynamicCollection = context.getDynamicallyAvailableCollection(collectionUri.toString()); if (dynamicCollection != null) { - items.addAll(dynamicCollection); + return dynamicCollection; } else { final MutableDocumentSet ndocs = new DefaultDocumentSet(); final XmldbURI uri = XmldbURI.create(collectionUri); - try (final Collection coll = context.getBroker().openCollection(uri, Lock.LockMode.READ_LOCK)) { + try (@Nullable final Collection coll = context.getBroker().openCollection(uri, Lock.LockMode.READ_LOCK)) { if (coll == null) { if (context.isRaiseErrorOnFailedRetrieval()) { throw new XPathException(this, ErrorCodes.FODC0002, "Can not access collection '" + uri + "'"); @@ -167,7 +164,9 @@ private void getCollectionItems(final URI collectionUri, final Sequence items) t } // add the docs to the items + final Sequence items = new ValueSequence(ndocs.getDocumentCount()); addAll(ndocs, items); + return items; } } diff --git a/exist-core/src/main/java/org/exist/xquery/functions/xmldb/FunXCollection.java b/exist-core/src/main/java/org/exist/xquery/functions/xmldb/FunXCollection.java index 54883d5ac1..4593b91835 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/xmldb/FunXCollection.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/xmldb/FunXCollection.java @@ -1,4 +1,28 @@ /* + * Elemental + * Copyright (C) 2024, Evolved Binary Ltd + * + * admin@evolvedbinary.com + * https://www.evolvedbinary.com | https://www.elemental.xyz + * + * 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; version 2.1. + * + * 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 + * + * NOTE: Parts of this file contain code from 'The eXist-db Authors'. + * The original license header is included below. + * + * ===================================================================== + * * eXist-db Open Source Native XML Database * Copyright (C) 2001 The eXist-db Authors * @@ -28,6 +52,7 @@ import org.exist.xquery.value.Sequence; import org.exist.xquery.value.Type; +import javax.annotation.Nullable; import java.net.URI; import static org.exist.xquery.FunctionDSL.*; @@ -60,13 +85,13 @@ public FunXCollection(final XQueryContext context, final FunctionSignature signa @Override public Sequence eval(final Sequence[] args, final Sequence contextSequence) throws XPathException { - final URI collectionUri; + @Nullable final URI collectionUri; if (args.length == 0 || args[0].isEmpty()) { collectionUri = null; } else { collectionUri = asUri(args[0].itemAt(0).getStringValue()); } - return getCollectionItems(new URI[] { collectionUri }); + return getCollectionItems(collectionUri); } } From 98c9e5617e4c7f8e3288b8f4d7a5a4b50e3aa17d Mon Sep 17 00:00:00 2001 From: Adam Retter Date: Tue, 28 Apr 2026 16:15:45 +0200 Subject: [PATCH 05/18] [optimize] Optimise the use of fn:collection and xmldb:xcollection --- .../xquery/functions/fn/ExtCollection.java | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/ExtCollection.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/ExtCollection.java index 5436c9e584..8d20afcfd4 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/ExtCollection.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/ExtCollection.java @@ -122,18 +122,16 @@ protected Sequence getCollectionItems(@Nullable final URI collectionUri) throws } private Sequence getDefaultCollectionItems() throws XPathException { - final Sequence docs = new ValueSequence(); - addAll(context.getStaticallyKnownDocuments(), docs); + final DocumentSet staticallyKnownDocuments = context.getStaticallyKnownDocuments(); + final Sequence items = new ValueSequence(staticallyKnownDocuments.getDocumentCount()); + addAll(staticallyKnownDocuments, items); + final Sequence dynamicCollection = context.getDynamicallyAvailableCollection(""); if (dynamicCollection != null) { - final Sequence result = new ValueSequence(); - result.addAll(docs); - result.addAll(dynamicCollection); - return result; - - } else { - return docs; + items.addAll(dynamicCollection); } + + return items; } private Sequence getCollectionUriItems(final URI collectionUri) throws XPathException { @@ -142,7 +140,7 @@ private Sequence getCollectionUriItems(final URI collectionUri) throws XPathExce return dynamicCollection; } else { - final MutableDocumentSet ndocs = new DefaultDocumentSet(); + @Nullable MutableDocumentSet docs = null; final XmldbURI uri = XmldbURI.create(collectionUri); try (@Nullable final Collection coll = context.getBroker().openCollection(uri, Lock.LockMode.READ_LOCK)) { if (coll == null) { @@ -150,11 +148,11 @@ private Sequence getCollectionUriItems(final URI collectionUri) throws XPathExce throw new XPathException(this, ErrorCodes.FODC0002, "Can not access collection '" + uri + "'"); } } else { + docs = new DefaultDocumentSet(); if (context.inProtectedMode()) { - context.getProtectedDocs().getDocsByCollection(coll, ndocs); + context.getProtectedDocs().getDocsByCollection(coll, docs); } else { - coll.allDocs(context.getBroker(), ndocs, - includeSubCollections, context.getProtectedDocs()); + coll.allDocs(context.getBroker(), docs, includeSubCollections, context.getProtectedDocs()); } } } catch (final PermissionDeniedException e) { @@ -163,9 +161,12 @@ private Sequence getCollectionUriItems(final URI collectionUri) throws XPathExce throw new XPathException(this, ErrorCodes.FODC0002, e.getMessage(), new StringValue(collectionUri.toString()), e); } - // add the docs to the items - final Sequence items = new ValueSequence(ndocs.getDocumentCount()); - addAll(ndocs, items); + if (docs == null || docs.getDocumentCount() == 0) { + return Sequence.EMPTY_SEQUENCE; + } + + final Sequence items = new ValueSequence(docs.getDocumentCount()); + addAll(docs, items); return items; } } From eaaaf0b348032f38a7e58975bf1fa693ba7dafc9 Mon Sep 17 00:00:00 2001 From: Adam Retter Date: Tue, 28 Apr 2026 16:16:40 +0200 Subject: [PATCH 06/18] [optimize] Only an intention lock is needed at this point --- .../src/main/java/org/exist/collections/MutableCollection.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exist-core/src/main/java/org/exist/collections/MutableCollection.java b/exist-core/src/main/java/org/exist/collections/MutableCollection.java index 03eb981dc0..904eed76ba 100644 --- a/exist-core/src/main/java/org/exist/collections/MutableCollection.java +++ b/exist-core/src/main/java/org/exist/collections/MutableCollection.java @@ -481,7 +481,7 @@ public MutableDocumentSet allDocs(final DBBroker broker, final MutableDocumentSe if(recursive && subColls != null) { // process the child collections for(final XmldbURI subCol : subColls) { - try(final Collection child = broker.openCollection(subCol, NO_LOCK)) { // NOTE: the recursive call below to child.addDocs will take a lock + try(final Collection child = broker.openCollection(subCol, INTENTION_READ)) { // NOTE: the recursive call below to child.addDocs will take a lock //A collection may have been removed in the meantime, so check first if(child != null) { child.allDocs(broker, docs, recursive, lockMap); From 14a5010ff035e69f9aeec30b33132e33e24402d3 Mon Sep 17 00:00:00 2001 From: Adam Retter Date: Tue, 28 Apr 2026 16:17:42 +0200 Subject: [PATCH 07/18] [bugfix] When serializing typed results from the REST API ensure that the XML Schema namespace is declared --- .../org/exist/storage/serializers/Serializer.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/exist-core/src/main/java/org/exist/storage/serializers/Serializer.java b/exist-core/src/main/java/org/exist/storage/serializers/Serializer.java index 5f47ca3bef..084c1c9a29 100644 --- a/exist-core/src/main/java/org/exist/storage/serializers/Serializer.java +++ b/exist-core/src/main/java/org/exist/storage/serializers/Serializer.java @@ -1093,6 +1093,9 @@ public void toSAX(final Sequence seq, int start, final int count, final boolean } if (wrap) { receiver.startPrefixMapping(Namespaces.EXIST_NS_PREFIX, Namespaces.EXIST_NS); + if (typed) { + receiver.startPrefixMapping(Namespaces.SCHEMA_NS_PREFIX, Namespaces.SCHEMA_NS); + } receiver.startElement(ELEM_RESULT_QNAME, attrs); } @@ -1107,6 +1110,9 @@ public void toSAX(final Sequence seq, int start, final int count, final boolean if (wrap) { receiver.endElement(ELEM_RESULT_QNAME); + if (typed) { + receiver.endPrefixMapping(Namespaces.SCHEMA_NS_PREFIX); + } receiver.endPrefixMapping(Namespaces.EXIST_NS_PREFIX); } receiver.endDocument(); @@ -1180,6 +1186,9 @@ public void toSAX(final Item item, final boolean wrap, final boolean typed) thro if (wrap) { receiver.startPrefixMapping(Namespaces.EXIST_NS_PREFIX, Namespaces.EXIST_NS); + if (typed) { + receiver.startPrefixMapping(Namespaces.SCHEMA_NS_PREFIX, Namespaces.SCHEMA_NS); + } receiver.startElement(ELEM_RESULT_QNAME, attrs); } @@ -1187,6 +1196,9 @@ public void toSAX(final Item item, final boolean wrap, final boolean typed) thro if (wrap) { receiver.endElement(ELEM_RESULT_QNAME); + if (typed) { + receiver.endPrefixMapping(Namespaces.SCHEMA_NS_PREFIX); + } receiver.endPrefixMapping(Namespaces.EXIST_NS_PREFIX); } From cb082ae5987c2eb0ba3c139d3a253a99e7e8d001 Mon Sep 17 00:00:00 2001 From: Adam Retter Date: Tue, 28 Apr 2026 16:29:49 +0200 Subject: [PATCH 08/18] [optimize] Optimize marshalling/demarshalling of typed REST API XML --- .../src/main/java/org/exist/xqj/Marshaller.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/exist-core/src/main/java/org/exist/xqj/Marshaller.java b/exist-core/src/main/java/org/exist/xqj/Marshaller.java index e6e9410810..b5fa130be1 100644 --- a/exist-core/src/main/java/org/exist/xqj/Marshaller.java +++ b/exist-core/src/main/java/org/exist/xqj/Marshaller.java @@ -253,7 +253,8 @@ public static Sequence demarshall(final XMLStreamReader parser) throws XMLStream if (!SEQ_ELEMENT.equals(parser.getLocalName())) { throw new XMLStreamException("Root element should be a " + SEQ_ELEMENT_PREFIXED_NAME); } - final ValueSequence result = new ValueSequence(); + + Sequence result = Sequence.EMPTY_SEQUENCE; while ((event = parser.next()) != XMLStreamConstants.END_DOCUMENT) { switch (event) { case XMLStreamConstants.START_ELEMENT : @@ -278,6 +279,10 @@ public static Sequence demarshall(final XMLStreamReader parser) throws XMLStream } else { item = new StringValue(null, parser.getElementText()).convertTo(type); } + + if (result == Sequence.EMPTY_SEQUENCE) { + result = new ValueSequence(); + } result.add(item); } break; @@ -310,10 +315,14 @@ private static Sequence demarshallSequence(final XQueryContext context, final No } private static Sequence demarshallValues(final XQueryContext context, final NodeImpl node) throws XMLStreamException, XPathException { - final ValueSequence result = new ValueSequence(); final InMemoryNodeSet sxValues = new InMemoryNodeSet(); node.selectChildren(new NameTest(Type.ELEMENT, VALUE_ELEMENT_QNAME), sxValues); + if (sxValues.isEmpty()) { + return Sequence.EMPTY_SEQUENCE; + } + + final ValueSequence result = new ValueSequence(sxValues.size()); for (final SequenceIterator itSxValue = sxValues.iterate(); itSxValue.hasNext();) { final ElementImpl sxValue = (ElementImpl) itSxValue.nextItem(); final Item item = demarshallValue(context, sxValue); From e1af401132c503f80d4158d5f8cc84826497847e Mon Sep 17 00:00:00 2001 From: Adam Retter Date: Tue, 28 Apr 2026 16:31:40 +0200 Subject: [PATCH 09/18] [refactor] Make fn:analyze-string compatible with Saxon versions 9 through 12 --- .../xquery/functions/fn/FunAnalyzeString.java | 135 ++++++++++++++++-- 1 file changed, 120 insertions(+), 15 deletions(-) diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunAnalyzeString.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunAnalyzeString.java index aedc36c638..8d8804be04 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunAnalyzeString.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunAnalyzeString.java @@ -45,9 +45,18 @@ */ package org.exist.xquery.functions.fn; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; +import net.bytebuddy.implementation.InvocationHandlerAdapter; +import net.bytebuddy.matcher.ElementMatchers; import net.sf.saxon.Configuration; import net.sf.saxon.om.Item; import net.sf.saxon.regex.RegexIterator; @@ -72,6 +81,21 @@ */ public class FunAnalyzeString extends BasicFunction { + /** + * Implements a ByteBuddy Invocation Handler to implement the `characters` + * method of Saxon 9's net.sf.saxon.regex.RegexIterator$MatchHandler + * and Saxon 12's net.sf.saxon.regex.RegexMatchHandler + */ + private static final InvocationHandler CHARACTERS_HANDLER = (final Object proxy, final Method method, final Object[] args) -> { + final MemTreeBuilder builder = ((AbstractSaxonRegexMatchHandler) proxy).builder; + final Object s = args[0]; + builder.characters(s.toString()); + return null; + }; + + private static final Constructor SAXON_MATCH_HANDLER_CLASS_CONSTRUCTOR = getSaxonMatchHandlerClassConstructor(createSaxonMatchHandlerClass()); + private static final Method SAXON_PROCESS_MATCHING_SUBSTRING_FN = getSaxonProcessMatchingSubstringFunction(); + private final static QName fnAnalyzeString = new QName("analyze-string", FnModule.NAMESPACE_URI); private final static QName QN_MATCH = new QName("match", FnModule.NAMESPACE_URI); @@ -185,26 +209,107 @@ private void analyzeString(final MemTreeBuilder builder, final String input, Str private void match(final MemTreeBuilder builder, final RegexIterator regexIterator) throws net.sf.saxon.trans.XPathException { builder.startElement(QN_MATCH, null); - regexIterator.processMatchingSubstring(new RegexIterator.MatchHandler() { - @Override - public void characters(final CharSequence s) { - builder.characters(s); - } - @Override - public void onGroupStart(final int groupNumber) throws net.sf.saxon.trans.XPathException { - final AttributesImpl attributes = new AttributesImpl(); - attributes.addAttribute("", QN_NR.getLocalPart(), QN_NR.getLocalPart(), "int", Integer.toString(groupNumber)); + try { + final AbstractSaxonRegexMatchHandler matchHandler = SAXON_MATCH_HANDLER_CLASS_CONSTRUCTOR.newInstance(builder); + SAXON_PROCESS_MATCHING_SUBSTRING_FN.invoke(regexIterator, matchHandler); + } catch (final InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new net.sf.saxon.trans.XPathException("Unable to dynamically invoke net.sf.saxon.regex.RegexIterator#processMatchingSubsOkay,tring: " + e.getMessage(), e); + } - builder.startElement(QN_GROUP, attributes); + builder.endElement(); + } + + private static Method getSaxonProcessMatchingSubstringFunction() { + final String saxonVersion = net.sf.saxon.Version.getProductVersion(); + try { + final Class matchHandlerInterfaceClazz; + if (saxonVersion.startsWith("12.")) { + matchHandlerInterfaceClazz = getSaxon12MatchHandlerInterfaceClass(); + } else { + matchHandlerInterfaceClazz = getSaxon9MatchHandlerInterfaceClass(); } - @Override - public void onGroupEnd(final int groupNumber) throws net.sf.saxon.trans.XPathException { - builder.endElement(); + return RegexIterator.class.getMethod("processMatchingSubstring", matchHandlerInterfaceClazz); + + } catch (final ClassNotFoundException | NoSuchMethodException e) { + throw new IllegalStateException("Unable to dynamically access Saxon RegexIterator#processMatchingSubstring method: " + e.getMessage(), e); + } + } + + private static Constructor getSaxonMatchHandlerClassConstructor(final Class saxonMatchHandlerClass) { + try { + return saxonMatchHandlerClass.getDeclaredConstructor(MemTreeBuilder.class); + } catch (final NoSuchMethodException e) { + throw new IllegalStateException("Unable to get constructor of dynamic Saxon Match Handler class: " + e.getMessage(), e); + } + } + + private static Class createSaxonMatchHandlerClass() { + final String saxonVersion = net.sf.saxon.Version.getProductVersion(); + try { + if (saxonVersion.startsWith("12.")) { + return createSaxon12MatchHandlerClass(); + } else { + return createSaxon9MatchHandlerClass(); } - }); - builder.endElement(); + } catch (final ClassNotFoundException e) { + throw new IllegalStateException("Unable to dynamically create Saxon Match Handler class: " + e.getMessage(), e); + } + } + + private static Class getSaxon12MatchHandlerInterfaceClass() throws ClassNotFoundException { + return Class.forName("net.sf.saxon.regex.RegexMatchHandler"); + } + + private static Class getSaxon9MatchHandlerInterfaceClass() throws ClassNotFoundException { + return Class.forName("net.sf.saxon.regex.RegexIterator$MatchHandler"); + } + + private static Class createSaxon12MatchHandlerClass() throws ClassNotFoundException { + final Class matchHandlerInterfaceClazz = getSaxon12MatchHandlerInterfaceClass(); + return createSaxonMatchHandlerClass(matchHandlerInterfaceClazz); + } + + private static Class createSaxon9MatchHandlerClass() throws ClassNotFoundException { + final Class matchHandlerInterfaceClazz = getSaxon9MatchHandlerInterfaceClass(); + return createSaxonMatchHandlerClass(matchHandlerInterfaceClazz); + } + + private static Class createSaxonMatchHandlerClass(final Class saxonMatchHandlerInterface) throws IllegalStateException{ + try { + return new ByteBuddy().subclass(AbstractSaxonRegexMatchHandler.class) + .implement(saxonMatchHandlerInterface) + .method(ElementMatchers.named("characters")) + .intercept(InvocationHandlerAdapter.of(CHARACTERS_HANDLER)) + .make() + .load(AbstractSaxonRegexMatchHandler.class.getClassLoader(), ClassLoadingStrategy.UsingLookup.of(MethodHandles.privateLookupIn(AbstractSaxonRegexMatchHandler.class, java.lang.invoke.MethodHandles.lookup()))) + .getLoaded(); + } catch (final IllegalAccessException e) { + throw new IllegalStateException("Unable to obtain lookup for dynamic Saxon Match Handler class: " + e.getMessage(), e); + } + } + + /** + * Implements the common methods of Saxon 9's net.sf.saxon.regex.RegexIterator$MatchHandler + * and Saxon 12's net.sf.saxon.regex.RegexMatchHandler + */ + private static abstract class AbstractSaxonRegexMatchHandler { + private final MemTreeBuilder builder; + + public AbstractSaxonRegexMatchHandler(final MemTreeBuilder builder) { + this.builder = builder; + } + + public void onGroupStart(final int groupNumber) { + final AttributesImpl attributes = new AttributesImpl(); + attributes.addAttribute("", QN_NR.getLocalPart(), QN_NR.getLocalPart(), "int", Integer.toString(groupNumber)); + builder.startElement(QN_GROUP, attributes); + } + + public void onGroupEnd(final int groupNumber) { + builder.endElement(); + } } private void nonMatch(final MemTreeBuilder builder, final Item item) { From 3d12dda5aa00661e88f1b92e4077c644d6287307 Mon Sep 17 00:00:00 2001 From: Adam Retter Date: Tue, 28 Apr 2026 16:35:19 +0200 Subject: [PATCH 10/18] [feature] When looking for conf.xml on the Classpath, also check the package org.exist.util --- .../main/java/org/exist/util/Configuration.java | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/exist-core/src/main/java/org/exist/util/Configuration.java b/exist-core/src/main/java/org/exist/util/Configuration.java index 6dbf02724c..889783c6b2 100644 --- a/exist-core/src/main/java/org/exist/util/Configuration.java +++ b/exist-core/src/main/java/org/exist/util/Configuration.java @@ -293,12 +293,23 @@ public Configuration(@Nullable String configFilename, Optional existHomeDi // firstly, try to read the configuration from a file within the // classpath try { - is = Configuration.class.getClassLoader().getResourceAsStream(configFilename); + // 1. we try the root of the class hierarchy + is = Configuration.class.getClassLoader().getResourceAsStream(configFilename); if (is != null) { - LOG.info("Reading configuration from classloader"); configFilePath = Optional.of(Paths.get(Configuration.class.getClassLoader().getResource(configFilename).toURI())); + LOG.info("Reading configuration from Class Loader: " + configFilePath.get()); + } + + if (is == null) { + // 2. we try the package org.exist.util + is = Configuration.class.getResourceAsStream(configFilename); + if (is != null) { + configFilePath = Optional.of(Paths.get(Configuration.class.getResource(configFilename).toURI())); + LOG.info("Reading configuration from Classpath org.exist.util: " + configFilePath.get()); + } } + } catch (final Exception e) { // EB: ignore and go forward, e.g. in case there is an absolute // file name for configFileName From 5cb1d7b469c35a92705d85406a9ae4ff38a58b1a Mon Sep 17 00:00:00 2001 From: Adam Retter Date: Tue, 28 Apr 2026 16:39:52 +0200 Subject: [PATCH 11/18] [feature] Add additional constructors and converted methods for several XDM Value Types --- .../xquery/value/AbstractDateTimeValue.java | 6 ++ .../xquery/value/DateTimeStampValue.java | 10 ++++ .../org/exist/xquery/value/DateTimeValue.java | 59 +++++++++++++++++-- .../org/exist/xquery/value/DateValue.java | 5 +- .../xquery/value/DayTimeDurationValue.java | 4 +- .../org/exist/xquery/value/TimeValue.java | 6 ++ .../xquery/value/YearMonthDurationValue.java | 4 +- 7 files changed, 85 insertions(+), 9 deletions(-) diff --git a/exist-core/src/main/java/org/exist/xquery/value/AbstractDateTimeValue.java b/exist-core/src/main/java/org/exist/xquery/value/AbstractDateTimeValue.java index ee123565c7..f830b6f98d 100644 --- a/exist-core/src/main/java/org/exist/xquery/value/AbstractDateTimeValue.java +++ b/exist-core/src/main/java/org/exist/xquery/value/AbstractDateTimeValue.java @@ -129,6 +129,12 @@ protected AbstractDateTimeValue(final Expression expression, String lexicalValue } } + protected static XMLGregorianCalendar toXMLGregorianCalendar(final GregorianCalendar gregorianCalendar) { + final XMLGregorianCalendar xgc = TimeUtils.getInstance().newXMLGregorianCalendar(gregorianCalendar); + xgc.normalize(); + return xgc; + } + /** * Utility method that is able to clone a calendar whose year is 0 * (whatever a year 0 means). diff --git a/exist-core/src/main/java/org/exist/xquery/value/DateTimeStampValue.java b/exist-core/src/main/java/org/exist/xquery/value/DateTimeStampValue.java index 5e9f71e943..3a198bbc89 100644 --- a/exist-core/src/main/java/org/exist/xquery/value/DateTimeStampValue.java +++ b/exist-core/src/main/java/org/exist/xquery/value/DateTimeStampValue.java @@ -56,6 +56,7 @@ import javax.xml.datatype.XMLGregorianCalendar; import javax.xml.namespace.QName; import java.nio.ByteBuffer; +import java.time.ZonedDateTime; /** * @author Radek Hübner @@ -83,6 +84,15 @@ public DateTimeStampValue(final Expression expression, final String dateTime) th checkValidTimezone(); } + public DateTimeStampValue(final ZonedDateTime zonedDateTime) throws XPathException { + this(null, zonedDateTime); + } + + public DateTimeStampValue(final Expression expression, final ZonedDateTime zonedDateTime) throws XPathException { + super(expression, zonedDateTime); + checkValidTimezone(); + } + private void checkValidTimezone() throws XPathException { if(calendar.getTimezone() == DatatypeConstants.FIELD_UNDEFINED) { throw new XPathException(getExpression(), ErrorCodes.ERROR, "Unable to create xs:dateTimeStamp, timezone missing."); diff --git a/exist-core/src/main/java/org/exist/xquery/value/DateTimeValue.java b/exist-core/src/main/java/org/exist/xquery/value/DateTimeValue.java index 13e982e7d4..bd70af7064 100644 --- a/exist-core/src/main/java/org/exist/xquery/value/DateTimeValue.java +++ b/exist-core/src/main/java/org/exist/xquery/value/DateTimeValue.java @@ -58,6 +58,12 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.time.Instant; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.temporal.TemporalField; import java.util.Date; import java.util.GregorianCalendar; @@ -96,7 +102,25 @@ public DateTimeValue(final Instant instant) { } public DateTimeValue(final Expression expression, final Instant instant) { - super(expression, dateToXMLGregorianCalendar(new Date(instant.toEpochMilli()))); + super(expression, toXMLGregorianCalendar(new Date(instant.toEpochMilli()))); + normalize(); + } + + public DateTimeValue(final ZonedDateTime zonedDateTime) { + this(null, zonedDateTime); + } + + public DateTimeValue(final Expression expression, final ZonedDateTime zonedDateTime) { + super(expression, toXMLGregorianCalendar(zonedDateTime)); + normalize(); + } + + public DateTimeValue(final LocalDateTime localDateTime) { + this(null, localDateTime); + } + + public DateTimeValue(final Expression expression, final LocalDateTime localDateTime) { + super(expression, toXMLGregorianCalendar(localDateTime)); normalize(); } @@ -105,7 +129,7 @@ public DateTimeValue(final Date date) { } public DateTimeValue(final Expression expression, Date date) { - super(expression, dateToXMLGregorianCalendar(date)); + super(expression, toXMLGregorianCalendar(date)); normalize(); } @@ -125,14 +149,35 @@ public DateTimeValue(final Expression expression, String dateTime) throws XPathE normalize(); } - private static XMLGregorianCalendar dateToXMLGregorianCalendar(Date date) { + private static XMLGregorianCalendar toXMLGregorianCalendar(final Date date) { final GregorianCalendar gc = new GregorianCalendar(); gc.setTime(date); - final XMLGregorianCalendar xgc = TimeUtils.getInstance().newXMLGregorianCalendar(gc); + return toXMLGregorianCalendar(gc); + } + + private static XMLGregorianCalendar toXMLGregorianCalendar(final LocalDateTime localDateTime) { + final XMLGregorianCalendar xgc = TimeUtils.getInstance().newXMLGregorianCalendar(); + xgc.setYear(localDateTime.getYear()); + xgc.setMonth(localDateTime.getMonthValue()); + xgc.setDay(localDateTime.getDayOfMonth()); + xgc.setHour(localDateTime.getHour()); + xgc.setMinute(localDateTime.getMinute()); + xgc.setSecond(localDateTime.getSecond()); + xgc.setMillisecond(localDateTime.getNano() / 1_000_000); + xgc.setTimezone(DatatypeConstants.FIELD_UNDEFINED); xgc.normalize(); return xgc; } + private static XMLGregorianCalendar toXMLGregorianCalendar(final Instant instant) { + return toXMLGregorianCalendar(LocalDateTime.ofInstant(instant, ZoneOffset.UTC)); + } + + private static XMLGregorianCalendar toXMLGregorianCalendar(final ZonedDateTime zonedDateTime) { + final GregorianCalendar gc = GregorianCalendar.from(zonedDateTime); + return toXMLGregorianCalendar(gc); + } + private static XMLGregorianCalendar fillCalendar(XMLGregorianCalendar calendar) { if (calendar.getHour() == DatatypeConstants.FIELD_UNDEFINED) { calendar.setHour(0); @@ -228,6 +273,12 @@ public T toJavaObject(final Class target) throws XPathException { final ByteBuffer buf = ByteBuffer.allocate(SERIALIZED_SIZE); serialize(buf); return (T) buf; + } else if (target == ZonedDateTime.class) { + return (T) calendar.toGregorianCalendar().toZonedDateTime(); + } else if (target == OffsetDateTime.class) { + return (T) calendar.toGregorianCalendar().toZonedDateTime().toOffsetDateTime(); + } else if (target == LocalDateTime.class) { + return (T)calendar.toGregorianCalendar().toZonedDateTime().toLocalDateTime(); } else { return super.toJavaObject(target); } diff --git a/exist-core/src/main/java/org/exist/xquery/value/DateValue.java b/exist-core/src/main/java/org/exist/xquery/value/DateValue.java index 262895c217..3caa57aed5 100644 --- a/exist-core/src/main/java/org/exist/xquery/value/DateValue.java +++ b/exist-core/src/main/java/org/exist/xquery/value/DateValue.java @@ -55,6 +55,7 @@ import javax.xml.datatype.XMLGregorianCalendar; import javax.xml.namespace.QName; import java.nio.ByteBuffer; +import java.time.LocalDate; import java.util.GregorianCalendar; /** @@ -93,7 +94,7 @@ public DateValue(final XMLGregorianCalendar calendar) throws XPathException { this(null, calendar); } - public DateValue(final Expression expression, XMLGregorianCalendar calendar) throws XPathException { + public DateValue(final Expression expression, final XMLGregorianCalendar calendar) throws XPathException { super(expression, stripCalendar(cloneXMLGregorianCalendar(calendar))); } @@ -179,6 +180,8 @@ public T toJavaObject(final Class target) throws XPathException { return (T) buf; } else if (target == Long.class || target == long.class) { return (T) Long.valueOf(serializeToLong()); + } else if (target == LocalDate.class) { + return (T)calendar.toGregorianCalendar().toZonedDateTime().toLocalDate(); } else { return super.toJavaObject(target); } diff --git a/exist-core/src/main/java/org/exist/xquery/value/DayTimeDurationValue.java b/exist-core/src/main/java/org/exist/xquery/value/DayTimeDurationValue.java index edf30aab58..299df41358 100644 --- a/exist-core/src/main/java/org/exist/xquery/value/DayTimeDurationValue.java +++ b/exist-core/src/main/java/org/exist/xquery/value/DayTimeDurationValue.java @@ -72,11 +72,11 @@ public class DayTimeDurationValue extends OrderedDurationValue { public static final Duration CANONICAL_ZERO_DURATION = TimeUtils.getInstance().newDuration(true, null, null, null, null, null, ZERO_DECIMAL); - DayTimeDurationValue(final Duration duration) throws XPathException { + public DayTimeDurationValue(final Duration duration) throws XPathException { this(null, duration); } - DayTimeDurationValue(final Expression expression, Duration duration) throws XPathException { + public DayTimeDurationValue(final Expression expression, Duration duration) throws XPathException { super(expression, duration); if (duration.isSet(DatatypeConstants.YEARS) || duration.isSet(DatatypeConstants.MONTHS)) { throw new XPathException(getExpression(), ErrorCodes.XPTY0004, "the value '" + duration + "' is not an xdt:dayTimeDuration since it specifies year or month values"); diff --git a/exist-core/src/main/java/org/exist/xquery/value/TimeValue.java b/exist-core/src/main/java/org/exist/xquery/value/TimeValue.java index faaf239789..9a8cc01ce7 100644 --- a/exist-core/src/main/java/org/exist/xquery/value/TimeValue.java +++ b/exist-core/src/main/java/org/exist/xquery/value/TimeValue.java @@ -55,6 +55,8 @@ import javax.xml.datatype.XMLGregorianCalendar; import javax.xml.namespace.QName; import java.nio.ByteBuffer; +import java.time.LocalTime; +import java.time.OffsetTime; import java.util.GregorianCalendar; /** @@ -172,6 +174,10 @@ public T toJavaObject(final Class target) throws XPathException { return (T) buf; } else if (target == Long.class || target == long.class) { return (T) Long.valueOf(serializeToLong()); + } else if (target == OffsetTime.class) { + return (T)calendar.toGregorianCalendar().toZonedDateTime().toOffsetDateTime().toOffsetTime(); + } else if (target == LocalTime.class) { + return (T)calendar.toGregorianCalendar().toZonedDateTime().toLocalTime(); } else { return super.toJavaObject(target); } diff --git a/exist-core/src/main/java/org/exist/xquery/value/YearMonthDurationValue.java b/exist-core/src/main/java/org/exist/xquery/value/YearMonthDurationValue.java index 3d0f333711..fc8b8921e2 100644 --- a/exist-core/src/main/java/org/exist/xquery/value/YearMonthDurationValue.java +++ b/exist-core/src/main/java/org/exist/xquery/value/YearMonthDurationValue.java @@ -71,11 +71,11 @@ public class YearMonthDurationValue extends OrderedDurationValue { public static final Duration CANONICAL_ZERO_DURATION = TimeUtils.getInstance().newDuration(true, null, BigInteger.ZERO, null, null, null, null); - YearMonthDurationValue(final Duration duration) throws XPathException { + public YearMonthDurationValue(final Duration duration) throws XPathException { this(null, duration); } - YearMonthDurationValue(final Expression expression, Duration duration) throws XPathException { + public YearMonthDurationValue(final Expression expression, Duration duration) throws XPathException { super(expression, duration); if (!duration.equals(DurationValue.CANONICAL_ZERO_DURATION)) { if (duration.isSet(DatatypeConstants.DAYS) || From 9d620913e1de861fbba36afb76cc69f1e089379f Mon Sep 17 00:00:00 2001 From: Adam Retter Date: Tue, 28 Apr 2026 16:42:44 +0200 Subject: [PATCH 12/18] [feature] Allow tests to configure use of temporary storage via the property 'exist.use-temporary-storage' --- .../src/main/java/org/exist/test/ExistEmbeddedServer.java | 8 ++++++-- .../src/main/java/org/exist/test/ExistWebServer.java | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/exist-core/src/main/java/org/exist/test/ExistEmbeddedServer.java b/exist-core/src/main/java/org/exist/test/ExistEmbeddedServer.java index 325e24561f..bdfae3061a 100644 --- a/exist-core/src/main/java/org/exist/test/ExistEmbeddedServer.java +++ b/exist-core/src/main/java/org/exist/test/ExistEmbeddedServer.java @@ -52,6 +52,8 @@ public class ExistEmbeddedServer extends ExternalResource { private static final Logger LOG = LogManager.getLogger(ExistEmbeddedServer.class); + public static final String USE_TEMPORARY_STORAGE_PROPERTY = "exist.use-temporary-storage"; + private final Optional instanceName; private final Optional configFile; private final Optional configProperties; @@ -141,7 +143,8 @@ public void startDb() throws DatabaseConfigurationException, EXistException, IOE } }); - if (useTemporaryStorage) { + final boolean propUseTemporaryStorage = Boolean.parseBoolean(System.getProperty(USE_TEMPORARY_STORAGE_PROPERTY, "false")); + if (useTemporaryStorage || propUseTemporaryStorage) { if (!temporaryStorage.isPresent()) { this.temporaryStorage = Optional.of(Files.createTempDirectory("org.exist.test.ExistEmbeddedServer")); } @@ -196,7 +199,8 @@ public void stopDb(final boolean clearTemporaryStorage) { // clear instance variables pool = null; - if(useTemporaryStorage && temporaryStorage.isPresent() && clearTemporaryStorage) { + final boolean propUseTemporaryStorage = Boolean.parseBoolean(System.getProperty(USE_TEMPORARY_STORAGE_PROPERTY, "false")); + if((useTemporaryStorage || propUseTemporaryStorage) && temporaryStorage.isPresent() && clearTemporaryStorage) { FileUtils.deleteQuietly(temporaryStorage.get()); temporaryStorage = Optional.empty(); } diff --git a/exist-core/src/main/java/org/exist/test/ExistWebServer.java b/exist-core/src/main/java/org/exist/test/ExistWebServer.java index da058038fc..00bfcf9d7a 100644 --- a/exist-core/src/main/java/org/exist/test/ExistWebServer.java +++ b/exist-core/src/main/java/org/exist/test/ExistWebServer.java @@ -48,6 +48,8 @@ public class ExistWebServer extends ExternalResource { private static final Logger LOG = LogManager.getLogger(ExistWebServer.class); + public static final String USE_TEMPORARY_STORAGE_PROPERTY = "exist.use-temporary-storage"; + private static final String CONFIG_PROP_FILES = "org.exist.db-connection.files"; private static final String CONFIG_PROP_JOURNAL_DIR = "org.exist.db-connection.recovery.journal-dir"; @@ -113,7 +115,8 @@ protected void before() throws Throwable { } if (server == null) { - if(useTemporaryStorage) { + final boolean propUseTemporaryStorage = Boolean.parseBoolean(System.getProperty(USE_TEMPORARY_STORAGE_PROPERTY, "false")); + if(useTemporaryStorage || propUseTemporaryStorage) { this.temporaryStorage = Optional.of(Files.createTempDirectory("org.exist.test.ExistWebServer")); final String absTemporaryStorage = temporaryStorage.get().toAbsolutePath().toString(); System.setProperty(CONFIG_PROP_FILES, absTemporaryStorage); @@ -166,7 +169,8 @@ protected void after() { server.shutdown(); server = null; - if(useTemporaryStorage && temporaryStorage.isPresent()) { + final boolean propUseTemporaryStorage = Boolean.parseBoolean(System.getProperty(USE_TEMPORARY_STORAGE_PROPERTY, "false")); + if((useTemporaryStorage || propUseTemporaryStorage) && temporaryStorage.isPresent()) { FileUtils.deleteQuietly(temporaryStorage.get()); temporaryStorage = Optional.empty(); System.clearProperty(CONFIG_PROP_JOURNAL_DIR); From bf98b21a22722cf030b3860809c8a2b9407eac12 Mon Sep 17 00:00:00 2001 From: Adam Retter Date: Tue, 28 Apr 2026 16:47:18 +0200 Subject: [PATCH 13/18] [bugfix] Make deserialization of typed values to the REST API more forgiving --- .../main/java/org/exist/xqj/Marshaller.java | 106 +++++++++++------- 1 file changed, 63 insertions(+), 43 deletions(-) diff --git a/exist-core/src/main/java/org/exist/xqj/Marshaller.java b/exist-core/src/main/java/org/exist/xqj/Marshaller.java index b5fa130be1..a2880a7566 100644 --- a/exist-core/src/main/java/org/exist/xqj/Marshaller.java +++ b/exist-core/src/main/java/org/exist/xqj/Marshaller.java @@ -57,13 +57,9 @@ import org.exist.xquery.functions.array.ArrayType; import org.exist.xquery.functions.map.MapType; import org.exist.xquery.value.*; -import org.w3c.dom.Comment; -import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; -import org.w3c.dom.ProcessingInstruction; -import org.w3c.dom.Text; import org.xml.sax.ContentHandler; import org.xml.sax.SAXException; import org.xml.sax.helpers.AttributesImpl; @@ -331,7 +327,7 @@ private static Sequence demarshallValues(final XQueryContext context, final Node return result; } - private static Item demarshallValue(final XQueryContext context, final ElementImpl sxValue) throws XMLStreamException, XPathException { + public static Item demarshallValue(final XQueryContext context, final ElementImpl sxValue) throws XMLStreamException, XPathException { int type = Type.ITEM; final String typeName = sxValue.getAttribute(ATTR_TYPE); if (!typeName.isEmpty()) { @@ -349,7 +345,8 @@ private static Item demarshallValue(final XQueryContext context, final ElementIm final InMemoryNodeSet sxEntries = new InMemoryNodeSet(); sxValue.selectChildren(new NameTest(Type.ELEMENT, ENTRY_ELEMENT_QNAME), sxEntries); - Node item = sxValue.getFirstChild(); + @Nullable Node item = null; + item = sxValue.getFirstChild(); if (type == Type.ATTRIBUTE || (type == Type.ITEM && attrNameString != null)) { if (attrNameString.isEmpty()) { @@ -385,50 +382,73 @@ private static Item demarshallValue(final XQueryContext context, final ElementIm switch (type) { case Type.ELEMENT: - if (item instanceof Document) { - return (ElementImpl) ((DocumentImpl) item).getDocumentElement(); - } else if (!(item instanceof Element)) { - throw new XMLStreamException("sx:value should only contain an Element if type is " + typeName); - } else { - return (ElementImpl) item; - } + do { + if (item.getNodeType() == Node.DOCUMENT_NODE) { + item = ((DocumentImpl) item).getDocumentElement(); + } + + if (item.getNodeType() == Node.ELEMENT_NODE) { + return (ElementImpl) item; + } + + item = item.getNextSibling(); + } while (item != null); + + throw new XMLStreamException("sx:value must contain an Element if type is " + typeName); + case Type.COMMENT: - if (!(item instanceof Comment)) { - throw new XMLStreamException("sx:value should only contain a Comment node if type is " + typeName); - } - return (CommentImpl) item; + do { + if (item.getNodeType() == Node.COMMENT_NODE) { + return (CommentImpl) item; + } + item = item.getNextSibling(); + } while (item != null); + + throw new XMLStreamException("sx:value must contain a Comment node if type is " + typeName); + case Type.PROCESSING_INSTRUCTION: - if (!(item instanceof ProcessingInstruction)) { - throw new XMLStreamException("sx:value should only contain a Processing Instruction node if type is " + typeName); - } - return (ProcessingInstructionImpl) item; + do { + if (item.getNodeType() == Node.PROCESSING_INSTRUCTION_NODE) { + return (ProcessingInstructionImpl) item; + } + item = item.getNextSibling(); + } while (item != null); + + throw new XMLStreamException("sx:value must contain a Processing Instruction node if type is " + typeName); + case Type.TEXT: - if (!(item instanceof Text)) { - throw new XMLStreamException("sx:value should only contain a Text node if type is " + typeName); - } - return (TextImpl) item; + do { + if (item.getNodeType() == Node.TEXT_NODE) { + return (TextImpl) item; + } + item = item.getNextSibling(); + } while (item != null); + case Type.DOCUMENT: default: - if (item instanceof Document || item instanceof Element) { - final DocumentBuilderReceiver receiver = new DocumentBuilderReceiver(((NodeImpl) item).getExpression()); - try { - receiver.startDocument(); - ((NodeImpl) item).copyTo(null, receiver); - receiver.endDocument(); - } catch (final SAXException e) { - throw new XPathException(item != null ? ((NodeImpl) item).getExpression() : null, "Error while demarshalling node: " + e.getMessage(), e); + do { + if (item.getNodeType() == Node.DOCUMENT_NODE || item.getNodeType() == Node.ELEMENT_NODE) { + final DocumentBuilderReceiver receiver = new DocumentBuilderReceiver(((NodeImpl) item).getExpression()); + try { + receiver.startDocument(); + ((NodeImpl) item).copyTo(null, receiver); + receiver.endDocument(); + } catch (final SAXException e) { + throw new XPathException(item != null ? ((NodeImpl) item).getExpression() : null, "Error while demarshalling node: " + e.getMessage(), e); + } + return (NodeImpl) receiver.getDocument(); } - return (NodeImpl) receiver.getDocument(); - } else { - throw new XMLStreamException("sx:value should only contain a Node if type is " + typeName); - } + item = item.getNextSibling(); + } while (item != null); + + throw new XMLStreamException("sx:value must contain a Document or Element if type is " + typeName); } - } else if (type == Type.ITEM && !(item instanceof Text)) { + } else if (type == Type.ITEM && item.getNodeType() != Node.TEXT_NODE) { // item() type requested and we have been given a node which is not a text() node return (NodeImpl) item; @@ -463,13 +483,13 @@ private static Item demarshallValue(final XQueryContext context, final ElementIm } else { // specific non-node type or text() final StringBuilder data = new StringBuilder(); - while (item != null) { - if (!(item.getNodeType() == Node.TEXT_NODE || item.getNodeType() == Node.CDATA_SECTION_NODE)) { - throw new XMLStreamException("sx:value should only contain text if type is " + typeName); + do { + if (item.getNodeType() == Node.TEXT_NODE || item.getNodeType() == Node.CDATA_SECTION_NODE) { + data.append(item.getNodeValue()); } - data.append(item.getNodeValue()); item = item.getNextSibling(); - } + } while (item != null); + return new StringValue(data.toString()).convertTo(type); } } From 06bfdc9d9dbef8f62ff67187d599aee9e266a014 Mon Sep 17 00:00:00 2001 From: Adam Retter Date: Tue, 28 Apr 2026 16:53:19 +0200 Subject: [PATCH 14/18] [feature] Allow the Context Item for query execution to be set via the REST API --- .../main/java/org/exist/http/RESTServer.java | 84 +++++++++++++++---- .../org/exist/http/RESTServerParameter.java | 18 +++- exist-core/src/main/xsd/rest-api.xsd | 7 ++ schema/exist-rest-api.xsd | 10 +++ 4 files changed, 100 insertions(+), 19 deletions(-) diff --git a/exist-core/src/main/java/org/exist/http/RESTServer.java b/exist-core/src/main/java/org/exist/http/RESTServer.java index 438dc0f209..531a44225f 100644 --- a/exist-core/src/main/java/org/exist/http/RESTServer.java +++ b/exist-core/src/main/java/org/exist/http/RESTServer.java @@ -337,14 +337,26 @@ public void doGet(final DBBroker broker, final Txn transaction, final HttpServle query = getParameter(request, Query); } } - final String _var = getParameter(request, Variables); - ElementImpl variables = null; + + @Nullable final String _contextItem = getParameter(request, Context_Item); + @Nullable ElementImpl contextItemParam = null; + try { + if (_contextItem != null) { + contextItemParam = parseXML(broker.getBrokerPool(), _contextItem); + } + } catch (final SAXException e) { + final XPathException x = new XPathException(contextItemParam != null ? contextItemParam.getExpression() : null, e.toString()); + writeXPathException(response, HttpServletResponse.SC_BAD_REQUEST, DEFAULT_ENCODING, query, path, x); + } + + @Nullable final String _var = getParameter(request, Variables); + @Nullable ElementImpl variablesParam = null; try { if (_var != null) { - variables = parseXML(broker.getBrokerPool(), _var); + variablesParam = parseXML(broker.getBrokerPool(), _var); } } catch (final SAXException e) { - final XPathException x = new XPathException(variables != null ? variables.getExpression() : null, e.toString()); + final XPathException x = new XPathException(variablesParam != null ? variablesParam.getExpression() : null, e.toString()); writeXPathException(response, HttpServletResponse.SC_BAD_REQUEST, DEFAULT_ENCODING, query, path, x); } @@ -435,7 +447,7 @@ public void doGet(final DBBroker broker, final Txn transaction, final HttpServle if (query != null) { // query parameter specified, search method does all the rest of the work try { - search(broker, transaction, query, path, null, variables, howmany, start, typed, outputProperties, + search(broker, transaction, query, path, null, contextItemParam, variablesParam, howmany, start, typed, outputProperties, wrap, cache, request, response); } catch (final XPathException e) { @@ -785,7 +797,8 @@ public void doPost(final DBBroker broker, final Txn transaction, final HttpServl int howmany = 10; int start = 1; boolean typed = false; - ElementImpl variables = null; + @Nullable ElementImpl contextItemParam = null; + @Nullable ElementImpl variablesParam = null; boolean enclose = true; boolean cache = false; String query = null; @@ -870,8 +883,11 @@ public void doPost(final DBBroker broker, final Txn transaction, final HttpServl } query = buf.toString(); + } else if (Context_Item.xmlKey().equals(child.getLocalName())) { + contextItemParam = (ElementImpl) child; + } else if (Variables.xmlKey().equals(child.getLocalName())) { - variables = (ElementImpl) child; + variablesParam = (ElementImpl) child; } else if (Properties.xmlKey().equals(child.getLocalName())) { Node node = child.getFirstChild(); @@ -900,7 +916,7 @@ public void doPost(final DBBroker broker, final Txn transaction, final HttpServl if (query != null) { try { - search(broker, transaction, query, path, null, variables, + search(broker, transaction, query, path, null, contextItemParam, variablesParam, howmany, start, typed, outputProperties, enclose, cache, request, response); } catch (final XPathException e) { @@ -1296,7 +1312,8 @@ private String getRequestContent(final HttpServletRequest request) throws IOExce * @param query the XQuery * @param path the path of the request * @param namespaces any XQuery namespace bindings - * @param variables any XQuery variable bindings + * @param contextItemParam optional XQuery Context Item + * @param variablesParam any XQuery variable bindings * @param howmany the number of items in the results to return * @param start the start position in the results to return * @param typed whether the result nodes should be typed @@ -1312,7 +1329,8 @@ private String getRequestContent(final HttpServletRequest request) throws IOExce */ protected void search(final DBBroker broker, final Txn transaction, final String query, final String path, @Nullable final List namespaces, - @Nullable final ElementImpl variables, final int howmany, + @Nullable final ElementImpl contextItemParam, + @Nullable final ElementImpl variablesParam, final int howmany, final int start, final boolean typed, final Properties outputProperties, final boolean wrap, final boolean cache, final HttpServletRequest request, @@ -1385,10 +1403,13 @@ protected void search(final DBBroker broker, final Txn transaction, final String compilationTime = 0; } - declareVariables(context, variables, request, response); + declareVariables(context, variablesParam, request, response); + + @Nullable final Item contextItem = extractContextItem(contextItemParam); + final Sequence contextSequence = contextItem != null ? new ValueSequence(contextItem) : null; final long executeStart = System.currentTimeMillis(); - final Sequence resultSequence = xquery.execute(broker, compiled, null, outputProperties); + final Sequence resultSequence = xquery.execute(broker, compiled, contextSequence, outputProperties); final long executionTime = System.currentTimeMillis() - executeStart; if (LOG.isDebugEnabled()) { @@ -1427,16 +1448,43 @@ private void declareNamespaces(final XQueryContext context, @Nullable final List } } + /** + * Extract the Context Item from the Element parameter. + * + * @param contextItem a parameter specifying the Context Item for the XQuery, or null + * + * @throws XPathException if an error occurs extracting the Context Item. + */ + private @Nullable Item extractContextItem(@Nullable final ElementImpl contextItem) throws XPathException { + if (contextItem == null) { + return null; + } + + @Nullable final NodeImpl value = contextItem.getFirstChild(new NameTest(Type.ELEMENT, Marshaller.VALUE_ELEMENT_QNAME)); + if (value == null) { + return null; + } + + try { + return Marshaller.demarshallValue(null, (ElementImpl) value); + } catch (final XMLStreamException xe) { + throw new XPathException((Expression) null, xe.toString()); + } + } + /** * Pass the request, response and session objects to the XQuery context. * - * @param context - * @param request - * @param response - * @throws XPathException + * @param context the context for the XQuery + * @param variables variable bindings for the XQuery, or null + * @param request the HTTP request + * @param response the HTTP response + * + * @throws XPathException if an error occurs declaring variables */ private HttpRequestWrapper declareVariables(final XQueryContext context, - final ElementImpl variables, final HttpServletRequest request, + @Nullable final ElementImpl variables, + final HttpServletRequest request, final HttpServletResponse response) throws XPathException { final HttpRequestWrapper reqw = new HttpRequestWrapper(request, formEncoding, containerEncoding); @@ -1514,7 +1562,7 @@ private void declareExternalAndXQJVariables(final XQueryContext context, } // get serialized sequence - final NodeImpl value = variable.getFirstChild(new NameTest(Type.ELEMENT, Marshaller.SEQUENCE_ELEMENT_QNAME)); + @Nullable final NodeImpl value = variable.getFirstChild(new NameTest(Type.ELEMENT, Marshaller.SEQUENCE_ELEMENT_QNAME)); final Sequence sequence; try { sequence = value == null ? Sequence.EMPTY_SEQUENCE : Marshaller.demarshall(context, value); diff --git a/exist-core/src/main/java/org/exist/http/RESTServerParameter.java b/exist-core/src/main/java/org/exist/http/RESTServerParameter.java index d4e23166cb..06d6621a21 100644 --- a/exist-core/src/main/java/org/exist/http/RESTServerParameter.java +++ b/exist-core/src/main/java/org/exist/http/RESTServerParameter.java @@ -25,7 +25,7 @@ * Enumeration of each Parameter * used by the RESTServer * - * @author Adam Retter + * @author Adam Retter */ enum RESTServerParameter { @@ -76,12 +76,28 @@ enum RESTServerParameter { * encoding? = string * method? = string> * (exist:text, + * exist:context-item?, * exist:variables?, * exist:properties?) * */ Query, + /** + * Can be used in either the Query String of a GET request + * or in the body of a POST request to specify a value + * for the XQuery Context item. + * + * Contexts: GET, POST + * + * The value of this prarameter, is an XML element with the format + * + * + * (sx:value) + * + */ + Context_Item, + /** * Can be used in either the Query String of a GET request * or in the body of a POST request to specify values for diff --git a/exist-core/src/main/xsd/rest-api.xsd b/exist-core/src/main/xsd/rest-api.xsd index c1edbc8334..943445d38d 100644 --- a/exist-core/src/main/xsd/rest-api.xsd +++ b/exist-core/src/main/xsd/rest-api.xsd @@ -35,6 +35,13 @@ + + + + + + + diff --git a/schema/exist-rest-api.xsd b/schema/exist-rest-api.xsd index ad34ef5511..b325ea93eb 100644 --- a/schema/exist-rest-api.xsd +++ b/schema/exist-rest-api.xsd @@ -55,6 +55,16 @@ + + + Context Item for the XQuery + + + + + + + Properties to pass to the Serializer. From d3fd64f7af333fe4f82d772ec87db6810559e4ab Mon Sep 17 00:00:00 2001 From: Adam Retter Date: Tue, 28 Apr 2026 17:00:26 +0200 Subject: [PATCH 15/18] [feature] Allow the Default Collection of the Dynamic Context for query execution to be set via the REST API --- .../main/java/org/exist/http/RESTServer.java | 42 ++++++++++++++++++- .../org/exist/http/RESTServerParameter.java | 16 +++++++ .../xquery/functions/fn/ExtCollection.java | 19 ++++++--- exist-core/src/main/xsd/rest-api.xsd | 7 ++++ schema/exist-rest-api.xsd | 10 +++++ 5 files changed, 87 insertions(+), 7 deletions(-) diff --git a/exist-core/src/main/java/org/exist/http/RESTServer.java b/exist-core/src/main/java/org/exist/http/RESTServer.java index 531a44225f..1ca05adde8 100644 --- a/exist-core/src/main/java/org/exist/http/RESTServer.java +++ b/exist-core/src/main/java/org/exist/http/RESTServer.java @@ -349,6 +349,17 @@ public void doGet(final DBBroker broker, final Txn transaction, final HttpServle writeXPathException(response, HttpServletResponse.SC_BAD_REQUEST, DEFAULT_ENCODING, query, path, x); } + @Nullable final String _defaultCollection = getParameter(request, Default_Collection); + @Nullable ElementImpl defaultCollectionParam = null; + try { + if (_defaultCollection != null) { + defaultCollectionParam = parseXML(broker.getBrokerPool(), _defaultCollection); + } + } catch (final SAXException e) { + final XPathException x = new XPathException(defaultCollectionParam != null ? defaultCollectionParam.getExpression() : null, e.toString()); + writeXPathException(response, HttpServletResponse.SC_BAD_REQUEST, DEFAULT_ENCODING, query, path, x); + } + @Nullable final String _var = getParameter(request, Variables); @Nullable ElementImpl variablesParam = null; try { @@ -447,7 +458,7 @@ public void doGet(final DBBroker broker, final Txn transaction, final HttpServle if (query != null) { // query parameter specified, search method does all the rest of the work try { - search(broker, transaction, query, path, null, contextItemParam, variablesParam, howmany, start, typed, outputProperties, + search(broker, transaction, query, path, null, contextItemParam, defaultCollectionParam, variablesParam, howmany, start, typed, outputProperties, wrap, cache, request, response); } catch (final XPathException e) { @@ -798,6 +809,7 @@ public void doPost(final DBBroker broker, final Txn transaction, final HttpServl int start = 1; boolean typed = false; @Nullable ElementImpl contextItemParam = null; + @Nullable ElementImpl defaultCollectionParam = null; @Nullable ElementImpl variablesParam = null; boolean enclose = true; boolean cache = false; @@ -886,6 +898,9 @@ public void doPost(final DBBroker broker, final Txn transaction, final HttpServl } else if (Context_Item.xmlKey().equals(child.getLocalName())) { contextItemParam = (ElementImpl) child; + } else if (Default_Collection.xmlKey().equals(child.getLocalName())) { + defaultCollectionParam = (ElementImpl) child; + } else if (Variables.xmlKey().equals(child.getLocalName())) { variablesParam = (ElementImpl) child; @@ -916,7 +931,7 @@ public void doPost(final DBBroker broker, final Txn transaction, final HttpServl if (query != null) { try { - search(broker, transaction, query, path, null, contextItemParam, variablesParam, + search(broker, transaction, query, path, null, contextItemParam, defaultCollectionParam, variablesParam, howmany, start, typed, outputProperties, enclose, cache, request, response); } catch (final XPathException e) { @@ -1313,6 +1328,7 @@ private String getRequestContent(final HttpServletRequest request) throws IOExce * @param path the path of the request * @param namespaces any XQuery namespace bindings * @param contextItemParam optional XQuery Context Item + * @param defaultCollectionParam optional XQuery Default Collection * @param variablesParam any XQuery variable bindings * @param howmany the number of items in the results to return * @param start the start position in the results to return @@ -1330,6 +1346,7 @@ private String getRequestContent(final HttpServletRequest request) throws IOExce protected void search(final DBBroker broker, final Txn transaction, final String query, final String path, @Nullable final List namespaces, @Nullable final ElementImpl contextItemParam, + @Nullable final ElementImpl defaultCollectionParam, @Nullable final ElementImpl variablesParam, final int howmany, final int start, final boolean typed, final Properties outputProperties, final boolean wrap, @@ -1403,6 +1420,7 @@ protected void search(final DBBroker broker, final Txn transaction, final String compilationTime = 0; } + setupDefaultCollection(context, defaultCollectionParam); declareVariables(context, variablesParam, request, response); @Nullable final Item contextItem = extractContextItem(contextItemParam); @@ -1472,6 +1490,26 @@ private void declareNamespaces(final XQueryContext context, @Nullable final List } } + private void setupDefaultCollection(final XQueryContext context, @Nullable final ElementImpl defaultCollectionParam) throws XPathException { + if (defaultCollectionParam == null) { + return; + } + + @Nullable final NodeImpl value = defaultCollectionParam.getFirstChild(new NameTest(Type.ELEMENT, Marshaller.SEQUENCE_ELEMENT_QNAME)); + if (value == null) { + return; + } + + try { + @Nullable final Sequence sequence = Marshaller.demarshall(context, value); + if (sequence != null) { + context.addDynamicallyAvailableCollection("", (broker, txn, uri) -> sequence); + } + } catch (final XMLStreamException xe) { + throw new XPathException((Expression) null, xe.toString()); + } + } + /** * Pass the request, response and session objects to the XQuery context. * diff --git a/exist-core/src/main/java/org/exist/http/RESTServerParameter.java b/exist-core/src/main/java/org/exist/http/RESTServerParameter.java index 06d6621a21..bdca0d4436 100644 --- a/exist-core/src/main/java/org/exist/http/RESTServerParameter.java +++ b/exist-core/src/main/java/org/exist/http/RESTServerParameter.java @@ -77,6 +77,7 @@ enum RESTServerParameter { * method? = string> * (exist:text, * exist:context-item?, + * exist:default-collection? * exist:variables?, * exist:properties?) * @@ -98,6 +99,21 @@ enum RESTServerParameter { */ Context_Item, + /** + * Can be used in either the Query String of a GET request + * or in the body of a POST request to specify the values + * for the Default Collection of the XQuery Dynamic Context. + * + * Contexts: GET, POST + * + * The value of this prarameter, is an XML element with the format + * + * + * (sx:sequence) + * + */ + Default_Collection, + /** * Can be used in either the Query String of a GET request * or in the body of a POST request to specify values for diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/ExtCollection.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/ExtCollection.java index 8d20afcfd4..e70c409a4f 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/ExtCollection.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/ExtCollection.java @@ -122,13 +122,21 @@ protected Sequence getCollectionItems(@Nullable final URI collectionUri) throws } private Sequence getDefaultCollectionItems() throws XPathException { - final DocumentSet staticallyKnownDocuments = context.getStaticallyKnownDocuments(); - final Sequence items = new ValueSequence(staticallyKnownDocuments.getDocumentCount()); - addAll(staticallyKnownDocuments, items); + @Nullable Sequence items = null; - final Sequence dynamicCollection = context.getDynamicallyAvailableCollection(""); + @Nullable final Sequence dynamicCollection = context.getDynamicallyAvailableCollection(""); if (dynamicCollection != null) { - items.addAll(dynamicCollection); + items = new ValueSequence(dynamicCollection); + } + + if (items == null) { + final DocumentSet staticallyKnownDocuments = context.getStaticallyKnownDocuments(); + items = new ValueSequence(staticallyKnownDocuments.getDocumentCount()); + addAll(staticallyKnownDocuments, items); + } + + if (items == null) { + items = Sequence.EMPTY_SEQUENCE; } return items; @@ -141,6 +149,7 @@ private Sequence getCollectionUriItems(final URI collectionUri) throws XPathExce } else { @Nullable MutableDocumentSet docs = null; + final XmldbURI uri = XmldbURI.create(collectionUri); try (@Nullable final Collection coll = context.getBroker().openCollection(uri, Lock.LockMode.READ_LOCK)) { if (coll == null) { diff --git a/exist-core/src/main/xsd/rest-api.xsd b/exist-core/src/main/xsd/rest-api.xsd index 943445d38d..3052fbb7b8 100644 --- a/exist-core/src/main/xsd/rest-api.xsd +++ b/exist-core/src/main/xsd/rest-api.xsd @@ -42,6 +42,13 @@ + + + + + + + diff --git a/schema/exist-rest-api.xsd b/schema/exist-rest-api.xsd index b325ea93eb..da282f56f7 100644 --- a/schema/exist-rest-api.xsd +++ b/schema/exist-rest-api.xsd @@ -65,6 +65,16 @@ + + + Values for the Default Collection of the Dynamic Context + + + + + + + Properties to pass to the Serializer. From 03f583088c6872998589d3e52d7c0d0315f6ee16 Mon Sep 17 00:00:00 2001 From: Adam Retter Date: Tue, 28 Apr 2026 17:30:04 +0200 Subject: [PATCH 16/18] [refactor] Switch to the Evolved Binary port of the buildversion Maven Plugin --- elemental-parent/pom.xml | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/elemental-parent/pom.xml b/elemental-parent/pom.xml index e51571bfc0..1a6067ecdd 100644 --- a/elemental-parent/pom.xml +++ b/elemental-parent/pom.xml @@ -382,9 +382,9 @@ 3.4.0 - com.code54.mojo - buildversion-plugin - 1.0.3 + com.evolvedbinary.maven.plugins + buildversion-maven-plugin + 2.0.0 org.apache.maven.plugins @@ -640,8 +640,8 @@ - com.code54.mojo - buildversion-plugin + com.evolvedbinary.maven.plugins + buildversion-maven-plugin validate @@ -807,19 +807,4 @@ - - - clojars.org - https://clojars.org/repo - - - - - - central-ossrh-staging - Central Portal - OSSRH Staging API - https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/ - - - From fc6c9689a3ad39480c1f047d147e8a6e2a331add Mon Sep 17 00:00:00 2001 From: Adam Retter Date: Tue, 28 Apr 2026 17:53:16 +0200 Subject: [PATCH 17/18] [bugfix] Fix race condition when sorting values --- .../exist/xquery/value/OrderedValueSequence.java | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/exist-core/src/main/java/org/exist/xquery/value/OrderedValueSequence.java b/exist-core/src/main/java/org/exist/xquery/value/OrderedValueSequence.java index d0008be270..d0c5099e98 100644 --- a/exist-core/src/main/java/org/exist/xquery/value/OrderedValueSequence.java +++ b/exist-core/src/main/java/org/exist/xquery/value/OrderedValueSequence.java @@ -212,7 +212,9 @@ public void sort() { // FastQSort.sort(items, 0, count - 1); Arrays.parallelSort(items, 0, count); + Arrays.stream(items, 0, count).parallel().forEach(Entry::clear); + encounteredPrimitiveTypesForOrderSpecs.forEach(BitSet::clear); } @Override @@ -408,7 +410,6 @@ public void setContextSequence(@Nullable final Sequence contextSequence) { } private static class Entry implements Comparable { - private final List encounteredPrimitiveTypesForOrderSpecs; private final List orderSpecs; private Item item; private final int pos; @@ -417,13 +418,11 @@ private static class Entry implements Comparable { /** * Private constructor, use {@link #create(List, List, Item, int, Sequence)} instead. * - * @param encounteredPrimitiveTypesForOrderSpecs a list of bitset which will be populated with the primitive type of each value in the entry of each orderspec * @param item the item in the sequence. * @param position the original position of the item in the result sequence. * @param values the values for the entry. */ - private Entry(final List encounteredPrimitiveTypesForOrderSpecs, final List orderSpecs, final Item item, final int position, final List values) { - this.encounteredPrimitiveTypesForOrderSpecs = encounteredPrimitiveTypesForOrderSpecs; + private Entry(final List orderSpecs, final Item item, final int position, final List values) { this.orderSpecs = orderSpecs; this.item = item; this.pos = position; @@ -474,7 +473,7 @@ public static Entry create(final List encounteredPrimitiveTypesForOrderS } } - return new Entry(encounteredPrimitiveTypesForOrderSpecs, orderSpecs, item, position, values); + return new Entry(orderSpecs, item, position, values); } @Override @@ -555,10 +554,7 @@ public String toString() { } public void clear() { - for (final BitSet encounteredPrimitiveTypesForOrderSpec : encounteredPrimitiveTypesForOrderSpecs) { - encounteredPrimitiveTypesForOrderSpec.clear(); - } - values = null; + this.values = null; } } From a1b6475821404bcff352a2ccfe3b92fa5ad064ea Mon Sep 17 00:00:00 2001 From: Adam Retter Date: Tue, 28 Apr 2026 18:39:22 +0200 Subject: [PATCH 18/18] [feature] Add a '-a' and '--no-auto-deploy' argument to the Java Admin Client and Jetty Server startup so that Auto Deployment of EXPath packages can be disabled from the CLI --- .../org/exist/client/CommandlineOptions.java | 12 ++++++++++-- .../org/exist/client/InteractiveClient.java | 7 +++++++ .../main/java/org/exist/jetty/JettyStart.java | 19 +++++++++++++++---- .../java/org/exist/xmldb/DatabaseImpl.java | 11 +++++++++++ 4 files changed, 43 insertions(+), 6 deletions(-) diff --git a/exist-core/src/main/java/org/exist/client/CommandlineOptions.java b/exist-core/src/main/java/org/exist/client/CommandlineOptions.java index 2e89c9dfe7..c7ab58af50 100644 --- a/exist-core/src/main/java/org/exist/client/CommandlineOptions.java +++ b/exist-core/src/main/java/org/exist/client/CommandlineOptions.java @@ -108,6 +108,10 @@ public class CommandlineOptions { .description("do not make embedded mode available") .defaultValue(false) .build(); + private static final Argument noAutoDeployArg = optionArgument("-a", "--no-auto-deploy") + .description("Disable auto-deployment of EXPath Packages") + .defaultValue(false) + .build(); /* gui arguments */ @@ -193,7 +197,7 @@ private static Optional optUri(final ParsedArguments parsedArguments, public static CommandlineOptions parse(final String[] args) throws ArgumentException, URISyntaxException { final ParsedArguments arguments = CommandLineParser - .withArguments(userArg, passwordArg, useSslArg, embeddedArg, embeddedConfigArg, noEmbeddedModeArg) + .withArguments(userArg, passwordArg, useSslArg, embeddedArg, embeddedConfigArg, noEmbeddedModeArg, noAutoDeployArg) .andArguments(noGuiArg, guiQueryDialogArg) .andArguments(mkColArg, rmColArg, setColArg) .andArguments(parseDocsArg, getDocArg, rmDocArg) @@ -215,6 +219,7 @@ public static CommandlineOptions parse(final String[] args) throws ArgumentExcep final boolean embedded = getBool(arguments, embeddedArg); final Optional embeddedConfig = getPathOpt(arguments, embeddedConfigArg); final boolean noEmbeddedMode = getBool(arguments, noEmbeddedModeArg); + final boolean noAutoDeploy = getBool(arguments, noAutoDeployArg); final boolean startGUI = !getBool(arguments, noGuiArg); final boolean openQueryGUI = getBool(arguments, guiQueryDialogArg); @@ -259,6 +264,7 @@ public static CommandlineOptions parse(final String[] args) throws ArgumentExcep embedded, embeddedConfig, noEmbeddedMode, + noAutoDeploy, startGUI, openQueryGUI, mkCol, @@ -278,7 +284,7 @@ public static CommandlineOptions parse(final String[] args) throws ArgumentExcep ); } - public CommandlineOptions(boolean quiet, boolean verbose, Optional outputFile, Map options, Optional username, Optional password, boolean useSSL, boolean embedded, Optional embeddedConfig, boolean noEmbeddedMode, boolean startGUI, boolean openQueryGUI, Optional mkCol, Optional rmCol, Optional setCol, List parseDocs, Optional getDoc, Optional rmDoc, Optional xpath, List queryFiles, Optional howManyResults, Optional traceQueriesFile, Optional setDoc, Optional xupdateFile, boolean reindex, boolean reindexRecurse) { + public CommandlineOptions(boolean quiet, boolean verbose, Optional outputFile, Map options, Optional username, Optional password, boolean useSSL, boolean embedded, Optional embeddedConfig, boolean noEmbeddedMode, boolean noAutoDeploy, boolean startGUI, boolean openQueryGUI, Optional mkCol, Optional rmCol, Optional setCol, List parseDocs, Optional getDoc, Optional rmDoc, Optional xpath, List queryFiles, Optional howManyResults, Optional traceQueriesFile, Optional setDoc, Optional xupdateFile, boolean reindex, boolean reindexRecurse) { this.quiet = quiet; this.verbose = verbose; this.outputFile = outputFile; @@ -289,6 +295,7 @@ public CommandlineOptions(boolean quiet, boolean verbose, Optional outputF this.embedded = embedded; this.embeddedConfig = embeddedConfig; this.noEmbeddedMode = noEmbeddedMode; + this.noAutoDeploy = noAutoDeploy; this.startGUI = startGUI; this.openQueryGUI = openQueryGUI; this.mkCol = mkCol; @@ -318,6 +325,7 @@ public CommandlineOptions(boolean quiet, boolean verbose, Optional outputF final boolean embedded; final Optional embeddedConfig; final boolean noEmbeddedMode; + final boolean noAutoDeploy; final boolean startGUI; final boolean openQueryGUI; diff --git a/exist-core/src/main/java/org/exist/client/InteractiveClient.java b/exist-core/src/main/java/org/exist/client/InteractiveClient.java index fbcfd5de0b..3b301b6855 100644 --- a/exist-core/src/main/java/org/exist/client/InteractiveClient.java +++ b/exist-core/src/main/java/org/exist/client/InteractiveClient.java @@ -161,6 +161,7 @@ public class InteractiveClient { public static final String CREATE_DATABASE = "create-database"; public static final String LOCAL_MODE = "local-mode-opt"; public static final String NO_EMBED_MODE = "NO_EMBED_MODE"; + public static final String NO_AUTO_DEPLOY = "no-autodeploy"; // values protected static final String EDIT_CMD = "emacsclient -t $file"; @@ -169,6 +170,7 @@ public class InteractiveClient { protected static final String SSL_ENABLE_DEFAULT = "FALSE"; protected static final String LOCAL_MODE_DEFAULT = "FALSE"; protected static final String NO_EMBED_MODE_DEFAULT = "FALSE"; + protected static final String NO_AUTO_DEPLOY_DEFAULT = "FALSE"; protected static final String USER_DEFAULT = SecurityManager.DBA_USER; protected static final String DRIVER_IMPL_CLASS = "org.exist.xmldb.DatabaseImpl"; @@ -188,6 +190,7 @@ public class InteractiveClient { DEFAULT_PROPERTIES.setProperty(PERMISSIONS, "false"); DEFAULT_PROPERTIES.setProperty(EXPAND_XINCLUDES, "true"); DEFAULT_PROPERTIES.setProperty(SSL_ENABLE, SSL_ENABLE_DEFAULT); + DEFAULT_PROPERTIES.setProperty(NO_AUTO_DEPLOY, NO_AUTO_DEPLOY_DEFAULT); } protected static final int[] COL_SIZES = new int[]{10, 10, 10, -1}; @@ -334,6 +337,7 @@ protected void connect() throws Exception { // Configure database database.setProperty(CREATE_DATABASE, "true"); database.setProperty(SSL_ENABLE, properties.getProperty(SSL_ENABLE)); + database.setProperty(NO_AUTO_DEPLOY, properties.getProperty(NO_AUTO_DEPLOY)); // secure empty configuration final String configProp = properties.getProperty(InteractiveClient.CONFIGURATION); @@ -1882,6 +1886,9 @@ protected void setPropertiesFromCommandLine(final CommandlineOptions options, fi if (options.noEmbeddedMode) { props.setProperty(NO_EMBED_MODE, "TRUE"); } + if (options.noAutoDeploy) { + props.setProperty(NO_AUTO_DEPLOY, "TRUE"); + } } /** diff --git a/exist-core/src/main/java/org/exist/jetty/JettyStart.java b/exist-core/src/main/java/org/exist/jetty/JettyStart.java index 472b0e94dd..fefaa19664 100644 --- a/exist-core/src/main/java/org/exist/jetty/JettyStart.java +++ b/exist-core/src/main/java/org/exist/jetty/JettyStart.java @@ -74,6 +74,7 @@ import se.softhouse.jargo.Argument; import se.softhouse.jargo.ArgumentException; import se.softhouse.jargo.CommandLineParser; +import se.softhouse.jargo.ParsedArguments; import java.io.IOException; import java.io.LineNumberReader; @@ -85,9 +86,10 @@ import java.util.*; import java.util.stream.Collectors; +import static org.exist.repo.AutoDeploymentTrigger.AUTODEPLOY_PROPERTY; +import static org.exist.util.ArgumentUtil.getBool; import static org.exist.util.ThreadUtils.newGlobalThread; -import static se.softhouse.jargo.Arguments.helpArgument; -import static se.softhouse.jargo.Arguments.stringArgument; +import static se.softhouse.jargo.Arguments.*; /** * This class provides a main method to start Jetty with eXist. It registers shutdown @@ -119,6 +121,10 @@ public class JettyStart extends Observable implements LifeCycle.Listener { private static final Argument existConfigFilePath = stringArgument() .description("Path to Elemental Config File") .build(); + private static final Argument noAutoDeployArg = optionArgument("-a", "--no-auto-deploy") + .description("Disable auto-deployment of EXPath Packages") + .defaultValue(false) + .build(); private static final Argument helpArg = helpArgument("-h", "--help"); @GuardedBy("this") private int status = STATUS_STOPPED; @@ -130,12 +136,17 @@ public static void main(final String[] args) { try { CompatibleJavaVersionCheck.checkForCompatibleJavaVersion(); - CommandLineParser + final ParsedArguments arguments = CommandLineParser .withArguments(jettyConfigFilePath, existConfigFilePath) - .andArguments(helpArg) + .andArguments(noAutoDeployArg, helpArg) .programName("startup" + (OSUtil.IS_WINDOWS ? ".bat" : ".sh")) .parse(args); + final boolean noAutoDeploy = getBool(arguments, noAutoDeployArg); + if (noAutoDeploy) { + System.setProperty(AUTODEPLOY_PROPERTY, "off"); + } + } catch (final StartException e) { if (e.getMessage() != null && !e.getMessage().isEmpty()) { System.err.println(e.getMessage()); diff --git a/exist-core/src/main/java/org/exist/xmldb/DatabaseImpl.java b/exist-core/src/main/java/org/exist/xmldb/DatabaseImpl.java index feaf3d9f32..6b61112f4f 100644 --- a/exist-core/src/main/java/org/exist/xmldb/DatabaseImpl.java +++ b/exist-core/src/main/java/org/exist/xmldb/DatabaseImpl.java @@ -50,6 +50,8 @@ import java.util.Map; import java.util.Optional; +import static org.exist.repo.AutoDeploymentTrigger.AUTODEPLOY_PROPERTY; + /** * The XMLDB driver class for eXist. This driver manages two different * internal implementations. The first communicates with a remote @@ -100,6 +102,8 @@ public class DatabaseImpl implements Database { private Boolean ssl_allow_self_signed = true; private Boolean ssl_verify_hostname = false; + private Boolean no_autodeploy = false; + public DatabaseImpl() { final String initdb = System.getProperty("exist.initdb"); if (initdb != null) { @@ -122,6 +126,10 @@ private void configure(final String instanceName) throws XMLDBException { config.setProperty(Journal.PROPERTY_RECOVERY_JOURNAL_DIR, Paths.get(journalDir)); } + if (no_autodeploy) { + System.setProperty(AUTODEPLOY_PROPERTY, "off"); + } + BrokerPool.configure(instanceName, 1, 5, config); if (shutdown != null) { BrokerPool.getInstance(instanceName).registerShutdownListener(shutdown); @@ -368,6 +376,7 @@ public String getName() throws XMLDBException { public final static String DATA_DIR = "data-dir"; public final static String JOURNAL_DIR = "journal-dir"; public final static String SSL_ENABLE = "ssl-enable"; + public final static String NO_AUTODEPLOY = "no-autodeploy"; public final static String SSL_ALLOW_SELF_SIGNED = "ssl-allow-self-signed"; public final static String SSL_VERIFY_HOSTNAME = "ssl-verify-hostname"; @@ -387,6 +396,7 @@ public String getProperty(final String property, final String defaultValue) thro case SSL_ENABLE -> ssl_enable.toString(); case SSL_ALLOW_SELF_SIGNED -> ssl_allow_self_signed.toString(); case SSL_VERIFY_HOSTNAME -> ssl_verify_hostname.toString(); + case NO_AUTODEPLOY -> no_autodeploy.toString(); default -> defaultValue; }; return value; @@ -403,6 +413,7 @@ public void setProperty(final String property, final String value) throws XMLDBE case SSL_ENABLE -> this.ssl_enable = Boolean.valueOf(value); case SSL_ALLOW_SELF_SIGNED -> this.ssl_allow_self_signed = Boolean.valueOf(value); case SSL_VERIFY_HOSTNAME -> this.ssl_verify_hostname = Boolean.valueOf(value); + case NO_AUTODEPLOY -> this.no_autodeploy = Boolean.valueOf(value); default -> LOG.warn("Ignoring unknown property: {}, value: {} ", property, value); } }