Skip to content

Commit 1c60a4e

Browse files
authored
Merge pull request #4483 from evolvedbinary/feature/fn-uri-collection#0,#1
[feature] Implement the fn:uri-collection function
2 parents d86e4f0 + 867bdbc commit 1c60a4e

File tree

5 files changed

+384
-1
lines changed

5 files changed

+384
-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
@@ -203,6 +203,8 @@ public class XQueryContext implements BinaryValueManager, Context {
203203
private XMLGregorianCalendar calendar = null;
204204
private TimeZone implicitTimeZone = null;
205205

206+
private final Map<String, Sequence> cachedUriCollectionResults = new HashMap<>();
207+
206208
/**
207209
* the watchdog object assigned to this query.
208210
*/
@@ -1403,6 +1405,8 @@ public void reset(final boolean keepGlobals) {
14031405
callStack.clear();
14041406
protectedDocuments = null;
14051407

1408+
cachedUriCollectionResults.clear();
1409+
14061410
if (!keepGlobals) {
14071411
globalVariables.clear();
14081412
}
@@ -2790,6 +2794,10 @@ public void setStaticDecimalFormat(final QName qnDecimalFormat, final DecimalFor
27902794
staticDecimalFormats.put(qnDecimalFormat, decimalFormat);
27912795
}
27922796

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

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
@@ -223,6 +223,8 @@ public class FnModule extends AbstractInternalModule {
223223
new FunctionDef(FunTrueOrFalse.fnFalse, FunTrueOrFalse.class),
224224
new FunctionDef(FunUpperOrLowerCase.fnLowerCase, FunUpperOrLowerCase.class),
225225
new FunctionDef(FunUpperOrLowerCase.fnUpperCase, FunUpperOrLowerCase.class),
226+
new FunctionDef(FunUriCollection.FS_URI_COLLECTION_SIGNATURES[0], FunUriCollection.class),
227+
new FunctionDef(FunUriCollection.FS_URI_COLLECTION_SIGNATURES[1], FunUriCollection.class),
226228
new FunctionDef(FunXmlToJson.FS_XML_TO_JSON[0], FunXmlToJson.class),
227229
new FunctionDef(FunXmlToJson.FS_XML_TO_JSON[1], FunXmlToJson.class),
228230
new FunctionDef(FunZeroOrOne.signature, FunZeroOrOne.class),
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
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.util.PatternFactory;
32+
import org.exist.xmldb.XmldbURI;
33+
import org.exist.xquery.*;
34+
import org.exist.xquery.value.*;
35+
36+
import java.net.URISyntaxException;
37+
import java.util.*;
38+
import java.util.regex.Pattern;
39+
import java.util.stream.Collectors;
40+
41+
import static org.exist.xquery.FunctionDSL.*;
42+
import static org.exist.xquery.functions.fn.FnModule.functionSignatures;
43+
44+
public class FunUriCollection extends BasicFunction {
45+
46+
private static final String FN_NAME = "uri-collection";
47+
private static final String FN_DESCRIPTION = "Returns a sequence of xs:anyURI values that represent the URIs in a URI collection.";
48+
private static final FunctionReturnSequenceType FN_RETURN = returnsOptMany(Type.ANY_URI,
49+
"the default URI collection, if $arg is not specified or is an empty sequence, " +
50+
"or the sequence of URIs that correspond to the supplied URI");
51+
private static final FunctionParameterSequenceType ARG = optParam("arg", Type.STRING,
52+
"An xs:string identifying a URI Collection. " +
53+
"The argument is interpreted as either an absolute xs:anyURI, or a relative xs:anyURI resolved " +
54+
"against the base-URI property from the static context. In eXist-db this function consults the " +
55+
"query hierarchy of the database. Query String parameters may be provided to " +
56+
"control the URIs returned by this function. " +
57+
"The parameter `match` may be used to provide a Regular Expression against which the result " +
58+
"sequence of URIs are filtered. " +
59+
"The parameter `content-type` may be used to determine the Internet Media Type (or generally " +
60+
"whether XML, Binary, and/or (Sub) Collection) URIs that are returned in the result sequence; " +
61+
"the special values: 'application/vnd.existdb.collection' includes (Sub) Collections, " +
62+
"'application/vnd.existdb.document' includes any document, " +
63+
"'application/vnd.existdb.document+xml' includes only XML documents, and " +
64+
"'application/vnd.existdb.document+binary' includes only Binary documents. By default, " +
65+
"`content-type=application/vnd.existdb.collection,application/vnd.existdb.document` " +
66+
"(i.e. all Collections and Documents). " +
67+
"The parameter `stable` may be used to determine if the function is deterministic. " +
68+
"By default `stable=yes` to ensure that the same results are returned by each call within the same " +
69+
"query."
70+
);
71+
public static final FunctionSignature[] FS_URI_COLLECTION_SIGNATURES = functionSignatures(
72+
FN_NAME,
73+
FN_DESCRIPTION,
74+
FN_RETURN,
75+
arities(
76+
arity(),
77+
arity(ARG)
78+
)
79+
);
80+
81+
private static final String KEY_CONTENT_TYPE = "content-type";
82+
private static final String VALUE_CONTENT_TYPE_DOCUMENT = "application/vnd.existdb.document";
83+
private static final String VALUE_CONTENT_TYPE_DOCUMENT_BINARY = "application/vnd.existdb.document+binary";
84+
private static final String VALUE_CONTENT_TYPE_DOCUMENT_XML = "application/vnd.existdb.document+xml";
85+
private static final String VALUE_CONTENT_TYPE_SUBCOLLECTION = "application/vnd.existdb.collection";
86+
private static final String[] VALUE_CONTENT_TYPES = {
87+
VALUE_CONTENT_TYPE_DOCUMENT,
88+
VALUE_CONTENT_TYPE_DOCUMENT_BINARY,
89+
VALUE_CONTENT_TYPE_DOCUMENT_XML,
90+
VALUE_CONTENT_TYPE_SUBCOLLECTION
91+
};
92+
93+
private static final String KEY_STABLE = "stable";
94+
private static final String VALUE_STABLE_NO = "no";
95+
private static final String VALUE_STABLE_YES = "yes";
96+
private static final String[] VALUE_STABLES = {
97+
VALUE_STABLE_NO,
98+
VALUE_STABLE_YES
99+
};
100+
101+
private static final String KEY_MATCH = "match";
102+
103+
public FunUriCollection(final XQueryContext context, final FunctionSignature signature) {
104+
super(context, signature);
105+
}
106+
107+
public Sequence eval(final Sequence[] args, final Sequence contextSequence) throws XPathException {
108+
final Sequence result;
109+
if (args.length == 0 || args[0].isEmpty() || args[0].toString().isEmpty()) {
110+
result = new AnyURIValue(XmldbURI.ROOT_COLLECTION);
111+
} else {
112+
final List<String> resultUris = new ArrayList<>();
113+
114+
final String uriWithQueryString = args[0].toString();
115+
final int queryStringIndex = uriWithQueryString.indexOf('?');
116+
final String uriWithoutQueryString = (queryStringIndex >= 0) ? uriWithQueryString.substring(0, queryStringIndex) : uriWithQueryString;
117+
String uriWithoutStableQueryString = uriWithQueryString.replaceAll(String.format("%s\\s*=\\s*\\byes|no\\b\\s*&+", KEY_STABLE), "");
118+
if (uriWithoutStableQueryString.endsWith("?")) {
119+
uriWithoutStableQueryString = uriWithoutStableQueryString.substring(0, uriWithoutStableQueryString.length() - 1);
120+
}
121+
122+
final XmldbURI uri;
123+
try {
124+
uri = XmldbURI.xmldbUriFor(uriWithoutQueryString);
125+
} catch (URISyntaxException e) {
126+
throw new XPathException(this, ErrorCodes.FODC0004, String.format("\"%s\" is not a valid URI.", args[0].toString()));
127+
}
128+
129+
final Map<String, String> queryStringMap = parseQueryString(uriWithQueryString);
130+
checkQueryStringMap(queryStringMap);
131+
132+
if ((!queryStringMap.containsKey(KEY_STABLE) || queryStringMap.get(KEY_STABLE).equals(VALUE_STABLE_YES)) &&
133+
context.getCachedUriCollectionResults().containsKey(uriWithoutStableQueryString)) {
134+
result = context.getCachedUriCollectionResults().get(uriWithoutStableQueryString);
135+
} else {
136+
final boolean binaryUrisIncluded = !queryStringMap.containsKey(KEY_CONTENT_TYPE) ||
137+
(queryStringMap.get(KEY_CONTENT_TYPE).equals(VALUE_CONTENT_TYPE_DOCUMENT) ||
138+
queryStringMap.get(KEY_CONTENT_TYPE).equals(VALUE_CONTENT_TYPE_DOCUMENT_BINARY));
139+
final boolean subcollectionUrisIncluded = !queryStringMap.containsKey(KEY_CONTENT_TYPE) ||
140+
queryStringMap.get(KEY_CONTENT_TYPE).equals(VALUE_CONTENT_TYPE_SUBCOLLECTION);
141+
final boolean xmlUrisIncluded = !queryStringMap.containsKey(KEY_CONTENT_TYPE) ||
142+
(queryStringMap.get(KEY_CONTENT_TYPE).equals(VALUE_CONTENT_TYPE_DOCUMENT) ||
143+
queryStringMap.get(KEY_CONTENT_TYPE).equals(VALUE_CONTENT_TYPE_DOCUMENT_XML));
144+
145+
try (final Collection collection = context.getBroker().openCollection(uri, Lock.LockMode.READ_LOCK)) {
146+
if (collection != null) {
147+
if (binaryUrisIncluded || xmlUrisIncluded) {
148+
final Iterator<DocumentImpl> documentIterator = collection.iterator(context.getBroker());
149+
while (documentIterator.hasNext()) {
150+
final DocumentImpl document = documentIterator.next();
151+
if ((xmlUrisIncluded && !(document instanceof BinaryDocument)) ||
152+
(binaryUrisIncluded && document instanceof BinaryDocument)) {
153+
resultUris.add(document.getURI().toString());
154+
}
155+
}
156+
}
157+
158+
if (subcollectionUrisIncluded) {
159+
final Iterator<XmldbURI> collectionsIterator = collection.collectionIterator(context.getBroker());
160+
while (collectionsIterator.hasNext()) {
161+
resultUris.add(uri.append(collectionsIterator.next()).toString());
162+
}
163+
}
164+
} else {
165+
throw new XPathException(this, ErrorCodes.FODC0002, String.format("Collection \"%s\" not found.", uri));
166+
}
167+
} catch (final LockException | PermissionDeniedException e) {
168+
throw new XPathException(this, ErrorCodes.FODC0002, e);
169+
}
170+
171+
if (queryStringMap.containsKey(KEY_MATCH) && queryStringMap.get(KEY_MATCH).length() > 0) {
172+
final Pattern pattern = PatternFactory.getInstance().getPattern(queryStringMap.get(KEY_MATCH));
173+
final List<String> matchedResultUris = resultUris.stream().filter(resultUri -> pattern.matcher(resultUri).find()).collect(Collectors.toList());
174+
if (matchedResultUris.isEmpty()) {
175+
result = Sequence.EMPTY_SEQUENCE;
176+
} else {
177+
result = new ValueSequence();
178+
for (String resultUri : matchedResultUris) {
179+
result.add(new AnyURIValue(resultUri));
180+
}
181+
}
182+
} else {
183+
result = new ValueSequence();
184+
for (String resultUri : resultUris) {
185+
result.add(new AnyURIValue(resultUri));
186+
}
187+
}
188+
189+
// only store the result if they were not previously stored - otherwise we loose stability!
190+
if (!context.getCachedUriCollectionResults().containsKey(uriWithoutStableQueryString)) {
191+
context.getCachedUriCollectionResults().put(uriWithoutStableQueryString, result);
192+
}
193+
}
194+
}
195+
196+
return result;
197+
}
198+
199+
private static Map<String, String> parseQueryString(final String uri) {
200+
final Map<String, String> map = new HashMap<>();
201+
if (uri != null) {
202+
final int questionMarkIndex = uri.indexOf('?');
203+
if (questionMarkIndex >= 0 && questionMarkIndex + 1 < uri.length()) {
204+
String[] keyValuePairs = uri.substring(questionMarkIndex + 1).split("&");
205+
for (String keyValuePair : keyValuePairs) {
206+
int equalIndex = keyValuePair.indexOf('=');
207+
if (equalIndex >= 0) {
208+
if (equalIndex + 1 < uri.length()) {
209+
map.put(keyValuePair.substring(0, equalIndex).trim(), keyValuePair.substring(equalIndex + 1).trim());
210+
} else {
211+
map.put(keyValuePair.substring(0, equalIndex).trim(), "");
212+
}
213+
} else {
214+
map.put(keyValuePair.trim(), "");
215+
}
216+
}
217+
}
218+
}
219+
220+
return map;
221+
}
222+
223+
private void checkQueryStringMap(final Map<String, String> queryStringMap) throws XPathException {
224+
for (Map.Entry<String, String> queryStringEntry : queryStringMap.entrySet()) {
225+
final String key = queryStringEntry.getKey();
226+
final String value = queryStringEntry.getValue();
227+
if (key.equals(KEY_CONTENT_TYPE)) {
228+
if (Arrays.stream(VALUE_CONTENT_TYPES).noneMatch(contentTypeValue -> contentTypeValue.equals(value))) {
229+
throw new XPathException(this, ErrorCodes.FODC0004, String.format("Invalid query-string value \"%s\".", queryStringEntry));
230+
}
231+
} else if (key.equals(KEY_STABLE)) {
232+
if (Arrays.stream(VALUE_STABLES).noneMatch(stableValue -> stableValue.equals(value))) {
233+
throw new XPathException(this, ErrorCodes.FODC0004, String.format("Invalid query-string value \"%s\".", queryStringEntry));
234+
}
235+
} else if (!key.equals(KEY_MATCH)) {
236+
throw new XPathException(this, ErrorCodes.FODC0004, String.format("Unexpected query string \"%s\".", queryStringEntry));
237+
}
238+
}
239+
}
240+
}

0 commit comments

Comments
 (0)