Skip to content

Commit e91e116

Browse files
committed
[bugfix] fn:transform include/import resolution
xmldb:exist: URIs were not being understood / resolved against correctly when looking for resources within the transformation code wrapping the Saxon transformer in our fn:transform implementation. Strictly they’re not valid URIs, which is what was the fundamental problem, so we need to add some special-case XMLDBURI handling. We also refactor just a little to pull the URI resolution code used by fn:transform into its own class in the fn:transform implementation package, and add unit tests for that extracted resolution.
1 parent 1385801 commit e91e116

File tree

5 files changed

+203
-51
lines changed

5 files changed

+203
-51
lines changed

exist-core/src/main/java/org/exist/util/SaxonConfiguration.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@
2222
package org.exist.util;
2323

2424
import net.jcip.annotations.ThreadSafe;
25+
import net.sf.saxon.lib.Feature;
2526
import net.sf.saxon.s9api.Processor;
2627
import net.sf.saxon.trans.XPathException;
2728
import org.apache.logging.log4j.LogManager;
2829
import org.apache.logging.log4j.Logger;
2930
import org.exist.storage.BrokerPool;
31+
import org.exist.xquery.functions.fn.transform.URIResolution;
3032

3133
import javax.xml.transform.stream.StreamSource;
3234
import java.io.IOException;
@@ -54,6 +56,7 @@ public final class SaxonConfiguration {
5456
private SaxonConfiguration(final net.sf.saxon.Configuration configuration) {
5557
this.configuration = configuration;
5658
this.processor = new Processor(configuration);
59+
//TODO (AP) We could further configure URI/Resource resolution for Saxon within eXist here
5760
}
5861

5962
/**

exist-core/src/main/java/org/exist/xquery/functions/fn/transform/Options.java

Lines changed: 3 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -507,41 +507,13 @@ private Source resolveStylesheetLocation(final String stylesheetLocation) throws
507507

508508
final URI uri = URI.create(stylesheetLocation);
509509
if (uri.isAbsolute()) {
510-
return resolvePossibleStylesheetLocation(stylesheetLocation);
510+
return URIResolution.resolveDocument(stylesheetLocation, context, fnTransform);
511511
} else {
512512
final AnyURIValue resolved = resolveURI(new AnyURIValue(stylesheetLocation), context.getBaseURI());
513-
return resolvePossibleStylesheetLocation(resolved.getStringValue());
513+
return URIResolution.resolveDocument(resolved.getStringValue(), context, fnTransform);
514514
}
515515
}
516516

517-
/**
518-
* Resolve an absolute stylesheet location
519-
*
520-
* @param location of the stylesheet
521-
* @return the resolved stylesheet as a source
522-
* @throws XPathException if the item does not exist, or is not a document
523-
*/
524-
private Source resolvePossibleStylesheetLocation(final String location) throws XPathException {
525-
526-
final Sequence document;
527-
try {
528-
document = DocUtils.getDocument(context, location);
529-
} catch (final PermissionDeniedException e) {
530-
throw new XPathException(fnTransform, ErrorCodes.FODC0002,
531-
"Can not access '" + location + "'" + e.getMessage());
532-
}
533-
if (document != null && document.hasOne() && Type.subTypeOf(document.getItemType(), Type.NODE)) {
534-
if (document instanceof NodeProxy proxy) {
535-
return new DOMSource(proxy.getNode());
536-
}
537-
else if (document.itemAt(0) instanceof Node node) {
538-
return new DOMSource(node);
539-
}
540-
}
541-
throw new XPathException(fnTransform, ErrorCodes.FODC0002,
542-
"Location '"+ location + "' returns an item which is not a document node");
543-
}
544-
545517
/**
546518
* URI resolution, the core should be the same as for fn:resolve-uri
547519
* @param relative URI to resolve
@@ -550,19 +522,11 @@ else if (document.itemAt(0) instanceof Node node) {
550522
* @throws XPathException if resolution is not possible
551523
*/
552524
private AnyURIValue resolveURI(final AnyURIValue relative, final AnyURIValue base) throws XPathException {
553-
final URI relativeURI;
554-
final URI baseURI;
555525
try {
556-
relativeURI = new URI(relative.getStringValue());
557-
baseURI = new URI(base.getStringValue() );
526+
return URIResolution.resolveURI(relative, base);
558527
} catch (final URISyntaxException e) {
559528
throw new XPathException(fnTransform, ErrorCodes.FORG0009, "unable to resolve a relative URI against a base URI in fn:transform(): " + e.getMessage(), null, e);
560529
}
561-
if (relativeURI.isAbsolute()) {
562-
return relative;
563-
} else {
564-
return new AnyURIValue(baseURI.resolve(relativeURI));
565-
}
566530
}
567531

568532
private XSLTVersion getXsltVersion(final Source xsltStylesheet) throws XPathException {

exist-core/src/main/java/org/exist/xquery/functions/fn/transform/Transform.java

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -214,22 +214,24 @@ private XsltExecutable compileExecutable(final Options options) throws XPathExce
214214
xsltCompiler.setParameter(new net.sf.saxon.s9api.QName(qKey.getPrefix(), qKey.getLocalPart()), value);
215215
}
216216

217-
// Take URI resolution into our own hands when there is no base
218-
xsltCompiler.setURIResolver((href, base) -> {
219-
try {
220-
final URI hrefURI = URI.create(href);
221-
if (options.resolvedStylesheetBaseURI.isEmpty() && !hrefURI.isAbsolute() && StringUtils.isEmpty(base)) {
222-
final XPathException resolutionException = new XPathException(fnTransform,
217+
xsltCompiler.setURIResolver(new URIResolution.CompileTimeURIResolver(context, fnTransform) {
218+
@Override public Source resolve(final String href, final String base) throws TransformerException {
219+
// Correct error from URI resolution when there is no base
220+
try {
221+
final URI hrefURI = URI.create(href);
222+
if (options.resolvedStylesheetBaseURI.isEmpty() && !hrefURI.isAbsolute() && StringUtils.isEmpty(base)) {
223+
final XPathException resolutionException = new XPathException(fnTransform,
223224
ErrorCodes.XTSE0165,
224225
"transform using a relative href, \n" +
225-
"using option stylesheet-text, but without stylesheet-base-uri");
226-
throw new TransformerException(resolutionException);
226+
"using option stylesheet-text, but without stylesheet-base-uri");
227+
throw new TransformerException(resolutionException);
228+
}
229+
} catch (final IllegalArgumentException e) {
230+
throw new TransformerException(e);
227231
}
228-
} catch (final IllegalArgumentException e) {
229-
throw new TransformerException(e);
232+
// Checked the special error case, defer to eXist resolution
233+
return super.resolve(href, base);
230234
}
231-
// Pass it back
232-
return null;
233235
});
234236

235237
try {
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/*
2+
* eXist-db Open Source Native XML Database
3+
* Copyright (C) 2001 The eXist-db Authors
4+
*
5+
6+
* http://www.exist-db.org
7+
*
8+
* This library is free software; you can redistribute it and/or
9+
* modify it under the terms of the GNU Lesser General Public
10+
* License as published by the Free Software Foundation; either
11+
* version 2.1 of the License, or (at your option) any later version.
12+
*
13+
* This library is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16+
* Lesser General Public License for more details.
17+
*
18+
* You should have received a copy of the GNU Lesser General Public
19+
* License along with this library; if not, write to the Free Software
20+
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21+
*/
22+
23+
package org.exist.xquery.functions.fn.transform;
24+
25+
import org.exist.dom.persistent.NodeProxy;
26+
import org.exist.security.PermissionDeniedException;
27+
import org.exist.xmldb.XmldbURI;
28+
import org.exist.xquery.ErrorCodes;
29+
import org.exist.xquery.Expression;
30+
import org.exist.xquery.XPathException;
31+
import org.exist.xquery.XQueryContext;
32+
import org.exist.xquery.util.DocUtils;
33+
import org.exist.xquery.value.AnyURIValue;
34+
import org.exist.xquery.value.Sequence;
35+
import org.exist.xquery.value.Type;
36+
import org.w3c.dom.Node;
37+
38+
import javax.xml.transform.Source;
39+
import javax.xml.transform.TransformerException;
40+
import javax.xml.transform.URIResolver;
41+
import javax.xml.transform.dom.DOMSource;
42+
import java.net.URI;
43+
import java.net.URISyntaxException;
44+
45+
public class URIResolution {
46+
47+
/**
48+
* URI resolution, the core should be the same as for fn:resolve-uri
49+
* @param relative URI to resolve
50+
* @param base to resolve against
51+
* @return resolved URI
52+
* @throws URISyntaxException if resolution is not possible
53+
*/
54+
static AnyURIValue resolveURI(final AnyURIValue relative, final AnyURIValue base) throws URISyntaxException {
55+
var relativeURI = new URI(relative.getStringValue());
56+
if (relativeURI.isAbsolute()) {
57+
return relative;
58+
}
59+
var baseURI = new URI(base.getStringValue() );
60+
try {
61+
var xBase = XmldbURI.xmldbUriFor(baseURI);
62+
var resolved = xBase.getURI().resolve(relativeURI);
63+
return new AnyURIValue(XmldbURI.XMLDB_URI_PREFIX + resolved);
64+
} catch (URISyntaxException e) {
65+
return new AnyURIValue(baseURI.resolve(relativeURI));
66+
} catch (XPathException e) {
67+
throw new RuntimeException(e);
68+
}
69+
}
70+
71+
static public class CompileTimeURIResolver implements URIResolver {
72+
73+
private final XQueryContext xQueryContext;
74+
private final Expression containingExpression;
75+
76+
public CompileTimeURIResolver(XQueryContext xQueryContext, Expression containingExpression) {
77+
this.xQueryContext = xQueryContext;
78+
this.containingExpression = containingExpression;
79+
}
80+
81+
@Override
82+
public Source resolve(final String href, final String base) throws TransformerException {
83+
84+
try {
85+
final AnyURIValue baseURI = new AnyURIValue(base);
86+
final AnyURIValue hrefURI = new AnyURIValue(href);
87+
var resolved = resolveURI(hrefURI, baseURI);
88+
return resolveDocument(resolved.getStringValue(), xQueryContext, containingExpression);
89+
} catch (URISyntaxException e) {
90+
throw new TransformerException("Failed to resolve " +
91+
href +
92+
" against " +
93+
base, e);
94+
} catch (org.exist.xquery.XPathException e) {
95+
throw new TransformerException("Failed to find document as result of resolving " +
96+
href +
97+
" against " +
98+
base, e);
99+
}
100+
}
101+
}
102+
103+
/**
104+
* Resolve an absolute document location, stylesheet or included source
105+
*
106+
* @param location of the stylesheet
107+
* @return the resolved stylesheet as a source
108+
* @throws org.exist.xquery.XPathException if the item does not exist, or is not a document
109+
*/
110+
static Source resolveDocument(final String location, final XQueryContext xQueryContext, Expression containingExpression) throws org.exist.xquery.XPathException {
111+
112+
final Sequence document;
113+
try {
114+
document = DocUtils.getDocument(xQueryContext, location);
115+
} catch (final PermissionDeniedException e) {
116+
throw new org.exist.xquery.XPathException(containingExpression, ErrorCodes.FODC0002,
117+
"Can not access '" + location + "'" + e.getMessage());
118+
}
119+
if (document == null || document.isEmpty()) {
120+
throw new org.exist.xquery.XPathException(containingExpression, ErrorCodes.FODC0002,
121+
"No document found at location '"+ location);
122+
}
123+
if (document.hasOne() && Type.subTypeOf(document.getItemType(), Type.NODE)) {
124+
if (document instanceof NodeProxy proxy) {
125+
return new DOMSource(proxy.getNode());
126+
}
127+
else if (document.itemAt(0) instanceof Node node) {
128+
return new DOMSource(node);
129+
}
130+
}
131+
throw new org.exist.xquery.XPathException(containingExpression, ErrorCodes.FODC0002,
132+
"Location '"+ location + "' returns an item which is not a document node");
133+
}
134+
}

exist-core/src/test/java/org/exist/xquery/functions/fn/transform/FunTransformTest.java

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,12 @@
2121
*/
2222
package org.exist.xquery.functions.fn.transform;
2323

24+
import org.exist.xquery.XPathException;
25+
import org.exist.xquery.value.AnyURIValue;
2426
import org.junit.jupiter.api.Test;
2527

2628
import java.math.BigDecimal;
29+
import java.net.URISyntaxException;
2730

2831
import static org.junit.jupiter.api.Assertions.*;
2932

@@ -52,4 +55,50 @@ void versionNumbers() throws Transform.PendingException {
5255
Options.XSLTVersion version311 = Options.XSLTVersion.fromDecimal(new BigDecimal("3.11"));
5356
});
5457
}
58+
59+
@Test public void resolution() throws XPathException, URISyntaxException {
60+
var base = new AnyURIValue("xmldb:exist:///db/apps/fn_transform/tei-toc2.xsl");
61+
var relative = new AnyURIValue("functions1.xsl");
62+
assertEquals(new AnyURIValue("xmldb:exist:/db/apps/fn_transform/functions1.xsl"),
63+
URIResolution.resolveURI(relative, base));
64+
65+
var base1_5 = new AnyURIValue("xmldb:exist:///db/apps/fn_transform/tei-toc2.xsl");
66+
var relative1_5 = new AnyURIValue("/functions1.xsl");
67+
assertEquals(new AnyURIValue("xmldb:exist:/functions1.xsl"),
68+
URIResolution.resolveURI(relative1_5, base1_5));
69+
70+
var base1_10 = new AnyURIValue("xmldb:exist:///db/apps/fn_transform/tei-toc2.xsl");
71+
var relative1_10 = new AnyURIValue("/fn_transform/functions1.xsl");
72+
assertEquals(new AnyURIValue("xmldb:exist:/fn_transform/functions1.xsl"),
73+
URIResolution.resolveURI(relative1_10, base1_10));
74+
75+
var base2 = new AnyURIValue("xmldb:exist:/db/apps/fn_transform/tei-toc2.xsl");
76+
assertEquals(new AnyURIValue("xmldb:exist:/db/apps/fn_transform/functions1.xsl"),
77+
URIResolution.resolveURI(relative, base2));
78+
79+
var base3 = new AnyURIValue("https://127.0.0.1:8088/db/apps/fn_transform/tei-toc2.xsl");
80+
var relative3 = new AnyURIValue("functions1.xsl");
81+
assertEquals(new AnyURIValue("https://127.0.0.1:8088/db/apps/fn_transform/functions1.xsl"),
82+
URIResolution.resolveURI(relative3, base3));
83+
84+
var base3_5 = new AnyURIValue("https://127.0.0.1:8088/db/apps/fn_transform/");
85+
var relative3_5 = new AnyURIValue("functions1.xsl");
86+
assertEquals(new AnyURIValue("https://127.0.0.1:8088/db/apps/fn_transform/functions1.xsl"),
87+
URIResolution.resolveURI(relative3_5, base3_5));
88+
89+
var base3_10 = new AnyURIValue("https://127.0.0.1:8088/db/apps/fn_transform/");
90+
var relative3_10 = new AnyURIValue("/functions1.xsl");
91+
assertEquals(new AnyURIValue("https://127.0.0.1:8088/functions1.xsl"),
92+
URIResolution.resolveURI(relative3_10, base3_10));
93+
94+
var base4 = new AnyURIValue("xmldb:exist:///db/apps/fn_transform/tei-toc2.xsl");
95+
var relative4 = new AnyURIValue("xmldb:exist:///a/b/c/functions1.xsl");
96+
assertEquals(new AnyURIValue("xmldb:exist:///a/b/c/functions1.xsl"),
97+
URIResolution.resolveURI(relative4, base4));
98+
99+
var base5 = new AnyURIValue("xmldb:exist:///db/apps/fn_transform/tei-toc2.xsl");
100+
var relative5 = new AnyURIValue("https://127.0.0.1:8088/a/b/c/functions1.xsl");
101+
assertEquals(new AnyURIValue("https://127.0.0.1:8088/a/b/c/functions1.xsl"),
102+
URIResolution.resolveURI(relative5, base5));
103+
}
55104
}

0 commit comments

Comments
 (0)