From 2955676d77def3c948f773d0f7627f12d21a55fa Mon Sep 17 00:00:00 2001 From: Laurettta Date: Thu, 7 May 2026 00:25:43 +0100 Subject: [PATCH] [bugfix] Fix relative xsl:import path resolution when stylesheet loaded from database Closes https://github.com/evolvedbinary/elemental/issues/169 When a stylesheet is loaded from the database as a document node, its base URI is stored as a bare path (e.g. /db/collection/a.xsl) with no scheme. The URI resolver did not recognise this as a database path and returned the relative href unchanged, causing xsl:import with a relative path to fail. The fix detects the bare path, extracts the directory, and reconstructs the full xmldb:exist:// URI so the import resolves correctly. --- .../functions/fn/transform/URIResolution.java | 7 ++- .../java/org/exist/xquery/TransformTest.java | 57 +++++++++++++++++++ .../fn/transform/FunTransformITTest.java | 54 ++++++++++++++++++ 3 files changed, 117 insertions(+), 1 deletion(-) diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/transform/URIResolution.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/transform/URIResolution.java index 5abbe73da1..9c637e427f 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/transform/URIResolution.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/transform/URIResolution.java @@ -56,8 +56,13 @@ static AnyURIValue resolveURI(final AnyURIValue relative, final AnyURIValue base if (relativeURI.isAbsolute()) { return relative; } - var baseURI = new URI(base.getStringValue() ); + var baseURI = new URI(base.getStringValue()); if (!baseURI.isAbsolute()) { + var basePath = base.getStringValue(); + if (basePath.startsWith("/")) { + var baseDir = basePath.substring(0, basePath.lastIndexOf('/') + 1); + return new AnyURIValue(XmldbURI.EMBEDDED_SERVER_URI_PREFIX + baseDir + relative.getStringValue()); + } return relative; } try { diff --git a/exist-core/src/test/java/org/exist/xquery/TransformTest.java b/exist-core/src/test/java/org/exist/xquery/TransformTest.java index c92b0c2739..6c01147374 100644 --- a/exist-core/src/test/java/org/exist/xquery/TransformTest.java +++ b/exist-core/src/test/java/org/exist/xquery/TransformTest.java @@ -95,6 +95,38 @@ public void transform() throws XMLDBException { "

End Template 1

" + "", result); } + + /** + * Tests that a stylesheet can import another stylesheet stored in the + * same database collection using a relative path (filename only, no directory). + * Stylesheet passed as a URI string. + */ + @Test + public void transformWithSameDirectoryImport() throws XMLDBException { + String query = + "import module namespace transform='http://exist-db.org/xquery/transform';\n" + + "let $xml := \n" + + "let $xsl := 'xmldb:exist:///db/"+TEST_COLLECTION_NAME+"/same-dir/a.xsl'\n" + + "return transform:transform($xml, $xsl, ())"; + String result = execQuery(query); + assertEquals("

From A

From B

", result); + } + + /** + * Tests that a stylesheet can import another stylesheet stored in the + * same database collection using a relative path (filename only, no directory). + * Stylesheet passed as a node via doc(). + */ + @Test + public void transformWithSameDirectoryImportViaNode() throws XMLDBException { + String query = + "import module namespace transform='http://exist-db.org/xquery/transform';\n" + + "let $xml := \n" + + "let $xsl := doc('xmldb:exist:///db/"+TEST_COLLECTION_NAME+"/same-dir/a.xsl')\n" + + "return transform:transform($xml, $xsl, ())"; + String result = execQuery(query); + assertEquals("

From A

From B

", result); + } private String execQuery(String query) throws XMLDBException { @@ -171,6 +203,31 @@ public void setUp() throws ClassNotFoundException, IllegalAccessException, Insta addXMLDocument(xsl1, doc1, "1.xsl"); addXMLDocument(xsl2, doc2, "2.xsl"); addXMLDocument(xsl3, doc3, "3.xsl"); + + service = + testCollection.getService( + CollectionManagementService.class); + + Collection sameDir = service.createCollection("same-dir"); + assertNotNull(sameDir); + + String docA = "\n" + + "\n"+ + "\n" + + "" + + "

