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

eXist-db / exist / 40387836984

26 Oct 2026 07:33PM UTC coverage: 58.173% (+0.005%) from 58.168%
40387836984

push

github

web-flow
Merge pull request #6731 from line-o/bugfix/binary-value-frame-ownership

[bugfix] Release a binary value with the scope that created it

32491 of 61752 branches covered (52.62%)

Branch coverage included in aggregate %.

92 of 129 new or added lines in 8 files covered. (71.32%)

27 existing lines in 6 files now uncovered.

85598 of 141244 relevant lines covered (60.6%)

2.73 hits per line

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

75.41
/exist-core/src/main/java/org/exist/xquery/XQueryContext.java
1
/*
2
 * eXist-db Open Source Native XML Database
3
 * Copyright (C) 2001 The eXist-db Authors
4
 *
5
 * info@exist-db.org
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
package org.exist.xquery;
23

24
import java.io.IOException;
25
import java.io.Reader;
26
import java.lang.reflect.Constructor;
27
import java.lang.reflect.InvocationTargetException;
28
import java.net.MalformedURLException;
29
import java.net.URI;
30
import java.net.URISyntaxException;
31
import java.nio.charset.Charset;
32
import java.nio.file.Path;
33
import java.util.*;
34
import java.util.concurrent.CopyOnWriteArrayList;
35
import java.util.concurrent.ThreadPoolExecutor;
36
import java.util.concurrent.atomic.AtomicReference;
37
import java.util.function.BiFunction;
38
import java.util.function.Consumer;
39
import java.util.function.Predicate;
40

41
import javax.annotation.Nullable;
42
import javax.xml.datatype.DatatypeConfigurationException;
43
import javax.xml.datatype.DatatypeFactory;
44
import javax.xml.datatype.Duration;
45
import javax.xml.datatype.XMLGregorianCalendar;
46
import javax.xml.stream.XMLStreamException;
47

48
import antlr.RecognitionException;
49
import antlr.TokenStreamException;
50
import antlr.collections.AST;
51
import com.evolvedbinary.j8fu.Either;
52
import com.evolvedbinary.j8fu.function.TriFunctionE;
53
import com.evolvedbinary.j8fu.function.QuadFunctionE;
54
import com.evolvedbinary.j8fu.tuple.Tuple2;
55
import com.ibm.icu.text.Collator;
56
import it.unimi.dsi.fastutil.Hash;
57
import it.unimi.dsi.fastutil.objects.*;
58
import net.jcip.annotations.Immutable;
59
import net.jcip.annotations.ThreadSafe;
60
import org.apache.logging.log4j.LogManager;
61
import org.apache.logging.log4j.Logger;
62
import org.exist.Database;
63
import org.exist.EXistException;
64
import org.exist.Namespaces;
65
import org.exist.collections.Collection;
66
import org.exist.debuggee.Debuggee;
67
import org.exist.debuggee.DebuggeeJoint;
68
import org.exist.dom.persistent.*;
69
import org.exist.dom.QName;
70
import org.exist.http.servlets.*;
71
import org.exist.dom.memtree.InMemoryXMLStreamReader;
72
import org.exist.dom.memtree.MemTreeBuilder;
73
import org.exist.dom.memtree.NodeImpl;
74
import org.exist.numbering.NodeId;
75
import org.exist.repo.ExistRepository;
76
import org.exist.security.AuthenticationException;
77
import org.exist.security.Permission;
78
import org.exist.security.PermissionDeniedException;
79
import org.exist.security.Subject;
80
import org.exist.source.*;
81
import org.exist.stax.ExtendedXMLStreamReader;
82
import org.exist.storage.BrokerPool;
83
import org.exist.storage.DBBroker;
84
import org.exist.storage.UpdateListener;
85
import org.exist.storage.lock.Lock.LockMode;
86
import org.exist.storage.lock.LockedDocumentMap;
87
import org.exist.storage.txn.Txn;
88
import org.exist.util.*;
89
import org.exist.util.hashtable.NamePool;
90
import org.exist.xmldb.XmldbURI;
91
import org.exist.xquery.parser.*;
92
import org.exist.xquery.pragmas.*;
93
import org.exist.xquery.update.Modification;
94
import org.exist.xquery.util.DocUtils;
95
import org.exist.xquery.util.SerializerUtils;
96
import org.exist.xquery.value.*;
97
import org.jgrapht.Graph;
98
import org.jgrapht.alg.interfaces.ShortestPathAlgorithm;
99
import org.jgrapht.alg.shortestpath.TransitNodeRoutingShortestPath;
100
import org.jgrapht.graph.DefaultEdge;
101
import org.jgrapht.graph.DefaultGraphType;
102
import org.jgrapht.opt.graph.fastutil.FastutilMapGraph;
103
import org.jgrapht.util.ConcurrencyUtil;
104
import org.jgrapht.util.SupplierUtil;
105
import org.w3c.dom.Node;
106

107
import static com.evolvedbinary.j8fu.OptionalUtil.or;
108
import static com.evolvedbinary.j8fu.tuple.Tuple.Tuple;
109
import static javax.xml.XMLConstants.XMLNS_ATTRIBUTE;
110
import static javax.xml.XMLConstants.XML_NS_PREFIX;
111
import static org.apache.commons.lang3.ArrayUtils.isEmpty;
112
import static org.apache.commons.lang3.ArrayUtils.isNotEmpty;
113
import static org.exist.Namespaces.XML_NS;
114
import static org.exist.util.MapUtil.hashMap;
115

116
/**
117
 * The current XQuery execution context. Contains the static as well as the dynamic
118
 * XQuery context components.
119
 *
120
 * @author <a href="mailto:wolfgang@exist-db.org">Wolfgang Meier</a>
121
 */
