• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

evolvedbinary / elemental / 982

29 Apr 2025 08:34PM UTC coverage: 56.409% (+0.007%) from 56.402%
982

push

circleci

adamretter
[feature] Improve README.md badges

28451 of 55847 branches covered (50.94%)

Branch coverage included in aggregate %.

77468 of 131924 relevant lines covered (58.72%)

0.59 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.86
/exist-core/src/main/java/org/exist/xquery/functions/fn/FunUriCollection.java
1
/*
2
 * Elemental
3
 * Copyright (C) 2024, Evolved Binary Ltd
4
 *
5
 * admin@evolvedbinary.com
6
 * https://www.evolvedbinary.com | https://www.elemental.xyz
7
 *
8
 * Use of this software is governed by the Business Source License 1.1
9
 * included in the LICENSE file and at www.mariadb.com/bsl11.
10
 *
11
 * Change Date: 2028-04-27
12
 *
13
 * On the date above, in accordance with the Business Source License, use
14
 * of this software will be governed by the Apache License, Version 2.0.
15
 *
16
 * Additional Use Grant: Production use of the Licensed Work for a permitted
17
 * purpose. A Permitted Purpose is any purpose other than a Competing Use.
18
 * A Competing Use means making the Software available to others in a commercial
19
 * product or service that: substitutes for the Software; substitutes for any
20
 * other product or service we offer using the Software that exists as of the
21
 * date we make the Software available; or offers the same or substantially
22
 * similar functionality as the Software.
23
 *
24
 * NOTE: Parts of this file contain code from 'The eXist-db Authors'.
25
 *       The original license header is included below.
26
 *
27
 * =====================================================================
28
 *
29
 * eXist-db Open Source Native XML Database
30
 * Copyright (C) 2001 The eXist-db Authors
31
 *
32
 * info@exist-db.org
33
 * http://www.exist-db.org
34
 *
35
 * This library is free software; you can redistribute it and/or
36
 * modify it under the terms of the GNU Lesser General Public
37
 * License as published by the Free Software Foundation; either
38
 * version 2.1 of the License, or (at your option) any later version.
39
 *
40
 * This library is distributed in the hope that it will be useful,
41
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
42
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
43
 * Lesser General Public License for more details.
44
 *
45
 * You should have received a copy of the GNU Lesser General Public
46
 * License along with this library; if not, write to the Free Software
47
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
48
 */
49
package org.exist.xquery.functions.fn;
50

51
import org.exist.collections.Collection;
52
import org.exist.dom.persistent.BinaryDocument;
53
import org.exist.dom.persistent.DocumentImpl;
54
import org.exist.security.PermissionDeniedException;
55
import org.exist.storage.lock.Lock;
56
import org.exist.util.LockException;
57
import org.exist.util.PatternFactory;
58
import org.exist.xmldb.XmldbURI;
59
import org.exist.xquery.*;
60
import org.exist.xquery.value.*;
61

62
import java.net.URISyntaxException;
63
import java.util.*;
64
import java.util.regex.Pattern;
65
import java.util.stream.Collectors;
66

67
import static org.exist.xquery.FunctionDSL.*;
68
import static org.exist.xquery.functions.fn.FnModule.functionSignatures;
69

