Skip to content

Commit 9d083ab

Browse files
joewizclaude
andcommitted
[feature] Native EXQuery Request Module for eXist-db
Implements the full 28-function EXQuery Request Module API (http://exquery.org/ns/request), compatible with BaseX: - General: method(), context-path() - URI: scheme(), hostname(), port(), path(), query(), uri() - Connection: address(), remote-hostname(), remote-address(), remote-port() - Parameters: parameter(), parameter-names(), parameter-map() - Headers: header(), header-names(), header-map() - Cookies: cookie(), cookie-names(), cookie-map() - Attributes: attribute(), attribute-names(), attribute-map(), set-attribute() Default prefix is `exrequest` to coexist with eXist's built-in `request` module (http://exist-db.org/xquery/request). Both modules can be imported simultaneously without prefix conflicts. 9 functions are new (not in the built-in module): context-path(), parameter-map(), header-map(), cookie-map(), attribute(), attribute-names(), attribute-map(), attribute($name, $default), and set-attribute(). Uses eXist's native XQueryContext.getHttpContext() — zero external dependencies. 62 tests using ExistEmbeddedServer with MockRequestWrapper. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8506e38 commit 9d083ab

22 files changed

+1129
-973
lines changed

.gitignore

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
.env
21
target/
3-
*.class
4-
*.jar
5-
*.xar
6-
.idea/
2+
.mvn/
3+
.m2-repo/
4+
.env
75
*.iml
8-
.settings/
9-
.project
6+
.idea/
107
.classpath
8+
.project
9+
.settings/

LICENSE

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
GNU LESSER GENERAL PUBLIC LICENSE
2+
Version 2.1, February 1999
3+
4+
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
5+
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
6+
Everyone is permitted to copy and distribute verbatim copies
7+
of this license document, but changing it is not allowed.
8+
9+
[This is the first released version of the Lesser GPL. It also counts
10+
as the successor of the GNU Library Public License, version 2, hence
11+
the version number 2.1.]

README.md

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# EXQuery Request Module for eXist-db
2+
3+
A standalone XAR package implementing the [EXQuery Request Module](https://exquery.github.io/expath-specs-playground/request-module-1.0-specification.html) for eXist-db. Provides 28 functions for inspecting HTTP requests — method, URI components, parameters, headers, cookies, and attributes — compatible with BaseX's Request Module API. Uses eXist's native `XQueryContext.getHttpContext()` — no external dependencies.
4+
5+
This module **coexists** with eXist's built-in request module (`http://exist-db.org/xquery/request`). They use different namespaces and different default prefixes, so both are available simultaneously with no conflicts.
6+
7+
## Install
8+
9+
Download the `.xar` from CI build artifacts and install with the eXist-db Package Manager or the `xst` CLI:
10+
11+
```bash
12+
xst package install exist-request-0.9.0-SNAPSHOT.xar
13+
```
14+
15+
## Coexistence with the built-in request module
16+
17+
eXist-db ships a built-in request module at `http://exist-db.org/xquery/request` (prefix: `request`). This package uses a different namespace (`http://exquery.org/ns/request`) and a different default prefix (`exrequest`), so both modules are available at the same time:
18+
19+
```xquery
20+
import module namespace request = "http://exist-db.org/xquery/request";
21+
import module namespace exrequest = "http://exquery.org/ns/request";
22+
23+
(: eXist-native — some functions unique to eXist :)
24+
request:get-parameter("id", ())
25+
26+
(: EXQuery standard — portable across eXist and BaseX :)
27+
exrequest:parameter("id")
28+
exrequest:parameter-map()
29+
exrequest:header-map()
30+
```
31+
32+
The `exrequest:*` functions follow the [EXQuery Request Module specification](https://exquery.github.io/expath-specs-playground/request-module-1.0-specification.html) — the same API implemented by BaseX. Code using `exrequest:*` is portable across both engines.
33+
34+
The `request:*` functions are eXist-native. Some overlap with the EXQuery API (e.g., `request:get-method()` vs `exrequest:method()`), but eXist's built-in module also has functions with no EXQuery equivalent (e.g., `request:get-uploaded-file-data()`).
35+
36+
### What's new in this module
37+
38+
This module provides 9 functions not available in eXist's built-in request module:
39+
40+
| Function | Description |
41+
|----------|-------------|
42+
| `exrequest:context-path()` | Servlet context path |
43+
| `exrequest:parameter-map()` | All parameters as XDM map |
44+
| `exrequest:header-map()` | All headers as XDM map |
45+
| `exrequest:cookie-map()` | All cookies as XDM map |
46+
| `exrequest:attribute-map()` | All attributes as XDM map |
47+
| `exrequest:attribute($name)` | Read a request attribute |
48+
| `exrequest:attribute($name, $default)` | Attribute with fallback |
49+
| `exrequest:attribute-names()` | All attribute names |
50+
| `exrequest:set-attribute($name, $value)` | Set a request attribute |
51+
52+
### Migration timeline
53+
54+
| Release | Status |
55+
|---------|--------|
56+
| **eXist 7.x** | Coexistence — both modules available, `request:*` is eXist-native, `exrequest:*` is EXQuery standard |
57+
| **eXist 8.0** | Deprecation of original `request:*` module (with compatibility shim) |
58+
| **eXist 9.0** | Prefix swap — `request:*` resolves to the EXQuery standard, original module available as `request-legacy:*` |
59+
60+
## Functions
61+
62+
| Function | Description |
63+
|----------|-------------|
64+
| `exrequest:method()` | HTTP request method (GET, POST, etc.) |
65+
| `exrequest:context-path()` | Servlet context path |
66+
| `exrequest:scheme()` | URI scheme (http or https) |
67+
| `exrequest:hostname()` | Server hostname |
68+
| `exrequest:port()` | Server port number |
69+
| `exrequest:path()` | Request path |
70+
| `exrequest:query()` | Raw query string |
71+
| `exrequest:uri()` | Full request URI |
72+
| `exrequest:address()` | Server address |
73+
| `exrequest:remote-hostname()` | Client hostname |
74+
| `exrequest:remote-address()` | Client IP address |
75+
| `exrequest:remote-port()` | Client port number |
76+
| `exrequest:parameter($name)` | Query/form parameter value(s) |
77+
| `exrequest:parameter($name, $default)` | Parameter with fallback |
78+
| `exrequest:parameter-names()` | All parameter names |
79+
| `exrequest:parameter-map()` | All parameters as XDM map |
80+
| `exrequest:header($name)` | HTTP header value |
81+
| `exrequest:header($name, $default)` | Header with fallback |
82+
| `exrequest:header-names()` | All header names |
83+
| `exrequest:header-map()` | All headers as XDM map |
84+
| `exrequest:cookie($name)` | Cookie value |
85+
| `exrequest:cookie($name, $default)` | Cookie with fallback |
86+
| `exrequest:cookie-names()` | All cookie names |
87+
| `exrequest:cookie-map()` | All cookies as XDM map |
88+
| `exrequest:attribute($name)` | Request attribute value |
89+
| `exrequest:attribute($name, $default)` | Attribute with fallback |
90+
| `exrequest:attribute-names()` | All attribute names |
91+
| `exrequest:attribute-map()` | All attributes as XDM map |
92+
| `exrequest:set-attribute($name, $value)` | Set a request attribute |
93+
94+
**Module namespace:** `http://exquery.org/ns/request`
95+
96+
## Examples
97+
98+
### Basic usage
99+
100+
```xquery
101+
import module namespace exrequest = "http://exquery.org/ns/request";
102+
103+
<request>
104+
<method>{exrequest:method()}</method>
105+
<path>{exrequest:path()}</path>
106+
<host>{exrequest:hostname()}:{exrequest:port()}</host>
107+
</request>
108+
```
109+
110+
### Parameters with defaults
111+
112+
```xquery
113+
import module namespace exrequest = "http://exquery.org/ns/request";
114+
115+
let $page := xs:integer(exrequest:parameter("page", "1"))
116+
let $limit := xs:integer(exrequest:parameter("limit", "20"))
117+
return
118+
<pagination offset="{($page - 1) * $limit}" limit="{$limit}"/>
119+
```
120+
121+
### Headers and cookies
122+
123+
```xquery
124+
import module namespace exrequest = "http://exquery.org/ns/request";
125+
126+
let $accept := exrequest:header("Accept", "*/*")
127+
let $session := exrequest:cookie("JSESSIONID", "")
128+
return
129+
<context accept="{$accept}" has-session="{$session != ''}"/>
130+
```
131+
132+
### Attributes (inter-stage communication)
133+
134+
```xquery
135+
import module namespace exrequest = "http://exquery.org/ns/request";
136+
137+
(: Set in controller.xq :)
138+
let $_ := exrequest:set-attribute("route.action", "list")
139+
140+
(: Read in target XQuery :)
141+
return exrequest:attribute("route.action")
142+
```
143+
144+
## Build
145+
146+
```bash
147+
JAVA_HOME=/path/to/java-21 mvn clean package -DskipTests
148+
```
149+
150+
Run integration tests (requires exist-core 7.0.0-SNAPSHOT in your local Maven repo):
151+
152+
```bash
153+
mvn test -Pintegration-tests
154+
```
155+
156+
## License
157+
158+
[GNU Lesser General Public License v2.1](https://opensource.org/licenses/LGPL-2.1)

pom.xml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
<groupId>org.exist-db</groupId>
1515
<artifactId>exist-request</artifactId>
16-
<version>1.0.0</version>
16+
<version>0.9.0-SNAPSHOT</version>
1717

1818
<name>EXQuery Request Module</name>
1919
<description>Native EXQuery Request Module for eXist-db, compatible with BaseX's 28-function API</description>
@@ -45,8 +45,8 @@
4545
<exist.version>7.0.0-SNAPSHOT</exist.version>
4646

4747
<!-- used in the EXPath Package Descriptor -->
48-
<package-name>http://exist-db.org/apps/request</package-name>
49-
<package-abbrev>request</package-abbrev>
48+
<package-name>http://exist-db.org/pkg/request</package-name>
49+
<package-abbrev>exist-request</package-abbrev>
5050

5151
<request.module.namespace>http://exquery.org/ns/request</request.module.namespace>
5252
<request.module.java.classname>org.exist.xquery.modules.request.RequestModule</request.module.java.classname>
@@ -65,6 +65,13 @@
6565
<profile>
6666
<id>integration-tests</id>
6767
<dependencies>
68+
<!-- exist-core as test scope so it's on the runtime classpath for tests -->
69+
<dependency>
70+
<groupId>org.exist-db</groupId>
71+
<artifactId>exist-core</artifactId>
72+
<version>${exist.version}</version>
73+
<scope>test</scope>
74+
</dependency>
6875
<dependency>
6976
<groupId>org.exist-db</groupId>
7077
<artifactId>exist-core</artifactId>

src/main/java/org/exist/xquery/modules/request/AttributeFunctions.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
import org.exist.http.servlets.RequestWrapper;
2525
import org.exist.xquery.*;
26+
import org.exist.xquery.functions.map.MapType;
2627
import org.exist.xquery.value.*;
2728

2829
import javax.annotation.Nonnull;
@@ -63,7 +64,7 @@ public class AttributeFunctions extends AbstractRequestFunction {
6364
public static final FunctionSignature FS_ATTRIBUTE_MAP = functionSignature(
6465
RequestModule.qname(FS_ATTRIBUTE_MAP_NAME),
6566
"Returns all request attributes as a map.",
66-
returns(Type.MAP, "a map of attribute names to values")
67+
returns(Type.MAP_ITEM, "a map of attribute names to values")
6768
);
6869

6970
private static final String FS_SET_ATTRIBUTE_NAME = "set-attribute";

src/main/java/org/exist/xquery/modules/request/CookieFunctions.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.exist.xquery.FunctionSignature;
2727
import org.exist.xquery.XPathException;
2828
import org.exist.xquery.XQueryContext;
29+
import org.exist.xquery.functions.map.MapType;
2930
import org.exist.xquery.value.*;
3031

3132
import javax.annotation.Nonnull;
@@ -64,7 +65,7 @@ public class CookieFunctions extends AbstractRequestFunction {
6465
public static final FunctionSignature FS_COOKIE_MAP = functionSignature(
6566
RequestModule.qname(FS_COOKIE_MAP_NAME),
6667
"Returns all cookies as a map. Each key is a cookie name, each value is the cookie value.",
67-
returns(Type.MAP, "a map of cookie names to values")
68+
returns(Type.MAP_ITEM, "a map of cookie names to values")
6869
);
6970

7071
public CookieFunctions(final XQueryContext context, final FunctionSignature signature) {

src/main/java/org/exist/xquery/modules/request/HeaderFunctions.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.exist.xquery.FunctionSignature;
2626
import org.exist.xquery.XPathException;
2727
import org.exist.xquery.XQueryContext;
28+
import org.exist.xquery.functions.map.MapType;
2829
import org.exist.xquery.value.*;
2930

3031
import javax.annotation.Nonnull;
@@ -65,7 +66,7 @@ public class HeaderFunctions extends AbstractRequestFunction {
6566
RequestModule.qname(FS_HEADER_MAP_NAME),
6667
"Returns all HTTP headers as a map. Each key is a header name, " +
6768
"each value is the header value string.",
68-
returns(Type.MAP, "a map of header names to values")
69+
returns(Type.MAP_ITEM, "a map of header names to values")
6970
);
7071

7172
public HeaderFunctions(final XQueryContext context, final FunctionSignature signature) {

src/main/java/org/exist/xquery/modules/request/ParameterFunctions.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.exist.xquery.FunctionSignature;
2626
import org.exist.xquery.XPathException;
2727
import org.exist.xquery.XQueryContext;
28+
import org.exist.xquery.functions.map.MapType;
2829
import org.exist.xquery.value.*;
2930

3031
import javax.annotation.Nonnull;
@@ -65,7 +66,7 @@ public class ParameterFunctions extends AbstractRequestFunction {
6566
RequestModule.qname(FS_PARAMETER_MAP_NAME),
6667
"Returns all query and form parameters as a map. Each key is a parameter name, " +
6768
"each value is one or more parameter values.",
68-
returns(Type.MAP, "a map of parameter names to values")
69+
returns(Type.MAP_ITEM, "a map of parameter names to values")
6970
);
7071

7172
public ParameterFunctions(final XQueryContext context, final FunctionSignature signature) {

src/main/java/org/exist/xquery/modules/request/RequestModule.java

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public class RequestModule extends AbstractInternalModule {
4646

4747
public static final String NAMESPACE_URI = "http://exquery.org/ns/request";
4848

49-
public static final String PREFIX = "request";
49+
public static final String PREFIX = "exrequest";
5050

5151
public static final String RELEASE = "1.0.0";
5252

@@ -71,27 +71,35 @@ public class RequestModule extends AbstractInternalModule {
7171
ConnectionFunctions.FS_REMOTE_HOSTNAME,
7272
ConnectionFunctions.FS_REMOTE_ADDRESS,
7373
ConnectionFunctions.FS_REMOTE_PORT),
74+
// ParameterFunctions — split because FS_PARAMETER is FunctionSignature[]
7475
functionDefs(ParameterFunctions.class,
7576
ParameterFunctions.FS_PARAMETER_NAMES,
76-
ParameterFunctions.FS_PARAMETER,
7777
ParameterFunctions.FS_PARAMETER_MAP),
78+
functionDefs(ParameterFunctions.class,
79+
ParameterFunctions.FS_PARAMETER),
80+
// HeaderFunctions — split because FS_HEADER is FunctionSignature[]
7881
functionDefs(HeaderFunctions.class,
7982
HeaderFunctions.FS_HEADER_NAMES,
80-
HeaderFunctions.FS_HEADER,
8183
HeaderFunctions.FS_HEADER_MAP),
84+
functionDefs(HeaderFunctions.class,
85+
HeaderFunctions.FS_HEADER),
86+
// CookieFunctions — split because FS_COOKIE is FunctionSignature[]
8287
functionDefs(CookieFunctions.class,
83-
CookieFunctions.FS_COOKIE,
8488
CookieFunctions.FS_COOKIE_NAMES,
8589
CookieFunctions.FS_COOKIE_MAP),
90+
functionDefs(CookieFunctions.class,
91+
CookieFunctions.FS_COOKIE),
92+
// AttributeFunctions — split because FS_ATTRIBUTE is FunctionSignature[]
8693
functionDefs(AttributeFunctions.class,
87-
AttributeFunctions.FS_ATTRIBUTE,
8894
AttributeFunctions.FS_ATTRIBUTE_NAMES,
8995
AttributeFunctions.FS_ATTRIBUTE_MAP,
90-
AttributeFunctions.FS_SET_ATTRIBUTE)
96+
AttributeFunctions.FS_SET_ATTRIBUTE),
97+
functionDefs(AttributeFunctions.class,
98+
AttributeFunctions.FS_ATTRIBUTE)
9199
);
92100

93101
public RequestModule(final Map<String, List<?>> parameters) {
94-
super(functions, parameters, true);
102+
super(functions, parameters, false);
95103
}
96104

97105
@Override

0 commit comments

Comments
 (0)