122
public class XQueryContext implements BinaryValueManager, Context {
123

124
    private static final Logger LOG = LogManager.getLogger(XQueryContext.class);
3 ✔
125

126
    public static final String ENABLE_QUERY_REWRITING_ATTRIBUTE = "enable-query-rewriting";
127
    public static final String XQUERY_BACKWARD_COMPATIBLE_ATTRIBUTE = "backwardCompatible";
128
    public static final String XQUERY_RAISE_ERROR_ON_FAILED_RETRIEVAL_ATTRIBUTE = "raise-error-on-failed-retrieval";
129
    public static final String ENFORCE_INDEX_USE_ATTRIBUTE = "enforce-index-use";
130

131
    //TODO : move elsewhere ?
132
    public static final String BUILT_IN_MODULE_URI_ATTRIBUTE = "uri";
133
    public static final String BUILT_IN_MODULE_CLASS_ATTRIBUTE = "class";
134
    public static final String BUILT_IN_MODULE_SOURCE_ATTRIBUTE = "src";
135

136
    public static final String PROPERTY_XQUERY_BACKWARD_COMPATIBLE = "xquery.backwardCompatible";
137
    public static final String PROPERTY_ENABLE_QUERY_REWRITING = "xquery.enable-query-rewriting";
138
    public static final String PROPERTY_XQUERY_RAISE_ERROR_ON_FAILED_RETRIEVAL = "xquery.raise-error-on-failed-retrieval";
139
    public static final boolean XQUERY_RAISE_ERROR_ON_FAILED_RETRIEVAL_DEFAULT = false;
140
    public static final String PROPERTY_ENFORCE_INDEX_USE = "xquery.enforce-index-use";
141

142
    //TODO : move elsewhere ?
143
    public static final String PROPERTY_BUILT_IN_MODULES = "xquery.modules";
144
    public static final String PROPERTY_STATIC_MODULE_MAP = "xquery.modules.static";
145
    public static final String PROPERTY_MODULE_PARAMETERS = "xquery.modules.parameters";
146
    public static final String PROPERTY_MODULE_REGISTRATIONS = "xquery.modules.registrations";
147

148
    public static final String JAVA_URI_START = "java:";
149
    //private static final String XMLDB_URI_START = "xmldb:exist://";
150

151
    private static final String TEMP_STORE_ERROR = "Error occurred while storing temporary data";
152
    public static final String XQUERY_CONTEXTVAR_XQUERY_UPDATE_ERROR = "_eXist_xquery_update_error";
153
    public static final String HTTP_SESSIONVAR_XMLDB_USER = "_eXist_xmldb_user";
154

155
    public static final String HTTP_REQ_ATTR_USER = "xquery.user";
156
    public static final String HTTP_REQ_ATTR_PASS = "xquery.password";
157

158
    // Static namespace/prefix mappings
159
    protected Map<String, String> staticNamespaces = new HashMap<>();
5 ✔
160

161
    // Static prefix/namespace mappings
162
    protected Map<String, String> staticPrefixes = new HashMap<>();
5 ✔
163

164
    // Local in-scope namespace/prefix mappings in the current context
165
    Map<String, String> inScopeNamespaces = new HashMap<>();
5 ✔
166

167
    // Local prefix/namespace mappings in the current context
168
    private Map<String, String> inScopePrefixes = new HashMap<>();
5 ✔
169

170
    // Inherited in-scope namespace/prefix mappings in the current context
171
    private Map<String, String> inheritedInScopeNamespaces = new HashMap<>();
5 ✔
172

173
    // Inherited prefix/namespace mappings in the current context
174
    private Map<String, String> inheritedInScopePrefixes = new HashMap<>();
5 ✔
175

176
    private boolean preserveNamespaces = true;
3 ✔
177

178
    private boolean inheritNamespaces = true;
3 ✔
179

180
    // Local namespace stack
181
    private final Deque<Map<String, String>> namespaceStack = new ArrayDeque<>();
5 ✔
182

183
    // Known user defined functions in the local module
184
    private Map<FunctionId, UserDefinedFunction> declaredFunctions = new Object2ObjectAVLTreeMap<>();
5 ✔
185

186
    // Globally declared variables
187
    protected Map<QName, Variable> globalVariables = new Object2ObjectRBTreeMap<>();
5 ✔
188

189
    // The last element in the linked list of local in-scope variables
190
    private LocalVariable lastVar = null;
3 ✔
191

192
    /**
193
     * O(1) lookup table from QName to the most-recently-declared LocalVariable
194
     * with that name. Maintained alongside the {@link #lastVar} linked list:
195
     * declareVariableBinding adds, popLocalVariables restores the prevSameName
196
     * chain. Visibility is enforced in resolveLocalVariable by comparing
197
     * {@link LocalVariable#markedUnder} to {@code contextStack.peek()}.
198
     */
199
    // Shared by reference with copies of this context (see copyFields,
200
    // updateContext) just like {@link #contextStack} and {@link #lastVar}.
201
    private Map<QName, LocalVariable> localVariableLookup = new HashMap<>();
5 ✔
202

203
    private Deque<LocalVariable> contextStack = new ArrayDeque<>();
5 ✔
204

205
    private final Deque<FunctionSignature> callStack = new ArrayDeque<>();
5 ✔
206

207
    // The current size of the variable stack
208
    private int variableStackSize = 0;
3 ✔
209

210
    /** Initial capacity of {@link #binaryValueFrameStarts}, grown on demand. */
211
    private static final int INITIAL_BINARY_VALUE_FRAMES = 8;
212

213
    /**
214
     * The {@link BinaryValue}s registered during query execution, in registration order.
215
     *
216
     * <p>The list is divided into frames by {@link #binaryValueFrameStarts}. A value belongs to
217
     * the innermost frame open when it was registered, and is released when that frame is left -
218
     * never by a frame which merely received it. This is what stops a called function from
219
     * closing its caller's value.</p>
220
     */
221
    private @Nullable List<BinaryValue> binaryValueInstances;
222

223
    /**
224
     * The index into {@link #binaryValueInstances} at which each open frame starts, innermost last.
225
     *
226
     * <p>Allocated lazily with {@link #binaryValueInstances}: until a query registers a binary
227
     * value there is nothing to own, so {@link #pushBinaryValueFrame()} only counts. Frames
228
     * already open at that point start at index 0, which is where registration begins - hence
229
     * the zero-filled array.</p>
230
     */
231
    private @Nullable int[] binaryValueFrameStarts;
232

233
    /** The number of open frames, counted whether or not any binary value has been registered. */
234
    private int binaryValueFrameDepth;
235

236
    // Unresolved references to user defined functions
237
    private final Deque<FunctionCall> forwardReferences = new ArrayDeque<>();
5 ✔
238

239
    // Inline functions using closures need to be cleared after execution
240
    private final Deque<UserDefinedFunction> closures = new ArrayDeque<>();
5 ✔
241

242
    // List of options declared for this query at compile time - i.e. declare option
243
    private List<Option> staticOptions = null;
3 ✔
244

245
    // List of options declared for this query at run time - i.e. util:declare-option()
246
    private List<Option> dynamicOptions = null;
3 ✔
247

248
    //The Calendar for this context : may be changed by some options
249
    private XMLGregorianCalendar calendar = null;
3 ✔
250
    private TimeZone implicitTimeZone = null;
3 ✔
251

252
    private final Map<String, Sequence> cachedUriCollectionResults = new HashMap<>();
5 ✔
253

254
    /**
255
     * the watchdog object assigned to this query.
256
     */
257
    protected XQueryWatchDog watchdog;
258

259
    /**
260
     * Loaded modules within this module.
261
     * <p>
262
     * The format of the map is: <code>Map&lt;NamespaceURI, Modules&gt;</code>.
263
     */
264
    protected Object2ObjectMap<String, Module[]> modules = new Object2ObjectOpenHashMap<>(8, Hash.VERY_FAST_LOAD_FACTOR);
7 ✔
265

266
    /**
267
     * Loaded modules, including ones bubbled up from imported modules.
268
     * <p>
269
     * The format of the map is: <code>Map&lt;NamespaceURI, Modules&gt;</code>
270
     */
271
    private Object2ObjectMap<String, Module[]> allModules = new Object2ObjectOpenHashMap<>();
5 ✔
272

273
    /**
274
     * Describes a graph of all the modules and how they import each other.
275
     */
276
    private @Nullable Graph<ModuleVertex, DefaultEdge> modulesDependencyGraph;
277
    private @Nullable ThreadPoolExecutor modulesDependencyGraphSPExecutor;
278

279
    /**
280
     * Used to save current state when modules are imported dynamically
281
     */
282
    private final SavedState savedState = new SavedState();
6 ✔
283

284
    /**
285
     * Whether some modules were rebound to new instances since the last time this context's query was analyzed. (This assumes that each context is
286
     * attached to at most one query.)
287
     */
288
    @SuppressWarnings("unused")
289
    private boolean modulesChanged = true;
3 ✔
290

291
    /**
292
     * The set of statically known documents specified as an array of paths to documents and collections.
293
     */
294
    private XmldbURI[] staticDocumentPaths = null;
3 ✔
295

296
    /**
297
     * The actual set of statically known documents. This will be generated on demand from staticDocumentPaths.
298
     */
299
    private DocumentSet staticDocuments = null;
3 ✔
300

301
    /**
302
     * The available documents of the dynamic context.
303
     * <p>
304
     * {@see https://www.w3.org/TR/xpath-31/#dt-available-docs}.
305
     */
306
    private Map<String, TriFunctionE<DBBroker, Txn, String, Either<org.exist.dom.memtree.DocumentImpl, DocumentImpl>, XPathException>> dynamicDocuments = null;
3 ✔
307

308
    /**
309
     * The available test resources of the dynamic context.
310
     * <p>
311
     * {@see https://www.w3.org/TR/xpath-31/#dt-available-text-resources}.
312
     */
313
    private Map<Tuple2<String, Charset>, QuadFunctionE<DBBroker, Txn, String, Charset, Reader, XPathException>> dynamicTextResources = null;
3 ✔
314

315
    /**
316
     * The available collections of the dynamic context.
317
     * <p>
318
     * {@see https://www.w3.org/TR/xpath-31/#dt-available-collections}.
319
     */
320
    private Map<String, TriFunctionE<DBBroker, Txn, String, Sequence, XPathException>> dynamicCollections = null;
3 ✔
321

322
    /**
323
     * A set of documents which were modified during the query, usually through an XQuery update extension. The documents will be checked after the
324
     * query completed to see if a defragmentation run is needed.
325
     */
326
    protected MutableDocumentSet modifiedDocuments = null;
3 ✔
327

328
    /**
329
     * A general-purpose map to set attributes in the current query context.
330
     */
331
    protected Map<String, Object> attributes = new HashMap<>();
5 ✔
332

333
    protected AnyURIValue baseURI = AnyURIValue.EMPTY_URI;
3 ✔
334

335
    private boolean baseURISetInProlog = false;
3 ✔
336

337
    protected String moduleLoadPath = ".";
3 ✔
338

339
    private String defaultFunctionNamespace = Function.BUILTIN_FUNCTION_NS;
3 ✔
340
    private AnyURIValue defaultElementNamespace = AnyURIValue.EMPTY_URI;
3 ✔
341
    private AnyURIValue defaultElementNamespaceSchema = AnyURIValue.EMPTY_URI;
3 ✔
342

343
    /**
344
     * The default collation URI.
345
     */
346
    private String defaultCollation = Collations.UNICODE_CODEPOINT_COLLATION_URI;
3 ✔
347

348
    /**
349
     * The default language
350
     */
351
    private static final String DefaultLanguage = Locale.getDefault().getLanguage();
3 ✔
352

353
    /**
354
     * Default Collator. Will be null for the default unicode codepoint collation.
355
     */
356
    private Collator defaultCollator = null;
3 ✔
357

358
    /**
359
     * Set to true to enable XPath 1.0 backwards compatibility.
360
     */
361
    private boolean backwardsCompatible = false;
3 ✔
362

363
    /**
364
     * Should whitespace inside node constructors be stripped?
365
     */
366
    private boolean stripWhitespace = true;
3 ✔
367

368
    /**
369
     * Should empty order greatest or least?
370
     */
371
    private boolean orderEmptyGreatest = false;
3 ✔
372

373
    /**
374
     * XQuery 3.0 - declare context item :=
375
     */
376
    private ContextItemDeclaration contextItemDeclaration = null;
3 ✔
377

378
    /**
379
     * The context item set in the query prolog or externally
380
     */
381
    private Sequence contextItem = Sequence.EMPTY_SEQUENCE;
3 ✔
382

383
    /**
384
     * The position of the currently processed item in the context sequence. This field has to be set on demand, for example, before calling the
385
     * fn:position() function.
386
     */
387
    private int contextPosition = 0;
3 ✔
388
    private Sequence contextSequence = null;
3 ✔
389

390
    /**
391
     * Shared name pool used by all in-memory documents constructed in this query context.
392
     */
393
    private NamePool sharedNamePool = null;
3 ✔
394

395
    /**
396
     * Stack for temporary document fragments.
397
     */
398
    private Deque<MemTreeBuilder> fragmentStack = new ArrayDeque<>();
5 ✔
399

400
    /**
401
     * The root of the expression tree.
402
     */
403
    private Expression rootExpression;
404

405
    /**
406
     * An incremental counter to count the expressions in the current XQuery. Used during compilation to assign a unique ID to every expression.
407
     */
408
    private int expressionCounter = 0;
3 ✔
409

410
    private LockedDocumentMap protectedDocuments = null;
3 ✔
411

412
    /**
413
     * The profiler instance used by this context.
414
     */
415
    protected Profiler profiler;
416

417
    //For holding the environment variables
418
    private Map<String, String> envs;
419

420
    private ContextUpdateListener updateListener = null;
3 ✔
421

422
    private boolean enableOptimizer = true;
3 ✔
423

424
    private boolean raiseErrorOnFailedRetrieval = XQUERY_RAISE_ERROR_ON_FAILED_RETRIEVAL_DEFAULT;
3 ✔
425

426
    private boolean isShared = false;
3 ✔
427

428
    private Source source = null;
3 ✔
429

430
    /**
431
     * How much of a failed execution may be disclosed to the caller. Recomputed from the current
432
     * subject on every execution and never cached with the compiled query, see {@link ErrorDisclosure}.
433
     */
434
    private ErrorDisclosure errorDisclosure = ErrorDisclosure.FULL;
3 ✔
435

436
    private DebuggeeJoint debuggeeJoint = null;
3 ✔
437

438
    private int xqueryVersion = 31;
3 ✔
439

440
    protected Database db;
441

442
    protected Configuration configuration;
443

444
    private boolean analyzed = false;
3 ✔
445

446
    /**
447
     * The Subject of the User that requested the execution of the XQuery
448
     * attached by this Context. This is not the same as the Effective User
449
     * as we may be executed setUid or setGid. The Effective User can be retrieved
450
     * through broker.getCurrentSubject()
451
     */
452
    private Subject realUser;
453

454
    /**
455
     * Indicates whether a user from a http session
456
     * was pushed onto the current broker from {@link XQueryContext#prepareForExecution()},
457
     * if so then we must pop the user in {@link XQueryContext#reset(boolean)}
458
     */
459
    private boolean pushedUserFromHttpSession = false;
3 ✔
460

461
    /**
462
     * The HTTP context within which the XQuery
463
     * is executing, or null if there is no
464
     * HTTP context.
465
     */
466
    private @Nullable HttpContext httpContext = null;
3 ✔
467
    /**
468
     * Sentinel QName for the default (unnamed) decimal format per XQuery 3.1 §4.10.
469
     */
470
    private static final QName UNNAMED_DECIMAL_FORMAT = new QName("__UNNAMED__", Function.BUILTIN_FUNCTION_NS);
6 ✔
471

472
    private final Map<QName, DecimalFormat> staticDecimalFormats = hashMap(Tuple(UNNAMED_DECIMAL_FORMAT, DecimalFormat.UNNAMED));
11 ✔
473

474
    // Only used for testing, e.g. {@code org.exist.test.runner.XQueryTestRunner}.
475
    private Optional<ExistRepository> testRepository = Optional.empty();
3 ✔
476

477
    /**
478
     * Holds a list of any new XQuery Contexts that
479
     * were created by the XQuery (owning this XQuery Context)
480
     * dynamically importing, compiling, and/or evaluating modules.
481
     * <p>
482
     * NOTE(AR) - This is needed to ensure that these "imported contexts" are
483
     * also correctly reset and cleaned up when this XQuery is finished.
484
     */
485
    private Set<XQueryContext> importedContexts = null;
3 ✔
486

487
    /**
488
     * Holds a list of the {@link #runCleanupTasks(Predicate)} functions
489
     * of the {@link #importedContexts}.
490
     * <p>
491
     * NOTE(AR) - This is needed to ensure that these the user call
492
     * {@link #reset()} or {@link #runCleanupTasks(Predicate)}
493
     * in any order.
494
     */
495
    private List<Consumer<Predicate<Object>>> importedContextsCleanupTasksFns = null;
3 ✔
496

497
    public XQueryContext() {
498
        this(null, null, null);
5 ✔
499
    }
1 ✔
500

501
    public XQueryContext(final Configuration configuration) {
502
        this(null, configuration, null);
5 ✔
503
    }
1 ✔
504

505
    public XQueryContext(final Database db) {
506
        this(db, null, null);
5 ✔
507
    }
1 ✔
508

509
    public XQueryContext(final Database db, final Profiler profiler) {
510
        this(db, null, profiler);
5 ✔
511
    }
1 ✔
512

513
    public XQueryContext(@Nullable final Database db, @Nullable final Configuration configuration, @Nullable final Profiler profiler) {
514
        this(db, configuration, profiler, true);
6 ✔
515
    }
1 ✔
516

517
    protected XQueryContext(@Nullable final Database db, @Nullable final Configuration configuration, @Nullable final Profiler profiler, final boolean loadDefaults) {
2 ✔
518
        this.db = db;
3 ✔
519

520
        // if needed, fallback to db.getConfiguration
521
        if (configuration != null) {
2 ✔
522
            this.configuration = configuration;
3 ✔
523
        } else if (db != null) {
3 ✔
524
            this.configuration = db.getConfiguration();
4 ✔
525
        } else {
1 ✔
526
            this.configuration = null;
3 ✔
527
        }
528

529
        // if needed, fallback to profiler for the db, or default profiler
530
        if (profiler != null) {
2 ✔
531
            this.profiler = profiler;
3 ✔
532
        } else if (db != null) {
3 ✔
533
            this.profiler = new Profiler(db);
6 ✔
534
        } else {
1 ✔
535
            this.profiler = new Profiler(null);
6 ✔
536
        }
537

538
        this.watchdog = new XQueryWatchDog(this);
6 ✔
539

540
        // load configuration defaults
541
        if (loadDefaults) {
2 ✔
542
            loadDefaults(this.configuration);
4 ✔
543
        }
544
    }
1 ✔
545

546
    public XQueryContext(final XQueryContext copyFrom) {
547
        this(copyFrom.db, copyFrom.configuration, copyFrom.profiler);
8 ✔
548

549
        for (final String prefix : copyFrom.staticNamespaces.keySet()) {
13 ✔
550
            if (XML_NS_PREFIX.equals(prefix) || XMLNS_ATTRIBUTE.equals(prefix)) {
8 !
551
                continue;
×
552
            }
553

554
            try {
555
                declareNamespace(prefix, copyFrom.staticNamespaces.get(prefix));
8 ✔
556
            } catch (final XPathException ex) {
1 ✔
557
                LOG.warn("Failed to copy namespace declaration for prefix '{}'", prefix, ex);
×
558
            }
559
        }
560

561
        // Copy in-scope namespaces registered via declareInScopeNamespace. Otherwise QName.parse()
562
        // will raise error XPST0081 when resolving path-step names in any of those.
563
        for (final Map.Entry<String, String> entry : copyFrom.inScopeNamespaces.entrySet()) {
13 ✔
564
            inScopeNamespaces.put(entry.getKey(), entry.getValue());
10 ✔
565
            inScopePrefixes.put(entry.getValue(), entry.getKey());
10 ✔
566
        }
567
    }
1 ✔
568

569

570
    /**
571
     * Get the HTTP context of the XQuery.
572
     *
573
     * @return the HTTP context, or null if the query
574
     * is not being executed within an HTTP context.
575
     */
576
    public @Nullable HttpContext getHttpContext() {
577
        return httpContext;
3 ✔
578
    }
579

580
    /**
581
     * Set the HTTP context of the XQuery.
582
     *
583
     * @param httpContext the HTTP context within which the XQuery
584
     *                    is being executed.
585
     */
586
    public void setHttpContext(@Nullable final HttpContext httpContext) {
587
        this.httpContext = httpContext;
3 ✔
588
    }
1 ✔
589

590
    /**
591
     * Set the EXPath repository used for testing,
592
     * only should be called from {@code org.exist.test.runner.XQueryTestRunner}.
593
     *
594
     * @param testRepository the EXPath repository to use for test execution.
595
     */
596
    public void setTestRepository(final Optional<ExistRepository> testRepository) {
597
        this.testRepository = testRepository;
3 ✔
598
    }
1 ✔
599

600
    /**
601
     * Get the EXPath repository configured for the BrokerPool, if present.
602
     *
603
     * @return the EXPath repository if present.
604
     */
605
    public Optional<ExistRepository> getRepository() {
606
        return or(
2 ✔
607
                testRepository,
2 ✔
608
                () -> Optional.ofNullable(getBroker()).map(DBBroker::getBrokerPool).flatMap(BrokerPool::getExpathRepo)
10 ✔
609
        );
610
    }
611

612
    /**
613
     * Resolve a Module from the EXPath Repository.
614
     *
615
     * @param namespace namespace URI
616
     * @param prefix namespace prefix
617
     *
618
     * @return the module or null
619
     *
620
     * @throws XPathException if the namespace URI is invalid (XQST0046),
621
     *     if the module could not be loaded (XQST0059) or compiled (XPST0003)
622
     */
623
    private @Nullable Module resolveInEXPathRepository(final String namespace, final String prefix)
624
            throws XPathException {
625
        // the repo and its eXist handler
626
        final Optional<ExistRepository> repo = getRepository();
3 ✔
627

628
        if (repo.isEmpty()) {
3 !
629
            return null;
×
630
        }
631

632
        // try an internal module
633
        final Module jMod = repo.get().resolveJavaModule(namespace, this);
7 ✔
634
        if (jMod != null) {
2 !
635
            return jMod;
×
636
        }
637

638
        // try an eXist-specific module
639
        final Path resolved = repo.get().resolveXQueryModule(namespace);
6 ✔
640
        if (resolved == null) {
2 ✔
641
            return null;
2 ✔
642
        }
643

644
        // use the resolved file
645
        String location = "";
2 ✔
646
        try {
647
            // see if the src exists in the database and if so, use that instead
648
            Source src = repo.get().resolveStoredXQueryModuleFromDb(getBroker(), resolved);
8 ✔
649
            if (src == null) {
2 !
650
                // fallback to load the source from the filesystem
651
                src = new FileSource(resolved, false);
×
652
            } else {
×
653
                final String sourceCollection = ((DBSource) src).getDocumentPath().getCollectionPath();
5 ✔
654
                if (".".equals(moduleLoadPath)) {
5 !
655
                    // module is a string passed to the xquery context, has therefore no location of its own
656
                    location = sourceCollection;
2 ✔
657
                } else {
1 ✔
658
                    // NOTE(AR) set the location of the module to import relative to this module's load path
659
                    // - so that transient imports of the imported module will resolve correctly!
660
                    location = relativizeOrFallback(moduleLoadPath, sourceCollection);
×
661
                }
662
            }
663

664
            // build a module object from the source
665
            return compileOrBorrowModule(namespace, prefix, location, src);
7 ✔
666

667
        } catch (final PermissionDeniedException | IllegalArgumentException e) {
×
668
            throw new XPathException(e.getMessage(), e);
×
669
        }
670
    }
671

672
    /**
673
     * Compute the location of an imported module relative to the importing module's load path,
674
     * falling back to the absolute source collection when the load path is not a relativizable
675
     * collection URI.
676
     *
677
     * Clients may send a synthetic value as the module load path for unsaved in-memory queries
678
     * (e.g. eXide sends {@code "xmldb:exist://__new__1"} for new untitled buffers). Such values
679
     * are not real collection URIs and cause {@link Path#relativize(Path)} to throw
680
     * {@link IllegalArgumentException}. In that case we fall back to the absolute source
681
     * collection so the import still resolves, matching the behaviour of the {@code "."}
682
     * load-path case.
683
     *
684
     * @param moduleLoadPath the load path of the importing module
685
     * @param sourceCollection the collection path of the module being imported
686
     * @return the relative location, or the absolute source collection if relativization fails
687
     */
688
    static String relativizeOrFallback(final String moduleLoadPath, final String sourceCollection) {
689
        try {
690
            final Path collectionPath = Path.of(XmldbURI.create(moduleLoadPath).getCollectionPath());
7 ✔
691
            final Path sourcePath = Path.of(sourceCollection);
5 ✔
692
            return collectionPath.relativize(sourcePath).toString();
5 ✔
693
        } catch (final IllegalArgumentException e) {
1 ✔
694
            LOG.debug("Module load path '{}' is not relativizable against source collection '{}'; using absolute collection path: {}",
3 ✔
695
                    moduleLoadPath, sourceCollection, e.getMessage());
4 ✔
696
            return sourceCollection;
2 ✔
697
        }
698
    }
699

700
    /**
701
     * Prepares the XQuery Context for use.
702
     * <p>
703
     * Should be called before compilation to prepare the query context,
704
     * or before re-execution if the query was cached.
705
     *
706
     * @throws XPathException in case of static error
707
     */
708
    public void prepareForReuse() throws XPathException {
709
        // prepare the variables of the internal modules (which were previously reset)
710
        for (final Module[] modules : allModules.values()) {
13 ✔
711
            for (final Module module : modules) {
16 ✔
712
                if (module instanceof InternalModule internalModule) {
6 ✔
713
                    internalModule.prepare(this);
3 ✔
714
                }
715
            }
716
        }
717
    }
1 ✔
718

719
    @Override
720
    public boolean hasParent() {
721
        return false;
×
722
    }
723

724
    @Override
725
    public XQueryContext getRootContext() {
726
        return this;
2 ✔
727
    }
728

729
    @Override
730
    public XQueryContext copyContext() {
731
        final XQueryContext ctx = new XQueryContext(this);
5 ✔
732
        copyFields(ctx);
3 ✔
733
        return ctx;
2 ✔
734
    }
735

736
    @Override
737
    public void updateContext(final XQueryContext from) {
738
        this.watchdog = from.watchdog;
4 ✔
739
        this.lastVar = from.lastVar;
4 ✔
740
        this.contextStack = from.contextStack;
4 ✔
741
        this.localVariableLookup = from.localVariableLookup;
4 ✔
742
        this.inScopeNamespaces = from.inScopeNamespaces;
4 ✔
743
        this.inScopePrefixes = from.inScopePrefixes;
4 ✔
744
        this.inheritedInScopeNamespaces = from.inheritedInScopeNamespaces;
4 ✔
745
        this.inheritedInScopePrefixes = from.inheritedInScopePrefixes;
4 ✔
746
        this.variableStackSize = from.variableStackSize;
4 ✔
747
        this.attributes = from.attributes;
4 ✔
748
        this.updateListener = from.updateListener;
4 ✔
749
        this.modules = from.modules;
4 ✔
750
        this.allModules = from.allModules;
4 ✔
751
        this.dynamicOptions = from.dynamicOptions;
4 ✔
752
        this.staticOptions = from.staticOptions;
4 ✔
753
        this.db = from.db;
4 ✔
754
        this.configuration = from.configuration;
4 ✔
755
        this.httpContext = from.httpContext;
4 ✔
756
    }
1 ✔
757

758
    protected void copyFields(final XQueryContext ctx) {
759
        ctx.calendar = this.calendar;
4 ✔
760
        ctx.implicitTimeZone = this.implicitTimeZone;
4 ✔
761
        ctx.baseURI = this.baseURI;
4 ✔
762
        ctx.baseURISetInProlog = this.baseURISetInProlog;
4 ✔
763
        ctx.staticDocumentPaths = this.staticDocumentPaths;
4 ✔
764
        ctx.staticDocuments = this.staticDocuments;
4 ✔
765
        ctx.dynamicDocuments = this.dynamicDocuments;
4 ✔
766
        ctx.dynamicTextResources = this.dynamicTextResources;
4 ✔
767
        ctx.dynamicCollections = this.dynamicCollections;
4 ✔
768
        ctx.moduleLoadPath = this.moduleLoadPath;
4 ✔
769
        ctx.defaultFunctionNamespace = this.defaultFunctionNamespace;
4 ✔
770
        ctx.defaultElementNamespace = this.defaultElementNamespace;
4 ✔
771
        ctx.defaultCollation = this.defaultCollation;
4 ✔
772
        ctx.defaultCollator = this.defaultCollator;
4 ✔
773
        ctx.backwardsCompatible = this.backwardsCompatible;
4 ✔
774
        ctx.enableOptimizer = this.enableOptimizer;
4 ✔
775
        ctx.stripWhitespace = this.stripWhitespace;
4 ✔
776
        ctx.preserveNamespaces = this.preserveNamespaces;
4 ✔
777
        ctx.inheritNamespaces = this.inheritNamespaces;
4 ✔
778
        ctx.orderEmptyGreatest = this.orderEmptyGreatest;
4 ✔
779

780
        ctx.declaredFunctions = new Object2ObjectAVLTreeMap<>(this.declaredFunctions);
7 ✔
781
        ctx.globalVariables = new Object2ObjectRBTreeMap<>(this.globalVariables);
7 ✔
782
        ctx.attributes = new HashMap<>(this.attributes);
7 ✔
783

784
        // make imported modules available in the new context
785
        ctx.modules = new Object2ObjectOpenHashMap<>(this.modules.size(), Hash.VERY_FAST_LOAD_FACTOR);
9 ✔
786
        for (final Object2ObjectMap.Entry<String, Module[]> entry : Object2ObjectMaps.fastIterable(this.modules)) {
13 ✔
787
            final String namespaceURI = entry.getKey();
4 ✔
788
            final Module[] modules = entry.getValue();
4 ✔
789

790
            ctx.modules.put(namespaceURI, Arrays.copyOf(modules, modules.length));
10 ✔
791

792
            final String prefix = this.staticPrefixes.get(namespaceURI);
6 ✔
793
            try {
794
                ctx.declareNamespace(prefix, namespaceURI);
4 ✔
795
            } catch (final XPathException e) {
1 ✔
796
                // ignore
797
                LOG.warn(e);
×
798
            }
799
        }
800

801
        ctx.allModules = new Object2ObjectOpenHashMap<>(this.allModules.size());
8 ✔
802
        for (final Object2ObjectMap.Entry<String, Module[]> allModulesEntry : Object2ObjectMaps.fastIterable(this.allModules)) {
13 ✔
803
            final Module[] modules = allModulesEntry.getValue();
4 ✔
804
            ctx.allModules.put(allModulesEntry.getKey(), Arrays.copyOf(modules, modules.length));
12 ✔
805
        }
806

807
        ctx.watchdog = this.watchdog;
4 ✔
808
        ctx.profiler = getProfiler();
4 ✔
809
        ctx.lastVar = this.lastVar;
4 ✔
810
        ctx.variableStackSize = getCurrentStackSize();
4 ✔
811
        ctx.contextStack = this.contextStack;
4 ✔
812
        ctx.localVariableLookup = this.localVariableLookup;
4 ✔
813
        ctx.staticNamespaces = new HashMap<>(this.staticNamespaces);
7 ✔
814
        ctx.staticPrefixes = new HashMap<>(this.staticPrefixes);
7 ✔
815

816
        if (this.dynamicOptions != null) {
3 !
817
            ctx.dynamicOptions = new ArrayList<>(this.dynamicOptions);
×
818
        }
819

820
        if (this.staticOptions != null) {
3 ✔
821
            ctx.staticOptions = new ArrayList<>(this.staticOptions);
7 ✔
822
        }
823

824
        ctx.source = this.source;
4 ✔
825
        ctx.httpContext = this.httpContext;
4 ✔
826
    }
1 ✔
827

828
    @Override
829
    public void prepareForExecution() {
830
        //if there is an existing user in the current http session
831
        //then set the DBBroker user
832
        final Subject user = getUserFromHttpSession();
3 ✔
833
        if (user != null) {
2 ✔
834
            getBroker().pushSubject(user);      //this will be popped in {@link XQueryContext#reset(boolean)}
4 ✔
835
            this.pushedUserFromHttpSession = true;
3 ✔
836
        }
837

838
        setRealUser(getBroker().getCurrentSubject());   //this will be unset in {@link XQueryContext#reset(boolean)}
5 ✔
839

840
        //Reset current context position
841
        setContextSequencePosition(0, null);
4 ✔
842
        //Note that, for some reasons, an XQueryContext might be used without calling this method
843
    }
1 ✔
844

845
    public void setContextItem(final Sequence contextItem) {
846
        this.contextItem = contextItem;
3 ✔
847
    }
1 ✔
848

849
    public void setContextItemDeclaration(final ContextItemDeclaration contextItemDeclaration) {
850
        this.contextItemDeclaration = contextItemDeclaration;
3 ✔
851
    }
1 ✔
852

853
    public ContextItemDeclaration getContextItemDeclartion() {
854
        return contextItemDeclaration;
3 ✔
855
    }
856

857
    public Sequence getContextItem() {
858
        return contextItem;
3 ✔
859
    }
860

861
    @Override
862
    public boolean isProfilingEnabled() {
863
        return profiler.isEnabled();
4 ✔
864
    }
865

866
    @Override
867
    public boolean isProfilingEnabled(final int verbosity) {
868
        return profiler.isEnabled() && profiler.verbosity() >= verbosity;
6 !
869
    }
870

871
    @Override
872
    public Profiler getProfiler() {
873
        return profiler;
3 ✔
874
    }
875

876
    @Override
877
    public void setRootExpression(final Expression expr) {
878
        this.rootExpression = expr;
3 ✔
879
    }
1 ✔
880

881
    @Override
882
    public Expression getRootExpression() {
883
        return rootExpression;
3 ✔
884
    }
885

886
    /**
887
     * Returns the next unique expression id. Every expression in the XQuery is identified by a unique id. During compilation, expressions are
888
     * assigned their id by calling this method.
889
     *
890
     * @return The next unique expression id.
891
     */
892
    int nextExpressionId() {
893
        return expressionCounter++;
8 ✔
894
    }
895

896
    @Override
897
    public int getExpressionCount() {
898
        return expressionCounter;
×
899
    }
900

901
    @Override
902
    public void declareNamespace(final String prefix, final String uri) throws XPathException {
903
        if (XML_NS_PREFIX.equals(prefix) || XMLNS_ATTRIBUTE.equals(prefix)) {
8 !
904
            throw new XPathException(rootExpression, ErrorCodes.XQST0070,
7 ✔
905
                    "Namespace predefined prefix '" + prefix + "' can not be bound");
2 ✔
906
        }
907

908
        if (XML_NS.equals(uri)) {
4 ✔
909
            throw new XPathException(rootExpression, ErrorCodes.XQST0070,
7 ✔
910
                    "Namespace URI '" + uri + "' must be bound to the 'xml' prefix");
2 ✔
911
        }
912

913
        final String nonNullPrefix = prefix == null ? "" : prefix;
6 ✔
914
        final String nonNullUri = uri == null ? "" : uri;
6 ✔
915

916
        //This prefix was not bound
917
        if (!staticNamespaces.containsKey(nonNullPrefix)) {
5 ✔
918
            if (nonNullUri.isEmpty()) {
3 ✔
919
                //Nothing to bind
920
                //TODO : check the specs : unbinding an NS which is not already bound may be disallowed.
921
                // FIXME (JL): despite the log message nothing happens in this execution branch
922
                LOG.warn("Unbinding unbound prefix '{}'", prefix);
4 ✔
923
            } else {
1 ✔
924
                //Bind it
925
                staticNamespaces.put(nonNullPrefix, nonNullUri);
6 ✔
926
                staticPrefixes.put(nonNullUri, nonNullPrefix);
6 ✔
927
            }
928
            return;
1 ✔
929
        }
930

931
        //This prefix was bound
932
        final String prevURI = staticNamespaces.get(nonNullPrefix);
6 ✔
933
        //Unbind it
934
        if (nonNullUri.isEmpty()) {
3 ✔
935
            // if an empty namespace is specified,
936
            // remove any existing mapping for this namespace
937
            // FIXME (JL): this allows to rebind namespaces to a different URI
938
            staticPrefixes.remove(nonNullUri);
5 ✔
939
            staticNamespaces.remove(nonNullPrefix);
5 ✔
940
            return;
1 ✔
941
        }
942

943
        //those prefixes can be rebound to different URIs
944
        if (("xs".equals(nonNullPrefix) && Namespaces.SCHEMA_NS.equals(prevURI))
8 !
945
                || ("xsi".equals(nonNullPrefix) && Namespaces.SCHEMA_INSTANCE_NS.equals(prevURI))
8 !
946
                || ("xdt".equals(nonNullPrefix) && Namespaces.XPATH_DATATYPES_NS.equals(prevURI))
8 !
947
                || ("fn".equals(nonNullPrefix) && Namespaces.XPATH_FUNCTIONS_NS.equals(prevURI))
8 !
948
                || ("math".equals(nonNullPrefix)) && Namespaces.XPATH_FUNCTIONS_MATH_NS.equals(prevURI)
8 !
949
                || ("local".equals(nonNullPrefix) && Namespaces.XQUERY_LOCAL_NS.equals(prevURI))) {
8 !
950

951
            staticPrefixes.remove(prevURI);
5 ✔
952
            staticNamespaces.remove(nonNullPrefix);
5 ✔
953

954
            staticNamespaces.put(nonNullPrefix, nonNullUri);
6 ✔
955
            staticPrefixes.put(nonNullUri, nonNullPrefix);
6 ✔
956
            return;
1 ✔
957
        }
958
        //Forbids rebinding the *same* prefix in a *different* namespace in this *same* context
959
        if (!nonNullUri.equals(prevURI)) {
4 ✔
960
            // XQST0066 for default element namespace redeclaration, XQST0033 for other prefixes
961
            final ErrorCodes.ErrorCode errorCode = nonNullPrefix.isEmpty() ? ErrorCodes.XQST0066 : ErrorCodes.XQST0033;
6 !
962
            throw new XPathException(rootExpression, errorCode,
7 ✔
963
                    "Cannot bind prefix '" + prefix + "' to '" + nonNullUri + "' it is already bound to '" + prevURI + "'");
4 ✔
964
        }
965

966
        // FIXME (JL): function call can end up in unhandled case
967
        // e.g. declareNamespace(null,null)
968
    }
1 ✔
969

970
    @Override
971
    public void declareNamespaces(final Map<String, String> namespaceMap) {
972
        for (final Map.Entry<String, String> entry : namespaceMap.entrySet()) {
12 ✔
973
            String prefix = entry.getKey();
4 ✔
974
            String uri = entry.getValue();
4 ✔
975

976
            if (prefix == null) {
2 !
977
                prefix = "";
×
978
            }
979

980
            if (uri == null) {
2 !
981
                uri = "";
×
982
            }
983
            staticNamespaces.put(prefix, uri);
6 ✔
984
            staticPrefixes.put(uri, prefix);
6 ✔
985
        }
986
    }
1 ✔
987

988
    @Override
989
    public void removeNamespace(final String uri) {
990
        staticPrefixes.remove(uri);
×
991

992
        for (final Iterator<String> i = staticNamespaces.values().iterator(); i.hasNext(); ) {
×
993
            if (i.next().equals(uri)) {
×
994
                i.remove();
×
995
                return;
×
996
            }
997
        }
998
        inScopePrefixes.remove(uri);
×
999

1000
        if (inScopeNamespaces != null) {
×
1001
            for (final Iterator<String> i = inScopeNamespaces.values().iterator(); i.hasNext(); ) {
×
1002
                if (i.next().equals(uri)) {
×
1003
                    i.remove();
×
1004
                    return;
×
1005
                }
1006
            }
1007
        }
1008
        inheritedInScopePrefixes.remove(uri);
×
1009

1010
        if (inheritedInScopeNamespaces != null) {
×
1011
            for (final Iterator<String> i = inheritedInScopeNamespaces.values().iterator(); i.hasNext(); ) {
×
1012
                if (i.next().equals(uri)) {
×
1013
                    i.remove();
×
1014
                    return;
×
1015
                }
1016
            }
1017
        }
1018
    }
×
1019

1020
    @Override
1021
    public void declareInScopeNamespace(final String prefix, final String uri) {
1022
        if (prefix == null || uri == null) {
4 !
1023
            throw new IllegalArgumentException("null argument passed to declareNamespace");
×
1024
        }
1025

1026
        //Activate the namespace by removing it from the inherited namespaces
1027
        if (inheritedInScopePrefixes.containsKey(getURIForPrefix(prefix))) {
7 ✔
1028
            inheritedInScopePrefixes.remove(uri);
5 ✔
1029
        }
1030

1031
        inheritedInScopeNamespaces.remove(prefix);
5 ✔
1032

1033
        inScopePrefixes.put(uri, prefix);
6 ✔
1034
        inScopeNamespaces.put(prefix, uri);
6 ✔
1035
    }
1 ✔
1036

1037
    @Override
1038
    public String getInScopeNamespace(final String prefix) {
1039
        return inScopeNamespaces == null ? null : inScopeNamespaces.get(prefix);
9 !
1040
    }
1041

1042
    @Override
1043
    public String getInScopePrefix(final String uri) {
1044
        return inScopePrefixes == null ? null : inScopePrefixes.get(uri);
9 !
1045
    }
1046

1047
    public Map<String, String> getInScopePrefixes() {
1048
        return inScopePrefixes;
3 ✔
1049
    }
1050

1051
    @Override
1052
    public String getInheritedNamespace(final String prefix) {
1053
        return inheritedInScopeNamespaces == null ? null : inheritedInScopeNamespaces.get(prefix);
9 !
1054
    }
1055

1056
    public Map<String, String> getAllInheritedNamespaces() {
1057
        return inheritedInScopeNamespaces;
3 ✔
1058
    }
1059

1060
    public Map<String, String> getInScopeNamespaces() {
1061
        return inScopeNamespaces;
3 ✔
1062
    }
1063

1064
    public MemTreeBuilder getCurrentDocumentBuilder() {
1065
        return documentBuilder;
3 ✔
1066
    }
1067

1068
    @Override
1069
    public String getInheritedPrefix(final String uri) {
1070
        return inheritedInScopePrefixes == null ? null : inheritedInScopePrefixes.get(uri);
9 !
1071
    }
1072

1073
    @Override
1074
    public String getURIForPrefix(final String prefix) {
1075
        // try in-scope namespace declarations
1076
        String uri = (inScopeNamespaces == null) ? null : inScopeNamespaces.get(prefix);
9 !
1077

1078
        if (uri != null) {
2 ✔
1079
            return uri;
2 ✔
1080
        }
1081

1082
        if (inheritNamespaces) {
3 ✔
1083
            uri = (inheritedInScopeNamespaces == null) ? null : inheritedInScopeNamespaces.get(prefix);
9 !
1084

1085
            if (uri != null) {
2 ✔
1086
                return uri;
2 ✔
1087
            }
1088
        }
1089
        return staticNamespaces.get(prefix);
6 ✔
1090
    }
1091

1092
    @Override
1093
    public String getPrefixForURI(final String uri) {
1094
        String prefix = (inScopePrefixes == null) ? null : inScopePrefixes.get(uri);
9 !
1095

1096
        if (prefix != null) {
2 !
1097
            return prefix;
×
1098
        }
1099

1100
        if (inheritNamespaces) {
3 !
1101
            prefix = (inheritedInScopePrefixes == null) ? null : inheritedInScopePrefixes.get(uri);
9 !
1102

1103
            if (prefix != null) {
2 !
1104
                return prefix;
×
1105
            }
1106
        }
1107
        return staticPrefixes.get(uri);
6 ✔
1108
    }
1109

1110
    @Override
1111
    public String getDefaultFunctionNamespace() {
1112
        return defaultFunctionNamespace;
3 ✔
1113
    }
1114

1115
    @Override
1116
    public void setDefaultFunctionNamespace(final String uri) throws XPathException {
1117
        // XQST0070: A namespace URI bound to the predefined xmlns or to xml (with a
1118
        // non-xml binding) is reserved and cannot appear in a default namespace declaration.
1119
        if (Namespaces.XMLNS_NS.equals(uri)) {
4 ✔
1120
            throw new XPathException(rootExpression, ErrorCodes.XQST0070,
7 ✔
1121
                    "The namespace URI '" + Namespaces.XMLNS_NS + "' cannot be used as the default function namespace");
1 ✔
1122
        }
1123
        if (XML_NS.equals(uri)) {
4 ✔
1124
            throw new XPathException(rootExpression, ErrorCodes.XQST0070,
7 ✔
1125
                    "The namespace URI '" + XML_NS + "' cannot be bound to a prefix other than 'xml'");
1 ✔
1126
        }
1127
        //Not sure for the 2nd clause : eXist-db forces the function NS as default.
1128
        if (defaultFunctionNamespace != null
3 !
1129
                && !defaultFunctionNamespace.equals(Function.BUILTIN_FUNCTION_NS)
5 !
1130
                && !defaultFunctionNamespace.equals(uri)) {
×
1131
            throw new XPathException(rootExpression, ErrorCodes.XQST0066,
×
1132
                    "Default function namespace is already set to: '" + defaultFunctionNamespace + "'");
×
1133
        }
1134
        defaultFunctionNamespace = uri;
3 ✔
1135
    }
1 ✔
1136

1137
    @Override
1138
    public String getDefaultElementNamespaceSchema() {
1139
        return defaultElementNamespaceSchema.getStringValue();
×
1140
    }
1141

1142
    @Override
1143
    public void setDefaultElementNamespaceSchema(final String uri) throws XPathException {
1144
        // eXist forces the empty element NS as default.
1145
        if (!defaultElementNamespaceSchema.equals(AnyURIValue.EMPTY_URI)) {
×
1146
            throw new XPathException(rootExpression, ErrorCodes.XQST0066, "Default function namespace schema is already set to: '" + defaultElementNamespaceSchema.getStringValue() + "'");
×
1147
        }
1148
        defaultElementNamespaceSchema = new AnyURIValue(uri);
×
1149
    }
×
1150

1151
    @Override
1152
    public String getDefaultElementNamespace() {
1153
        return defaultElementNamespace.getStringValue();
×
1154
    }
1155

1156
    @Override
1157
    public void setDefaultElementNamespace(final String uri, @Nullable final String schema) throws XPathException {
1158
        // XQST0070: A namespace URI bound to the predefined xmlns or to xml (with a
1159
        // non-xml binding) is reserved and cannot appear in a default namespace declaration.
1160
        if (Namespaces.XMLNS_NS.equals(uri)) {
4 ✔
1161
            throw new XPathException(rootExpression, ErrorCodes.XQST0070,
7 ✔
1162
                    "The namespace URI '" + Namespaces.XMLNS_NS + "' cannot be used as the default element namespace");
1 ✔
1163
        }
1164
        if (XML_NS.equals(uri)) {
4 ✔
1165
            throw new XPathException(rootExpression, ErrorCodes.XQST0070,
7 ✔
1166
                    "The namespace URI '" + XML_NS + "' cannot be bound to a prefix other than 'xml'");
1 ✔
1167
        }
1168
        // eXist forces the empty element NS as default.
1169
        if (!defaultElementNamespace.equals(AnyURIValue.EMPTY_URI)) {
5 !
1170
            throw new XPathException(rootExpression, ErrorCodes.XQST0066,
×
1171
                    "Default element namespace is already set to: '" + defaultElementNamespace.getStringValue() + "'");
×
1172
        }
1173
        defaultElementNamespace = new AnyURIValue(uri);
6 ✔
1174

1175
        if (schema != null) {
2 !
1176
            defaultElementNamespaceSchema = new AnyURIValue(schema);
×
1177
        }
1178
    }
1 ✔
1179

1180
    @Override
1181
    public void setDefaultCollation(final String uri) throws XPathException {
1182
        if (uri.equals(Collations.UNICODE_CODEPOINT_COLLATION_URI) || uri.equals(Collations.CODEPOINT_SHORT)) {
×
1183
            defaultCollation = Collations.UNICODE_CODEPOINT_COLLATION_URI;
×
1184
            defaultCollator = null;
×
1185
        }
1186

1187
        final URI uriTest;
1188
        try {
1189
            uriTest = new URI(uri);
×
1190
        } catch (final URISyntaxException e) {
×
1191
            throw new XPathException(rootExpression, ErrorCodes.XQST0038, "Unknown collation : '" + uri + "'");
×
1192
        }
1193

1194
        if (uri.startsWith(Collations.EXIST_COLLATION_URI) || uri.charAt(0) == '?' || uriTest.isAbsolute()) {
×
1195
            defaultCollator = Collations.getCollationFromURI(uri, rootExpression);
×
1196
            defaultCollation = uri;
×
1197
        } else {
×
1198
            String absUri = getBaseURI().getStringValue() + uri;
×
1199
            defaultCollator = Collations.getCollationFromURI(absUri, rootExpression);
×
1200
            defaultCollation = absUri;
×
1201
        }
1202
    }
×
1203

1204
    @Override
1205
    public String getDefaultCollation() {
1206
        return defaultCollation;
3 ✔
1207
    }
1208

1209
    @Override
1210
    public Collator getCollator(String uri) throws XPathException {
1211
        return getCollator(uri, ErrorCodes.XQST0076);
5 ✔
1212
    }
1213

1214
    @Override
1215
    public Collator getCollator(String uri, final ErrorCodes.ErrorCode errorCode) throws XPathException {
1216
        if (uri == null) {
2 !
1217
            return defaultCollator;
×
1218
        }
1219

1220
        // if the uri is relative, resolve it against the base uri
1221
        try {
1222
            final URI u = new AnyURIValue(uri).toURI();
6 ✔
1223
            if (!u.isAbsolute()) {
3 ✔
1224
                final URI uu = getBaseURI().toURI().resolve(u);
6 ✔
1225
                if (uu.isAbsolute()) {
3 !
1226
                    uri = uu.toString();
×
1227
                }
1228
            }
1229
        } catch (final XPathException e) {
×
1230
            // no-op
1231
        }
1232

1233
        return Collations.getCollationFromURI(uri, rootExpression, errorCode);
6 ✔
1234
    }
1235

1236
    @Override
1237
    public Collator getDefaultCollator() {
1238
        return defaultCollator;
3 ✔
1239
    }
1240

1241
    @Override
1242
    public void setStaticallyKnownDocuments(final XmldbURI[] docs) {
1243
        staticDocumentPaths = docs;
3 ✔
1244
    }
1 ✔
1245

1246
    @Override
1247
    public void setStaticallyKnownDocuments(final DocumentSet set) {
1248
        staticDocuments = set;
3 ✔
1249
    }
1 ✔
1250

1251
    public void addDynamicallyAvailableDocument(final String uri,
1252
                                                final TriFunctionE<DBBroker, Txn, String, Either<org.exist.dom.memtree.DocumentImpl, DocumentImpl>, XPathException> supplier) {
1253
        if (dynamicDocuments == null) {
3 !
1254
            dynamicDocuments = new HashMap<>();
5 ✔
1255
        }
1256
        dynamicDocuments.put(uri, supplier);
6 ✔
1257
    }
1 ✔
1258

1259
    public void addDynamicallyAvailableTextResource(final String uri, final Charset encoding,
1260
                                                    final QuadFunctionE<DBBroker, Txn, String, Charset, Reader, XPathException> supplier) {
1261
        if (dynamicTextResources == null) {
3 !
1262
            dynamicTextResources = new HashMap<>();
5 ✔
1263
        }
1264
        dynamicTextResources.put(Tuple(uri, encoding), supplier);
8 ✔
1265
    }
1 ✔
1266

1267
    public void addDynamicallyAvailableCollection(final String uri,
1268
                                                  final TriFunctionE<DBBroker, Txn, String, Sequence, XPathException> supplier) {
1269
        if (dynamicCollections == null) {
3 !
1270
            dynamicCollections = new HashMap<>();
5 ✔
1271
        }
1272
        dynamicCollections.put(uri, supplier);
6 ✔
1273
    }
1 ✔
1274

1275
    @Override
1276
    public void setCalendar(final XMLGregorianCalendar newCalendar) {
1277
        this.calendar = (XMLGregorianCalendar) newCalendar.clone();
×
1278
    }
×
1279

1280
    @Override
1281
    public void setTimeZone(final TimeZone newTimeZone) {
1282
        this.implicitTimeZone = newTimeZone;
×
1283
    }
×
1284

1285
    @Override
1286
    public XMLGregorianCalendar getCalendar() {
1287
        //TODO : we might prefer to return null
1288
        if (calendar == null) {
3 ✔
1289
            try {
1290
                //Initialize to current dateTime
1291
                calendar = DatatypeFactory.newInstance().newXMLGregorianCalendar(new GregorianCalendar());
7 ✔
1292
            } catch (final DatatypeConfigurationException e) {
1 ✔
1293
                LOG.error(e.getMessage(), e);
×
1294
            }
1295
        }
1296

1297
        //That's how we ensure stability of that static context function
1298
        return calendar;
3 ✔
1299
    }
1300

1301
    @Override
1302
    public TimeZone getImplicitTimeZone() {
1303
        if (implicitTimeZone == null) {
3 !
1304
            implicitTimeZone = TimeZone.getDefault();
×
1305

1306
            if (implicitTimeZone.inDaylightTime(new Date())) {
×
1307
                implicitTimeZone.setRawOffset(implicitTimeZone.getRawOffset() + implicitTimeZone.getDSTSavings());
×
1308
            }
1309
        }
1310

1311
        //That's how we ensure stability of that static context function
1312
        return this.implicitTimeZone;
3 ✔
1313
    }
1314

1315
    @Override
1316
    public DocumentSet getStaticallyKnownDocuments() throws XPathException {
1317
        if (staticDocuments != null) {
3 ✔
1318

1319
            // the document set has already been built, return it
1320
            return staticDocuments;
3 ✔
1321
        }
1322

1323
        if (protectedDocuments != null) {
3 ✔
1324
            staticDocuments = protectedDocuments.toDocumentSet();
5 ✔
1325
            return staticDocuments;
3 ✔
1326
        }
1327
        final MutableDocumentSet ndocs = new DefaultDocumentSet(40);
5 ✔
1328

1329
        if (staticDocumentPaths == null) {
3 ✔
1330

1331
            // no path defined: return all documents in the db
1332
            try {
1333
                getBroker().getAllXMLResources(ndocs);
5 ✔
1334
            } catch (final PermissionDeniedException | LockException e) {
1 ✔
1335
                LOG.warn(e);
×
1336
                throw new XPathException(rootExpression, "Permission denied to read resource all resources: " + e.getMessage(), e);
×
1337
            }
1338
        } else {
1339
            for (final XmldbURI staticDocumentPath : staticDocumentPaths) {
17 ✔
1340

1341
                try {
1342
                    final Collection collection = getBroker().getCollection(staticDocumentPath);
5 ✔
1343

1344
                    if (collection != null) {
2 ✔
1345
                        collection.allDocs(getBroker(), ndocs, true);
7 ✔
1346
                    } else {
1 ✔
1347
                        try (final LockedDocument lockedDocument = getBroker().getXMLResource(staticDocumentPath, LockMode.READ_LOCK)) {
10 ✔
1348

1349
                            final DocumentImpl doc = lockedDocument == null ? null : lockedDocument.getDocument();
5 !
1350
                            if (doc != null) {
2 !
1351

1352
                                if (doc.getPermissions().validate(
3 ✔
1353
                                        getBroker().getCurrentSubject(), Permission.READ)) {
5 !
1354

1355
                                    ndocs.add(doc);
3 ✔
1356
                                }
1357
                            }
1358
                        }
1359
                    }
1360
                } catch (final PermissionDeniedException | LockException e) {
×
1361
                    LOG.warn("Permission denied to read resource {}. Skipping it.", staticDocumentPath);
×
1362
                }
1363
            }
1364
        }
1365
        staticDocuments = ndocs;
3 ✔
1366
        return staticDocuments;
3 ✔
1367
    }
1368

1369
    public DocumentSet getStaticDocs() {
1370
        return staticDocuments;
3 ✔
1371
    }
1372

1373
    /**
1374
     * Gets a document from the "Available documents" of the
1375
     * dynamic context.
1376
     *
1377
     * @param uri the URI by which the document was registered
1378
     * @return sequence of available documents matching the URI
1379
     * @throws XPathException in case of dynamic error
1380
     */
1381
    public @Nullable Sequence getDynamicallyAvailableDocument(final String uri) throws XPathException {
1382
        if (dynamicDocuments == null) {
3 ✔
1383
            return null;
2 ✔
1384
        }
1385

1386
        final TriFunctionE<DBBroker, Txn, String, Either<org.exist.dom.memtree.DocumentImpl, DocumentImpl>, XPathException> docSupplier
1 ✔
1387
                = dynamicDocuments.get(uri);
5 ✔
1388
        if (docSupplier == null) {
2 !
1389
            return null;
×
1390
        }
1391

1392
        return docSupplier.apply(getBroker(), getBroker().getCurrentTransaction(), uri).fold(md -> md, pd -> (Sequence) pd);
16 ✔
1393
    }
1394

1395
    /**
1396
     * Gets a text resource from the "Available text resources" of the
1397
     * dynamic context.
1398
     *
1399
     * @param uri the URI by which the document was registered
1400
     * @param charset the charset to use for retrieving the resource
1401
     * @return a reader to read the resource content from
1402
     * @throws XPathException in case of a dynamic error
1403
     */
1404
    public @Nullable Reader getDynamicallyAvailableTextResource(final String uri, final Charset charset)
1405
            throws XPathException {
1406
        if (dynamicTextResources == null) {
3 !
1407
            return null;
2 ✔
1408
        }
1409

1410
        final QuadFunctionE<DBBroker, Txn, String, Charset, Reader, XPathException> textResourceSupplier
×
1411
                = dynamicTextResources.get(Tuple(uri, charset));
×
1412
        if (textResourceSupplier == null) {
×
1413
            return null;
×
1414
        }
1415

1416
        return textResourceSupplier.apply(getBroker(), getBroker().getCurrentTransaction(), uri, charset);
×
1417
    }
1418

1419
    /**
1420
     * Gets a text resource from the "Available text resources" of the
1421
     * dynamic context, matching by URI only. This is used when no encoding
1422
     * is specified, allowing the resource to be found regardless of what
1423
     * charset it was registered with.
1424
     *
1425
     * @param uri the URI of the resource to retrieve
1426
     * @return a reader to read the resource content from, or null if not found
1427
     * @throws XPathException in case of a dynamic error
1428
     */
1429
    public @Nullable Reader getDynamicallyAvailableTextResourceByUri(final String uri)
1430
            throws XPathException {
1431
        if (dynamicTextResources == null) {
3 ✔
1432
            return null;
2 ✔
1433
        }
1434

1435
        for (final Map.Entry<Tuple2<String, Charset>, QuadFunctionE<DBBroker, Txn, String, Charset, Reader, XPathException>> entry : dynamicTextResources.entrySet()) {
13 !
1436
            if (entry.getKey()._1.equals(uri)) {
8 !
1437
                final Charset registeredCharset = entry.getKey()._2;
6 ✔
1438
                return entry.getValue().apply(getBroker(), getBroker().getCurrentTransaction(), uri, registeredCharset);
13 ✔
1439
            }
1440
        }
1441
        return null;
×
1442
    }
1443

1444
    /**
1445
     * Gets a collection from the "Available collections" of the
1446
     * dynamic context.
1447
     *
1448
     * @param uri the URI of the collection to retrieve
1449
     * @return a sequence of document nodes
1450
     * @throws XPathException in case of dynamic error
1451
     */
1452
    public @Nullable Sequence getDynamicallyAvailableCollection(final String uri) throws XPathException {
1453
        if (dynamicCollections == null) {
3 ✔
1454
            return null;
2 ✔
1455
        }
1456

1457
        final TriFunctionE<DBBroker, Txn, String, Sequence, XPathException> collectionSupplier
1 ✔
1458
                = dynamicCollections.get(uri);
5 ✔
1459
        if (collectionSupplier == null) {
2 !
1460
            return null;
×
1461
        }
1462

1463
        return collectionSupplier.apply(getBroker(), getBroker().getCurrentTransaction(), uri);
10 ✔
1464
    }
1465

1466
    @Override
1467
    public ExtendedXMLStreamReader getXMLStreamReader(final NodeValue nv) throws XMLStreamException, IOException {
1468
        final ExtendedXMLStreamReader reader;
1469
        if (nv.getImplementationType() == NodeValue.IN_MEMORY_NODE) {
3 !
1470
            final NodeImpl node = (NodeImpl) nv;
3 ✔
1471
            final org.exist.dom.memtree.DocumentImpl ownerDoc = node.getNodeType() == Node.DOCUMENT_NODE ? (org.exist.dom.memtree.DocumentImpl) node : node.getOwnerDocument();
7 !
1472
            reader = new InMemoryXMLStreamReader(ownerDoc, ownerDoc);
6 ✔
1473
        } else {
1 ✔
1474
            final NodeProxy proxy = (NodeProxy) nv;
×
1475
            reader = getBroker().newXMLStreamReader(new NodeProxy(rootExpression, proxy.getOwnerDocument(), NodeId.DOCUMENT_NODE, proxy.getOwnerDocument().getFirstChildAddress()), false);
×
1476
        }
1477
        return reader;
2 ✔
1478
    }
1479

1480
    @Override
1481
    public void setProtectedDocs(final LockedDocumentMap map) {
1482
        this.protectedDocuments = map;
3 ✔
1483
    }
1 ✔
1484

1485
    @Override
1486
    public LockedDocumentMap getProtectedDocs() {
1487
        return this.protectedDocuments;
3 ✔
1488
    }
1489

1490
    @Override
1491
    public boolean inProtectedMode() {
1492
        return protectedDocuments != null;
7 ✔
1493
    }
1494

1495
    @Override
1496
    public boolean lockDocumentsOnLoad() {
1497
        return false;
2 ✔
1498
    }
1499

1500
    @Override
1501
    public void setShared(final boolean shared) {
1502
        isShared = shared;
3 ✔
1503
    }
1 ✔
1504

1505
    @Override
1506
    public boolean isShared() {
1507
        return isShared;
×
1508
    }
1509

1510
    @Override
1511
    public void addModifiedDoc(final DocumentImpl document) {
1512
        if (modifiedDocuments == null) {
3 ✔
1513
            modifiedDocuments = new DefaultDocumentSet();
5 ✔
1514
        }
1515
        modifiedDocuments.add(document);
4 ✔
1516
    }
1 ✔
1517

1518
    @Override
1519
    public void reset() {
1520
        reset(false);
3 ✔
1521
    }
1 ✔
1522

1523
    @Override
1524
    public void reset(final boolean keepGlobals) {
1525
        if (importedContexts != null) {
3 ✔
1526
            for (final XQueryContext importedContext : importedContexts) {
12 ✔
1527
                importedContext.reset(keepGlobals);
3 ✔
1528
            }
1529
            importedContexts = null;
3 ✔
1530
        }
1531

1532
        setRealUser(null);
3 ✔
1533

1534
        if (this.pushedUserFromHttpSession) {
3 ✔
1535
            try {
1536
                getBroker().popSubject();
4 ✔
1537
            } finally {
1 ✔
1538
                this.pushedUserFromHttpSession = false;
3 ✔
1539
            }
1540
        }
1541

1542
        if (modifiedDocuments != null) {
3 ✔
1543
            try {
1544
                Modification.checkFragmentation(this, modifiedDocuments);
4 ✔
1545
            } catch (final LockException | EXistException e) {
1 ✔
1546
                LOG.warn("Error while checking modified documents: {}", e.getMessage(), e);
×
1547
            }
1548
            modifiedDocuments = null;
3 ✔
1549
        }
1550

1551
        calendar = null;
3 ✔
1552
        implicitTimeZone = null;
3 ✔
1553

1554
        resetDocumentBuilder();
2 ✔
1555

1556
        contextSequence = null;
3 ✔
1557
        contextItem = Sequence.EMPTY_SEQUENCE;
3 ✔
1558

1559
        if (!keepGlobals) {
2 ✔
1560
            // do not reset the statically known documents
1561
            staticDocumentPaths = null;
3 ✔
1562
            staticDocuments = null;
3 ✔
1563
            dynamicDocuments = null;
3 ✔
1564
            dynamicTextResources = null;
3 ✔
1565
            dynamicCollections = null;
3 ✔
1566
        }
1567

1568
        if (!isShared) {
3 ✔
1569
            lastVar = null;
3 ✔
1570
            localVariableLookup.clear();
3 ✔
1571
        }
1572

1573
        // a reused context starts a fresh execution, with no scope open
1574
        binaryValueFrameDepth = 0;
3 ✔
1575

1576
        // clear inline functions using closures
1577
        closures.forEach(func -> func.setClosureVariables(null));
8 ✔
1578
        closures.clear();
3 ✔
1579

1580
        fragmentStack = new ArrayDeque<>();
5 ✔
1581
        callStack.clear();
3 ✔
1582
        protectedDocuments = null;
3 ✔
1583

1584
        cachedUriCollectionResults.clear();
3 ✔
1585

1586
        if (!keepGlobals) {
2 ✔
1587
            globalVariables.clear();
3 ✔
1588
        }
1589

1590
        if (dynamicOptions != null) {
3 !
1591
            dynamicOptions.clear(); //clear any dynamic options
×
1592
        }
1593

1594
        if (!isShared) {
3 ✔
1595
            watchdog.reset();
3 ✔
1596
        }
1597

1598
        /*
1599
            NOTE: we use `modules` (and not `allModules`) here so as to only reset
1600
            the modules of this module.
1601
            The inner call to `module.reset` will be called on sub-modules
1602
            which in-turn will reset their modules, and so on.
1603
         */
1604
        for (final Module[] modules : modules.values()) {
13 ✔
1605
            for (final Module module : modules) {
16 ✔
1606
                module.reset(this, keepGlobals);
4 ✔
1607
            }
1608
        }
1609

1610
        savedState.restore();
3 ✔
1611

1612
        attributes.clear();
3 ✔
1613

1614
        // In-scope namespaces declared outside an element constructor -- by attribute namespace
1615
        // fixup, or by a caller configuring the context before execution -- land in these top-level
1616
        // maps. On a pooled context they would otherwise survive into whichever execution next
1617
        // borrows it, where the fixup would trust them (#6704).
1618
        //
1619
        // Only when globals are not kept: compilation also resets, with keepGlobals, when the
1620
        // optimizer rewrites the tree and the query is analyzed again, and namespaces the caller
1621
        // declared before compiling must still resolve then.
1622
        //
1623
        // Fresh maps rather than clear(): updateContext() shares these by reference with
1624
        // util:eval's inner context, and resetting that must not empty the outer query's bindings
1625
        // mid-evaluation.
1626
        if (!keepGlobals) {
2 ✔
1627
            inScopeNamespaces = new HashMap<>();
5 ✔
1628
            inScopePrefixes = new HashMap<>();
5 ✔
1629
            inheritedInScopeNamespaces = new HashMap<>();
5 ✔
1630
            inheritedInScopePrefixes = new HashMap<>();
5 ✔
1631
        }
1632

1633
        clearUpdateListeners();
2 ✔
1634

1635
        profiler.endQuery();
3 ✔
1636

1637
        if (!keepGlobals) {
2 ✔
1638
            httpContext = null;
3 ✔
1639
        }
1640

1641
        if (modulesDependencyGraphSPExecutor != null) {
3 ✔
1642
            try {
1643
                ConcurrencyUtil.shutdownExecutionService(modulesDependencyGraphSPExecutor);
3 ✔
1644
            } catch (final InterruptedException e) {
1 ✔
1645
                Thread.currentThread().interrupt();
×
1646
            }
1647
            modulesDependencyGraphSPExecutor = null;
3 ✔
1648
        }
1649

1650
        analyzed = false;
3 ✔
1651
    }
1 ✔
1652

1653
    @Override
1654
    public boolean stripWhitespace() {
1655
        return stripWhitespace;
3 ✔
1656
    }
1657

1658
    @Override
1659
    public void setStripWhitespace(final boolean strip) {
1660
        this.stripWhitespace = strip;
3 ✔
1661
    }
1 ✔
1662

1663
    @Override
1664
    public boolean preserveNamespaces() {
1665
        return preserveNamespaces;
3 ✔
1666
    }
1667

1668
    @Override
1669
    public void setPreserveNamespaces(final boolean preserve) {
1670
        this.preserveNamespaces = preserve;
3 ✔
1671
    }
1 ✔
1672

1673
    @Override
1674
    public boolean inheritNamespaces() {
1675
        return inheritNamespaces;
3 ✔
1676
    }
1677

1678
    @Override
1679
    public void setInheritNamespaces(final boolean inherit) {
1680
        this.inheritNamespaces = inherit;
3 ✔
1681
    }
1 ✔
1682

1683
    @Override
1684
    public boolean orderEmptyGreatest() {
1685
        return orderEmptyGreatest;
3 ✔
1686
    }
1687

1688
    @Override
1689
    public void setOrderEmptyGreatest(final boolean order) {
1690
        this.orderEmptyGreatest = order;
×
1691
    }
×
1692

1693
    @Override
1694
    public Iterator<Module> getModules() {
1695
        return new CollectionOfArrayIterator<>(modules.values());
7 ✔
1696
    }
1697

1698
    @Override
1699
    public Iterator<Module> getRootModules() {
1700
        return getAllModules();
3 ✔
1701
    }
1702

1703
    @Override
1704
    public Iterator<Module> getAllModules() {
1705
        return new CollectionOfArrayIterator<>(allModules.values());
7 ✔
1706
    }
1707

1708
    @Override
1709
    @Nullable
1710
    public Module[] getModules(final String namespaceURI) {
1711
        return modules.get(namespaceURI);
6 ✔
1712
    }
1713

1714
    @Override
1715
    public Module[] getRootModules(final String namespaceURI) {
1716
        return allModules.get(namespaceURI);
6 ✔
1717
    }
1718

1719
    @Override
1720
    public void setModules(final String namespaceURI, @Nullable final Module[] modules) {
1721
        if (modules == null) {
2 !
1722
            this.modules.remove(namespaceURI); // unbind the module
×
1723
        } else {
×
1724
            this.modules.put(namespaceURI, modules);
6 ✔
1725
        }
1726
        setRootModules(namespaceURI, modules);
4 ✔
1727
    }
1 ✔
1728

1729
    @Override
1730
    public void addModule(final String namespaceURI, final Module module) {
1731
        if (module == null) {
2 !
1732
            throw new IllegalArgumentException();
×
1733
        } else {
1734
            this.modules.compute(namespaceURI, addToMapValueArray(module));
7 ✔
1735
        }
1736
        addRootModule(namespaceURI, module);
4 ✔
1737
    }
1 ✔
1738

1739
    private Graph<ModuleVertex, DefaultEdge> getModulesDependencyGraph() {
1740
        // NOTE(AR) intentionally lazily initialised!
1741
        if (modulesDependencyGraph == null) {
3 ✔
1742
            this.modulesDependencyGraph = new FastutilMapGraph<>(null, SupplierUtil.createDefaultEdgeSupplier(), DefaultGraphType.directedPseudograph().asUnweighted());
9 ✔
1743
        }
1744
        return modulesDependencyGraph;
3 ✔
1745
    }
1746

1747
    /**
1748
     * Add a vertex to the Modules Dependency Graph.
1749
     *
1750
     * @param moduleVertex the module vertex
1751
     */
1752
    protected void addModuleVertex(final ModuleVertex moduleVertex) {
1753
        getModulesDependencyGraph().addVertex(moduleVertex);
5 ✔
1754
    }
1 ✔
1755

1756
    /**
1757
     * Check if a vertex exists in the Modules Dependency Graph.
1758
     *
1759
     * @param moduleVertex the module vertex to look for
1760
     *
1761
     * @return true if the module vertex exists, false otherwise
1762
     */
1763
    protected boolean hasModuleVertex(final ModuleVertex moduleVertex) {
1764
        return getModulesDependencyGraph().containsVertex(moduleVertex);
5 ✔
1765
    }
1766

1767
    /**
1768
     * Add an edge between two Modules in the Dependency Graph.
1769
     *
1770
     * @param source the importing module
1771
     * @param sink the imported module
1772
     */
1773
    protected void addModuleEdge(final ModuleVertex source, final ModuleVertex sink) {
1774
        getModulesDependencyGraph().addEdge(source, sink);
6 ✔
1775
    }
1 ✔
1776

1777
    /**
1778
     * Look for a path between two Modules in the Dependency Graph.
1779
     *
1780
     * @param source the module to start searching from
1781
     * @param sink the destination module to attempt to reach
1782
     *
1783
     * @return true, if there is a path between the mdoules, false otherwise
1784
     */
1785
    protected boolean hasModulePath(final ModuleVertex source, final ModuleVertex sink) {
1786
        if (modulesDependencyGraph == null) {
3 !
1787
            return false;
×
1788
        }
1789

1790
        // NOTE(AR) intentionally lazily initialised!
1791
        if (modulesDependencyGraphSPExecutor == null) {
3 ✔
1792
            modulesDependencyGraphSPExecutor = ConcurrencyUtil.createThreadPoolExecutor(2);
4 ✔
1793
        }
1794

1795
        final ShortestPathAlgorithm<ModuleVertex, DefaultEdge> spa = new TransitNodeRoutingShortestPath<>(getModulesDependencyGraph(), modulesDependencyGraphSPExecutor);
8 ✔
1796
        return spa.getPath(source, sink) != null;
9 ✔
1797
    }
1798

1799
    protected void setRootModules(final String namespaceURI, @Nullable final Module[] modules) {
1800
        if (modules == null) {
2 !
1801
            allModules.remove(namespaceURI); // unbind the module
×
1802
            return;
×
1803
        }
1804

1805
        if (allModules.get(namespaceURI) != modules) {
6 !
1806
            setModulesChanged();
×
1807
        }
1808
        allModules.put(namespaceURI, modules);
6 ✔
1809
    }
1 ✔
1810

1811
    protected void addRootModule(final String namespaceURI, final Module module) {
1812
        if (module == null) {
2 !
1813
            throw new IllegalArgumentException();
×
1814
        } else {
1815
            final Module[] current = this.allModules.get(namespaceURI);
6 ✔
1816
            final Module[] updated = this.allModules.compute(namespaceURI, addToMapValueArray(module));
8 ✔
1817
            if (current != updated) {
3 ✔
1818
                setModulesChanged();  // NOTE: intentional object identity comparison
2 ✔
1819
            }
1820
        }
1821
    }
1 ✔
1822

1823
    protected void setModulesChanged() {
1824
        this.modulesChanged = true;
3 ✔
1825
    }
1 ✔
1826

1827
    @Override
1828
    public boolean checkModulesValid() {
1829
        for (final Module[] modules : allModules.values()) {
13 ✔
1830
            for (final Module module : modules) {
16 ✔
1831
                if (!module.isInternalModule()) {
3 ✔
1832
                    if (!((ExternalModule) module).moduleIsValid()) {
4 ✔
1833
                        if (LOG.isDebugEnabled()) {
3 !
1834
                            LOG.debug("Module with URI {} has changed and needs to be reloaded", module.getNamespaceURI());
×
1835
                        }
1836
                        return false;
2 ✔
1837
                    }
1838
                }
1839
            }
1840
        }
1841
        return true;
2 ✔
1842
    }
1843

1844
    @Override
1845
    public void analyzeAndOptimizeIfModulesChanged(final Expression expr) throws XPathException {
1846
        if (analyzed) {
3 ✔
1847
            return;
1 ✔
1848
        }
1849
        analyzed = true;
3 ✔
1850
        for (final Module[] modules : expr.getContext().modules.values()) {
14 ✔
1851
            for (final Module module : modules) {
16 ✔
1852
                if (!module.isInternalModule()) {
3 ✔
1853
                    final Expression root = ((ExternalModule) module).getRootExpression();
4 ✔
1854
                    ((ExternalModule) module).getContext().analyzeAndOptimizeIfModulesChanged(root);
5 ✔
1855
                }
1856
            }
1857
        }
1858
        expr.analyze(new AnalyzeContextInfo());
5 ✔
1859

1860
        if (optimizationsEnabled()) {
3 ✔
1861
            final Optimizer optimizer = new Optimizer(this);
5 ✔
1862
            expr.accept(optimizer);
3 ✔
1863

1864
            if (optimizer.hasOptimized()) {
3 ✔
1865
                reset(true);
3 ✔
1866
                expr.resetState(true);
3 ✔
1867
                expr.analyze(new AnalyzeContextInfo());
5 ✔
1868
            }
1869
        }
1870
        modulesChanged = false;
3 ✔
1871
    }
1 ✔
1872

1873
    @Override
1874
    public @Nullable Module loadBuiltInModule(final String namespaceURI, final String moduleClass) {
1875
        Module[] modules = null;
2 ✔
1876
        if (namespaceURI != null) {
2 !
1877
            modules = getModules(namespaceURI);
4 ✔
1878
        }
1879

1880
        if (modules != null) {
2 !
1881
            for (final Module module : modules) {
×
1882
                if (moduleClass.equals(module.getClass().getName())) {
×
1883
                    if (LOG.isDebugEnabled()) {
×
1884
                        LOG.debug("module {} is already present", namespaceURI);
×
1885
                    }
1886
                    return module;
×
1887
                }
1888
            }
1889
        }
1890

1891
        return initBuiltInModule(namespaceURI, moduleClass);
5 ✔
1892
    }
1893

1894
    @SuppressWarnings("unchecked")
1895
    Module initBuiltInModule(final String namespaceURI, final String moduleClassName) {
1896
        try {
1897
            // lookup the class
1898
            final ClassLoader existClassLoader;
1899
            if (getBroker() != null) {
3 ✔
1900
                existClassLoader = getBroker().getBrokerPool().getClassLoader();
5 ✔
1901
            } else {
1 ✔
1902
                existClassLoader = Thread.currentThread().getContextClassLoader();
3 ✔
1903
            }
1904

1905
            final Class<?> mClass = Class.forName(moduleClassName, false, existClassLoader);
5 ✔
1906

1907
            if (!(Module.class.isAssignableFrom(mClass))) {
4 !
1908
                LOG.info("failed to load module. {} is not an instance of org.exist.xquery.Module.", moduleClassName);
×
1909
                return null;
×
1910
            }
1911
            // INOTE: expathrepo
1912
            final Module module = instantiateModule(namespaceURI, (Class<Module>) mClass,
5 ✔
1913
                    getConfiguration() != null ? (Map<String, Map<String, List<? extends Object>>>) getConfiguration().getProperty(PROPERTY_MODULE_PARAMETERS) : Collections.emptyMap());
10 ✔
1914

1915
            if (LOG.isDebugEnabled()) {
3 !
1916
                LOG.debug("module {} loaded successfully.", module.getNamespaceURI());
×
1917
            }
1918
            return module;
2 ✔
1919
        } catch (final ClassNotFoundException e) {
1 ✔
1920
            LOG.warn("module class {} not found. Skipping...", moduleClassName);
4 ✔
1921
            return null;
2 ✔
1922
        }
1923
    }
1924

1925
    private @Nullable Module instantiateModule(final String namespaceURI, final Class<Module> moduleClass,
1926
                                               @Nullable final Map<String, Map<String, List<? extends Object>>> moduleParameters) {
1927
        try {
1928
            final Constructor<Module> constructor = XQueryContext.getModuleConstructor(moduleClass);
3 ✔
1929

1930
            if (constructor == null) {
2 !
1931
                LOG.warn("Module {} has no matching public constructor!", moduleClass.getName());
×
1932
                return null;
×
1933
            }
1934

1935
            final Module module;
1936
            if (constructor.getParameterCount() == 1) {
4 !
1937
                // a configuration built without conf.xml may have no module parameters at all
1938
                module = constructor.newInstance(moduleParameters != null ? moduleParameters.get(namespaceURI) : null);
16 ✔
1939
            } else {
1 ✔
1940
                // attempt for a constructor that takes 0 arguments
1941
                module = constructor.newInstance();
×
1942
            }
1943

1944
            if (namespaceURI != null && !module.getNamespaceURI().equals(namespaceURI)) {
7 !
1945
                LOG.warn("the module declares a different namespace URI. Expected: {} found: {}", namespaceURI, module.getNamespaceURI());
×
1946
                return null;
×
1947
            }
1948

1949
            if (getPrefixForURI(module.getNamespaceURI()) == null && !module.getDefaultPrefix().isEmpty()) {
9 !
1950
                declareNamespace(module.getDefaultPrefix(), module.getNamespaceURI());
6 ✔
1951
            }
1952

1953
            modules.compute(module.getNamespaceURI(), addToMapValueArray(module));
8 ✔
1954
            allModules.compute(module.getNamespaceURI(), addToMapValueArray(module));
8 ✔
1955

1956
            if (module instanceof InternalModule internalModule) {
6 !
1957
                internalModule.prepare(this);
3 ✔
1958
            }
1959
            return module;
2 ✔
1960
        } catch (final InstantiationException | IllegalAccessException | InvocationTargetException | XPathException e) {
×
1961
            LOG.warn("error while instantiating module class {}", moduleClass.getName(), e);
×
1962
            return null;
×
1963
        }
1964
    }
1965

1966
    private static @Nullable Constructor<Module> getModuleConstructor(Class<Module> moduleClazz) {
1967
        try {
1968
            // prefer constructor that takes one Map argument
1969
            return moduleClazz.getConstructor(Map.class);
9 ✔
1970
        } catch (final NoSuchMethodException noPreferredConstructor) {
×
1971
            // attempt for a constructor that takes 0 arguments
1972
            try {
1973
                return moduleClazz.getConstructor();
×
1974
            } catch (final NoSuchMethodException noUsableConstructor) {
×
1975
                return null;
×
1976
            }
1977
        }
1978
    }
1979

1980
    private static BiFunction<String, Module[], Module[]> addToMapValueArray(final Module module) {
1981
        return (namespaceURI, modules) -> {
3 ✔
1982
            if (modules == null) {
2 ✔
1983
                return new Module[]{module};
7 ✔
1984
            }
1985

1986
            // check if the module is already present
1987
            for (final Module existingModule : modules) {
16 ✔
1988
                if (existingModule == module) {  // NOTE: intentional object identity comparison
3 ✔
1989
                    return modules;  // already present, no further action needed
2 ✔
1990
                }
1991
            }
1992

1993
            // add the module to the end of current array of modules
1994
            final Module[] newModules = Arrays.copyOf(modules, modules.length + 1);
8 ✔
1995
            newModules[newModules.length - 1] = module;
7 ✔
1996
            return newModules;
2 ✔
1997
        };
1998
    }
1999

2000
    @Override
2001
    public void declareFunction(final UserDefinedFunction function) throws XPathException {
2002
        // TODO: redeclaring functions should be forbidden. however, throwing an
2003
        // exception will currently break util:eval.
2004

2005
        final QName name = function.getSignature().getName();
4 ✔
2006
        final String uri = name.getNamespaceURI();
3 ✔
2007

2008
        if (uri.isEmpty()) {
3 !
2009
            throw new XPathException(function, ErrorCodes.XQST0060,
×
2010
                    "Every declared function name must have a non-null namespace URI, " +
×
2011
                            "but function '" + name + "' does not meet this requirement.");
×
2012
        }
2013

2014
        if (Namespaces.PROTECTED_NS.contains(uri)) {
4 !
2015
            throw new XPathException(function, ErrorCodes.XQST0045,
×
2016
                    "Function '" + name + "' is in the forbidden namespace '" + uri + "'");
×
2017
        }
2018

2019
        final FunctionSignature signature = function.getSignature();
3 ✔
2020
        final FunctionId functionKey = signature.getFunctionId();
3 ✔
2021
        if (declaredFunctions.containsKey(functionKey)) {
5 ✔
2022
            throw new XPathException(function, ErrorCodes.XQST0034,
6 ✔
2023
                    "Function " + signature.getName().toURIQualifiedName() + '#' + signature.getArgumentCount()
6 ✔
2024
                            + " is already defined.");
2025
        }
2026

2027
        declaredFunctions.put(functionKey, function);
6 ✔
2028
    }
1 ✔
2029

2030
    @Override
2031
    public @Nullable UserDefinedFunction resolveFunction(final QName name, final int argCount) {
2032
        final FunctionId id = new FunctionId(name, argCount);
6 ✔
2033
        return declaredFunctions.get(id);
6 ✔
2034
    }
2035

2036
    @Override
2037
    public Iterator<FunctionSignature> getSignaturesForFunction(final QName name) {
2038
        final List<FunctionSignature> signatures = new ArrayList<>(2);
5 ✔
2039
        for (final UserDefinedFunction func : declaredFunctions.values()) {
13 ✔
2040
            if (func.getName().equals(name)) {
5 ✔
2041
                signatures.add(func.getSignature());
5 ✔
2042
            }
2043
        }
2044
        return signatures.iterator();
3 ✔
2045
    }
2046

2047
    @Override
2048
    public Iterator<UserDefinedFunction> localFunctions() {
2049
        return declaredFunctions.values().iterator();
5 ✔
2050
    }
2051

2052
    @Override
2053
    public LocalVariable declareVariableBinding(final LocalVariable var) throws XPathException {
2054
        if (lastVar != null) {
3 ✔
2055
            lastVar.addAfter(var);
4 ✔
2056
        }
2057
        lastVar = var;
3 ✔
2058
        var.setStackPosition(getCurrentStackSize());
4 ✔
2059
        var.markedUnder = contextStack.peek();
6 ✔
2060
        var.prevSameName = localVariableLookup.put(var.getQName(), var);
9 ✔
2061
        return var;
2 ✔
2062
    }
2063

2064
    @Override
2065
    public Variable declareGlobalVariable(final Variable var) {
2066
        globalVariables.put(var.getQName(), var);
7 ✔
2067
        var.setStackPosition(getCurrentStackSize());
4 ✔
2068
        return var;
2 ✔
2069
    }
2070

2071
    @Override
2072
    public void undeclareGlobalVariable(final QName name) {
2073
        globalVariables.remove(name);
×
2074
    }
×
2075

2076
    @Override
2077
    public Variable declareVariable(final String qname, final Object value) throws XPathException {
2078
        try {
2079
            return declareVariable(QName.parse(this, qname, null), value);
8 ✔
2080
        } catch (final QName.IllegalQNameException e) {
×
2081
            throw new XPathException(rootExpression, ErrorCodes.XPST0081, "No namespace defined for prefix: " + qname);
×
2082
        }
2083
    }
2084

2085
    @Override
2086
    public Variable declareVariable(final QName qn, final Object value) throws XPathException {
2087
        final Module[] modules = getModules(qn.getNamespaceURI());
5 ✔
2088

2089
        if (isNotEmpty(modules)) {
3 ✔
2090
            if (modules.length > 1) {
4 !
2091
                // TODO(AR) is seems that we will never enter this state...
2092
                throw new IllegalStateException("There is more than one module, but the variable can only be declared in one!");
×
2093
            }
2094

2095
            return modules[0].declareVariable(qn, value);
7 ✔
2096
        }
2097

2098
        final Sequence val = XPathUtil.javaObjectToXPath(value, this, rootExpression);
6 ✔
2099

2100
        final Variable var;
2101
        if (globalVariables.containsKey(qn)) {
5 ✔
2102
            var = globalVariables.get(qn);
6 ✔
2103
        } else {
1 ✔
2104
            var = new VariableImpl(qn);
5 ✔
2105
            globalVariables.put(qn, var);
6 ✔
2106
        }
2107

2108
        // deliberate duplication of code to return early
2109
        if (var.getSequenceType() == null) {
3 ✔
2110
            var.setValue(val);
3 ✔
2111
            return var;
2 ✔
2112
        }
2113

2114
        final Cardinality actualCardinality;
2115

2116
        if (val.isEmpty()) {
3 !
2117
            actualCardinality = Cardinality.EMPTY_SEQUENCE;
×
2118
        } else if (val.hasMany()) {
3 !
2119
            actualCardinality = Cardinality._MANY;
×
2120
        } else {
×
2121
            actualCardinality = Cardinality.EXACTLY_ONE;
2 ✔
2122
        }
2123

2124
        //Type.EMPTY is *not* a subtype of other types ; checking cardinality first
2125
        if (!var.getSequenceType().getCardinality().isSuperCardinalityOrEqualOf(actualCardinality)) {
6 !
2126
            throw new XPathException(rootExpression, ErrorCodes.XPTY0004,
×
2127
                    "XPTY0004: Invalid cardinality for variable $" + var.getQName() + ". " +
×
2128
                            "Expected " + var.getSequenceType().getCardinality().getHumanDescription() + ", " +
×
2129
                            "got " + actualCardinality.getHumanDescription());
×
2130
        }
2131

2132
        //TODO : ignore nodes right now ; they are returned as xs:untypedAtomicType
2133
        if (!val.isEmpty() && !Type.subTypeOf(val.getItemType(), var.getSequenceType().getPrimaryType())) {
10 !
2134
            throw new XPathException(rootExpression, ErrorCodes.XPTY0004,
×
2135
                    "XPTY0004: Invalid type for variable $" + var.getQName() + ". " +
×
2136
                            "Expected " + Type.getTypeName(var.getSequenceType().getPrimaryType()) + ", " +
×
2137
                            "got " + Type.getTypeName(val.getItemType()));
×
2138
        }
2139

2140
        //TODO : should we allow global variable *re*declaration ?
2141
        var.setValue(val);
3 ✔
2142
        return var;
2 ✔
2143
    }
2144

2145
    @Override
2146
    public Variable resolveVariable(final String name) throws XPathException {
2147
        try {
2148
            final QName qn = QName.parse(this, name, null);
5 ✔
2149
            return resolveVariable(qn);
4 ✔
2150
        } catch (final QName.IllegalQNameException e) {
×
2151
            throw new XPathException(rootExpression, ErrorCodes.XPST0081, "No namespace defined for prefix " + name);
×
2152
        }
2153
    }
2154

2155
    @Override
2156
    public Variable resolveVariable(final QName qname) throws XPathException {
2157
        return resolveVariable(null, qname);
5 ✔
2158
    }
2159

2160
    @Override
2161
    public Variable resolveVariable(@Nullable final AnalyzeContextInfo contextInfo, final QName qname) throws XPathException {
2162
        // check if the variable is declared local
2163
        Variable var = resolveLocalVariable(qname);
4 ✔
2164

2165
        // check if the variable is declared in a module
2166
        if (var == null) {
2 ✔
2167
            final Module[] modules = getModules(qname.getNamespaceURI());
5 ✔
2168

2169
            if (modules != null) {
2 ✔
2170
                for (final Module module : modules) {
16 ✔
2171
                    var = module.resolveVariable(contextInfo, qname);
5 ✔
2172
                    if (var != null) {
2 ✔
2173
                        break;
1 ✔
2174
                    }
2175
                }
2176
            }
2177
        }
2178

2179
        // check if the variable is declared global
2180
        if (var == null) {
2 ✔
2181
            var = globalVariables.get(qname);
6 ✔
2182
        }
2183

2184
        //if (var == null)
2185
        //  throw new XPathException(rootExpression, "variable $" + qname + " is not bound");
2186
        return var;
2 ✔
2187
    }
2188

2189
    Variable resolveGlobalVariable(final QName qname) {
2190
        return globalVariables.get(qname);
6 ✔
2191
    }
2192

2193
    protected Variable resolveLocalVariable(final QName qname) throws XPathException {
2194
        // O(1) fast path. The linked-list walk previously here is O(N) per
2195
        // call and O(N²) when a body of N variables is analyzed.
2196
        final LocalVariable var = localVariableLookup.get(qname);
6 ✔
2197
        if (var == null) {
2 ✔
2198
            return null;
2 ✔
2199
        }
2200
        // Visibility: var is visible if it was declared under the current
2201
        // contextStack mark — the same boundary the linked-list walk used to
2202
        // express by stopping at {@code contextStack.peek()}.
2203
        if (var.markedUnder != contextStack.peek()) {
6 ✔
2204
            return null;
2 ✔
2205
        }
2206
        return var;
2 ✔
2207
    }
2208

2209
    /**
2210
     * Returns the first (earliest-declared) local variable currently in scope, or
2211
     * {@code null} if there are no local variables in scope.
2212
     *
2213
     * <p>Walks backward from {@code lastVar} to find the oldest variable declared in the
2214
     * current scope, stopping at the context-stack boundary.  Used by
2215
     * {@link OrderByClause#eval} to recover the true first active variable when
2216
     * {@link AbstractFLWORClause#getStartVariable()} returns a stale reference
2217
     * (e.g. in a FLWOR with two {@code order by} clauses where the inner one is
2218
     * evaluated during the outer one's {@code postEval} replay).</p>
2219
     */
2220
    LocalVariable getFirstLocalVariable() {
2221
        final LocalVariable end = contextStack.peek();
5 ✔
2222
        LocalVariable first = null;
2 ✔
2223
        for (LocalVariable var = lastVar; var != null && var != end; var = var.before) {
12 !
2224
            first = var;
2 ✔
2225
        }
2226
        return first;
2 ✔
2227
    }
2228

2229
    @Override
2230
    public boolean isVarDeclared(final QName qname) {
2231
        final Module[] modules = getModules(qname.getNamespaceURI());
5 ✔
2232
        if (modules != null) {
2 !
2233
            for (final Module module : modules) {
×
2234
                if (module.isVarDeclared(qname)) {
×
2235
                    return true;
×
2236
                }
2237
            }
2238
        }
2239
        return globalVariables.containsKey(qname);
5 ✔
2240
    }
2241

2242
    @Override
2243
    public Map<QName, Variable> getVariables() {
2244
        final Map<QName, Variable> variables = new Object2ObjectRBTreeMap<>(globalVariables);
×
2245
        LocalVariable end = contextStack.peek();
×
2246
        for (LocalVariable var = lastVar; var != null; var = var.before) {
×
2247
            if (var == end) {
×
2248
                break;
×
2249
            }
2250
            variables.put(var.getQName(), var);
×
2251
        }
2252
        return variables;
×
2253
    }
2254

2255
    @Override
2256
    public Map<QName, Variable> getLocalVariables() {
2257
        final Map<QName, Variable> variables = new HashMap<>();
×
2258
        LocalVariable end = contextStack.peek();
×
2259
        for (LocalVariable var = lastVar; var != null; var = var.before) {
×
2260
            if (var == end) {
×
2261
                break;
×
2262
            }
2263
            variables.put(var.getQName(), var);
×
2264
        }
2265
        return variables;
×
2266
    }
2267

2268
    /**
2269
     * Return a copy of all currently visible local variables.
2270
     * Used by {@link InlineFunction} to implement closures.
2271
     *
2272
     * @return currently visible local variables as a stack
2273
     */
2274
    public List<ClosureVariable> getLocalStack() {
2275
        List<ClosureVariable> closure = null;
2 ✔
2276
        final LocalVariable end = contextStack.peek();
5 ✔
2277
        for (LocalVariable var = lastVar; var != null; var = var.before) {
9 ✔
2278

2279
            if (var == end) {
3 ✔
2280
                break;
1 ✔
2281
            }
2282

2283
            if (closure == null) {
2 ✔
2284
                closure = new ArrayList<>(6);
5 ✔
2285
            }
2286
            closure.add(new ClosureVariable(var));
7 ✔
2287
        }
2288

2289
        return closure;
2 ✔
2290
    }
2291

2292
    @Override
2293
    public Map<QName, Variable> getGlobalVariables() {
2294
        return new Object2ObjectRBTreeMap<>(globalVariables);
×
2295
    }
2296

2297
    /**
2298
     * Restore a saved stack of local variables. Used to implement closures.
2299
     *
2300
     * @param stack the stack of local variables
2301
     * @throws XPathException if the stack cannot be restored
2302
     */
2303
    public void restoreStack(final List<ClosureVariable> stack) throws XPathException {
2304
        for (int i = stack.size() - 1; i > -1; i--) {
10 ✔
2305
            declareVariableBinding(new ClosureVariable(stack.get(i)));
10 ✔
2306
        }
2307
    }
1 ✔
2308

2309
    @Override
2310
    public void setBackwardsCompatibility(boolean backwardsCompatible) {
2311
        this.backwardsCompatible = backwardsCompatible;
×
2312
    }
×
2313

2314
    @Override
2315
    public boolean isBackwardsCompatible() {
2316
        return this.backwardsCompatible;
3 ✔
2317
    }
2318

2319
    @Override
2320
    public boolean isRaiseErrorOnFailedRetrieval() {
2321
        return raiseErrorOnFailedRetrieval;
3 ✔
2322
    }
2323

2324
    public Database getDatabase() {
2325
        return db;
3 ✔
2326
    }
2327

2328
    @Override
2329
    public DBBroker getBroker() {
2330
        if (db != null) {
3 ✔
2331
            return db.getActiveBroker();
4 ✔
2332
        }
2333
        return null;
2 ✔
2334
    }
2335

2336
    public Configuration getConfiguration() {
2337
        return configuration;
3 ✔
2338
    }
2339

2340
    @Override
2341
    public Subject getSubject() {
2342
        return getBroker().getCurrentSubject();
4 ✔
2343
    }
2344

2345
    /**
2346
     * If there is a HTTP Session, and a User has been stored in the session then this will return the user object from the session.
2347
     *
2348
     * @return The user or null if there is no session or no user
2349
     */
2350
    Subject getUserFromHttpSession() {
2351
        final Optional<RequestWrapper> maybeRequest = Optional.ofNullable(getHttpContext())
4 ✔
2352
                .map(HttpContext::getRequest);
2 ✔
2353

2354
        if (maybeRequest.isPresent()) {
3 ✔
2355
            final RequestWrapper request = maybeRequest.get();
4 ✔
2356
            final Object user = request.getAttribute(HTTP_REQ_ATTR_USER);
4 ✔
2357
            final Object passAttr = request.getAttribute(HTTP_REQ_ATTR_PASS);
4 ✔
2358
            if (user != null) {
2 !
2359
                final String password = passAttr == null ? null : passAttr.toString();
×
2360
                try {
2361
                    return getBroker().getBrokerPool().getSecurityManager().authenticate(user.toString(), password);
×
2362
                } catch (final AuthenticationException e) {
×
2363
                    LOG.error("User can not be authenticated: {}", user.toString());
×
2364
                }
2365
            } else {
×
2366
                final Optional<SessionWrapper> maybeSession = Optional.ofNullable(getHttpContext())
4 ✔
2367
                        .map(HttpContext::getSession);
2 ✔
2368
                if (maybeSession.isPresent()) {
3 ✔
2369
                    try {
2370
                        return (Subject) maybeSession.get().getAttribute(HTTP_SESSIONVAR_XMLDB_USER);
7 ✔
2371
                    } catch (final IllegalStateException e) {
×
2372
                        // this is thrown if HttpSession#getAttribute is called on an invalid session
2373
                        return null;
×
2374
                    }
2375
                }
2376
            }
2377
        }
2378

2379
        return null;
2 ✔
2380
    }
2381

2382
    /**
2383
     * The builder used for creating in-memory document fragments.
2384
     */
2385
    private MemTreeBuilder documentBuilder = null;
3 ✔
2386

2387
    @Override
2388
    public MemTreeBuilder getDocumentBuilder() {
2389
        if (documentBuilder == null) {
3 ✔
2390
            documentBuilder = new MemTreeBuilder(rootExpression, this);
8 ✔
2391
            documentBuilder.startDocument();
3 ✔
2392
        }
2393
        return documentBuilder;
3 ✔
2394
    }
2395

2396
    @Override
2397
    public MemTreeBuilder getDocumentBuilder(final boolean explicitCreation) {
2398
        if (documentBuilder == null) {
3 !
2399
            documentBuilder = new MemTreeBuilder(rootExpression, this);
8 ✔
2400
            documentBuilder.startDocument(explicitCreation);
4 ✔
2401
        }
2402
        return documentBuilder;
3 ✔
2403
    }
2404

2405
    private void resetDocumentBuilder() {
2406
        this.documentBuilder = null;
3 ✔
2407
    }
1 ✔
2408

2409
    private void setDocumentBuilder(final MemTreeBuilder documentBuilder) {
2410
        this.documentBuilder = documentBuilder;
3 ✔
2411
    }
1 ✔
2412

2413
    @Override
2414
    public NamePool getSharedNamePool() {
2415
        if (sharedNamePool == null) {
3 ✔
2416
            sharedNamePool = new NamePool();
5 ✔
2417
        }
2418
        return sharedNamePool;
3 ✔
2419
    }
2420

2421
    @Override
2422
    public XQueryContext getContext() {
2423
        return null;
×
2424
    }
2425

2426
    @Override
2427
    public void prologEnter(final Expression expr) {
2428
        if (debuggeeJoint != null) {
3 !
2429
            debuggeeJoint.prologEnter(expr);
×
2430
        }
2431
    }
1 ✔
2432

2433
    @Override
2434
    public void expressionStart(final Expression expr) throws TerminatedException {
2435
        if (debuggeeJoint != null) {
3 !
2436
            debuggeeJoint.expressionStart(expr);
×
2437
        }
2438
    }
1 ✔
2439

2440
    @Override
2441
    public void expressionEnd(final Expression expr) {
2442
        if (debuggeeJoint != null) {
3 !
2443
            debuggeeJoint.expressionEnd(expr);
×
2444
        }
2445
    }
1 ✔
2446

2447
    @Override
2448
    public void stackEnter(final Expression expr) throws TerminatedException {
2449
        if (debuggeeJoint != null) {
3 !
2450
            debuggeeJoint.stackEnter(expr);
×
2451
        }
2452
    }
1 ✔
2453

2454
    @Override
2455
    public void stackLeave(final Expression expr) {
2456
        if (debuggeeJoint != null) {
3 !
2457
            debuggeeJoint.stackLeave(expr);
×
2458
        }
2459
    }
1 ✔
2460

2461
    @Override
2462
    public void proceed() throws TerminatedException {
2463
        getWatchDog().proceed(null);
4 ✔
2464
    }
1 ✔
2465

2466
    @Override
2467
    public void proceed(final Expression expr) throws TerminatedException {
2468
        getWatchDog().proceed(expr);
4 ✔
2469
    }
1 ✔
2470

2471
    @Override
2472
    public void proceed(final Expression expr, final MemTreeBuilder builder) throws TerminatedException {
2473
        getWatchDog().proceed(expr, builder);
5 ✔
2474
    }
1 ✔
2475

2476
    @Override
2477
    public void setWatchDog(final XQueryWatchDog watchdog) {
2478
        this.watchdog = watchdog;
3 ✔
2479
    }
1 ✔
2480

2481
    @Override
2482
    public XQueryWatchDog getWatchDog() {
2483
        return watchdog;
3 ✔
2484
    }
2485

2486
    private static final MemTreeBuilder NULL_DOCUMENT_BUILDER = new MemTreeBuilder((Expression) null);
6 ✔
2487

2488
    @Override
2489
    public void pushDocumentContext() {
2490
        if (documentBuilder == null) {
3 ✔
2491
            fragmentStack.push(NULL_DOCUMENT_BUILDER);
4 ✔
2492
        } else {
1 ✔
2493
            fragmentStack.push(documentBuilder);
5 ✔
2494
            resetDocumentBuilder();
2 ✔
2495
        }
2496
    }
1 ✔
2497

2498
    @Override
2499
    public void popDocumentContext() {
2500
        if (!fragmentStack.isEmpty()) {
4 !
2501
            final MemTreeBuilder prevBuilder = fragmentStack.pop();
5 ✔
2502
            if (prevBuilder == NULL_DOCUMENT_BUILDER) {
3 ✔
2503
                setDocumentBuilder(null);
3 ✔
2504
            } else {
1 ✔
2505
                setDocumentBuilder(prevBuilder);
3 ✔
2506
            }
2507
        }
2508
    }
1 ✔
2509

2510
    @Override
2511
    public void setBaseURI(final AnyURIValue uri) {
2512
        setBaseURI(uri, false);
4 ✔
2513
    }
1 ✔
2514

2515
    @Override
2516
    public void setBaseURI(final AnyURIValue uri, final boolean setInProlog) {
2517
        if (baseURISetInProlog) {
3 !
2518
            return;
×
2519
        }
2520

2521
        // TODO(JL): the previous code hinted that null _should_ be treated differently
2522
        // baseURI = (uri == null) ? AnyURIValue.EMPTY_URI : uri;
2523
        baseURI = uri;
3 ✔
2524
        baseURISetInProlog = setInProlog;
3 ✔
2525
    }
1 ✔
2526

2527
    @Override
2528
    public void setModuleLoadPath(final String path) {
2529
        this.moduleLoadPath = path;
3 ✔
2530
    }
1 ✔
2531

2532
    @Override
2533
    public String getModuleLoadPath() {
2534
        return moduleLoadPath;
3 ✔
2535
    }
2536

2537
    @Override
2538
    public boolean isBaseURIDeclared() {
2539
        return baseURI != null && !baseURI.equals(AnyURIValue.EMPTY_URI);
12 !
2540
    }
2541

2542
    @Override
2543
    public AnyURIValue getBaseURI() throws XPathException {
2544
        // the base URI in the static context is established according to the
2545
        // principles outlined in [RFC3986] Section 5.1—that is, it defaults
2546
        // first to the base URI of the encapsulating entity, then to the URI
2547
        // used to retrieve the entity, and finally to an implementation-defined
2548
        // default. If the URILiteral in the base URI declaration is a relative
2549
        // URI, then it is made absolute by resolving it with respect to this
2550
        // same hierarchy.
2551

2552
        // It is not intrinsically an error if this process fails to establish
2553
        // an absolute base URI; however, the base URI in the static context
2554
        // is then undefined, and any attempt to use its value may result in
2555
        // an error [err:XPST0001].
2556
//        if ((baseURI == null) || baseURI.equals(AnyURIValue.EMPTY_URI)) {
2557
//            //throw new XPathException(rootExpression, ErrorCodes.XPST0001, "Base URI of the static context  has not been assigned a value.");
2558
//            // We catch and resolve this to the XmlDbURI.ROOT_COLLECTION_URI
2559
//            // at least in DocumentImpl so maybe we should do it here./ljo
2560
//        }
2561
        return baseURI;
3 ✔
2562
    }
2563

2564
    @Override
2565
    public void setContextSequencePosition(final int pos, final Sequence sequence) {
2566
        contextPosition = pos;
3 ✔
2567
        contextSequence = sequence;
3 ✔
2568
    }
1 ✔
2569

2570
    @Override
2571
    public int getContextPosition() {
2572
        return contextPosition;
3 ✔
2573
    }
2574

2575
    @Override
2576
    public Sequence getContextSequence() {
2577
        return contextSequence;
3 ✔
2578
    }
2579

2580
    @Override
2581
    public void pushInScopeNamespaces() {
2582
        pushInScopeNamespaces(true);
3 ✔
2583
    }
1 ✔
2584

2585
    @Override
2586
    public void pushInScopeNamespaces(final boolean inherit) {
2587
        //TODO : push into an inheritedInScopeNamespaces HashMap... and return an empty HashMap
2588
        namespaceStack.push(inheritedInScopeNamespaces);
5 ✔
2589
        namespaceStack.push(inheritedInScopePrefixes);
5 ✔
2590
        namespaceStack.push(inScopeNamespaces);
5 ✔
2591
        namespaceStack.push(inScopePrefixes);
5 ✔
2592

2593
        //Current namespaces now become inherited just like the previous inherited ones
2594
        if (inherit) {
2 ✔
2595
            inheritedInScopeNamespaces = new HashMap<>(inheritedInScopeNamespaces);
7 ✔
2596
            inheritedInScopeNamespaces.putAll(inScopeNamespaces);
5 ✔
2597
            inheritedInScopePrefixes = new HashMap<>(inheritedInScopePrefixes);
7 ✔
2598
            inheritedInScopePrefixes.putAll(inScopePrefixes);
5 ✔
2599
        } else {
1 ✔
2600
            inheritedInScopeNamespaces = new HashMap<>();
5 ✔
2601
            inheritedInScopePrefixes = new HashMap<>();
5 ✔
2602
        }
2603

2604
        //TODO : consider dynamic instanciation
2605
        inScopeNamespaces = new HashMap<>();
5 ✔
2606
        inScopePrefixes = new HashMap<>();
5 ✔
2607
    }
1 ✔
2608

2609
    @Override
2610
    public void popInScopeNamespaces() {
2611
        inScopePrefixes = namespaceStack.pop();
6 ✔
2612
        inScopeNamespaces = namespaceStack.pop();
6 ✔
2613
        inheritedInScopePrefixes = namespaceStack.pop();
6 ✔
2614
        inheritedInScopeNamespaces = namespaceStack.pop();
6 ✔
2615
    }
1 ✔
2616

2617
    @Override
2618
    public void pushNamespaceContext() {
2619
        final Map<String, String> m = new HashMap<>(staticNamespaces);
6 ✔
2620
        final Map<String, String> p = new HashMap<>(staticPrefixes);
6 ✔
2621
        namespaceStack.push(staticNamespaces);
5 ✔
2622
        namespaceStack.push(staticPrefixes);
5 ✔
2623
        staticNamespaces = m;
3 ✔
2624
        staticPrefixes = p;
3 ✔
2625
    }
1 ✔
2626

2627
    @Override
2628
    public void popNamespaceContext() {
2629
        staticPrefixes = namespaceStack.pop();
6 ✔
2630
        staticNamespaces = namespaceStack.pop();
6 ✔
2631
    }
1 ✔
2632

2633
    @Override
2634
    public LocalVariable markLocalVariables(final boolean newContext) {
2635
        if (newContext) {
2 ✔
2636
            if (lastVar == null) {
3 ✔
2637
                lastVar = new LocalVariable(QName.EMPTY_QNAME);
6 ✔
2638
            }
2639
            contextStack.push(lastVar);
5 ✔
2640
        }
2641
        variableStackSize++;
6 ✔
2642
        pushBinaryValueFrame();
2 ✔
2643
        return lastVar;
3 ✔
2644
    }
2645

2646
    @Override
2647
    public void popLocalVariables(@Nullable final LocalVariable var) {
2648
        popLocalVariables(var, null);
4 ✔
2649
    }
1 ✔
2650

2651
    /**
2652
     * Restore the local variable stack to the position marked by variable var.
2653
     *
2654
     * <p>Walks {@link #lastVar} backward to {@code var} (or to the start when
2655
     * {@code var} is {@code null}), unwinding each variable's
2656
     * {@code prevSameName} chain into {@link #localVariableLookup} in
2657
     * REVERSE-of-declaration order so that names with multiple bindings in the
2658
     * popped scope settle on the still-visible binding, not on a popped one.
2659
     *
2660
     * <p>Any {@link BinaryValue} created within the scope being left is released here, unless it
2661
     * is reachable from {@code resultSeq} and so escapes to the enclosing scope. When
2662
     * {@code resultSeq} is null the escape set is unknown, so nothing is released and the values
2663
     * are handed to the enclosing scope instead - deferring release to the end of the query is
2664
     * recoverable, closing a value the query still needs is not.</p>
2665
     *
2666
     * @param var       only clear variables after this variable, or null
2667
     * @param resultSeq the result sequence
2668
     */
2669
    public void popLocalVariables(@Nullable final LocalVariable var, @Nullable final Sequence resultSeq) {
2670
        leaveBinaryValueScope(resultSeq);
3 ✔
2671

2672
        for (LocalVariable cursor = lastVar; cursor != null && cursor != var; cursor = cursor.before) {
12 ✔
2673
            if (localVariableLookup.get(cursor.getQName()) == cursor) {
7 ✔
2674
                if (cursor.prevSameName != null) {
3 ✔
2675
                    localVariableLookup.put(cursor.getQName(), cursor.prevSameName);
8 ✔
2676
                } else {
1 ✔
2677
                    localVariableLookup.remove(cursor.getQName());
6 ✔
2678
                }
2679
            }
2680
            cursor.prevSameName = null;
3 ✔
2681
        }
2682

2683
        if (var != null) {
2 ✔
2684
            // clear all variables registered after var. they should be out of scope.
2685
            LocalVariable outOfScope = var.after;
3 ✔
2686
            while (outOfScope != null) {
3 ✔
2687
                if (outOfScope != var && !outOfScope.isClosureVar()) {
6 !
2688
                    outOfScope.destroy(this, resultSeq);
4 ✔
2689
                }
2690
                outOfScope = outOfScope.after;
3 ✔
2691
            }
2692
            // reset the stack
2693
            var.after = null;
3 ✔
2694

2695
            if (!contextStack.isEmpty() && (var == contextStack.peek())) {
9 ✔
2696
                contextStack.pop();
4 ✔
2697
            }
2698
        }
2699
        lastVar = var;
3 ✔
2700
        variableStackSize--;
6 ✔
2701
    }
1 ✔
2702

2703
    /**
2704
     * Leave the binary value frame belonging to a local variable scope.
2705
     *
2706
     * @param resultSeq the sequence the scope returns, or null when it is not known - in which case
2707
     *     the frame promotes rather than releases
2708
     */
2709
    private void leaveBinaryValueScope(@Nullable final Sequence resultSeq) {
2710
        if (resultSeq != null) {
2 ✔
2711
            popBinaryValueFrame(resultSeq);
3 ✔
2712
        } else {
1 ✔
2713
            promoteBinaryValueFrame();
2 ✔
2714
        }
2715
    }
1 ✔
2716

2717
    /**
2718
     * Register a inline function using closure variables so it can be cleared
2719
     * after query execution.
2720
     *
2721
     * @param func an inline function definition using closure variables
2722
     */
2723
    void pushClosure(final UserDefinedFunction func) {
2724
        closures.add(func);
5 ✔
2725
    }
1 ✔
2726

2727
    @Override
2728
    public int getCurrentStackSize() {
2729
        return variableStackSize;
3 ✔
2730
    }
2731

2732
    @Override
2733
    public void functionStart(final FunctionSignature signature) {
2734
        callStack.push(signature);
4 ✔
2735
    }
1 ✔
2736

2737
    @Override
2738
    public void functionEnd() {
2739
        if (callStack.isEmpty()) {
4 !
2740
            LOG.warn("Function call stack is empty, but XQueryContext.functionEnd() was called. This "
×
2741
                    + "could indicate a concurrency issue (shared XQueryContext?)");
2742
        } else {
×
2743
            callStack.pop();
4 ✔
2744
        }
2745
    }
1 ✔
2746

2747
    @Override
2748
    public boolean tailRecursiveCall(final FunctionSignature signature) {
2749
        return callStack.contains(signature);
5 ✔
2750
    }
2751

2752
    @Override
2753
    public @Nullable Module[] importModule(@Nullable String namespaceURI, @Nullable String prefix, @Nullable AnyURIValue[] locationHints) throws XPathException {
2754

2755
        if (XML_NS_PREFIX.equals(prefix) || XMLNS_ATTRIBUTE.equals(prefix)) {
8 ✔
2756
            throw new XPathException(rootExpression, ErrorCodes.XQST0070,
7 ✔
2757
                    "The prefix declared for a module import must not be 'xml' or 'xmlns'.");
1 ✔
2758
        }
2759

2760
        if (namespaceURI != null && namespaceURI.isEmpty()) {
5 ✔
2761
            throw new XPathException(rootExpression, ErrorCodes.XQST0088,
7 ✔
2762
                    "The first URILiteral in a module import must be of nonzero length.");
1 ✔
2763
        }
2764

2765
        Module[] modules = null;
2 ✔
2766

2767
        if (namespaceURI != null) {
2 ✔
2768
            modules = getRootModules(namespaceURI);
4 ✔
2769
        }
2770

2771
        if (isNotEmpty(modules)) {
3 ✔
2772
            if (LOG.isDebugEnabled()) {
3 !
2773
                LOG.debug("Module {} already present.", namespaceURI);
×
2774
            }
2775
            // Set locally to remember the dependency in case it was inherited.
2776
            setModules(namespaceURI, modules);
4 ✔
2777

2778
        } else {
1 ✔
2779
            // if location is not specified, try to resolve in expath repo
2780
            if (isEmpty(locationHints) && namespaceURI != null) {
5 !
2781
                final Module module = resolveInEXPathRepository(namespaceURI, prefix);
5 ✔
2782
                if (module != null) {
2 ✔
2783
                    modules = new Module[]{module};
7 ✔
2784
                }
2785
            }
2786

2787
            if (isEmpty(modules)) {
3 ✔
2788

2789
                if (isEmpty(locationHints) && namespaceURI != null) {
5 !
2790
                    // check if there's a static mapping in the configuration
2791
                    final String moduleLocation = getModuleLocation(namespaceURI);
4 ✔
2792
                    if (moduleLocation != null) {
2 ✔
2793
                        locationHints = new AnyURIValue[]{new AnyURIValue(moduleLocation)};
10 ✔
2794
                    }
2795

2796
                    if (isEmpty(locationHints)) {
3 ✔
2797
                        locationHints = new AnyURIValue[]{new AnyURIValue(namespaceURI)};
10 ✔
2798
                    }
2799
                }
2800

2801
                for (int i = 0; i < locationHints.length; i++) {
8 ✔
2802
                    final Module module = importModuleFromLocation(namespaceURI, prefix, locationHints[i]);
8 ✔
2803
                    if (module != null) {
2 ✔
2804
                        if (modules == null) {
2 ✔
2805
                            modules = new Module[1];
3 ✔
2806
                        } else if (i >= modules.length) {
5 !
2807
                            modules = Arrays.copyOf(modules, modules.length + 1);
8 ✔
2808
                        }
2809
                        modules[modules.length - 1] = module;
7 ✔
2810
                    }
2811
                }
2812

2813
            } // NOTE: expathrepo related, closes the EXPath else (if module != null)
2814
        }
2815

2816
        if (isNotEmpty(modules)) {
3 ✔
2817
            if (namespaceURI == null) {
2 ✔
2818
                namespaceURI = modules[0].getNamespaceURI();
5 ✔
2819
            }
2820
            if (prefix == null) {
2 ✔
2821
                prefix = modules[0].getDefaultPrefix();
5 ✔
2822
            }
2823
            declareNamespace(prefix, namespaceURI);
4 ✔
2824
        }
2825

2826
        return modules;
2 ✔
2827
    }
2828

2829
    protected @Nullable Module importModuleFromLocation(final String namespaceURI, @Nullable final String prefix, final AnyURIValue locationHint) throws XPathException {
2830
        String location = locationHint.toString();
3 ✔
2831

2832
        // is it a Java module?
2833
        if (location.startsWith(JAVA_URI_START)) {
4 ✔
2834
            location = location.substring(JAVA_URI_START.length());
5 ✔
2835
            return loadBuiltInModule(namespaceURI, location);
5 ✔
2836
        }
2837

2838
        // Is the module source stored in the database?
2839
        if (location.startsWith(XmldbURI.XMLDB_URI_PREFIX)
4 ✔
2840
                || ((location.indexOf(':') == -1) && moduleLoadPath.startsWith(XmldbURI.XMLDB_URI_PREFIX))) {
10 ✔
2841

2842
            try {
2843
                XmldbURI locationUri = XmldbURI.xmldbUriFor(location);
3 ✔
2844

2845
                if (moduleLoadPath.startsWith(XmldbURI.XMLDB_URI_PREFIX)) {
5 ✔
2846
                    final XmldbURI moduleLoadPathUri = XmldbURI.xmldbUriFor(moduleLoadPath);
4 ✔
2847
                    locationUri = moduleLoadPathUri.resolveCollectionPath(locationUri);
4 ✔
2848
                }
2849

2850
                try (final LockedDocument lockedSourceDoc = getBroker().getXMLResource(locationUri.toCollectionPathURI(), LockMode.READ_LOCK)) {
11 ✔
2851

2852
                    final DocumentImpl sourceDoc = lockedSourceDoc == null ? null : lockedSourceDoc.getDocument();
7 ✔
2853
                    if (sourceDoc == null) {
2 ✔
2854
                        throw moduleLoadException("Module location hint URI '"
4 ✔
2855
                                + location + "' does not refer to anything.", location);
2 ✔
2856
                    }
2857

2858
                    if (sourceDoc.getResourceType() != DocumentImpl.BINARY_FILE
4 !
2859
                            || !"application/xquery".equals(sourceDoc.getMimeType())) {
5 !
2860
                        throw moduleLoadException("Module location hint URI '"
×
2861
                                + location + "' does not refer to an XQuery.", location);
×
2862
                    }
2863

2864
                    final Source moduleSource = new DBSource(getBroker().getBrokerPool(), (BinaryDocument) sourceDoc, true);
10 ✔
2865
                    return compileOrBorrowModule(namespaceURI, prefix, location, moduleSource);
7 ✔
2866

2867
                } catch (final PermissionDeniedException e) {
1 ✔
2868
                    throw moduleLoadException("Permission denied to read module source from location hint URI '" + location + ".", location, e);
7 ✔
2869
                }
2870
            } catch (final URISyntaxException e) {
×
2871
                throw moduleLoadException("Invalid module location hint URI '" + location + "'.", location, e);
×
2872
            }
2873

2874
        }
2875

2876
        // No. Load from file or URL
2877
        final Source moduleSource;
2878
        try {
2879
            //TODO: use URIs to ensure proper resolution of relative locations
2880
            final String contextPath;
2881
            if (source instanceof FileSource fileSource) {
9 ✔
2882
                final Path sourcePath = fileSource.getPath();
3 ✔
2883
                contextPath = sourcePath.resolveSibling(moduleLoadPath).normalize().toString();
7 ✔
2884
            } else {
1 ✔
2885
                contextPath = moduleLoadPath;
3 ✔
2886
            }
2887

2888
            moduleSource = SourceFactory.getSource(getBroker(), contextPath, location, true);
7 ✔
2889
            if (moduleSource == null) {
2 ✔
2890
                throw moduleLoadException("Source for module '" + namespaceURI + "' " +
4 ✔
2891
                        "not found module location hint URI '" + location + "'.", location);
3 ✔
2892
            }
2893
        } catch (final MalformedURLException e) {
×
2894
            throw moduleLoadException("Invalid module location hint URI '" + location + "'.", location, e);
×
2895
        } catch (final IOException e) {
×
2896
            throw moduleLoadException("Source for module '" + namespaceURI + "' could not be read, " +
×
2897
                    "module location hint URI '" + location + "'.", location, e);
×
2898
        } catch (final PermissionDeniedException e) {
×
2899
            throw moduleLoadException("Permission denied to read module source from location hint URI '" + location + ".", location, e);
×
2900
        }
2901

2902
        return compileOrBorrowModule(namespaceURI, prefix, location, moduleSource);
7 ✔
2903
    }
2904

2905
    protected XPathException moduleLoadException(final String message, final String moduleLocation)
2906
            throws XPathException {
2907
        return new XPathException(rootExpression, ErrorCodes.XQST0059, message, new ValueSequence(new StringValue(moduleLocation)));
15 ✔
2908
    }
2909

2910
    protected XPathException moduleLoadException(final String message, final String moduleLocation, final Exception e)
2911
            throws XPathException {
2912
        return new XPathException(rootExpression, ErrorCodes.XQST0059, message, new ValueSequence(new StringValue(moduleLocation)), e);
16 ✔
2913
    }
2914

2915
    @SuppressWarnings("unchecked")
2916
    @Override
2917
    public String getModuleLocation(final String namespaceURI) {
2918
        final Map<String, String> moduleMap =
1 ✔
2919
                (Map<String, String>) getConfiguration().getProperty(PROPERTY_STATIC_MODULE_MAP);
5 ✔
2920
        return moduleMap.get(namespaceURI);
5 ✔
2921
    }
2922

2923
    @SuppressWarnings("unchecked")
2924
    @Override
2925
    public Iterator<String> getMappedModuleURIs() {
2926
        final Map<String, String> moduleMap =
1 ✔
2927
                (Map<String, String>) getConfiguration().getProperty(PROPERTY_STATIC_MODULE_MAP);
5 ✔
2928
        return moduleMap.keySet().iterator();
4 ✔
2929
    }
2930

2931
    /**
2932
     * Compile of borrow an already compile module from the cache.
2933
     *
2934
     * @param namespaceURI the module namespace URI
2935
     * @param prefix the module namespace prefix
2936
     * @param location the location hint
2937
     * @param source the source for the module
2938
     *
2939
     * @return the module or null
2940
     *
2941
     * @throws XPathException if the module could not be loaded (XQST0059) or compiled (XPST0003)
2942
     */
2943
    private ExternalModule compileOrBorrowModule(final String namespaceURI, final String prefix, final String location,
2944
                                                 final Source source) throws XPathException {
2945
        final ExternalModule module = compileModule(namespaceURI, prefix, location, source);
7 ✔
2946
        if (module != null) {
2 ✔
2947
            addModule(module.getNamespaceURI(), module);
5 ✔
2948
            declareModuleVars(module);
3 ✔
2949
        }
2950
        return module;
2 ✔
2951
    }
2952

2953
    /**
2954
     * Compile an XQuery Module
2955
     *
2956
     * @param namespaceURI the namespace URI of the module.
2957
     * @param prefix       the namespace prefix of the module.
2958
     * @param location     the location of the module
2959
     * @param source       the source of the module.
2960
     * @return The compiled module, or null if the source is not a module
2961
     * @throws XPathException if the module could not be loaded (XQST0059) or compiled (XPST0003)
2962
     */
2963
    private @Nullable ExternalModule compileModule(String namespaceURI, final String prefix, final String location,
2964
                                                   final Source source) throws XPathException {
2965
        if (LOG.isDebugEnabled()) {
3 !
2966
            LOG.debug("Loading module from {}", location);
×
2967
        }
2968

2969
        try (final Reader reader = source.getReader()) {
7 ✔
2970
            if (reader == null) {
2 ✔
2971
                throw moduleLoadException("failed to load module: '" + namespaceURI + "' from: " +
4 ✔
2972
                        "'" + source + "', location: '" + location + "'. Source not found. ", location);
5 ✔
2973
            }
2974

2975
            if (namespaceURI == null) {
2 ✔
2976
                final QName qname = source.isModule();
3 ✔
2977
                if (qname == null) {
2 ✔
2978
                    return null;
2 ✔
2979
                }
2980
                namespaceURI = qname.getNamespaceURI();
3 ✔
2981
            }
2982

2983
            final ExternalModuleImpl modExternal = new ExternalModuleImpl(namespaceURI, prefix);
6 ✔
2984

2985
            // NOTE(AR) this is needed to support cyclic imports in XQuery 3.1, see: https://github.com/eXist-db/exist/pull/4996
2986
            addModule(namespaceURI, modExternal);
4 ✔
2987
            addModuleVertex(new ModuleVertex(namespaceURI, location));
7 ✔
2988

2989
            final XQueryContext modContext = new ModuleContext(this, namespaceURI, prefix, location);
8 ✔
2990
            modExternal.setContext(modContext);
3 ✔
2991
            final XQueryLexer lexer = new XQueryLexer(modContext, reader);
6 ✔
2992
            final XQueryParser parser = new XQueryParser(lexer);
5 ✔
2993
            final XQueryTreeParser astParser = new XQueryTreeParser(modContext, modExternal);
6 ✔
2994

2995
            try {
2996
                parser.xpath();
2 ✔
2997

2998
                if (parser.foundErrors()) {
3 !
2999
                    if (LOG.isDebugEnabled()) {
×
3000
                        LOG.debug(parser.getErrorMessage());
×
3001
                    }
3002
                    throw new XPathException(rootExpression, ErrorCodes.XPST0003, "error found while loading module from " + location + ": " + parser.getErrorMessage());
×
3003
                }
3004

3005
                final AST ast = parser.getAST();
3 ✔
3006

3007
                final PathExpr path = new PathExpr(modContext);
5 ✔
3008
                astParser.xpath(ast, path);
4 ✔
3009

3010
                if (astParser.foundErrors()) {
3 !
3011
                    throw new XPathException(rootExpression, ErrorCodes.XPST0003, "error found while loading module from " + location + ": " + astParser.getErrorMessage(), astParser.getLastException());
×
3012
                }
3013

3014
                modExternal.setRootExpression(path);
3 ✔
3015

3016
                if (namespaceURI != null && !modExternal.getNamespaceURI().equals(namespaceURI)) {
7 !
3017
                    throw new XPathException(rootExpression, ErrorCodes.XQST0059, "namespace URI declared by module (" + modExternal.getNamespaceURI() + ") does not match namespace URI in import statement, which was: " + namespaceURI);
11 ✔
3018
                }
3019

3020
                // Set source information on module context
3021
//            String sourceClassName = source.getClass().getName();
3022
//            modContext.setSourceKey(source.getKey().toString());
3023
                // Extract the source type from the classname by removing the package prefix and the "Source" suffix
3024
//            modContext.setSourceType( sourceClassName.substring( 17, sourceClassName.length() - 6 ) );
3025

3026
                modExternal.setSource(source);
3 ✔
3027
                modContext.setSource(source);
3 ✔
3028
                modExternal.setIsReady(true);
3 ✔
3029
                return modExternal;
2 ✔
3030
            } catch (final RecognitionException e) {
×
3031
                throw new XPathException(e.getLine(), e.getColumn(), ErrorCodes.XPST0003, "error found while loading module from " + location + ": " + e.getMessage());
×
3032
            } catch (final TokenStreamException e) {
×
3033
                throw new XPathException(rootExpression, ErrorCodes.XPST0003, "error found while loading module from " + location + ": " + e.getMessage(), e);
×
3034
            } catch (final XPathException e) {
1 ✔
3035
                e.prependMessage("Error while loading module " + location + ": ");
4 ✔
3036
                throw e;
2 ✔
3037
            }
3038
        } catch (final IOException e) {
×
3039
            throw moduleLoadException("IO exception while loading module '" + namespaceURI + "'" + " from '" + source + "'", location, e);
×
3040
        }
3041
    }
3042

3043
    private void declareModuleVars(final Module module) {
3044
        final String moduleNS = module.getNamespaceURI();
3 ✔
3045

3046
        for (final Iterator<Variable> i = globalVariables.values().iterator(); i.hasNext(); ) {
9 ✔
3047
            final Variable var = i.next();
4 ✔
3048

3049
            if (moduleNS.equals(var.getQName().getNamespaceURI())) {
6 ✔
3050
                module.declareVariable(var);
4 ✔
3051
                i.remove();
2 ✔
3052
            }
3053
        }
3054
    }
1 ✔
3055

3056
    @Override
3057
    public void addForwardReference(final FunctionCall call) {
3058
        forwardReferences.add(call);
5 ✔
3059
    }
1 ✔
3060

3061
    @Override
3062
    public void resolveForwardReferences() throws XPathException {
3063
        while (!forwardReferences.isEmpty()) {
5 ✔
3064
            final FunctionCall call = forwardReferences.pop();
5 ✔
3065
            final QName qname = call.getQName();
3 ✔
3066
            final int argumentCount = call.getArgumentCount();
3 ✔
3067
            final UserDefinedFunction func = call.getContext().resolveFunction(qname, argumentCount);
6 ✔
3068

3069
            if (func == null) {
2 ✔
3070
                throw new XPathException(call, ErrorCodes.XPST0017,
6 ✔
3071
                        Function.functionNotFoundErrorDescription(call.getContext(), qname, argumentCount));
5 ✔
3072
            }
3073
            call.resolveForwardReference(func);
3 ✔
3074
        }
3075
    }
1 ✔
3076

3077
    /**
3078
     * Get environment variables. The variables shall not change
3079
     * during execution of query.
3080
     *
3081
     * @return Map of environment variables
3082
     */
3083
    public Map<String, String> getEnvironmentVariables() {
3084
        if (envs == null) {
×
3085
            envs = System.getenv();
×
3086
        }
3087
        return envs;
×
3088
    }
3089

3090
    /**
3091
     * Gets the Effective user
3092
     * i.e. the user that the query is executing as
3093
     *
3094
     * @return The Effective User
3095
     */
3096
    public Subject getEffectiveUser() {
3097
        return getBroker().getCurrentSubject();
4 ✔
3098
    }
3099

3100
    /**
3101
     * Gets the Real User
3102
     * i.e. the user that initiated execution of the query
3103
     * Note this is not necessarily the same as the user that the
3104
     * query is executing as
3105
     *
3106
     * @return The Real User
3107
     * @see org.exist.xquery.XQueryContext#getEffectiveUser()
3108
     */
3109
    public Subject getRealUser() {
3110
        return realUser;
3 ✔
3111
    }
3112

3113
    private void setRealUser(final Subject realUser) {
3114
        this.realUser = realUser;
3 ✔
3115
    }
1 ✔
3116

3117
    /**
3118
     * Get a static decimal format.
3119
     *
3120
     * @param qnDecimalFormat the name of the decimal format, or null for the UNNAMED format.
3121
     * @return the decimal format, or null if there is no format matching the name
3122
     */
3123
    public @Nullable DecimalFormat getStaticDecimalFormat(@Nullable final QName qnDecimalFormat) {
3124
        final QName format = qnDecimalFormat == null ? UNNAMED_DECIMAL_FORMAT : qnDecimalFormat;
6 ✔
3125
        return staticDecimalFormats.get(format);
6 ✔
3126
    }
3127

3128
    /**
3129
     * Set a static decimal format.
3130
     *
3131
     * @param qnDecimalFormat the name of the decimal format
3132
     * @param decimalFormat the decimal format
3133
     */
3134
    public void setStaticDecimalFormat(final QName qnDecimalFormat, final DecimalFormat decimalFormat) {
3135
        staticDecimalFormats.put(qnDecimalFormat, decimalFormat);
6 ✔
3136
    }
1 ✔
3137

3138
    public void setDefaultStaticDecimalFormat(final DecimalFormat decimalFormat) {
3139
        staticDecimalFormats.put(UNNAMED_DECIMAL_FORMAT, decimalFormat);
6 ✔
3140
    }
1 ✔
3141

3142
    public Map<String, Sequence> getCachedUriCollectionResults() {
3143
        return cachedUriCollectionResults;
3 ✔
3144
    }
3145

3146
    /**
3147
     * Add a reference to an additional XQuery Context that
3148
     * was created by the XQuery (owning this XQuery Context)
3149
     * dynamically importing, compiling, and/or evaluating modules.
3150
     *
3151
     * NOTE(AR) - This is needed to ensure that these "imported contexts" are
3152
     * also correctly reset and cleaned up when this XQuery is finished.
3153
     *
3154
     * @param importedContext the dynamically created content for importing a module.
3155
     */
3156
    public void addImportedContext(final XQueryContext importedContext) {
3157
        if (importedContexts == null) {
3 ✔
3158
            importedContexts = new HashSet<>();
5 ✔
3159
            importedContextsCleanupTasksFns = new ArrayList<>();
5 ✔
3160
        }
3161
        importedContexts.add(importedContext);
5 ✔
3162
        importedContextsCleanupTasksFns.add(importedContext::runCleanupTasks);
9 ✔
3163
    }
1 ✔
3164

3165
    /**
3166
     * Save state
3167
     */
3168
    private class SavedState {
6 ✔
3169
        private Object2ObjectOpenHashMap<String, Module[]> modulesSaved = null;
3 ✔
3170
        private Object2ObjectOpenHashMap<String, Module[]> allModulesSaved = null;
3 ✔
3171
        private Map<String, String> staticNamespacesSaved = null;
3 ✔
3172
        private Map<String, String> staticPrefixesSaved = null;
3 ✔
3173

3174
        void save() {
3175
            if (modulesSaved != null) {
3 ✔
3176
                return;
1 ✔
3177
            }
3178

3179
            modulesSaved = new Object2ObjectOpenHashMap<>(modules.size(), Hash.VERY_FAST_LOAD_FACTOR);
10 ✔
3180
            for (final Object2ObjectMap.Entry<String, Module[]> entry : Object2ObjectMaps.fastIterable(modules)) {
14 ✔
3181
                final Module[] mods = entry.getValue();
4 ✔
3182
                modulesSaved.put(entry.getKey(), Arrays.copyOf(mods, mods.length));
12 ✔
3183
            }
3184

3185
            allModulesSaved = new Object2ObjectOpenHashMap<>(allModules.size());
9 ✔
3186
            for (final Object2ObjectMap.Entry<String, Module[]> entry : Object2ObjectMaps.fastIterable(allModules)) {
14 ✔
3187
                final Module[] mods = entry.getValue();
4 ✔
3188
                allModulesSaved.put(entry.getKey(), Arrays.copyOf(mods, mods.length));
12 ✔
3189
            }
3190

3191
            staticNamespacesSaved = new HashMap<>(staticNamespaces);
8 ✔
3192
            staticPrefixesSaved = new HashMap<>(staticPrefixes);
8 ✔
3193
        }
1 ✔
3194

3195
        void restore() {
3196
            if (modulesSaved == null) {
3 ✔
3197
                return;
1 ✔
3198
            }
3199

3200
            modules = modulesSaved;
5 ✔
3201
            modulesSaved = null;
3 ✔
3202
            allModules = allModulesSaved;
5 ✔
3203
            allModulesSaved = null;
3 ✔
3204
            staticNamespaces = staticNamespacesSaved;
5 ✔
3205
            staticNamespacesSaved = null;
3 ✔
3206
            staticPrefixes = staticPrefixesSaved;
5 ✔
3207
            staticPrefixesSaved = null;
3 ✔
3208
        }
1 ✔
3209
    }
3210

3211
    /**
3212
     * Before a dynamic import, make sure relevant parts of the current context a saved
3213
     * to the stack. This is important for util:import-module. The context will be restored
3214
     * during {@link #reset()}.
3215
     */
3216
    public void saveState() {
3217
        savedState.save();
3 ✔
3218
    }
1 ✔
3219

3220
    @Override
3221
    public boolean optimizationsEnabled() {
3222
        return enableOptimizer;
3 ✔
3223
    }
3224

3225
    @Override
3226
    public void addOption(final String name, final String value) throws XPathException {
3227
        if (staticOptions == null) {
3 ✔
3228
            staticOptions = new ArrayList<>();
5 ✔
3229
        }
3230
        addOption(staticOptions, name, value);
6 ✔
3231
    }
1 ✔
3232

3233
    @Override
3234
    public void addDynamicOption(final String name, final String value) throws XPathException {
3235
        if (dynamicOptions == null) {
×
3236
            dynamicOptions = new ArrayList<>();
×
3237
        }
3238
        addOption(dynamicOptions, name, value);
×
3239
    }
×
3240

3241
    private void addOption(final List<Option> options, final String name, final String value) throws XPathException {
3242
        final QName qn;
3243
        try {
3244
            qn = QName.parse(this, name, defaultFunctionNamespace);
6 ✔
3245
        } catch (final QName.IllegalQNameException e) {
1 ✔
3246
            throw new XPathException(rootExpression, ErrorCodes.XPST0081, "No namespace defined for prefix " + name);
×
3247
        }
3248

3249
        final Option option = new Option(rootExpression, qn, value);
8 ✔
3250
        // if the option exists, remove it first
3251
        options.remove(option);
4 ✔
3252
        //add option
3253
        options.add(option);
4 ✔
3254

3255
        // check predefined options
3256
        if (Option.PROFILE_QNAME.compareTo(qn) == 0) {
4 ✔
3257
            // configure profiling
3258
            profiler.configure(option);
4 ✔
3259

3260
        } else if (Option.TIMEOUT_QNAME.compareTo(qn) == 0) {
5 !
3261
            watchdog.setTimeoutFromOption(option);
×
3262

3263
        } else if (Option.OUTPUT_SIZE_QNAME.compareTo(qn) == 0) {
4 ✔
3264
            watchdog.setMaxNodesFromOption(option);
4 ✔
3265

3266
        } else if (Option.OPTIMIZE_QNAME.compareTo(qn) == 0) {
5 ✔
3267
            final String[] params = option.tokenizeContents();
3 ✔
3268
            if (params.length > 0) {
3 !
3269
                final String[] param = Option.parseKeyValuePair(params[0]);
5 ✔
3270
                if (param != null && "enable".equals(param[0])) {
8 !
3271
                    enableOptimizer = "yes".equals(param[1]);
7 ✔
3272
                }
3273
            }
3274
        }
1 ✔
3275
        //TODO : not sure how these 2 options might/have to be related
3276
        else if (Option.OPTIMIZE_IMPLICIT_TIMEZONE.compareTo(qn) == 0) {
4 ✔
3277
            //TODO : error check
3278
            final Duration duration = TimeUtils.getInstance().newDuration(option.getContents());
5 ✔
3279
            implicitTimeZone = new SimpleTimeZone((int) duration.getTimeInMillis(new Date()), "XQuery context");
12 ✔
3280

3281
        } else if (Option.CURRENT_DATETIME.compareTo(qn) == 0) {
5 ✔
3282
            //TODO : error check
3283
            final DateTimeValue dtv = new DateTimeValue(option.getContents());
6 ✔
3284
            calendar = (XMLGregorianCalendar) dtv.calendar.clone();
6 ✔
3285
        }
3286
    }
1 ✔
3287

3288
    @Override
3289
    public Option getOption(final QName qname) {
3290
        if (dynamicOptions != null) {
3 !
3291
            for (final Option option : dynamicOptions) {
×
3292
                if (qname.equals(option.getQName())) {
×
3293
                    return option;
×
3294
                }
3295
            }
3296
        }
3297

3298
        if (staticOptions != null) {
3 ✔
3299
            for (final Option option : staticOptions) {
12 ✔
3300
                if (qname.equals(option.getQName())) {
5 ✔
3301
                    return option;
2 ✔
3302
                }
3303
            }
3304
        }
3305

3306
        return null;
2 ✔
3307
    }
3308

3309
    @Override
3310
    public Pragma getPragma(final String name, final String contents) throws XPathException {
3311
        final QName qname;
3312
        try {
3313
            qname = QName.parse(this, name);
4 ✔
3314
        } catch (final QName.IllegalQNameException e) {
1 ✔
3315
            throw new XPathException(rootExpression, ErrorCodes.XPST0081, "No namespace defined for prefix " + name);
×
3316
        }
3317

3318
        final String ns = qname.getNamespaceURI();
3 ✔
3319
        if (ns.isEmpty()) {
3 !
3320
            throw new XPathException(rootExpression, ErrorCodes.XPST0081,
×
3321
                    "The namespace URI for Pragma ('" + name + "') is empty");
×
3322
        }
3323

3324
        if (!Namespaces.EXIST_NS.equals(ns)) {
4 !
3325
            // TODO(JL): should we throw or a least warn here?
3326
            return null;
×
3327
        }
3328

3329
        final String sanitizedContents = StringValue.trimWhitespace(contents);
3 ✔
3330

3331
        return switch (qname.getLocalPart()) {
5 !
3332
            case Optimize.OPTIMIZE_PRAGMA_LOCAL_NAME ->
3333
                    new Optimize(rootExpression, this, qname, sanitizedContents, true);
10 ✔
3334
            case TimePragma.TIME_PRAGMA_LOCAL_NAME, TimePragma.DEPRECATED_TIMER_PRAGMA_LOCAL_NAME ->
NEW
3335
                    new TimePragma(rootExpression, qname, sanitizedContents);
×
3336
            case ProfilePragma.PROFILING_PRAGMA_LOCAL_NAME ->
NEW
3337
                    new ProfilePragma(rootExpression, qname, sanitizedContents);
×
3338
            case ForceIndexUse.FORCE_INDEX_USE_PRAGMA_LOCAL_NAME ->
3339
                    new ForceIndexUse(rootExpression, qname, sanitizedContents);
8 ✔
3340
            case NoIndexPragma.NO_INDEX_PRAGMA_LOCAL_NAME ->
3341
                    new NoIndexPragma(rootExpression, qname, sanitizedContents);
8 ✔
UNCOV
3342
            default -> null;
×
3343
        };
3344
    }
3345

3346
    @Override
3347
    public DocumentImpl storeTemporaryDoc(final org.exist.dom.memtree.DocumentImpl doc) throws XPathException {
3348
        try {
3349
            final DocumentImpl targetDoc = getBroker().storeTempResource(doc);
×
3350

3351
            if (targetDoc == null) {
×
3352
                throw new XPathException(rootExpression, "Internal error: failed to store temporary doc fragment");
×
3353
            }
3354
            LOG.warn("Stored: {}: {}", targetDoc.getDocId(), targetDoc.getURI(), new Throwable());
×
3355
            return targetDoc;
×
3356
        } catch (final EXistException | LockException | PermissionDeniedException e) {
×
3357
            throw new XPathException(rootExpression, TEMP_STORE_ERROR, e);
×
3358
        }
3359
    }
3360

3361
    @Override
3362
    public void setAttribute(final String attribute, final Object value) {
3363
        attributes.put(attribute, value);
6 ✔
3364
    }
1 ✔
3365

3366
    @Override
3367
    public Object getAttribute(final String attribute) {
3368
        return attributes.get(attribute);
5 ✔
3369
    }
3370

3371
    /**
3372
     * Load the default prefix/namespace mappings table and set up internal functions.
3373
     *
3374
     * @param config the configuration if available
3375
     */
3376
    @SuppressWarnings("unchecked")
3377
    void loadDefaults(@Nullable final Configuration config) {
3378
        loadDefaultNS();
2 ✔
3379

3380
        if (config == null) {
2 ✔
3381
            return;
1 ✔
3382
        }
3383

3384
        // Switch: enable optimizer
3385
        final String queryRewritingOption = config.getProperty(PROPERTY_ENABLE_QUERY_REWRITING, "no");
6 ✔
3386
        this.enableOptimizer = "yes".equals(queryRewritingOption);
5 ✔
3387

3388
        // Switch: Backward compatibility
3389
        final String backwardsCompatOption = config.getProperty(PROPERTY_XQUERY_BACKWARD_COMPATIBLE, "yes");
6 ✔
3390
        this.backwardsCompatible = "yes".equals(backwardsCompatOption);
5 ✔
3391

3392
        // Switch: raiseErrorOnFailedRetrieval
3393
        this.raiseErrorOnFailedRetrieval =
2 ✔
3394
                config.getProperty(PROPERTY_XQUERY_RAISE_ERROR_ON_FAILED_RETRIEVAL, Boolean.FALSE);
6 ✔
3395

3396
        // Get map of built-in modules
3397
        final Map<String, Class<Module>> builtInModules =
1 ✔
3398
                (Map<String, Class<Module>>) config.getProperty(PROPERTY_BUILT_IN_MODULES);
4 ✔
3399

3400
        if (builtInModules == null) {
2 ✔
3401
            return;
1 ✔
3402
        }
3403

3404
        // Iterate on all map entries
3405
        for (final Map.Entry<String, Class<Module>> entry : builtInModules.entrySet()) {
12 ✔
3406
            addBuiltInModuleOrDeclareNamespace(config, entry);
4 ✔
3407
        }
3408
    }
1 ✔
3409

3410
    @SuppressWarnings("unchecked")
3411
    private void addBuiltInModuleOrDeclareNamespace(final Configuration config, final Map.Entry<String, Class<Module>> entry) {
3412
        final String namespaceURI = entry.getKey();
4 ✔
3413
        // first check if the module has already been loaded in the parent context
3414
        final Module[] modules = getModules(namespaceURI);
4 ✔
3415

3416
        // we have to instantiate the module class
3417
        final Class<Module> moduleClass = entry.getValue();
4 ✔
3418
        Module foundModule = null;
2 ✔
3419
        if (modules != null) {
2 ✔
3420
            for (final Module module : modules) {
15 !
3421
                if (moduleClass.equals(module.getClass())) {
5 !
3422
                    foundModule = module;
2 ✔
3423
                    break;
1 ✔
3424
                }
3425
            }
3426
        }
3427

3428
        if (foundModule == null) {
2 ✔
3429
            // Module does not exist yet, instantiate
3430
            instantiateModule(namespaceURI, moduleClass,
5 ✔
3431
                    (Map<String, Map<String, List<? extends Object>>>) config.getProperty(PROPERTY_MODULE_PARAMETERS));
4 ✔
3432
            return;
1 ✔
3433
        }
3434

3435
        if (getPrefixForURI(namespaceURI) == null && !foundModule.getDefaultPrefix().isEmpty()) {
8 !
3436
            // make sure the namespaces of default modules are known,
3437
            // even if they were imported in a parent context
3438
            try {
3439
                declareNamespace(foundModule.getDefaultPrefix(), foundModule.getNamespaceURI());
6 ✔
3440

3441
            } catch (final XPathException e) {
1 ✔
3442
                LOG.warn("Internal error while loading default modules: {}", e.getMessage(), e);
×
3443
            }
3444
        }
3445
    }
1 ✔
3446

3447
    /**
3448
     * Load default namespaces, e.g. xml, xsi, xdt, fn, local, exist and dbgp.
3449
     */
3450
    private void loadDefaultNS() {
3451
        try {
3452
            // default namespaces
3453
            staticNamespaces.put(XML_NS_PREFIX, XML_NS);
6 ✔
3454
            staticPrefixes.put(XML_NS, XML_NS_PREFIX);
6 ✔
3455
            declareNamespace("xs", Namespaces.SCHEMA_NS);
4 ✔
3456
            declareNamespace("xsi", Namespaces.SCHEMA_INSTANCE_NS);
4 ✔
3457

3458
            //required for backward compatibility
3459
            declareNamespace("xdt", Namespaces.XPATH_DATATYPES_NS);
4 ✔
3460
            declareNamespace("fn", Namespaces.XPATH_FUNCTIONS_NS);
4 ✔
3461
            declareNamespace("local", Namespaces.XQUERY_LOCAL_NS);
4 ✔
3462
            declareNamespace(Namespaces.W3C_XQUERY_XPATH_ERROR_PREFIX, Namespaces.W3C_XQUERY_XPATH_ERROR_NS);
4 ✔
3463

3464
            //*not* as standard NS
3465
            declareNamespace(Namespaces.EXIST_NS_PREFIX, Namespaces.EXIST_NS);
4 ✔
3466
            declareNamespace(Namespaces.EXIST_JAVA_BINDING_NS_PREFIX, Namespaces.EXIST_JAVA_BINDING_NS);
4 ✔
3467
            declareNamespace(Namespaces.EXIST_XQUERY_XPATH_ERROR_PREFIX, Namespaces.EXIST_XQUERY_XPATH_ERROR_NS);
4 ✔
3468

3469
            //TODO : include "err" namespace ?
3470
            declareNamespace("dbgp", Debuggee.NAMESPACE_URI);
4 ✔
3471

3472
        } catch (final XPathException e) {
1 ✔
3473
            //ignored because it should never happen
3474
            if (LOG.isDebugEnabled()) {
×
3475
                LOG.debug(e);
×
3476
            }
3477
        }
3478
    }
1 ✔
3479

3480
    @Override
3481
    public void registerUpdateListener(final UpdateListener listener) {
3482
        if (updateListener == null) {
3 ✔
3483
            updateListener = new ContextUpdateListener();
5 ✔
3484
            final DBBroker broker = getBroker();
3 ✔
3485
            broker.getBrokerPool().getNotificationService().subscribe(updateListener);
6 ✔
3486
        }
3487
        updateListener.addListener(listener);
4 ✔
3488
    }
1 ✔
3489

3490
    protected void clearUpdateListeners() {
3491
        if (updateListener != null) {
3 ✔
3492
            final DBBroker broker = getBroker();
3 ✔
3493
            broker.getBrokerPool().getNotificationService().unsubscribe(updateListener);
6 ✔
3494
            updateListener = null;
3 ✔
3495
        }
3496
    }
1 ✔
3497

3498
    @Override
3499
    public void checkOptions(final Properties properties) throws XPathException {
3500
        checkLegacyOptions(properties);
3 ✔
3501

3502
        // Phase 1: Process parameter-document first (provides base settings)
3503
        processParameterDocument(dynamicOptions, properties);
5 ✔
3504
        processParameterDocument(staticOptions, properties);
5 ✔
3505

3506
        // Phase 2: Process inline options (override parameter-document settings)
3507
        if (dynamicOptions != null) {
3 !
3508
            for (final Option option : dynamicOptions) {
×
3509
                if (Namespaces.XSLT_XQUERY_SERIALIZATION_NS.equals(option.getQName().getNamespaceURI())
×
3510
                        && !"parameter-document".equals(option.getQName().getLocalPart())) {
×
3511
                    SerializerUtils.setProperty(option.getQName().getLocalPart(), option.getContents(), properties,
×
3512
                            this::getURIForPrefix);
×
3513
                }
3514
            }
3515
        }
3516

3517
        if (staticOptions != null) {
3 ✔
3518
            for (final Option option : staticOptions) {
12 ✔
3519
                if (Namespaces.XSLT_XQUERY_SERIALIZATION_NS.equals(option.getQName().getNamespaceURI())
6 ✔
3520
                        && !"parameter-document".equals(option.getQName().getLocalPart())
6 !
3521
                        && !properties.containsKey(option.getQName().getLocalPart())) {
6 ✔
3522
                    SerializerUtils.setProperty(option.getQName().getLocalPart(), option.getContents(), properties,
7 ✔
3523
                            this::getURIForPrefix);
2 ✔
3524
                }
3525
            }
3526
        }
3527
    }
1 ✔
3528

3529
    /**
3530
     * Process the parameter-document serialization option if present.
3531
     * Loads the referenced XML file and extracts serialization parameters.
3532
     */
3533
    private void processParameterDocument(final List<Option> options, final Properties properties) throws XPathException {
3534
        if (options == null) return;
3 ✔
3535
        for (final Option option : options) {
11 ✔
3536
            if (Namespaces.XSLT_XQUERY_SERIALIZATION_NS.equals(option.getQName().getNamespaceURI())
6 ✔
3537
                    && "parameter-document".equals(option.getQName().getLocalPart())) {
6 !
3538
                final String docPath = option.getContents().trim();
×
3539
                if (docPath.isEmpty()) continue;
×
3540
                try {
3541
                    // Resolve relative to static base URI
3542
                    URI resolvedUri;
3543
                    final AnyURIValue baseURI = getBaseURI();
×
3544
                    if (baseURI != null && !baseURI.getStringValue().isEmpty()) {
×
3545
                        resolvedUri = new URI(baseURI.getStringValue()).resolve(docPath);
×
3546
                    } else {
×
3547
                        resolvedUri = new URI(docPath);
×
3548
                    }
3549

3550
                    // Load and parse the XML document
3551
                    final java.io.InputStream is;
3552
                    if ("file".equals(resolvedUri.getScheme())) {
×
3553
                        is = new java.io.FileInputStream(new java.io.File(resolvedUri));
×
3554
                    } else if (resolvedUri.getScheme() == null) {
×
3555
                        // Bare path — try as file
3556
                        is = new java.io.FileInputStream(resolvedUri.getPath());
×
3557
                    } else {
×
3558
                        is = resolvedUri.toURL().openStream();
×
3559
                    }
3560

3561
                    try (is) {
×
3562
                        final org.exist.dom.memtree.DocumentImpl doc = DocUtils.parse(this, is);
×
3563
                        if (doc != null) {
×
3564
                            SerializerUtils.getSerializationOptions(
×
3565
                                    getRootExpression(), doc, properties);
×
3566
                        }
3567
                    }
3568
                } catch (final Exception e) {
×
3569
                    // Parameter document loading failure is not fatal — log and continue
3570
                    if (LOG.isDebugEnabled()) {
×
3571
                        LOG.debug("Failed to load parameter-document '{}': {}", docPath, e.getMessage());
×
3572
                    }
3573
                }
3574
            }
3575
        }
3576
    }
1 ✔
3577

3578
    /**
3579
     * Legacy method to check serialization properties set via option exist:serialize.
3580
     *
3581
     * @param properties the serialization properties
3582
     * @throws XPathException if there is an unknown serialization property
3583
     */
3584
    private void checkLegacyOptions(final Properties properties) throws XPathException {
3585
        final Option pragma = getOption(Option.SERIALIZE_QNAME);
4 ✔
3586
        if (pragma == null) {
2 ✔
3587
            return;
1 ✔
3588
        }
3589

3590
        final String[] contents = pragma.tokenizeContents();
3 ✔
3591

3592
        for (final String content : contents) {
16 ✔
3593
            final String[] pair = Option.parseKeyValuePair(content);
3 ✔
3594

3595
            if (pair == null) {
2 !
3596
                throw new XPathException(rootExpression, "Unknown parameter found in " + pragma.getQName().getStringValue()
×
3597
                        + ": '" + content + "'");
×
3598
            }
3599

3600
            if (LOG.isDebugEnabled()) {
3 !
3601
                LOG.debug("Setting serialization property from pragma: {} = {}", pair[0], pair[1]);
×
3602
            }
3603

3604
            properties.setProperty(pair[0], pair[1]);
9 ✔
3605
        }
3606
    }
1 ✔
3607

3608
    @Override
3609
    public void setDebuggeeJoint(final DebuggeeJoint joint) {
3610
        //XXX: if (debuggeeJoint != null) ???
3611
        debuggeeJoint = joint;
×
3612
    }
×
3613

3614
    @Override
3615
    public DebuggeeJoint getDebuggeeJoint() {
3616
        return debuggeeJoint;
×
3617
    }
3618

3619
    @Override
3620
    public boolean isDebugMode() {
3621
        return debuggeeJoint != null && isVarDeclared(Debuggee.SESSION);
×
3622
    }
3623

3624
    @Override
3625
    public boolean requireDebugMode() {
3626
        return isVarDeclared(Debuggee.SESSION);
4 ✔
3627
    }
3628

3629
    @Override
3630
    public void pushBinaryValueFrame() {
3631
        if (binaryValueFrameStarts != null) {
3 ✔
3632
            recordBinaryValueFrameStart();
2 ✔
3633
        }
3634
        binaryValueFrameDepth++;
6 ✔
3635
    }
1 ✔
3636

3637
    private void recordBinaryValueFrameStart() {
3638
        if (binaryValueFrameDepth == binaryValueFrameStarts.length) {
6 ✔
3639
            binaryValueFrameStarts = Arrays.copyOf(binaryValueFrameStarts, binaryValueFrameDepth * 2);
9 ✔
3640
        }
3641
        binaryValueFrameStarts[binaryValueFrameDepth] = binaryValueInstances.size();
8 ✔
3642
    }
1 ✔
3643

3644
    @Override
3645
    public void popBinaryValueFrame(@Nullable final Sequence escaping) {
3646
        final int start = leaveBinaryValueFrame();
3 ✔
3647
        if (start < 0) {
2 ✔
3648
            return;
1 ✔
3649
        }
3650

3651
        // release in the reverse of the order the values were registered in
3652
        for (int i = binaryValueInstances.size() - 1; i >= start; i--) {
11 ✔
3653
            final BinaryValue binaryValue = binaryValueInstances.get(i);
6 ✔
3654

3655
            if (escaping != null && (escaping == binaryValue || escaping.containsReference(binaryValue))) {
9 !
3656
                // the value escapes this frame, so it now belongs to the enclosing one
3657
                continue;
1 ✔
3658
            }
3659

3660
            try {
3661
                binaryValue.close();
2 ✔
3662
            } catch (final IOException e) {
1 ✔
NEW
3663
                LOG.warn("Unable to close binary value on leaving scope: {}", e.getMessage(), e);
×
3664
            }
3665
            binaryValueInstances.remove(i);
5 ✔
3666
        }
3667
    }
1 ✔
3668

3669
    @Override
3670
    public void promoteBinaryValueFrame() {
3671
        // everything the frame owns is handed to the enclosing frame; nothing is closed
3672
        leaveBinaryValueFrame();
3 ✔
3673
    }
1 ✔
3674

3675
    /**
3676
     * Leave the innermost frame.
3677
     *
3678
     * @return the index into {@link #binaryValueInstances} at which the frame started,
3679
     *     or -1 if it holds nothing to release
3680
     */
3681
    private int leaveBinaryValueFrame() {
3682
        if (binaryValueFrameDepth == 0) {
3 ✔
3683
            // an unbalanced pop: the values stay in the pre-frame region and are released
3684
            // by the BinaryValueCleanupTask at the end of the query
3685
            return -1;
2 ✔
3686
        }
3687

3688
        binaryValueFrameDepth--;
6 ✔
3689

3690
        if (binaryValueFrameStarts == null) {
3 ✔
3691
            // no binary value has ever been registered with this context
3692
            return -1;
2 ✔
3693
        }
3694

3695
        return Math.min(binaryValueFrameStarts[binaryValueFrameDepth], binaryValueInstances.size());
10 ✔
3696
    }
3697

3698
    @Override
3699
    public void registerBinaryValueInstance(final BinaryValue binaryValue) {
3700
        if (binaryValueInstances == null) {
3 ✔
3701
            binaryValueInstances = new ArrayList<>();
5 ✔
3702
            binaryValueFrameStarts = new int[Math.max(binaryValueFrameDepth, INITIAL_BINARY_VALUE_FRAMES)];
7 ✔
3703
        }
3704

3705
        if (cleanupTasks.isEmpty() || cleanupTasks.stream().noneMatch(ct -> ct instanceof BinaryValueCleanupTask)) {
13 !
3706
            cleanupTasks.add(new BinaryValueCleanupTask());
7 ✔
3707
        }
3708

3709
        binaryValueInstances.add(binaryValue);
5 ✔
3710
    }
1 ✔
3711

3712
    /**
3713
     * Cleanup Task which is responsible for relasing the streams
3714
     * of any {@link BinaryValue} which have been used during
3715
     * query execution
3716
     */
3717
    public static class BinaryValueCleanupTask implements CleanupTask {
3 ✔
3718
        @Override
3719
        public void cleanup(final XQueryContext context, final Predicate<Object> predicate) {
3720
            if (context.binaryValueInstances == null) {
3 !
3721
                return;
×
3722
            }
3723

3724
            // release in the reverse of the order the values were registered in
3725
            for (int i = context.binaryValueInstances.size() - 1; i >= 0; i--) {
10 ✔
3726
                final BinaryValue bv = context.binaryValueInstances.get(i);
6 ✔
3727
                try {
3728
                    if (predicate.test(bv)) {
4 ✔
3729
                        bv.close();
2 ✔
3730
                        context.binaryValueInstances.remove(i);
5 ✔
3731
                    }
3732
                } catch (final IOException e) {
1 ✔
3733
                    LOG.error("Unable to close binary value: {}", e.getMessage(), e);
×
3734
                }
3735
            }
3736

3737
            // any frame still open when the query ends is void; whatever it held has either
3738
            // been released above or was spared by the predicate, and belongs to no frame now
3739
            context.binaryValueFrameDepth = 0;
3 ✔
3740
        }
1 ✔
3741
    }
3742

3743
    @Override
3744
    public String getCacheClass() {
3745
        return (String) getConfiguration().getProperty(Configuration.BINARY_CACHE_CLASS_PROPERTY);
6 ✔
3746
    }
3747

3748
    @Override
3749
    public void destroyBinaryValue(final BinaryValue value) {
3750
        if (binaryValueInstances == null) {
3 !
NEW
3751
            return;
×
3752
        }
3753

3754
        // by identity: two distinct binary values can compare equal
3755
        int idx = -1;
2 ✔
3756
        for (int i = binaryValueInstances.size() - 1; i >= 0; i--) {
10 !
3757
            if (binaryValueInstances.get(i) == value) {
6 ✔
3758
                idx = i;
2 ✔
3759
                break;
1 ✔
3760
            }
3761
        }
3762
        if (idx == -1) {
3 !
NEW
3763
            return;
×
3764
        }
3765

3766
        binaryValueInstances.remove(idx);
5 ✔
3767

3768
        // frames starting after the removed value shift down with it
3769
        for (int frame = 0; frame < binaryValueFrameDepth; frame++) {
8 ✔
3770
            if (binaryValueFrameStarts[frame] > idx) {
6 ✔
3771
                binaryValueFrameStarts[frame]--;
8 ✔
3772
            }
3773
        }
3774
    }
1 ✔
3775

3776
    public void setXQueryVersion(int version) {
3777
        xqueryVersion = version;
3 ✔
3778
    }
1 ✔
3779

3780
    public int getXQueryVersion() {
3781
        return xqueryVersion;
3 ✔
3782
    }
3783

3784
    @Override
3785
    public Source getSource() {
3786
        return source;
3 ✔
3787
    }
3788

3789
    @Override
3790
    public void setSource(final Source source) {
3791
        this.source = source;
3 ✔
3792
    }
1 ✔
3793

3794
    /**
3795
     * Get how much of a failed execution may be disclosed to the caller.
3796
     *
3797
     * @return the error disclosure level, never null
3798
     */
3799
    public ErrorDisclosure getErrorDisclosure() {
3800
        return errorDisclosure;
3 ✔
3801
    }
3802

3803
    /**
3804
     * Set how much of a failed execution may be disclosed to the caller.
3805
     *
3806
     * This must be recomputed from the current subject on every execution — a compiled query is
3807
     * pooled and shared between users, so the level of a previous execution must never be reused.
3808
     *
3809
     * @param errorDisclosure the error disclosure level
3810
     */
3811
    public void setErrorDisclosure(final ErrorDisclosure errorDisclosure) {
3812
        this.errorDisclosure = errorDisclosure;
3 ✔
3813
    }
1 ✔
3814

3815
    @Override
3816
    public String getDefaultLanguage() {
3817
        return DefaultLanguage;
2 ✔
3818
    }
3819

3820
    /**
3821
     * NOTE: the {@link #unsubscribe()} method can be called
3822
     * from {@link org.exist.storage.NotificationService#unsubscribe(UpdateListener)}
3823
     * by another thread, so this class needs to be thread-safe.
3824
     */
3825
    @ThreadSafe
3826
    private static class ContextUpdateListener implements UpdateListener {
3 ✔
3827
        /*
3828
         * We use Concurrent safe data structures here, so that we don't have
3829
         * to block any calling threads.
3830
         *
3831
         * The AtomicReference enables us to quickly clear the listeners
3832
         * in #unsubscribe() and maintain happens-before integrity whilst
3833
         * unsubscribing them. The CopyOnWriteArrayList allows
3834
         * us to add listeners whilst iterating over a snapshot
3835
         * of existing iterators in other methods.
3836
         */
3837
        private final AtomicReference<List<UpdateListener>> listeners = new AtomicReference<>(new CopyOnWriteArrayList<>());
8 ✔
3838

3839
        private void addListener(final UpdateListener listener) {
3840
            listeners.get().add(listener);
7 ✔
3841
        }
1 ✔
3842

3843
        @Override
3844
        public void documentUpdated(final DocumentImpl document, final int event) {
3845
            listeners.get().forEach(listener -> listener.documentUpdated(document, event));
13 ✔
3846
        }
1 ✔
3847

3848
        @Override
3849
        public void unsubscribe() {
3850
            List<UpdateListener> prev = listeners.get();
5 ✔
3851
            final List<UpdateListener> next = new CopyOnWriteArrayList<>();
4 ✔
3852
            while (!listeners.compareAndSet(prev, next)) {
7 !
3853
                prev = listeners.get();
×
3854
            }
3855

3856
            prev.forEach(UpdateListener::unsubscribe);
3 ✔
3857
        }
1 ✔
3858

3859
        @Override
3860
        public void nodeMoved(final NodeId oldNodeId, final NodeHandle newNode) {
3861
            listeners.get().forEach(listener -> listener.nodeMoved(oldNodeId, newNode));
13 ✔
3862
        }
1 ✔
3863

3864
        @Override
3865
        public void debug() {
3866
            if (LOG.isDebugEnabled()) {
×
3867
                LOG.debug("XQueryContext: {} document update listeners", listeners.get().size());
×
3868
            }
3869

3870
            listeners.get().forEach(UpdateListener::debug);
×
3871
        }
×
3872
    }
3873

3874
    private final List<CleanupTask> cleanupTasks = new ArrayList<>();
5 ✔
3875

3876
    public void registerCleanupTask(final CleanupTask cleanupTask) {
3877
        cleanupTasks.add(cleanupTask);
5 ✔
3878
    }
1 ✔
3879

3880
    public interface CleanupTask {
3881
        void cleanup(final XQueryContext context, final Predicate<Object> predicate);
3882
    }
3883

3884
    @Override
3885
    public void runCleanupTasks(final Predicate<Object> predicate) {
3886
        if (importedContextsCleanupTasksFns != null) {
3 ✔
3887
            for (final Consumer<Predicate<Object>> importedContextsCleanupTasksFn : importedContextsCleanupTasksFns) {
12 ✔
3888
                importedContextsCleanupTasksFn.accept(predicate);
3 ✔
3889
            }
3890
            importedContextsCleanupTasksFns = null;
3 ✔
3891
        }
3892

3893
        for (final CleanupTask cleanupTask : cleanupTasks) {
12 ✔
3894
            try {
3895
                cleanupTask.cleanup(this, predicate);
4 ✔
3896
            } catch (final Throwable t) {
1 ✔
3897
                LOG.error("Cleaning up XQueryContext: Ignoring: {}", t.getMessage(), t);
×
3898
            }
3899
        }
3900
        // now it is safe to clear the cleanup tasks list as we know they have run
3901
        // do not move this anywhere else
3902
        cleanupTasks.clear();
3 ✔
3903
    }
1 ✔
3904

3905
    @Immutable
3906
    public static class HttpContext {
3907
        private final RequestWrapper request;
3908
        private final ResponseWrapper response;
3909
        private final SessionWrapper session;
3910

3911
        public HttpContext(final RequestWrapper request, final ResponseWrapper response, final SessionWrapper session) {
2 ✔
3912
            this.request = request;
3 ✔
3913
            this.response = response;
3 ✔
3914
            this.session = session;
3 ✔
3915
        }
1 ✔
3916

3917
        public HttpContext(final RequestWrapper request, final ResponseWrapper response) {
2 ✔
3918
            this.request = request;
3 ✔
3919
            this.response = response;
3 ✔
3920
            this.session = request.getSession(false);
5 ✔
3921
        }
1 ✔
3922

3923
        public RequestWrapper getRequest() {
3924
            return request;
3 ✔
3925
        }
3926

3927
        public ResponseWrapper getResponse() {
3928
            return response;
3 ✔
3929
        }
3930

3931
        public SessionWrapper getSession() {
3932
            return session;
3 ✔
3933
        }
3934

3935
        /**
3936
         * Returns a new HttpContext with the new session set.
3937
         * <p>
3938
         * The request and response are referenced from this object.
3939
         *
3940
         * @param newSession the new session to set.
3941
         * @return the new HttpContext.
3942
         */
3943
        public HttpContext setSession(final SessionWrapper newSession) {
3944
            return new HttpContext(request, response, newSession);
9 ✔
3945
        }
3946
    }
3947

3948
    @Override
3949
    public String toString() {
3950
        return getStringValue();
×
3951
    }
3952

3953
    public String getStringValue() {
3954
        final StringBuilder sb = new StringBuilder();
×
3955
        sb.append('{');
×
3956

3957
        sb.append("dynamicDocuments: {");
×
3958
        if (dynamicDocuments != null) {
×
3959
            for (final String key : dynamicDocuments.keySet()) {
×
3960
                sb.append(key).append("-> ");
×
3961
                sb.append(dynamicDocuments.get(key));
×
3962
            }
3963
        }
3964
        sb.append('}');
×
3965
        sb.append('\n');
×
3966

3967
        sb.append("dynamicTextResources: {");
×
3968
        if (dynamicTextResources != null) {
×
NEW
3969
            for (final Map.Entry<Tuple2<String, Charset>, QuadFunctionE<DBBroker, Txn, String, Charset, Reader, XPathException>> entry : dynamicTextResources.entrySet()) {
×
3970
                sb.append(entry.getKey()).append("-> ").append(entry.getValue());
×
3971
            }
3972
        }
3973
        sb.append('}');
×
3974
        sb.append('\n');
×
3975

3976
        sb.append("dynamicCollections: {");
×
3977
        if (dynamicCollections != null) {
×
3978
            for (final Map.Entry<String,
×
3979
                    TriFunctionE<DBBroker, Txn, String, Sequence, XPathException>> entry : dynamicCollections.entrySet()) {
×
3980
                sb.append(entry.getKey()).append("-> ").append(entry.getValue());
×
3981
            }
3982
        }
3983
        sb.append('}');
×
3984
        sb.append('\n');
×
3985

3986
        sb.append("baseURI: ");
×
3987
        try {
3988
            sb.append(getBaseURI()).append('\n');
×
3989
        } catch (final XPathException e) {
×
3990
            sb.append("?");
×
3991
        }
3992

3993
        sb.append("inScopePrefixes: {");
×
3994
        for (final Map.Entry<String, String> entry : inScopePrefixes.entrySet()) {
×
3995
            sb.append(entry.getKey()).append("-> ").append(entry.getValue());
×
3996
        }
3997
        sb.append('}');
×
3998
        sb.append('\n');
×
3999

4000
        sb.append("inScopeNamespaces: {");
×
4001
        for (final Map.Entry<String, String> entry : inScopeNamespaces.entrySet()) {
×
4002
            sb.append(entry.getKey()).append("-> ").append(entry.getValue());
×
4003
        }
4004
        sb.append('}');
×
4005
        sb.append('\n');
×
4006

4007
        sb.append("modules: {");
×
4008
        for (final Map.Entry<String, Module[]> entry : modules.entrySet()) {
×
4009
            sb.append(entry.getKey()).append("-> ");
×
4010
            for (final Module module : modules.get(entry.getKey())) {
×
4011
                sb.append("namespaceURI: ").append(module.getNamespaceURI()).append('\n');
×
4012
                sb.append("defaultPrefix: ").append(module.getDefaultPrefix()).append('\n');
×
4013
                sb.append("description: ").append(module.getDescription()).append('\n');
×
4014
                for (final Iterator<QName> it = module.getGlobalVariables(); it.hasNext(); ) {
×
4015
                    final QName qName = it.next();
×
4016
                    sb.append(qName).append(';');
×
4017
                }
4018
            }
4019
        }
4020
        sb.append('}');
×
4021
        sb.append('\n');
×
4022

4023
        sb.append("allModules: {");
×
4024
        for (final Map.Entry<String, Module[]> entry : allModules.entrySet()) {
×
4025
            sb.append(entry.getKey()).append("-> ");
×
4026
            for (final Module module : allModules.get(entry.getKey())) {
×
4027
                sb.append("namespaceURI: ").append(module.getNamespaceURI()).append('\n');
×
4028
                sb.append("defaultPrefix: ").append(module.getDefaultPrefix()).append('\n');
×
4029
                sb.append("description: ").append(module.getDescription()).append('\n');
×
4030
                for (final Iterator<QName> it = module.getGlobalVariables(); it.hasNext(); ) {
×
4031
                    final QName qName = it.next();
×
4032
                    sb.append(qName).append(';');
×
4033
                }
4034
            }
4035
        }
4036
        sb.append('}');
×
4037
        sb.append('\n');
×
4038

4039
        sb.append('}');
×
4040

4041
        return sb.toString();
×
4042
    }
4043

4044
    @Immutable
4045
    public static class ModuleVertex {
4046
        private final String namespaceURI;
4047
        private final String location;
4048

4049
        public ModuleVertex(final String namespaceURI) {
×
4050
            this.namespaceURI = namespaceURI;
×
4051
            this.location = null;
×
4052
        }
×
4053

4054
        public ModuleVertex(final String namespaceURI, final String location) {
2 ✔
4055
            this.namespaceURI = namespaceURI;
3 ✔
4056
            this.location = location;
3 ✔
4057
        }
1 ✔
4058

4059
        @Override
4060
        public boolean equals(final Object o) {
4061
            if (this == o) {
3 ✔
4062
                return true;
2 ✔
4063
            }
4064

4065
            if (o == null || getClass() != o.getClass()) {
7 !
4066
                return false;
×
4067
            }
4068

4069
            final ModuleVertex that = (ModuleVertex) o;
3 ✔
4070
            return namespaceURI.equals(that.namespaceURI) && location.equals(that.location);
16 ✔
4071
        }
4072

4073
        @Override
4074
        public int hashCode() {
4075
            int result = namespaceURI.hashCode();
4 ✔
4076
            result = 31 * result + location.hashCode();
8 ✔
4077
            return result;
2 ✔
4078
        }
4079

4080
        @Override
4081
        public String toString() {
4082
            return "Module{" +
×
4083
                    "namespaceURI='" + namespaceURI + '\'' +
×
4084
                    "location='" + location + '\'' +
×
4085
                    '}';
4086
        }
4087
    }
4088
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc