From 0ac5a273496d9ddb0b5deef23642af7111405722 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Wed, 17 Mar 2021 16:31:31 +0000 Subject: [PATCH 01/32] Bump Saxon-HE from 9.9.1-8 to 10.3 Bumps Saxon-HE from 9.9.1-8 to 10.3. Signed-off-by: dependabot-preview[bot] --- elemental-parent/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/elemental-parent/pom.xml b/elemental-parent/pom.xml index 0c3d3e2598..adca5d2467 100644 --- a/elemental-parent/pom.xml +++ b/elemental-parent/pom.xml @@ -105,7 +105,7 @@ 4.0.4 4.0.6 2.0.17 - 9.9.1-8 + 10.3 1C From 4c7d6e24bffea5e192cd725cb790577a16e71e4b Mon Sep 17 00:00:00 2001 From: Adam Retter Date: Fri, 22 May 2020 22:46:18 +0200 Subject: [PATCH 02/32] [bugfix] Avoid startDocument/endDocument being called multiple times on the Content Handler --- exist-core/pom.xml | 4 +++ .../java/org/exist/xslt/EXistDbXMLReader.java | 27 ++++++++++++++++-- .../xslt/StylesheetResolverAndCompiler.java | 28 +++++++++++++++++-- 3 files changed, 54 insertions(+), 5 deletions(-) diff --git a/exist-core/pom.xml b/exist-core/pom.xml index 677e244476..3c7fb33492 100644 --- a/exist-core/pom.xml +++ b/exist-core/pom.xml @@ -1601,7 +1601,9 @@ src/main/java/org/exist/xquery/value/ValueSequence.java src/test/java/org/exist/xquery/value/YearMonthDurationTest.java src/main/java/org/exist/xquery/value/YearMonthDurationValue.java + src/main/java/org/exist/xslt/EXistDbXMLReader.java src/main/java/org/exist/xslt/EXistURIResolver.java + src/main/java/org/exist/xslt/StylesheetResolverAndCompiler.java src/main/java/org/exist/xslt/XsltURIResolverHelper.java src/main/java/org/exist/xupdate/Append.java src/main/java/org/exist/xupdate/Conditional.java @@ -2588,7 +2590,9 @@ src/main/java/org/exist/xquery/value/ValueSequence.java src/test/java/org/exist/xquery/value/YearMonthDurationTest.java src/main/java/org/exist/xquery/value/YearMonthDurationValue.java + src/main/java/org/exist/xslt/EXistDbXMLReader.java src/main/java/org/exist/xslt/EXistURIResolver.java + src/main/java/org/exist/xslt/StylesheetResolverAndCompiler.java src/main/java/org/exist/xslt/XsltURIResolverHelper.java src/main/java/org/exist/xupdate/Append.java src/main/java/org/exist/xupdate/Conditional.java diff --git a/exist-core/src/main/java/org/exist/xslt/EXistDbXMLReader.java b/exist-core/src/main/java/org/exist/xslt/EXistDbXMLReader.java index 70d6a880f9..16d2b14803 100644 --- a/exist-core/src/main/java/org/exist/xslt/EXistDbXMLReader.java +++ b/exist-core/src/main/java/org/exist/xslt/EXistDbXMLReader.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 * @@ -39,6 +63,7 @@ * {@link org.xml.sax.XMLReader} that uses an {@link org.exist.storage.serializers.Serializer} * to send a database document to a content handler. * + * @author Paul Merchant, Jr. */ public class EXistDbXMLReader implements XMLReader, Locator { @@ -94,8 +119,6 @@ public void parse(final InputSource input) { this.contentHandler.setDocumentLocator(this); serializer.setSAXHandlers(this.contentHandler, null); serializer.toSAX(source.getDocument()); - - this.contentHandler.endDocument(); } catch (SAXParseException e) { LOG.error("SaxParseException: {}", e.getMessage(), e); diff --git a/exist-core/src/main/java/org/exist/xslt/StylesheetResolverAndCompiler.java b/exist-core/src/main/java/org/exist/xslt/StylesheetResolverAndCompiler.java index 4b90b81eac..651ca91bb3 100644 --- a/exist-core/src/main/java/org/exist/xslt/StylesheetResolverAndCompiler.java +++ b/exist-core/src/main/java/org/exist/xslt/StylesheetResolverAndCompiler.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 * @@ -53,6 +77,7 @@ /** * {@link javax.xml.transform.Templates} resolver and compiler. * + * @author Adam Retter * @author Dmitriy Shabanov */ @ThreadSafe @@ -146,7 +171,6 @@ private Templates compileTemplates( final TemplatesHandler handler = factory(broker.getBrokerPool(), errorListener).newTemplatesHandler(); handler.setSystemId(stylesheet.getBaseURI()); - handler.startDocument(); final Serializer serializer = broker.borrowSerializer(); try { @@ -156,8 +180,6 @@ private Templates compileTemplates( broker.returnSerializer(serializer); } - handler.endDocument(); - final Templates t = handler.getTemplates(); //check for errors From 7a05ac0dcfa46cca8f6d2f78784ba1c22a9abf52 Mon Sep 17 00:00:00 2001 From: Adam Retter Date: Fri, 22 May 2020 22:47:19 +0200 Subject: [PATCH 03/32] [bugfix] Set the Lexical Handler when parsing Stylesheets --- exist-core/src/main/java/org/exist/xslt/EXistDbXMLReader.java | 3 ++- .../java/org/exist/xslt/StylesheetResolverAndCompiler.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/exist-core/src/main/java/org/exist/xslt/EXistDbXMLReader.java b/exist-core/src/main/java/org/exist/xslt/EXistDbXMLReader.java index 16d2b14803..5b647fba21 100644 --- a/exist-core/src/main/java/org/exist/xslt/EXistDbXMLReader.java +++ b/exist-core/src/main/java/org/exist/xslt/EXistDbXMLReader.java @@ -58,6 +58,7 @@ import org.xml.sax.Locator; import org.xml.sax.SAXParseException; import org.xml.sax.XMLReader; +import org.xml.sax.ext.LexicalHandler; /** * {@link org.xml.sax.XMLReader} that uses an {@link org.exist.storage.serializers.Serializer} @@ -117,7 +118,7 @@ public void parse(final InputSource input) { try { this.source = input; this.contentHandler.setDocumentLocator(this); - serializer.setSAXHandlers(this.contentHandler, null); + serializer.setSAXHandlers(this.contentHandler, this.contentHandler instanceof LexicalHandler ? (LexicalHandler)this.contentHandler : null); serializer.toSAX(source.getDocument()); } catch (SAXParseException e) { diff --git a/exist-core/src/main/java/org/exist/xslt/StylesheetResolverAndCompiler.java b/exist-core/src/main/java/org/exist/xslt/StylesheetResolverAndCompiler.java index 651ca91bb3..be9fe784de 100644 --- a/exist-core/src/main/java/org/exist/xslt/StylesheetResolverAndCompiler.java +++ b/exist-core/src/main/java/org/exist/xslt/StylesheetResolverAndCompiler.java @@ -71,6 +71,7 @@ import org.exist.xmldb.XmldbURI; import org.exist.xquery.Constants; import org.xml.sax.SAXException; +import org.xml.sax.ext.LexicalHandler; import static org.exist.xslt.XsltURIResolverHelper.getXsltURIResolver; @@ -174,7 +175,7 @@ private Templates compileTemplates( final Serializer serializer = broker.borrowSerializer(); try { - serializer.setSAXHandlers(handler, null); + serializer.setSAXHandlers(handler, handler instanceof LexicalHandler ? (LexicalHandler)handler : null); serializer.toSAX(stylesheet); } finally { broker.returnSerializer(serializer); From 45279571031eaefc94bd65297a115632a57fa74e Mon Sep 17 00:00:00 2001 From: Adam Retter Date: Fri, 22 May 2020 22:47:59 +0200 Subject: [PATCH 04/32] [bugfix] Fix test for Saxon 10 --- .../exist/dom/memtree/DocumentImplTest.java | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/exist-core/src/test/java/org/exist/dom/memtree/DocumentImplTest.java b/exist-core/src/test/java/org/exist/dom/memtree/DocumentImplTest.java index 0dcc7b2055..fae0752059 100644 --- a/exist-core/src/test/java/org/exist/dom/memtree/DocumentImplTest.java +++ b/exist-core/src/test/java/org/exist/dom/memtree/DocumentImplTest.java @@ -117,33 +117,25 @@ public void checkNamespaces_saxon() throws IOException, ParserConfigurationExcep final Element elem = doc.getDocumentElement(); final NamedNodeMap attrs = elem.getAttributes(); - assertEquals(3, attrs.getLength()); + assertEquals(2, attrs.getLength()); int index = 0; final Attr attr1 = (Attr)attrs.item(index++); assertEquals(Node.ATTRIBUTE_NODE, attr1.getNodeType()); assertEquals(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, attr1.getNamespaceURI()); - assertEquals(XMLConstants.XMLNS_ATTRIBUTE, attr1.getPrefix()); - assertEquals(XMLConstants.XML_NS_PREFIX, attr1.getLocalName()); - assertEquals(XMLConstants.XMLNS_ATTRIBUTE + ":" + XMLConstants.XML_NS_PREFIX, attr1.getNodeName()); - assertEquals(XMLConstants.XML_NS_URI, attr1.getValue()); + assertEquals(null, attr1.getPrefix()); + assertEquals(XMLConstants.XMLNS_ATTRIBUTE, attr1.getLocalName()); + assertEquals(XMLConstants.XMLNS_ATTRIBUTE, attr1.getNodeName()); + assertEquals("http://exist-db.org/xquery/repo", attr1.getValue()); final Attr attr2 = (Attr)attrs.item(index++); assertEquals(Node.ATTRIBUTE_NODE, attr2.getNodeType()); assertEquals(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, attr2.getNamespaceURI()); - assertEquals(null, attr2.getPrefix()); - assertEquals(XMLConstants.XMLNS_ATTRIBUTE, attr2.getLocalName()); - assertEquals(XMLConstants.XMLNS_ATTRIBUTE, attr2.getNodeName()); + assertEquals(XMLConstants.XMLNS_ATTRIBUTE, attr2.getPrefix()); + assertEquals("repo", attr2.getLocalName()); + assertEquals(XMLConstants.XMLNS_ATTRIBUTE + ":repo", attr2.getNodeName()); assertEquals("http://exist-db.org/xquery/repo", attr2.getValue()); - - final Attr attr3 = (Attr)attrs.item(index++); - assertEquals(Node.ATTRIBUTE_NODE, attr3.getNodeType()); - assertEquals(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, attr3.getNamespaceURI()); - assertEquals(XMLConstants.XMLNS_ATTRIBUTE, attr3.getPrefix()); - assertEquals("repo", attr3.getLocalName()); - assertEquals(XMLConstants.XMLNS_ATTRIBUTE + ":repo", attr3.getNodeName()); - assertEquals("http://exist-db.org/xquery/repo", attr3.getValue()); } @Test From ea9b82265f960d5b02209f428a4a3823577781ef Mon Sep 17 00:00:00 2001 From: Adam Retter Date: Fri, 22 May 2020 23:04:46 +0200 Subject: [PATCH 05/32] [bugfix] Fix the uses of appending charaters to Saxon's FastStringBuffer --- .../java/org/exist/xquery/value/DayTimeDurationValue.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 299df41358..1fe896d10f 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 @@ -151,14 +151,14 @@ public String getStringValue() { //Copied from Saxon 8.6.1 final FastStringBuffer sb = new FastStringBuffer(32); if (canonicalDuration.getSign() < 0) { - sb.append('-'); + sb.cat('-'); } - sb.append('P'); + sb.cat('P'); if (d != 0) { sb.append(d + "D"); } if (d == 0 || h != 0 || m != 0 || s.intValue() != 0) { - sb.append('T'); + sb.cat('T'); } if (h != 0) { sb.append(h + "H"); From b40d85d477476f30928e8eec57c18ea31f895105 Mon Sep 17 00:00:00 2001 From: Adam Retter Date: Wed, 23 Sep 2026 16:55:21 +0200 Subject: [PATCH 06/32] [bugfix] Fix the uses of Location from Saxon's s9api --- .../java/org/exist/xquery/functions/fn/transform/Transform.java | 1 - 1 file changed, 1 deletion(-) diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/transform/Transform.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/transform/Transform.java index 8ae6be6143..e35fd858fa 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/transform/Transform.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/transform/Transform.java @@ -48,7 +48,6 @@ import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import io.lacuna.bifurcan.IEntry; -import net.sf.saxon.expr.parser.Location; import net.sf.saxon.om.StructuredQName; import net.sf.saxon.s9api.*; import net.sf.saxon.serialize.SerializationProperties; From 685bf69e32410eb27629e5d3bbc27a2d8f0ea311 Mon Sep 17 00:00:00 2001 From: Adam Retter Date: Wed, 23 Sep 2026 17:27:44 +0200 Subject: [PATCH 07/32] [feature] Update to Saxon 12.10 --- elemental-parent/pom.xml | 2 +- exist-core/pom.xml | 12 +++- .../exist/storage/serializers/Serializer.java | 13 +++- .../exist/validation/XmlLibraryChecker.java | 2 +- .../xquery/functions/fn/FunAnalyzeString.java | 20 +++--- .../exist/xquery/functions/fn/FunMatches.java | 33 +++++++++- .../exist/xquery/functions/fn/FunReplace.java | 16 +++-- .../fn/transform/SerializationParameters.java | 46 +++++++------- .../functions/fn/transform/Transform.java | 62 +++++++++++++++---- .../xquery/value/DayTimeDurationValue.java | 10 +-- .../org/exist/xquery/value/DoubleValue.java | 7 +-- .../org/exist/xquery/value/FloatValue.java | 4 +- .../xquery3/transform/fnTransform68.xqm | 28 ++++++++- 13 files changed, 185 insertions(+), 70 deletions(-) diff --git a/elemental-parent/pom.xml b/elemental-parent/pom.xml index adca5d2467..a70808a1eb 100644 --- a/elemental-parent/pom.xml +++ b/elemental-parent/pom.xml @@ -105,7 +105,7 @@ 4.0.4 4.0.6 2.0.17 - 10.3 + 12.10 1C diff --git a/exist-core/pom.xml b/exist-core/pom.xml index 3c7fb33492..54946fb090 100644 --- a/exist-core/pom.xml +++ b/exist-core/pom.xml @@ -444,9 +444,9 @@ - org.exist-db - exist-saxon-regex - 9.4.0-9.e1 + xyz.elemental.fork.saxon + saxon-regex-translator + 12.10.e3 @@ -825,6 +825,7 @@ src/test/xquery/xquery3/parse-xml.xqm src/test/xquery/xquery3/serialize.xql src/test/xquery/xquery3/xml-to-json.xql + src/test/xquery/xquery3/transform/fnTransform68.xqm src/main/java/org/exist/BTreeTest.java src/main/java/org/exist/Indexer.java src/test/java/org/exist/Indexer2Test.java @@ -1434,6 +1435,7 @@ src/test/java/org/exist/xquery/functions/fn/FunLangTest.java src/main/java/org/exist/xquery/functions/fn/FunLast.java src/main/java/org/exist/xquery/functions/fn/FunLocalName.java + src/main/java/org/exist/xquery/functions/fn/FunMatches.java src/main/java/org/exist/xquery/functions/fn/FunMax.java src/main/java/org/exist/xquery/functions/fn/FunMin.java src/main/java/org/exist/xquery/functions/fn/FunName.java @@ -1488,6 +1490,7 @@ src/main/java/org/exist/xquery/functions/fn/transform/Convert.java src/main/java/org/exist/xquery/functions/fn/transform/Delivery.java src/main/java/org/exist/xquery/functions/fn/transform/Options.java + src/main/java/org/exist/xquery/functions/fn/transform/SerializationParameters.java src/main/java/org/exist/xquery/functions/fn/transform/Transform.java src/main/java/org/exist/xquery/functions/fn/transform/TreeUtils.java src/test/java/org/exist/xquery/functions/inspect/InspectModuleTest.java @@ -1668,6 +1671,7 @@ src/test/xquery/xquery3/postfix-expr.xqm src/test/xquery/xquery3/serialize.xql src/test/xquery/xquery3/xml-to-json.xql + src/test/xquery/xquery3/transform/fnTransform68.xqm src/main/java/org/exist/BTreeTest.java src/main/java/org/exist/Indexer.java src/test/java/org/exist/Indexer2Test.java @@ -2392,6 +2396,7 @@ src/test/java/org/exist/xquery/functions/fn/FunLangTest.java src/main/java/org/exist/xquery/functions/fn/FunLast.java src/main/java/org/exist/xquery/functions/fn/FunLocalName.java + src/main/java/org/exist/xquery/functions/fn/FunMatches.java src/main/java/org/exist/xquery/functions/fn/FunMax.java src/main/java/org/exist/xquery/functions/fn/FunMin.java src/main/java/org/exist/xquery/functions/fn/FunName.java @@ -2450,6 +2455,7 @@ src/main/java/org/exist/xquery/functions/fn/transform/Delivery.java src/test/java/org/exist/xquery/functions/fn/transform/FunTransformITTest.java src/main/java/org/exist/xquery/functions/fn/transform/Options.java + src/main/java/org/exist/xquery/functions/fn/transform/SerializationParameters.java src/main/java/org/exist/xquery/functions/fn/transform/Transform.java src/main/java/org/exist/xquery/functions/fn/transform/TreeUtils.java src/test/java/org/exist/xquery/functions/inspect/InspectModuleTest.java 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 084c1c9a29..4676e3da17 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 @@ -70,6 +70,7 @@ import com.evolvedbinary.j8fu.lazy.LazyVal; import io.lacuna.bifurcan.IEntry; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import net.sf.saxon.lib.ResourceResolverWrappingURIResolver; import org.apache.commons.io.output.StringBuilderWriter; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -920,9 +921,13 @@ public void setStylesheet(final Document doc, final @Nullable String stylesheet) throw new TransformerConfigurationException("stylesheet not found: " + stylesheetUri); } - //TODO: use xmldbURI + final net.sf.saxon.TransformerFactoryImpl saxonFactory = (net.sf.saxon.TransformerFactoryImpl) factory.get(); + final net.sf.saxon.Configuration saxonConfiguration = saxonFactory.getConfiguration(); + @Nullable net.sf.saxon.lib.ResourceResolver prevResourceResolver = null; if (xsl.getCollection() != null) { - factory.get().setURIResolver(new InternalURIResolver(xsl.getCollection().getURI().toString())); + prevResourceResolver = saxonConfiguration.getResourceResolver(); + final URIResolver internalUriResolver = new InternalURIResolver(xsl.getCollection().getURI().toString()); + saxonConfiguration.setResourceResolver(new ResourceResolverWrappingURIResolver(internalUriResolver)); } // save handlers @@ -941,7 +946,9 @@ public void setStylesheet(final Document doc, final @Nullable String stylesheet) // restore handlers receiver = oldReceiver; - factory.get().setURIResolver(null); + if (prevResourceResolver != null) { + saxonConfiguration.setResourceResolver(prevResourceResolver); + } } LOG.debug("compiling stylesheet took {}", System.currentTimeMillis() - start); if (templates != null) { diff --git a/exist-core/src/main/java/org/exist/validation/XmlLibraryChecker.java b/exist-core/src/main/java/org/exist/validation/XmlLibraryChecker.java index bcedb7bfb3..0ddcdecac5 100644 --- a/exist-core/src/main/java/org/exist/validation/XmlLibraryChecker.java +++ b/exist-core/src/main/java/org/exist/validation/XmlLibraryChecker.java @@ -79,7 +79,7 @@ public class XmlLibraryChecker { * Possible XML Transformers, at least one must be valid */ private static final ClassVersion[] VALID_TRANSFORMERS = { - new ClassVersion("Saxon", "9.9.1", "net.sf.saxon.Version.getProductVersion()") + new ClassVersion("Saxon", "12.10", "net.sf.saxon.Version.getProductVersion()") }; /** 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 f42ba69e65..a89354df9b 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 @@ -62,6 +62,8 @@ import net.sf.saxon.om.Item; import net.sf.saxon.regex.RegexIterator; import net.sf.saxon.regex.RegularExpression; +import net.sf.saxon.str.StringView; +import net.sf.saxon.str.UnicodeString; import org.exist.dom.QName; import org.exist.dom.memtree.MemTreeBuilder; import org.exist.xquery.*; @@ -141,6 +143,8 @@ public class FunAnalyzeString extends BasicFunction { ) }; + private static final UnicodeString EMPTY_UNICODE_STRING = StringView.of(""); + public FunAnalyzeString(final XQueryContext context, final FunctionSignature signature) { super(context, signature); } @@ -152,12 +156,14 @@ public Sequence eval(final Sequence[] args, final Sequence contextSequence) thro final MemTreeBuilder builder = context.getDocumentBuilder(); builder.startDocument(); builder.startElement(new QName("analyze-string-result", FnModule.NAMESPACE_URI), null); - String input = ""; + final UnicodeString input; if (!args[0].isEmpty()) { - input = args[0].itemAt(0).getStringValue(); + input = StringView.of(args[0].itemAt(0).getStringValue()); + } else { + input = EMPTY_UNICODE_STRING; } if (input != null && !input.isEmpty()) { - final String pattern = args[1].itemAt(0).getStringValue(); + final UnicodeString pattern = StringView.of(args[1].itemAt(0).getStringValue()); String flags = ""; if (args.length == 3) { flags = args[2].itemAt(0).getStringValue(); @@ -172,14 +178,14 @@ public Sequence eval(final Sequence[] args, final Sequence contextSequence) thro } } - private void analyzeString(final MemTreeBuilder builder, final String input, String pattern, final String flags) throws XPathException { + private void analyzeString(final MemTreeBuilder builder, final UnicodeString input, final UnicodeString pattern, final String flags) throws XPathException { final Configuration config = context.getBroker().getBrokerPool().getSaxonConfiguration(); final List warnings = new ArrayList<>(1); try { final RegularExpression regularExpression = config.compileRegularExpression(pattern, flags, "XP30", warnings); - if (regularExpression.matches("")) { + if (regularExpression.matches(EMPTY_UNICODE_STRING)) { throw new XPathException(this, ErrorCodes.FORX0003, "regular expression could match empty string"); } @@ -199,7 +205,7 @@ private void analyzeString(final MemTreeBuilder builder, final String input, Str LOG.warn(warning); } } catch (final net.sf.saxon.trans.XPathException e) { - switch (e.getErrorCodeLocalPart()) { + switch (e.getErrorCodeQName().getLocalPart()) { case "FORX0001" -> throw new XPathException(this, ErrorCodes.FORX0001, e.getMessage()); case "FORX0002" -> throw new XPathException(this, ErrorCodes.FORX0002, e.getMessage()); case "FORX0003" -> throw new XPathException(this, ErrorCodes.FORX0003, e.getMessage()); @@ -316,7 +322,7 @@ public void onGroupEnd(final int groupNumber) { private void nonMatch(final MemTreeBuilder builder, final Item item) { builder.startElement(QN_NON_MATCH, null); - builder.characters(item.getStringValueCS()); + builder.characters(item.getStringValue()); builder.endElement(); } } diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunMatches.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunMatches.java index f0e3ef9d35..21cc6dd368 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunMatches.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunMatches.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 * @@ -21,6 +45,8 @@ */ package org.exist.xquery.functions.fn; +import net.sf.saxon.str.StringView; +import net.sf.saxon.str.UnicodeString; import org.exist.EXistException; import org.exist.dom.persistent.DocumentSet; import org.exist.dom.persistent.ExtArrayNodeSet; @@ -57,6 +83,7 @@ *

* Based on the java.util.regex package for regular expression support. * + * @author Adam Retter * @author Wolfgang Meier */ public final class FunMatches extends Function implements Optimizable, IndexUseReporter { @@ -507,12 +534,12 @@ private Sequence evalGeneric(final Sequence contextSequence, final Item contextI final int flags = parseFlags(this, xmlRegexFlags); return BooleanValue.valueOf(match(string, pattern,flags)); } else { - return BooleanValue.valueOf(matchXmlRegex(string, pattern, xmlRegexFlags)); + return BooleanValue.valueOf(matchXmlRegex(StringView.of(string), StringView.of(pattern), xmlRegexFlags)); } } - private boolean matchXmlRegex(final String string, final String pattern, final String flags) throws XPathException { + private boolean matchXmlRegex(final UnicodeString string, final UnicodeString pattern, final String flags) throws XPathException { try { List warnings = new ArrayList<>(1); RegularExpression regex = context.getBroker().getBrokerPool() @@ -526,7 +553,7 @@ private boolean matchXmlRegex(final String string, final String pattern, final S return regex.containsMatch(string); } catch (final net.sf.saxon.trans.XPathException e) { - switch (e.getErrorCodeLocalPart()) { + switch (e.getErrorCodeQName().getLocalPart()) { case "FORX0001" -> throw new XPathException(this, ErrorCodes.FORX0001, "Invalid regular expression: " + e.getMessage()); case "FORX0002" -> throw new XPathException(this, ErrorCodes.FORX0002, "Invalid regular expression: " + e.getMessage()); // no FORX0003 here since fn:matches is allowed to match an empty string diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunReplace.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunReplace.java index a57fd52693..a1a311524c 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunReplace.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunReplace.java @@ -51,6 +51,8 @@ import net.sf.saxon.Configuration; import net.sf.saxon.functions.Replace; import net.sf.saxon.regex.RegularExpression; +import net.sf.saxon.str.StringView; +import net.sf.saxon.str.UnicodeString; import org.exist.dom.QName; import org.exist.xquery.*; import org.exist.xquery.value.FunctionParameterSequenceType; @@ -117,6 +119,8 @@ public class FunReplace extends BasicFunction { ) ); + private static final UnicodeString EMPTY_UNICODE_STRING = StringView.of(""); + public FunReplace(final XQueryContext context, final FunctionSignature signature) { super(context, signature); } @@ -134,9 +138,9 @@ public Sequence eval(final Sequence[] args, final Sequence contextSequence) thro } else { flags = ""; } - final String string = stringArg.getStringValue(); - final String pattern = args[1].itemAt(0).getStringValue(); - final String replace = args[2].itemAt(0).getStringValue(); + final UnicodeString string = StringView.of(stringArg.getStringValue()); + final UnicodeString pattern = StringView.of(args[1].itemAt(0).getStringValue()); + final UnicodeString replace = StringView.of(args[2].itemAt(0).getStringValue()); final Configuration config = context.getBroker().getBrokerPool().getSaxonConfiguration(); @@ -144,7 +148,7 @@ public Sequence eval(final Sequence[] args, final Sequence contextSequence) thro try { final RegularExpression regularExpression = config.compileRegularExpression(pattern, flags, "XP30", warnings); - if (regularExpression.matches("")) { + if (regularExpression.matches(EMPTY_UNICODE_STRING)) { throw new XPathException(this, ErrorCodes.FORX0003, "regular expression could match empty string"); } @@ -156,11 +160,11 @@ public Sequence eval(final Sequence[] args, final Sequence contextSequence) thro throw new XPathException(this, ErrorCodes.FORX0004, msg); } } - final CharSequence res = regularExpression.replace(string, replace); + final UnicodeString res = regularExpression.replace(string, replace); result = new StringValue(this, res.toString()); } catch (final net.sf.saxon.trans.XPathException e) { - switch (e.getErrorCodeLocalPart()) { + switch (e.getErrorCodeQName().getLocalPart()) { case "FORX0001" -> throw new XPathException(this, ErrorCodes.FORX0001, e.getMessage()); case "FORX0002" -> throw new XPathException(this, ErrorCodes.FORX0002, e.getMessage()); case "FORX0003" -> throw new XPathException(this, ErrorCodes.FORX0003, e.getMessage()); diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/transform/SerializationParameters.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/transform/SerializationParameters.java index f1dbc1c4b0..22b9c03f98 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/transform/SerializationParameters.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/transform/SerializationParameters.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 * @@ -19,7 +43,6 @@ * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ - package org.exist.xquery.functions.fn.transform; import com.evolvedbinary.j8fu.tuple.Tuple3; @@ -219,25 +242,6 @@ static SerializationProperties getAsSerializationProperties(final MapType params static final StructuredQName qNameCharacterMap = new StructuredQName("", "http://www.exist-db.org", "fn-transform-charactermap"); - /** - * {@link CharacterMap} doesn't provide a method of naming a combined map - *

- * We need our combined map to have a name to find it in a {@link CharacterMapIndex} - */ - static class NamedCombinedCharacterMap extends CharacterMap { - - private final StructuredQName name; - - public NamedCombinedCharacterMap(final StructuredQName name, final Iterable list) { - super(list); - this.name = name; - } - - @Override public StructuredQName getName() { - return this.name; - } - } - /** * Combine the serialization properties from a compiled stylesheet * with the serialization properties supplied as parameters to fn:transform @@ -283,7 +287,7 @@ static SerializationProperties combinePropertiesAndCharacterMaps( } final CharacterMap combinedMap = combinedProperties.getCharacterMapIndex().getCharacterMap(qNameCharacterMap); allMaps.add(combinedMap); - final CharacterMap repairedCombinedMap = new NamedCombinedCharacterMap(qNameCharacterMap, allMaps); + final CharacterMap repairedCombinedMap = new CharacterMap(allMaps, qNameCharacterMap); combinedProperties.getCharacterMapIndex().putCharacterMap( qNameCharacterMap, repairedCombinedMap); diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/transform/Transform.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/transform/Transform.java index e35fd858fa..3cbedfb2bd 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/transform/Transform.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/transform/Transform.java @@ -56,6 +56,7 @@ import org.apache.logging.log4j.Logger; import org.exist.dom.QName; import org.exist.util.Holder; +import org.exist.util.UUIDGenerator; import org.exist.xquery.ErrorCodes; import org.exist.xquery.XPathException; import org.exist.xquery.XQueryContext; @@ -66,6 +67,7 @@ import org.w3c.dom.Node; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import javax.xml.transform.ErrorListener; import javax.xml.transform.Source; import javax.xml.transform.TransformerException; @@ -132,7 +134,7 @@ public Sequence eval(final Sequence[] args, final Sequence contextSequence) thro final Options options = new Options(context, fnTransform, toSaxon, (MapType) args[0].itemAt(0)); //TODO(AR) Saxon recommends to use a StreamSource or SAXSource instead of DOMSource for performance - final Optional sourceNode = Transform.getSourceNode(options.sourceNode, context.getBaseURI()); + final Optional sourceNode = Transform.getSourceNode(options.sourceNode, context); if (options.xsltVersion.equals(V1_0) || options.xsltVersion.equals(V2_0) || options.xsltVersion.equals(V3_0)) { try { @@ -161,7 +163,9 @@ public Sequence eval(final Sequence[] args, final Sequence contextSequence) thro final Xslt30Transformer xslt30Transformer = xsltExecutable.load30(); - options.initialMode.ifPresent(qNameValue -> xslt30Transformer.setInitialMode(Convert.ToSaxon.of(qNameValue.getQName()))); + if (options.initialMode.isPresent()) { + xslt30Transformer.setInitialMode(Convert.ToSaxon.of(options.initialMode.get().getQName())); + } xslt30Transformer.setInitialTemplateParameters(options.templateParams, false); xslt30Transformer.setInitialTemplateParameters(options.tunnelParams, true); if (options.baseOutputURI.isPresent()) { @@ -192,18 +196,21 @@ public Sequence eval(final Sequence[] args, final Sequence contextSequence) thro final XdmItem xdmItem = (XdmItem) toSaxon.of(item); xslt30Transformer.setGlobalContextItem(xdmItem); } else if (sourceNode.isPresent()) { - final Document document; Source source = sourceNode.get(); final Node node = ((DOMSource)sourceNode.get()).getNode(); if (!(node instanceof org.exist.dom.memtree.DocumentImpl) && !(node instanceof org.exist.dom.persistent.DocumentImpl)) { //The source may not be a document //If it isn't, it should be part of a document, so we build a DOMSource to use - document = node.getOwnerDocument(); - source = new DOMSource(document); + final Document document = node.getOwnerDocument(); + final String baseUri = getBaseUri(node, context); + source = newDomSource(document, baseUri + "#global-context-item"); + } else { + source = newDomSource(node, source.getSystemId() + "#global-context-item"); } final var brokerPool = context.getBroker().getBrokerPool(); final DocumentBuilder sourceBuilder = brokerPool.getSaxonProcessor().newDocumentBuilder(); final XdmNode xdmNode = sourceBuilder.build(source); + xslt30Transformer.setGlobalContextItem(xdmNode); } else { xslt30Transformer.setGlobalContextItem(null); @@ -288,7 +295,7 @@ private XPathException originalXPathException(final String prefix, @Nonnull fina Throwable cause = e; while (cause != null) { if (cause instanceof XPathException) { - return new XPathException(fnTransform, ((XPathException) cause).getErrorCode(), prefix + cause.getMessage()); + return new XPathException(fnTransform, ((XPathException) cause).getErrorCode(), prefix + cause.getMessage(), cause); } cause = cause.getCause(); } @@ -305,15 +312,15 @@ private XPathException originalXPathException(final String prefix, @Nonnull fina } catch (final IllegalArgumentException ee) { errorCode = new ErrorCodes.DynamicErrorCode(errorCodeQName, cause.getMessage()); } - return new XPathException(fnTransform, errorCode, prefix + cause.getMessage()); + return new XPathException(fnTransform, errorCode, prefix + cause.getMessage(), cause); } else { - return new XPathException(fnTransform, defaultErrorCode, prefix + cause.getMessage()); + return new XPathException(fnTransform, defaultErrorCode, prefix + cause.getMessage(), cause); } } cause = cause.getCause(); } - return new XPathException(fnTransform, defaultErrorCode, prefix + e.getMessage()); + return new XPathException(fnTransform, defaultErrorCode, prefix + e.getMessage(), cause); } /** @@ -400,7 +407,9 @@ private MapType invokeApplyTemplates() throws XPathException, SaxonApiException final Sequence initialMatchSelection = options.initialMatchSelection.get(); final Item item = initialMatchSelection.itemAt(0); if (item instanceof Document) { - final Source sourceIMS = new DOMSource((Document)item, context.getBaseURI().getStringValue()); + final Document document = (Document) item; + final String baseUri = getBaseUri(document, context); + final Source sourceIMS = newDomSource(document, baseUri); xslt30Transformer.applyTemplates(sourceIMS, destination); } else { final XdmValue selection = toSaxon.of(initialMatchSelection); @@ -459,8 +468,37 @@ private Sequence postProcess(final AtomicValue key, final Sequence before, final } } - private static Optional getSourceNode(final Optional sourceNode, final AnyURIValue baseURI) { - return sourceNode.map(NodeValue::getNode).map(node -> new DOMSource(node, baseURI.getStringValue())); + private static Optional getSourceNode(final Optional sourceNode, final XQueryContext context) { + return sourceNode.map(NodeValue::getNode).map(node -> newDomSource(node, getBaseUri(node, context))); + } + + private static String getBaseUri(final Node node, final XQueryContext context) { + @Nullable String baseUri = node.getBaseURI(); + if (node instanceof org.exist.dom.persistent.NodeImpl) { + // persistent nodes + if (node.getNodeType() != Node.DOCUMENT_NODE && node.getOwnerDocument().getBaseURI().equals(baseUri)) { + baseUri += "#" + ((org.exist.dom.persistent.NodeImpl) node).getNodeId().toString(); + } + + } else { + // in-memory nodes + if (isNullOrEmpty(baseUri)) { + try { + baseUri = context.getBaseURI().getStringValue(); + } catch (final XPathException e) { + // noop + } + if (isNullOrEmpty(baseUri)) { + baseUri += "#" + UUIDGenerator.getUUIDversion4(); + } + } + } + + return baseUri; + } + + private static DOMSource newDomSource(final Node node, final String baseUri) { + return new DOMSource(node, baseUri); } private static class ErrorListenerLog4jAdapter implements ErrorListener { 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 1fe896d10f..fd7e044186 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 @@ -45,7 +45,7 @@ */ package org.exist.xquery.value; -import net.sf.saxon.tree.util.FastStringBuffer; +import net.sf.saxon.str.UnicodeBuilder; import net.sf.saxon.value.FloatingPointConverter; import org.exist.storage.io.VariableByteArrayOutputStream; import org.exist.storage.io.VariableByteBufferInput; @@ -149,16 +149,16 @@ public String getStringValue() { } //Copied from Saxon 8.6.1 - final FastStringBuffer sb = new FastStringBuffer(32); + final UnicodeBuilder sb = new UnicodeBuilder(32); if (canonicalDuration.getSign() < 0) { - sb.cat('-'); + sb.append('-'); } - sb.cat('P'); + sb.append('P'); if (d != 0) { sb.append(d + "D"); } if (d == 0 || h != 0 || m != 0 || s.intValue() != 0) { - sb.cat('T'); + sb.append('T'); } if (h != 0) { sb.append(h + "H"); diff --git a/exist-core/src/main/java/org/exist/xquery/value/DoubleValue.java b/exist-core/src/main/java/org/exist/xquery/value/DoubleValue.java index 63d26a0f8f..4c9486ca0e 100644 --- a/exist-core/src/main/java/org/exist/xquery/value/DoubleValue.java +++ b/exist-core/src/main/java/org/exist/xquery/value/DoubleValue.java @@ -46,7 +46,7 @@ package org.exist.xquery.value; import com.ibm.icu.text.Collator; -import net.sf.saxon.tree.util.FastStringBuffer; +import net.sf.saxon.str.UnicodeString; import net.sf.saxon.value.FloatingPointConverter; import org.exist.util.ByteConversion; import org.exist.xquery.Constants; @@ -133,10 +133,9 @@ public int getType() { @Override public String getStringValue() { - final FastStringBuffer sb = new FastStringBuffer(20); //0 is a dummy parameter - FloatingPointConverter.appendDouble(sb, value, false); - return sb.toString(); + final UnicodeString str = FloatingPointConverter.convertDouble(value, false); + return str.toString(); } public double getValue() { diff --git a/exist-core/src/main/java/org/exist/xquery/value/FloatValue.java b/exist-core/src/main/java/org/exist/xquery/value/FloatValue.java index fc1ec67300..fc4a3a6cc0 100644 --- a/exist-core/src/main/java/org/exist/xquery/value/FloatValue.java +++ b/exist-core/src/main/java/org/exist/xquery/value/FloatValue.java @@ -46,7 +46,7 @@ package org.exist.xquery.value; import com.ibm.icu.text.Collator; -import net.sf.saxon.tree.util.FastStringBuffer; +import net.sf.saxon.str.UnicodeBuilder; import net.sf.saxon.value.FloatingPointConverter; import org.exist.util.ByteConversion; import org.exist.xquery.Constants; @@ -143,7 +143,7 @@ public String getStringValue() throws XPathException { return s; */ - final FastStringBuffer sb = new FastStringBuffer(20); + final UnicodeBuilder sb = new UnicodeBuilder(20); //0 is a dummy parameter FloatingPointConverter.appendFloat(sb, value, false); return sb.toString(); diff --git a/exist-core/src/test/xquery/xquery3/transform/fnTransform68.xqm b/exist-core/src/test/xquery/xquery3/transform/fnTransform68.xqm index 7686ee2bc4..6ef673d725 100644 --- a/exist-core/src/test/xquery/xquery3/transform/fnTransform68.xqm +++ b/exist-core/src/test/xquery/xquery3/transform/fnTransform68.xqm @@ -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 : @@ -36,7 +60,7 @@ declare variable $testTransform:transform-68-xsl-text := document { }; declare - %test:assertError("FOXT0001") + %test:assertTrue function testTransform:transform-68-supports-dynamic-evaluation() { let $xsl := $testTransform:transform-68-xsl-text let $result := fn:transform(map{ @@ -45,7 +69,7 @@ function testTransform:transform-68-supports-dynamic-evaluation() { "delivery-format" : "serialized", "stylesheet-params": map { QName("","v"): "2" }, "requested-properties" : map{fn:QName('http://www.w3.org/1999/XSL/Transform','supports-dynamic-evaluation'):true()}}) - return contains($result?output,">2") + return $result?output }; declare From 7ecedc08b94a44e97af86b02ecb77dc9db9a4b97 Mon Sep 17 00:00:00 2001 From: marmoure Date: Mon, 7 Sep 2026 10:45:49 +0200 Subject: [PATCH 08/32] [feature] Create the function stub and add to the fn namespace --- .../xquery/functions/fn/FnInvisibleXml.java | 57 +++++++++++++++++++ .../exist/xquery/functions/fn/FnModule.java | 3 +- 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java new file mode 100644 index 0000000000..46c0f2bb1b --- /dev/null +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java @@ -0,0 +1,57 @@ +/* + * 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 + */ +package org.exist.xquery.functions.fn; + +import org.exist.dom.QName; +import org.exist.xquery.BasicFunction; +import org.exist.xquery.Cardinality; +import org.exist.xquery.FunctionSignature; +import org.exist.xquery.XPathException; +import org.exist.xquery.XQueryContext; +import org.exist.xquery.value.FunctionReturnSequenceType; +import org.exist.xquery.value.Sequence; +import org.exist.xquery.value.StringValue; +import org.exist.xquery.value.Type; + +public class FnInvisibleXml extends BasicFunction { + + private static final String FS_INVISIBLE_XML_NAME = "invisible-xml"; + + /** + * Note for the arguments one of them takses 2 types and I need to create a super type use transform.java as example + */ + + public final static FunctionSignature FNS_INVISIBLE_XML = new FunctionSignature( + new QName(FS_INVISIBLE_XML_NAME, FnModule.NAMESPACE_URI), + "Evaluates invisible XML.", + null, + new FunctionReturnSequenceType(Type.STRING, Cardinality.EXACTLY_ONE, "The evaluated string.") + ); + + public FnInvisibleXml(final XQueryContext context, final FunctionSignature signature) { + super(context, signature); + } + + @Override + public Sequence eval(final Sequence[] args, final Sequence contextSequence) throws XPathException { + return new StringValue("TODO TODO TODO, maybe next"); + } +} \ No newline at end of file diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FnModule.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FnModule.java index e801e9ce75..797309895f 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FnModule.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FnModule.java @@ -299,7 +299,8 @@ public class FnModule extends AbstractInternalModule { new FunctionDef(FnRandomNumberGenerator.FS_RANDOM_NUMBER_GENERATOR[0], FnRandomNumberGenerator.class), new FunctionDef(FnRandomNumberGenerator.FS_RANDOM_NUMBER_GENERATOR[1], FnRandomNumberGenerator.class), new FunctionDef(FunContainsToken.FS_CONTAINS_TOKEN[0], FunContainsToken.class), - new FunctionDef(FunContainsToken.FS_CONTAINS_TOKEN[1], FunContainsToken.class) + new FunctionDef(FunContainsToken.FS_CONTAINS_TOKEN[1], FunContainsToken.class), + new FunctionDef(FnInvisibleXml.FNS_INVISIBLE_XML, FnInvisibleXml.class) }; static { From 8568e4d3b462895bb687e6d079a16b1fa142695c Mon Sep 17 00:00:00 2001 From: marmoure Date: Tue, 8 Sep 2026 15:25:14 +0200 Subject: [PATCH 09/32] [feature] Add error codes. --- .../java/org/exist/xquery/ErrorCodes.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/exist-core/src/main/java/org/exist/xquery/ErrorCodes.java b/exist-core/src/main/java/org/exist/xquery/ErrorCodes.java index 6183cc35b2..067cbd0ec1 100644 --- a/exist-core/src/main/java/org/exist/xquery/ErrorCodes.java +++ b/exist-core/src/main/java/org/exist/xquery/ErrorCodes.java @@ -328,6 +328,9 @@ public enum W3CErrorCode implements IErrorCode { FOXT0003 ("XSLT transformation failed"), FOXT0004 ("XSLT transformation has been disabled"), FOXT0006 ("XSLT output contains non-accepted characters"), + FOIX0001 ("Invalid Invisible XML grammar."), + FOIX0002 ("Input provided could not be parsed successfully."), + FOIX0003 ("No Invisible XML processor is available."), XTSE0165 ("It is a static error if the processor is not able to retrieve the resource identified by the URI reference [ in the href attribute of xsl:include or xsl:import] , or if the resource that is retrieved does not contain a stylesheet module conforming to this specification."); private final ErrorCode errorCode; @@ -1537,6 +1540,24 @@ public DynamicErrorCode(final QName qname, @Nullable final String description) { @Deprecated public static final ErrorCode FOXT0006 = W3CErrorCode.FOXT0006.errorCode; + /** + * @deprecated Use {@link W3CErrorCode#FOIX0001}. + */ + @Deprecated + public static final ErrorCode FOIX0001 = W3CErrorCode.FOIX0001.errorCode; + + /** + * @deprecated Use {@link W3CErrorCode#FOIX0002}. + */ + @Deprecated + public static final ErrorCode FOIX0002 = W3CErrorCode.FOIX0002.errorCode; + + /** + * @deprecated Use {@link W3CErrorCode#FOIX0003}. + */ + @Deprecated + public static final ErrorCode FOIX0003 = W3CErrorCode.FOIX0003.errorCode; + /** * @deprecated Use {@link W3CErrorCode#XTSE0165}. */ From 365ab1f2249c907fd9112cc6459495be19619a38 Mon Sep 17 00:00:00 2001 From: marmoure Date: Wed, 9 Sep 2026 18:43:51 +0200 Subject: [PATCH 10/32] [bugfix] Typo --- .../src/main/java/org/exist/xquery/functions/fn/FnModule.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FnModule.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FnModule.java index 797309895f..16367be1c4 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FnModule.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FnModule.java @@ -300,7 +300,7 @@ public class FnModule extends AbstractInternalModule { new FunctionDef(FnRandomNumberGenerator.FS_RANDOM_NUMBER_GENERATOR[1], FnRandomNumberGenerator.class), new FunctionDef(FunContainsToken.FS_CONTAINS_TOKEN[0], FunContainsToken.class), new FunctionDef(FunContainsToken.FS_CONTAINS_TOKEN[1], FunContainsToken.class), - new FunctionDef(FnInvisibleXml.FNS_INVISIBLE_XML, FnInvisibleXml.class) + new FunctionDef(FnInvisibleXml.FS_INVISIBLE_XML, FnInvisibleXml.class) }; static { From 07e463927e7cd55d9d080d2974afbe3b15e3a9df Mon Sep 17 00:00:00 2001 From: marmoure Date: Wed, 9 Sep 2026 18:44:15 +0200 Subject: [PATCH 11/32] [feature] Use markup blitz for the IXML grammar parsing --- exist-core/pom.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/exist-core/pom.xml b/exist-core/pom.xml index 54946fb090..80e2a50099 100644 --- a/exist-core/pom.xml +++ b/exist-core/pom.xml @@ -584,6 +584,12 @@
+ + de.bottlecaps + markup-blitz + 1.12 + + xyz.elemental.fork.org.exist-db From 84fbbfe6fc56a78f30c90e43930334fe9d4f4b24 Mon Sep 17 00:00:00 2001 From: marmoure Date: Wed, 9 Sep 2026 18:45:24 +0200 Subject: [PATCH 12/32] [feature] First attempt at creating a User defined function and returning it. --- .../xquery/functions/fn/FnInvisibleXml.java | 52 ++++++++++++------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java index 46c0f2bb1b..5cda61ff49 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java @@ -20,31 +20,22 @@ */ package org.exist.xquery.functions.fn; +import static org.exist.xquery.functions.fn.FnModule.functionSignature; +import static org.exist.xquery.FunctionDSL.returns; + import org.exist.dom.QName; -import org.exist.xquery.BasicFunction; -import org.exist.xquery.Cardinality; -import org.exist.xquery.FunctionSignature; -import org.exist.xquery.XPathException; -import org.exist.xquery.XQueryContext; -import org.exist.xquery.value.FunctionReturnSequenceType; -import org.exist.xquery.value.Sequence; -import org.exist.xquery.value.StringValue; -import org.exist.xquery.value.Type; +import org.exist.xquery.*; +import org.exist.xquery.value.*; public class FnInvisibleXml extends BasicFunction { private static final String FS_INVISIBLE_XML_NAME = "invisible-xml"; - /** - * Note for the arguments one of them takses 2 types and I need to create a super type use transform.java as example - */ - - public final static FunctionSignature FNS_INVISIBLE_XML = new FunctionSignature( + public final static FunctionSignature FS_INVISIBLE_XML = new FunctionSignature( new QName(FS_INVISIBLE_XML_NAME, FnModule.NAMESPACE_URI), "Evaluates invisible XML.", null, - new FunctionReturnSequenceType(Type.STRING, Cardinality.EXACTLY_ONE, "The evaluated string.") - ); + new FunctionReturnSequenceType(Type.FUNCTION, Cardinality.EXACTLY_ONE, "The parser function.")); public FnInvisibleXml(final XQueryContext context, final FunctionSignature signature) { super(context, signature); @@ -52,6 +43,31 @@ public FnInvisibleXml(final XQueryContext context, final FunctionSignature signa @Override public Sequence eval(final Sequence[] args, final Sequence contextSequence) throws XPathException { - return new StringValue("TODO TODO TODO, maybe next"); + final IxmlParserFunction fn = new IxmlParserFunction(context); // QUESTIONS : where context?? + final FunctionCall call = new FunctionCall(context, fn); + return new FunctionReference(call); + } + + private static class IxmlParserFunction extends UserDefinedFunction { + + IxmlParserFunction(final XQueryContext context) { + super(context, functionSignature( + "invisible-xml", + "Gets the next random number generator.", + returns(Type.STRING, "just a random string for now"))); + } + + @Override + public Sequence eval(final Sequence contextSequence, final Item contextItem) throws XPathException { + return new StringValue("inner function return value"); + } + + @Override + public void accept(final ExpressionVisitor visitor) { + if (visited) { + return; + } + visited = true; + } } -} \ No newline at end of file +} From 495f4b3b5ed0901b0cd9cafb39acb8bf0b396913 Mon Sep 17 00:00:00 2001 From: marmoure Date: Wed, 9 Sep 2026 18:55:31 +0200 Subject: [PATCH 13/32] [feature] The inner function can take input now, and it retuns it as it is. --- .../java/org/exist/xquery/functions/fn/FnInvisibleXml.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java index 5cda61ff49..7e4a215f20 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java @@ -21,6 +21,7 @@ package org.exist.xquery.functions.fn; import static org.exist.xquery.functions.fn.FnModule.functionSignature; +import static org.exist.xquery.FunctionDSL.param; import static org.exist.xquery.FunctionDSL.returns; import org.exist.dom.QName; @@ -54,12 +55,14 @@ private static class IxmlParserFunction extends UserDefinedFunction { super(context, functionSignature( "invisible-xml", "Gets the next random number generator.", - returns(Type.STRING, "just a random string for now"))); + returns(Type.STRING, "just a random string for now"), + param("Parser inpuit", Type.STRING, "param description"))); } @Override public Sequence eval(final Sequence contextSequence, final Item contextItem) throws XPathException { - return new StringValue("inner function return value"); + String parserInput = getCurrentArguments()[0].itemAt(0).getStringValue(); + return new StringValue(parserInput); } @Override From 12634f13f194c691d4c48354a96ee5b253e85a54 Mon Sep 17 00:00:00 2001 From: marmoure Date: Wed, 9 Sep 2026 19:13:54 +0200 Subject: [PATCH 14/32] [feature] Use the default IXML parser to parse input and return it as string --- .../org/exist/xquery/functions/fn/FnInvisibleXml.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java index 7e4a215f20..791695fda7 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java @@ -28,6 +28,8 @@ import org.exist.xquery.*; import org.exist.xquery.value.*; +import de.bottlecaps.markup.Blitz; + public class FnInvisibleXml extends BasicFunction { private static final String FS_INVISIBLE_XML_NAME = "invisible-xml"; @@ -62,7 +64,12 @@ private static class IxmlParserFunction extends UserDefinedFunction { @Override public Sequence eval(final Sequence contextSequence, final Item contextItem) throws XPathException { String parserInput = getCurrentArguments()[0].itemAt(0).getStringValue(); - return new StringValue(parserInput); + // generate the default ixml grammar + String ixmlGrammar = Blitz.ixmlGrammar(); + // parse the input using the ixml grammar + String generatedXML = Blitz.generate(ixmlGrammar).parse(parserInput); + + return new StringValue(generatedXML); } @Override From ccf9c86f2800af393b4ebdc0b5ee8332124bc790 Mon Sep 17 00:00:00 2001 From: marmoure Date: Thu, 10 Sep 2026 12:42:58 +0200 Subject: [PATCH 15/32] [feature] Build a document from the Blitz grammar response. --- .../xquery/functions/fn/FnInvisibleXml.java | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java index 791695fda7..5323b2f9cd 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java @@ -18,18 +18,31 @@ * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ + package org.exist.xquery.functions.fn; import static org.exist.xquery.functions.fn.FnModule.functionSignature; import static org.exist.xquery.FunctionDSL.param; import static org.exist.xquery.FunctionDSL.returns; +import org.exist.Namespaces; import org.exist.dom.QName; +import org.exist.dom.memtree.DocumentImpl; +import org.exist.dom.memtree.MemTreeBuilder; +import org.exist.dom.memtree.NodeImpl; +import org.exist.dom.memtree.SAXAdapter; +import org.exist.util.XMLReaderPool; +import org.exist.validation.ValidationReport; import org.exist.xquery.*; +import org.exist.xquery.functions.validation.Shared; import org.exist.xquery.value.*; +import org.xml.sax.*; import de.bottlecaps.markup.Blitz; +import java.io.IOException; +import java.io.StringReader; + public class FnInvisibleXml extends BasicFunction { private static final String FS_INVISIBLE_XML_NAME = "invisible-xml"; @@ -57,7 +70,7 @@ private static class IxmlParserFunction extends UserDefinedFunction { super(context, functionSignature( "invisible-xml", "Gets the next random number generator.", - returns(Type.STRING, "just a random string for now"), + returns(Type.DOCUMENT, "just a random string for now"), param("Parser inpuit", Type.STRING, "param description"))); } @@ -69,7 +82,25 @@ public Sequence eval(final Sequence contextSequence, final Item contextItem) thr // parse the input using the ixml grammar String generatedXML = Blitz.generate(ixmlGrammar).parse(parserInput); - return new StringValue(generatedXML); + return parse(generatedXML, null); + } + + private Sequence parse(final String xmlContent, final Sequence[] args) throws XPathException { + DocumentImpl document = null; + final SAXAdapter adapter = new SAXAdapter(context); + try { + XMLReaderPool pool = context.getBroker().getBrokerPool().getParserPool(); + XMLReader reader = pool.borrowXMLReader(); + reader.setContentHandler(adapter); + reader.setProperty(Namespaces.SAX_LEXICAL_HANDLER, adapter); + reader.parse(new InputSource(new StringReader(xmlContent))); + document = adapter.getDocument(); + } catch (Exception e) { + + } finally { + return document; + } + } @Override From 96237fac958aa9e6ea94ab1a54fd9e66c59d09b7 Mon Sep 17 00:00:00 2001 From: Adam Retter Date: Thu, 10 Sep 2026 13:36:46 +0200 Subject: [PATCH 16/32] Review and discussion --- .../xquery/functions/fn/FnInvisibleXml.java | 131 ++++++++++++------ 1 file changed, 90 insertions(+), 41 deletions(-) diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java index 5323b2f9cd..cceb2c91de 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java @@ -21,25 +21,26 @@ package org.exist.xquery.functions.fn; +import static com.evolvedbinary.j8fu.Either.Left; +import static com.evolvedbinary.j8fu.Either.Right; +import static org.exist.xquery.FunctionDSL.optParam; import static org.exist.xquery.functions.fn.FnModule.functionSignature; import static org.exist.xquery.FunctionDSL.param; import static org.exist.xquery.FunctionDSL.returns; +import com.evolvedbinary.j8fu.Either; import org.exist.Namespaces; -import org.exist.dom.QName; -import org.exist.dom.memtree.DocumentImpl; -import org.exist.dom.memtree.MemTreeBuilder; -import org.exist.dom.memtree.NodeImpl; import org.exist.dom.memtree.SAXAdapter; import org.exist.util.XMLReaderPool; -import org.exist.validation.ValidationReport; import org.exist.xquery.*; -import org.exist.xquery.functions.validation.Shared; +import org.exist.xquery.functions.map.MapType; import org.exist.xquery.value.*; +import org.w3c.dom.Element; import org.xml.sax.*; import de.bottlecaps.markup.Blitz; +import javax.annotation.Nullable; import java.io.IOException; import java.io.StringReader; @@ -47,11 +48,13 @@ public class FnInvisibleXml extends BasicFunction { private static final String FS_INVISIBLE_XML_NAME = "invisible-xml"; - public final static FunctionSignature FS_INVISIBLE_XML = new FunctionSignature( - new QName(FS_INVISIBLE_XML_NAME, FnModule.NAMESPACE_URI), + final static FunctionSignature FS_INVISIBLE_XML = functionSignature( + FS_INVISIBLE_XML_NAME, "Evaluates invisible XML.", - null, - new FunctionReturnSequenceType(Type.FUNCTION, Cardinality.EXACTLY_ONE, "The parser function.")); + returns(Type.FUNCTION, "The iXML parsing function"), + optParam("grammar", Type.ITEM, "The iXML grammar"), + optParam("options", Type.MAP_ITEM, "Options for the iXML parser") + ); public FnInvisibleXml(final XQueryContext context, final FunctionSignature signature) { super(context, signature); @@ -59,56 +62,102 @@ public FnInvisibleXml(final XQueryContext context, final FunctionSignature signa @Override public Sequence eval(final Sequence[] args, final Sequence contextSequence) throws XPathException { - final IxmlParserFunction fn = new IxmlParserFunction(context); // QUESTIONS : where context?? - final FunctionCall call = new FunctionCall(context, fn); - return new FunctionReference(call); + final Sequence optionsArg = args[1]; + @Nullable final MapType options; + if (optionsArg.isEmpty()) { + options = null; + } else { + options = optionsArg.itemAt(0).toJavaObject(MapType.class); + } + + final IxmlParserFunction fn; + final Sequence grammarArg = args[0]; + if (grammarArg.isEmpty()) { + // no grammar provided + fn = new IxmlParserFunction(context, (StringValue) null, options); + + } else if (grammarArg.getItemType() == Type.STRING) { + // grammar is a string + final StringValue grammarString = grammarArg.itemAt(0).toJavaObject(StringValue.class); + fn = new IxmlParserFunction(context, grammarString, options); + + } else { + // grammar is an element + final Element grammarElement = grammarArg.itemAt(0).toJavaObject(Element.class); + fn = new IxmlParserFunction(context, grammarElement, options); + } + + final InlineFunction functionResult = new InlineFunction(context, fn); + return functionResult.eval(contextSequence, null); } private static class IxmlParserFunction extends UserDefinedFunction { - IxmlParserFunction(final XQueryContext context) { - super(context, functionSignature( - "invisible-xml", - "Gets the next random number generator.", - returns(Type.DOCUMENT, "just a random string for now"), - param("Parser inpuit", Type.STRING, "param description"))); + private static final String FS_PARSE_INVISIBLE_XML_NAME = "parse-invisible-xml"; + private static final FunctionSignature FS_PARSE_INVISIBLE_XML = functionSignature( + FS_PARSE_INVISIBLE_XML_NAME, + "Gets the next random number generator.", + returns(Type.DOCUMENT, "just a random string for now"), + param("Parser inpuit", Type.STRING, "param description") + ); + + @Nullable final Either grammar; + @Nullable final MapType options; + + IxmlParserFunction(final XQueryContext context, @Nullable final StringValue grammar, @Nullable final MapType options) { + super(context, FS_PARSE_INVISIBLE_XML); + this.grammar = Left(grammar); + this.options = options; + } + + IxmlParserFunction(final XQueryContext context, @Nullable final Element grammar, @Nullable final MapType options) { + super(context, FS_PARSE_INVISIBLE_XML); + this.grammar = Right(grammar); + this.options = options; } @Override public Sequence eval(final Sequence contextSequence, final Item contextItem) throws XPathException { - String parserInput = getCurrentArguments()[0].itemAt(0).getStringValue(); + + // get the input + final Sequence inputArg = getArguments(contextSequence, contextItem)[0]; + final String input = inputArg.itemAt(0).getStringValue(); + // generate the default ixml grammar - String ixmlGrammar = Blitz.ixmlGrammar(); + final String ixmlGrammar; + if (grammar == null) { + ixmlGrammar = Blitz.ixmlGrammar(); + } else if (grammar.isLeft()) { + ixmlGrammar = grammar.left().get().getStringValue(); + } else { + // TODO(YB) use XQuerySerializer class to serialize Element to String + ixmlGrammar = ... + } + + // TODO(YB) set any options + // parse the input using the ixml grammar - String generatedXML = Blitz.generate(ixmlGrammar).parse(parserInput); + final String generatedXML = Blitz.generate(ixmlGrammar).parse(input); - return parse(generatedXML, null); + return parse(generatedXML); } - private Sequence parse(final String xmlContent, final Sequence[] args) throws XPathException { - DocumentImpl document = null; + private Sequence parse(final String xmlContent) throws XPathException { + final XMLReaderPool pool = context.getBroker().getBrokerPool().getParserPool(); final SAXAdapter adapter = new SAXAdapter(context); - try { - XMLReaderPool pool = context.getBroker().getBrokerPool().getParserPool(); - XMLReader reader = pool.borrowXMLReader(); + final XMLReader reader = pool.borrowXMLReader(); + try (final StringReader stringReader = new StringReader(xmlContent)) { reader.setContentHandler(adapter); reader.setProperty(Namespaces.SAX_LEXICAL_HANDLER, adapter); - reader.parse(new InputSource(new StringReader(xmlContent))); - document = adapter.getDocument(); - } catch (Exception e) { - + reader.parse(new InputSource(stringReader)); + return adapter.getDocument(); + } catch (final SAXException | IOException e) { + // TODO(YB) add error code + throw new XPathException(this, e.getMessage(), e); } finally { - return document; + pool.returnXMLReader(reader); } } - - @Override - public void accept(final ExpressionVisitor visitor) { - if (visited) { - return; - } - visited = true; - } } } From 999157122e95903bb3d099d16a5427fe3187a183 Mon Sep 17 00:00:00 2001 From: marmoure Date: Fri, 11 Sep 2026 12:15:21 +0200 Subject: [PATCH 17/32] [WIP] Trying to resolve args and get function call running --- .../xquery/functions/fn/FnInvisibleXml.java | 58 +++++++++++++------ 1 file changed, 40 insertions(+), 18 deletions(-) diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java index cceb2c91de..2399d6566b 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java @@ -29,11 +29,13 @@ import static org.exist.xquery.FunctionDSL.returns; import com.evolvedbinary.j8fu.Either; +import com.github.krukow.clj_lang.Obj; import org.exist.Namespaces; +import org.apache.commons.io.output.StringBuilderWriter; import org.exist.dom.memtree.SAXAdapter; import org.exist.util.XMLReaderPool; +import org.exist.util.serializer.XQuerySerializer; import org.exist.xquery.*; -import org.exist.xquery.functions.map.MapType; import org.exist.xquery.value.*; import org.w3c.dom.Element; import org.xml.sax.*; @@ -43,6 +45,8 @@ import javax.annotation.Nullable; import java.io.IOException; import java.io.StringReader; +import java.util.Map; +import java.util.Properties; public class FnInvisibleXml extends BasicFunction { @@ -53,8 +57,7 @@ public class FnInvisibleXml extends BasicFunction { "Evaluates invisible XML.", returns(Type.FUNCTION, "The iXML parsing function"), optParam("grammar", Type.ITEM, "The iXML grammar"), - optParam("options", Type.MAP_ITEM, "Options for the iXML parser") - ); + optParam("options", Type.MAP_ITEM, "Options for the iXML parser")); public FnInvisibleXml(final XQueryContext context, final FunctionSignature signature) { super(context, signature); @@ -63,11 +66,12 @@ public FnInvisibleXml(final XQueryContext context, final FunctionSignature signa @Override public Sequence eval(final Sequence[] args, final Sequence contextSequence) throws XPathException { final Sequence optionsArg = args[1]; - @Nullable final MapType options; + @Nullable + final Map options; if (optionsArg.isEmpty()) { options = null; } else { - options = optionsArg.itemAt(0).toJavaObject(MapType.class); + options = (Map) optionsArg.itemAt(0).toJavaObject(Map.class); } final IxmlParserFunction fn; @@ -83,12 +87,12 @@ public Sequence eval(final Sequence[] args, final Sequence contextSequence) thro } else { // grammar is an element - final Element grammarElement = grammarArg.itemAt(0).toJavaObject(Element.class); - fn = new IxmlParserFunction(context, grammarElement, options); + final Element grammarItem = grammarArg.itemAt(0).toJavaObject(Element.class); + fn = new IxmlParserFunction(context, grammarItem, options); } - final InlineFunction functionResult = new InlineFunction(context, fn); - return functionResult.eval(contextSequence, null); + final FunctionCall invisibleXmlFunctionCall = new FunctionCall(context, fn); + return new FunctionReference(invisibleXmlFunctionCall); } private static class IxmlParserFunction extends UserDefinedFunction { @@ -98,19 +102,22 @@ private static class IxmlParserFunction extends UserDefinedFunction { FS_PARSE_INVISIBLE_XML_NAME, "Gets the next random number generator.", returns(Type.DOCUMENT, "just a random string for now"), - param("Parser inpuit", Type.STRING, "param description") - ); + param("Parser inpuit", Type.STRING, "param description")); - @Nullable final Either grammar; - @Nullable final MapType options; + @Nullable + final Either grammar; + @Nullable + final Map options; - IxmlParserFunction(final XQueryContext context, @Nullable final StringValue grammar, @Nullable final MapType options) { + IxmlParserFunction(final XQueryContext context, @Nullable final StringValue grammar, + @Nullable final Map options) { super(context, FS_PARSE_INVISIBLE_XML); this.grammar = Left(grammar); this.options = options; } - IxmlParserFunction(final XQueryContext context, @Nullable final Element grammar, @Nullable final MapType options) { + IxmlParserFunction(final XQueryContext context, @Nullable final Element grammar, + @Nullable final Map options) { super(context, FS_PARSE_INVISIBLE_XML); this.grammar = Right(grammar); this.options = options; @@ -120,7 +127,7 @@ private static class IxmlParserFunction extends UserDefinedFunction { public Sequence eval(final Sequence contextSequence, final Item contextItem) throws XPathException { // get the input - final Sequence inputArg = getArguments(contextSequence, contextItem)[0]; + final Sequence inputArg = getCurrentArguments()[0]; final String input = inputArg.itemAt(0).getStringValue(); // generate the default ixml grammar @@ -130,8 +137,15 @@ public Sequence eval(final Sequence contextSequence, final Item contextItem) thr } else if (grammar.isLeft()) { ixmlGrammar = grammar.left().get().getStringValue(); } else { - // TODO(YB) use XQuerySerializer class to serialize Element to String - ixmlGrammar = ... + // grammar is an element: serialize it to a String + try (final StringBuilderWriter writer = new StringBuilderWriter()) { + final XQuerySerializer xqSerializer = new XQuerySerializer( + context.getBroker(), new Properties(), writer); + xqSerializer.serialize((Sequence) grammar.right().get()); + ixmlGrammar = writer.toString(); + } catch (final SAXException e) { + throw new XPathException(this, e.getMessage(), e); + } } // TODO(YB) set any options @@ -142,6 +156,14 @@ public Sequence eval(final Sequence contextSequence, final Item contextItem) thr return parse(generatedXML); } + @Override + public void accept(final ExpressionVisitor visitor) { + if (visited) { + return; + } + visited = true; + } + private Sequence parse(final String xmlContent) throws XPathException { final XMLReaderPool pool = context.getBroker().getBrokerPool().getParserPool(); final SAXAdapter adapter = new SAXAdapter(context); From 1f8d6b6ebcbb4e8bf36cf1a69a5700643ad0ee41 Mon Sep 17 00:00:00 2001 From: marmoure Date: Mon, 14 Sep 2026 17:08:59 +0200 Subject: [PATCH 18/32] [wip] The 3 paths are working --- .../xquery/functions/fn/FnInvisibleXml.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java index 2399d6566b..347e46e32e 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java @@ -29,7 +29,6 @@ import static org.exist.xquery.FunctionDSL.returns; import com.evolvedbinary.j8fu.Either; -import com.github.krukow.clj_lang.Obj; import org.exist.Namespaces; import org.apache.commons.io.output.StringBuilderWriter; import org.exist.dom.memtree.SAXAdapter; @@ -128,14 +127,21 @@ public Sequence eval(final Sequence contextSequence, final Item contextItem) thr // get the input final Sequence inputArg = getCurrentArguments()[0]; - final String input = inputArg.itemAt(0).getStringValue(); + final String input = inputArg.getStringValue(); // generate the default ixml grammar final String ixmlGrammar; + // the null check here is wrong if (grammar == null) { - ixmlGrammar = Blitz.ixmlGrammar(); - } else if (grammar.isLeft()) { - ixmlGrammar = grammar.left().get().getStringValue(); + // something went horabily wrong + throw new XPathException("idk how it can be null"); + } + if (grammar.isLeft()) { + if (grammar.left().get() == null) { + ixmlGrammar = Blitz.ixmlGrammar(); + } else { + ixmlGrammar = grammar.left().get().getStringValue(); + } } else { // grammar is an element: serialize it to a String try (final StringBuilderWriter writer = new StringBuilderWriter()) { From 6fcd393a27360f4fc0a793a5a0fd9192c7f1db99 Mon Sep 17 00:00:00 2001 From: marmoure Date: Mon, 14 Sep 2026 18:12:21 +0200 Subject: [PATCH 19/32] [feature] Set the fail on error option --- .../exist/xquery/functions/fn/FnInvisibleXml.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java index 347e46e32e..bb3083d3b2 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java @@ -154,10 +154,18 @@ public Sequence eval(final Sequence contextSequence, final Item contextItem) thr } } - // TODO(YB) set any options + boolean failOnError = false; + if (options != null) { + failOnError = Boolean.TRUE.equals(options.get("fail-on-error")); + } // parse the input using the ixml grammar - final String generatedXML = Blitz.generate(ixmlGrammar).parse(input); + final String generatedXML; + if (failOnError) { + generatedXML = Blitz.generate(ixmlGrammar).parse(input, Blitz.Option.FAIL_ON_ERROR); + } else { + generatedXML = Blitz.generate(ixmlGrammar).parse(input); + } return parse(generatedXML); } @@ -180,7 +188,6 @@ private Sequence parse(final String xmlContent) throws XPathException { reader.parse(new InputSource(stringReader)); return adapter.getDocument(); } catch (final SAXException | IOException e) { - // TODO(YB) add error code throw new XPathException(this, e.getMessage(), e); } finally { pool.returnXMLReader(reader); From 60697399915b75b35f15d0b1885eb0f1d2ada8e8 Mon Sep 17 00:00:00 2001 From: marmoure Date: Mon, 14 Sep 2026 18:19:27 +0200 Subject: [PATCH 20/32] [feature] Use MapType from the xquery functions instead --- .../xquery/functions/fn/FnInvisibleXml.java | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java index bb3083d3b2..3566557327 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java @@ -35,6 +35,7 @@ import org.exist.util.XMLReaderPool; import org.exist.util.serializer.XQuerySerializer; import org.exist.xquery.*; +import org.exist.xquery.functions.map.MapType; import org.exist.xquery.value.*; import org.w3c.dom.Element; import org.xml.sax.*; @@ -44,7 +45,6 @@ import javax.annotation.Nullable; import java.io.IOException; import java.io.StringReader; -import java.util.Map; import java.util.Properties; public class FnInvisibleXml extends BasicFunction { @@ -65,13 +65,9 @@ public FnInvisibleXml(final XQueryContext context, final FunctionSignature signa @Override public Sequence eval(final Sequence[] args, final Sequence contextSequence) throws XPathException { final Sequence optionsArg = args[1]; - @Nullable - final Map options; - if (optionsArg.isEmpty()) { - options = null; - } else { - options = (Map) optionsArg.itemAt(0).toJavaObject(Map.class); - } + final MapType options = optionsArg.isEmpty() + ? new MapType(context) + : (MapType) optionsArg.itemAt(0); final IxmlParserFunction fn; final Sequence grammarArg = args[0]; @@ -103,20 +99,21 @@ private static class IxmlParserFunction extends UserDefinedFunction { returns(Type.DOCUMENT, "just a random string for now"), param("Parser inpuit", Type.STRING, "param description")); + private static final StringValue FAIL_ON_ERROR_KEY = new StringValue("fail-on-error"); + @Nullable final Either grammar; - @Nullable - final Map options; + final MapType options; IxmlParserFunction(final XQueryContext context, @Nullable final StringValue grammar, - @Nullable final Map options) { + final MapType options) { super(context, FS_PARSE_INVISIBLE_XML); this.grammar = Left(grammar); this.options = options; } IxmlParserFunction(final XQueryContext context, @Nullable final Element grammar, - @Nullable final Map options) { + final MapType options) { super(context, FS_PARSE_INVISIBLE_XML); this.grammar = Right(grammar); this.options = options; @@ -154,10 +151,8 @@ public Sequence eval(final Sequence contextSequence, final Item contextItem) thr } } - boolean failOnError = false; - if (options != null) { - failOnError = Boolean.TRUE.equals(options.get("fail-on-error")); - } + final boolean failOnError = options.contains(FAIL_ON_ERROR_KEY) + && options.get(FAIL_ON_ERROR_KEY).effectiveBooleanValue(); // parse the input using the ixml grammar final String generatedXML; From 5f348c05f6e79579c604a0882485a84bb270edd1 Mon Sep 17 00:00:00 2001 From: marmoure Date: Mon, 14 Sep 2026 18:31:45 +0200 Subject: [PATCH 21/32] [feature] Add error codes --- .../xquery/functions/fn/FnInvisibleXml.java | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java index 3566557327..6f0dbdeca2 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java @@ -41,6 +41,8 @@ import org.xml.sax.*; import de.bottlecaps.markup.Blitz; +import de.bottlecaps.markup.BlitzException; +import de.bottlecaps.markup.blitz.Parser; import javax.annotation.Nullable; import java.io.IOException; @@ -147,19 +149,28 @@ public Sequence eval(final Sequence contextSequence, final Item contextItem) thr xqSerializer.serialize((Sequence) grammar.right().get()); ixmlGrammar = writer.toString(); } catch (final SAXException e) { - throw new XPathException(this, e.getMessage(), e); + throw new XPathException(this, ErrorCodes.FOIX0001, e.getMessage(), e); } } final boolean failOnError = options.contains(FAIL_ON_ERROR_KEY) && options.get(FAIL_ON_ERROR_KEY).effectiveBooleanValue(); + final Parser parser; + try { + parser = Blitz.generate(ixmlGrammar); + } catch (final BlitzException e) { + throw new XPathException(this, ErrorCodes.FOIX0001, e.getMessage(), e); + } + // parse the input using the ixml grammar final String generatedXML; - if (failOnError) { - generatedXML = Blitz.generate(ixmlGrammar).parse(input, Blitz.Option.FAIL_ON_ERROR); - } else { - generatedXML = Blitz.generate(ixmlGrammar).parse(input); + try { + generatedXML = failOnError + ? parser.parse(input, Blitz.Option.FAIL_ON_ERROR) + : parser.parse(input); + } catch (final BlitzException e) { + throw new XPathException(this, ErrorCodes.FOIX0002, e.getMessage(), e); } return parse(generatedXML); From c8d385206d9a5d0498ca386c698baa321cf6a82f Mon Sep 17 00:00:00 2001 From: marmoure Date: Tue, 15 Sep 2026 10:46:08 +0200 Subject: [PATCH 22/32] [bugfix] Update the inner function description. --- .../java/org/exist/xquery/functions/fn/FnInvisibleXml.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java index 6f0dbdeca2..96064da2c1 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java @@ -97,9 +97,9 @@ private static class IxmlParserFunction extends UserDefinedFunction { private static final String FS_PARSE_INVISIBLE_XML_NAME = "parse-invisible-xml"; private static final FunctionSignature FS_PARSE_INVISIBLE_XML = functionSignature( FS_PARSE_INVISIBLE_XML_NAME, - "Gets the next random number generator.", - returns(Type.DOCUMENT, "just a random string for now"), - param("Parser inpuit", Type.STRING, "param description")); + "Parses the input using the given iXML grammar.", + returns(Type.DOCUMENT, "The parsed document"), + param("input", Type.STRING, "The input to parse")); private static final StringValue FAIL_ON_ERROR_KEY = new StringValue("fail-on-error"); From 27644afe74ffcb621f0f1a9d3660f8bc7a3237fb Mon Sep 17 00:00:00 2001 From: marmoure Date: Tue, 15 Sep 2026 18:40:05 +0200 Subject: [PATCH 23/32] [test] Create unit test for the invisible-xml function --- .../functions/fn/FnInvisibleXmlTest.java | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 exist-core/src/test/java/org/exist/xquery/functions/fn/FnInvisibleXmlTest.java diff --git a/exist-core/src/test/java/org/exist/xquery/functions/fn/FnInvisibleXmlTest.java b/exist-core/src/test/java/org/exist/xquery/functions/fn/FnInvisibleXmlTest.java new file mode 100644 index 0000000000..838223cde3 --- /dev/null +++ b/exist-core/src/test/java/org/exist/xquery/functions/fn/FnInvisibleXmlTest.java @@ -0,0 +1,157 @@ +/* + * 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 + */ +package org.exist.xquery.functions.fn; + +import org.exist.test.ExistXmldbEmbeddedServer; +import org.junit.ClassRule; +import org.junit.Test; +import org.xmldb.api.base.XMLDBException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class FnInvisibleXmlTest { + + // xquery version "3.1"; + + // let $date-grammar := " date = year, -'-', month, -'-', day . + // year = d, d, d, d . + // month = '0', d | '1', ['0'|'1'|'2'] . + // day = ['0'|'1'|'2'], d | '3', ['0'|'1'] . + // -d = ['0'-'9'] ." + + // let $valid-date-input := "2023-10-31" + + // let $invalid-date-input := "2023-10-32" + + // let $alphabit-grammar := "S=A. A='a'." + + // let $alphabit-valid-input := "a" + // (:expected a :) + // let $alphabit-invalid-input := "b" + // (:let $result := $parser("b"):) + // (:return $result/*/@*:state = 'failed':) + // (:expected true() :) + // (:FOIX0002 when fail-on is true:) + + // (:let $parser := fn:invisible-xml($grammar, map { }):) + // let $parser-fail-on := fn:invisible-xml($alphabit-grammar, map { + // "fail-on-error": true() }) + + // return $parser-fail-on($alphabit-invalid-input) + + @ClassRule + public static final ExistXmldbEmbeddedServer existEmbeddedServer = new ExistXmldbEmbeddedServer(false, true, true); + + private static final String DATE_GRAMMAR = " date = year, -'-', month, -'-', day .\n" + + " year = d, d, d, d .\n" + + "month = '0', d | '1', ['0'|'1'|'2'] .\n" + + " day = ['0'|'1'|'2'], d | '3', ['0'|'1'] .\n" + + " -d = ['0'-'9'] ."; + private static final String DATE_VALID_INPUT = "2023-10-31"; + private static final String DATE_INVALID_INPUT = "2023-10-32"; + + private static final String ALPHABIT_GRAMMAR = "S=A. A='a'."; + private static final String ALPHABIT_VALID_INPUT = "a"; + private static final String ALPHABIT_INVALID_INPUT = "b"; + + private static final String NO_OPTIONS = "map { }"; + private static final String FAIL_ON_ERROR_OPTIONS = "map { \"fail-on-error\": true() }"; + + @Test + public void dateValidInput() throws XMLDBException { + final String result = existEmbeddedServer + .executeOneValue(parseQuery(DATE_GRAMMAR, NO_OPTIONS, DATE_VALID_INPUT)); + assertEquals("20231031", result); + } + + @Test + public void dateInvalidInput() throws XMLDBException { + final String result = existEmbeddedServer + .executeOneValue(failedStateQuery(DATE_GRAMMAR, NO_OPTIONS, DATE_INVALID_INPUT)); + assertEquals("true", result); + } + + @Test + public void dateInvalidInputFailOnError() { + assertError("FOIX0002", parseQuery(DATE_GRAMMAR, FAIL_ON_ERROR_OPTIONS, DATE_INVALID_INPUT)); + } + + @Test + public void alphabitValidInput() throws XMLDBException { + final String result = existEmbeddedServer + .executeOneValue(parseQuery(ALPHABIT_GRAMMAR, NO_OPTIONS, ALPHABIT_VALID_INPUT)); + assertEquals("a", result); + } + + @Test + public void alphabitValidInputFailOnError() throws XMLDBException { + final String result = existEmbeddedServer + .executeOneValue(parseQuery(ALPHABIT_GRAMMAR, FAIL_ON_ERROR_OPTIONS, ALPHABIT_VALID_INPUT)); + assertEquals("a", result); + } + + @Test + public void alphabitInvalidInput() throws XMLDBException { + final String result = existEmbeddedServer + .executeOneValue(failedStateQuery(ALPHABIT_GRAMMAR, NO_OPTIONS, ALPHABIT_INVALID_INPUT)); + assertEquals("true", result); + } + + @Test + public void alphabitInvalidInputFailOnError() { + assertError("FOIX0002", parseQuery(ALPHABIT_GRAMMAR, FAIL_ON_ERROR_OPTIONS, ALPHABIT_INVALID_INPUT)); + } + + /** + * Builds a query that creates a parser from the grammar and options, and + * applies it to the input. + */ + private static String parseQuery(final String grammar, final String options, final String input) { + return "let $parser := fn:invisible-xml(" + stringLiteral(grammar) + ", " + options + ")\n" + + "return fn:serialize($parser(" + stringLiteral(input) + "))"; + } + + /** + * Builds a query that checks whether parsing the input produced a failed state. + */ + private static String failedStateQuery(final String grammar, final String options, final String input) { + return "let $parser := fn:invisible-xml(" + stringLiteral(grammar) + ", " + options + ")\n" + + "let $result := $parser(" + stringLiteral(input) + ")\n" + + "return $result/*/@*:state = 'failed'"; + } + + private static String stringLiteral(final String value) { + return "\"" + value.replace("\"", "\"\"") + "\""; + } + + private static void assertError(final String errorCode, final String query) { + try { + existEmbeddedServer.executeOneValue(query); + } catch (final XMLDBException e) { + assertTrue(e.getMessage(), e.getMessage().contains(errorCode)); + return; + } + + fail("Expected XPathException: err:" + errorCode); + } +} From 45de5044795fc0f6318da817421582cba08f854f Mon Sep 17 00:00:00 2001 From: marmoure Date: Wed, 16 Sep 2026 20:00:45 +0200 Subject: [PATCH 24/32] [bugfix] Make sure the param is registered --- .../org/exist/xquery/functions/fn/FnInvisibleXml.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java index 96064da2c1..77efc14cca 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java @@ -108,8 +108,9 @@ private static class IxmlParserFunction extends UserDefinedFunction { final MapType options; IxmlParserFunction(final XQueryContext context, @Nullable final StringValue grammar, - final MapType options) { + final MapType options) throws XPathException { super(context, FS_PARSE_INVISIBLE_XML); + addVariable("input"); this.grammar = Left(grammar); this.options = options; } @@ -200,5 +201,10 @@ private Sequence parse(final String xmlContent) throws XPathException { } } + + // @Override + // public String toString() { + // return "testing-invisible-xml"; + // } } } From 3450dd67219486deaef61625b49daefba923db4a Mon Sep 17 00:00:00 2001 From: marmoure Date: Thu, 17 Sep 2026 10:17:03 +0200 Subject: [PATCH 25/32] [bugfix] clean up --- .../org/exist/xquery/functions/fn/FnInvisibleXml.java | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java index 77efc14cca..79c8f9b748 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java @@ -129,12 +129,9 @@ public Sequence eval(final Sequence contextSequence, final Item contextItem) thr final Sequence inputArg = getCurrentArguments()[0]; final String input = inputArg.getStringValue(); - // generate the default ixml grammar final String ixmlGrammar; - // the null check here is wrong if (grammar == null) { - // something went horabily wrong - throw new XPathException("idk how it can be null"); + throw new XPathException("Internal error: null grammar."); } if (grammar.isLeft()) { if (grammar.left().get() == null) { @@ -201,10 +198,5 @@ private Sequence parse(final String xmlContent) throws XPathException { } } - - // @Override - // public String toString() { - // return "testing-invisible-xml"; - // } } } From e88759e983bd330cd868c954eb59620304f7da12 Mon Sep 17 00:00:00 2001 From: marmoure Date: Tue, 22 Sep 2026 19:31:56 +0200 Subject: [PATCH 26/32] [feature] Remove the deprecated error codes and use new w3c error code --- .../main/java/org/exist/xquery/ErrorCodes.java | 18 ------------------ .../xquery/functions/fn/FnInvisibleXml.java | 6 +++--- 2 files changed, 3 insertions(+), 21 deletions(-) diff --git a/exist-core/src/main/java/org/exist/xquery/ErrorCodes.java b/exist-core/src/main/java/org/exist/xquery/ErrorCodes.java index 067cbd0ec1..88b6c1b660 100644 --- a/exist-core/src/main/java/org/exist/xquery/ErrorCodes.java +++ b/exist-core/src/main/java/org/exist/xquery/ErrorCodes.java @@ -1540,24 +1540,6 @@ public DynamicErrorCode(final QName qname, @Nullable final String description) { @Deprecated public static final ErrorCode FOXT0006 = W3CErrorCode.FOXT0006.errorCode; - /** - * @deprecated Use {@link W3CErrorCode#FOIX0001}. - */ - @Deprecated - public static final ErrorCode FOIX0001 = W3CErrorCode.FOIX0001.errorCode; - - /** - * @deprecated Use {@link W3CErrorCode#FOIX0002}. - */ - @Deprecated - public static final ErrorCode FOIX0002 = W3CErrorCode.FOIX0002.errorCode; - - /** - * @deprecated Use {@link W3CErrorCode#FOIX0003}. - */ - @Deprecated - public static final ErrorCode FOIX0003 = W3CErrorCode.FOIX0003.errorCode; - /** * @deprecated Use {@link W3CErrorCode#XTSE0165}. */ diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java index 79c8f9b748..2c1c3b03f4 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java @@ -147,7 +147,7 @@ public Sequence eval(final Sequence contextSequence, final Item contextItem) thr xqSerializer.serialize((Sequence) grammar.right().get()); ixmlGrammar = writer.toString(); } catch (final SAXException e) { - throw new XPathException(this, ErrorCodes.FOIX0001, e.getMessage(), e); + throw new XPathException(this, ErrorCodes.W3CErrorCode.FOIX0001.getErrorCode(), e.getMessage(), e); } } @@ -158,7 +158,7 @@ public Sequence eval(final Sequence contextSequence, final Item contextItem) thr try { parser = Blitz.generate(ixmlGrammar); } catch (final BlitzException e) { - throw new XPathException(this, ErrorCodes.FOIX0001, e.getMessage(), e); + throw new XPathException(this, ErrorCodes.W3CErrorCode.FOIX0001.getErrorCode(), e.getMessage(), e); } // parse the input using the ixml grammar @@ -168,7 +168,7 @@ public Sequence eval(final Sequence contextSequence, final Item contextItem) thr ? parser.parse(input, Blitz.Option.FAIL_ON_ERROR) : parser.parse(input); } catch (final BlitzException e) { - throw new XPathException(this, ErrorCodes.FOIX0002, e.getMessage(), e); + throw new XPathException(this, ErrorCodes.W3CErrorCode.FOIX0002.getErrorCode(), e.getMessage(), e); } return parse(generatedXML); From 3837d7e68ac715fe99a6c39c4cbf533df0482750 Mon Sep 17 00:00:00 2001 From: marmoure Date: Tue, 22 Sep 2026 19:33:26 +0200 Subject: [PATCH 27/32] [refactor] Remove xquery test case comment from unit tests file --- .../functions/fn/FnInvisibleXmlTest.java | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/exist-core/src/test/java/org/exist/xquery/functions/fn/FnInvisibleXmlTest.java b/exist-core/src/test/java/org/exist/xquery/functions/fn/FnInvisibleXmlTest.java index 838223cde3..df2a03b642 100644 --- a/exist-core/src/test/java/org/exist/xquery/functions/fn/FnInvisibleXmlTest.java +++ b/exist-core/src/test/java/org/exist/xquery/functions/fn/FnInvisibleXmlTest.java @@ -31,34 +31,6 @@ public class FnInvisibleXmlTest { - // xquery version "3.1"; - - // let $date-grammar := " date = year, -'-', month, -'-', day . - // year = d, d, d, d . - // month = '0', d | '1', ['0'|'1'|'2'] . - // day = ['0'|'1'|'2'], d | '3', ['0'|'1'] . - // -d = ['0'-'9'] ." - - // let $valid-date-input := "2023-10-31" - - // let $invalid-date-input := "2023-10-32" - - // let $alphabit-grammar := "S=A. A='a'." - - // let $alphabit-valid-input := "a" - // (:expected a :) - // let $alphabit-invalid-input := "b" - // (:let $result := $parser("b"):) - // (:return $result/*/@*:state = 'failed':) - // (:expected true() :) - // (:FOIX0002 when fail-on is true:) - - // (:let $parser := fn:invisible-xml($grammar, map { }):) - // let $parser-fail-on := fn:invisible-xml($alphabit-grammar, map { - // "fail-on-error": true() }) - - // return $parser-fail-on($alphabit-invalid-input) - @ClassRule public static final ExistXmldbEmbeddedServer existEmbeddedServer = new ExistXmldbEmbeddedServer(false, true, true); From 6073a6d36e9d0c3eab577a27fe68cadaf8d2c12d Mon Sep 17 00:00:00 2001 From: marmoure Date: Tue, 22 Sep 2026 19:34:27 +0200 Subject: [PATCH 28/32] [bugfix] Static variables should be upper-case --- .../xquery/functions/fn/FnInvisibleXmlTest.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/exist-core/src/test/java/org/exist/xquery/functions/fn/FnInvisibleXmlTest.java b/exist-core/src/test/java/org/exist/xquery/functions/fn/FnInvisibleXmlTest.java index df2a03b642..19bdb296bf 100644 --- a/exist-core/src/test/java/org/exist/xquery/functions/fn/FnInvisibleXmlTest.java +++ b/exist-core/src/test/java/org/exist/xquery/functions/fn/FnInvisibleXmlTest.java @@ -32,7 +32,8 @@ public class FnInvisibleXmlTest { @ClassRule - public static final ExistXmldbEmbeddedServer existEmbeddedServer = new ExistXmldbEmbeddedServer(false, true, true); + public static final ExistXmldbEmbeddedServer EXIST_EMBEDDED_SERVER = new ExistXmldbEmbeddedServer(false, true, + true); private static final String DATE_GRAMMAR = " date = year, -'-', month, -'-', day .\n" + " year = d, d, d, d .\n" + @@ -51,14 +52,14 @@ public class FnInvisibleXmlTest { @Test public void dateValidInput() throws XMLDBException { - final String result = existEmbeddedServer + final String result = EXIST_EMBEDDED_SERVER .executeOneValue(parseQuery(DATE_GRAMMAR, NO_OPTIONS, DATE_VALID_INPUT)); assertEquals("20231031", result); } @Test public void dateInvalidInput() throws XMLDBException { - final String result = existEmbeddedServer + final String result = EXIST_EMBEDDED_SERVER .executeOneValue(failedStateQuery(DATE_GRAMMAR, NO_OPTIONS, DATE_INVALID_INPUT)); assertEquals("true", result); } @@ -70,21 +71,21 @@ public void dateInvalidInputFailOnError() { @Test public void alphabitValidInput() throws XMLDBException { - final String result = existEmbeddedServer + final String result = EXIST_EMBEDDED_SERVER .executeOneValue(parseQuery(ALPHABIT_GRAMMAR, NO_OPTIONS, ALPHABIT_VALID_INPUT)); assertEquals("a", result); } @Test public void alphabitValidInputFailOnError() throws XMLDBException { - final String result = existEmbeddedServer + final String result = EXIST_EMBEDDED_SERVER .executeOneValue(parseQuery(ALPHABIT_GRAMMAR, FAIL_ON_ERROR_OPTIONS, ALPHABIT_VALID_INPUT)); assertEquals("a", result); } @Test public void alphabitInvalidInput() throws XMLDBException { - final String result = existEmbeddedServer + final String result = EXIST_EMBEDDED_SERVER .executeOneValue(failedStateQuery(ALPHABIT_GRAMMAR, NO_OPTIONS, ALPHABIT_INVALID_INPUT)); assertEquals("true", result); } @@ -118,7 +119,7 @@ private static String stringLiteral(final String value) { private static void assertError(final String errorCode, final String query) { try { - existEmbeddedServer.executeOneValue(query); + EXIST_EMBEDDED_SERVER.executeOneValue(query); } catch (final XMLDBException e) { assertTrue(e.getMessage(), e.getMessage().contains(errorCode)); return; From de6567629504bb481e0ef90aea32f28a29f43b66 Mon Sep 17 00:00:00 2001 From: marmoure Date: Tue, 22 Sep 2026 19:37:24 +0200 Subject: [PATCH 29/32] [bugfix] Refrence the actual error code instead of reproducing it as a string --- .../xquery/functions/fn/FnInvisibleXmlTest.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/exist-core/src/test/java/org/exist/xquery/functions/fn/FnInvisibleXmlTest.java b/exist-core/src/test/java/org/exist/xquery/functions/fn/FnInvisibleXmlTest.java index 19bdb296bf..fa557a38a4 100644 --- a/exist-core/src/test/java/org/exist/xquery/functions/fn/FnInvisibleXmlTest.java +++ b/exist-core/src/test/java/org/exist/xquery/functions/fn/FnInvisibleXmlTest.java @@ -21,6 +21,7 @@ package org.exist.xquery.functions.fn; import org.exist.test.ExistXmldbEmbeddedServer; +import org.exist.xquery.ErrorCodes; import org.junit.ClassRule; import org.junit.Test; import org.xmldb.api.base.XMLDBException; @@ -66,7 +67,8 @@ public void dateInvalidInput() throws XMLDBException { @Test public void dateInvalidInputFailOnError() { - assertError("FOIX0002", parseQuery(DATE_GRAMMAR, FAIL_ON_ERROR_OPTIONS, DATE_INVALID_INPUT)); + assertError(ErrorCodes.W3CErrorCode.FOIX0002, + parseQuery(DATE_GRAMMAR, FAIL_ON_ERROR_OPTIONS, DATE_INVALID_INPUT)); } @Test @@ -92,7 +94,8 @@ public void alphabitInvalidInput() throws XMLDBException { @Test public void alphabitInvalidInputFailOnError() { - assertError("FOIX0002", parseQuery(ALPHABIT_GRAMMAR, FAIL_ON_ERROR_OPTIONS, ALPHABIT_INVALID_INPUT)); + assertError(ErrorCodes.W3CErrorCode.FOIX0002, + parseQuery(ALPHABIT_GRAMMAR, FAIL_ON_ERROR_OPTIONS, ALPHABIT_INVALID_INPUT)); } /** @@ -117,14 +120,14 @@ private static String stringLiteral(final String value) { return "\"" + value.replace("\"", "\"\"") + "\""; } - private static void assertError(final String errorCode, final String query) { + private static void assertError(final ErrorCodes.W3CErrorCode errorCode, final String query) { try { EXIST_EMBEDDED_SERVER.executeOneValue(query); } catch (final XMLDBException e) { - assertTrue(e.getMessage(), e.getMessage().contains(errorCode)); + assertTrue(e.getMessage(), e.getMessage().contains(errorCode.name())); return; } - fail("Expected XPathException: err:" + errorCode); + fail("Expected XPathException: err:" + errorCode.name()); } } From 78154e15383902fadf741f6a3dd8b779de00750c Mon Sep 17 00:00:00 2001 From: marmoure Date: Tue, 22 Sep 2026 19:44:19 +0200 Subject: [PATCH 30/32] [test] test case when no grammar was provided --- .../functions/fn/FnInvisibleXmlTest.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/exist-core/src/test/java/org/exist/xquery/functions/fn/FnInvisibleXmlTest.java b/exist-core/src/test/java/org/exist/xquery/functions/fn/FnInvisibleXmlTest.java index fa557a38a4..c812ad94ff 100644 --- a/exist-core/src/test/java/org/exist/xquery/functions/fn/FnInvisibleXmlTest.java +++ b/exist-core/src/test/java/org/exist/xquery/functions/fn/FnInvisibleXmlTest.java @@ -98,6 +98,20 @@ public void alphabitInvalidInputFailOnError() { parseQuery(ALPHABIT_GRAMMAR, FAIL_ON_ERROR_OPTIONS, ALPHABIT_INVALID_INPUT)); } + @Test + public void noGrammarProvidedNoOptions() throws XMLDBException { + final String result = EXIST_EMBEDDED_SERVER + .executeOneValue(failedStateQueryNoGrammar(NO_OPTIONS, ALPHABIT_GRAMMAR)); + assertEquals("false", result); + } + + @Test + public void noGrammarProvidedWithOptions() throws XMLDBException { + final String result = EXIST_EMBEDDED_SERVER + .executeOneValue(failedStateQueryNoGrammar(FAIL_ON_ERROR_OPTIONS, ALPHABIT_GRAMMAR)); + assertEquals("false", result); + } + /** * Builds a query that creates a parser from the grammar and options, and * applies it to the input. @@ -116,6 +130,15 @@ private static String failedStateQuery(final String grammar, final String option "return $result/*/@*:state = 'failed'"; } + /* + * Builds a query that creates a parser with no grammar + */ + private static String failedStateQueryNoGrammar(final String options, final String input) { + return "let $parser := fn:invisible-xml((), " + options + ")\n" + + "let $result := $parser(" + stringLiteral(input) + ")\n" + + "return $result/*/@*:state = 'failed'"; + } + private static String stringLiteral(final String value) { return "\"" + value.replace("\"", "\"\"") + "\""; } From bed1ba4d3c80acac6744b808365b7ed5fd8b7cf7 Mon Sep 17 00:00:00 2001 From: marmoure Date: Tue, 22 Sep 2026 20:03:57 +0200 Subject: [PATCH 31/32] [bugfix] Fix the license headers --- exist-core/pom.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/exist-core/pom.xml b/exist-core/pom.xml index 80e2a50099..8503e77c5a 100644 --- a/exist-core/pom.xml +++ b/exist-core/pom.xml @@ -799,6 +799,8 @@ src/test/java/org/exist/xquery/functions/xquery3/SerializeTest.java src/main/java/org/exist/xquery/value/ArrayWrapper.java src/test/java/org/exist/xquery/value/DateTimeTypesTest.java + src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java + src/test/java/org/exist/xquery/functions/fn/FnInvisibleXmlTest.java @@ -2351,6 +2353,8 @@ src/main/java/org/exist/xquery/functions/fn/FnFormatDates.java src/main/java/org/exist/xquery/functions/fn/FnHasChildren.java src/main/java/org/exist/xquery/functions/fn/FnInnerMost.java + src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java + src/test/java/org/exist/xquery/functions/fn/FnInvisibleXmlTest.java src/main/java/org/exist/xquery/functions/fn/FnModule.java src/main/java/org/exist/xquery/functions/fn/FnOuterMost.java src/main/java/org/exist/xquery/functions/fn/FunAbs.java From b6dbf1ca5f2dc02057b3ea64f1ee9817bab4bef1 Mon Sep 17 00:00:00 2001 From: marmoure Date: Tue, 22 Sep 2026 20:33:06 +0200 Subject: [PATCH 32/32] [feature] Only the grammar can be null, left and right values can never be null --- .../xquery/functions/fn/FnInvisibleXml.java | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java index 2c1c3b03f4..dbe137c956 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FnInvisibleXml.java @@ -75,7 +75,7 @@ public Sequence eval(final Sequence[] args, final Sequence contextSequence) thro final Sequence grammarArg = args[0]; if (grammarArg.isEmpty()) { // no grammar provided - fn = new IxmlParserFunction(context, (StringValue) null, options); + fn = new IxmlParserFunction(context, options); } else if (grammarArg.getItemType() == Type.STRING) { // grammar is a string @@ -107,7 +107,14 @@ private static class IxmlParserFunction extends UserDefinedFunction { final Either grammar; final MapType options; - IxmlParserFunction(final XQueryContext context, @Nullable final StringValue grammar, + IxmlParserFunction(final XQueryContext context, final MapType options) throws XPathException { + super(context, FS_PARSE_INVISIBLE_XML); + addVariable("input"); + this.grammar = null; + this.options = options; + } + + IxmlParserFunction(final XQueryContext context, final StringValue grammar, final MapType options) throws XPathException { super(context, FS_PARSE_INVISIBLE_XML); addVariable("input"); @@ -115,9 +122,10 @@ private static class IxmlParserFunction extends UserDefinedFunction { this.options = options; } - IxmlParserFunction(final XQueryContext context, @Nullable final Element grammar, - final MapType options) { + IxmlParserFunction(final XQueryContext context, final Element grammar, + final MapType options) throws XPathException { super(context, FS_PARSE_INVISIBLE_XML); + addVariable("input"); this.grammar = Right(grammar); this.options = options; } @@ -131,14 +139,14 @@ public Sequence eval(final Sequence contextSequence, final Item contextItem) thr final String ixmlGrammar; if (grammar == null) { - throw new XPathException("Internal error: null grammar."); - } - if (grammar.isLeft()) { - if (grammar.left().get() == null) { + // no grammar provided: use the default Invisible XML specification grammar + try { ixmlGrammar = Blitz.ixmlGrammar(); - } else { - ixmlGrammar = grammar.left().get().getStringValue(); + } catch (final BlitzException e) { + throw new XPathException(this, ErrorCodes.W3CErrorCode.FOIX0003.getErrorCode(), e.getMessage(), e); } + } else if (grammar.isLeft()) { + ixmlGrammar = grammar.left().get().getStringValue(); } else { // grammar is an element: serialize it to a String try (final StringBuilderWriter writer = new StringBuilderWriter()) {