From 5d8c7db650d7a1067c83ff961d6a68e18577e44e Mon Sep 17 00:00:00 2001 From: Joe Wicentowski Date: Sun, 15 Mar 2026 23:45:11 -0400 Subject: [PATCH 1/2] [refactor] Upgrade Saxon-HE from 9.9 to 12.5; eliminate exist-saxon-regex fork Upgrade Saxon-HE from 9.9.1-8 to 12.5 and remove the exist-saxon-regex fork (org.exist-db:exist-saxon-regex:9.4.0-9.e1), a copy of Saxon 9.4's internal regex classes that has been maintained separately for over a decade. Saxon 12's public regex API makes the fork unnecessary. Saxon 12 API migration: - FastStringBuffer removed: use FloatValue.floatToString() and DoubleValue.doubleToString() for XPath-compliant formatting - Regex APIs now take UnicodeString: wrap with StringView.of() - XPathException.getErrorCodeLocalPart() replaced by getErrorCodeQName().getLocalPart() - RegexIterator.MatchHandler moved to top-level RegexMatchHandler - Xslt30Transformer.setInitialMode() now throws SaxonApiException - Saxon 12 rejects duplicate document-URIs in the document pool - Saxon 12 rejects null URIResolver and explicit xml namespace declarations in DOM and SAX pipelines - Saxon 12's LinkedTreeBuilder rejects duplicate startDocument events exist-saxon-regex replaced by Saxon 12's JavaRegularExpression API. Full exist-core test suite: 6533 tests, 0 failures, 0 errors. Co-Authored-By: Claude Opus 4.6 (1M context) --- exist-core/pom.xml | 13 --- .../org/exist/dom/persistent/ElementImpl.java | 16 +++ .../exist/http/urlrewrite/RewriteConfig.java | 14 +-- .../exist/storage/serializers/Serializer.java | 3 +- .../exist/util/XMLBackwardsCompatHandler.java | 106 ++++++++++++++++++ .../exist/validation/XmlLibraryChecker.java | 2 +- .../xquery/functions/fn/FunAnalyzeString.java | 26 +++-- .../exist/xquery/functions/fn/FunMatches.java | 9 +- .../exist/xquery/functions/fn/FunReplace.java | 13 ++- .../functions/fn/transform/Transform.java | 13 ++- .../org/exist/xquery/regex/RegexUtil.java | 23 ++-- .../xquery/value/DayTimeDurationValue.java | 36 +----- .../org/exist/xquery/value/DoubleValue.java | 8 +- .../org/exist/xquery/value/FloatValue.java | 7 +- .../java/org/exist/xslt/EXistDbXMLReader.java | 8 +- .../xslt/StylesheetResolverAndCompiler.java | 17 ++- .../exist/dom/memtree/DocumentImplTest.java | 25 ++--- .../xquery3/transform/fnTransform68.xqm | 2 +- exist-parent/pom.xml | 2 +- .../spatial/AbstractGMLJDBCIndexWorker.java | 16 ++- 20 files changed, 229 insertions(+), 130 deletions(-) create mode 100644 exist-core/src/main/java/org/exist/util/XMLBackwardsCompatHandler.java diff --git a/exist-core/pom.xml b/exist-core/pom.xml index 09d016548b3..4d682360d7b 100644 --- a/exist-core/pom.xml +++ b/exist-core/pom.xml @@ -391,19 +391,6 @@ Saxon-HE - - org.exist-db - exist-saxon-regex - 9.4.0-9.e1 - - - - net.sf.saxon - Saxon-HE - - - - com.evolvedbinary.thirdparty.org.apache.xmlrpc xmlrpc-common diff --git a/exist-core/src/main/java/org/exist/dom/persistent/ElementImpl.java b/exist-core/src/main/java/org/exist/dom/persistent/ElementImpl.java index 4cf5fd93107..610750d9059 100644 --- a/exist-core/src/main/java/org/exist/dom/persistent/ElementImpl.java +++ b/exist-core/src/main/java/org/exist/dom/persistent/ElementImpl.java @@ -822,6 +822,7 @@ public Attr getAttributeNodeNS(final String namespaceURI, final String localName @Override public NamedNodeMap getAttributes() { final org.exist.dom.NamedNodeMapImpl map = new NamedNodeMapImpl(ownerDocument, true); + if(hasAttributes()) { try(final DBBroker broker = ownerDocument.getBrokerPool().getBroker(); final INodeIterator iterator = broker.getNodeIterator(this)) { @@ -837,6 +838,14 @@ public NamedNodeMap getAttributes() { if(next.getNodeType() != Node.ATTRIBUTE_NODE) { break; } + // Skip namespace declarations for the XML namespace — the xml prefix + // is always implicitly bound and Saxon 12 rejects any explicit + // declaration involving http://www.w3.org/XML/1998/namespace + if (next.getNodeType() == Node.ATTRIBUTE_NODE + && Namespaces.XMLNS_NS.equals(next.getNamespaceURI()) + && XMLConstants.XML_NS_URI.equals(next.getNodeValue())) { + continue; + } map.setNamedItem(next); } } catch(final EXistException | IOException e) { @@ -847,6 +856,13 @@ public NamedNodeMap getAttributes() { for (final Map.Entry entry : namespaceMappings.entrySet()) { final String prefix = entry.getKey(); final String ns = entry.getValue(); + // Skip namespace declarations involving the XML namespace URI — + // Saxon 12 rejects any explicit declaration of the xml prefix + // or binding of the XML namespace to a non-xml prefix + if (XMLConstants.XML_NS_PREFIX.equals(prefix) + || XMLConstants.XML_NS_URI.equals(ns)) { + continue; + } final QName attrName = new QName(prefix, Namespaces.XMLNS_NS, XMLConstants.XMLNS_ATTRIBUTE); final AttrImpl attr = new AttrImpl(getExpression(), attrName, ns, null); attr.setOwnerDocument(ownerDocument); diff --git a/exist-core/src/main/java/org/exist/http/urlrewrite/RewriteConfig.java b/exist-core/src/main/java/org/exist/http/urlrewrite/RewriteConfig.java index 8310d7350e3..ef457e2aa8e 100644 --- a/exist-core/src/main/java/org/exist/http/urlrewrite/RewriteConfig.java +++ b/exist-core/src/main/java/org/exist/http/urlrewrite/RewriteConfig.java @@ -31,9 +31,8 @@ import org.exist.security.PermissionDeniedException; import org.exist.storage.DBBroker; import org.exist.storage.lock.Lock.LockMode; -import org.exist.thirdparty.net.sf.saxon.functions.regex.JDK15RegexTranslator; -import org.exist.thirdparty.net.sf.saxon.functions.regex.RegexSyntaxException; -import org.exist.thirdparty.net.sf.saxon.functions.regex.RegularExpression; +import net.sf.saxon.regex.JavaRegularExpression; +import net.sf.saxon.str.StringView; import org.exist.util.XMLReaderPool; import org.exist.xmldb.XmldbURI; import org.exist.xquery.Constants; @@ -271,16 +270,13 @@ private static final class Mapping { private Mapping(String regex, final URLRewrite action) throws ServletException { try { - final int options = RegularExpression.XML11 | RegularExpression.XPATH30; - int flagbits = 0; - - final List warnings = new ArrayList<>(); - regex = JDK15RegexTranslator.translate(regex, options, flagbits, warnings); + final JavaRegularExpression javaRegex = new JavaRegularExpression(StringView.of(regex), ""); + regex = javaRegex.getJavaRegularExpression(); this.pattern = Pattern.compile(regex, 0); this.action = action; this.matcher = pattern.matcher(""); - } catch (final RegexSyntaxException e) { + } catch (final net.sf.saxon.trans.XPathException e) { throw new ServletException("Syntax error in regular expression specified for path. " + e.getMessage(), e); } 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 90c90ef5a14..30bf698dc56 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 @@ -825,7 +825,8 @@ public void setStylesheet(final Document doc, final @Nullable String stylesheet) // restore handlers receiver = oldReceiver; - factory.get().setURIResolver(null); + // Saxon 12 rejects null URIResolver; reset to default identity resolver + factory.get().setURIResolver((href, base) -> null); } LOG.debug("compiling stylesheet took {}", System.currentTimeMillis() - start); if (templates != null) { diff --git a/exist-core/src/main/java/org/exist/util/XMLBackwardsCompatHandler.java b/exist-core/src/main/java/org/exist/util/XMLBackwardsCompatHandler.java new file mode 100644 index 00000000000..47e364d09cb --- /dev/null +++ b/exist-core/src/main/java/org/exist/util/XMLBackwardsCompatHandler.java @@ -0,0 +1,106 @@ +/* + * eXist-db Open Source Native XML Database + * Copyright (C) 2001 The eXist-db Authors + * + * info@exist-db.org + * http://www.exist-db.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.util; + +import org.xml.sax.Attributes; +import org.xml.sax.ContentHandler; +import org.xml.sax.Locator; +import org.xml.sax.SAXException; + +/** + * A SAX ContentHandler wrapper that suppresses duplicate startDocument/endDocument calls. + * Saxon 12's LinkedTreeBuilder does not tolerate receiving startDocument more than once, + * which can happen when eXist's Serializer sends document events that overlap with + * explicitly-called startDocument/endDocument in the XSLT compilation pipeline. + */ +public class XMLBackwardsCompatHandler implements ContentHandler { + + private final ContentHandler delegate; + private boolean documentStarted = false; + + public XMLBackwardsCompatHandler(final ContentHandler delegate) { + this.delegate = delegate; + } + + @Override + public void startDocument() throws SAXException { + if (!documentStarted) { + documentStarted = true; + delegate.startDocument(); + } + } + + @Override + public void endDocument() throws SAXException { + // Suppress — the caller will call endDocument on the delegate directly + } + + @Override + public void setDocumentLocator(final Locator locator) { + delegate.setDocumentLocator(locator); + } + + @Override + public void startPrefixMapping(final String prefix, final String uri) throws SAXException { + // Saxon 12 rejects any namespace declaration involving the XML namespace URI + // (http://www.w3.org/XML/1998/namespace) — the xml prefix is always implicitly bound + if ("xml".equals(prefix) || javax.xml.XMLConstants.XML_NS_URI.equals(uri)) { + return; + } + delegate.startPrefixMapping(prefix, uri); + } + + @Override + public void endPrefixMapping(final String prefix) throws SAXException { + delegate.endPrefixMapping(prefix); + } + + @Override + public void startElement(final String uri, final String localName, final String qName, final Attributes atts) throws SAXException { + delegate.startElement(uri, localName, qName, atts); + } + + @Override + public void endElement(final String uri, final String localName, final String qName) throws SAXException { + delegate.endElement(uri, localName, qName); + } + + @Override + public void characters(final char[] ch, final int start, final int length) throws SAXException { + delegate.characters(ch, start, length); + } + + @Override + public void ignorableWhitespace(final char[] ch, final int start, final int length) throws SAXException { + delegate.ignorableWhitespace(ch, start, length); + } + + @Override + public void processingInstruction(final String target, final String data) throws SAXException { + delegate.processingInstruction(target, data); + } + + @Override + public void skippedEntity(final String name) throws SAXException { + delegate.skippedEntity(name); + } +} 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 5b9a570f3f3..ad240345548 100644 --- a/exist-core/src/main/java/org/exist/validation/XmlLibraryChecker.java +++ b/exist-core/src/main/java/org/exist/validation/XmlLibraryChecker.java @@ -54,7 +54,7 @@ public class XmlLibraryChecker { * Possible XML Transformers, at least one must be valid */ private final static ClassVersion[] validTransformers = { - new ClassVersion("Saxon", "8.9.0", "net.sf.saxon.Version.getProductVersion()"), + new ClassVersion("Saxon", "12.0", "net.sf.saxon.Version.getProductVersion()"), new ClassVersion("Xalan", "Xalan Java 2.7.1", "org.apache.xalan.Version.getVersion()"), }; 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 67332bffa4f..ef2a3fc9871 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 @@ -28,9 +28,11 @@ import java.util.regex.PatternSyntaxException; import net.sf.saxon.Configuration; -import net.sf.saxon.om.Item; import net.sf.saxon.regex.RegexIterator; +import net.sf.saxon.regex.RegexMatchHandler; 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.*; @@ -131,15 +133,15 @@ private void analyzeString(final MemTreeBuilder builder, final String input, Str final List warnings = new ArrayList<>(1); try { - final RegularExpression regularExpression = config.compileRegularExpression(pattern, flags, "XP30", warnings); - if (regularExpression.matches("")) { + final RegularExpression regularExpression = config.compileRegularExpression(StringView.of(pattern), flags, "XP30", warnings); + if (regularExpression.matches(StringView.of(""))) { throw new XPathException(this, ErrorCodes.FORX0003, "regular expression could match empty string"); } //TODO(AR) cache the regular expression... might be possible through Saxon config - final RegexIterator regexIterator = regularExpression.analyze(input); - Item item; + final RegexIterator regexIterator = regularExpression.analyze(StringView.of(input)); + net.sf.saxon.value.StringValue item; while ((item = regexIterator.next()) != null) { if (regexIterator.isMatching()) { match(builder, regexIterator); @@ -154,7 +156,7 @@ private void analyzeString(final MemTreeBuilder builder, final String input, Str } catch (final net.sf.saxon.trans.XPathException e) { // Saxon's XP30 regex translator rejects some valid patterns. // Fall back to Java regex before giving up. - if ("FORX0002".equals(e.getErrorCodeLocalPart())) { + if ("FORX0002".equals(e.getErrorCodeQName().getLocalPart())) { try { analyzeStringJavaRegex(builder, input, pattern, flags); return; @@ -162,7 +164,7 @@ private void analyzeString(final MemTreeBuilder builder, final String input, Str // Java regex fallback also failed — throw original Saxon error below } } - 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()); @@ -236,10 +238,10 @@ private void analyzeStringJavaRegex(final MemTreeBuilder builder, final String i private void match(final MemTreeBuilder builder, final RegexIterator regexIterator) throws net.sf.saxon.trans.XPathException { builder.startElement(QN_MATCH, null); - regexIterator.processMatchingSubstring(new RegexIterator.MatchHandler() { + regexIterator.processMatchingSubstring(new RegexMatchHandler() { @Override - public void characters(final CharSequence s) { - builder.characters(s); + public void characters(final UnicodeString s) { + builder.characters(s.toString()); } @Override @@ -258,9 +260,9 @@ public void onGroupEnd(final int groupNumber) throws net.sf.saxon.trans.XPathExc builder.endElement(); } - private void nonMatch(final MemTreeBuilder builder, final Item item) { + private void nonMatch(final MemTreeBuilder builder, final net.sf.saxon.value.StringValue 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 289cada28d2..a6a9de84617 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 @@ -47,6 +47,7 @@ import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import net.sf.saxon.regex.RegularExpression; +import net.sf.saxon.str.StringView; import static org.exist.xquery.FunctionDSL.*; import static org.exist.xquery.functions.fn.FnModule.functionSignatures; @@ -530,19 +531,19 @@ private boolean matchXmlRegex(final String string, final String pattern, final S List warnings = new ArrayList<>(1); RegularExpression regex = context.getBroker().getBrokerPool() .getSaxonConfiguration() - .compileRegularExpression(pattern, flags, "XP30", warnings); + .compileRegularExpression(StringView.of(pattern), flags, "XP30", warnings); for (final String warning : warnings) { LOG.warn(warning); } - return regex.containsMatch(string); + return regex.containsMatch(StringView.of(string)); } catch (final net.sf.saxon.trans.XPathException e) { // Saxon's XP30 regex translator rejects some valid patterns: // \b/\B word boundaries, certain quantifier sequences, \p{Is} names, etc. // Fall back to Java regex before giving up. - if ("FORX0002".equals(e.getErrorCodeLocalPart())) { + if ("FORX0002".equals(e.getErrorCodeQName().getLocalPart())) { try { final String javaPattern = translateRegexp( this, pattern, flags.contains("x"), flags.contains("i")); @@ -552,7 +553,7 @@ private boolean matchXmlRegex(final String string, final String pattern, final S // Java regex fallback also failed — throw original Saxon error below } } - 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 e8a9e81d378..93bfe85b6e0 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 @@ -30,6 +30,7 @@ import net.sf.saxon.Configuration; import net.sf.saxon.functions.Replace; import net.sf.saxon.regex.RegularExpression; +import net.sf.saxon.str.StringView; import org.exist.dom.QName; import org.exist.xquery.*; import org.exist.xquery.value.FunctionParameterSequenceType; @@ -136,26 +137,26 @@ public Sequence eval(final Sequence[] args, final Sequence contextSequence) thro final List warnings = new ArrayList<>(1); try { - final RegularExpression regularExpression = config.compileRegularExpression(pattern, flags, "XP30", warnings); - if (regularExpression.matches("")) { + final RegularExpression regularExpression = config.compileRegularExpression(StringView.of(pattern), flags, "XP30", warnings); + if (regularExpression.matches(StringView.of(""))) { throw new XPathException(this, ErrorCodes.FORX0003, "regular expression could match empty string"); } //TODO(AR) cache the regular expression... might be possible through Saxon config if (!hasLiteral(flags)) { - final String msg = Replace.checkReplacement(replace); + final String msg = Replace.checkReplacement(StringView.of(replace)); if (msg != null) { throw new XPathException(this, ErrorCodes.FORX0004, msg); } } - final CharSequence res = regularExpression.replace(string, replace); + final net.sf.saxon.str.UnicodeString res = regularExpression.replace(StringView.of(string), StringView.of(replace)); result = new StringValue(this, res.toString()); } catch (final net.sf.saxon.trans.XPathException e) { // Saxon's XP30 regex translator rejects some valid patterns. // Fall back to Java regex before giving up. - if ("FORX0002".equals(e.getErrorCodeLocalPart())) { + if ("FORX0002".equals(e.getErrorCodeQName().getLocalPart())) { try { final String javaPattern = translateRegexp( this, pattern, flags.contains("x"), flags.contains("i")); @@ -170,7 +171,7 @@ public Sequence eval(final Sequence[] args, final Sequence contextSequence) thro // Java regex fallback also failed — throw original Saxon error below } } - 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/Transform.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/transform/Transform.java index 4b398934b5f..301ff0e7635 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 @@ -138,7 +138,13 @@ 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()) { + try { + xslt30Transformer.setInitialMode(Convert.ToSaxon.of(options.initialMode.get().getQName())); + } catch (final SaxonApiException e) { + throw new XPathException(fnTransform, ErrorCodes.FOXT0003, "Unable to set initial mode: " + e.getMessage(), e); + } + } xslt30Transformer.setInitialTemplateParameters(options.templateParams, false); xslt30Transformer.setInitialTemplateParameters(options.tunnelParams, true); if (options.baseOutputURI.isPresent()) { @@ -426,7 +432,10 @@ 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())); + // Saxon 12 rejects duplicate document-URIs in the document pool. + // Don't set a system ID on the source DOMSource to avoid collisions + // with the stylesheet or other documents sharing the same base URI. + return sourceNode.map(NodeValue::getNode).map(node -> new DOMSource(node)); } private static class ErrorListenerLog4jAdapter implements ErrorListener { diff --git a/exist-core/src/main/java/org/exist/xquery/regex/RegexUtil.java b/exist-core/src/main/java/org/exist/xquery/regex/RegexUtil.java index d54ca496c01..576fc60801a 100644 --- a/exist-core/src/main/java/org/exist/xquery/regex/RegexUtil.java +++ b/exist-core/src/main/java/org/exist/xquery/regex/RegexUtil.java @@ -21,17 +21,14 @@ */ package org.exist.xquery.regex; -import org.exist.thirdparty.net.sf.saxon.functions.regex.JDK15RegexTranslator; -import org.exist.thirdparty.net.sf.saxon.functions.regex.RegexSyntaxException; -import org.exist.thirdparty.net.sf.saxon.functions.regex.RegularExpression; +import net.sf.saxon.regex.JavaRegularExpression; +import net.sf.saxon.str.StringView; import org.exist.xquery.ErrorCodes; import org.exist.xquery.Expression; import org.exist.xquery.XPathException; import org.exist.xquery.value.StringValue; import javax.annotation.Nullable; -import java.util.ArrayList; -import java.util.List; import java.util.regex.Pattern; /** @@ -140,21 +137,19 @@ public static boolean hasIgnoreWhitespace(final int flags) { * @throws XPathException if the XQuery Regular Expression is invalid. */ public static String translateRegexp(final Expression context, final String pattern, final boolean ignoreWhitespace, final boolean caseBlind) throws XPathException { - // convert pattern to Java regex syntax + // convert pattern to Java regex syntax using Saxon's regex translator try { - final int options = RegularExpression.XML11 | RegularExpression.XPATH30; - - int flagbits = 0; + final StringBuilder flags = new StringBuilder(); if (ignoreWhitespace) { - flagbits |= Pattern.COMMENTS; + flags.append('x'); } if (caseBlind) { - flagbits |= Pattern.CASE_INSENSITIVE; + flags.append('i'); } - final List warnings = new ArrayList<>(); - return JDK15RegexTranslator.translate(pattern, options, flagbits, warnings); - } catch (final RegexSyntaxException e) { + final JavaRegularExpression regex = new JavaRegularExpression(StringView.of(pattern), flags.toString()); + return regex.getJavaRegularExpression(); + } catch (final net.sf.saxon.trans.XPathException e) { throw new XPathException(context, ErrorCodes.FORX0002, "Conversion from XPath F&O 3.0 regular expression syntax to Java regular expression syntax failed: " + e.getMessage(), new StringValue(pattern), e); } } 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 4bb02170e71..1a7fc08aca2 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 @@ -21,8 +21,6 @@ */ package org.exist.xquery.value; -import net.sf.saxon.tree.util.FastStringBuffer; -import net.sf.saxon.value.FloatingPointConverter; import org.exist.xquery.ErrorCodes; import org.exist.xquery.Expression; import org.exist.xquery.XPathException; @@ -119,7 +117,7 @@ public String getStringValue() { } //Copied from Saxon 8.6.1 - final FastStringBuffer sb = new FastStringBuffer(32); + final StringBuilder sb = new StringBuilder(32); if (canonicalDuration.getSign() < 0) { sb.append('-'); } @@ -137,38 +135,10 @@ public String getStringValue() { sb.append(m + "M"); } if ((s.intValue() != 0) || (d == 0 && m == 0 && h == 0)) { - //TODO : ugly -> factorize - //sb.append(Integer.toString(s.intValue())); - //double ms = s.doubleValue() - s.intValue(); - //if (ms != 0.0) { - // sb.append("."); - // sb.append(Double.toString(ms).substring(2)); - //} - //0 is a dummy parameter - FloatingPointConverter.appendFloat(sb, s.floatValue(), false); + sb.append(net.sf.saxon.value.FloatValue.floatToString(s.floatValue())); sb.append("S"); - /* - if (micros == 0) { - sb.append(s + "S"); - } else { - long ms = (s * 1000000) + micros; - String mss = ms + ""; - if (s == 0) { - mss = "0000000" + mss; - mss = mss.substring(mss.length()-7); - } - sb.append(mss.substring(0, mss.length()-6)); - sb.append('.'); - int lastSigDigit = mss.length()-1; - while (mss.charAt(lastSigDigit) == '0') { - lastSigDigit--; - } - sb.append(mss.substring(mss.length()-6, lastSigDigit+1)); - sb.append('S'); - } - */ } - //End of copy + //End of copy return sb.toString(); } 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 e92f8c1d772..6b44c7b5151 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 @@ -22,8 +22,7 @@ package org.exist.xquery.value; import com.ibm.icu.text.Collator; -import net.sf.saxon.tree.util.FastStringBuffer; -import net.sf.saxon.value.FloatingPointConverter; + import org.exist.util.ByteConversion; import org.exist.xquery.Constants; import org.exist.xquery.ErrorCodes; @@ -94,10 +93,7 @@ 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(); + return net.sf.saxon.value.DoubleValue.doubleToString(value).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 78e3c6acb7a..9fc0607bc96 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 @@ -22,8 +22,6 @@ package org.exist.xquery.value; import com.ibm.icu.text.Collator; -import net.sf.saxon.tree.util.FastStringBuffer; -import net.sf.saxon.value.FloatingPointConverter; import org.exist.util.ByteConversion; import org.exist.xquery.Constants; import org.exist.xquery.ErrorCodes; @@ -116,10 +114,7 @@ public String getStringValue() throws XPathException { return s; */ - final FastStringBuffer sb = new FastStringBuffer(20); - //0 is a dummy parameter - FloatingPointConverter.appendFloat(sb, value, false); - return sb.toString(); + return net.sf.saxon.value.FloatValue.floatToString(value).toString(); } /* (non-Javadoc) 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 70d6a880f91..9d1d98bb358 100644 --- a/exist-core/src/main/java/org/exist/xslt/EXistDbXMLReader.java +++ b/exist-core/src/main/java/org/exist/xslt/EXistDbXMLReader.java @@ -90,9 +90,13 @@ public void parse(final InputSource input) { final Serializer serializer = source.getBroker().borrowSerializer(); try { - this.source = input; + this.source = input; this.contentHandler.setDocumentLocator(this); - serializer.setSAXHandlers(this.contentHandler, null); + // Filter out the implicit xml namespace that eXist's persistent DOM + // stores in its namespace mappings. Saxon 12 rejects SAX events + // declaring the XML namespace URI (http://www.w3.org/XML/1998/namespace). + final ContentHandler filtered = new org.exist.util.XMLBackwardsCompatHandler(this.contentHandler); + serializer.setSAXHandlers(filtered, null); serializer.toSAX(source.getDocument()); this.contentHandler.endDocument(); 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 4b90b81eac4..7bd5f0e82f3 100644 --- a/exist-core/src/main/java/org/exist/xslt/StylesheetResolverAndCompiler.java +++ b/exist-core/src/main/java/org/exist/xslt/StylesheetResolverAndCompiler.java @@ -39,6 +39,7 @@ import org.apache.logging.log4j.Logger; import org.exist.dom.persistent.DocumentImpl; import org.exist.dom.persistent.LockedDocument; +import org.xml.sax.ContentHandler; import org.exist.security.PermissionDeniedException; import org.exist.storage.BrokerPool; import org.exist.storage.DBBroker; @@ -145,12 +146,24 @@ private Templates compileTemplates( //factory.setURIResolver(new EXistURIResolver(broker, stylesheet.getCollection().getURI().toString())); final TemplatesHandler handler = factory(broker.getBrokerPool(), errorListener).newTemplatesHandler(); - handler.setSystemId(stylesheet.getBaseURI()); + // Use the full xmldb URI as the system ID so Saxon 12 can resolve + // relative xsl:import/xsl:include hrefs via the EXistURIResolver + final String baseURI = stylesheet.getBaseURI(); + if (baseURI != null && !baseURI.contains(":")) { + handler.setSystemId(XmldbURI.EMBEDDED_SERVER_URI_PREFIX + baseURI); + } else { + handler.setSystemId(baseURI); + } handler.startDocument(); + // Wrap the handler to suppress duplicate startDocument/endDocument events. + // Serializer.toSAX() may send its own doc events (depending on GENERATE_DOC_EVENTS), + // and Saxon 12 does not tolerate duplicate calls. + final ContentHandler guard = new org.exist.util.XMLBackwardsCompatHandler(handler); + final Serializer serializer = broker.borrowSerializer(); try { - serializer.setSAXHandlers(handler, null); + serializer.setSAXHandlers(guard, null); serializer.toSAX(stylesheet); } finally { broker.returnSerializer(serializer); 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 8be4a1dc654..34a10b73c9b 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 @@ -95,33 +95,26 @@ public void checkNamespaces_saxon() throws IOException, ParserConfigurationExcep final Element elem = doc.getDocumentElement(); final NamedNodeMap attrs = elem.getAttributes(); - assertEquals(3, attrs.getLength()); + // Saxon 12 no longer includes the implicit xml namespace declaration + 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 diff --git a/exist-core/src/test/xquery/xquery3/transform/fnTransform68.xqm b/exist-core/src/test/xquery/xquery3/transform/fnTransform68.xqm index 7686ee2bc4f..a72cfb57707 100644 --- a/exist-core/src/test/xquery/xquery3/transform/fnTransform68.xqm +++ b/exist-core/src/test/xquery/xquery3/transform/fnTransform68.xqm @@ -36,7 +36,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{ diff --git a/exist-parent/pom.xml b/exist-parent/pom.xml index 8a9e707cdb5..df619c6aa46 100644 --- a/exist-parent/pom.xml +++ b/exist-parent/pom.xml @@ -134,7 +134,7 @@ 1.8.1.3 1.8.1.3-jakarta-ee10 2.1.3 - 9.9.1-8 + 12.5 6.0.23 2.11.0 4.13.2 diff --git a/extensions/indexes/spatial/src/main/java/org/exist/indexing/spatial/AbstractGMLJDBCIndexWorker.java b/extensions/indexes/spatial/src/main/java/org/exist/indexing/spatial/AbstractGMLJDBCIndexWorker.java index 7228a15aca4..6cccd9d7ad0 100644 --- a/extensions/indexes/spatial/src/main/java/org/exist/indexing/spatial/AbstractGMLJDBCIndexWorker.java +++ b/extensions/indexes/spatial/src/main/java/org/exist/indexing/spatial/AbstractGMLJDBCIndexWorker.java @@ -632,7 +632,21 @@ public Element streamGeometryToElement(final Geometry geometry, final String srs //2) gmlPrefix //3) other stuff... //This will possibly require some changes in GeometryTransformer - gmlString = gmlTransformer.transform(geometry); + // Force the JDK's built-in TransformerFactory for GeoTools. + // Saxon 12's IdentityTransformer rejects SAXSources whose XMLReader + // does not support the lexical-handler property, which GeoTools' reader doesn't. + final String savedFactory = System.getProperty("javax.xml.transform.TransformerFactory"); + try { + System.setProperty("javax.xml.transform.TransformerFactory", + "com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl"); + gmlString = gmlTransformer.transform(geometry); + } finally { + if (savedFactory != null) { + System.setProperty("javax.xml.transform.TransformerFactory", savedFactory); + } else { + System.clearProperty("javax.xml.transform.TransformerFactory"); + } + } } catch (final TransformerException e) { throw new SpatialIndexException(e); } From 2e2d1e89b6e56931bac7f71d146454cac840b685 Mon Sep 17 00:00:00 2001 From: Joe Wicentowski Date: Wed, 27 May 2026 09:01:38 -0400 Subject: [PATCH 2/2] [refactor] Address PR #6212 review feedback from @reinhapa and @dizzzz @reinhapa: replace fully-qualified Saxon class names with imports where the class doesn't clash with an eXist class of the same name: - RewriteConfig.java -- import net.sf.saxon.trans.XPathException (file does not import any other XPathException) - FunAnalyzeString.java -- import net.sf.saxon.value.StringValue (file does not import any other StringValue) - EXistDbXMLReader.java + StylesheetResolverAndCompiler.java -- import org.exist.util.XMLBackwardsCompatHandler (eXist class, no clash) The remaining net.sf.saxon.trans.XPathException catch sites in FunReplace.java, FunMatches.java, and FunAnalyzeString.java still need the fully-qualified form because those files import org.exist.xquery.*, which brings org.exist.xquery.XPathException into scope. Replying inline on the PR with this rationale. @dizzzz: address the readability and reuse suggestions: - Add org.exist.util.SaxonConversions helper class with static doubleToString(double) and floatToString(float) methods. Updates DoubleValue and FloatValue to delegate via the helper, hiding the Saxon class reference behind a single point of contact. - Predefine StringView.of("") as a private static final EMPTY_STRING_VIEW in FunReplace.java and FunAnalyzeString.java; avoids per-call allocation of an empty StringView. - Bump the regex-syntax-version arg from "XP30" to "XP31" in compileRegularExpression calls (FunMatches, FunReplace, FunAnalyzeString). XP 3.1 was largely additive (maps); the regex surface is unchanged from XP 3.0. Test suite confirms no regressions. - XMLBackwardsCompatHandler.endDocument now logs at DEBUG when called without a preceding startDocument (spurious SAX event). Normal duplicate-suppression path stays silent. - StylesheetResolverAndCompiler.java -- reorder imports to put org.exist.* and org.xml.sax.* in their conventional alphabetical groupings. Verified: mvn install -pl exist-core -am builds cleanly, XQuery3Tests 1026/1026 pass. --- .../exist/http/urlrewrite/RewriteConfig.java | 3 +- .../java/org/exist/util/SaxonConversions.java | 60 +++++++++++++++++++ .../exist/util/XMLBackwardsCompatHandler.java | 13 +++- .../xquery/functions/fn/FunAnalyzeString.java | 14 +++-- .../exist/xquery/functions/fn/FunMatches.java | 4 +- .../exist/xquery/functions/fn/FunReplace.java | 12 ++-- .../org/exist/xquery/value/DoubleValue.java | 3 +- .../org/exist/xquery/value/FloatValue.java | 3 +- .../java/org/exist/xslt/EXistDbXMLReader.java | 3 +- .../xslt/StylesheetResolverAndCompiler.java | 5 +- 10 files changed, 102 insertions(+), 18 deletions(-) create mode 100644 exist-core/src/main/java/org/exist/util/SaxonConversions.java diff --git a/exist-core/src/main/java/org/exist/http/urlrewrite/RewriteConfig.java b/exist-core/src/main/java/org/exist/http/urlrewrite/RewriteConfig.java index ef457e2aa8e..745dd98d106 100644 --- a/exist-core/src/main/java/org/exist/http/urlrewrite/RewriteConfig.java +++ b/exist-core/src/main/java/org/exist/http/urlrewrite/RewriteConfig.java @@ -33,6 +33,7 @@ import org.exist.storage.lock.Lock.LockMode; import net.sf.saxon.regex.JavaRegularExpression; import net.sf.saxon.str.StringView; +import net.sf.saxon.trans.XPathException; import org.exist.util.XMLReaderPool; import org.exist.xmldb.XmldbURI; import org.exist.xquery.Constants; @@ -276,7 +277,7 @@ private Mapping(String regex, final URLRewrite action) throws ServletException { this.pattern = Pattern.compile(regex, 0); this.action = action; this.matcher = pattern.matcher(""); - } catch (final net.sf.saxon.trans.XPathException e) { + } catch (final XPathException e) { throw new ServletException("Syntax error in regular expression specified for path. " + e.getMessage(), e); } diff --git a/exist-core/src/main/java/org/exist/util/SaxonConversions.java b/exist-core/src/main/java/org/exist/util/SaxonConversions.java new file mode 100644 index 00000000000..c177a8de8c0 --- /dev/null +++ b/exist-core/src/main/java/org/exist/util/SaxonConversions.java @@ -0,0 +1,60 @@ +/* + * eXist-db Open Source Native XML Database + * Copyright (C) 2001 The eXist-db Authors + * + * info@exist-db.org + * http://www.exist-db.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.util; + +/** + * Conversion utilities that delegate to Saxon's value classes for spec-compliant + * lexical forms of XDM numeric types. + * + * Centralising these calls keeps Saxon class references out of eXist's own + * {@code DoubleValue} / {@code FloatValue}, where the class names collide with + * Saxon's identically-named classes. Call sites here use a single point of + * contact to the Saxon API, which makes future Saxon upgrades easier to audit. + */ +public final class SaxonConversions { + + private SaxonConversions() { + // utility class — not instantiable + } + + /** + * Convert a {@code double} to its XDM lexical form per F&O 3.1 §4.10.2, + * using Saxon's spec-compliant implementation. + * + * @param value the double to convert + * @return the XDM lexical form (e.g. {@code "NaN"}, {@code "-INF"}, {@code "1.5E2"}) + */ + public static String doubleToString(final double value) { + return net.sf.saxon.value.DoubleValue.doubleToString(value).toString(); + } + + /** + * Convert a {@code float} to its XDM lexical form per F&O 3.1 §4.10.2, + * using Saxon's spec-compliant implementation. + * + * @param value the float to convert + * @return the XDM lexical form + */ + public static String floatToString(final float value) { + return net.sf.saxon.value.FloatValue.floatToString(value).toString(); + } +} diff --git a/exist-core/src/main/java/org/exist/util/XMLBackwardsCompatHandler.java b/exist-core/src/main/java/org/exist/util/XMLBackwardsCompatHandler.java index 47e364d09cb..b63527fcfc9 100644 --- a/exist-core/src/main/java/org/exist/util/XMLBackwardsCompatHandler.java +++ b/exist-core/src/main/java/org/exist/util/XMLBackwardsCompatHandler.java @@ -21,6 +21,8 @@ */ package org.exist.util; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.xml.sax.Attributes; import org.xml.sax.ContentHandler; import org.xml.sax.Locator; @@ -34,6 +36,8 @@ */ public class XMLBackwardsCompatHandler implements ContentHandler { + private static final Logger LOG = LogManager.getLogger(XMLBackwardsCompatHandler.class); + private final ContentHandler delegate; private boolean documentStarted = false; @@ -51,7 +55,14 @@ public void startDocument() throws SAXException { @Override public void endDocument() throws SAXException { - // Suppress — the caller will call endDocument on the delegate directly + // Suppress — the caller will call endDocument on the delegate directly. + // If endDocument arrives before any startDocument, that's a spurious SAX event + // (a downstream issue rather than the duplicate-startDocument case this guard + // exists for) — log at debug level so it's visible during diagnosis without + // adding noise to normal operation. + if (!documentStarted && LOG.isDebugEnabled()) { + LOG.debug("endDocument received without a preceding startDocument; suppressing"); + } } @Override 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 ef2a3fc9871..d1c8055d30b 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 @@ -33,6 +33,7 @@ import net.sf.saxon.regex.RegularExpression; import net.sf.saxon.str.StringView; import net.sf.saxon.str.UnicodeString; +import net.sf.saxon.value.StringValue; import org.exist.dom.QName; import org.exist.dom.memtree.MemTreeBuilder; import org.exist.xquery.*; @@ -55,6 +56,9 @@ */ public class FunAnalyzeString extends BasicFunction { + /** Reused for empty-match detection — avoids per-call allocation of an empty StringView. */ + private static final UnicodeString EMPTY_STRING_VIEW = StringView.of(""); + private final static QName fnAnalyzeString = new QName("analyze-string", Function.BUILTIN_FUNCTION_NS); private final static QName QN_MATCH = new QName("match", Function.BUILTIN_FUNCTION_NS); @@ -133,15 +137,15 @@ private void analyzeString(final MemTreeBuilder builder, final String input, Str final List warnings = new ArrayList<>(1); try { - final RegularExpression regularExpression = config.compileRegularExpression(StringView.of(pattern), flags, "XP30", warnings); - if (regularExpression.matches(StringView.of(""))) { + final RegularExpression regularExpression = config.compileRegularExpression(StringView.of(pattern), flags, "XP31", warnings); + if (regularExpression.matches(EMPTY_STRING_VIEW)) { throw new XPathException(this, ErrorCodes.FORX0003, "regular expression could match empty string"); } //TODO(AR) cache the regular expression... might be possible through Saxon config final RegexIterator regexIterator = regularExpression.analyze(StringView.of(input)); - net.sf.saxon.value.StringValue item; + StringValue item; while ((item = regexIterator.next()) != null) { if (regexIterator.isMatching()) { match(builder, regexIterator); @@ -154,7 +158,7 @@ private void analyzeString(final MemTreeBuilder builder, final String input, Str LOG.warn(warning); } } catch (final net.sf.saxon.trans.XPathException e) { - // Saxon's XP30 regex translator rejects some valid patterns. + // Saxon's XP31 regex translator rejects some valid patterns. // Fall back to Java regex before giving up. if ("FORX0002".equals(e.getErrorCodeQName().getLocalPart())) { try { @@ -260,7 +264,7 @@ public void onGroupEnd(final int groupNumber) throws net.sf.saxon.trans.XPathExc builder.endElement(); } - private void nonMatch(final MemTreeBuilder builder, final net.sf.saxon.value.StringValue item) { + private void nonMatch(final MemTreeBuilder builder, final StringValue item) { builder.startElement(QN_NON_MATCH, null); 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 a6a9de84617..8ff848aba67 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 @@ -531,7 +531,7 @@ private boolean matchXmlRegex(final String string, final String pattern, final S List warnings = new ArrayList<>(1); RegularExpression regex = context.getBroker().getBrokerPool() .getSaxonConfiguration() - .compileRegularExpression(StringView.of(pattern), flags, "XP30", warnings); + .compileRegularExpression(StringView.of(pattern), flags, "XP31", warnings); for (final String warning : warnings) { LOG.warn(warning); @@ -540,7 +540,7 @@ private boolean matchXmlRegex(final String string, final String pattern, final S return regex.containsMatch(StringView.of(string)); } catch (final net.sf.saxon.trans.XPathException e) { - // Saxon's XP30 regex translator rejects some valid patterns: + // Saxon's XP31 regex translator rejects some valid patterns: // \b/\B word boundaries, certain quantifier sequences, \p{Is} names, etc. // Fall back to Java regex before giving up. if ("FORX0002".equals(e.getErrorCodeQName().getLocalPart())) { 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 93bfe85b6e0..1a824198b44 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 @@ -31,6 +31,7 @@ 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; @@ -47,6 +48,9 @@ */ public class FunReplace extends BasicFunction { + /** Reused for empty-match detection — avoids per-call allocation of an empty StringView. */ + private static final UnicodeString EMPTY_STRING_VIEW = StringView.of(""); + private static final QName FS_REPLACE_NAME = new QName("replace", Function.BUILTIN_FUNCTION_NS); private static final String FS_REPLACE_DESCRIPTION = @@ -137,8 +141,8 @@ public Sequence eval(final Sequence[] args, final Sequence contextSequence) thro final List warnings = new ArrayList<>(1); try { - final RegularExpression regularExpression = config.compileRegularExpression(StringView.of(pattern), flags, "XP30", warnings); - if (regularExpression.matches(StringView.of(""))) { + final RegularExpression regularExpression = config.compileRegularExpression(StringView.of(pattern), flags, "XP31", warnings); + if (regularExpression.matches(EMPTY_STRING_VIEW)) { throw new XPathException(this, ErrorCodes.FORX0003, "regular expression could match empty string"); } @@ -150,11 +154,11 @@ public Sequence eval(final Sequence[] args, final Sequence contextSequence) thro throw new XPathException(this, ErrorCodes.FORX0004, msg); } } - final net.sf.saxon.str.UnicodeString res = regularExpression.replace(StringView.of(string), StringView.of(replace)); + final UnicodeString res = regularExpression.replace(StringView.of(string), StringView.of(replace)); result = new StringValue(this, res.toString()); } catch (final net.sf.saxon.trans.XPathException e) { - // Saxon's XP30 regex translator rejects some valid patterns. + // Saxon's XP31 regex translator rejects some valid patterns. // Fall back to Java regex before giving up. if ("FORX0002".equals(e.getErrorCodeQName().getLocalPart())) { try { 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 6b44c7b5151..da0891e70b6 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 @@ -24,6 +24,7 @@ import com.ibm.icu.text.Collator; import org.exist.util.ByteConversion; +import org.exist.util.SaxonConversions; import org.exist.xquery.Constants; import org.exist.xquery.ErrorCodes; import org.exist.xquery.Expression; @@ -93,7 +94,7 @@ public int getType() { @Override public String getStringValue() { - return net.sf.saxon.value.DoubleValue.doubleToString(value).toString(); + return SaxonConversions.doubleToString(value); } 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 9fc0607bc96..3015ac4d01f 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 @@ -23,6 +23,7 @@ import com.ibm.icu.text.Collator; import org.exist.util.ByteConversion; +import org.exist.util.SaxonConversions; import org.exist.xquery.Constants; import org.exist.xquery.ErrorCodes; import org.exist.xquery.Expression; @@ -114,7 +115,7 @@ public String getStringValue() throws XPathException { return s; */ - return net.sf.saxon.value.FloatValue.floatToString(value).toString(); + return SaxonConversions.floatToString(value); } /* (non-Javadoc) 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 9d1d98bb358..8b6e41c3711 100644 --- a/exist-core/src/main/java/org/exist/xslt/EXistDbXMLReader.java +++ b/exist-core/src/main/java/org/exist/xslt/EXistDbXMLReader.java @@ -25,6 +25,7 @@ import org.apache.logging.log4j.Logger; import org.exist.storage.serializers.Serializer; +import org.exist.util.XMLBackwardsCompatHandler; import org.xml.sax.ContentHandler; import org.xml.sax.DTDHandler; @@ -95,7 +96,7 @@ public void parse(final InputSource input) { // Filter out the implicit xml namespace that eXist's persistent DOM // stores in its namespace mappings. Saxon 12 rejects SAX events // declaring the XML namespace URI (http://www.w3.org/XML/1998/namespace). - final ContentHandler filtered = new org.exist.util.XMLBackwardsCompatHandler(this.contentHandler); + final ContentHandler filtered = new XMLBackwardsCompatHandler(this.contentHandler); serializer.setSAXHandlers(filtered, null); serializer.toSAX(source.getDocument()); 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 7bd5f0e82f3..1bde96e8c78 100644 --- a/exist-core/src/main/java/org/exist/xslt/StylesheetResolverAndCompiler.java +++ b/exist-core/src/main/java/org/exist/xslt/StylesheetResolverAndCompiler.java @@ -39,14 +39,15 @@ import org.apache.logging.log4j.Logger; import org.exist.dom.persistent.DocumentImpl; import org.exist.dom.persistent.LockedDocument; -import org.xml.sax.ContentHandler; import org.exist.security.PermissionDeniedException; import org.exist.storage.BrokerPool; import org.exist.storage.DBBroker; import org.exist.storage.lock.Lock.LockMode; import org.exist.storage.serializers.Serializer; +import org.exist.util.XMLBackwardsCompatHandler; import org.exist.xmldb.XmldbURI; import org.exist.xquery.Constants; +import org.xml.sax.ContentHandler; import org.xml.sax.SAXException; import static org.exist.xslt.XsltURIResolverHelper.getXsltURIResolver; @@ -159,7 +160,7 @@ private Templates compileTemplates( // Wrap the handler to suppress duplicate startDocument/endDocument events. // Serializer.toSAX() may send its own doc events (depending on GENERATE_DOC_EVENTS), // and Saxon 12 does not tolerate duplicate calls. - final ContentHandler guard = new org.exist.util.XMLBackwardsCompatHandler(handler); + final ContentHandler guard = new XMLBackwardsCompatHandler(handler); final Serializer serializer = broker.borrowSerializer(); try {