From A

" + + "
" + + "
"; + + String docB = "\n" + + "\n"+ + "" + + "

From B

" + + "
" + + "
"; + + addXMLDocument(sameDir, docA, "a.xsl"); + addXMLDocument(sameDir, docB, "b.xsl"); } @After diff --git a/exist-core/src/test/java/org/exist/xquery/functions/fn/transform/FunTransformITTest.java b/exist-core/src/test/java/org/exist/xquery/functions/fn/transform/FunTransformITTest.java index 1ca74a5b9e..a737651d97 100644 --- a/exist-core/src/test/java/org/exist/xquery/functions/fn/transform/FunTransformITTest.java +++ b/exist-core/src/test/java/org/exist/xquery/functions/fn/transform/FunTransformITTest.java @@ -58,6 +58,35 @@ */ public class FunTransformITTest { + private static final XmldbURI TEST_IMPORT_XSLT_COLLECTION = XmldbURI.create("/db/fn-transform-import-test"); + private static final XmldbURI IMPORT_A_XSLT_NAME = XmldbURI.create("a.xsl"); + private static final XmldbURI IMPORT_B_XSLT_NAME = XmldbURI.create("b.xsl"); + + private static final String IMPORT_A_XSLT = + "\n" + + " \n" + + " \n" + + "

From A

\n" + + "
\n" + + "
"; + + private static final String IMPORT_B_XSLT = + "\n" + + "

From B

\n" + + "
"; + + private static final String SAME_DIR_IMPORT_QUERY = + "fn:transform(map {\n" + + " \"stylesheet-location\": \"xmldb:exist:///db/fn-transform-import-test/a.xsl\",\n" + + " \"source-node\": document { }\n" + + "})?output"; + + private static final String SAME_DIR_IMPORT_VIA_NODE_QUERY = + "fn:transform(map {\n" + + " \"stylesheet-node\": doc(\"xmldb:exist:///db/fn-transform-import-test/a.xsl\"),\n" + + " \"source-node\": document { }\n" + + "})?output"; + private static final XmldbURI TEST_IDENTITY_XSLT_COLLECTION = XmldbURI.create("/db/transform-identity-test"); private static final XmldbURI IDENTITY_XSLT_NAME = XmldbURI.create("xsl-identity.xslt"); @@ -143,6 +172,26 @@ public class FunTransformITTest { @ClassRule public static ExistEmbeddedServer existEmbeddedServer = new ExistEmbeddedServer(true, true); + /** + * Tests that fn:transform can resolve a relative xsl:import (same directory) + * when the stylesheet is given as a URI location. + */ + @Test + public void sameDirectoryImportViaLocation() throws XPathException, PermissionDeniedException, EXistException { + final Source expected = Input.fromString("

From A

From B

").build(); + expectQuery(SAME_DIR_IMPORT_QUERY, expected); + } + + /** + * Tests that fn:transform can resolve a relative xsl:import (same directory) + * when the stylesheet is given as a node loaded from the database. + */ + @Test + public void sameDirectoryImportViaNode() throws XPathException, PermissionDeniedException, EXistException { + final Source expected = Input.fromString("

From A

From B

").build(); + expectQuery(SAME_DIR_IMPORT_VIA_NODE_QUERY, expected); + } + @Test public void identityPersistentDom() throws XPathException, PermissionDeniedException, EXistException { final Source expected = Input.fromString(IDENTITY_XML).build(); @@ -212,6 +261,11 @@ public static void storeResources() throws EXistException, PermissionDeniedExcep Tuple(IDENTITY_XML_NAME, IDENTITY_XML) ); + createCollection(broker, transaction, TEST_IMPORT_XSLT_COLLECTION, + Tuple(IMPORT_A_XSLT_NAME, IMPORT_A_XSLT), + Tuple(IMPORT_B_XSLT_NAME, IMPORT_B_XSLT) + ); + transaction.commit(); } }