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

evolvedbinary / elemental / 982

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

push

circleci

adamretter
[feature] Improve README.md badges

28451 of 55847 branches covered (50.94%)

Branch coverage included in aggregate %.

77468 of 131924 relevant lines covered (58.72%)

0.59 hits per line

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

43.68
/exist-core/src/main/java/org/exist/xquery/value/SubSequence.java
1
/*
2
 * Elemental
3
 * Copyright (C) 2024, Evolved Binary Ltd
4
 *
5
 * admin@evolvedbinary.com
6
 * https://www.evolvedbinary.com | https://www.elemental.xyz
7
 *
8
 * Use of this software is governed by the Business Source License 1.1
9
 * included in the LICENSE file and at www.mariadb.com/bsl11.
10
 *
11
 * Change Date: 2028-04-27
12
 *
13
 * On the date above, in accordance with the Business Source License, use
14
 * of this software will be governed by the Apache License, Version 2.0.
15
 *
16
 * Additional Use Grant: Production use of the Licensed Work for a permitted
17
 * purpose. A Permitted Purpose is any purpose other than a Competing Use.
18
 * A Competing Use means making the Software available to others in a commercial
19
 * product or service that: substitutes for the Software; substitutes for any
20
 * other product or service we offer using the Software that exists as of the
21
 * date we make the Software available; or offers the same or substantially
22
 * similar functionality as the Software.
23
 */
24
package org.exist.xquery.value;
25

26
import com.evolvedbinary.j8fu.tuple.Tuple2;
27
import org.apache.logging.log4j.LogManager;
28
import org.apache.logging.log4j.Logger;
29
import org.exist.collections.Collection;
30
import org.exist.dom.memtree.DocumentImpl;
31
import org.exist.dom.memtree.NodeImpl;
32
import org.exist.dom.persistent.*;
33
import org.exist.numbering.NodeId;
34
import org.exist.xquery.Cardinality;
35
import org.exist.xquery.Expression;
36
import org.exist.xquery.XPathException;
37
import org.exist.xquery.XQueryContext;
38
import org.w3c.dom.Document;
39
import org.w3c.dom.Node;
40

41
import javax.annotation.Nullable;
42
import java.util.*;
43

44
import static com.evolvedbinary.j8fu.tuple.Tuple.Tuple;
45

46
/**
47
 * An immutable sequence that wraps an existing
48
 * sequence, and provides access to a subset
49
 * of the wrapped sequence, i.e. a sub-sequence.
50
 *
51
 * @author <a href="mailto:adam@evolvedbinary.com">Adam Retter</a>
52
 */
