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 params = new ArrayList<>(); + params.add(XMLRPC_BASE_URI_BODY.getBytes(UTF_8)); + params.add(new HashMap<>()); + var handle = xmlrpc.execute("executeQuery", params); + assertThat(handle).isInstanceOf(Integer.class); + + params.clear(); + params.add(handle); + params.add(0); + params.add(new HashMap()); + byte[] item = (byte[]) xmlrpc.execute("retrieve", params); + assertThat(item).isNotNull(); + var s = new String(item, UTF_8); + assertThat(s).isNotEmpty(); + } + + static private String readEntityElement(final HttpEntity entity) throws IOException { + final var inputStream = entity.getContent(); + final var textBuilder = new StringBuilder(); + try (Reader reader = new BufferedReader(new InputStreamReader + (inputStream, StandardCharsets.UTF_8))) { + int c = 0; + while ((c = reader.read()) != -1) { + textBuilder.append((char) c); + } + } + return textBuilder.toString(); + } + + static private XmlRpcClient getXmlRpcClient() throws MalformedURLException { + XmlRpcClient client = new XmlRpcClient(); + XmlRpcClientConfigImpl config = new XmlRpcClientConfigImpl(); + config.setEnabledForExtensions(true); + config.setServerURL(new URL(getXmlRpcUri())); + config.setBasicUserName("admin"); + config.setBasicPassword(""); + client.setConfig(config); + return client; + } + +} diff --git a/extensions/exquery/restxq/src/test/java/org/exist/extensions/exquery/restxq/impl/IntegrationTest.java b/extensions/exquery/restxq/src/test/java/org/exist/extensions/exquery/restxq/impl/IntegrationTest.java deleted file mode 100644 index a18d5907f3f..00000000000 --- a/extensions/exquery/restxq/src/test/java/org/exist/extensions/exquery/restxq/impl/IntegrationTest.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * 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.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.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 java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; - -import static org.junit.Assert.assertEquals; -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.junit.Assert.assertNotNull; - -public class IntegrationTest { - - private static String COLLECTION_CONFIG = - "\n" + - "\n" + - " \n" + - " \n" + - " \n" + - ""; - - private static String TEST_COLLECTION = "/db/restxq/integration-test"; - - private static ContentType XQUERY_CONTENT_TYPE = ContentType.create("application/xquery", "UTF-8"); - private static String XQUERY1 = - "xquery version \"3.0\";\n" + - "\n" + - "module namespace mod1 = \"http://mod1\";\n" + - "\n" + - "declare namespace output = \"https://www.w3.org/2010/xslt-xquery-serialization\";\n" + - "\n" + - "declare\n" + - " %rest:GET\n" + - " %rest:path(\"/media-type-json1\")\n" + - " %output:media-type(\"application/json\")\n" + - " %output:method(\"json\")\n" + - "function mod1:media-type-json1() {\n" + - " \n" + - "};"; - private static String XQUERY1_FILENAME = "restxq-tests1.xqm"; - 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"; - } - - @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 - .Put(getRestUri() + TEST_COLLECTION + "/" + XQUERY1_FILENAME) - .bodyString(XQUERY1, XQUERY_CONTENT_TYPE) - ).returnResponse(); - assertEquals(HttpStatus.SC_CREATED, response.getStatusLine().getStatusCode()); - - response = executor.execute(Request - .Get(getRestUri() + "/db/?_query=rest:resource-functions()") - ).returnResponse(); - assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode()); - assertNotNull(response.getEntity().getContent()); - } - - @Ignore("TODO(AR) need to figure out how to access the RESTXQ API from {@link ExistWebServer}") - @Test - public void mediaTypeJson1() throws IOException { - final HttpResponse response = executor.execute(Request - .Get(getRestXqUri() + "/media-type-json1") - .addHeader(new BasicHeader("Accept", "application/json")) - ).returnResponse(); - - assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode()); - assertEquals("", response.getEntity().toString()); - } - - private static String asString(final InputStream inputStream) throws IOException { - final StringBuilder builder = new StringBuilder(); - try(final Reader reader = new InputStreamReader(inputStream, UTF_8)) { - final char cbuf[] = new char[4096]; - int read = -1; - while((read = reader.read(cbuf)) > -1) { - builder.append(cbuf, 0, read); - } - } - return builder.toString(); - } - -} diff --git a/extensions/exquery/restxq/src/test/resources-filtered/conf.xml b/extensions/exquery/restxq/src/test/resources-filtered/conf.xml index 7600f855254..c28fc543558 100644 --- a/extensions/exquery/restxq/src/test/resources-filtered/conf.xml +++ b/extensions/exquery/restxq/src/test/resources-filtered/conf.xml @@ -738,6 +738,9 @@ + + + diff --git a/extensions/exquery/restxq/src/test/resources/standalone-webapp/WEB-INF/controller-config.xml b/extensions/exquery/restxq/src/test/resources/standalone-webapp/WEB-INF/controller-config.xml new file mode 100644 index 00000000000..a0e03fa7d10 --- /dev/null +++ b/extensions/exquery/restxq/src/test/resources/standalone-webapp/WEB-INF/controller-config.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/extensions/exquery/restxq/src/test/resources/standalone-webapp/WEB-INF/web.xml b/extensions/exquery/restxq/src/test/resources/standalone-webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..65aef9598b5 --- /dev/null +++ b/extensions/exquery/restxq/src/test/resources/standalone-webapp/WEB-INF/web.xml @@ -0,0 +1,123 @@ + + + + + eXist-db – Open Source Native XML Database + eXist-db XML Database + + + org.exist.xmlrpc.RpcServlet + org.exist.xmlrpc.RpcServlet + + enabledForExtensions + true + + + + + EXistServlet + org.exist.http.servlets.EXistServlet + + configuration + conf.xml + + + basedir + WEB-INF/ + + + start + true + + 2 + + + + XQueryURLRewrite + org.exist.http.urlrewrite.XQueryURLRewrite + + config + WEB-INF/controller-config.xml + + + + + XSLTServlet + org.exist.http.servlets.XSLTServlet + + + + + RestXqServlet + org.exist.extensions.exquery.restxq.impl.RestXqServlet + 3 + + + + + XQueryServlet + org.exist.http.servlets.XQueryServlet + + + uri + xmldb:exist:///db + + + + form-encoding + UTF-8 + + + + container-encoding + UTF-8 + + + + encoding + UTF-8 + + + + hide-error-messages + false + + + + + + XQueryURLRewrite + /* + + diff --git a/extensions/exquery/restxq/src/test/resources/standalone-webapp/dir1/base-uri-xqueryservlet.xqy b/extensions/exquery/restxq/src/test/resources/standalone-webapp/dir1/base-uri-xqueryservlet.xqy new file mode 100644 index 00000000000..c1605fcffd5 --- /dev/null +++ b/extensions/exquery/restxq/src/test/resources/standalone-webapp/dir1/base-uri-xqueryservlet.xqy @@ -0,0 +1,8 @@ +xquery version "3.1"; + + { static-base-uri() } + { exists(static-base-uri()) } + { resolve-uri('#foobaz', static-base-uri() ) } + { resolve-uri('#foobar') } + { resolve-uri('path/to/file#foobar') } +