diff --git a/exist-core/src/main/java/org/exist/http/RESTServer.java b/exist-core/src/main/java/org/exist/http/RESTServer.java
index 03f7e290ca8..e5657f9af4f 100644
--- a/exist-core/src/main/java/org/exist/http/RESTServer.java
+++ b/exist-core/src/main/java/org/exist/http/RESTServer.java
@@ -21,6 +21,8 @@
*/
package org.exist.http;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.exist.EXistException;
@@ -84,8 +86,6 @@
import org.xml.sax.helpers.AttributesImpl;
import org.xml.sax.helpers.XMLFilterImpl;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
import javax.xml.XMLConstants;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.stream.XMLStreamException;
@@ -96,6 +96,8 @@
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Field;
+import java.net.URI;
+import java.net.URISyntaxException;
import java.util.Properties;
import java.util.*;
import java.util.function.BiFunction;
@@ -1374,7 +1376,7 @@ protected void search(final DBBroker broker, final Txn transaction, final String
}
context.setStaticallyKnownDocuments(new XmldbURI[]{pathUri});
- context.setBaseURI(new AnyURIValue(pathUri.toString()));
+ context.setBaseURI(BaseURI.dbBaseURIFromLocation(pathUri.getURI()));
declareNamespaces(context, namespaces);
declareVariables(context, variables, request, response);
@@ -1559,6 +1561,12 @@ private void executeXQuery(final DBBroker broker, final Txn transaction, final D
context.prepareForReuse();
}
+ try {
+ context.setBaseURI(BaseURI.dbBaseURIFromLocation(new URI(servletPath)));
+ } catch (URISyntaxException e) {
+ LOG.warn("Path {} is not valid for forming a base URI, base URI not set.", servletPath);
+ }
+
// TODO: don't hardcode this?
context.setModuleLoadPath(
XmldbURI.EMBEDDED_SERVER_URI.append(
diff --git a/exist-core/src/main/java/org/exist/http/servlets/XQueryServlet.java b/exist-core/src/main/java/org/exist/http/servlets/XQueryServlet.java
index cd5c3d4c651..efc77603379 100644
--- a/exist-core/src/main/java/org/exist/http/servlets/XQueryServlet.java
+++ b/exist-core/src/main/java/org/exist/http/servlets/XQueryServlet.java
@@ -21,6 +21,12 @@
*/
package org.exist.http.servlets;
+import jakarta.servlet.ServletConfig;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletOutputStream;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.exist.EXistException;
@@ -39,19 +45,15 @@
import org.exist.util.serializer.XQuerySerializer;
import org.exist.xmldb.XmldbURI;
import org.exist.xquery.*;
+import org.exist.xquery.value.AnyURIValue;
import org.exist.xquery.value.Item;
import org.exist.xquery.value.Sequence;
-import jakarta.servlet.ServletConfig;
-import jakarta.servlet.ServletException;
-import jakarta.servlet.ServletOutputStream;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
-import jakarta.servlet.http.HttpSession;
import javax.xml.transform.OutputKeys;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
+import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.nio.file.Files;
@@ -435,6 +437,7 @@ protected void process(HttpServletRequest request, HttpServletResponse response)
if (query==null) {
context = new XQueryContext(getPool());
context.setModuleLoadPath(moduleLoadPath);
+ context.setBaseURI(new AnyURIValue(new URI("file:///").resolve(path).toString()));
try {
query = xquery.compile(context, source);
@@ -446,9 +449,11 @@ protected void process(HttpServletRequest request, HttpServletResponse response)
}
} else {
- context = query.getContext();
- context.setModuleLoadPath(moduleLoadPath);
- context.prepareForReuse();
+ context = query.getContext();
+
+ context.setModuleLoadPath(moduleLoadPath);
+ context.setBaseURI(new AnyURIValue(new URI("file:///").resolve(path).toString()));
+ context.prepareForReuse();
}
final Properties outputProperties = new Properties();
diff --git a/exist-core/src/main/java/org/exist/util/BaseURI.java b/exist-core/src/main/java/org/exist/util/BaseURI.java
new file mode 100644
index 00000000000..d6c641bdf32
--- /dev/null
+++ b/exist-core/src/main/java/org/exist/util/BaseURI.java
@@ -0,0 +1,54 @@
+/*
+ * 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.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.exist.xmldb.XmldbURI;
+import org.exist.xquery.XPathException;
+import org.exist.xquery.value.AnyURIValue;
+
+import java.net.URI;
+
+public class BaseURI {
+
+ private final static Logger LOG = LogManager.getLogger(BaseURI.class);
+
+ /**
+ * Convert the location of a resource function into an XML database URI
+ * @param uri the location of the resource function
+ * @return the input uri with an xmldb:exist:// prefix (if it had no scheme before)
+ * if uri has a scheme (is absolute) the original uri is wrapped, unaltered
+ */
+ public static AnyURIValue dbBaseURIFromLocation(final URI uri) {
+ if (uri.getScheme() == null) {
+ try {
+ return new AnyURIValue(XmldbURI.EMBEDDED_SERVER_URI_PREFIX + uri);
+ } catch (XPathException e) {
+ LOG.warn("Could not create {} URI from {}", XmldbURI.XMLDB_URI_PREFIX, uri);
+ throw new RuntimeException(e);
+ }
+ }
+ // already absolute
+ return new AnyURIValue(uri);
+ }
+}
diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunResolveURI.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunResolveURI.java
index 46a50aa77ab..35f61e656bf 100644
--- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunResolveURI.java
+++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunResolveURI.java
@@ -21,25 +21,9 @@
*/
package org.exist.xquery.functions.fn;
-import java.net.URI;
-import java.net.URISyntaxException;
-
import org.exist.dom.QName;
-import org.exist.xquery.Cardinality;
-import org.exist.xquery.Dependency;
-import org.exist.xquery.ErrorCodes;
-import org.exist.xquery.Function;
-import org.exist.xquery.FunctionSignature;
-import org.exist.xquery.Profiler;
-import org.exist.xquery.XPathException;
-import org.exist.xquery.XQueryContext;
-import org.exist.xquery.value.AnyURIValue;
-import org.exist.xquery.value.FunctionParameterSequenceType;
-import org.exist.xquery.value.FunctionReturnSequenceType;
-import org.exist.xquery.value.Item;
-import org.exist.xquery.value.Sequence;
-import org.exist.xquery.value.SequenceType;
-import org.exist.xquery.value.Type;
+import org.exist.xquery.*;
+import org.exist.xquery.value.*;
/**
* Implements the fn:resolve-uri() function.
@@ -131,20 +115,12 @@ public Sequence eval(Sequence contextSequence, Item contextItem) throws XPathExc
relative = (AnyURIValue)item;
} catch (final XPathException e) {
throw new XPathException(this, ErrorCodes.FORG0002, "invalid argument to fn:resolve-uri(): " + e.getMessage(), seq, e);
- }
- URI relativeURI;
- URI baseURI;
+ }
try {
- relativeURI = new URI(relative.getStringValue());
- baseURI = new URI(base.getStringValue() );
- } catch (final URISyntaxException e) {
+ return base.resolve(relative);
+ } catch (XPathException e) {
throw new XPathException(this, ErrorCodes.FORG0009, "unable to resolve a relative URI against a base URI in fn:resolve-uri(): " + e.getMessage(), null, e);
}
- if (relativeURI.isAbsolute()) {
- result = relative;
- } else {
- result = new AnyURIValue(this, baseURI.resolve(relativeURI));
- }
}
if (context.getProfiler().isEnabled())
diff --git a/exist-core/src/main/java/org/exist/xquery/value/AnyURIValue.java b/exist-core/src/main/java/org/exist/xquery/value/AnyURIValue.java
index ae0e070c5fd..d84bb94b712 100644
--- a/exist-core/src/main/java/org/exist/xquery/value/AnyURIValue.java
+++ b/exist-core/src/main/java/org/exist/xquery/value/AnyURIValue.java
@@ -34,6 +34,7 @@
import java.net.URISyntaxException;
import java.net.URL;
import java.util.BitSet;
+import java.util.regex.Pattern;
/**
* @author Wolfgang Meier
@@ -434,6 +435,56 @@ public URI toURI() throws XPathException {
}
}
+ final Pattern nonstandardScheme = Pattern.compile("(\\w+:)(\\w+:)?");
+
+ /**
+ * Resolution of AnyURI which resolves non-standard XmldbURI-style URIs
+ * as well as the URIs the rest of the world understands.
+ *
+ * @param relativeURI to resolve using this as the base
+ * @return a resolved AnyURI by the rules of resolution in
+ * {@link ...}
+ *
+ * @throws XPathException if the URIs are not valid or resolvable
+ */
+ public AnyURIValue resolve(AnyURIValue relativeURI) throws XPathException {
+ try {
+ var base = escape(uri);
+ var relative = new URI(escape(relativeURI.uri));
+ if (relative.isAbsolute()) {
+ return relativeURI;
+ }
+ var matcher = nonstandardScheme.matcher(base);
+ if (matcher.find(0) && matcher.groupCount() > 1 && matcher.group(2) != null) {
+ // rewrite non-standard URIs for resolution:
+ // "xmldb:exist:" --> "xmldb:"
+ // "https:" is unchanged
+ var compliantBase = matcher.group(1) + base.substring(matcher.end(0));
+ var resolved = escape(new URI(compliantBase).resolve(relative).toString());
+ var resolvedMatcher = nonstandardScheme.matcher(resolved);
+ if (!resolvedMatcher.find(0)) {
+ throw new XPathException(getExpression(), ErrorCodes.FORG0009,
+ "Failed to resolve " + relativeURI + " against " + this +
+ ", resolved string " + resolved + " did not have the expected pattern " +
+ nonstandardScheme);
+ }
+ if (matcher.group().startsWith(resolvedMatcher.group())) {
+ // reverse the rewrite
+ // "xmldb:" --> "xmldb:exist:"
+ resolved = matcher.group() + resolved.substring(resolvedMatcher.end(0));
+ }
+ return new AnyURIValue(resolved);
+ }
+
+ return new AnyURIValue(escape(new URI(base).resolve(relative).toString()));
+
+ } catch (URISyntaxException e) {
+ throw new XPathException(getExpression(),
+ ErrorCodes.FORG0009,
+ "Failed to resolve " + relativeURI + " against " + this, e);
+ }
+ }
+
@Override
public int hashCode() {
return uri.hashCode();
diff --git a/exist-core/src/test/xquery/xquery3/fnresolveUri.xqm b/exist-core/src/test/xquery/xquery3/fnresolveUri.xqm
new file mode 100644
index 00000000000..28f3eb2a7b1
--- /dev/null
+++ b/exist-core/src/test/xquery/xquery3/fnresolveUri.xqm
@@ -0,0 +1,122 @@
+(:
+ : 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
+ :)
+xquery version "3.1";
+
+module namespace testResolveURI="http://exist-db.org/xquery/test/fnResolveURI";
+
+declare namespace test="http://exist-db.org/xquery/xqsuite";
+
+declare
+ %test:assertEquals("https:/one/two/boo")
+function testResolveURI:resolve-path() {
+ fn:resolve-uri("boo", "https:///one/two/three")
+};
+
+declare
+ %test:assertEquals("https:/one/two/three#boo")
+function testResolveURI:resolve-frag() {
+ fn:resolve-uri("#boo", "https:///one/two/three")
+};
+
+declare
+ %test:assertEquals("https://host.com#boo")
+function testResolveURI:resolve-frag-only() {
+ fn:resolve-uri("#boo", "https://host.com")
+};
+
+declare
+ %test:assertError("err:FORG0002") (: non-existent path, invalid arg :)
+function testResolveURI:resolve-frag-only-nopath() {
+ fn:resolve-uri("#boo", "https://")
+};
+
+declare
+ %test:assertEquals("https:/#boo") (: why does the // disappear ? :)
+function testResolveURI:resolve-frag-only-abspath() {
+ fn:resolve-uri("#boo", "https:///")
+};
+
+declare
+ %test:assertEquals("https://host.com/#boo")
+function testResolveURI:resolve-frag-only2() {
+ fn:resolve-uri("#boo", "https://host.com/")
+};
+
+declare
+ %test:assertEquals("https:/one/two/four#boo")
+function testResolveURI:resolve-path-frag() {
+ fn:resolve-uri("four#boo", "https:///one/two/three")
+};
+
+declare
+ %test:assertEquals("https:/one/two/three/four#boo")
+function testResolveURI:resolve-path-frag2() {
+ fn:resolve-uri("four#boo", "https:///one/two/three/")
+};
+
+declare
+ %test:assertEquals("https://one/two/boo")
+function testResolveURI:resolve-rel() {
+ fn:resolve-uri("boo", "https://one/two/three")
+};
+
+declare
+ %test:assertEquals("https://one/two/boo.xml")
+function testResolveURI:resolve-rel-file() {
+ fn:resolve-uri("boo.xml", "https://one/two/three")
+};
+
+declare
+ %test:assertEquals("xmldb:exist://one/two/boo.xml")
+function testResolveURI:resolve-rel-file-x() {
+ fn:resolve-uri("boo.xml", "xmldb:exist://one/two/three")
+};
+
+declare
+ %test:assertEquals("https://alpha/beta/gamma")
+function testResolveURI:resolve-abs() {
+ fn:resolve-uri("https://alpha/beta/gamma", "xmldb:exist://one/two/three")
+};
+
+declare
+ %test:assertEquals("xmldb:exist://alpha/beta/gamma")
+function testResolveURI:resolve-abs2() {
+ fn:resolve-uri("xmldb:exist://alpha/beta/gamma", "https://one/two/three")
+};
+
+declare
+ %test:assertEquals("xmldb://alpha/beta/gamma")
+function testResolveURI:resolve-abs3() {
+ fn:resolve-uri("xmldb://alpha/beta/gamma", "xmldb:exist://one/two/three")
+};
+
+declare
+ %test:assertEquals("exist://alpha/beta/gamma")
+function testResolveURI:resolve-abs4() {
+ fn:resolve-uri("exist://alpha/beta/gamma", "xmldb:exist://one/two/three")
+};
+
+declare
+ %test:assertEquals("//alpha/beta/gamma")
+function testResolveURI:resolve-rel-rel() {
+ fn:resolve-uri("//alpha/beta/gamma", "//one/two/three")
+};
diff --git a/extensions/exquery/restxq/pom.xml b/extensions/exquery/restxq/pom.xml
index e1ab8b81232..440cfcc404f 100644
--- a/extensions/exquery/restxq/pom.xml
+++ b/extensions/exquery/restxq/pom.xml
@@ -199,6 +199,28 @@
${project.version}
test
+
+ org.junit.jupiter
+ junit-jupiter-params
+ test
+
+
+ org.assertj
+ assertj-core
+ test
+
+
+
+
+ org.eclipse.jetty
+ jetty-deploy
+ test
+
+
+ org.eclipse.jetty
+ jetty-jmx
+ test
+
@@ -252,6 +274,18 @@
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ ${project.basedir}/../../../exist-jetty-config/target/classes/org/exist/jetty
+ ${project.build.testOutputDirectory}/conf.xml
+ ${project.build.testOutputDirectory}/standalone-webapp
+ ${project.build.testOutputDirectory}/log4j2.xml
+
+
+
diff --git a/extensions/exquery/restxq/src/main/java/org/exist/extensions/exquery/restxq/impl/ResourceFunctionExecutorImpl.java b/extensions/exquery/restxq/src/main/java/org/exist/extensions/exquery/restxq/impl/ResourceFunctionExecutorImpl.java
index c73040c42dd..0ecdb64b809 100644
--- a/extensions/exquery/restxq/src/main/java/org/exist/extensions/exquery/restxq/impl/ResourceFunctionExecutorImpl.java
+++ b/extensions/exquery/restxq/src/main/java/org/exist/extensions/exquery/restxq/impl/ResourceFunctionExecutorImpl.java
@@ -82,6 +82,8 @@
import org.exquery.xquery.TypedValue;
import org.exquery.xquery3.FunctionSignature;
+import static org.exist.util.BaseURI.dbBaseURIFromLocation;
+
/**
*
* @author Adam Retter
@@ -133,6 +135,10 @@ public Sequence execute(final ResourceFunction resourceFunction, final Iterable<
//set the request object - can later be used by the EXQuery Request Module
xqueryContext.setAttribute(EXQ_REQUEST_ATTR, request);
+
+ //the base URI is the location of the function in the DB
+ var baseURI = dbBaseURIFromLocation(resourceFunction.getXQueryLocation());
+ xqueryContext.setBaseURI(baseURI);
//TODO this is a workaround?
declareVariables(xqueryContext);
diff --git a/extensions/exquery/restxq/src/test/java/org/exist/extensions/exquery/restxq/impl/BaseURITest.java b/extensions/exquery/restxq/src/test/java/org/exist/extensions/exquery/restxq/impl/BaseURITest.java
new file mode 100644
index 00000000000..671c23a8ded
--- /dev/null
+++ b/extensions/exquery/restxq/src/test/java/org/exist/extensions/exquery/restxq/impl/BaseURITest.java
@@ -0,0 +1,414 @@
+/*
+ * Copyright © 2001, Adam Retter
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the name of the nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.exist.extensions.exquery.restxq.impl;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.client.fluent.Executor;
+import org.apache.http.client.fluent.Request;
+import org.apache.http.entity.ContentType;
+import org.apache.http.message.BasicHeader;
+import org.apache.xmlrpc.XmlRpcException;
+import org.apache.xmlrpc.client.XmlRpcClient;
+import org.apache.xmlrpc.client.XmlRpcClientConfigImpl;
+import org.exist.TestUtils;
+import org.exist.collections.CollectionConfiguration;
+import org.exist.test.ExistWebServer;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.xml.sax.SAXException;
+
+import javax.xml.parsers.ParserConfigurationException;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.http.HttpStatus.SC_CREATED;
+import static org.apache.http.HttpStatus.SC_OK;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+public class BaseURITest {
+
+ private static final String COLLECTION_CONFIG = """
+
+
+
+
+
+
+ """;
+
+ private static String TEST_COLLECTION = "/db/restxq/integration-test";
+
+ private static ContentType XQUERY_CONTENT_TYPE = ContentType.create("application/xquery", "UTF-8");
+
+ private static String XQUERY_MEDIA_FILENAME = "restxq-tests-media.xqm";
+
+ private static final String XQUERY_MEDIA_BODY =
+ """
+ xquery version "3.0";
+
+ module namespace mod1 = "http://mod1";
+
+ declare namespace output = "https://www.w3.org/2010/xslt-xquery-serialization";
+
+ declare
+ %rest:GET
+ %rest:path("/media-type-json1")
+ %rest:produces("application/json")
+ function mod1:media-type-json1() {
+
+ };
+ """;
+
+ private static String XQUERY_BASE_URI_FILENAME = "restxq-tests-base-uri.xqm";
+
+ private static final String XQUERY_BASE_URI_BODY =
+ """
+ xquery version "3.1";
+
+ module namespace ex = "http://example/restxq/1";
+ import module namespace rest = "http://exquery.org/ns/restxq";
+
+ declare
+ %rest:GET
+ %rest:path("/base-uri")
+ function ex:base-uri-using-restxq() {
+
+ {static-base-uri()}
+ { exists(static-base-uri()) }
+ { resolve-uri('#foobaz', static-base-uri() ) }
+ { resolve-uri('#foobar') }
+ { resolve-uri('path/to/file#foobar') }
+
+ };
+ """;
+
+ private static final String XMLRPC_BASE_URI_BODY =
+ """
+
+ {static-base-uri()}
+ { exists(static-base-uri()) }
+ { resolve-uri('#foobar') }
+ { resolve-uri('path/to/file#foobar') }
+
+ """;
+
+ private static Executor executor = null;
+
+ @ClassRule
+ public static ExistWebServer existWebServer = new ExistWebServer(true, false, true, true);
+
+ private static String getServerUri() {
+ return "http://localhost:" + existWebServer.getPort();
+ }
+
+ private static String getRestUri() {
+ return getServerUri() + "/rest";
+ }
+
+ private static String getRestXqUri() {
+ return getServerUri() + "/restxq";
+ }
+
+ private static String getRestServerUri() {
+ return getServerUri() + "/rest";
+ }
+
+ private static String getXmlRpcUri() {
+ return getServerUri() + "/xmlrpc";
+ }
+
+ /**
+ * Upload RESTXQ resource functions prior to tests using them
+ *
+ * @throws IOException
+ */
+ @BeforeClass
+ public static void storeResourceFunctions() throws IOException {
+ executor = Executor.newInstance()
+ .auth(TestUtils.ADMIN_DB_USER, TestUtils.ADMIN_DB_PWD)
+ .authPreemptive(new HttpHost("localhost", existWebServer.getPort()));
+
+ HttpResponse response = null;
+
+ response = executor.execute(Request
+ .Put(getRestUri() + "/db/system/config" + TEST_COLLECTION + "/" + CollectionConfiguration.DEFAULT_COLLECTION_CONFIG_FILE)
+ .bodyString(COLLECTION_CONFIG, ContentType.APPLICATION_XML)
+ ).returnResponse();
+ assertEquals(HttpStatus.SC_CREATED, response.getStatusLine().getStatusCode());
+
+ response = executor.execute(Request
+ .Get(getRestUri() + "/db/?_query=rest:resource-functions()")
+ ).returnResponse();
+ assertEquals(SC_OK, response.getStatusLine().getStatusCode());
+ assertNotNull(response.getEntity().getContent());
+
+ response = executor.execute(Request
+ .Put(getRestUri() + TEST_COLLECTION + "/" + XQUERY_BASE_URI_FILENAME)
+ .bodyString(XQUERY_BASE_URI_BODY, XQUERY_CONTENT_TYPE)
+ ).returnResponse();
+ assertThat(response.getStatusLine().getStatusCode()).isEqualTo(HttpStatus.SC_CREATED);
+
+ response = executor.execute(Request
+ .Put(getRestUri() + TEST_COLLECTION + "/" + XQUERY_MEDIA_FILENAME)
+ .bodyString(XQUERY_MEDIA_BODY, XQUERY_CONTENT_TYPE)
+ ).returnResponse();
+ assertEquals(HttpStatus.SC_CREATED, response.getStatusLine().getStatusCode());
+
+ response = executor.execute(Request
+ .Get(getRestUri() + "/db/?_query=rest:resource-functions()")
+ ).returnResponse();
+ assertEquals(SC_OK, response.getStatusLine().getStatusCode());
+ assertNotNull(response.getEntity().getContent());
+ var result = readEntityElement(response.getEntity());
+ assertThat(result).contains("");
+ assertThat(result).contains("");
+ }
+
+ @Ignore("Test the return of non XML media types - TBD")
+ @Test
+ public void mediaTypeJson1() throws IOException, ParserConfigurationException, SAXException {
+ final HttpResponse response = executor.execute(Request
+ .Get(getRestXqUri() + "/media-type-json1")
+ .addHeader(new BasicHeader("Accept", "application/json"))
+ ).returnResponse();
+
+ assertEquals(SC_OK, response.getStatusLine().getStatusCode());
+
+ final var entity = response.getEntity();
+ assertThat(entity.getContentType().getValue()).isEqualTo("application/json; charset=UTF-8");
+ var result = readEntityElement(entity);
+ assertThat(result).contains("xmldb:exist://" +TEST_COLLECTION + "/" + XQUERY_BASE_URI_FILENAME +
+ "");
+ assertThat(result).contains("true");
+ assertThat(result).contains("xmldb:exist:" + TEST_COLLECTION + "/" + XQUERY_BASE_URI_FILENAME + "#foobaz");
+ assertThat(result).contains("xmldb:exist:" + TEST_COLLECTION + "/" + XQUERY_BASE_URI_FILENAME + "#foobar");
+ assertThat(result).contains("xmldb:exist:" + TEST_COLLECTION + "/path/to/file#foobar");
+ }
+
+ /**
+ * Execute XQuery as the _query parameter of a request
+ * @throws IOException
+ */
+ @Test public void baseURIRestServerQuery() throws IOException {
+
+ var query = URLEncoder.encode("static-base-uri()", StandardCharsets.UTF_8);
+ var response = executor.execute(Request
+ .Get(getRestServerUri() + "/db/restserver/baseuri?_query=" + query)
+ .addHeader(new BasicHeader("Accept", "application/xml"))
+ ).returnResponse();
+
+ assertThat(response.getStatusLine().getStatusCode()).isEqualTo(SC_OK);
+ final var entity = response.getEntity();
+ assertThat(entity.getContentType().getValue()).isEqualTo("application/xml; charset=UTF-8");
+
+ var result = readEntityElement(entity);
+ assertThat(result).contains("xmldb:exist:///db/restserver/baseuri");
+ }
+
+ private static final String XML_QUERY_BASE_URI = """
+ xquery version "3.1";
+
+ { static-base-uri() }
+ { exists(static-base-uri()) }
+ { resolve-uri('#foobaz', static-base-uri() ) }
+ { resolve-uri('#foobar') }
+
+ """;
+
+ /**
+ * Execute XML_QUERY_BASE_URI supplied as the body of a REST server put request
+ *
+ * 1. PUT the script
+ * 2. GET the result of executing the script
+ *
+ * @throws IOException
+ */
+ @Test public void baseURIRestServerScript() throws IOException {
+
+ final var credentials = Base64.encodeBase64String("admin:".getBytes(UTF_8));
+
+ var response = executor.execute(Request
+ .Put(getRestServerUri() + "/db/test/test.xq")
+ .addHeader(new BasicHeader("Authorization", "Basic " + credentials))
+ .addHeader(new BasicHeader("Accept", "*/*"))
+ .addHeader(new BasicHeader("Content-Type", "application/xquery; charset=UTF-8"))
+ .bodyString(XML_QUERY_BASE_URI, ContentType.TEXT_XML)
+ ).returnResponse();
+ assertThat(response.getStatusLine().getStatusCode()).isEqualTo(SC_CREATED);
+
+ response = executor.execute(Request
+ .Get(getRestServerUri() + "/db/test/test.xq")
+ .addHeader(new BasicHeader("Accept", "application/xml"))
+ .addHeader(new BasicHeader("Authorization", "Basic " + credentials))
+ ).returnResponse();
+ final var entity = response.getEntity();
+ final var content = readEntityElement(entity);
+ assertThat(response.getStatusLine().getStatusCode()).as("Message was: %s", content).isEqualTo(SC_OK);
+ assertThat(content).contains("xmldb:exist:///db/test/test.xq");
+ assertThat(content).contains("true");
+ assertThat(content).contains("xmldb:exist:/db/test/test.xq#foobar");
+ assertThat(content).contains("xmldb:exist:/db/test/test.xq#foobaz");
+ }
+
+
+ private static final String XML_QUERY_EXTENDED_BASE_URI = """
+
+
+
+ """.formatted(XML_QUERY_BASE_URI);
+
+ @Test public void baseURIExtendedQuery() throws IOException {
+
+ final var credentials = Base64.encodeBase64String("admin:".getBytes(UTF_8));
+
+ var response = executor.execute(Request
+ .Post(getRestServerUri() + "/db/test-rest-static-base-uri")
+ .addHeader(new BasicHeader("Authorization", "Basic " + credentials))
+ .addHeader(new BasicHeader("Accept", "*/*"))
+ .addHeader(new BasicHeader("Content-Type", "application/xml; charset=UTF-8"))
+ .bodyString(XML_QUERY_EXTENDED_BASE_URI, ContentType.TEXT_XML)
+ ).returnResponse();
+
+ assertThat(response.getStatusLine().getStatusCode()).isEqualTo(SC_OK);
+ final var entity = response.getEntity();
+ assertThat(readEntityElement(entity)).contains("xmldb:exist:///db/test-rest-static-base-uri");
+ }
+
+ @Test public void baseURIXQueryServlet() throws IOException {
+
+ final var credentials = Base64.encodeBase64String("admin:".getBytes(UTF_8));
+
+ var response = executor.execute(Request
+ .Get(getServerUri() + "/dir1/base-uri-xqueryservlet.xqy")
+ .addHeader(new BasicHeader("Authorization", "Basic " + credentials))
+ .addHeader(new BasicHeader("Accept", "*/*"))
+ ).returnResponse();
+
+ final var entity = response.getEntity();
+ assertThat(response.getStatusLine().getStatusCode()).isEqualTo(SC_OK);
+ final var content = readEntityElement(entity);
+ // The static-base-uri is the DB root URI, not a subpath - as we are executing an external file, this seems reasonable
+ //assertThat(content).contains("xmldb:exist:///db");
+ assertThat(content).contains("file:/");
+ assertThat(content).contains("/dir1/base-uri-xqueryservlet.xqy");
+ assertThat(content).contains("true");
+ assertThat(content).contains("file:/");
+ assertThat(content).contains("/dir1/base-uri-xqueryservlet.xqy#foobar");
+ assertThat(content).contains("file:/");
+ assertThat(content).contains("/dir1/base-uri-xqueryservlet.xqy#foobaz");
+ assertThat(content).contains("file:/");
+ assertThat(content).contains("/dir1/path/to/file#foobar");
+ }
+
+ @Test public void baseURIXmlRpc() throws MalformedURLException, XmlRpcException {
+ final XmlRpcClient xmlrpc = getXmlRpcClient();
+
+ //sanity check
+ var result = xmlrpc.execute("hasCollection", List.of("/db"));
+ assertThat(result instanceof Boolean b).isTrue();
+
+ List