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

eXist-db / exist / 41106813683

30 Oct 2026 06:25PM UTC coverage: 58.246% (+0.07%) from 58.179%
41106813683

push

github

web-flow
Merge pull request #6941 from joewiz/optimize/6895-direct-attribute-select

32560 of 61746 branches covered (52.73%)

Branch coverage included in aggregate %.

14 of 15 new or added lines in 2 files covered. (93.33%)

19 existing lines in 4 files now uncovered.

85668 of 141233 relevant lines covered (60.66%)

2.73 hits per line

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

78.45
/exist-core/src/main/java/org/exist/xquery/LocationStep.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 org.exist.collections.Collection;
25
import org.exist.dom.persistent.*;
26
import org.exist.indexing.StructuralIndex;
27
import org.exist.dom.memtree.InMemoryNodeSet;
28
import org.exist.dom.memtree.NodeImpl;
29
import org.exist.numbering.NodeId;
30
import org.exist.stax.*;
31
import org.exist.storage.ElementValue;
32
import org.exist.storage.IndexSpec;
33
import org.exist.storage.UpdateListener;
34
import org.exist.xmldb.XmldbURI;
35
import org.exist.xquery.value.*;
36
import org.w3c.dom.Node;
37
import org.w3c.dom.NodeList;
38

39
import javax.annotation.Nullable;
40
import javax.xml.stream.StreamFilter;
41
import javax.xml.stream.XMLStreamException;
42
import javax.xml.stream.XMLStreamReader;
43
import java.io.IOException;
44
import java.util.ArrayDeque;
45
import java.util.Deque;
46
import java.util.Iterator;
47

48
/**
49
 * Processes all location path steps (like descendant::*, ancestor::XXX).
50
 *
51
 * The results of the first evaluation of the expression are cached for the
52
 * lifetime of the object and only reloaded if the context sequence (as passed
53
 * to the {@link #eval(Sequence, Item)} method) has changed.
54
 *
55
 * @author wolf
56
 */