53
public class SubSequence extends AbstractSequence {
54
    private static final Logger LOG = LogManager.getLogger(SubSequence.class);
1✔
55

56
    private final long fromInclusive;
57
    private final long toExclusive;
58
    private final Sequence sequence;
59

60
    /**
61
     * @param fromInclusive The starting position in the {@code sequence} for the sub-sequence,
62
     *     should be 1 for the first item in the {@code sequence}. This can be out-of-bounds
63
     *     for the {@code sequence}.
64
     * @param sequence The underlying sequence, for which we will provide a sub-sequence.
65
     */
66
    public SubSequence(final long fromInclusive, final Sequence sequence) {
67
        this(fromInclusive, Long.MAX_VALUE, sequence);
×
68
    }
×
69

70
    /**
71
     * @param fromInclusive The starting position in the {@code sequence} for the sub-sequence,
72
     *     should be 1 for the first item in the {@code sequence}. This can be out-of-bounds
73
     *     for the {@code sequence}.
74
     * @param toExclusive The End of sequence position for the sub-sequence. If you want everything
75
     *     from the sequence, then this is the {@link Sequence#getItemCountLong()} + 1.
76
     *     Specifying an ending position past the end of the sequence is allowed.
77
     *     If you don't know the length of the sequence, then {@link Long#MAX_VALUE} can be used.
78
     * @param sequence The underlying sequence, for which we will provide a sub-sequence.
79
     */
80
    public SubSequence(final long fromInclusive, final long toExclusive, final Sequence sequence) {
1✔
81
        this.fromInclusive = fromInclusive <= 0 ? 1 : fromInclusive;
1✔
82
        this.toExclusive = toExclusive;
1✔
83
        this.sequence = sequence;
1✔
84
    }
1✔
85

86
    @Override
87
    public void add(final Item item) throws XPathException {
88
        throw new XPathException((Expression) null, "Cannot add an item to a sub-sequence");
×
89
    }
90

91
    @Override
92
    public int getItemType() {
93
        return sequence.getItemType();
1✔
94
    }
95

96
    @Override
97
    public SequenceIterator iterate() throws XPathException {
98
        if (isEmpty()) {
1✔
99
            return SequenceIterator.EMPTY_ITERATOR;
1✔
100
        }
101

102
        return new SubSequenceIterator(fromInclusive, toExclusive, sequence);
1✔
103
    }
104

105
    @Override
106
    public SequenceIterator unorderedIterator() throws XPathException {
107
        return iterate();
×
108
    }
109

110
    @Override
111
    public long getItemCountLong() {
112
        if (toExclusive < 1) {
1✔
113
            return 0;
1✔
114
        }
115

116
        long subseqAvailable = sequence.getItemCountLong() - (fromInclusive - 1);
1✔
117
        if (subseqAvailable < 0) {
1!
118
            subseqAvailable = 0;
×
119
        }
120

121
        long length = toExclusive - fromInclusive;
1✔
122
        if (length < 0) {
1!
123
            length = 0;
×
124
        }
125

126
        return Math.min(length, subseqAvailable);
1✔
127
    }
128

129
    @Override
130
    public boolean isEmpty() {
131
        final long length = toExclusive - fromInclusive;
1✔
132
        return length < 1 || sequence.isEmpty() || sequence.getItemCountLong() - fromInclusive < 0;
1!
133
    }
134

135
    @Override
136
    public boolean hasOne() {
137
        final long subseqAvailable = sequence.getItemCountLong() - (fromInclusive - 1);
1✔
138
        final long length = toExclusive - fromInclusive;
1✔
139
        return subseqAvailable > 0 && length == 1;
1✔
140
    }
141

142
    @Override
143
    public boolean hasMany() {
144
        final long subseqAvailable = sequence.getItemCountLong() - (fromInclusive - 1);
1✔
145
        final long length = toExclusive - fromInclusive;
1✔
146
        return subseqAvailable > 1 && length > 1;
1✔
147
    }
148

149
    @Override
150
    public void removeDuplicates() {
151
    }
1✔
152

153
    @Override
154
    public Cardinality getCardinality() {
155
        final long length = toExclusive - fromInclusive;
1✔
156
        if (length < 1 || sequence.isEmpty()) {
1!
157
           return Cardinality.EMPTY_SEQUENCE;
1✔
158
        }
159

160
        final long subseqAvailable = sequence.getItemCountLong() - (fromInclusive - 1);
1✔
161
        if (subseqAvailable < 1) {
1✔
162
            return Cardinality.EMPTY_SEQUENCE;
1✔
163
        }
164

165
        if (subseqAvailable > 0 && length == 1) {
1!
166
            return Cardinality.EXACTLY_ONE;
1✔
167
        }
168

169
        if (subseqAvailable > 1 && length > 1) {
1!
170
            return Cardinality._MANY;
1✔
171
        }
172

173
        throw new IllegalStateException("Unknown Cardinality of: " + toString());
×
174
    }
175

176
    @Override
177
    public Item itemAt(final int pos) {
178
        // NOTE: remember that itemAt(pos) is zero based index addressing!
179
        final long length = toExclusive - fromInclusive;
1✔
180
        if (pos < 0 || pos >= length) {
1!
181
            return null;
1✔
182
        }
183

184
        final long subseqAvailable = sequence.getItemCountLong() - (fromInclusive - 1);
1✔
185
        if (pos >= subseqAvailable) {
1✔
186
            return null;
1✔
187
        }
188

189
        return sequence.itemAt((int) fromInclusive - 1 + pos);
1✔
190
    }
191

192
    @Override
193
    public Sequence tail() {
194
        if (isEmpty() || hasOne()) {
1!
195
            return Sequence.EMPTY_SEQUENCE;
1✔
196
        }
197

198
        return new SubSequence(fromInclusive + 1, toExclusive, sequence);
1✔
199
    }
200

201
    @Override
202
    public NodeSet toNodeSet() throws XPathException {
203
        if (isEmpty()) {
1!
204
            return NodeSet.EMPTY_SET;
×
205
        }
206

207
        final Map<DocumentImpl, Tuple2<DocumentImpl, org.exist.dom.persistent.DocumentImpl>> expandedDocs = new HashMap<>();
1✔
208
        final NodeSet nodeSet = new NewArrayNodeSet();
1✔
209
        final SequenceIterator iterator = iterate();
1✔
210
        while (iterator.hasNext()) {
1✔
211
            final Item item = iterator.nextItem();
1✔
212

213
            if (!Type.subTypeOf(item.getType(), Type.NODE)) {
1!
214
                throw new XPathException((Expression) null, "Type error: the sub-sequence cannot be converted into" +
×
215
                        " a node set. It contains an item of type: " + Type.getTypeName(item.getType()));
×
216
            }
217

218
            final NodeValue v = (NodeValue) item;
1✔
219
            if (v.getImplementationType() != NodeValue.PERSISTENT_NODE) {
1!
220
                final NodeProxy p = makePersistent((NodeImpl)v, expandedDocs);
×
221
                if (p == null) {
×
222
                    throw new XPathException((Expression) null, "Type error: the sub-sequence cannot be converted into" +
×
223
                            " a node set. It contains an in-memory node which cannot be persisted.");
224
                } else {
225
                    nodeSet.add(p);
×
226
                }
227
            } else {
×
228
                nodeSet.add((NodeProxy) v);
1✔
229
            }
230
        }
231

232
        return nodeSet;
1✔
233
    }
234

235
    private @Nullable NodeProxy makePersistent(NodeImpl node, final Map<DocumentImpl, Tuple2<DocumentImpl, org.exist.dom.persistent.DocumentImpl>> expandedDocs) throws XPathException {
236
        // found an in-memory document
237
        final DocumentImpl doc;
238
        if (node.getType() == Type.DOCUMENT) {
×
239
            doc = (DocumentImpl) node;
×
240
        } else {
×
241
            doc = node.getOwnerDocument();
×
242
        }
243

244
        if (doc == null) {
×
245
            return null;
×
246
        }
247

248
        final DocumentImpl expandedDoc;
249
        final org.exist.dom.persistent.DocumentImpl newDoc;
250
        if(expandedDocs.containsKey(doc)) {
×
251
            final Tuple2<DocumentImpl, org.exist.dom.persistent.DocumentImpl> expandedDocNewDoc = expandedDocs.get(doc);
×
252
            expandedDoc = expandedDocNewDoc._1;
×
253
            newDoc = expandedDocNewDoc._2;
×
254
        } else {
×
255
            // make this document persistent: doc.makePersistent()
256
            // returns a map of all root node ids mapped to the corresponding
257
            // persistent node. We scan the current sequence and replace all
258
            // in-memory nodes with their new persistent node objects.
259
            expandedDoc = doc.expandRefs(null);
×
260
            newDoc = expandedDoc.makePersistent();
×
261
            expandedDocs.put(doc, Tuple(expandedDoc, newDoc));
×
262
        }
263

264

265
        if (newDoc != null) {
×
266
            final NodeId rootId = newDoc.getBrokerPool().getNodeFactory().createInstance();
×
267
            if (node.getImplementationType() != NodeValue.PERSISTENT_NODE) {
×
268
                final Document nodeOwnerDoc;
269
                if (node.getNodeType() == Node.DOCUMENT_NODE) {
×
270
                    nodeOwnerDoc = (Document) node;
×
271
                } else {
×
272
                    nodeOwnerDoc = node.getOwnerDocument();
×
273
                }
274

275
                if (nodeOwnerDoc == doc) {
×
276
                    if (node.getNodeType() == Node.ATTRIBUTE_NODE) {
×
277
                        node = expandedDoc.getAttribute(node.getNodeNumber());
×
278
                    } else {
×
279
                        node = expandedDoc.getNode(node.getNodeNumber());
×
280
                    }
281
                    NodeId nodeId = node.getNodeId();
×
282
                    if (nodeId == null) {
×
283
                        throw new XPathException((Expression) null, "Internal error: nodeId == null");
×
284
                    }
285
                    if (node.getNodeType() == Node.DOCUMENT_NODE) {
×
286
                        nodeId = rootId;
×
287
                    } else {
×
288
                        nodeId = rootId.append(nodeId);
×
289
                    }
290
                    final NodeProxy p = new NodeProxy(node.getExpression(), newDoc, nodeId, node.getNodeType());
×
291
                    // replace the node by the NodeProxy
292
                    return p;
×
293
                }
294
            }
295
        }
296

297
        return null;
×
298
    }
299

300
    @Override
301
    public MemoryNodeSet toMemNodeSet() throws XPathException {
302
        if (isEmpty()) {
×
303
            return MemoryNodeSet.EMPTY;
×
304
        }
305

306
        final ValueSequence memNodeSet = new ValueSequence(getItemCount());
×
307
        final Set<DocumentImpl> expandedDocs = new HashSet<>();
×
308
        final SequenceIterator iterator = iterate();
×
309
        while (iterator.hasNext()) {
×
310
            final Item item = iterator.nextItem();
×
311
            if (!Type.subTypeOf(item.getType(), Type.NODE)) {
×
312
                throw new XPathException((Expression) null, "Type error: the sub-sequence cannot be converted into" +
×
313
                        " a MemoryNodeSet. It contains items which are not nodes");
314
            }
315

316
            final NodeValue v = (NodeValue) item;
×
317
            if (v.getImplementationType() == NodeValue.PERSISTENT_NODE) {
×
318
                throw new XPathException((Expression) null, "Type error: the sub-sequence cannot be converted into" +
×
319
                        " a MemoryNodeSet. It contains nodes from stored resources.");
320
            }
321

322
            final org.exist.dom.memtree.NodeImpl node = (org.exist.dom.memtree.NodeImpl)item;
×
323
            final DocumentImpl ownerDoc = node.getNodeType() == Node.DOCUMENT_NODE ? (DocumentImpl) node : node.getOwnerDocument();
×
324

325
            if (ownerDoc.hasReferenceNodes()  && !expandedDocs.contains(ownerDoc)) {
×
326
                ownerDoc.expand();
×
327
                expandedDocs.add(ownerDoc);
×
328
            }
329

330
            memNodeSet.add(node);
×
331
        }
332
        return memNodeSet;
×
333
    }
334

335
    @Override
336
    public DocumentSet getDocumentSet() {
337
        try {
338
            final MutableDocumentSet docs = new DefaultDocumentSet();
1✔
339
            final SequenceIterator iterator = iterate();
1✔
340
            while (iterator.hasNext()) {
1✔
341
                final Item item = iterator.nextItem();
1✔
342
                if (Type.subTypeOf(item.getType(), Type.NODE)) {
1!
343
                    final NodeValue node = (NodeValue) item;
×
344
                    if (node.getImplementationType() == NodeValue.PERSISTENT_NODE) {
×
345
                        docs.add((org.exist.dom.persistent.DocumentImpl) node.getOwnerDocument());
×
346
                    }
347
                }
348
            }
349
            return docs;
1✔
350
        } catch (final XPathException e) {
×
351
            LOG.error(e);
×
352
            return DocumentSet.EMPTY_DOCUMENT_SET;
×
353
        }
354
    }
355

356
    @Override
357
    public Iterator<Collection> getCollectionIterator() {
358
        try {
359
            return new CollectionIterator(iterate());
×
360
        } catch (final XPathException e) {
×
361
            LOG.error(e);
×
362
            return super.getCollectionIterator();
×
363
        }
364
    }
365

366
    @Override
367
    public boolean isPersistentSet() {
368
        final SequenceIterator iterator;
369
        try {
370
             iterator = iterate();
1✔
371
        } catch (final XPathException e) {
1✔
372
            throw new RuntimeException(e); // should never happen!
×
373
        }
374

375
        // needed to guard against returning true for an empty-sequence below
376
        if (!iterator.hasNext()) {
1!
377
            return false;
×
378
        }
379

380
        while (iterator.hasNext()) {
1✔
381
            final Item item = iterator.nextItem();
1✔
382
            if (!(item instanceof NodeValue nv)) {
1✔
383
                return false;
1✔
384
            }
385
            if (nv.getImplementationType() != NodeValue.PERSISTENT_NODE) {
1!
386
                return false;
×
387
            }
388
        }
389
        // else, all items were persistent
390
        return true;
1✔
391
    }
392

393
    @Override
394
    public int conversionPreference(final Class<?> javaClass) {
395
        return sequence.conversionPreference(javaClass);
×
396
    }
397

398
    @Override
399
    public boolean isCacheable() {
400
        return sequence.isCacheable();
1✔
401
    }
402

403
    @Override
404
    public int getState() {
405
        return sequence.getState();
1✔
406
    }
407

408
    @Override
409
    public boolean hasChanged(final int previousState) {
410
        return sequence.hasChanged(previousState);
×
411
    }
412

413
    @Override
414
    public boolean isCached() {
415
        return sequence.isCached();
×
416
    }
417

418
    @Override
419
    public void setIsCached(final boolean cached) {
420
        sequence.setIsCached(cached);
×
421
    }
×
422

423
    @Override
424
    public void setSelfAsContext(final int contextId) throws XPathException {
425
        sequence.setSelfAsContext(contextId);
×
426
    }
×
427

428
    @Override
429
    public void clearContext(final int contextId) throws XPathException {
430
        sequence.clearContext(contextId);
1✔
431
    }
1✔
432

433
    @Override
434
    public boolean containsReference(final Item item) {
435
        try {
436
            for (final SequenceIterator it = iterate(); it.hasNext(); ) {
×
437
                final Item i = it.nextItem();
×
438
                if (i == item) {
×
439
                    return true;
×
440
                }
441
            }
442
            return false;
×
443
        } catch (final XPathException e) {
×
444
            LOG.warn(e.getMessage(), e);
×
445
            return false;
×
446
        }
447
    }
448

449
    @Override
450
    public boolean contains(final Item item) {
451
        try {
452
            for (final SequenceIterator it = iterate(); it.hasNext(); ) {
×
453
                final Item i = it.nextItem();
×
454
                if (i.equals(item)) {
×
455
                    return true;
×
456
                }
457
            }
458
            return false;
×
459
        } catch (final XPathException e) {
×
460
            LOG.warn(e.getMessage(), e);
×
461
            return false;
×
462
        }
463
    }
464

465
    @Override
466
    public void destroy(final XQueryContext context, @Nullable final Sequence contextSequence) {
467
        sequence.destroy(context, contextSequence);
1✔
468
    }
1✔
469

470
    @Override
471
    public String toString() {
472
        final StringBuilder builder = new StringBuilder();
×
473
        builder.append("SubSequence(")
×
474
        .append("fi=").append(fromInclusive)
×
475
        .append(", ")
×
476
        .append("te=").append(toExclusive)
×
477
        .append(", ")
×
478
        .append(sequence.toString())
×
479
        .append(')');
×
480
        return builder.toString();
×
481
    }
482

483
    private static class CollectionIterator implements Iterator<Collection> {
484
        private final SequenceIterator iterator;
485
        private Collection nextCollection = null;
×
486

487
        CollectionIterator(final SequenceIterator iterator) {
×
488
            this.iterator = iterator;
×
489
            next();
×
490
        }
×
491

492
        @Override
493
        public boolean hasNext() {
494
            return nextCollection != null;
×
495
        }
496

497
        @Override
498
        public Collection next() {
499
            final Collection oldCollection = nextCollection;
×
500
            nextCollection = null;
×
501
            while (iterator.hasNext()) {
×
502
                final Item item = iterator.nextItem();
×
503
                if (Type.subTypeOf(item.getType(), Type.NODE)) {
×
504
                    final NodeValue node = (NodeValue) item;
×
505
                    if (node.getImplementationType() == NodeValue.PERSISTENT_NODE) {
×
506
                        final NodeProxy p = (NodeProxy) node;
×
507
                        if (!p.getOwnerDocument().getCollection().equals(oldCollection)) {
×
508
                            nextCollection = p.getOwnerDocument().getCollection();
×
509
                            break;
×
510
                        }
511
                    }
512
                }
513
            }
514
            return oldCollection;
×
515
        }
516
    }
517

518
    private static class SubSequenceIterator implements SequenceIterator {
519
        private long position;
520
        private final long toExclusive;
521
        private final SequenceIterator iterator;
522

523
        public SubSequenceIterator(final long fromInclusive, final long toExclusive, final Sequence sequence) throws XPathException {
1✔
524
            this.position = 1;
1✔
525
            this.toExclusive = toExclusive;
1✔
526
            this.iterator = sequence.iterate();
1✔
527

528
            // move sequence iterator to start of sub-sequence
529
            if (position != fromInclusive) {
1✔
530
                // move to start
531
                if (iterator.skip(fromInclusive - position) > -1) {
1!
532
                    position = fromInclusive;
1✔
533
                } else {
1✔
534
                    // SequenceIterator does not support skipping, we have to iterate through each item :-/
535
                    for (; position < fromInclusive; position++) {
×
536
                        iterator.nextItem();
×
537
                    }
538
                }
539
            }
540
        }
1✔
541

542
        @Override
543
        public boolean hasNext() {
544
            return iterator.hasNext() && position < toExclusive;
1✔
545
        }
546

547
        @Override
548
        public Item nextItem() {
549
            if (iterator.hasNext() && position < toExclusive) {
1!
550
                final Item item = iterator.nextItem();
1✔
551
                position++;
1✔
552
                return item;
1✔
553
            }
554

555
            return null;
×
556
        }
557

558
        @Override
559
        public long skippable() {
560
            return Math.min(iterator.skippable(), toExclusive - position);
1✔
561
        }
562

563
        @Override
564
        public long skip(final long n) {
565
            final long seqSkipable = iterator.skippable();
1✔
566
            if (seqSkipable == -1) {
1!
567
                return -1; // underlying iterator does not support skipping
×
568
            }
569

570
            final long skip = Math.min(n, Math.min(seqSkipable, toExclusive - position));
1✔
571
            if (skip <= 0) {
1✔
572
                return 0;  // nothing to skip
1✔
573
            }
574

575
            final long skipped = iterator.skip(skip);
1✔
576
            position += skipped;
1✔
577
            return skipped;
1✔
578
        }
579
    }
580
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc