Skip to content

Commit d7c2c38

Browse files
[feature] Implement the fn:uri-collection function
1 parent 2b0738c commit d7c2c38

File tree

5 files changed

+351
-1
lines changed

5 files changed

+351
-1
lines changed

exist-core/src/main/java/org/exist/xquery/ErrorCodes.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ public class ErrorCodes {
150150
public static final ErrorCode FODC0001 = new W3CErrorCode("FODC0001", "No context document.");
151151
public static final ErrorCode FODC0002 = new W3CErrorCode("FODC0002", "Error retrieving resource.");
152152
public static final ErrorCode FODC0003 = new W3CErrorCode("FODC0003", "Function stability not defined.");
153-
public static final ErrorCode FODC0004 = new W3CErrorCode("FODC0004", "Invalid argument to fn:collection.");
153+
public static final ErrorCode FODC0004 = new W3CErrorCode("FODC0004", "Invalid argument to fn:collection or fn:uri-collection.");
154154
public static final ErrorCode FODC0005 = new W3CErrorCode("FODC0005", "Invalid argument to fn:doc or fn:doc-available.");
155155
public static final ErrorCode FODT0001 = new W3CErrorCode("FODT0001", "Overflow/underflow in date/time operation.");
156156
public static final ErrorCode FODT0002 = new W3CErrorCode("FODT0002", "Overflow/underflow in duration operation.");

exist-core/src/main/java/org/exist/xquery/XQueryContext.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,8 @@ public class XQueryContext implements BinaryValueManager, Context {
144144
public static final String HTTP_REQ_ATTR_USER = "xquery.user";
145145
public static final String HTTP_REQ_ATTR_PASS = "xquery.password";
146146

147+
public static final String DEFAULT_URI_COLLECTION = "/db";
148+
147149
// Static namespace/prefix mappings
148150
protected Map<String, String> staticNamespaces = new HashMap<>();
149151

@@ -203,6 +205,8 @@ public class XQueryContext implements BinaryValueManager, Context {
203205
private XMLGregorianCalendar calendar = null;
204206
private TimeZone implicitTimeZone = null;
205207

208+
private final Map<String, Sequence> cachedUriCollectionResults = new HashMap<String, Sequence>();
209+
206210
/**
207211
* the watchdog object assigned to this query.
208212
*/
@@ -2791,6 +2795,10 @@ public void setStaticDecimalFormat(final QName qnDecimalFormat, final DecimalFor
27912795
staticDecimalFormats.put(qnDecimalFormat, decimalFormat);
27922796
}
27932797

2798+
public Map<String, Sequence> getCachedUriCollectionResults() {
2799+
return cachedUriCollectionResults;
2800+
}
2801+
27942802
/**
27952803
* Save state
27962804
*/

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,8 @@ public class FnModule extends AbstractInternalModule {
218218
new FunctionDef(FunTrueOrFalse.fnFalse, FunTrueOrFalse.class),
219219
new FunctionDef(FunUpperOrLowerCase.fnLowerCase, FunUpperOrLowerCase.class),
220220
new FunctionDef(FunUpperOrLowerCase.fnUpperCase, FunUpperOrLowerCase.class),
221+
new FunctionDef(FunUriCollection.FS_URI_COLLECTION_SIGNATURES[0], FunUriCollection.class),
222+
new FunctionDef(FunUriCollection.FS_URI_COLLECTION_SIGNATURES[1], FunUriCollection.class),
221223
new FunctionDef(FunXmlToJson.FS_XML_TO_JSON[0], FunXmlToJson.class),
222224
new FunctionDef(FunXmlToJson.FS_XML_TO_JSON[1], FunXmlToJson.class),
223225
new FunctionDef(FunZeroOrOne.signature, FunZeroOrOne.class),
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
/*
2+
* eXist-db Open Source Native XML Database
3+
* Copyright (C) 2001 The eXist-db Authors
4+
*
5+
6+
* http://www.exist-db.org
7+
*
8+
* This library is free software; you can redistribute it and/or
9+
* modify it under the terms of the GNU Lesser General Public
10+
* License as published by the Free Software Foundation; either
11+
* version 2.1 of the License, or (at your option) any later version.
12+
*
13+
* This library is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16+
* Lesser General Public License for more details.
17+
*
18+
* You should have received a copy of the GNU Lesser General Public
19+
* License along with this library; if not, write to the Free Software
20+
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21+
*/
22+
23+
package org.exist.xquery.functions.fn;
24+
25+
import org.exist.collections.Collection;
26+
import org.exist.dom.persistent.BinaryDocument;
27+
import org.exist.dom.persistent.DocumentImpl;
28+
import org.exist.security.PermissionDeniedException;
29+
import org.exist.storage.lock.Lock;
30+
import org.exist.util.LockException;
31+
import org.exist.xmldb.XmldbURI;
32+
import org.exist.xquery.*;
33+
import org.exist.xquery.value.*;
34+
35+
import java.net.URISyntaxException;
36+
import java.util.*;
37+
import java.util.regex.Pattern;
38+
import java.util.stream.Collectors;
39+
40+
import static org.exist.xquery.FunctionDSL.*;
41+
import static org.exist.xquery.functions.fn.FnModule.functionSignatures;
42+
43+
public class FunUriCollection extends BasicFunction {
44+
45+
private static final String FN_NAME = "uri-collection";
46+
private static final String FN_DESCRIPTION = "Returns a sequence of xs:anyURI values that represent the URIs in a URI collection.";
47+
private static final FunctionReturnSequenceType FN_RETURN = returnsOptMany(Type.STRING,
48+
"the default URI collection, if $arg is not specified or is an empty sequence, " +
49+
"or the sequence of URIs that correspond to the supplied URI");
50+
private static final FunctionParameterSequenceType ARG = optParam("arg", Type.STRING, "The base-URI property from the static context, or an empty sequence");
51+
public static final FunctionSignature[] FS_URI_COLLECTION_SIGNATURES = functionSignatures(
52+
FN_NAME,
53+
FN_DESCRIPTION,
54+
FN_RETURN,
55+
arities(
56+
arity(),
57+
arity(ARG)
58+
)
59+
);
60+
61+
private static final String KEY_CONTENT_TYPE = "content-type";
62+
private static final String VALUE_CONTENT_TYPE_DOCUMENT = "application/vnd.existdb.document";
63+
private static final String VALUE_CONTENT_TYPE_DOCUMENT_BINARY = "application/vnd.existdb.document+binary";
64+
private static final String VALUE_CONTENT_TYPE_DOCUMENT_XML = "application/vnd.existdb.document+xml";
65+
private static final String VALUE_CONTENT_TYPE_SUBCOLLECTION = "application/vnd.existdb.collection";
66+
private static final String[] VALUE_CONTENT_TYPES = {
67+
VALUE_CONTENT_TYPE_DOCUMENT,
68+
VALUE_CONTENT_TYPE_DOCUMENT_BINARY,
69+
VALUE_CONTENT_TYPE_DOCUMENT_XML,
70+
VALUE_CONTENT_TYPE_SUBCOLLECTION
71+
};
72+
73+
private static final String KEY_STABLE = "stable";
74+
private static final String VALUE_STABLE_NO = "no";
75+
private static final String VALUE_STABLE_YES = "yes";
76+
private static final String[] VALUE_STABLES = {
77+
VALUE_STABLE_NO,
78+
VALUE_STABLE_YES
79+
};
80+
81+
private static final String KEY_MATCH = "match";
82+
83+
public FunUriCollection(final XQueryContext context, final FunctionSignature signature) {
84+
super(context, signature);
85+
}
86+
87+
public Sequence eval(final Sequence[] args, final Sequence contextSequence) throws XPathException {
88+
final Sequence result;
89+
if (args.length == 0 || args[0].isEmpty() || args[0].toString().isEmpty()) {
90+
if (XQueryContext.DEFAULT_URI_COLLECTION != null && XQueryContext.DEFAULT_URI_COLLECTION.length() > 0) {
91+
result = new StringValue(XQueryContext.DEFAULT_URI_COLLECTION);
92+
} else {
93+
throw new XPathException(this, ErrorCodes.FODC0002, "No URI is supplied and default resource collection is absent.");
94+
}
95+
} else {
96+
final List<String> resultUris = new LinkedList<String>();
97+
98+
final String uriWithQueryString = args[0].toString();
99+
final int queryStringIndex = uriWithQueryString.indexOf('?');
100+
final String uriWithoutQueryString = (queryStringIndex >= 0) ? uriWithQueryString.substring(0, queryStringIndex) : uriWithQueryString;
101+
String uriWithoutStableQueryString = uriWithQueryString.replaceAll(String.format("%s\\s*=\\s*\\byes|no\\b\\s*&+", KEY_STABLE), "");
102+
if (uriWithoutStableQueryString.endsWith("?")) {
103+
uriWithoutStableQueryString = uriWithoutStableQueryString.substring(0, uriWithoutStableQueryString.length() - 1);
104+
}
105+
106+
final XmldbURI uri;
107+
try {
108+
uri = XmldbURI.xmldbUriFor(uriWithoutQueryString);
109+
} catch (URISyntaxException e) {
110+
throw new XPathException(this, ErrorCodes.FODC0004, String.format("\"%s\" is not a valid URI.", args[0].toString()));
111+
}
112+
113+
final Map<String, String> queryStringMap = parseQueryString(uriWithQueryString);
114+
checkQueryStringMap(queryStringMap);
115+
116+
if ((!queryStringMap.containsKey(KEY_STABLE) || queryStringMap.get(KEY_STABLE).equals(VALUE_STABLE_YES)) &&
117+
context.getCachedUriCollectionResults().containsKey(uriWithoutStableQueryString)) {
118+
result = context.getCachedUriCollectionResults().get(uriWithoutStableQueryString);
119+
} else {
120+
final boolean binaryUrisIncluded = !queryStringMap.containsKey(KEY_CONTENT_TYPE) ||
121+
(queryStringMap.get(KEY_CONTENT_TYPE).equals(VALUE_CONTENT_TYPE_DOCUMENT) ||
122+
queryStringMap.get(KEY_CONTENT_TYPE).equals(VALUE_CONTENT_TYPE_DOCUMENT_BINARY));
123+
final boolean subcollectionUrisIncluded = !queryStringMap.containsKey(KEY_CONTENT_TYPE) ||
124+
queryStringMap.get(KEY_CONTENT_TYPE).equals(VALUE_CONTENT_TYPE_SUBCOLLECTION);
125+
final boolean xmlUrisIncluded = !queryStringMap.containsKey(KEY_CONTENT_TYPE) ||
126+
(queryStringMap.get(KEY_CONTENT_TYPE).equals(VALUE_CONTENT_TYPE_DOCUMENT) ||
127+
queryStringMap.get(KEY_CONTENT_TYPE).equals(VALUE_CONTENT_TYPE_DOCUMENT_XML));
128+
129+
try (final Collection collection = context.getBroker().openCollection(uri, Lock.LockMode.READ_LOCK)) {
130+
if (binaryUrisIncluded || xmlUrisIncluded) {
131+
final Iterator<DocumentImpl> documentIterator = collection.iterator(context.getBroker());
132+
while (documentIterator.hasNext()) {
133+
final DocumentImpl document = documentIterator.next();
134+
if ((xmlUrisIncluded && !(document instanceof BinaryDocument)) ||
135+
(binaryUrisIncluded && document instanceof BinaryDocument)) {
136+
resultUris.add(document.getURI().toString());
137+
}
138+
}
139+
}
140+
141+
if (subcollectionUrisIncluded) {
142+
final Iterator<XmldbURI> collectionsIterator = collection.collectionIterator(context.getBroker());
143+
while (collectionsIterator.hasNext()) {
144+
resultUris.add(uri.append(collectionsIterator.next()).toString());
145+
}
146+
}
147+
} catch (final LockException | PermissionDeniedException e) {
148+
throw new XPathException(this, ErrorCodes.FODC0002, e);
149+
}
150+
151+
if (queryStringMap.containsKey(KEY_MATCH) && queryStringMap.get(KEY_MATCH).length() > 0) {
152+
final Pattern pattern = Pattern.compile(queryStringMap.get(KEY_MATCH));
153+
final List<String> matchedResultUris = resultUris.stream().filter(resultUri -> pattern.matcher(resultUri).find()).collect(Collectors.toList());
154+
if (matchedResultUris.isEmpty()) {
155+
result = Sequence.EMPTY_SEQUENCE;
156+
} else {
157+
result = new ValueSequence();
158+
for (String resultUri : matchedResultUris) {
159+
result.add(new StringValue(resultUri));
160+
}
161+
}
162+
} else {
163+
result = new ValueSequence();
164+
for (String resultUri : resultUris) {
165+
result.add(new StringValue(resultUri));
166+
}
167+
}
168+
context.getCachedUriCollectionResults().put(uriWithoutStableQueryString, result);
169+
}
170+
}
171+
172+
return result;
173+
}
174+
175+
private static Map<String, String> parseQueryString(final String uri) {
176+
final Map<String, String> map = new HashMap<>();
177+
final int questionMarkIndex = (uri == null) ? -1 : uri.indexOf('?');
178+
if (questionMarkIndex >= 0 && questionMarkIndex + 1 < uri.length()) {
179+
String[] keyValuePairs = uri.substring(questionMarkIndex + 1).split("&");
180+
for (String keyValuePair : keyValuePairs) {
181+
int equalIndex = keyValuePair.indexOf('=');
182+
if (equalIndex >= 0) {
183+
if (equalIndex + 1 < uri.length()) {
184+
map.put(keyValuePair.substring(0, equalIndex).trim(), keyValuePair.substring(equalIndex + 1).trim());
185+
} else {
186+
map.put(keyValuePair.substring(0, equalIndex).trim(), "");
187+
}
188+
} else {
189+
map.put(keyValuePair.trim(), "");
190+
}
191+
}
192+
}
193+
194+
return map;
195+
}
196+
197+
private void checkQueryStringMap(final Map<String, String> queryStringMap) throws XPathException {
198+
for (Map.Entry<String, String> queryStringEntry : queryStringMap.entrySet()) {
199+
final String key = queryStringEntry.getKey();
200+
final String value = queryStringEntry.getValue();
201+
if (key.equals(KEY_CONTENT_TYPE)) {
202+
if (!Arrays.stream(VALUE_CONTENT_TYPES).anyMatch(contentTypeValue -> contentTypeValue.equals(value))) {
203+
throw new XPathException(this, ErrorCodes.FODC0004, String.format("Invalid query-string value \"%s\".", queryStringEntry));
204+
}
205+
} else if (key.equals(KEY_STABLE)) {
206+
if (!Arrays.stream(VALUE_STABLES).anyMatch(stableValue -> stableValue.equals(value))) {
207+
throw new XPathException(this, ErrorCodes.FODC0004, String.format("Invalid query-string value \"%s\".", queryStringEntry));
208+
}
209+
} else if (!key.equals(KEY_MATCH)) {
210+
throw new XPathException(this, ErrorCodes.FODC0004, String.format("Unexpected query string \"%s\".", queryStringEntry));
211+
}
212+
}
213+
}
214+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
(:
2+
: eXist-db Open Source Native XML Database
3+
: Copyright (C) 2001 The eXist-db Authors
4+
:
5+
6+
: http://www.exist-db.org
7+
:
8+
: This library is free software; you can redistribute it and/or
9+
: modify it under the terms of the GNU Lesser General Public
10+
: License as published by the Free Software Foundation; either
11+
: version 2.1 of the License, or (at your option) any later version.
12+
:
13+
: This library is distributed in the hope that it will be useful,
14+
: but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16+
: Lesser General Public License for more details.
17+
:
18+
: You should have received a copy of the GNU Lesser General Public
19+
: License along with this library; if not, write to the Free Software
20+
: Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21+
:)
22+
xquery version "3.1";
23+
24+
module namespace fnuc="http://exist-db.org/xquery/test/function_uri_collection";
25+
26+
declare namespace test="http://exist-db.org/xquery/xqsuite";
27+
declare namespace x="httpx://x/ns/1.0";
28+
29+
declare variable $fnuc:COLLECTION_NAME := "/test-collection";
30+
declare variable $fnuc:SUBCOLLECTION_NAME := $fnuc:COLLECTION_NAME||"/subcol";
31+
declare variable $fnuc:COLLECTION := collection("/db"||$fnuc:COLLECTION_NAME);
32+
declare variable $fnuc:SUBCOLLECTION := collection("/db"||$fnuc:SUBCOLLECTION_NAME);
33+
34+
declare
35+
%test:setUp
36+
function fnuc:setup() {
37+
let $collection := xmldb:create-collection("/db", "test-collection")
38+
39+
return
40+
(
41+
xmldb:create-collection("/db", $fnuc:SUBCOLLECTION_NAME),
42+
xmldb:store("/db"||$fnuc:SUBCOLLECTION_NAME, "test-subcol.xml", <container><a/><b/></container>),
43+
xmldb:store($collection, "test.bin", "binary", "application/octet-stream"),
44+
xmldb:store($collection, "test.xml", document { <container><a/><b/></container>})
45+
)
46+
};
47+
48+
declare
49+
%test:tearDown
50+
function fnuc:cleanup() {
51+
xmldb:remove("/db/test-collection")
52+
};
53+
54+
declare
55+
%test:assertEquals("/db")
56+
function fnuc:no-argument() {
57+
fn:uri-collection()
58+
};
59+
60+
declare
61+
%test:assertError("FODC0004")
62+
function fnuc:invalid-uri() {
63+
fn:uri-collection(":invalid-uri")
64+
};
65+
66+
declare
67+
%test:assertEquals("/db/test-collection/test.bin", "/db/test-collection/test.xml", "/db/test-collection/subcol")
68+
function fnuc:all-uris() {
69+
fn:uri-collection("/db/test-collection")
70+
};
71+
72+
declare
73+
%test:assertEquals("/db/test-collection/subcol")
74+
function fnuc:subcollection-uris() {
75+
fn:uri-collection("/db/test-collection?content-type=application/vnd.existdb.collection")
76+
};
77+
78+
declare
79+
%test:assertEquals("/db/test-collection/test.bin", "/db/test-collection/test.xml")
80+
function fnuc:document-uris() {
81+
fn:uri-collection("/db/test-collection?content-type=application/vnd.existdb.document")
82+
};
83+
84+
declare
85+
%test:assertEquals("/db/test-collection/test.xml")
86+
function fnuc:xml-document-uris() {
87+
fn:uri-collection("/db/test-collection?content-type=application/vnd.existdb.document+xml")
88+
};
89+
90+
declare
91+
%test:assertEquals("/db/test-collection/test.bin")
92+
function fnuc:binary-document-uris() {
93+
fn:uri-collection("/db/test-collection?content-type=application/vnd.existdb.document+binary")
94+
};
95+
96+
declare
97+
%test:assertEquals("/db/test-collection/test.bin")
98+
function fnuc:match-uris() {
99+
fn:uri-collection("/db/test-collection?match=.*\.bin")
100+
};
101+
102+
declare
103+
%test:assertEmpty
104+
function fnuc:no-match-uris() {
105+
fn:uri-collection("/db/test-collection?match=.*\.nonexisting")
106+
};
107+
108+
declare
109+
%test:assertEquals("/db/test-collection/test.bin", "/db/test-collection/test.xml", "/db/test-collection/subcol")
110+
function fnuc:stable() {
111+
let $c1 := fn:uri-collection("/db/test-collection?stable=yes")
112+
let $r := xmldb:remove("/db/test-collection", "test.xml")
113+
let $c2 := fn:uri-collection("/db/test-collection?stable=yes")
114+
let $a := xmldb:store("/db/test-collection", "test.xml", document { <container><a/><b/></container>})
115+
return $c2
116+
};
117+
118+
declare
119+
%test:assertEquals("/db/test-collection/test.bin", "/db/test-collection/subcol")
120+
function fnuc:not-stable() {
121+
let $c1 := fn:uri-collection("/db/test-collection?stable=no")
122+
let $r := xmldb:remove("/db/test-collection", "test.xml")
123+
let $c2 := fn:uri-collection("/db/test-collection?stable=no")
124+
let $a := xmldb:store("/db/test-collection", "test.xml", document { <container><a/><b/></container>})
125+
return $c2
126+
};

0 commit comments

Comments
 (0)