57
public class LocationStep extends Step {
58

59
    private static final int INDEX_SCAN_THRESHOLD = 10000;
60

61
    private NodeSet currentSet = null;
6 ✔
62
    private DocumentSet currentDocs = null;
6 ✔
63
    protected UpdateListener listener = null;
6 ✔
64
    protected Expression parent = null;
6 ✔
65

66
    /**
67
     * Whether this step is the initial (first) step of its parent expression,
68
     * as determined during {@link #analyze(AnalyzeContextInfo)}. This is cached
69
     * because the parent's child list may be rewritten after analysis (for
70
     * example, {@link Function#checkArgument} wraps a function argument in a
71
     * {@link DynamicCardinalityCheck} or type-check expression), which would
72
     * otherwise make a lazy {@code parent.getSubExpression(0) == this} test in
73
     * {@link #getDependencies()} report the wrong answer at evaluation time.
74
     */
75
    private boolean initialStep = false;
6 ✔
76

77
    //private int parentDeps = Dependency.UNKNOWN_DEPENDENCY;
78
    private boolean preloadedData = false;
6 ✔
79
    protected boolean optimized = false;
6 ✔
80
//    private boolean inUpdate = false;
81
    private boolean useDirectChildSelect = false;
6 ✔
82
    private boolean applyPredicate = true;
6 ✔
83

84
    // Cache for the current NodeTest type
85
    private Integer nodeTestType = null;
6 ✔
86

87
    /**
88
     * Creates a new instance.
89
     *
90
     * @param context the XQuery context
91
     * @param axis    the axis of the location step
92
     */
93
    public LocationStep(final XQueryContext context, final int axis) {
94
        super(context, axis);
4 ✔
95
    }
1 ✔
96

97
    /**
98
     * Creates a new instance.
99
     *
100
     * @param context the XQuery context
101
     * @param axis the axis of the location step
102
     * @param test a node test on the axis
103
     */
104
    public LocationStep(final XQueryContext context, final  int axis, final NodeTest test) {
105
        super(context, axis, test);
5 ✔
106
    }
1 ✔
107

108
    @Override
109
    public int getDependencies() {
110
        int deps = Dependency.CONTEXT_SET;
2 ✔
111

112
        // self axis has an obvious dependency on the context item
113
        // likewise we depend on the context item if this is the initial step of
114
        // the enclosing expression (outside a predicate). initialStep is captured
115
        // during analyze, before the parent's child list may be rewritten (e.g. a
116
        // function argument being wrapped in a cardinality/type check). See #6521.
117
        if (!this.inPredicate &&
3 ✔
118
                (this.axis == Constants.SELF_AXIS || this.initialStep)) {
7 ✔
119
            deps = deps | Dependency.CONTEXT_ITEM;
4 ✔
120
        }
121

122
        // TODO : normally, we should call this one...
123
        // int deps = super.getDependencies(); ???
124
        if (predicates != null) {
3 ✔
125
            for (final Predicate pred : predicates) {
17 ✔
126
                deps |= pred.getDependencies();
5 ✔
127
            }
128
        }
129

130
        // TODO : should we remove the CONTEXT_ITEM dependency returned by the
131
        // predicates ? See the comment above.
132
        // consider nested predicates however...
133

134
        return deps;
2 ✔
135
    }
136

137
    /**
138
     * If the current path expression depends on local variables from a for
139
     * expression, we can optimize by preloading entire element or attribute
140
     * sets.
141
     *
142
     * @return Whether or not we can optimize
143
     */
144
    private boolean hasPreloadedData() {
145
        // TODO : log elsewhere ?
146
        if (preloadedData) {
3 ✔
147
            context.getProfiler().message(this, Profiler.OPTIMIZATIONS, null,
7 ✔
148
                    "Preloaded NodeSets");
1 ✔
149
            return true;
2 ✔
150
        }
151
//                if (inUpdate)
152
//                        return false;
153
//                if ((parentDeps & Dependency.LOCAL_VARS) == Dependency.LOCAL_VARS) {
154
//                        context.getProfiler().message(this, Profiler.OPTIMIZATIONS, null,
155
//                                        "Preloaded NodeSets");
156
//                        return true;
157
//                }
158
        return false;
2 ✔
159
    }
160

161
    /**
162
     * The method <code>setPreloadedData</code>
163
     *
164
     * @param docs  a <code>DocumentSet</code> value
165
     * @param nodes a <code>NodeSet</code> value
166
     */
167
    public void setPreloadedData(final DocumentSet docs, final NodeSet nodes) {
168
        this.preloadedData = true;
3 ✔
169
        this.currentDocs = docs;
3 ✔
170
        this.currentSet = nodes;
3 ✔
171
        this.optimized = true;
3 ✔
172
    }
1 ✔
173

174
    /**
175
     * The method <code>applyPredicate</code>
176
     *
177
     * @param outerSequence   a <code>Sequence</code> value
178
     * @param contextSequence a <code>Sequence</code> value
179
     * @return a <code>Sequence</code> value
180
     * @throws XPathException if an error occurs
181
     */
182
    private Sequence applyPredicate(Sequence outerSequence, final Sequence contextSequence) throws XPathException {
183
        if (contextSequence == null) {
2 !
184
            return Sequence.EMPTY_SEQUENCE;
×
185
        }
186
        if (predicates == null
3 ✔
187
                || !applyPredicate
3 ✔
188
                || (!(contextSequence instanceof VirtualNodeSet) && contextSequence
4 ✔
189
                .isEmpty()))
2 ✔
190
        // Nothing to apply
191
        {
192
            return contextSequence;
2 ✔
193
        }
194
        Sequence result;
195
        final Predicate pred = predicates[0];
5 ✔
196
        // If the current step is an // abbreviated step, we have to treat the
197
        // predicate
198
        // specially to get the context position right. //a[1] translates to
199
        // /descendant-or-self::node()/a[1],
200
        // so we need to return the 1st a from any parent of a.
201
        //
202
        // If the predicate is known to return a node set, no special treatment
203
        // is required.
204
        if (abbreviatedStep
3 ✔
205
                && (pred.getExecutionMode() == Predicate.ExecutionMode.POSITIONAL || !contextSequence
5 ✔
206
                .isPersistentSet())) {
2 ✔
207
            result = new ValueSequence();
4 ✔
208
            ((ValueSequence) result).keepUnOrdered(unordered);
5 ✔
209
            if (contextSequence.isPersistentSet()) {
3 ✔
210
                final NodeSet contextSet = contextSequence.toNodeSet();
3 ✔
211
                outerSequence = contextSet.getParents(-1);
4 ✔
212
                for (final SequenceIterator i = outerSequence.iterate(); i.hasNext(); ) {
7 ✔
213
                    final NodeValue node = (NodeValue) i.nextItem();
4 ✔
214
                    final Sequence newContextSeq = contextSet.selectParentChild(
3 ✔
215
                            (NodeSet) node, NodeSet.DESCENDANT,
3 ✔
216
                            getExpressionId());
2 ✔
217
                    final Sequence temp = processPredicate(outerSequence,
4 ✔
218
                            newContextSeq);
1 ✔
219
                    result.addAll(temp);
3 ✔
220
                }
221
            } else {
1 ✔
222
                final MemoryNodeSet nodes = contextSequence.toMemNodeSet();
3 ✔
223
                outerSequence = nodes.getParents(new AnyNodeTest());
6 ✔
224
                for (final SequenceIterator i = outerSequence.iterate(); i.hasNext(); ) {
7 ✔
225
                    final NodeValue node = (NodeValue) i.nextItem();
4 ✔
226
                    final InMemoryNodeSet newSet = new InMemoryNodeSet();
4 ✔
227
                    ((NodeImpl) node).selectChildren(test, newSet);
6 ✔
228
                    final Sequence temp = processPredicate(outerSequence, newSet);
5 ✔
229
                    result.addAll(temp);
3 ✔
230
                }
231
            }
232
        } else {
1 ✔
233
            result = processPredicate(outerSequence, contextSequence);
5 ✔
234
        }
235
        return result;
2 ✔
236
    }
237

238
    private Sequence processPredicate(Sequence outerSequence, final Sequence contextSequence) throws XPathException {
239
        Sequence result = contextSequence;
2 ✔
240
        if (predicates != null) {
3 !
241
            for (int i = 0; i < predicates.length && (result instanceof VirtualNodeSet || !result.isEmpty()); i++) {
15 ✔
242
                // TODO : log and/or profile ?
243
                final Predicate pred = predicates[i];
5 ✔
244
                pred.setContextDocSet(getContextDocSet());
4 ✔
245
                result = pred.evalPredicate(outerSequence, result, axis);
7 ✔
246
                // subsequent predicates operate on the result of the previous one
247
                outerSequence = null;
2 ✔
248
                context.setContextSequencePosition(0, null);
5 ✔
249
            }
250
        }
251
        return result;
2 ✔
252
    }
253

254
    @Override
255
    public void analyze(final AnalyzeContextInfo contextInfo) throws XPathException {
256
        this.parent = contextInfo.getParent();
4 ✔
257

258
        unordered = (contextInfo.getFlags() & UNORDERED) > 0;
8 !
259

260
//        parentDeps = parent.getDependencies();
261
//        if ((contextInfo.getFlags() & IN_UPDATE) > 0) {
262
//            inUpdate = true;
263
//        }
264
//                if ((contextInfo.getFlags() & SINGLE_STEP_EXECUTION) > 0) {
265
//                        preloadedData = true;
266
//                }
267
        if ((contextInfo.getFlags() & USE_TREE_TRAVERSAL) > 0) {
5 ✔
268
            useDirectChildSelect = true;
3 ✔
269
        }
270
        // Mark ".", which is expanded as self::node() by the parser
271
        // even though it may *also* be relevant with atomic sequences
272
        if (this.axis == Constants.SELF_AXIS
4 ✔
273
                && this.test.getType() == Type.NODE) {
5 ✔
274
            contextInfo.addFlag(DOT_TEST);
3 ✔
275
        }
276

277
        //Change axis from descendant-or-self to descendant for '//'
278
        if (this.axis == Constants.DESCENDANT_SELF_AXIS && isAbbreviated()) {
7 ✔
279
            this.axis = Constants.DESCENDANT_AXIS;
3 ✔
280
        }
281

282
        final Expression contextStep;
283
        final NodeTest stepTest;
284
        final NodeTest contextStepTest;
285

286
        // static analysis for empty-sequence
287
        switch (axis) {
3 ✔
288
            case Constants.SELF_AXIS:
289
                if (getTest().getType() == Type.NODE) {
5 ✔
290
                    break;
1 ✔
291
                }
292

293
                // WM: the following checks will only work on simple filters like //a[self::b], so we
294
                // have to make sure they are not applied to more complex expression types
295
                if (parent.getSubExpressionCount() > 1) {
5 ✔
296
                    break;
1 ✔
297
                }
298

299
                contextStep = contextInfo.getContextStep();
3 ✔
300
                if (!(contextStep instanceof LocationStep cStep)) {
6 ✔
301
                    break;
302
                }
303

304
                stepTest = getTest();
3 ✔
305
                contextStepTest = cStep.getTest();
3 ✔
306

307
                if (!Type.subTypeOf(stepTest.getType(), contextStepTest.getType())) {
6 !
308
                    // return empty sequence
309
                    contextInfo.setStaticType(Type.EMPTY_SEQUENCE);
×
310
                    staticReturnType = Type.EMPTY_SEQUENCE;
×
311
                    break;
×
312
                }
313

314
                if (!stepTest.isWildcardTest() &&
3 ✔
315
                        !contextStepTest.isWildcardTest() &&
3 ✔
316
                        !contextStepTest.equals(stepTest)) {
4 !
317
                    // return empty sequence
318
                    contextInfo.setStaticType(Type.EMPTY_SEQUENCE);
×
319
                    staticReturnType = Type.EMPTY_SEQUENCE;
×
320
                }
321
                break;
×
322
//                      case Constants.DESCENDANT_AXIS:
323
            case Constants.DESCENDANT_SELF_AXIS:
324
                contextStep = contextInfo.getContextStep();
3 ✔
325
                if (!(contextStep instanceof LocationStep cStep)) {
6 ✔
326
                    break;
327
                }
328

329
                stepTest = getTest();
3 ✔
330
                contextStepTest = cStep.getTest();
3 ✔
331

332
                if ((contextStepTest.getType() == Type.ATTRIBUTE || contextStepTest.getType() == Type.TEXT) &&
8 !
333
                        contextStepTest != stepTest) {
×
334
                    // return empty sequence
335
                    contextInfo.setStaticType(Type.EMPTY_SEQUENCE);
×
336
                    staticReturnType = Type.EMPTY_SEQUENCE;
×
337
                }
338
                break;
339
//                case Constants.PARENT_AXIS:
340
//                case Constants.ATTRIBUTE_AXIS:
341
            default:
342
        }
343

344
        // TODO : log somewhere ?
345
        super.analyze(contextInfo);
3 ✔
346

347
        // Capture whether this step is the initial step of its parent expression
348
        // now, while the parent's child list still reflects the source structure.
349
        // It may be rewritten afterwards (e.g. Function.checkArgument wrapping the
350
        // argument), so deferring this test to getDependencies() is unreliable.
351
        this.initialStep = parent != null && parent.getSubExpressionCount() > 0
9 !
352
                && parent.getSubExpression(0) == this;
9 ✔
353
    }
1 ✔
354

355
    @Override
356
    public Sequence eval(Sequence contextSequence, final Item contextItem)
357
            throws XPathException {
358
        if (context.getProfiler().isEnabled()) {
5 !
359
            context.getProfiler().start(this);
×
360
            context.getProfiler().message(this, Profiler.DEPENDENCIES,
×
361
                    "DEPENDENCIES",
×
362
                    Dependency.getDependenciesName(this.getDependencies()));
×
363
            if (contextSequence != null) {
×
364
                context.getProfiler().message(this, Profiler.START_SEQUENCES,
×
365
                        "CONTEXT SEQUENCE", contextSequence);
×
366
            }
367
            if (contextItem != null) {
×
368
                context.getProfiler().message(this, Profiler.START_SEQUENCES,
×
369
                        "CONTEXT ITEM", contextItem.toSequence());
×
370
            }
371
        }
372

373
        if (contextItem != null) {
2 ✔
374
            contextSequence = contextItem.toSequence();
3 ✔
375
        }
376

377
        Sequence result;
378
        if (needsComputation()) {
3 ✔
379
            if (contextSequence == null) {
2 ✔
380
                throw new XPathException(this,
5 ✔
381
                        ErrorCodes.XPDY0002, "Undefined context sequence for '"
3 ✔
382
                        + this + "'");
1 ✔
383
            }
384

385
            try {
386
                switch (axis) {
3 !
387

388
                    case Constants.DESCENDANT_AXIS:
389
                    case Constants.DESCENDANT_SELF_AXIS:
390
                        result = getDescendants(context, contextSequence);
6 ✔
391
                        break;
1 ✔
392

393
                    case Constants.CHILD_AXIS:
394
                        // VirtualNodeSets may have modified the axis ; checking the
395
                        // type
396
                        // TODO : further checks ?
397
//                                if (this.test.getType() == Type.ATTRIBUTE) {
398
//                                        this.axis = Constants.ATTRIBUTE_AXIS;
399
//                                        result = getAttributes(context, contextSequence);
400
//                                } else {
401
                        result = getChildren(context, contextSequence);
6 ✔
402
//                                }
403
                        break;
1 ✔
404

405
                    case Constants.ANCESTOR_SELF_AXIS:
406
                    case Constants.ANCESTOR_AXIS:
407
                        result = getAncestors(context, contextSequence);
6 ✔
408
                        break;
1 ✔
409

410
                    case Constants.PARENT_AXIS:
411
                        result = getParents(context, contextSequence);
6 ✔
412
                        break;
1 ✔
413

414
                    case Constants.SELF_AXIS:
415
                        if (!(contextSequence instanceof VirtualNodeSet)
3 ✔
416
                                && Type.subTypeOf(contextSequence.getItemType(),
3 ✔
417
                                Type.ANY_ATOMIC_TYPE)) {
2 ✔
418
                            // This test is copied from the legacy method
419
                            // getSelfAtomic()
420
                            if (!test.isWildcardTest()) {
4 !
421
                                throw new XPathException(this, test.toString()
×
422
                                        + " cannot be applied to an atomic value.");
423
                            }
424
                            result = contextSequence;
2 ✔
425
                        } else {
1 ✔
426
                            result = getSelf(context, contextSequence);
6 ✔
427
                        }
428
                        break;
1 ✔
429

430
                    case Constants.ATTRIBUTE_AXIS:
431
                    case Constants.DESCENDANT_ATTRIBUTE_AXIS:
432
                        result = getAttributes(context, contextSequence);
6 ✔
433
                        break;
1 ✔
434

435
                    case Constants.PRECEDING_AXIS:
436
                    case Constants.FOLLOWING_AXIS:
437
                        result = getPrecedingOrFollowing(context, contextSequence);
6 ✔
438
                        break;
1 ✔
439

440
                    case Constants.PRECEDING_SIBLING_AXIS:
441
                    case Constants.FOLLOWING_SIBLING_AXIS:
442
                        result = getSiblings(context, contextSequence);
6 ✔
443
                        break;
1 ✔
444

445
                    default:
446
                        throw new IllegalArgumentException("Unsupported axis specified");
×
447
                }
448
            } catch (final XPathException e) {
1 ✔
449
                if (e.getLine() <= 0) {
3 !
450
                    e.setLocation(getLine(), getColumn(), getSource());
8 ✔
451
                }
452
                throw e;
2 ✔
453
            }
454
        } else {
455
            result = NodeSet.EMPTY_SET;
2 ✔
456
        }
457
        if (axis != Constants.SELF_AXIS && contextSequence != null
6 !
458
                && contextSequence.isCacheable()) {
3 ✔
459
            registerUpdateListener();
2 ✔
460
        }
461
        // Remove duplicate nodes
462
        result.removeDuplicates();
2 ✔
463
        // Apply the predicate
464
        result = applyPredicate(contextSequence, result);
5 ✔
465

466
        if (context.getProfiler().isEnabled()) {
5 !
467
            context.getProfiler().end(this, "", result);
×
468
        }
469
        // actualReturnType = result.getItemType();
470

471
        return result;
2 ✔
472
    }
473

474
    // Avoid unnecessary tests (these should be detected by the parser)
475
    private boolean needsComputation() {
476
        // TODO : log this ?
477
        switch (axis) {
3 ✔
478
            // Certainly not exhaustive
479
            case Constants.ANCESTOR_SELF_AXIS:
480
            case Constants.PARENT_AXIS:
481
                // case Constants.SELF_AXIS:
482
                if (nodeTestType == null) {
3 ✔
483
                    nodeTestType = test.getType();
6 ✔
484
                }
485
                if (nodeTestType != Type.DOCUMENT
5 ✔
486
                        && nodeTestType != Type.NODE
5 ✔
487
                        && nodeTestType != Type.ELEMENT
5 ✔
488
                        && nodeTestType != Type.PROCESSING_INSTRUCTION) {
5 !
489
                    if (context.getProfiler().isEnabled()) {
5 !
490
                        context.getProfiler().message(this, Profiler.OPTIMIZATIONS,
×
491
                                "OPTIMIZATION", "avoid useless computations");
×
492
                    }
493
                    return false;
2 ✔
494
                }
495

496
        }
497
        return true;
2 ✔
498
    }
499

500
    private Sequence getSelf(final XQueryContext context, final Sequence contextSequence) throws XPathException {
501
        if (!contextSequence.isPersistentSet()) {
3 ✔
502
            final MemoryNodeSet nodes = contextSequence.toMemNodeSet();
3 ✔
503
            return nodes.getSelf(test);
5 ✔
504
        }
505

506
        if (hasPreloadedData()) {
3 ✔
507
            @Nullable final NodeSet ns;
508
            if (contextSequence instanceof NodeSet set) {
6 !
509
                ns = set;
2 ✔
510
            } else {
1 ✔
511
                ns = null;
×
512
            }
513

514
            final NewArrayNodeSet newCurrentSet = new NewArrayNodeSet(currentSet.getItemCount());
7 ✔
515
            for (NodeProxy p : currentSet) {
12 ✔
516
                if (test.isWildcardTest()) {
4 ✔
517
                    if (ns != null) {
2 !
518
                        @Nullable final NodeProxy np = ns.get(p);
4 ✔
519
                        if (np != null) {
2 !
520
                            p = np;
2 ✔
521
                        }
522
                    }
523
                } else {
1 ✔
524
                    if (ns != null) {
2 !
525
                        @Nullable final NodeProxy np = ns.get(p);
4 ✔
526
                        if (np != null && np.getMatches() != null) {
5 !
527
                            p.addMatch(np.getMatches());
×
528
                        }
529
                    }
530
                }
531
                p.addContextNode(contextId, p);
5 ✔
532
                newCurrentSet.add(p);
3 ✔
533
            }
534
            currentSet = newCurrentSet;
3 ✔
535
            return newCurrentSet;
2 ✔
536
        }
537

538
        final NodeSet contextSet = contextSequence.toNodeSet();
3 ✔
539

540
        // A processing instruction is addressable by name, but the structural index has only an
541
        // element half and an attribute half -- there is nowhere to look one up. Without this
542
        // branch a named test fell through to the index and searched for an *element* of that
543
        // name, so self::processing-instruction(NAME) never matched. Matching directly against
544
        // the context set needs no index; the VirtualNodeSet the other axes use is not usable on
545
        // the self axis, which is why the branch that had been commented out here stayed unused.
546
        if (test.getType() == Type.PROCESSING_INSTRUCTION) {
5 ✔
547
            return selfProcessingInstructions(contextSet);
4 ✔
548
        }
549

550
        if (test.isWildcardTest()) {
4 ✔
551
            if (nodeTestType == null) {
3 ✔
552
                nodeTestType = test.getType();
6 ✔
553
            }
554

555
            if (Type.subTypeOf(nodeTestType, Type.NODE)) {
6 !
556
                if (Expression.NO_CONTEXT_ID != contextId) {
4 ✔
557
                    if (contextSet instanceof VirtualNodeSet set && test.getType() == Type.NODE) {
11 ✔
558
                        // self::node() selects every node of the set, which can stay virtual
559
                        set.setInPredicate(true);
3 ✔
560
                        set.setContextId(contextId);
4 ✔
561
                        set.setSelfIsContext();
2 ✔
562
                        return contextSet;
2 ✔
563
                    }
564
                    // only the nodes that pass the test: returning the whole context set made
565
                    // e.g. node()[self::comment()] select every node, and node()[not(self::*)] none
566
                    final NewArrayNodeSet results = new NewArrayNodeSet();
4 ✔
567
                    for (final NodeProxy p : contextSet) {
11 ✔
568
                        if (test.matches(p)) {
5 ✔
569
                            p.addContextNode(contextId, p);
5 ✔
570
                            results.add(p);
3 ✔
571
                        }
572
                    }
573
                    return results;
2 ✔
574
                } else {
575
                    final NewArrayNodeSet results = new NewArrayNodeSet();
4 ✔
576
                    for (final NodeProxy p : contextSet) {
11 ✔
577
                        if(test.matches(p)) {
5 ✔
578
                            results.add(p);
3 ✔
579
                        }
580
                    }
581
                    return results;
2 ✔
582
                }
583
            } else {
584
                final VirtualNodeSet vset = new VirtualNodeSet(context.getBroker(), axis, test, contextId, contextSet);
×
585
                vset.setInPredicate(Expression.NO_CONTEXT_ID != contextId);
×
586
                return vset;
×
587
            }
588
        } else {
589
            final DocumentSet docs = getDocumentSet(contextSet);
4 ✔
590
            final StructuralIndex index = context.getBroker().getStructuralIndex();
4 ✔
591
            if (context.getProfiler().isEnabled()) {
4 !
592
                context.getProfiler().message(this, Profiler.OPTIMIZATIONS,
×
593
                        "OPTIMIZATION",
×
594
                        "Using structural index '" + index.toString() + "'");
×
595
            }
596
            final NodeSelector selector = new SelfSelector(contextSet, contextId);
7 ✔
597
            return index.findElementsByTagName(indexTypeFor(test), docs, test.getName(), selector, this);
12 ✔
598
        }
599
    }
600

601
    /**
602
     * Selects the half of the structural index that a node test should be looked up in.
603
     *
604
     * <p>The index is split into an element half and an attribute half. A named attribute kind
605
     * test such as {@code self::attribute(id)} must be looked up in the latter; searching the
606
     * element half for an element of that name always comes back empty.</p>
607
     *
608
     * <p>Processing instructions are handled before this point, so only element and attribute
609
     * tests reach here.</p>
610
     *
611
     * @param test the node test being applied
612
     * @return {@link ElementValue#ATTRIBUTE} for an attribute test, {@link ElementValue#ELEMENT} otherwise
613
     */
614
    private static byte indexTypeFor(final NodeTest test) {
615
        return test.getType() == Type.ATTRIBUTE ? ElementValue.ATTRIBUTE : ElementValue.ELEMENT;
8 ✔
616
    }
617

618
    /**
619
     * Selects the processing instructions in {@code contextSet} that satisfy this step's test.
620
     *
621
     * <p>A processing instruction is addressable by name, but the structural index has only an
622
     * element half and an attribute half, so there is nowhere to look one up. Matching directly
623
     * against the context set needs no index. (The {@code VirtualNodeSet} the other axes use for
624
     * processing instructions is not usable on the self axis — it leaves the result without the
625
     * context it needs — which is why the branch that once stood here was commented out.)</p>
626
     *
627
     * @param contextSet the nodes on the self axis
628
     * @return those matching this step's test, carrying the current context where one applies
629
     */
630
    private Sequence selfProcessingInstructions(final NodeSet contextSet) {
631
        final NewArrayNodeSet results = new NewArrayNodeSet();
4 ✔
632
        for (final NodeProxy p : contextSet) {
11 ✔
633
            if (test.matches(p)) {
5 ✔
634
                if (Expression.NO_CONTEXT_ID != contextId) {
4 ✔
635
                    p.addContextNode(contextId, p);
5 ✔
636
                }
637
                results.add(p);
3 ✔
638
            }
639
        }
640
        return results;
2 ✔
641
    }
642

643
    protected Sequence getAttributes(final XQueryContext context, final Sequence contextSequence)
644
            throws XPathException {
645
        if (!contextSequence.isPersistentSet()) {
3 ✔
646
            final MemoryNodeSet nodes = contextSequence.toMemNodeSet();
3 ✔
647
            if (axis == Constants.DESCENDANT_ATTRIBUTE_AXIS) {
4 ✔
648
                return nodes.getDescendantAttributes(test);
5 ✔
649
            } else {
650
                return nodes.getAttributes(test);
5 ✔
651
            }
652
        }
653
        final NodeSet contextSet = contextSequence.toNodeSet();
3 ✔
654
        if (!hasPreloadedData() && test.isWildcardTest()) {
7 ✔
655
            final NodeSet result = new VirtualNodeSet(context.getBroker(), axis, test, contextId, contextSet);
13 ✔
656
            ((VirtualNodeSet) result).setInPredicate(Expression.NO_CONTEXT_ID != contextId);
10 ✔
657
            return result;
2 ✔
658
            // if there's just a single known node in the context, it is faster
659
            // do directly search for the attribute in the parent node.
660
        }
661
        if (hasPreloadedData()) {
3 ✔
662
            final DocumentSet docs = getDocumentSet(contextSet);
4 ✔
663
            synchronized (context) {
4 ✔
664
                if (currentSet == null
3 !
665
                        || currentDocs == null
3 !
666
                        || (!optimized && !(docs == currentDocs || docs
3 !
667
                        .equalDocs(currentDocs)))) {
×
668
                    final StructuralIndex index = context.getBroker().getStructuralIndex();
×
669
                    if (context.getProfiler().isEnabled()) {
×
670
                        context.getProfiler().message(
×
671
                                this,
×
672
                                Profiler.OPTIMIZATIONS,
×
673
                                "OPTIMIZATION",
×
674
                                "Using structural index '" + index.toString()
×
675
                                        + "'");
676
                    }
677
                    // TODO : why a null selector here ? We have one below !
678
                    currentSet = index.findElementsByTagName(ElementValue.ATTRIBUTE, docs, test.getName(), null, this);
×
679
                    currentDocs = docs;
×
680
                    registerUpdateListener();
×
681
                }
682
                return switch (axis) {
6 !
683
                    case Constants.ATTRIBUTE_AXIS ->
684
                            currentSet.selectParentChild(contextSet, NodeSet.DESCENDANT, contextId);
8 ✔
685
                    case Constants.DESCENDANT_ATTRIBUTE_AXIS ->
686
                            currentSet.selectAncestorDescendant(contextSet, NodeSet.DESCENDANT, false, contextId,
×
687
                                    true);
×
688
                    default -> throw new IllegalArgumentException("Unsupported axis specified");
×
689
                };
690
            }
691
        } else if (canSelectAttributesDirectly(context, contextSet)) {
5 ✔
692
            return contextSet.directSelectAttribute(context.getBroker(), test, contextId);
9 ✔
693
        } else {
694
            final DocumentSet docs = getDocumentSet(contextSet);
4 ✔
695
            final StructuralIndex index = context.getBroker().getStructuralIndex();
4 ✔
696
            if (context.getProfiler().isEnabled()) {
4 !
697
                context.getProfiler().message(this, Profiler.OPTIMIZATIONS,
×
698
                        "OPTIMIZATION",
×
699
                        "Using structural index '" + index.toString() + "'");
×
700
            }
701
            if (!contextSet.getProcessInReverseOrder()) {
3 !
702
                return index.findDescendantsByTagName(ElementValue.ATTRIBUTE, test.getName(), axis, docs, contextSet,
11 ✔
703
                        contextId, this);
3 ✔
704
            } else {
705
                final NodeSelector selector = switch (axis) {
×
706
                    case Constants.ATTRIBUTE_AXIS -> new ChildSelector(contextSet, contextId);
×
707
                    case Constants.DESCENDANT_ATTRIBUTE_AXIS -> new DescendantSelector(contextSet, contextId);
×
708
                    default -> throw new IllegalArgumentException("Unsupported axis specified");
×
709
                };
710
                return index.findElementsByTagName(ElementValue.ATTRIBUTE, docs, test.getName(), selector, this);
×
711
            }
712
        }
713
    }
714

715
    /**
716
     * Whether to read each context element's own attributes instead of asking the structural index, for a
717
     * named attribute step without preloaded data. The structural index runs one range query per context
718
     * node over its whole subtree, so a lookup on a node high in the tree, such as a parent or ancestor
719
     * step's result, costs time in proportion to the document's size. For a large context set the index
720
     * is cheaper per node, so the direct read is limited to sets smaller than {@link #INDEX_SCAN_THRESHOLD}.
721
     */
722
    private boolean canSelectAttributesDirectly(final XQueryContext context, final NodeSet contextSet) {
723
        return axis == Constants.ATTRIBUTE_AXIS
8 ✔
724
                && !(contextSet instanceof VirtualNodeSet)
3 ✔
725
                && contextSet.getLength() < INDEX_SCAN_THRESHOLD
4 ✔
726
                && !hasLegacyRangeIndex(context, contextSet);
4 ✔
727
    }
728

729
    /**
730
     * Whether a collection of the context nodes configures a legacy range index (by path or by QName).
731
     * The structural index records such an index's type with each attribute it returns, and comparisons
732
     * use that type, so these attributes can't be read directly from their elements.
733
     */
734
    private static boolean hasLegacyRangeIndex(final XQueryContext context, final NodeSet contextSet) {
735
        for (final Iterator<Collection> i = contextSet.getCollectionIterator(); i.hasNext(); ) {
7 ✔
736
            final Collection collection = i.next();
4 ✔
737
            if (collection.getURI().equalsInternal(XmldbURI.SYSTEM_COLLECTION_URI)) {
5 !
NEW
738
                continue;
×
739
            }
740
            final IndexSpec indexSpec = collection.getIndexConfiguration(context.getBroker());
5 ✔
741
            if (indexSpec != null && (indexSpec.hasIndexesByPath() || indexSpec.hasIndexesByQName())) {
8 !
742
                return true;
2 ✔
743
            }
744
        }
745
        return false;
2 ✔
746
    }
747

748
    private Sequence getChildren(final XQueryContext context, final Sequence contextSequence) throws XPathException {
749
        if (!contextSequence.isPersistentSet()) {
3 ✔
750
            final MemoryNodeSet nodes = contextSequence.toMemNodeSet();
3 ✔
751
            return nodes.getChildren(test);
5 ✔
752
        }
753

754
        final NodeSet contextSet = contextSequence.toNodeSet();
3 ✔
755
        // TODO : understand this. I guess comments should be treated in a
756
        // similar way ? -pb
757
        if ((!hasPreloadedData() && test.isWildcardTest()) || test.getType() == Type.PROCESSING_INSTRUCTION) {
12 ✔
758
            // test is one out of *, text(), node() including
759
            // processing-instruction(targetname)
760
            final VirtualNodeSet vset = new VirtualNodeSet(context.getBroker(), axis, test, contextId, contextSet);
13 ✔
761
            vset.setInPredicate(Expression.NO_CONTEXT_ID != contextId);
9 ✔
762
            return vset;
2 ✔
763
        }
764

765
        // IndexStatistics stats = (IndexStatistics)
766
        // context.getBroker().getBrokerPool().
767
        // getIndexManager().getIndexById(IndexStatistics.ID);
768
        // int parentDepth = stats.getMaxParentDepth(test.getName());
769
        // LOG.debug("parentDepth for " + test.getName() + ": " + parentDepth);
770

771
        if (useDirectChildSelect) {
3 ✔
772
            final NewArrayNodeSet result = new NewArrayNodeSet();
4 ✔
773
            for (final NodeProxy p : contextSet) {
11 ✔
774
                result.addAll(p.directSelectChild(test.getName(), contextId));
9 ✔
775
            }
776
            return result;
2 ✔
777
        } else if (hasPreloadedData()) {
3 ✔
778
            final DocumentSet docs = getDocumentSet(contextSet);
4 ✔
779
            synchronized (context) {
4 ✔
780
                // TODO : understand why this one is different from the other
781
                // ones
782
                if (currentSet == null
3 !
783
                        || currentDocs == null
3 !
784
                        || (!optimized && !(docs == currentDocs || docs
3 !
785
                        .equalDocs(currentDocs)))) {
×
786
                    final StructuralIndex index = context.getBroker().getStructuralIndex();
×
787
                    if (context.getProfiler().isEnabled()) {
×
788
                        context.getProfiler().message(
×
789
                                this,
×
790
                                Profiler.OPTIMIZATIONS,
×
791
                                "OPTIMIZATION",
×
792
                                "Using structural index '" + index.toString()
×
793
                                        + "'");
794
                    }
795
                    currentSet = index.findElementsByTagName(ElementValue.ELEMENT, docs, test.getName(), null, this);
×
796
                    currentDocs = docs;
×
797
                    registerUpdateListener();
×
798
                }
799
                return currentSet.selectParentChild(contextSet, NodeSet.DESCENDANT, contextId);
10 ✔
800
            }
801
        } else {
802
            final DocumentSet docs = getDocumentSet(contextSet);
4 ✔
803
            final StructuralIndex index = context.getBroker().getStructuralIndex();
4 ✔
804
            if (context.getProfiler().isEnabled()) {
4 !
805
                context.getProfiler().message(this, Profiler.OPTIMIZATIONS,
×
806
                        "OPTIMIZATION",
×
807
                        "Using structural index '" + index.toString() + "'");
×
808
            }
809
            if (!contextSet.getProcessInReverseOrder() && !(contextSet instanceof VirtualNodeSet) &&
6 !
810
                    contextSet.getLength() < INDEX_SCAN_THRESHOLD) {
4 !
811
                return index.findDescendantsByTagName(ElementValue.ELEMENT,
4 ✔
812
                        test.getName(), axis, docs, contextSet,
7 ✔
813
                        contextId, parent);
4 ✔
814
            } else {
815
                // if (contextSet instanceof VirtualNodeSet)
816
                // ((VirtualNodeSet)contextSet).realize();
817
                final NodeSelector selector = new ChildSelector(contextSet, contextId);
7 ✔
818
                return index.findElementsByTagName(ElementValue.ELEMENT, docs, test.getName(), selector, this);
10 ✔
819
            }
820
        }
821
    }
822

823
    private Sequence getDescendants(final XQueryContext context, final Sequence contextSequence) throws XPathException {
824
        if (!contextSequence.isPersistentSet()) {
3 ✔
825
            final MemoryNodeSet nodes = contextSequence.toMemNodeSet();
3 ✔
826
            return nodes.getDescendants(axis == Constants.DESCENDANT_SELF_AXIS,
10 ✔
827
                    test);
2 ✔
828
        }
829

830
        final NodeSet contextSet = contextSequence.toNodeSet();
3 ✔
831
        // TODO : understand this. I guess comments should be treated in a
832
        // similar way ? -pb
833
        if ((!hasPreloadedData() && test.isWildcardTest())
7 ✔
834
                || test.getType() == Type.PROCESSING_INSTRUCTION) {
5 !
835
            // test is one out of *, text(), node() including
836
            // processing-instruction(targetname)
837
            final VirtualNodeSet vset = new VirtualNodeSet(context.getBroker(), axis, test, contextId, contextSet);
13 ✔
838
            vset.setInPredicate(Expression.NO_CONTEXT_ID != contextId);
7 !
839
            return vset;
2 ✔
840
        } else if (hasPreloadedData()) {
3 ✔
841
            final DocumentSet docs = getDocumentSet(contextSet);
4 ✔
842
            synchronized (context) {
4 ✔
843
                // TODO : understand why this one is different from the other
844
                // ones
845
                if (currentSet == null
3 !
846
                        || currentDocs == null
3 !
847
                        || (!optimized && !(docs == currentDocs || docs
3 !
848
                        .equalDocs(currentDocs)))) {
×
849
                    final StructuralIndex index = context.getBroker().getStructuralIndex();
×
850
                    if (context.getProfiler().isEnabled()) {
×
851
                        context.getProfiler().message(
×
852
                                this,
×
853
                                Profiler.OPTIMIZATIONS,
×
854
                                "OPTIMIZATION",
×
855
                                "Using structural index '" + index.toString()
×
856
                                        + "'");
857
                    }
858
                    currentSet = index.findElementsByTagName(ElementValue.ELEMENT, docs, test.getName(), null, this);
×
859
                    currentDocs = docs;
×
860
                    registerUpdateListener();
×
861
                }
862

863
                return switch (axis) {
6 !
864
                    case Constants.DESCENDANT_SELF_AXIS ->
865
                            currentSet.selectAncestorDescendant(contextSet, NodeSet.DESCENDANT, true, contextId,
9 ✔
866
                                    true);
1 ✔
867
                    case Constants.DESCENDANT_AXIS ->
868
                            currentSet.selectAncestorDescendant(contextSet, NodeSet.DESCENDANT, false, contextId,
9 ✔
869
                                    true);
1 ✔
870
                    default -> throw new IllegalArgumentException("Unsupported axis specified");
×
871
                };
872
            }
873
        } else {
874
            final DocumentSet docs = contextSet.getDocumentSet();
3 ✔
875
            final StructuralIndex index = context.getBroker().getStructuralIndex();
4 ✔
876
            if (context.getProfiler().isEnabled()) {
4 !
877
                context.getProfiler().message(this, Profiler.OPTIMIZATIONS,
×
878
                        "OPTIMIZATION",
×
879
                        "Using structural index '" + index.toString() + "'");
×
880
            }
881
            if (!contextSet.getProcessInReverseOrder()
3 !
882
                    && (contextSet instanceof VirtualNodeSet || contextSet.getLength() < INDEX_SCAN_THRESHOLD)) {
7 !
883
                return index.findDescendantsByTagName(ElementValue.ELEMENT, test.getName(), axis, docs, contextSet,
11 ✔
884
                        contextId, this);
3 ✔
885
            } else {
886
                final NodeSelector selector = switch (axis) {
×
887
                    case Constants.DESCENDANT_SELF_AXIS -> new DescendantOrSelfSelector(contextSet, contextId);
×
888
                    case Constants.DESCENDANT_AXIS -> new DescendantSelector(contextSet, contextId);
×
889
                    default -> throw new IllegalArgumentException("Unsupported axis specified");
×
890
                };
891
                return index.findElementsByTagName(ElementValue.ELEMENT, docs, test.getName(), selector, this);
×
892
            }
893

894
        }
895
    }
896

897
    /**
898
     * Get's the sibling nodes of the context set
899
     *
900
     * @param context         a <code>XQueryContext</code> value
901
     * @param contextSequence a <code>NodeSet</code> value
902
     * @return a <code>NodeSet</code> value
903
     * @throws XPathException in case of dynamic error
904
     */
905
    protected Sequence getSiblings(final XQueryContext context, final Sequence contextSequence) throws XPathException {
906
        if (!contextSequence.isPersistentSet()) {
3 ✔
907
            final MemoryNodeSet nodes = contextSequence.toMemNodeSet();
3 ✔
908
            if (axis == Constants.PRECEDING_SIBLING_AXIS) {
4 ✔
909
                return nodes.getPrecedingSiblings(test);
5 ✔
910
            } else {
911
                return nodes.getFollowingSiblings(test);
5 ✔
912
            }
913
        }
914

915
        final NodeSet contextSet = contextSequence.toNodeSet();
3 ✔
916

917
        // A processing instruction is not in the structural index, so it is matched while streaming
918
        // the siblings, like a wildcard test. The VirtualNodeSet it was handed to instead found no
919
        // sibling processing instructions at all.
920
        if (test.isWildcardTest() || test.getType() == Type.PROCESSING_INSTRUCTION) {
9 ✔
921
            final AVLTreeNodeSet result = new AVLTreeNodeSet();
4 ✔
922
            try {
923
                final int limit = computeLimit();
3 ✔
924
                for (final NodeProxy current : contextSet) {
11 ✔
925
                    // document-node() does not have any preceding or following elements
926
                    if (NodeId.DOCUMENT_NODE.equals(current.getNodeId())) {
5 !
927
                        continue;
×
928
                    }
929

930
                    final IEmbeddedXMLStreamReader reader;
931
                    final StreamFilter filter;
932
                    if (axis == Constants.PRECEDING_SIBLING_AXIS) {
4 ✔
933
                        final NodeId startNodeId;
934
                        if (NodeId.DOCUMENT_NODE.equals(current.getNodeId().getParentId())) {
6 ✔
935
                            // parent would be document-node(), start from document-node()/node()[1]
936
                            startNodeId = NodeId.ROOT_NODE;
2 ✔
937
                        } else {
1 ✔
938
                            startNodeId = current.getNodeId().getParentId().getChild(1);
6 ✔
939
                        }
940
                        final NodeProxy startNode = new NodeProxy(this, current.getOwnerDocument(), startNodeId);
8 ✔
941

942
                        reader = context.getBroker().getXMLStreamReader(startNode, false);
6 ✔
943
                        filter = new PrecedingSiblingFilter(test, startNode, current, result, contextId);
12 ✔
944
                    } else {
1 ✔
945
                        reader = context.getBroker().getXMLStreamReader(current, false);
6 ✔
946
                        filter = new FollowingSiblingFilter(test, current, result, contextId, limit);
12 ✔
947
                    }
948

949
                    reader.filter(filter);
3 ✔
950
                }
951
            } catch (final IOException | XMLStreamException e) {
1 ✔
952
                throw new XPathException(this, e);
×
953
            }
954

955
            return result;
2 ✔
956
        } else {
957
            // TODO : no test on preloaded data ?
958
            final DocumentSet docs = getDocumentSet(contextSet);
4 ✔
959
            synchronized (context) {
4 ✔
960
                if (currentSet == null || currentDocs == null || !(docs.equalDocs(currentDocs))) {
11 !
961
                    final StructuralIndex index = context.getBroker().getStructuralIndex();
4 ✔
962
                    if (context.getProfiler().isEnabled()) {
4 !
963
                        context.getProfiler().message(
×
964
                                this,
×
965
                                Profiler.OPTIMIZATIONS,
×
966
                                "OPTIMIZATION",
×
967
                                "Using structural index '" + index.toString()
×
968
                                        + "'");
969
                    }
970
                    currentSet = index.findElementsByTagName(ElementValue.ELEMENT, docs, test.getName(), null, this);
11 ✔
971
                    currentDocs = docs;
3 ✔
972
                    registerUpdateListener();
2 ✔
973
                }
974
                return switch (axis) {
6 !
975
                    case Constants.PRECEDING_SIBLING_AXIS -> currentSet.selectPrecedingSiblings(contextSet, contextId);
7 ✔
976
                    case Constants.FOLLOWING_SIBLING_AXIS -> currentSet.selectFollowingSiblings(contextSet, contextId);
7 ✔
977
                    default -> throw new IllegalArgumentException("Unsupported axis specified");
×
978
                };
979
            }
980
        }
981
    }
982

983
    /**
984
     * Get the preceding or following axis nodes
985
     *
986
     * @param context the xquery context
987
     * @param contextSequence the context sequence
988
     *
989
     * @return the nodes from the preceding or following axis
990
     *
991
     * @throws XPathException if an error occurs
992
     */
993
    private Sequence getPrecedingOrFollowing(final XQueryContext context, final Sequence contextSequence)
994
            throws XPathException {
995
        final int position = computeLimit();
3 ✔
996

997
        // process an in-memory node set
998
        if (!contextSequence.isPersistentSet()) {
3 ✔
999
            final MemoryNodeSet nodes = contextSequence.toMemNodeSet();
3 ✔
1000
            if (position > -1) {
3 ✔
1001
                applyPredicate = false;
3 ✔
1002
            }
1003

1004
            if (axis == Constants.PRECEDING_AXIS) {
4 ✔
1005
                return nodes.getPreceding(test, position);
6 ✔
1006
            } else {
1007
                return nodes.getFollowing(test, position);
6 ✔
1008
            }
1009
        }
1010

1011
        final NodeSet contextSet = contextSequence.toNodeSet();
3 ✔
1012
        // TODO : understand this. I guess comments should be treated in a
1013
        // similar way ? -pb
1014
        if (test.getType() == Type.PROCESSING_INSTRUCTION) {
5 !
1015
            final VirtualNodeSet vset = new VirtualNodeSet(context.getBroker(), axis, test, contextId, contextSet);
×
1016
            vset.setInPredicate(Expression.NO_CONTEXT_ID != contextId);
×
1017
            return vset;
×
1018
        }
1019

1020
        // handle node(), * etc.
1021
        if (test.isWildcardTest()) {
4 ✔
1022
            try {
1023
                final NodeSet result = new NewArrayNodeSet();
4 ✔
1024
                for (final NodeProxy next : contextSet) {
11 ✔
1025
                    final NodeList cl = next.getOwnerDocument().getChildNodes();
4 ✔
1026
                    for (int j = 0; j < cl.getLength(); j++) {
8 ✔
1027
                        final NodeHandle node = (NodeHandle) cl.item(j);
5 ✔
1028
                        final NodeProxy root = new NodeProxy(this, node);
6 ✔
1029
                        final StreamFilter filter;
1030
                        if (axis == Constants.PRECEDING_AXIS) {
4 ✔
1031
                            filter = new PrecedingFilter(test, root, next, result, contextId, position);
13 ✔
1032
                        } else {
1 ✔
1033
                            filter = new FollowingFilter(test, root, next, result, contextId, position);
13 ✔
1034
                        }
1035
                        // See readerStartForWildcardAxis: for FOLLOWING_AXIS we now start the
1036
                        // reader at the reference node, eliminating the position-dependent
1037
                        // doc-start walk reported in #2129.
1038
                        final IEmbeddedXMLStreamReader reader = context.getBroker()
3 ✔
1039
                                .getXMLStreamReader(readerStartForWildcardAxis(node, root, next), false);
7 ✔
1040
                        reader.filter(filter);
3 ✔
1041
                    }
1042
                }
1043
                return result;
2 ✔
1044
            } catch (final XMLStreamException | IOException e) {
×
1045
                throw new XPathException(this, e);
×
1046
            }
1047
        } else {
1048
            // TODO : no test on preloaded data ?
1049
            final DocumentSet docs = getDocumentSet(contextSet);
4 ✔
1050
            synchronized (context) {
4 ✔
1051
                if (currentSet == null || currentDocs == null || !(docs.equalDocs(currentDocs))) {
11 !
1052
                    final StructuralIndex index = context.getBroker().getStructuralIndex();
4 ✔
1053
                    if (context.getProfiler().isEnabled()) {
4 !
1054
                        context.getProfiler().message(this, Profiler.OPTIMIZATIONS, "OPTIMIZATION",
×
1055
                                "Using structural index '" + index.toString() + "'");
×
1056
                    }
1057
                    currentSet = index.findElementsByTagName(ElementValue.ELEMENT, docs, test.getName(), null, this);
11 ✔
1058
                    currentDocs = docs;
3 ✔
1059
                    registerUpdateListener();
2 ✔
1060
                }
1061

1062
                if (position > -1) {
3 ✔
1063
                    try {
1064
                        applyPredicate = false;
3 ✔
1065
                        if (axis == Constants.PRECEDING_AXIS) {
4 ✔
1066
                            return currentSet.selectPreceding(contextSet, position, contextId);
10 ✔
1067
                        } else {
1068
                            return currentSet.selectFollowing(contextSet, position, contextId);
10 ✔
1069
                        }
1070
                    } catch (final UnsupportedOperationException e) {
×
1071
                        if (axis == Constants.PRECEDING_AXIS) {
×
1072
                            return currentSet.selectPreceding(contextSet, contextId);
×
1073
                        } else {
1074
                            return currentSet.selectFollowing(contextSet, contextId);
×
1075
                        }
1076
                    }
1077
                } else {
1078
                    if (axis == Constants.PRECEDING_AXIS) {
4 ✔
1079
                        return currentSet.selectPreceding(contextSet, contextId);
9 ✔
1080
                    } else {
1081
                        return currentSet.selectFollowing(contextSet, contextId);
9 ✔
1082
                    }
1083
                }
1084
            }
1085
        }
1086
    }
1087

1088
    /**
1089
     * Decide where the wildcard preceding-or-following StAX reader should start.
1090
     *
1091
     * <p>For PRECEDING_AXIS we keep the historical behaviour and walk the document-child's
1092
     * subtree from its root; the {@link PrecedingFilter} short-circuits as soon as the reader
1093
     * crosses the reference node.</p>
1094
     *
1095
     * <p>For FOLLOWING_AXIS we start the reader at the reference node itself when it lies
1096
     * inside this document-child's subtree. The {@link FollowingFilter} already skips the
1097
     * reference node and its descendants (via its isAfter / isDescendantOf checks) and
1098
     * terminates on END_ELEMENT at the document-child's tree level, so starting later in
1099
     * document order is safe and removes the O(refPosition) doc-start walk reported in
1100
     * issue #2129. When the reference node is in some other document-child's subtree, fall
1101
     * back to walking from this subtree's root - every event in it is by definition after
1102
     * the reference node.</p>
1103
     */
1104
    private NodeHandle readerStartForWildcardAxis(final NodeHandle node, final NodeProxy root,
1105
            final NodeProxy next) {
1106
        if (axis != Constants.FOLLOWING_AXIS) {
4 ✔
1107
            return node;
2 ✔
1108
        }
1109
        final NodeId rootId = root.getNodeId();
3 ✔
1110
        final NodeId refId = next.getNodeId();
3 ✔
1111
        return refId.equals(rootId) || refId.isDescendantOf(rootId) ? next : node;
12 ✔
1112
    }
1113

1114
    /**
1115
     * If the optimizer has determined that the first filter after this step is a simple positional
1116
     * predicate and can be optimized, try to precompute the position and return it to limit the
1117
     * number of items being processed by axes.
1118
     *
1119
     * @return max number of nodes to be processed until position is reached
1120
     * @throws XPathException if a dynamic error occurs
1121
     */
1122
    private int computeLimit() throws XPathException {
1123
        int position = -1;
2 ✔
1124
        if (this.checkPositionalFilters(this.inPredicate)) {
5 ✔
1125
            final Predicate pred = predicates[0];
5 ✔
1126
            final Sequence seq = pred.preprocess();
3 ✔
1127

1128
            final NumericValue v = (NumericValue) seq.itemAt(0);
5 ✔
1129
            // Non integers return... nothing, not even an error !
1130
            if (!v.hasFractionalPart() && !v.isZero()) {
6 !
1131
                position = v.getInt();
3 ✔
1132
            }
1133
        }
1134
        return position;
2 ✔
1135
    }
1136

1137
    /**
1138
     * Get the ancestor axis nodes
1139
     *
1140
     * @param context the xquery context
1141
     * @param contextSequence the context sequence
1142
     *
1143
     * @return the ancestor nodes
1144
     *
1145
     * @throws XPathException if an error occurs
1146
     */
1147
    protected Sequence getAncestors(final XQueryContext context, final Sequence contextSequence) throws XPathException {
1148
        if (!contextSequence.isPersistentSet()) {
3 ✔
1149
            final MemoryNodeSet nodes = contextSequence.toMemNodeSet();
3 ✔
1150
            return nodes.getAncestors(axis == Constants.ANCESTOR_SELF_AXIS, test);
12 ✔
1151
        }
1152

1153
        final NodeSet contextSet = contextSequence.toNodeSet();
3 ✔
1154
        if (test.isWildcardTest()) {
4 ✔
1155
            final NodeSet result = new NewArrayNodeSet();
4 ✔
1156
            result.setProcessInReverseOrder(true);
3 ✔
1157
            for (final NodeProxy current : contextSet) {
11 ✔
1158
                NodeProxy ancestor;
1159
                if (axis == Constants.ANCESTOR_SELF_AXIS && test.matches(current)) {
9 ✔
1160
                    ancestor = new NodeProxy(this, current);
6 ✔
1161
                    ancestor.setNodeType(Node.ELEMENT_NODE);
3 ✔
1162
                    final NodeProxy t = result.get(ancestor);
4 ✔
1163
                    if (t == null) {
2 !
1164
                        if (Expression.NO_CONTEXT_ID != contextId) {
4 ✔
1165
                            ancestor.addContextNode(contextId, current);
5 ✔
1166
                        } else {
1 ✔
1167
                            NodeProxy.propagatePredicateContextFrom(ancestor, current, contextId);
5 ✔
1168
                        }
1169
                        ancestor.addMatches(current);
3 ✔
1170
                        result.add(ancestor);
3 ✔
1171
                    } else {
1 ✔
1172
                        t.addContextNode(contextId, current);
×
1173
                        t.addMatches(current);
×
1174
                    }
1175
                }
1176

1177
                NodeId parentID = current.getNodeId().getParentId();
4 ✔
1178
                while (parentID != null) {
3 ✔
1179
                    ancestor = new NodeProxy(this, current.getOwnerDocument(), parentID, parentID == NodeId.DOCUMENT_NODE ? Node.DOCUMENT_NODE : Node.ELEMENT_NODE);
14 ✔
1180
                    // Filter out the temporary nodes wrapper element
1181
                    if (!(parentID.getTreeLevel() == 1 && current.getOwnerDocument().getCollection().isTempCollection())) {
9 !
1182
                        if (test.matches(ancestor)) {
5 ✔
1183
                            final NodeProxy t = result.get(ancestor);
4 ✔
1184
                            if (t == null) {
2 ✔
1185
                                if (Expression.NO_CONTEXT_ID != contextId) {
4 ✔
1186
                                    ancestor.addContextNode(contextId, current);
5 ✔
1187
                                } else {
1 ✔
1188
                                    NodeProxy.propagatePredicateContextFrom(ancestor, current, contextId);
5 ✔
1189
                                }
1190
                                ancestor.addMatches(current);
3 ✔
1191
                                result.add(ancestor);
3 ✔
1192
                            } else {
1 ✔
1193
                                t.addContextNode(contextId, current);
5 ✔
1194
                                t.addMatches(current);
3 ✔
1195
                            }
1196
                        }
1197
                    }
1198
                    parentID = parentID.getParentId();
3 ✔
1199
                }
1200
            }
1201
            return result;
2 ✔
1202
        } else if (hasPreloadedData()) {
3 !
1203
            final DocumentSet docs = getDocumentSet(contextSet);
×
1204
            synchronized (context) {
×
1205
                if (currentSet == null || currentDocs == null || (!optimized && !(docs == currentDocs || docs.equalDocs(currentDocs)))) {
×
1206
                    final StructuralIndex index = context.getBroker().getStructuralIndex();
×
1207
                    if (context.getProfiler().isEnabled()) {
×
1208
                        context.getProfiler().message(
×
1209
                                this,
×
1210
                                Profiler.OPTIMIZATIONS,
×
1211
                                "OPTIMIZATION",
×
1212
                                "Using structural index '" + index.toString()
×
1213
                                        + "'");
1214
                    }
1215
                    currentSet = index.findElementsByTagName(ElementValue.ELEMENT, docs, test.getName(), null, this);
×
1216
                    currentDocs = docs;
×
1217
                    registerUpdateListener();
×
1218
                }
1219
                return switch (axis) {
×
1220
                    case Constants.ANCESTOR_SELF_AXIS -> currentSet.selectAncestors(contextSet, true, contextId);
×
1221
                    case Constants.ANCESTOR_AXIS -> currentSet.selectAncestors(contextSet, false, contextId);
×
1222
                    default -> throw new IllegalArgumentException("Unsupported axis specified");
×
1223
                };
1224
            }
1225
        } else {
1226
            final DocumentSet docs = getDocumentSet(contextSet);
4 ✔
1227
            final StructuralIndex index = context.getBroker().getStructuralIndex();
4 ✔
1228
            if (context.getProfiler().isEnabled()) {
4 !
1229
                context.getProfiler().message(this, Profiler.OPTIMIZATIONS,
×
1230
                        "OPTIMIZATION",
×
1231
                        "Using structural index '" + index.toString() + "'");
×
1232
            }
1233
            return index.findAncestorsByTagName(ElementValue.ELEMENT, test.getName(), axis, docs, contextSet, contextId);
13 ✔
1234
        }
1235
    }
1236

1237
    /**
1238
     * Get the parent axis nodes
1239
     *
1240
     * @param context the xquery context
1241
     * @param contextSequence the context sequence
1242
     *
1243
     * @return the parent nodes
1244
     *
1245
     * @throws XPathException if an error occurs
1246
     */
1247
    protected Sequence getParents(final XQueryContext context, final Sequence contextSequence) throws XPathException {
1248
        if (!contextSequence.isPersistentSet()) {
3 ✔
1249
            final MemoryNodeSet nodes = contextSequence.toMemNodeSet();
3 ✔
1250
            return nodes.getParents(test);
5 ✔
1251
        }
1252

1253
        final NodeSet contextSet = contextSequence.toNodeSet();
3 ✔
1254
        if (test.isWildcardTest()) {
4 ✔
1255
            final NodeSet temp = contextSet.getParents(contextId);
5 ✔
1256
            final NodeSet result = new NewArrayNodeSet();
4 ✔
1257
            for (final NodeProxy p : temp) {
11 ✔
1258
                if (test.matches(p)) {
5 ✔
1259
                    result.add(p);
3 ✔
1260
                }
1261
            }
1262
            return result;
2 ✔
1263
        } else if (hasPreloadedData()) {
3 ✔
1264
            final DocumentSet docs = getDocumentSet(contextSet);
4 ✔
1265
            synchronized (context) {
4 ✔
1266
                if (currentSet == null || currentDocs == null || (!optimized && !(docs == currentDocs || docs.equalDocs(currentDocs)))) {
9 !
1267
                    final StructuralIndex index = context.getBroker().getStructuralIndex();
×
1268
                    if (context.getProfiler().isEnabled()) {
×
1269
                        context.getProfiler().message(
×
1270
                                this,
×
1271
                                Profiler.OPTIMIZATIONS,
×
1272
                                "OPTIMIZATION",
×
1273
                                "Using structural index '" + index.toString()
×
1274
                                        + "'");
1275
                    }
1276
                    currentSet = index.findElementsByTagName(ElementValue.ELEMENT, docs, test.getName(), null, this);
×
1277
                    currentDocs = docs;
×
1278
                    registerUpdateListener();
×
1279
                }
1280
                return contextSet.selectParentChild(currentSet, NodeSet.ANCESTOR);
8 ✔
1281
            }
1282
        } else {
1283
            final DocumentSet docs = getDocumentSet(contextSet);
4 ✔
1284
            final StructuralIndex index = context.getBroker().getStructuralIndex();
4 ✔
1285
            if (context.getProfiler().isEnabled()) {
4 !
1286
                context.getProfiler().message(this, Profiler.OPTIMIZATIONS,
×
1287
                        "OPTIMIZATION",
×
1288
                        "Using structural index '" + index.toString() + "'");
×
1289
            }
1290
            return index.findAncestorsByTagName(ElementValue.ELEMENT, test.getName(), Constants.PARENT_AXIS, docs, contextSet, contextId);
12 ✔
1291
        }
1292
    }
1293

1294
    /**
1295
     * Get the document set
1296
     *
1297
     * @param contextSet the context set
1298
     * @return the document set
1299
     */
1300
    protected DocumentSet getDocumentSet(final NodeSet contextSet) {
1301
        DocumentSet ds = getContextDocSet();
3 ✔
1302
        if (ds == null) {
2 ✔
1303
            ds = contextSet.getDocumentSet();
3 ✔
1304
        }
1305
        return ds;
2 ✔
1306
    }
1307

1308
    /**
1309
     * Get the parent expression
1310
     *
1311
     * @return the parent expression
1312
     */
1313
    public Expression getParentExpression() {
1314
        return this.parent;
3 ✔
1315
    }
1316

1317
    /**
1318
     * Register the update listener
1319
     */
1320
    protected void registerUpdateListener() {
1321
        if (listener == null) {
3 ✔
1322
            listener = new UpdateListener() {
12 ✔
1323
                @Override
1324
                public void documentUpdated(final DocumentImpl document, final int event) {
1325
                    if (document == null || event == UpdateListener.ADD || event == UpdateListener.REMOVE) {
7 !
1326
                        // clear all
1327
                        currentDocs = null;
4 ✔
1328
                        currentSet = null;
4 ✔
1329
                    } else {
1 ✔
1330
                        if (currentDocs != null && currentDocs.contains(document.getDocId())) {
4 !
1331
                            currentDocs = null;
×
1332
                            currentSet = null;
×
1333
                        }
1334
                    }
1335
                }
1 ✔
1336

1337
                @Override
1338
                public void nodeMoved(final NodeId oldNodeId, final NodeHandle newNode) {
1339
                    //no-op
1340
                }
1 ✔
1341

1342
                @Override
1343
                public void unsubscribe() {
1344
                    LocationStep.this.listener = null;
4 ✔
1345
                }
1 ✔
1346

1347
                @Override
1348
                public void debug() {
1349
                    if (LOG.isDebugEnabled()) {
×
1350
                        LOG.debug("UpdateListener: Line: {}; id: {}", LocationStep.this.toString(), LocationStep.this.getExpressionId());
×
1351
                    }
1352
                }
×
1353
            };
1354
            context.registerUpdateListener(listener);
5 ✔
1355
        }
1356
    }
1 ✔
1357

1358
    @Override
1359
    public void accept(final ExpressionVisitor visitor) {
1360
        visitor.visitLocationStep(this);
3 ✔
1361
    }
1 ✔
1362

1363
    /**
1364
     * Set the parent expression
1365
     *
1366
     * @param parent the parent expression
1367
     */
1368
    public void setParent(final Expression parent) {
1369
        this.parent = parent;
3 ✔
1370
    }
1 ✔
1371

1372
    @Override
1373
    public void resetState(final boolean postOptimization) {
1374
        super.resetState(postOptimization);
3 ✔
1375
        if (!postOptimization) {
2 ✔
1376
            // TODO : preloadedData = false ?
1377
            // No : introduces a regression in testMatchCount
1378
            // TODO : Investigate...
1379
            currentSet = null;
3 ✔
1380
            currentDocs = null;
3 ✔
1381
            optimized = false;
3 ✔
1382
            listener = null;
3 ✔
1383
        }
1384
    }
1 ✔
1385

1386
    private abstract class AbstractFilterBase implements StreamFilter {
1387
        final NodeTest test;
1388
        final NodeSet result;
1389
        final int limit;
1390
        int nodesRead = 0;
3 ✔
1391
        final int contextId;
1392

1393
        AbstractFilterBase(final NodeTest test, final NodeSet result, final int contextId, final int limit) {
5 ✔
1394
            this.test = test;
3 ✔
1395
            this.result = result;
3 ✔
1396
            this.contextId = contextId;
3 ✔
1397
            this.limit = limit;
3 ✔
1398
            if (limit > -1 && context.getProfiler().traceFunctions()) {
8 ✔
1399
                context.getProfiler().traceOptimization(context, PerformanceStats.OptimizationType.POSITIONAL_PREDICATE,
7 ✔
1400
                        LocationStep.this);
1 ✔
1401
            }
1402
        }
1 ✔
1403
    }
1404

1405
    private class FollowingSiblingFilter extends AbstractFilterBase {
1406
        final NodeProxy start;
1407
        final int level;
1408
        boolean sibling = false;
3 ✔
1409

1410
        FollowingSiblingFilter(final NodeTest test, final NodeProxy start, final NodeSet result,
1411
                final int contextId, final int limit) {
3 ✔
1412
            super(test, result, contextId, limit);
7 ✔
1413
            this.start = start;
3 ✔
1414
            this.level = start.getNodeId().getTreeLevel();
5 ✔
1415
        }
1 ✔
1416

1417
        @Override
1418
        public boolean accept(final XMLStreamReader reader) {
1419
            final NodeId currentId = (NodeId) reader.getProperty(ExtendedXMLStreamReader.PROPERTY_NODE_ID);
5 ✔
1420
            final int currentLevel = currentId.getTreeLevel();
3 ✔
1421

1422
            if (!sibling) {
3 ✔
1423
                // skip over the start node to the first sibling
1424
                sibling = currentId.equals(start.getNodeId());
7 ✔
1425

1426
            } else if (currentLevel == level && !reader.isEndElement() && test.matches(reader)) {
13 ✔
1427
                // sibling which matches the test
1428
                NodeProxy sibling = result.get(start.getOwnerDocument(), currentId);
8 ✔
1429
                if (sibling == null) {
2 ✔
1430
                    sibling = new NodeProxy(null, start.getOwnerDocument(), currentId,
9 ✔
1431
                            StaXUtil.streamType2DOM(reader.getEventType()), ((EmbeddedXMLStreamReader) reader).getCurrentPosition());
6 ✔
1432

1433
                    if (Expression.IGNORE_CONTEXT != contextId) {
4 !
1434
                        if (Expression.NO_CONTEXT_ID == contextId) {
4 ✔
1435
                            NodeProxy.propagatePredicateContextFrom(sibling, start, contextId);
6 ✔
1436
                        } else {
1 ✔
1437
                            sibling.addContextNode(contextId, start);
6 ✔
1438
                        }
1439
                    }
1440
                    result.add(sibling);
4 ✔
1441
                } else if (Expression.NO_CONTEXT_ID != contextId) {
5 !
1442
                    sibling.addContextNode(contextId, start);
6 ✔
1443
                }
1444
                nodesRead++;
6 ✔
1445
                if (this.limit > -1 && nodesRead == this.limit) {
9 ✔
1446
                    return false;
2 ✔
1447
                }
1448

1449
            } else if (currentLevel < level) {
4 ✔
1450
                // exited the parent node, so stop filtering
1451
                return false;
2 ✔
1452
            }
1453

1454
            return true;
2 ✔
1455
        }
1456
    }
1457

1458
    private class PrecedingSiblingFilter extends AbstractFilterBase {
1459
        final int level;
1460
        final NodeProxy referenceNode;
1461

1462
        PrecedingSiblingFilter(final NodeTest test, final NodeProxy start, final NodeProxy referenceNode,
1463
                final NodeSet result, final int contextId) {
3 ✔
1464
            super(test, result, contextId, -1);
7 ✔
1465
            this.level = start.getNodeId().getTreeLevel();
5 ✔
1466
            this.referenceNode = referenceNode;
3 ✔
1467
        }
1 ✔
1468

1469
        @Override
1470
        public boolean accept(final XMLStreamReader reader) {
1471
            final NodeId currentId = (NodeId) reader.getProperty(ExtendedXMLStreamReader.PROPERTY_NODE_ID);
5 ✔
1472

1473
            final NodeId refId = referenceNode.getNodeId();
4 ✔
1474
            if (currentId.equals(refId)) {
4 ✔
1475
                // reached the reference node
1476
                return false;
2 ✔
1477
            }
1478

1479
            if (reader.isEndElement()) {
3 ✔
1480
                return true;
2 ✔
1481
            }
1482
            if (currentId.getTreeLevel() == level && test.matches(reader)) {
10 ✔
1483
                // sibling which matches the test
1484
                NodeProxy sibling = result.get(referenceNode.getOwnerDocument(), currentId);
8 ✔
1485
                if (sibling == null) {
2 ✔
1486
                    sibling = new NodeProxy(null, referenceNode.getOwnerDocument(), currentId,
9 ✔
1487
                            StaXUtil.streamType2DOM(reader.getEventType()), ((EmbeddedXMLStreamReader) reader).getCurrentPosition());
6 ✔
1488
                    if (Expression.IGNORE_CONTEXT != contextId) {
4 !
1489
                        if (Expression.NO_CONTEXT_ID == contextId) {
4 ✔
1490
                            NodeProxy.propagatePredicateContextFrom(sibling, referenceNode, contextId);
6 ✔
1491
                        } else {
1 ✔
1492
                            sibling.addContextNode(contextId, referenceNode);
6 ✔
1493
                        }
1494
                    }
1495
                    result.add(sibling);
4 ✔
1496
                } else if (Expression.NO_CONTEXT_ID != contextId) {
5 !
1497
                    sibling.addContextNode(contextId, referenceNode);
6 ✔
1498
                }
1499
            }
1500

1501
            return true;
2 ✔
1502
        }
1503
    }
1504

1505
    private class FollowingFilter extends AbstractFilterBase {
1506
        final NodeProxy root;
1507
        final NodeProxy referenceNode;
1508
        boolean isAfter = false;
3 ✔
1509

1510
        FollowingFilter(final NodeTest test, final NodeProxy root, final NodeProxy referenceNode, final NodeSet result,
1511
                final int contextId, final int limit) {
3 ✔
1512
            super(test, result, contextId, limit);
7 ✔
1513
            this.root = root;
3 ✔
1514
            this.referenceNode = referenceNode;
3 ✔
1515
        }
1 ✔
1516

1517
        @Override
1518
        public boolean accept(final XMLStreamReader reader) {
1519
            final NodeId currentId = (NodeId) reader.getProperty(ExtendedXMLStreamReader.PROPERTY_NODE_ID);
5 ✔
1520
            if (reader.getEventType() == XMLStreamReader.END_ELEMENT) {
4 ✔
1521
                if (currentId.getTreeLevel() == root.getNodeId().getTreeLevel()) {
7 ✔
1522
                    // exited the root element, so  stop filtering
1523
                    return false;
2 ✔
1524
                }
1525

1526
                return true;
2 ✔
1527
            }
1528

1529
            final NodeId refId = referenceNode.getNodeId();
4 ✔
1530

1531
            if (!isAfter) {
3 ✔
1532
                isAfter = currentId.compareTo(refId) > 0 && !currentId.isDescendantOf(refId);
13 ✔
1533
            }
1534

1535
            if (isAfter && !refId.isDescendantOf(currentId) && test.matches(reader)) {
12 !
1536
                final NodeProxy proxy = new NodeProxy(null, referenceNode.getOwnerDocument(), currentId,
9 ✔
1537
                        StaXUtil.streamType2DOM(reader.getEventType()), ((EmbeddedXMLStreamReader) reader).getCurrentPosition());
6 ✔
1538
                if (Expression.IGNORE_CONTEXT != contextId) {
4 !
1539
                    if (Expression.NO_CONTEXT_ID == contextId) {
4 ✔
1540
                        NodeProxy.propagatePredicateContextFrom(proxy, referenceNode, contextId);
6 ✔
1541
                    } else {
1 ✔
1542
                        proxy.addContextNode(contextId, referenceNode);
6 ✔
1543
                    }
1544
                }
1545
                result.add(proxy);
4 ✔
1546
                nodesRead++;
6 ✔
1547
                if (this.limit > -1 && nodesRead == this.limit) {
9 ✔
1548
                    return false;
2 ✔
1549
                }
1550
            }
1551
            return true;
2 ✔
1552
        }
1553
    }
1554

1555
    private class PrecedingFilter extends AbstractFilterBase {
1556
        final NodeProxy root;
1557
        final NodeProxy referenceNode;
1558
        // Sliding window of the most recent {@code limit} matches. Non-null only
1559
        // when limit > 0 (positional predicate {@code [K]} present). The K-th
1560
        // preceding element in axis order is the (K-th-from-end) match in doc
1561
        // order, so any match earlier than the K most recent cannot be selected
1562
        // and may be discarded as new ones are found.
1563
        final Deque<NodeProxy> window;
1564

1565
        PrecedingFilter(final NodeTest test, final NodeProxy root, final NodeProxy referenceNode, final NodeSet result,
1566
                final int contextId, final int limit) {
3 ✔
1567
            super(test, result, contextId, limit);
7 ✔
1568
            this.root = root;
3 ✔
1569
            this.referenceNode = referenceNode;
3 ✔
1570
            this.window = limit > 0 ? new ArrayDeque<>(limit) : null;
10 ✔
1571
        }
1 ✔
1572

1573
        @Override
1574
        public boolean accept(final XMLStreamReader reader) {
1575
            final NodeId currentId = (NodeId) reader.getProperty(ExtendedXMLStreamReader.PROPERTY_NODE_ID);
5 ✔
1576

1577
            if (reader.getEventType() == XMLStreamReader.END_ELEMENT) {
4 ✔
1578
                // exited the root element, so  stop filtering
1579
                if (currentId.getTreeLevel() == root.getNodeId().getTreeLevel()) {
7 !
1580
                    flushWindow();
×
1581
                    return false;
×
1582
                }
1583
                return true;
2 ✔
1584
            }
1585

1586
            final NodeId refId = referenceNode.getNodeId();
4 ✔
1587
            if (currentId.compareTo(refId) >= 0) {
4 ✔
1588
                flushWindow();
2 ✔
1589
                return false;
2 ✔
1590
            }
1591

1592
            if (!refId.isDescendantOf(currentId) && test.matches(reader)) {
9 ✔
1593
                final NodeProxy proxy = new NodeProxy(null, referenceNode.getOwnerDocument(), currentId,
9 ✔
1594
                        StaXUtil.streamType2DOM(reader.getEventType()), ((EmbeddedXMLStreamReader) reader).getCurrentPosition());
6 ✔
1595
                if (Expression.IGNORE_CONTEXT != contextId) {
4 !
1596
                    if (Expression.NO_CONTEXT_ID == contextId) {
4 ✔
1597
                        NodeProxy.propagatePredicateContextFrom(proxy, referenceNode, contextId);
6 ✔
1598
                    } else {
1 ✔
1599
                        proxy.addContextNode(contextId, referenceNode);
6 ✔
1600
                    }
1601
                }
1602
                if (window != null) {
3 ✔
1603
                    if (window.size() == limit) {
6 ✔
1604
                        window.pollFirst();
4 ✔
1605
                    }
1606
                    window.addLast(proxy);
4 ✔
1607
                } else {
1 ✔
1608
                    result.add(proxy);
4 ✔
1609
                }
1610
            }
1611
            return true;
2 ✔
1612
        }
1613

1614
        private void flushWindow() {
1615
            if (window != null) {
3 ✔
1616
                for (final NodeProxy proxy : window) {
12 ✔
1617
                    result.add(proxy);
4 ✔
1618
                }
1619
                window.clear();
3 ✔
1620
            }
1621
        }
1 ✔
1622
    }
1623

1624
}
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