70
public class FunUriCollection extends BasicFunction {
71

72
    private static final String FN_NAME = "uri-collection";
73
    private static final String FN_DESCRIPTION = "Returns a sequence of xs:anyURI values that represent the URIs in a URI collection.";
74
    private static final FunctionReturnSequenceType FN_RETURN = returnsOptMany(Type.ANY_URI,
1✔
75
            "the default URI collection, if $arg is not specified or is an empty sequence, " +
1✔
76
                    "or the sequence of URIs that correspond to the supplied URI");
77
    private static final FunctionParameterSequenceType ARG = optParam("arg", Type.STRING,
1✔
78
            "An xs:string identifying a URI Collection. " +
1✔
79
                    "The argument is interpreted as either an absolute xs:anyURI, or a relative xs:anyURI resolved " +
80
                    "against the base-URI property from the static context. In Elemental this function consults the " +
81
                    "query hierarchy of the database. Query String parameters may be provided to " +
82
                    "control the URIs returned by this function. " +
83
                    "The parameter `match` may be used to provide a Regular Expression against which the result " +
84
                    "sequence of URIs are filtered. " +
85
                    "The parameter `content-type` may be used to determine the Internet Media Type (or generally " +
86
                    "whether XML, Binary, and/or (Sub) Collection) URIs that are returned in the result sequence; " +
87
                    "the special values: 'application/vnd.existdb.collection' includes (Sub) Collections, " +
88
                    "'application/vnd.existdb.document' includes any document, " +
89
                    "'application/vnd.existdb.document+xml' includes only XML documents, and " +
90
                    "'application/vnd.existdb.document+binary' includes only Binary documents. By default, " +
91
                    "`content-type=application/vnd.existdb.collection,application/vnd.existdb.document` " +
92
                    "(i.e. all Collections and Documents). " +
93
                    "The parameter `stable` may be used to determine if the function is deterministic. " +
94
                    "By default `stable=yes` to ensure that the same results are returned by each call within the same " +
95
                    "query."
96
    );
97
    public static final FunctionSignature[] FS_URI_COLLECTION_SIGNATURES = functionSignatures(
1✔
98
            FN_NAME,
1✔
99
            FN_DESCRIPTION,
1✔
100
            FN_RETURN,
1✔
101
            arities(
1✔
102
                    arity(),
1✔
103
                    arity(ARG)
1✔
104
            )
105
        );
106

107
    private static final String KEY_CONTENT_TYPE = "content-type";
108
    private static final String VALUE_CONTENT_TYPE_DOCUMENT = "application/vnd.existdb.document";
109
    private static final String VALUE_CONTENT_TYPE_DOCUMENT_BINARY = "application/vnd.existdb.document+binary";
110
    private static final String VALUE_CONTENT_TYPE_DOCUMENT_XML = "application/vnd.existdb.document+xml";
111
    private static final String VALUE_CONTENT_TYPE_SUBCOLLECTION = "application/vnd.existdb.collection";
112
    private static final String[] VALUE_CONTENT_TYPES = {
1✔
113
            VALUE_CONTENT_TYPE_DOCUMENT,
1✔
114
            VALUE_CONTENT_TYPE_DOCUMENT_BINARY,
1✔
115
            VALUE_CONTENT_TYPE_DOCUMENT_XML,
1✔
116
            VALUE_CONTENT_TYPE_SUBCOLLECTION
1✔
117
    };
118

119
    private static final String KEY_STABLE = "stable";
120
    private static final String VALUE_STABLE_NO = "no";
121
    private static final String VALUE_STABLE_YES = "yes";
122
    private static final String[] VALUE_STABLES = {
1✔
123
            VALUE_STABLE_NO,
1✔
124
            VALUE_STABLE_YES
1✔
125
    };
126

127
    private static final String KEY_MATCH = "match";
1✔
128

129
    public FunUriCollection(final XQueryContext context, final FunctionSignature signature) {
130
        super(context, signature);
1✔
131
    }
1✔
132

133
    public Sequence eval(final Sequence[] args, final Sequence contextSequence) throws XPathException {
134
        final Sequence result;
135
        if (args.length == 0 || args[0].isEmpty() || args[0].toString().isEmpty()) {
1!
136
            result = new AnyURIValue(XmldbURI.ROOT_COLLECTION);
1✔
137
        } else {
1✔
138
            final List<String> resultUris = new ArrayList<>();
1✔
139

140
            final String uriWithQueryString = args[0].toString();
1✔
141
            final int queryStringIndex = uriWithQueryString.indexOf('?');
1✔
142
            final String uriWithoutQueryString = (queryStringIndex >= 0) ? uriWithQueryString.substring(0, queryStringIndex) : uriWithQueryString;
1✔
143
            String uriWithoutStableQueryString = uriWithQueryString.replaceAll(String.format("%s\\s*=\\s*\\byes|no\\b\\s*&+", KEY_STABLE), "");
1✔
144
            if (uriWithoutStableQueryString.endsWith("?")) {
1✔
145
                uriWithoutStableQueryString = uriWithoutStableQueryString.substring(0, uriWithoutStableQueryString.length() - 1);
1✔
146
            }
147

148
            final XmldbURI uri;
149
            try {
150
                uri = XmldbURI.xmldbUriFor(uriWithoutQueryString);
1✔
151
            } catch (URISyntaxException e) {
1✔
152
                throw new XPathException(this, ErrorCodes.FODC0004, String.format("\"%s\" is not a valid URI.", args[0].toString()));
1✔
153
            }
154

155
            final Map<String, String> queryStringMap = parseQueryString(uriWithQueryString);
1✔
156
            checkQueryStringMap(queryStringMap);
1✔
157

158
            if ((!queryStringMap.containsKey(KEY_STABLE) || queryStringMap.get(KEY_STABLE).equals(VALUE_STABLE_YES)) &&
1✔
159
                    context.getCachedUriCollectionResults().containsKey(uriWithoutStableQueryString)) {
1✔
160
                result = context.getCachedUriCollectionResults().get(uriWithoutStableQueryString);
1✔
161
            } else {
1✔
162
                final boolean binaryUrisIncluded = !queryStringMap.containsKey(KEY_CONTENT_TYPE) ||
1✔
163
                        (queryStringMap.get(KEY_CONTENT_TYPE).equals(VALUE_CONTENT_TYPE_DOCUMENT) ||
1✔
164
                         queryStringMap.get(KEY_CONTENT_TYPE).equals(VALUE_CONTENT_TYPE_DOCUMENT_BINARY));
1✔
165
                final boolean subcollectionUrisIncluded = !queryStringMap.containsKey(KEY_CONTENT_TYPE) ||
1✔
166
                        queryStringMap.get(KEY_CONTENT_TYPE).equals(VALUE_CONTENT_TYPE_SUBCOLLECTION);
1✔
167
                final boolean xmlUrisIncluded = !queryStringMap.containsKey(KEY_CONTENT_TYPE) ||
1✔
168
                        (queryStringMap.get(KEY_CONTENT_TYPE).equals(VALUE_CONTENT_TYPE_DOCUMENT) ||
1✔
169
                                queryStringMap.get(KEY_CONTENT_TYPE).equals(VALUE_CONTENT_TYPE_DOCUMENT_XML));
1✔
170

171
                try (final Collection collection = context.getBroker().openCollection(uri, Lock.LockMode.READ_LOCK)) {
1✔
172
                    if (collection != null) {
1✔
173
                        if (binaryUrisIncluded || xmlUrisIncluded) {
1✔
174
                            final Iterator<DocumentImpl> documentIterator = collection.iterator(context.getBroker());
1✔
175
                            while (documentIterator.hasNext()) {
1✔
176
                                final DocumentImpl document = documentIterator.next();
1✔
177
                                if ((xmlUrisIncluded && !(document instanceof BinaryDocument)) ||
1✔
178
                                        (binaryUrisIncluded && document instanceof BinaryDocument)) {
1✔
179
                                    resultUris.add(document.getURI().toString());
1✔
180
                                }
181
                            }
182
                        }
183

184
                        if (subcollectionUrisIncluded) {
1✔
185
                            final Iterator<XmldbURI> collectionsIterator = collection.collectionIterator(context.getBroker());
1✔
186
                            while (collectionsIterator.hasNext()) {
1✔
187
                                resultUris.add(uri.append(collectionsIterator.next()).toString());
1✔
188
                            }
189
                        }
190
                    } else {
1✔
191
                        throw new XPathException(this, ErrorCodes.FODC0002, String.format("Collection \"%s\" not found.", uri));
1✔
192
                    }
193
                } catch (final LockException | PermissionDeniedException e) {
×
194
                    throw new XPathException(this, ErrorCodes.FODC0002, e);
×
195
                }
196

197
                if (queryStringMap.containsKey(KEY_MATCH) && queryStringMap.get(KEY_MATCH).length() > 0) {
1!
198
                    final Pattern pattern = PatternFactory.getInstance().getPattern(queryStringMap.get(KEY_MATCH));
1✔
199
                    final List<String> matchedResultUris = resultUris.stream().filter(resultUri -> pattern.matcher(resultUri).find()).collect(Collectors.toList());
1✔
200
                    if (matchedResultUris.isEmpty()) {
1✔
201
                        result = Sequence.EMPTY_SEQUENCE;
1✔
202
                    } else {
1✔
203
                        result = new ValueSequence();
1✔
204
                        for (String resultUri : matchedResultUris) {
1✔
205
                            result.add(new AnyURIValue(resultUri));
1✔
206
                        }
207
                    }
208
                } else {
1✔
209
                    result = new ValueSequence();
1✔
210
                    for (String resultUri : resultUris) {
1✔
211
                        result.add(new AnyURIValue(resultUri));
1✔
212
                    }
213
                }
214

215
                // only store the result if they were not previously stored - otherwise we loose stability!
216
                if (!context.getCachedUriCollectionResults().containsKey(uriWithoutStableQueryString)) {
1✔
217
                    context.getCachedUriCollectionResults().put(uriWithoutStableQueryString, result);
1✔
218
                }
219
            }
220
        }
221

222
        return result;
1✔
223
    }
224

225
    private static Map<String, String> parseQueryString(final String uri) {
226
        final Map<String, String> map = new HashMap<>();
1✔
227
        if (uri != null) {
1!
228
            final int questionMarkIndex = uri.indexOf('?');
1✔
229
            if (questionMarkIndex >= 0 && questionMarkIndex + 1 < uri.length()) {
1!
230
                String[] keyValuePairs = uri.substring(questionMarkIndex + 1).split("&");
1✔
231
                for (String keyValuePair : keyValuePairs) {
1✔
232
                    int equalIndex = keyValuePair.indexOf('=');
1✔
233
                    if (equalIndex >= 0) {
1!
234
                        if (equalIndex + 1 < uri.length()) {
1!
235
                            map.put(keyValuePair.substring(0, equalIndex).trim(), keyValuePair.substring(equalIndex + 1).trim());
1✔
236
                        } else {
1✔
237
                            map.put(keyValuePair.substring(0, equalIndex).trim(), "");
×
238
                        }
239
                    } else {
×
240
                        map.put(keyValuePair.trim(), "");
×
241
                    }
242
                }
243
            }
244
        }
245

246
        return map;
1✔
247
    }
248

249
    private void checkQueryStringMap(final Map<String, String> queryStringMap) throws XPathException {
250
        for (Map.Entry<String, String> queryStringEntry : queryStringMap.entrySet()) {
1✔
251
            final String key = queryStringEntry.getKey();
1✔
252
            final String value = queryStringEntry.getValue();
1✔
253
            if (key.equals(KEY_CONTENT_TYPE)) {
1✔
254
                if (Arrays.stream(VALUE_CONTENT_TYPES).noneMatch(contentTypeValue -> contentTypeValue.equals(value))) {
1!
255
                    throw new XPathException(this, ErrorCodes.FODC0004, String.format("Invalid query-string value \"%s\".", queryStringEntry));
×
256
                }
257
            } else if (key.equals(KEY_STABLE)) {
1✔
258
                if (Arrays.stream(VALUE_STABLES).noneMatch(stableValue -> stableValue.equals(value))) {
1!
259
                    throw new XPathException(this, ErrorCodes.FODC0004, String.format("Invalid query-string value \"%s\".", queryStringEntry));
×
260
                }
261
            } else if (!key.equals(KEY_MATCH)) {
1!
262
                throw new XPathException(this, ErrorCodes.FODC0004, String.format("Unexpected query string \"%s\".", queryStringEntry));
×
263
            }
264
        }
265
    }
1✔
266
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc