From 8cfb84c7e2091b1e40b5fa15de7a4bcb91b9bde7 Mon Sep 17 00:00:00 2001 From: Joe Wicentowski Date: Sun, 15 Mar 2026 23:45:11 -0400 Subject: [PATCH] [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 | 24 ++-- .../exist/xquery/functions/fn/FunMatches.java | 7 +- .../exist/xquery/functions/fn/FunReplace.java | 11 +- .../functions/fn/transform/Transform.java | 15 ++- .../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, 227 insertions(+), 128 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 94ed898b780..372b1ecb01a 100644 --- a/exist-core/pom.xml +++ b/exist-core/pom.xml @@ -390,19 +390,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 c490a4dfaff..ec98af10fd1 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 6303fd67940..ace13810fd3 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; @@ -272,16 +271,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 e2f2166443b..27b5aaec44e 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 8fe035492a7..c6cce6bf13d 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 @@ -25,9 +25,11 @@ import java.util.List; 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.*; @@ -126,15 +128,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); @@ -147,7 +149,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()); @@ -158,10 +160,10 @@ private void analyzeString(final MemTreeBuilder builder, final String input, Str private void match(final MemTreeBuilder builder, final RegexIterator regexIterator) throws net.sf.saxon.trans.XPathException { builder.startElement(QN_MATCH, null); - regexIterator.processMatchingSubstring(new RegexIterator.MatchHandler() { + 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 @@ -180,9 +182,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 6f06bd772ce..962fd628458 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; @@ -517,16 +518,16 @@ 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) { - 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 6dea523469a..6bd745b3e65 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 @@ -27,6 +27,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; @@ -119,24 +120,24 @@ 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) { - 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 fe9d58239f8..accad4f74d2 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()) { @@ -365,7 +371,7 @@ 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 Source sourceIMS = new DOMSource((Document)item); xslt30Transformer.applyTemplates(sourceIMS, destination); } else { final XdmValue selection = toSaxon.of(initialMatchSelection); @@ -425,7 +431,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 cf960f108f2..3a38935556e 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; @@ -118,7 +116,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('-'); } @@ -136,38 +134,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 3cd6cd24094..e526c48ec0b 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; @@ -93,10 +92,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 6c67124e711..631287e43b3 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; @@ -115,10 +113,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 c485b63a05c..f720e0ae1bd 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 @@ -94,33 +94,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 4c9650a180c..8535c1ff1d4 100644 --- a/exist-parent/pom.xml +++ b/exist-parent/pom.xml @@ -121,7 +121,7 @@ 1.8.1.3 1.8.1.3-jakarta5 2.1.3 - 9.9.1-8 + 12.5 6.0.21 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); }