• 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

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

51
import org.apache.commons.io.input.UnsynchronizedByteArrayInputStream;
52
import org.apache.commons.io.output.UnsynchronizedByteArrayOutputStream;
53
import org.exist.EXistException;
54
import org.exist.Namespaces;
55
import org.exist.dom.NamedNodeMapImpl;
56
import org.exist.dom.NodeListImpl;
57
import org.exist.dom.QName;
58
import org.exist.dom.QName.IllegalQNameException;
59
import org.exist.indexing.IndexController;
60
import org.exist.indexing.StreamListener;
61
import org.exist.indexing.StreamListener.ReindexMode;
62
import org.exist.numbering.NodeId;
63
import org.exist.stax.ExtendedXMLStreamReader;
64
import org.exist.stax.IEmbeddedXMLStreamReader;
65
import org.exist.storage.*;
66
import org.exist.storage.btree.Value;
67
import org.exist.storage.dom.INodeIterator;
68
import org.exist.storage.txn.TransactionException;
69
import org.exist.storage.txn.TransactionManager;
70
import org.exist.storage.txn.Txn;
71
import org.exist.util.ByteArrayPool;
72
import org.exist.util.ByteConversion;
73
import org.exist.util.UTF8;
74
import org.exist.util.pool.NodePool;
75
import org.exist.xmldb.XmldbURI;
76
import org.exist.xquery.Constants;
77
import org.exist.xquery.Expression;
78
import org.exist.xquery.value.StringValue;
79
import org.w3c.dom.*;
80

81
import javax.xml.XMLConstants;
82
import javax.xml.stream.XMLStreamConstants;
83
import javax.xml.stream.XMLStreamException;
84
import java.io.DataInputStream;
85
import java.io.DataOutputStream;
86
import java.io.IOException;
87
import java.io.InputStream;
88
import java.util.*;
89
import java.util.function.Function;
90

91
import static org.exist.dom.QName.Validity.ILLEGAL_FORMAT;
92

93
/**
94
 * ElementImpl.java
95
 *
96
 * @author Wolfgang Meier
97
 */
98
public class ElementImpl extends NamedNode<ElementImpl> implements Element {
99

100
    public static final int LENGTH_ELEMENT_CHILD_COUNT = 4; //sizeof int
101
    public static final int LENGTH_ATTRIBUTES_COUNT = 2; //sizeof short
102
    public static final int LENGTH_NS_ID = 2; //sizeof short
103
    public static final int LENGTH_PREFIX_LENGTH = 2; //sizeof short
104

105
    private short attributes = 0; // number of attributes
1✔
106
    private int children = 0; // number of elements AND attributes
1✔
107

108
    private int position = 0;
1✔
109
    private Map<String, String> namespaceMappings = null;
1✔
110
    private int indexType = RangeIndexSpec.NO_INDEX;
1✔
111
    private boolean preserveWS = false;
1✔
112
    private boolean isDirty = false;
1✔
113

114
    public ElementImpl() {
115
        this((Expression) null);
1✔
116
    }
1✔
117

118
    public ElementImpl(final Expression expression) {
119
        super(expression, Node.ELEMENT_NODE);
1✔
120
    }
1✔
121

122
    /**
123
     * Constructor for the ElementImpl object
124
     * @param symbols for ElementImpl
125
     * @param nodeName Description of the Parameter
126
     */
127
    public ElementImpl(final QName nodeName, final SymbolTable symbols) throws DOMException {
128
        this(null, nodeName, symbols);
×
129
    }
×
130

131
    /**
132
     * Constructor for the ElementImpl object
133
     * @param expression the expression from which this element derives
134
     * @param symbols for ElementImpl
135
     * @param nodeName Description of the Parameter
136
     */
137
    public ElementImpl(final Expression expression, final QName nodeName, final SymbolTable symbols) throws DOMException {
138
        super(expression, Node.ELEMENT_NODE, nodeName);
1✔
139
        this.nodeName = nodeName;
1✔
140
        if(symbols.getSymbol(nodeName.getLocalPart()) < 0) {
1!
141
            throw new DOMException(DOMException.INVALID_ACCESS_ERR,
×
142
                "Too many element/attribute names registered in the database. No of distinct names is limited to 16bit. Aborting store.");
×
143
        }
144
    }
1✔
145

146
    public ElementImpl(final ElementImpl other) {
147
        this(null, other);
×
148
    }
×
149

150
    public ElementImpl(final Expression expression, final ElementImpl other) {
151
        super(expression, other);
1✔
152
        this.children = other.children;
1✔
153
        this.attributes = other.attributes;
1✔
154
        this.namespaceMappings = other.namespaceMappings;
1✔
155
        this.indexType = other.indexType;
1✔
156
        this.position = other.position;
1✔
157
    }
1✔
158

159
    /**
160
     * Reset this element to its initial state.
161
     */
162
    @Override
163
    public void clear() {
164
        super.clear();
1✔
165
        attributes = 0;
1✔
166
        children = 0;
1✔
167
        position = 0;
1✔
168
        namespaceMappings = null;
1✔
169
        //TODO : reset below as well ? -pb
170
        //indexType
171
        //preserveWS
172
    }
1✔
173

174
    public void setIndexType(final int idxType) {
175
        this.indexType = idxType;
1✔
176
    }
1✔
177

178
    public int getIndexType() {
179
        return indexType;
1✔
180
    }
181

182
    @Override
183
    public boolean isDirty() {
184
        return isDirty;
1✔
185
    }
186

187
    @Override
188
    public void setDirty(final boolean dirty) {
189
        this.isDirty = dirty;
1✔
190
    }
1✔
191

192
    public void setPosition(final int position) {
193
        this.position = position;
1✔
194
    }
1✔
195

196
    public int getPosition() {
197
        return position;
1✔
198
    }
199

200
    public boolean declaresNamespacePrefixes() {
201
        return namespaceMappings != null && !namespaceMappings.isEmpty();
1!
202
    }
203

204
    /**
205
     * Serializes a (persistent DOM) Element to a byte array
206
     *
207
     * data = signature childCount nodeIdUnitsLength nodeId attributesCount localNameId namespace? prefixData?
208
     *
209
     * signature = [byte] 0x20 | localNameType | hasNamespace? | isDirty?
210
     *
211
     * localNameType = noContent OR intContent OR shortContent OR byteContent
212
     * noContent = 0x0
213
     * intContent = 0x1
214
     * shortContent = 0x2
215
     * byteContent = 0x3
216
     *
217
     * hasNamespace = 0x10
218
     *
219
     * isDirty = 0x8
220
     *
221
     * childCount = [int] (4 bytes) The number of child nodes
222
     *
223
     * nodeIdUnitsLength = [short] (2 bytes) The number of units of the element's NodeId
224
     * nodeId = @see org.exist.numbering.DLNBase#serialize(byte[], int)
225
     *
226
     * attributesCount = [short] (2 bytes) The number of attributes
227
     *
228
     * localNameId = [int] (4 bytes) | [short] (2 bytes) | [byte] 1 byte. The Id of the element's local name from SymbolTable (symbols.dbx)
229
     *
230
     * namespace = namespaceUriId namespacePrefixLength elementNamespacePrefix?
231
     * namespaceUriId = [short] (2 bytes) The Id of the namespace URI from SymbolTable (symbols.dbx)
232
     * namespacePrefixLength = [short] (2 bytes)
233
     * elementNamespacePrefix = eUtf8
234
     *
235
     * eUtf8 = {@link org.exist.util.UTF8#encode(java.lang.String, byte[], int)}
236
     *
237
     * prefixData = namespaceMappingsCount namespaceMapping+
238
     * namespaceMappingsCount = [short] (2 bytes)
239
     * namespaceMapping = namespacePrefix namespaceUriId
240
     * namespacePrefix = jUtf8
241
     *
242
     * jUtf8 = {@link java.io.DataOutputStream#writeUTF(java.lang.String)}
243
     *
244
     * @return the returned byte array after use must be returned to the ByteArrayPool
245
     *     by calling {@link ByteArrayPool#releaseByteArray(byte[])}
246
     */
247
    @Override
248
    public byte[] serialize() {
249
        if(nodeId == null) {
1!
250
            throw new RuntimeException("nodeId = null for element: " +
×
251
                getQName().getStringValue());
×
252
        }
253

254
        try {
255
            final SymbolTable symbols = ownerDocument.getBrokerPool().getSymbols();
1✔
256
            final byte[] prefixData;
257
            // serialize namespace prefixes declared in this element
258
            if(declaresNamespacePrefixes()) {
1✔
259
                try(final UnsynchronizedByteArrayOutputStream bout = new UnsynchronizedByteArrayOutputStream(64);
1✔
260
                    final DataOutputStream out = new DataOutputStream(bout)) {
1✔
261
                    out.writeShort(namespaceMappings.size());
1✔
262
                    for (final Map.Entry<String, String> namespaceMapping : namespaceMappings.entrySet()) {
1✔
263
                        //TODO(AR) could store the prefix from the symbol table
264
                        out.writeUTF(namespaceMapping.getKey());
1✔
265
                        final short nsId = symbols.getNSSymbol(namespaceMapping.getValue());
1✔
266
                        out.writeShort(nsId);
1✔
267
                    }
268
                    prefixData = bout.toByteArray();
1✔
269
                }
270
            } else {
271
                prefixData = null;
1✔
272
            }
273

274
            final short id = symbols.getSymbol(this);
1✔
275
            final boolean hasNamespace = nodeName.hasNamespace();
1✔
276
            short nsId = 0;
1✔
277
            if(hasNamespace) {
1✔
278
                nsId = symbols.getNSSymbol(nodeName.getNamespaceURI());
1✔
279
            }
280
            final byte idSizeType = Signatures.getSizeType(id);
1✔
281
            byte signature = (byte) ((Signatures.Elem << 0x5) | idSizeType);
1✔
282
            int prefixLen = 0;
1✔
283
            if(hasNamespace) {
1✔
284
                if(nodeName.getPrefix() != null && nodeName.getPrefix().length() > 0) {
1✔
285
                    //TODO(AR) could store the prefix from the symbol table
286
                    prefixLen = UTF8.encoded(nodeName.getPrefix());
1✔
287
                }
288
                signature |= 0x10;
1✔
289
            }
290
            if(isDirty) {
1✔
291
                signature |= 0x8;
1✔
292
            }
293
            final int nodeIdLen = nodeId.size();
1✔
294
            final byte[] data =
1✔
295
                ByteArrayPool.getByteArray(
1✔
296
                    StoredNode.LENGTH_SIGNATURE_LENGTH + LENGTH_ELEMENT_CHILD_COUNT +
1✔
297
                        NodeId.LENGTH_NODE_ID_UNITS +
298
                        nodeIdLen + LENGTH_ATTRIBUTES_COUNT +
1✔
299
                        Signatures.getLength(idSizeType) +
1✔
300
                        (hasNamespace ? prefixLen + 4 : 0) +
1✔
301
                        (prefixData != null ? prefixData.length : 0)
1✔
302
                );
303
            int next = 0;
1✔
304
            data[next] = signature;
1✔
305
            next += StoredNode.LENGTH_SIGNATURE_LENGTH;
1✔
306
            ByteConversion.intToByte(children, data, next);
1✔
307
            next += LENGTH_ELEMENT_CHILD_COUNT;
1✔
308
            ByteConversion.shortToByte((short) nodeId.units(), data, next);
1✔
309
            next += NodeId.LENGTH_NODE_ID_UNITS;
1✔
310
            nodeId.serialize(data, next);
1✔
311
            next += nodeIdLen;
1✔
312
            ByteConversion.shortToByte(attributes, data, next);
1✔
313
            next += LENGTH_ATTRIBUTES_COUNT;
1✔
314
            Signatures.write(idSizeType, id, data, next);
1✔
315
            next += Signatures.getLength(idSizeType);
1✔
316
            if(hasNamespace) {
1✔
317
                ByteConversion.shortToByte(nsId, data, next);
1✔
318
                next += LENGTH_NS_ID;
1✔
319
                ByteConversion.shortToByte((short) prefixLen, data, next);
1✔
320
                next += LENGTH_PREFIX_LENGTH;
1✔
321
                if(nodeName.getPrefix() != null && nodeName.getPrefix().length() > 0) {
1✔
322
                    UTF8.encode(nodeName.getPrefix(), data, next);
1✔
323
                }
324
                next += prefixLen;
1✔
325
            }
326
            if(prefixData != null) {
1✔
327
                System.arraycopy(prefixData, 0, data, next, prefixData.length);
1✔
328
            }
329
            return data;
1✔
330
        } catch(final IOException e) {
×
331
            LOG.error(e);
×
332
            return null;
×
333
        }
334
    }
335

336
    public static StoredNode deserialize(final byte[] data, final int start, final int len,
337
            final DocumentImpl doc, final boolean pooled) {
338
        final int end = start + len;
1✔
339
        int pos = start;
1✔
340
        final byte idSizeType = (byte) (data[pos] & 0x03);
1✔
341
        boolean isDirty = (data[pos] & 0x8) == 0x8;
1✔
342
        final boolean hasNamespace = (data[pos] & 0x10) == 0x10;
1✔
343
        pos += StoredNode.LENGTH_SIGNATURE_LENGTH;
1✔
344
        final int children = ByteConversion.byteToInt(data, pos);
1✔
345
        pos += LENGTH_ELEMENT_CHILD_COUNT;
1✔
346
        final int dlnLen = ByteConversion.byteToShort(data, pos);
1✔
347
        pos += NodeId.LENGTH_NODE_ID_UNITS;
1✔
348
        final NodeId dln = doc.getBrokerPool().getNodeFactory().createFromData(dlnLen, data, pos);
1✔
349
        pos += dln.size();
1✔
350
        final short attributes = ByteConversion.byteToShort(data, pos);
1✔
351
        pos += LENGTH_ATTRIBUTES_COUNT;
1✔
352
        final short id = (short) Signatures.read(idSizeType, data, pos);
1✔
353
        pos += Signatures.getLength(idSizeType);
1✔
354
        short nsId = 0;
1✔
355
        String prefix = null;
1✔
356
        if(hasNamespace) {
1✔
357
            nsId = ByteConversion.byteToShort(data, pos);
1✔
358
            pos += LENGTH_NS_ID;
1✔
359
            int prefixLen = ByteConversion.byteToShort(data, pos);
1✔
360
            pos += LENGTH_PREFIX_LENGTH;
1✔
361
            if(prefixLen > 0) {
1✔
362
                prefix = UTF8.decode(data, pos, prefixLen).toString();
1✔
363
            }
364
            pos += prefixLen;
1✔
365
        }
366
        final String name = doc.getBrokerPool().getSymbols().getName(id);
1✔
367
        String namespace = XMLConstants.NULL_NS_URI;
1✔
368
        if(nsId != 0) {
1✔
369
            namespace = doc.getBrokerPool().getSymbols().getNamespace(nsId);
1✔
370
        }
371

372
        final ElementImpl node;
373
        if(pooled) {
1!
374
            node = (ElementImpl) NodePool.getInstance().borrowNode(Node.ELEMENT_NODE);
×
375
        } else {
×
376
            node = new ElementImpl((Expression) null);
1✔
377
        }
378
        node.setNodeId(dln);
1✔
379
        node.nodeName = doc.getBrokerPool().getSymbols().getQName(Node.ELEMENT_NODE, namespace, name, prefix);
1✔
380
        node.children = children;
1✔
381
        node.attributes = attributes;
1✔
382
        node.isDirty = isDirty;
1✔
383
        node.setOwnerDocument(doc);
1✔
384
        //TO UNDERSTAND : why is this code here ?
385
        if(end > pos) {
1✔
386
            final byte[] pfxData = new byte[end - pos];
1✔
387
            System.arraycopy(data, pos, pfxData, 0, end - pos);
1✔
388
            final InputStream bin = new UnsynchronizedByteArrayInputStream(pfxData);
1✔
389
            final DataInputStream in = new DataInputStream(bin);
1✔
390
            try {
391
                final short prefixCount = in.readShort();
1✔
392
                for(int i = 0; i < prefixCount; i++) {
1✔
393
                    prefix = in.readUTF();
1✔
394
                    nsId = in.readShort();
1✔
395
                    node.addNamespaceMapping(prefix, doc.getBrokerPool().getSymbols().getNamespace(nsId));
1✔
396
                }
397
            } catch(final IOException e) {
1✔
398
                LOG.error(e);
×
399
            }
400
        }
401
        return node;
1✔
402
    }
403

404
    public static QName readQName(final Value value, final DocumentImpl document, final NodeId nodeId) {
405
        final byte[] data = value.data();
1✔
406
        int offset = value.start();
1✔
407
        final byte idSizeType = (byte) (data[offset] & 0x03);
1✔
408
        final boolean hasNamespace = (data[offset] & 0x10) == 0x10;
1✔
409
        offset += StoredNode.LENGTH_SIGNATURE_LENGTH;
1✔
410
        offset += LENGTH_ELEMENT_CHILD_COUNT;
1✔
411
        offset += NodeId.LENGTH_NODE_ID_UNITS;
1✔
412
        offset += nodeId.size();
1✔
413
        offset += LENGTH_ATTRIBUTES_COUNT;
1✔
414
        final short id = (short) Signatures.read(idSizeType, data, offset);
1✔
415
        offset += Signatures.getLength(idSizeType);
1✔
416
        short nsId = 0;
1✔
417
        String prefix = null;
1✔
418
        if(hasNamespace) {
1✔
419
            nsId = ByteConversion.byteToShort(data, offset);
1✔
420
            offset += LENGTH_NS_ID;
1✔
421
            int prefixLen = ByteConversion.byteToShort(data, offset);
1✔
422
            offset += LENGTH_PREFIX_LENGTH;
1✔
423
            if(prefixLen > 0) {
1!
424
                prefix = UTF8.decode(data, offset, prefixLen).toString();
×
425
            }
426
            offset += prefixLen;
1✔
427
        }
428
        final String name = document.getBrokerPool().getSymbols().getName(id);
1✔
429
        final String namespace;
430
        if(nsId != 0) {
1✔
431
            namespace = document.getBrokerPool().getSymbols().getNamespace(nsId);
1✔
432
        } else {
1✔
433
            namespace = XMLConstants.NULL_NS_URI;
1✔
434
        }
435
        return new QName(name, namespace, prefix == null ? XMLConstants.DEFAULT_NS_PREFIX : prefix);
1!
436
    }
437

438
    public static void readNamespaceDecls(final List<String[]> namespaces, final Value value, final DocumentImpl document, final NodeId nodeId) {
439
        final byte[] data = value.data();
1✔
440
        int offset = value.start();
1✔
441
        final int end = offset + value.getLength();
1✔
442
        final byte idSizeType = (byte) (data[offset] & 0x03);
1✔
443
        final boolean hasNamespace = (data[offset] & 0x10) == 0x10;
1✔
444
        offset += StoredNode.LENGTH_SIGNATURE_LENGTH;
1✔
445
        offset += LENGTH_ELEMENT_CHILD_COUNT;
1✔
446
        offset += NodeId.LENGTH_NODE_ID_UNITS;
1✔
447
        offset += nodeId.size();
1✔
448
        offset += LENGTH_ATTRIBUTES_COUNT;
1✔
449
        offset += Signatures.getLength(idSizeType);
1✔
450
        if(hasNamespace) {
1✔
451
            offset += LENGTH_NS_ID;
1✔
452
            int prefixLen = ByteConversion.byteToShort(data, offset);
1✔
453
            offset += LENGTH_PREFIX_LENGTH;
1✔
454
            offset += prefixLen;
1✔
455
        }
456
        if(end > offset) {
1✔
457
            final byte[] pfxData = new byte[end - offset];
1✔
458
            System.arraycopy(data, offset, pfxData, 0, end - offset);
1✔
459
            final InputStream bin = new UnsynchronizedByteArrayInputStream(pfxData);
1✔
460
            final DataInputStream in = new DataInputStream(bin);
1✔
461
            try {
462
                final short prefixCount = in.readShort();
1✔
463
                String prefix;
464
                short nsId;
465
                for(int i = 0; i < prefixCount; i++) {
1✔
466
                    prefix = in.readUTF();
1✔
467
                    nsId = in.readShort();
1✔
468
                    namespaces.add(new String[]{prefix, document.getBrokerPool().getSymbols().getNamespace(nsId)});
1✔
469
                }
470
            } catch(final IOException e) {
1✔
471
                LOG.error(e);
×
472
            }
473
        }
474
    }
1✔
475

476
    public void addNamespaceMapping(final String prefix, final String ns) {
477
        if (prefix == null) {
1!
478
            return;
×
479
        }
480

481
        if (namespaceMappings == null) {
1✔
482
            namespaceMappings = new HashMap<>(1);
1✔
483
        } else if (namespaceMappings.containsKey(prefix)) {
1!
484
            return;
×
485
        }
486

487
        namespaceMappings.put(prefix, ns);
1✔
488
    }
1✔
489

490
    /**
491
     * Append a child to this node. This method does not rearrange the
492
     * node tree and is only used internally by the parser.
493
     * @param prevNode node to append child to
494
     * @param child node to append
495
     *
496
     * @throws DOMException in case of a DOM error
497
     */
498
    public void appendChildInternal(final IStoredNode prevNode, final NodeHandle child) throws DOMException {
499
        final NodeId childId;
500
        if(prevNode == null) {
1✔
501
            childId = getNodeId().newChild();
1✔
502
        } else {
1✔
503
            if(prevNode.getNodeId() == null) {
1!
504
                LOG.warn("{} : {}", getQName(), prevNode.getNodeName());
×
505
            }
506
            childId = prevNode.getNodeId().nextSibling();
1✔
507
        }
508
        child.setNodeId(childId);
1✔
509
        ++children;
1✔
510
    }
1✔
511

512
    @Override
513
    public Node appendChild(final Node newChild) throws DOMException {
514
        if(newChild.getNodeType() != Node.DOCUMENT_NODE && newChild.getOwnerDocument() != null && newChild.getOwnerDocument() != ownerDocument) {
×
515
            throw new DOMException(DOMException.WRONG_DOCUMENT_ERR, "Owning document IDs do not match");
×
516
        }
517

518
        if(newChild == this) {
×
519
            throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR,
×
520
                    "Cannot append an element to itself");
×
521
        }
522

523
        if(newChild.getNodeType() == DOCUMENT_NODE) {
×
524
            throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR,
×
525
                        "A Document Node may not be appended to an element");
×
526
        }
527

528
        if(newChild.getNodeType() == DOCUMENT_TYPE_NODE) {
×
529
            throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR,
×
530
                    "A Document Type Node may not be appended to an element");
×
531
        }
532

533
        if(newChild instanceof IStoredNode) {
×
534
            final NodeId newChildId = ((IStoredNode)newChild).getNodeId();
×
535
            if(newChildId != null && getNodeId().isDescendantOf(newChildId)) {
×
536
                throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR,
×
537
                        "The node to append is one of this node's ancestors");
×
538
            }
539
        }
540

541
        final TransactionManager transact = ownerDocument.getBrokerPool().getTransactionManager();
×
542
        final org.exist.dom.NodeListImpl nl = new org.exist.dom.NodeListImpl();
×
543
        nl.add(newChild);
×
544
        try(final DBBroker broker = ownerDocument.getBrokerPool().getBroker();
×
545
                final Txn transaction = transact.beginTransaction()) {
×
546
            appendChildren(transaction, nl, 0);
×
547
            broker.storeXMLResource(transaction, getOwnerDocument());
×
548
            transact.commit(transaction); // bugID 3419602
×
549
            return getLastChild();
×
550
        } catch(final Exception e) {
×
551
            throw new DOMException(DOMException.INVALID_STATE_ERR, e.getMessage());
×
552
        }
553
    }
554

555
    private void appendAttributes(final Txn transaction, final NodeListImpl attribs) throws DOMException {
556
        final NodeList duplicateAttrs = findDupAttributes(attribs);
1✔
557
        removeAppendAttributes(transaction, duplicateAttrs, attribs);
1✔
558
    }
1✔
559

560
    private NodeList checkForAttributes(final Txn transaction, final NodeList nodes) throws DOMException {
561
        org.exist.dom.NodeListImpl attribs = null;
1✔
562
        org.exist.dom.NodeListImpl rest = null;
1✔
563
        for(int i = 0; i < nodes.getLength(); i++) {
1✔
564
            final Node next = nodes.item(i);
1✔
565
            if(next.getNodeType() == Node.ATTRIBUTE_NODE) {
1✔
566
                if(!next.getNodeName().startsWith(XMLConstants.XMLNS_ATTRIBUTE)) {
1!
567
                    if(attribs == null) {
1!
568
                        attribs = new org.exist.dom.NodeListImpl();
1✔
569
                    }
570
                    attribs.add(next);
1✔
571
                }
572
            } else if(attribs != null) {
1!
573
                if(rest == null) {
×
574
                    rest = new org.exist.dom.NodeListImpl();
×
575
                }
576
                rest.add(next);
×
577
            }
578
        }
579
        if(attribs == null) {
1✔
580
            return nodes;
1✔
581
        }
582
        appendAttributes(transaction, attribs);
1✔
583
        return rest;
1✔
584
    }
585

586
    @Override
587
    public void appendChildren(final Txn transaction, NodeList nodes, final int child) throws DOMException {
588
        // attributes are handled differently. Call checkForAttributes to extract them.
589
        nodes = checkForAttributes(transaction, nodes);
1✔
590
        if(nodes == null || nodes.getLength() == 0) {
1!
591
            return;
1✔
592
        }
593

594
        try(final DBBroker broker = ownerDocument.getBrokerPool().getBroker()) {
1✔
595
            final NodePath path = getPath();
1✔
596
            StreamListener listener = null;
1✔
597
            final IndexController indexes = broker.getIndexController();
1✔
598
            //May help getReindexRoot() to make some useful things
599
            indexes.setDocument(ownerDocument);
1✔
600
            final IStoredNode reindexRoot = indexes.getReindexRoot(this, path, true, true);
1✔
601
            indexes.setMode(ReindexMode.STORE);
1✔
602
            // only reindex if reindexRoot is an ancestor of the current node
603
            if(reindexRoot == null) {
1!
604
                listener = indexes.getStreamListener();
1✔
605
            }
606
            if(children == 0) {
1✔
607
                // no children: append a new child
608
                appendChildren(transaction, nodeId.newChild(), null, new NodeImplRef(this), path, nodes, listener);
1✔
609
            } else {
1✔
610
                if(child == 1) {
1✔
611
                    final Node firstChild = getFirstChild();
1✔
612
                    insertBefore(transaction, nodes, firstChild);
1✔
613
                } else {
1✔
614
                    if(child > 1 && child <= children) {
1!
615
                        final NodeList cl = getAttrsAndChildNodes();
1✔
616
                        final IStoredNode<?> last = (IStoredNode<?>) cl.item(child - 2);
1✔
617
                        insertAfter(transaction, nodes, last);
1✔
618
                    } else {
1✔
619
                        final IStoredNode<?> last = (IStoredNode<?>) getLastChild(true);
1✔
620
                        appendChildren(transaction, last.getNodeId().nextSibling(), null,
1✔
621
                            new NodeImplRef(getLastNode(last)), path, nodes, listener);
1✔
622
                    }
623
                }
624
            }
625
            broker.updateNode(transaction, this, false);
1✔
626
            indexes.reindex(transaction, reindexRoot, ReindexMode.STORE);
1✔
627
            broker.flush();
1✔
628
        } catch(final EXistException e) {
×
629
            LOG.warn("Exception while appending child node: {}", e.getMessage(), e);
×
630
        }
631
    }
1✔
632

633
    /**
634
     * Internal append.
635
     *
636
     * @throws DOMException
637
     */
638
    private void appendChildren(final Txn transaction,
639
            NodeId newNodeId, final NodeId followingId,
640
            final NodeImplRef last, final NodePath lastPath,
641
            final NodeList nodes, final StreamListener listener) throws DOMException {
642

643
        if(last == null || last.getNode() == null || last.getNode().getOwnerDocument() == null) {
1!
644
            throw new DOMException(DOMException.INVALID_MODIFICATION_ERR, "invalid node");
×
645
        }
646
        children += nodes.getLength();
1✔
647
        for(int i = 0; i < nodes.getLength(); i++) {
1✔
648
            final Node child = nodes.item(i);
1✔
649
            appendChild(transaction, newNodeId, last, lastPath, child, listener);
1✔
650
            NodeId next = newNodeId.nextSibling();
1✔
651
            if(followingId != null && next.equals(followingId)) {
1✔
652
                next = newNodeId.insertNode(followingId);
1✔
653
                if(LOG.isDebugEnabled()) {
1!
654
                    LOG.debug("Node ID collision on {}. Using {} instead.", followingId, next);
×
655
                }
656
            }
657
            newNodeId = next;
1✔
658
        }
659
    }
1✔
660

661
    private QName attrName(Attr attr) {
662
        final String ns = attr.getNamespaceURI();
1✔
663
        final String prefix = (Namespaces.XML_NS.equals(ns) ? XMLConstants.XML_NS_PREFIX : attr.getPrefix());
1✔
664
        String name = attr.getLocalName();
1✔
665
        if(name == null) {
1✔
666
            name = attr.getName();
1✔
667
        }
668
        return new QName(name, ns, prefix);
1✔
669
    }
670

671
    private Node appendChild(final Txn transaction, final NodeId newNodeId, final NodeImplRef last, final NodePath lastPath, final Node child, final StreamListener listener)
672
        throws DOMException {
673
        if(last == null || last.getNode() == null) {
1!
674
            throw new DOMException(DOMException.INVALID_MODIFICATION_ERR, "invalid node");
×
675
        }
676
        final DocumentImpl owner = getOwnerDocument();
1✔
677
        try(final DBBroker broker = ownerDocument.getBrokerPool().getBroker()) {
1✔
678
            switch(child.getNodeType()) {
1!
679

680
                case Node.DOCUMENT_FRAGMENT_NODE:
681
                    appendChildren(transaction, newNodeId, null, last, lastPath,
×
682
                        child.getChildNodes(), listener);
×
683
                    return null; // TODO: implement document fragments so
×
684
                //we can return all newly appended children
685

686
                case Node.ELEMENT_NODE:
687
                    // create new element
688
                    final ElementImpl elem =
1✔
689
                        new ElementImpl(getExpression(),
1✔
690
                            new QName(child.getLocalName() == null ?
1✔
691
                                child.getNodeName() : child.getLocalName(),
1✔
692
                                child.getNamespaceURI(),
1✔
693
                                child.getPrefix()),
1✔
694
                            broker.getBrokerPool().getSymbols()
1✔
695
                        );
696
                    elem.setNodeId(newNodeId);
1✔
697
                    elem.setOwnerDocument(owner);
1✔
698
                    final org.exist.dom.NodeListImpl ch = new org.exist.dom.NodeListImpl();
1✔
699
                    final NamedNodeMap attribs = child.getAttributes();
1✔
700
                    int numActualAttribs = 0;
1✔
701
                    for(int i = 0; i < attribs.getLength(); i++) {
1✔
702
                        final Attr attr = (Attr) attribs.item(i);
1✔
703
                        if(!attr.getNodeName().startsWith(XMLConstants.XMLNS_ATTRIBUTE)) {
1✔
704
                            ch.add(attr);
1✔
705
                            numActualAttribs++;
1✔
706
                        } else {
1✔
707
                            final String xmlnsDecl = attr.getNodeName();
1✔
708
                            final String prefix = xmlnsDecl.length() == 5 ? XMLConstants.DEFAULT_NS_PREFIX : xmlnsDecl.substring(6);
1!
709
                            elem.addNamespaceMapping(prefix, attr.getNodeValue());
1✔
710
                        }
711
                    }
712
                    final NodeList cl = child.getChildNodes();
1✔
713
                    for(int i = 0; i < cl.getLength(); i++) {
1✔
714
                        final Node n = cl.item(i);
1✔
715
                        ch.add(n);
1✔
716
                    }
717
                    elem.setChildCount(ch.getLength());
1✔
718
                    if(numActualAttribs != (short) numActualAttribs) {
1!
719
                        throw new DOMException(DOMException.INVALID_MODIFICATION_ERR, "Too many attributes");
×
720
                    }
721
                    elem.setAttributes((short) numActualAttribs);
1✔
722
                    lastPath.addComponent(elem.getQName());
1✔
723
                    // insert the node
724
                    broker.insertNodeAfter(transaction, last.getNode(), elem);
1✔
725
                    broker.indexNode(transaction, elem, lastPath);
1✔
726
                    final IndexController indexes = broker.getIndexController();
1✔
727
                    indexes.startIndexDocument(transaction, listener);
1✔
728
                    try {
729
                        indexes.indexNode(transaction, elem, lastPath, listener);
1✔
730
                        elem.setChildCount(0);
1✔
731
                        last.setNode(elem);
1✔
732
                        //process child nodes
733
                        elem.appendChildren(transaction, newNodeId.newChild(), null, last, lastPath, ch, listener);
1✔
734
                        broker.endElement(elem, lastPath, null);
1✔
735
                        indexes.endElement(transaction, elem, lastPath, listener);
1✔
736
                    } finally {
1✔
737
                        indexes.endIndexDocument(transaction, listener);
1✔
738
                    }
739
                    lastPath.removeLastComponent();
1✔
740
                    return elem;
1✔
741

742
                case Node.TEXT_NODE:
743
                    final TextImpl text = new TextImpl(getExpression(), newNodeId, ((Text)child).getData());
1✔
744
                    text.setOwnerDocument(owner);
1✔
745
                    // insert the node
746
                    broker.insertNodeAfter(transaction, last.getNode(), text);
1✔
747
                    broker.indexNode(transaction, text, lastPath);
1✔
748
                    broker.getIndexController().indexNode(transaction, text, lastPath, listener);
1✔
749
                    last.setNode(text);
1✔
750
                    return text;
1✔
751

752
                case Node.CDATA_SECTION_NODE:
753
                    final CDATASectionImpl cdata = new CDATASectionImpl(getExpression(), newNodeId, ((CDATASection)child).getData());
1✔
754
                    cdata.setOwnerDocument(owner);
1✔
755
                    // insert the node
756
                    broker.insertNodeAfter(transaction, last.getNode(), cdata);
1✔
757
                    broker.indexNode(transaction, cdata, lastPath);
1✔
758
                    last.setNode(cdata);
1✔
759
                    return cdata;
1✔
760

761
                case Node.ATTRIBUTE_NODE:
762
                    final Attr attr = (Attr) child;
1✔
763
                    final QName attrName = attrName(attr);
1✔
764
                    final AttrImpl attrib = new AttrImpl(getExpression(), attrName, attr.getValue(), broker.getBrokerPool().getSymbols());
1✔
765
                    attrib.setNodeId(newNodeId);
1✔
766
                    attrib.setOwnerDocument(owner);
1✔
767
                    if(attrName.getNamespaceURI() != null && attrName.compareTo(Namespaces.XML_ID_QNAME) == Constants.EQUAL) {
1!
768
                        // an xml:id attribute. Normalize the attribute and set its type to ID
769
                        attrib.setValue(StringValue.trimWhitespace(StringValue.collapseWhitespace(attrib.getValue())));
1✔
770
                        attrib.setType(AttrImpl.ID);
1✔
771
                    } else {
1✔
772
                        attrib.setQName(new QName(attrib.getQName(), ElementValue.ATTRIBUTE));
1✔
773
                    }
774
                    broker.insertNodeAfter(transaction, last.getNode(), attrib);
1✔
775
                    broker.indexNode(transaction, attrib, lastPath);
1✔
776
                    broker.getIndexController().indexNode(transaction, attrib, lastPath, listener);
1✔
777
                    last.setNode(attrib);
1✔
778
                    return attrib;
1✔
779

780
                case Node.COMMENT_NODE:
781
                    final CommentImpl comment = new CommentImpl(getExpression(), ((Comment)child).getData());
×
782
                    comment.setNodeId(newNodeId);
×
783
                    comment.setOwnerDocument(owner);
×
784
                    // insert the node
785
                    broker.insertNodeAfter(transaction, last.getNode(), comment);
×
786
                    broker.indexNode(transaction, comment, lastPath);
×
787
                    last.setNode(comment);
×
788
                    return comment;
×
789

790
                case Node.PROCESSING_INSTRUCTION_NODE:
791
                    final ProcessingInstructionImpl pi =
×
792
                        new ProcessingInstructionImpl(getExpression(), newNodeId,
×
793
                            ((ProcessingInstruction)child).getTarget(),
×
794
                            ((ProcessingInstruction)child).getData());
×
795
                    pi.setOwnerDocument(owner);
×
796
                    //insert the node
797
                    broker.insertNodeAfter(transaction, last.getNode(), pi);
×
798
                    broker.indexNode(transaction, pi, lastPath);
×
799
                    last.setNode(pi);
×
800
                    return pi;
×
801

802
                default:
803
                    throw new DOMException(DOMException.INVALID_MODIFICATION_ERR, "Unknown node type: " + child.getNodeType() + " " + child.getNodeName());
×
804
            }
805
        } catch(final EXistException e) {
×
806
            LOG.warn("Exception while appending node: {}", e.getMessage(), e);
×
807
        }
808

809
        return null;
×
810
    }
811

812
    @Override
813
    public boolean hasAttributes() {
814
        return attributes > 0;
1✔
815
    }
816

817
    public void setAttributes(final short attribNum) {
818
        attributes = attribNum;
1✔
819
    }
1✔
820

821
    @Override
822
    public String getAttribute(final String name) {
823
        final Attr attr = findAttribute(name);
1✔
824
        return attr != null ? attr.getValue() : "";
1✔
825
    }
826

827
    @Override
828
    public String getAttributeNS(final String namespaceURI, final String localName) {
829
        final Attr attr = findAttribute(new QName(localName, namespaceURI));
1✔
830
        return attr != null ? attr.getValue() : "";
1✔
831
    }
832

833
    @Deprecated //move as soon as getAttributeNS null issue resolved 
834
    public String _getAttributeNS(final String namespaceURI, final String localName) {
835
        final Attr attr = findAttribute(new QName(localName, namespaceURI));
×
836
        return attr != null ? attr.getValue() : null;
×
837
    }
838

839
    @Override
840
    public Attr getAttributeNode(final String name) {
841
        return findAttribute(name);
1✔
842
    }
843

844
    @Override
845
    public Attr getAttributeNodeNS(final String namespaceURI, final String localName) {
846
        return findAttribute(new QName(localName, namespaceURI));
1✔
847
    }
848

849
    @Override
850
    public NamedNodeMap getAttributes() {
851
        final org.exist.dom.NamedNodeMapImpl map = new NamedNodeMapImpl(ownerDocument, true);
1✔
852
        if(hasAttributes()) {
1✔
853
            try(final DBBroker broker = ownerDocument.getBrokerPool().getBroker();
1✔
854
                final INodeIterator iterator = broker.getNodeIterator(this)) {
1✔
855

856
                iterator.next();    // skip self
1✔
857
                final int childCount = getChildCount();
1✔
858
                for(int i = 0; i < childCount; i++) {
1✔
859
                    final IStoredNode next = iterator.next();
1✔
860
                    if (next == null) {
1!
861
                        LOG.warn("Miscounted getChildCount() index: {} was null of: {}", i, childCount);
×
862
                        continue;
×
863
                    }
864
                    if(next.getNodeType() != Node.ATTRIBUTE_NODE) {
1✔
865
                        break;
1✔
866
                    }
867
                    map.setNamedItem(next);
1✔
868
                }
869
            } catch(final EXistException | IOException e) {
×
870
                LOG.warn("Exception while retrieving attributes: {}", e.getMessage());
×
871
            }
872
        }
873
        if(declaresNamespacePrefixes()) {
1✔
874
            for (final Map.Entry<String, String> entry : namespaceMappings.entrySet()) {
1✔
875
                final String prefix = entry.getKey();
1✔
876
                final String ns = entry.getValue();
1✔
877
                final QName attrName = new QName(prefix, Namespaces.XMLNS_NS, XMLConstants.XMLNS_ATTRIBUTE);
1✔
878
                final AttrImpl attr = new AttrImpl(getExpression(), attrName, ns, null);
1✔
879
                attr.setOwnerDocument(ownerDocument);
1✔
880
                map.setNamedItem(attr);
1✔
881
            }
882
        }
883
        return map;
1✔
884
    }
885

886
    private AttrImpl findAttribute(final String qname) {
887
        try(final DBBroker broker  = ownerDocument.getBrokerPool().getBroker();
1✔
888
                final INodeIterator iterator = broker.getNodeIterator(this)) {
1✔
889
            iterator.next();
1✔
890
            return findAttribute(qname, iterator, this);
1✔
891
        } catch(final EXistException | IOException e) {
×
892
            LOG.warn("Exception while retrieving attributes: {}", e.getMessage());
×
893
        }
894
        return null;
×
895
    }
896

897
    private AttrImpl findAttribute(final String qname, final INodeIterator iterator, final IStoredNode current) {
898
        final int childCount = current.getChildCount();
1✔
899
        IStoredNode next;
900
        for(int i = 0; i < childCount; i++) {
1✔
901
            next = iterator.next();
1✔
902
            if(next.getNodeType() != Node.ATTRIBUTE_NODE) {
1✔
903
                break;
1✔
904
            }
905
            if(next.getNodeName().equals(qname)) {
1✔
906
                return (AttrImpl) next;
1✔
907
            }
908
        }
909
        return null;
1✔
910
    }
911

912
    private AttrImpl findAttribute(final QName qname) {
913
        try(final DBBroker broker = ownerDocument.getBrokerPool().getBroker();
1✔
914
                final INodeIterator iterator = broker.getNodeIterator(this)) {
1✔
915
            iterator.next();
1✔
916
            return findAttribute(qname, iterator, this);
1✔
917
        } catch(final EXistException | IOException e) {
×
918
            LOG.warn("Exception while retrieving attributes: {}", e.getMessage());
×
919
        }
920
        return null;
×
921
    }
922

923
    private AttrImpl findAttribute(final QName qname, final INodeIterator iterator, final IStoredNode current) {
924
        final int childCount = current.getChildCount();
1✔
925
        for(int i = 0; i < childCount; i++) {
1✔
926
            final IStoredNode next = iterator.next();
1✔
927
            if(next.getNodeType() != Node.ATTRIBUTE_NODE) {
1✔
928
                break;
1✔
929
            }
930
            if(next.getQName().equals(qname)) {
1✔
931
                return (AttrImpl) next;
1✔
932
            }
933
        }
934
        return null;
1✔
935
    }
936

937
    /**
938
     * Returns a list of all attribute nodes in attrs that are already present
939
     * in the current element.
940
     *
941
     * @param attrs
942
     * @return The attributes list
943
     * @throws DOMException
944
     */
945
    private NodeList findDupAttributes(final NodeList attrs) throws DOMException {
946
        org.exist.dom.NodeListImpl dupList = null;
1✔
947
        final NamedNodeMap map = getAttributes();
1✔
948
        for(int i = 0; i < attrs.getLength(); i++) {
1✔
949
            final Node attr = attrs.item(i);
1✔
950
            //Workaround: Xerces sometimes returns null for getLocalPart() !!!!
951
            String localName = attr.getLocalName();
1✔
952
            if(localName == null) {
1✔
953
                localName = attr.getNodeName();
1✔
954
            }
955
            final Node duplicate = map.getNamedItemNS(attr.getNamespaceURI(), localName);
1✔
956
            if(duplicate != null) {
1✔
957
                if(dupList == null) {
1!
958
                    dupList = new org.exist.dom.NodeListImpl();
1✔
959
                }
960
                dupList.add(duplicate);
1✔
961
            }
962
        }
963
        return dupList;
1✔
964
    }
965

966
    @Override
967
    public int getChildCount() {
968
        return children;
1✔
969
    }
970

971
    @Override
972
    public boolean hasChildNodes() {
973
        return children - attributes > 0;
1✔
974
    }
975

976
    @Override
977
    public NodeList getChildNodes() {
978
        final int childNodesLen = children - attributes;
1✔
979
        final org.exist.dom.NodeListImpl childList = new org.exist.dom.NodeListImpl(childNodesLen);
1✔
980
        if (childNodesLen > 0) {
1✔
981
            getChildren(false, childList);
1✔
982
        }
983
        return childList;
1✔
984
    }
985

986
    /**
987
     * Similar to {@link #getChildNodes()} but also includes attributes
988
     *
989
     * @return Attributes and child nodes
990
     */
991
    private NodeList getAttrsAndChildNodes() {
992
        final org.exist.dom.NodeListImpl childList = new org.exist.dom.NodeListImpl(children);
1✔
993
        if (children > 0) {
1!
994
            getChildren(true, childList);
1✔
995
        }
996
        return childList;
1✔
997
    }
998

999
    private void getChildren(final boolean includeAttributes, final org.exist.dom.NodeListImpl childList) {
1000
        try (final DBBroker broker = ownerDocument.getBrokerPool().getBroker()) {
1✔
1001
            final int thisLevel = nodeId.getTreeLevel();
1✔
1002
            final int childLevel = thisLevel + 1;
1✔
1003
            for (final IEmbeddedXMLStreamReader reader = broker.getXMLStreamReader(this, includeAttributes); reader.hasNext(); ) {
1!
1004
                final int status = reader.next();
1✔
1005
                final NodeId otherId = (NodeId) reader.getProperty(ExtendedXMLStreamReader.PROPERTY_NODE_ID);
1✔
1006
                final int otherLevel = otherId.getTreeLevel();
1✔
1007

1008
                //NOTE(AR): The order of the checks below has been carefully chosen to optimize non-empty children, which is likely the most common case!
1009

1010
                // skip descendants
1011
                if (otherLevel > childLevel) {
1✔
1012
                    continue;
1✔
1013
                }
1014

1015
                if (status == XMLStreamConstants.END_ELEMENT) {
1✔
1016
                    if (otherLevel == thisLevel) {
1✔
1017
                        // finished `this` element...
1018
                        break;  // exit-for
1✔
1019
                    }
1020
                    // skip over any other END_ELEMENT(s)
1021
                } else {
1022
                    if (otherLevel == childLevel) {
1✔
1023
                        // child
1024
                        childList.add(reader.getNode());
1✔
1025
                    }
1026
                }
1027
            }
1028
        } catch(final IOException | XMLStreamException | EXistException e) {
×
1029
            LOG.warn("Internal error while reading child nodes: {}", e.getMessage(), e);
×
1030
        }
1031
    }
1✔
1032

1033
    @Override
1034
    public NodeList getElementsByTagName(final String name) {
1035
        if(name != null && name.equals(QName.WILDCARD)) {
1!
1036
            return getElementsByTagName(new QName.WildcardLocalPartQName(XMLConstants.DEFAULT_NS_PREFIX));
×
1037
        } else {
1038
            try {
1039
                return getElementsByTagName(new QName(name));
1✔
1040
            } catch (final IllegalQNameException e) {
×
1041
                throw new DOMException(DOMException.INVALID_CHARACTER_ERR, "name is invalid");
×
1042
            }
1043
        }
1044
    }
1045

1046
    @Override
1047
    public NodeList getElementsByTagNameNS(final String namespaceURI, final String localName) {
1048
        final boolean wildcardNS = namespaceURI != null && namespaceURI.equals(QName.WILDCARD);
1!
1049
        final boolean wildcardLocalPart = localName != null && localName.equals(QName.WILDCARD);
1!
1050

1051
        if(wildcardNS && wildcardLocalPart) {
1!
1052
            return getElementsByTagName(QName.WildcardQName.getInstance());
×
1053
        } else if(wildcardNS) {
1!
1054
            return getElementsByTagName(new QName.WildcardNamespaceURIQName(localName));
×
1055
        } else if(wildcardLocalPart) {
1!
1056
            return getElementsByTagName(new QName.WildcardLocalPartQName(namespaceURI));
×
1057
        } else {
1058
            return getElementsByTagName(new QName(localName, namespaceURI));
1✔
1059
        }
1060
    }
1061

1062
    private NodeList getElementsByTagName(final QName qname) {
1063
        return getOwnerDocument().findElementsByTagName(this, qname);
1✔
1064
    }
1065

1066
    @Override
1067
    public Node getFirstChild() {
1068
        if(!hasChildNodes()) {
1✔
1069
            return null;
1✔
1070
        }
1071

1072
        try(final DBBroker broker = ownerDocument.getBrokerPool().getBroker();
1✔
1073
                final INodeIterator iterator = broker.getNodeIterator(this)) {
1✔
1074
            iterator.next();
1✔
1075
            IStoredNode next;
1076
            for(int i = 0; i < getChildCount(); i++) {
1!
1077
                next = iterator.next();
1✔
1078
                if(next.getNodeType() != Node.ATTRIBUTE_NODE) {
1✔
1079
                    return next;
1✔
1080
                }
1081
            }
1082
        } catch(final EXistException | IOException e) {
1!
1083
            LOG.warn("Exception while retrieving child node: {}", e.getMessage(), e);
×
1084
        }
1085
        return null;
×
1086
    }
1087

1088
    @Override
1089
    public Node getLastChild() {
1090
        return getLastChild(false);
×
1091
    }
1092

1093
    /**
1094
     * Get the last child.
1095
     *
1096
     * @param attributesAreChildren In the DLN model attributes have child node ids,
1097
     *     however in the DOM model attributes are not child nodes. Set true for DLN
1098
     *     or false for DOM.
1099
     *
1100
     * @return the last child.
1101
     */
1102
    private Node getLastChild(final boolean attributesAreChildren) {
1103
        if ((!attributesAreChildren) && (!hasChildNodes())) {
1!
1104
            // DOM model
1105
            return null;
×
1106
        } else if (!(hasChildNodes() || hasAttributes())) {
1!
1107
            // DLN model
1108
            return null;
×
1109
        }
1110

1111
        Node node = null;
1✔
1112
        if (!isDirty) {
1✔
1113
            final NodeId child = nodeId.getChild(children);
1✔
1114
            node = ownerDocument.getNode(new NodeProxy(getExpression(), ownerDocument, child));
1✔
1115
        }
1116
        if (node == null) {
1✔
1117
            final NodeList cl;
1118
            if (!attributesAreChildren) {
1!
1119
                // DOM model
1120
                cl = getChildNodes();
×
1121
            } else {
×
1122
                // DLN model
1123
                cl = getAttrsAndChildNodes();
1✔
1124
            }
1125
            return cl.item(cl.getLength() - 1);
1✔
1126
        }
1127
        return node;
1✔
1128
    }
1129

1130
    @Override
1131
    public String getTagName() {
1132
        return nodeName.getStringValue();
1✔
1133
    }
1134

1135
    @Override
1136
    public boolean hasAttribute(final String name) {
1137
        return findAttribute(name) != null;
1✔
1138
    }
1139

1140
    @Override
1141
    public boolean hasAttributeNS(final String namespaceURI, final String localName) {
1142
        return findAttribute(new QName(localName, namespaceURI)) != null;
×
1143
    }
1144

1145
    @Override
1146
    public String getTextContent() throws DOMException {
1147
        //TODO : parametrize the boolean value ?
1148
        try(final DBBroker broker = ownerDocument.getBrokerPool().getBroker()) {
1✔
1149
            return broker.getNodeValue(this, false);
1✔
1150
        } catch(final EXistException e) {
×
1151
            LOG.warn("Exception while reading node value: {}", e.getMessage(), e);
×
1152
        }
1153
        return "";
×
1154
    }
1155

1156
    @Override
1157
    public void removeAttribute(final String name) throws DOMException {
1158
        final Attr attr = getAttributeNode(name);
×
1159
        if(attr == null) {
×
1160
            return;
×
1161
        }
1162

1163
        removeAttributeNode(attr);
×
1164
    }
×
1165

1166
    @Override
1167
    public void removeAttributeNS(final String namespaceURI, final String name) throws DOMException {
1168
        final Attr attr = getAttributeNodeNS(namespaceURI, name);
×
1169
        if(attr == null) {
×
1170
            return;
×
1171
        }
1172
    }
×
1173

1174
    @Override
1175
    public Attr removeAttributeNode(final Attr oldAttr) throws DOMException {
1176
        try(final DBBroker broker = ownerDocument.getBrokerPool().getBroker();
×
1177
            final Txn transaction = broker.getBrokerPool().getTransactionManager().beginTransaction()) {
×
1178
            try {
1179
                if (!(oldAttr instanceof IStoredNode<?> old)) {
×
1180
                    throw new DOMException(DOMException.WRONG_DOCUMENT_ERR, "Wrong node type");
×
1181
                }
1182
                if (!old.getNodeId().isChildOf(nodeId)) {
×
1183
                    throw new DOMException(DOMException.NOT_FOUND_ERR, "node " +
×
1184
                            old.getNodeId().getParentId() +
×
1185
                            " is not a child of element " + nodeId);
×
1186
                }
1187
                final NodePath oldPath = old.getPath();
×
1188
                // remove old custom indexes
1189
                final IndexController indexes = broker.getIndexController();
×
1190
                indexes.reindex(transaction, old,
×
1191
                        ReindexMode.REMOVE_SOME_NODES);
×
1192
                broker.removeNode(transaction, old, oldPath, null);
×
1193
                children--;
×
1194
                attributes--;
×
1195
            } finally {
×
1196
                broker.endRemove(transaction);
×
1197
            }
1198
        } catch (final EXistException e) {
×
1199
            LOG.error(e);
×
1200
            throw new DOMException(DOMException.INVALID_ACCESS_ERR, e.getMessage());
×
1201
        }
1202

1203
        return oldAttr;
×
1204
    }
1205

1206
    @Override
1207
    public void setAttribute(final String name, final String value) throws DOMException {
1208
        final QName qname;
1209
        try {
1210
            qname = new QName(name);
×
1211
        } catch (final IllegalQNameException e) {
×
1212
            throw new DOMException(DOMException.INVALID_CHARACTER_ERR, "name is invalid");
×
1213
        }
1214

1215
        // check the QName is valid for use
1216
        if(qname.isValid(false) != QName.Validity.VALID.val) {
×
1217
            throw new DOMException(DOMException.INVALID_CHARACTER_ERR, "name is invalid");
×
1218
        }
1219

1220
        setAttribute(qname, value, qn -> getAttributeNode(qn.getLocalPart()));
×
1221
    }
×
1222

1223
    @Override
1224
    public void setAttributeNS(final String namespaceURI, final String qualifiedName, final String value) throws DOMException {
1225
        final QName qname;
1226
        try {
1227
            qname = QName.parse(namespaceURI, qualifiedName);
×
1228
        } catch (final IllegalQNameException e) {
×
1229
            final short errCode;
1230
            if(e.getValidity() == ILLEGAL_FORMAT.val || (e.getValidity() & QName.Validity.INVALID_NAMESPACE.val) == QName.Validity.INVALID_NAMESPACE.val) {
×
1231
                errCode = DOMException.NAMESPACE_ERR;
×
1232
            } else {
×
1233
                errCode = DOMException.INVALID_CHARACTER_ERR;
×
1234
            }
1235
            throw new DOMException(errCode, "qualified name is invalid");
×
1236
        }
1237

1238
        // check the QName is valid for use
1239
        final byte validity = qname.isValid(false);
×
1240
        if((validity & QName.Validity.INVALID_LOCAL_PART.val) == QName.Validity.INVALID_LOCAL_PART.val) {
×
1241
            throw new DOMException(DOMException.INVALID_CHARACTER_ERR, "qualified name is invalid");
×
1242
        } else if((validity & QName.Validity.INVALID_NAMESPACE.val) == QName.Validity.INVALID_NAMESPACE.val) {
×
1243
            throw new DOMException(DOMException.NAMESPACE_ERR, "qualified name is invalid");
×
1244
        }
1245

1246
        setAttribute(qname, value, qn -> getAttributeNodeNS(qn.getNamespaceURI(), qn.getLocalPart()));
×
1247
    }
×
1248

1249
    private void setAttribute(final QName attrName, final String value, final Function<QName, Attr> getFn) {
1250
        final Attr existingAttr = getFn.apply(attrName);
×
1251
        if(existingAttr != null) {
×
1252

1253
            // update an existing attribute
1254

1255
            existingAttr.setValue(value);
×
1256

1257
            try(final DBBroker broker = ownerDocument.getBrokerPool().getBroker();
×
1258
                final Txn transaction = broker.getBrokerPool().getTransactionManager().beginTransaction()) {
×
1259

1260
                if (!(existingAttr instanceof IStoredNode<?> existing)) {
×
1261
                    throw new DOMException(DOMException.WRONG_DOCUMENT_ERR, "Wrong node type");
×
1262
                }
1263
                if (!existing.getNodeId().isChildOf(nodeId)) {
×
1264
                    throw new DOMException(DOMException.NOT_FOUND_ERR, "node " +
×
1265
                            existing.getNodeId().getParentId() +
×
1266
                            " is not a child of element " + nodeId);
×
1267
                }
1268

1269
                // update old custom indexes
1270
                final IndexController indexes = broker.getIndexController();
×
1271
                indexes.reindex(transaction, existing, ReindexMode.STORE);
×
1272

1273
                broker.updateNode(transaction, existing, true);
×
1274

1275
                transaction.commit();
×
1276
            } catch (final EXistException e) {
×
1277
                LOG.error(e);
×
1278
                throw new DOMException(DOMException.INVALID_ACCESS_ERR, e.getMessage());
×
1279
            }
1280
        } else {
1281

1282
            // create a new attribute
1283

1284
            try(final DBBroker broker = ownerDocument.getBrokerPool().getBroker()) {
×
1285

1286
                final AttrImpl attrib = new AttrImpl(getExpression(), attrName, value, broker.getBrokerPool().getSymbols());
×
1287
                appendChild(attrib);
×
1288
            } catch (final EXistException e) {
×
1289
                LOG.error(e);
×
1290
                throw new DOMException(DOMException.INVALID_ACCESS_ERR, e.getMessage());
×
1291
            }
1292
        }
1293
    }
×
1294

1295
    @Override
1296
    public Attr setAttributeNode(final Attr newAttr) throws DOMException {
1297
        return setAttributeNode(newAttr, qname -> getAttributeNode(qname.getLocalPart()));
×
1298
    }
1299

1300
    @Override
1301
    public Attr setAttributeNodeNS(final Attr newAttr) {
1302
        return setAttributeNode(newAttr, qname -> getAttributeNodeNS(qname.getNamespaceURI(), qname.getLocalPart()));
×
1303
    }
1304

1305
    private Attr setAttributeNode(final Attr newAttr, final Function<QName, Attr> getFn) {
1306
        final QName attrName = new QName(newAttr.getLocalName(), newAttr.getNamespaceURI(), newAttr.getPrefix(), ElementValue.ATTRIBUTE);
×
1307
        final Attr existingAttr = getFn.apply(attrName);
×
1308
        if (existingAttr != null) {
×
1309
            if(existingAttr.equals(newAttr)) {
×
1310
                return newAttr;
×
1311
            }
1312

1313
            // update an existing attribute
1314
            existingAttr.setValue(newAttr.getValue());
×
1315

1316
            try(final DBBroker broker = ownerDocument.getBrokerPool().getBroker();
×
1317
                final Txn transaction = broker.getBrokerPool().getTransactionManager().beginTransaction()) {
×
1318

1319
                if (!(existingAttr instanceof IStoredNode<?> existing)) {
×
1320
                    throw new DOMException(DOMException.WRONG_DOCUMENT_ERR, "Wrong node type");
×
1321
                }
1322
                if (!existing.getNodeId().isChildOf(nodeId)) {
×
1323
                    throw new DOMException(DOMException.NOT_FOUND_ERR, "node " +
×
1324
                            existing.getNodeId().getParentId() +
×
1325
                            " is not a child of element " + nodeId);
×
1326
                }
1327

1328
                // update old custom indexes
1329
                final IndexController indexes = broker.getIndexController();
×
1330
                indexes.reindex(transaction, existing, ReindexMode.STORE);
×
1331

1332
                broker.updateNode(transaction, existing, true);
×
1333

1334
                transaction.commit();
×
1335
            } catch (final EXistException e) {
×
1336
                LOG.error(e);
×
1337
                throw new DOMException(DOMException.INVALID_ACCESS_ERR, e.getMessage());
×
1338
            }
1339

1340
            return existingAttr;
×
1341

1342
        } else {
1343

1344
            // create a new attribute
1345

1346
            try(final DBBroker broker = ownerDocument.getBrokerPool().getBroker()) {
×
1347

1348
                final AttrImpl attrib = new AttrImpl(getExpression(), attrName, newAttr.getValue(), broker.getBrokerPool().getSymbols());
×
1349
                return (Attr)appendChild(attrib);
×
1350
            } catch (final EXistException e) {
×
1351
                LOG.error(e);
×
1352
                throw new DOMException(DOMException.INVALID_ACCESS_ERR, e.getMessage());
×
1353
            }
1354
        }
1355

1356

1357
    }
1358

1359
    public void setChildCount(final int count) {
1360
        children = count;
1✔
1361
    }
1✔
1362

1363
    public void setNamespaceMappings(final Map<String, String> map) {
1364
        if (this.namespaceMappings == null) {
1!
1365
            this.namespaceMappings = new HashMap<>(map);
1✔
1366
        } else {
1✔
1367
            this.namespaceMappings.clear();
×
1368
            this.namespaceMappings.putAll(map);
×
1369
        }
1370
        for (final String ns : namespaceMappings.values()) {
1✔
1371
            ownerDocument.getBrokerPool().getSymbols().getNSSymbol(ns);
1✔
1372
        }
1373
    }
1✔
1374

1375
    public Iterator<String> getPrefixes() {
1376

1377
        if (namespaceMappings == null) {
1!
1378
            return Collections.<String>emptySet().iterator();
×
1379
        }
1380
        return namespaceMappings.keySet().iterator();
1✔
1381
    }
1382

1383
    public String getNamespaceForPrefix(final String prefix) {
1384
        if (namespaceMappings == null) {
1✔
1385
            return null;
1✔
1386
        }
1387
        return namespaceMappings.get(prefix);
1✔
1388
    }
1389

1390
    /**
1391
     * @see java.lang.Object#toString()
1392
     */
1393
    @Override
1394
    public String toString() {
1395
        return toString(true);
1✔
1396
    }
1397

1398
    /**
1399
     */
1400
    @Override
1401
    public String toString(final boolean top) {
1402
        return toString(top, new TreeSet<>());
1✔
1403
    }
1404

1405
    /**
1406
     * Method toString.
1407
     */
1408
    private String toString(final boolean top, final Set<String> namespaces) {
1409
        final StringBuilder buf = new StringBuilder();
1✔
1410
        buf.append('<');
1✔
1411
        buf.append(nodeName);
1✔
1412
        //Remove false to have a verbose output
1413
        //if (top && false) {
1414
        //buf.append(" xmlns:exist=\""+ Namespaces.EXIST_NS + "\"");
1415
        //buf.append(" exist:id=\"");
1416
        //buf.append(getNodeId());
1417
        //buf.append("\" exist:document=\"");
1418
        //buf.append(((DocumentImpl)getOwnerDocument()).getFileURI());
1419
        //buf.append("\"");
1420
        //}
1421
        if(declaresNamespacePrefixes()) {
1!
1422
            // declare namespaces used by this element
1423
            for(final Map.Entry<String, String> namespaceMapping : namespaceMappings.entrySet()) {
×
1424
                final String prefix = namespaceMapping.getKey();
×
1425
                final String namespace = namespaceMapping.getValue();
×
1426
                buf.append(' ').append(XMLConstants.XMLNS_ATTRIBUTE);
×
1427
                if(!prefix.isEmpty()){
×
1428
                    buf
×
1429
                            .append(':')
×
1430
                            .append(prefix);
×
1431
                }
1432
                buf
×
1433
                        .append("=\"")
×
1434
                        .append(namespace)
×
1435
                        .append('"');
×
1436

1437
                namespaces.add(namespace);
×
1438
            }
1439
        }
1440
        if(nodeName.getNamespaceURI().length() > 0
1!
1441
            && (!namespaces.contains(nodeName.getNamespaceURI()))) {
×
1442
            buf.append(' ')
×
1443
                .append(XMLConstants.XMLNS_ATTRIBUTE)
×
1444
                .append(':')
×
1445
                .append(nodeName.getPrefix()).append("=\"")
×
1446
                .append(nodeName.getNamespaceURI())
×
1447
                .append('"');
×
1448
        }
1449

1450
        if(getInternalAddress() == UNKNOWN_NODE_IMPL_ADDRESS) {
1!
1451
            // not yet stored in the database, so we cannot retrieve attribute and child nodes
1452
            buf.append(" ...");
×
1453

1454
        } else {
×
1455
            // retrieve attributes and child nodes from storage
1456

1457
            final NamedNodeMap attrs = getAttributes();
1✔
1458
            for(int i = 0; i < attrs.getLength(); i++) {
1✔
1459
                final Attr attr = (Attr)attrs.item(i);
1✔
1460
                buf.append(' ')
1✔
1461
                        .append(attr.getName())
1✔
1462
                        .append("=\"")
1✔
1463
                        .append(escapeXml(attr))
1✔
1464
                        .append("\"");
1✔
1465
            }
1466

1467
            final StringBuilder children = new StringBuilder();
1✔
1468
            final NodeList childNodes = getChildNodes();
1✔
1469
            for (int i = 0; i < childNodes.getLength(); i++) {
1✔
1470
                final Node child = childNodes.item(i);
1✔
1471
                switch (child.getNodeType()) {
1✔
1472
                    case Node.ELEMENT_NODE:
1473
                        children.append(((ElementImpl) child).toString(false, namespaces));
1✔
1474
                        break;
1✔
1475

1476
                    default:
1477
                        children.append(child.toString());
1✔
1478
                }
1479
            }
1480

1481
            if (childNodes.getLength() > 0) {
1!
1482
                buf.append(">");
1✔
1483
                buf.append(children.toString());
1✔
1484
                buf.append("</").append(nodeName).append(">");
1✔
1485
            } else {
1✔
1486
                buf.append("/>");
×
1487
            }
1488
        }
1489

1490
        return buf.toString();
1✔
1491
    }
1492

1493
    @Override
1494
    public Node insertBefore(final Node newChild, final Node refChild) throws DOMException {
1495
        if(refChild == null) {
×
1496
            return appendChild(newChild);
×
1497
        } else if(!(refChild instanceof IStoredNode)) {
×
1498
            throw new DOMException(DOMException.WRONG_DOCUMENT_ERR, "Wrong node type");
×
1499
        }
1500

1501
        final org.exist.dom.NodeListImpl nl = new org.exist.dom.NodeListImpl();
×
1502
        nl.add(newChild);
×
1503
        final TransactionManager transact = ownerDocument.getBrokerPool().getTransactionManager();
×
1504

1505
        try(final DBBroker broker = ownerDocument.getBrokerPool().getBroker();
×
1506
                final Txn transaction = transact.beginTransaction()) {
×
1507
            insertBefore(transaction, nl, refChild);
×
1508
            broker.storeXMLResource(transaction, getOwnerDocument());
×
1509
            transact.commit(transaction);
×
1510
            return refChild.getPreviousSibling();
×
1511
        } catch(final TransactionException e) {
×
1512
            throw new DOMException(DOMException.NO_MODIFICATION_ALLOWED_ERR, e.getMessage());
×
1513
        } catch(final EXistException e) {
×
1514
            LOG.warn("Exception while inserting node: {}", e.getMessage(), e);
×
1515
        }
1516
        return null;
×
1517
    }
1518

1519
    /**
1520
     * Insert a list of nodes at the position before the reference
1521
     * child.
1522
     */
1523
    @Override
1524
    public void insertBefore(final Txn transaction, final NodeList nodes, final Node refChild) throws DOMException {
1525
        if(refChild == null) {
1!
1526
            //TODO : use NodeImpl.UNKNOWN_NODE_IMPL_GID ? -pb
1527
            appendChildren(transaction, nodes, -1);
×
1528
            return;
×
1529
        } else if(!(refChild instanceof IStoredNode)) {
1!
1530
            throw new DOMException(DOMException.WRONG_DOCUMENT_ERR, "wrong node type");
×
1531
        }
1532

1533
        try(final DBBroker broker = ownerDocument.getBrokerPool().getBroker()) {
1✔
1534
            final NodePath path = getPath();
1✔
1535
            final IndexController indexes = broker.getIndexController();
1✔
1536
            //May help getReindexRoot() to make some useful things
1537
            indexes.setDocument(ownerDocument);
1✔
1538
            final IStoredNode reindexRoot = indexes.getReindexRoot(this, path, true, true);
1✔
1539
            indexes.setMode(ReindexMode.STORE);
1✔
1540

1541
            final StreamListener listener;
1542
            if(reindexRoot == null) {
1!
1543
                listener = indexes.getStreamListener();
1✔
1544
            } else {
1✔
1545
                listener = null;
×
1546
            }
1547

1548
            final IStoredNode<?> following = (IStoredNode<?>) refChild;
1✔
1549
            final IStoredNode<?> previous = (IStoredNode<?>) ((StoredNode<?>) following).getPreviousSibling(true);
1✔
1550
            if(previous == null) {
1✔
1551
                // there's no sibling node before the new node
1552
                final NodeId newId = following.getNodeId().insertBefore();
1✔
1553
                appendChildren(transaction, newId, following.getNodeId(), new NodeImplRef(this),
1✔
1554
                    path, nodes, listener);
1✔
1555
            } else {
1✔
1556
                // insert the new node between the preceding and following sibling
1557
                final NodeId newId = previous.getNodeId().insertNode(following.getNodeId());
1✔
1558
                appendChildren(transaction, newId, following.getNodeId(),
1✔
1559
                    new NodeImplRef(getLastNode(previous)), path, nodes, listener);
1✔
1560
            }
1561
            setDirty(true);
1✔
1562
            broker.updateNode(transaction, this, true);
1✔
1563
            indexes.reindex(transaction, reindexRoot, ReindexMode.STORE);
1✔
1564
            broker.flush();
1✔
1565
        } catch(final EXistException e) {
×
1566
            LOG.warn("Exception while inserting node: {}", e.getMessage(), e);
×
1567
        }
1568
    }
1✔
1569

1570
    /**
1571
     * Insert a list of nodes at the position following the reference
1572
     * child.
1573
     * @param transaction the transaction
1574
     * @param nodes to be inserted
1575
     * @param refChild nodes will be added after
1576
     * @throws DOMException in case of a DOM error
1577
     */
1578
    @Override
1579
    public void insertAfter(final Txn transaction, final NodeList nodes, final Node refChild) throws DOMException {
1580
        if(refChild == null) {
1!
1581
            //TODO : use NodeImpl.UNKNOWN_NODE_IMPL_GID ? -pb
1582
            appendChildren(null, nodes, -1);
×
1583
            return;
×
1584
        } else if(!(refChild instanceof IStoredNode)) {
1!
1585
            throw new DOMException(DOMException.WRONG_DOCUMENT_ERR, "wrong node type: ");
×
1586
        }
1587

1588
        try(final DBBroker broker = ownerDocument.getBrokerPool().getBroker()) {
1✔
1589
            final NodePath path = getPath();
1✔
1590
            final IndexController indexes = broker.getIndexController();
1✔
1591
            //May help getReindexRoot() to make some useful things
1592
            indexes.setDocument(ownerDocument);
1✔
1593
            final IStoredNode reindexRoot = indexes.getReindexRoot(this, path, true, true);
1✔
1594
            indexes.setMode(ReindexMode.STORE);
1✔
1595

1596
            final StreamListener listener;
1597
            if(reindexRoot == null) {
1!
1598
                listener = indexes.getStreamListener();
1✔
1599
            } else {
1✔
1600
                listener = null;
×
1601
            }
1602

1603
            final IStoredNode<?> previous = (IStoredNode<?>) refChild;
1✔
1604
            final IStoredNode<?> following = (IStoredNode<?>) previous.getNextSibling();
1✔
1605
            final NodeId followingId = following == null ? null : following.getNodeId();
1✔
1606
            final NodeId newNodeId = previous.getNodeId().insertNode(followingId);
1✔
1607
            appendChildren(transaction, newNodeId, followingId, new NodeImplRef(getLastNode(previous)), path, nodes, listener);
1✔
1608
            setDirty(true);
1✔
1609
            broker.updateNode(transaction, this, true);
1✔
1610
            indexes.reindex(transaction, reindexRoot, ReindexMode.STORE);
1✔
1611
            broker.flush();
1✔
1612
        } catch(final EXistException e) {
×
1613
            LOG.warn("Exception while inserting node: {}", e.getMessage(), e);
×
1614
        }
1615
    }
1✔
1616

1617
    /**
1618
     * Update the contents of this element. The passed list of nodes
1619
     * becomes the new content.
1620
     * @param transaction the transaction
1621
     * @param newContent the context
1622
     * @throws DOMException in case of a DOM exception
1623
     */
1624
    public void update(final Txn transaction, final NodeList newContent) throws DOMException {
1625
        final NodePath path = getPath();
1✔
1626
        // remove old child nodes
1627
        final NodeList nodes = getAttrsAndChildNodes();
1✔
1628
        try(final DBBroker broker = ownerDocument.getBrokerPool().getBroker()) {
1✔
1629
            final IndexController indexes = broker.getIndexController();
1✔
1630
            //May help getReindexRoot() to make some useful things
1631
            indexes.setDocument(ownerDocument);
1✔
1632
            final IStoredNode reindexRoot = indexes.getReindexRoot(this, path, true, true);
1✔
1633
            indexes.setMode(ReindexMode.REMOVE_SOME_NODES);
1✔
1634
            final StreamListener listener;
1635
            if(reindexRoot == null) {
1!
1636
                listener = indexes.getStreamListener();
1✔
1637
            } else {
1✔
1638
                listener = null;
×
1639
                indexes.reindex(transaction, reindexRoot, ReindexMode.REMOVE_SOME_NODES);
×
1640
            }
1641
            // TODO: fix once range index has been moved to new architecture
1642
            final IStoredNode valueReindexRoot = broker.getValueIndex().getReindexRoot(this, path);
1✔
1643
            broker.getValueIndex().reindex(valueReindexRoot);
1✔
1644
            IStoredNode last = this;
1✔
1645
            int i = nodes.getLength();
1✔
1646
            for(; i > 0; i--) {
1✔
1647
                IStoredNode<?> child = (IStoredNode<?>) nodes.item(i - 1);
1✔
1648
                if(child.getNodeType() == Node.ATTRIBUTE_NODE) {
1✔
1649
                    last = child;
1✔
1650
                    break;
1✔
1651
                }
1652
                if(child.getNodeType() == Node.ELEMENT_NODE) {
1✔
1653
                    path.addComponent(child.getQName());
1✔
1654
                }
1655
                broker.removeAllNodes(transaction, child, path, listener);
1✔
1656
                if(child.getNodeType() == Node.ELEMENT_NODE) {
1✔
1657
                    path.removeLastComponent();
1✔
1658
                }
1659
            }
1660
            indexes.flush();
1✔
1661
            indexes.setMode(ReindexMode.STORE);
1✔
1662
            indexes.getStreamListener();
1✔
1663
            broker.endRemove(transaction);
1✔
1664
            children = i;
1✔
1665
            final NodeId newNodeId = last == this ? nodeId.newChild() : last.getNodeId().nextSibling();
1✔
1666
            //Append new content
1667
            appendChildren(transaction, newNodeId, null, new NodeImplRef(last), path, newContent, listener);
1✔
1668
            broker.updateNode(transaction, this, false);
1✔
1669
            indexes.reindex(transaction, reindexRoot, ReindexMode.STORE);
1✔
1670
            broker.getValueIndex().reindex(valueReindexRoot);
1✔
1671
            broker.flush();
1✔
1672
        } catch(final EXistException e) {
×
1673
            LOG.warn("Exception while inserting node: {}", e.getMessage(), e);
×
1674
        }
1675
    }
1✔
1676

1677
    /**
1678
     * Update a child node. This method will only update the child node
1679
     * but not its potential descendant nodes.
1680
     *
1681
     * @param oldChild to be replaced
1682
     * @param newChild to be added
1683
     *
1684
     * @return the new node
1685
     *
1686
     * @throws DOMException in case of a DOM error
1687
     */
1688
    @Override
1689
    public IStoredNode updateChild(final Txn transaction, final Node oldChild, final Node newChild) throws DOMException {
1690
        if((!(oldChild instanceof IStoredNode<?> oldNode)) || (!(newChild instanceof IStoredNode<?> newNode))) {
1!
1691
            throw new DOMException(DOMException.WRONG_DOCUMENT_ERR, "Wrong node type");
×
1692
        }
1693

1694
        if(!oldNode.getNodeId().getParentId().equals(nodeId)) {
1!
1695
            throw new DOMException(DOMException.NOT_FOUND_ERR,
×
1696
                "Node is not a child of this element");
×
1697
        }
1698

1699
        if(newNode.getNodeType() == Node.ATTRIBUTE_NODE && newNode.getQName().equals(Namespaces.XML_ID_QNAME)) {
1✔
1700
            // an xml:id attribute. Normalize the attribute and set its type to ID
1701
            final AttrImpl attr = (AttrImpl) newNode;
1✔
1702
            attr.setValue(StringValue.trimWhitespace(StringValue.collapseWhitespace(attr.getValue())));
1✔
1703
            attr.setType(AttrImpl.ID);
1✔
1704
        }
1705
        IStoredNode<?> previousNode = (IStoredNode<?>) ((StoredNode<?>) oldNode).getPreviousSibling(true);
1✔
1706
        if(previousNode == null) {
1✔
1707
            previousNode = this;
1✔
1708
        } else {
1✔
1709
            previousNode = getLastNode(previousNode);
1✔
1710
        }
1711
        final NodePath currentPath = getPath();
1✔
1712
        final NodePath oldPath = oldNode.getPath(currentPath);
1✔
1713

1714
        try(final DBBroker broker = ownerDocument.getBrokerPool().getBroker()) {
1✔
1715
            final IndexController indexes = broker.getIndexController();
1✔
1716
            //May help getReindexRoot() to make some useful things
1717
            indexes.setDocument(ownerDocument);
1✔
1718

1719
            //Check if the change affects any ancestor nodes, which then need to be reindexed later
1720
            IStoredNode reindexRoot = indexes.getReindexRoot(oldNode, oldPath, false);
1✔
1721
            indexes.setMode(ReindexMode.REMOVE_SOME_NODES);
1✔
1722
            //Remove indexes
1723
            if(reindexRoot == null) {
1!
1724
                reindexRoot = oldNode;
×
1725
            }
1726
            indexes.reindex(transaction, reindexRoot, ReindexMode.REMOVE_SOME_NODES);
1✔
1727
            //TODO: fix once range index has been moved to new architecture
1728
            final NativeValueIndex valueIndex = broker.getValueIndex();
1✔
1729
            final IStoredNode valueReindexRoot = valueIndex.getReindexRoot(this, oldPath);
1✔
1730
            valueIndex.reindex(valueReindexRoot);
1✔
1731
            //Remove the actual node data
1732
            broker.removeNode(transaction, oldNode, oldPath, null);
1✔
1733
            broker.endRemove(transaction);
1✔
1734
            newNode.setNodeId(oldNode.getNodeId());
1✔
1735

1736
            //Reinsert the new node data
1737
            broker.insertNodeAfter(transaction, previousNode, newNode);
1✔
1738
            final NodePath path = newNode.getPath(currentPath);
1✔
1739
            broker.indexNode(transaction, newNode, path);
1✔
1740
            if(newNode.getNodeType() == Node.ELEMENT_NODE) {
1✔
1741
                broker.endElement(newNode, path, null);
1✔
1742
            }
1743
            broker.updateNode(transaction, this, true);
1✔
1744

1745
            //Recreate indexes on ancestor nodes
1746
            indexes.reindex(transaction, reindexRoot, ReindexMode.STORE);
1✔
1747
            valueIndex.reindex(valueReindexRoot);
1✔
1748
            broker.flush();
1✔
1749
        } catch(final EXistException e) {
×
1750
            LOG.warn("Exception while inserting node: {}", e.getMessage(), e);
×
1751
        }
1752
        return newNode;
1✔
1753
    }
1754

1755
    @Override
1756
    public Node removeChild(final Txn transaction, final Node oldChild) throws DOMException {
1757
        if(!(oldChild instanceof IStoredNode<?> oldNode)) {
1!
1758
            throw new DOMException(DOMException.WRONG_DOCUMENT_ERR, "wrong node type");
×
1759
        }
1760

1761
        if(!oldNode.getNodeId().getParentId().equals(nodeId)) {
1!
1762
            throw new DOMException(DOMException.NOT_FOUND_ERR,
×
1763
                "node is not a child of this element");
×
1764
        }
1765

1766
        final NodePath oldPath = oldNode.getPath();
1✔
1767
        try(final DBBroker broker = ownerDocument.getBrokerPool().getBroker()) {
1✔
1768
            final IndexController indexes = broker.getIndexController();
1✔
1769
            indexes.setDocument(ownerDocument);
1✔
1770
            final IStoredNode reindexRoot = indexes.getReindexRoot(oldNode, oldPath, false);
1✔
1771
            indexes.setMode(ReindexMode.REMOVE_SOME_NODES);
1✔
1772
            final StreamListener listener;
1773
            if(reindexRoot == null) {
1!
1774
                listener = indexes.getStreamListener();
×
1775
            } else {
×
1776
                listener = null;
1✔
1777
                indexes.reindex(transaction, reindexRoot, ReindexMode.REMOVE_SOME_NODES);
1✔
1778
            }
1779
            broker.removeAllNodes(transaction, oldNode, oldPath, listener);
1✔
1780
            --children;
1✔
1781
            if(oldChild.getNodeType() == Node.ATTRIBUTE_NODE) {
1✔
1782
                --attributes;
1✔
1783
            }
1784
            broker.endRemove(transaction);
1✔
1785
            setDirty(true);
1✔
1786
            broker.updateNode(transaction, this, false);
1✔
1787
            broker.flush();
1✔
1788
            if(reindexRoot != null && !reindexRoot.getNodeId().equals(oldNode.getNodeId())) {
1!
1789
                indexes.reindex(transaction, reindexRoot, ReindexMode.STORE);
×
1790
            }
1791
        } catch(final EXistException e) {
×
1792
            LOG.warn("Exception while inserting node: {}", e.getMessage(), e);
×
1793
        }
1794
        return oldNode;
1✔
1795
    }
1796

1797
    public void removeAppendAttributes(final Txn transaction, final NodeList removeList, final NodeList appendList) {
1798

1799
        try(final DBBroker broker = ownerDocument.getBrokerPool().getBroker()) {
1✔
1800
            final IndexController indexes = broker.getIndexController();
1✔
1801
            if(removeList != null) {
1✔
1802
                try {
1803
                    for(int i = 0; i < removeList.getLength(); i++) {
1✔
1804
                        final Node oldChild = removeList.item(i);
1✔
1805
                        if(!(oldChild instanceof IStoredNode<?> old)) {
1!
1806
                            throw new DOMException(DOMException.WRONG_DOCUMENT_ERR, "Wrong node type");
×
1807
                        }
1808
                        if(!old.getNodeId().isChildOf(nodeId)) {
1!
1809
                            throw new DOMException(DOMException.NOT_FOUND_ERR, "node " +
×
1810
                                old.getNodeId().getParentId() +
×
1811
                                " is not a child of element " + nodeId);
×
1812
                        }
1813
                        final NodePath oldPath = old.getPath();
1✔
1814
                        // remove old custom indexes
1815
                        indexes.reindex(transaction, old,
1✔
1816
                                ReindexMode.REMOVE_SOME_NODES);
1✔
1817
                        broker.removeNode(transaction, old, oldPath, null);
1✔
1818
                        children--;
1✔
1819
                        attributes--;
1✔
1820
                    }
1821
                } finally {
1✔
1822
                    broker.endRemove(transaction);
1✔
1823
                }
1824
            }
1825
            final NodePath path = getPath();
1✔
1826
            indexes.setDocument(ownerDocument, ReindexMode.STORE);
1✔
1827
            final IStoredNode reindexRoot = indexes.getReindexRoot(this, path, true, true);
1✔
1828
            final StreamListener listener = reindexRoot == null ? indexes.getStreamListener() : null;
1!
1829
            if (children == 0) {
1✔
1830
                appendChildren(transaction, nodeId.newChild(), null,
1✔
1831
                    new NodeImplRef(this), path, appendList, listener);
1✔
1832
            } else {
1✔
1833
                if(attributes == 0) {
1✔
1834
                    final IStoredNode<?> firstChild = (IStoredNode<?>) getFirstChild();
1✔
1835
                    final NodeId newNodeId = firstChild.getNodeId().insertBefore();
1✔
1836
                    appendChildren(transaction, newNodeId, firstChild.getNodeId(),
1✔
1837
                        new NodeImplRef(this), path, appendList, listener);
1✔
1838
                } else {
1✔
1839
                    final AttribVisitor visitor = new AttribVisitor();
1✔
1840
                    accept(visitor);
1✔
1841
                    final NodeId firstChildId = visitor.firstChild == null ? null : visitor.firstChild.getNodeId();
1✔
1842
                    final NodeId newNodeId = visitor.lastAttrib.getNodeId().insertNode(firstChildId);
1✔
1843
                    appendChildren(transaction, newNodeId, firstChildId, new NodeImplRef(visitor.lastAttrib),
1✔
1844
                        path, appendList, listener);
1✔
1845
                }
1846
                setDirty(true);
1✔
1847
            }
1848
            attributes += appendList.getLength();
1✔
1849

1850
            broker.updateNode(transaction, this, true);
1✔
1851
            broker.flush();
1✔
1852
            indexes.reindex(transaction, reindexRoot,
1✔
1853
                    ReindexMode.STORE);
1✔
1854
        } catch (final EXistException e) {
×
1855
            LOG.warn("Exception while inserting node: {}", e.getMessage(), e);
×
1856
        }
1857
    }
1✔
1858

1859
    private class AttribVisitor implements NodeVisitor {
1✔
1860
        private IStoredNode lastAttrib = null;
1✔
1861
        private IStoredNode firstChild = null;
1✔
1862

1863
        @Override
1864
        public boolean visit(final IStoredNode node) {
1865
            if(node.getNodeType() == Node.ATTRIBUTE_NODE) {
1✔
1866
                lastAttrib = node;
1✔
1867
            } else if(node.getNodeId().isChildOf(ElementImpl.this.nodeId)) {
1✔
1868
                firstChild = node;
1✔
1869
                return false;
1✔
1870
            }
1871
            return true;
1✔
1872
        }
1873
    }
1874

1875
    /**
1876
     * Replaces the oldChild with the newChild
1877
     *
1878
     * @param transaction the transaction
1879
     * @param newChild to replace oldChild
1880
     * @param oldChild to be replaced by newChild
1881
     *
1882
     * @return The new node (this differs from the {@link org.w3c.dom.Node#replaceChild(Node, Node)} specification)
1883
     *
1884
     * @throws DOMException in case of a DOM error
1885
     *
1886
     * @see org.w3c.dom.Node#replaceChild(org.w3c.dom.Node, org.w3c.dom.Node)
1887
     */
1888
    @Override
1889
    public Node replaceChild(final Txn transaction, final Node newChild, final Node oldChild) throws DOMException {
1890
        if(!(oldChild instanceof IStoredNode<?> oldNode)) {
1!
1891
            throw new DOMException(DOMException.WRONG_DOCUMENT_ERR, "Wrong node type");
×
1892
        }
1893
        if(!oldNode.getNodeId().getParentId().equals(nodeId)) {
1!
1894
            throw new DOMException(DOMException.NOT_FOUND_ERR,
×
1895
                "Node is not a child of this element");
×
1896
        }
1897

1898
        final NodePath thisPath = getPath();
1✔
1899
        IStoredNode<?> previous = (IStoredNode<?>) ((StoredNode<?>) oldNode).getPreviousSibling(true);
1✔
1900
        if(previous == null) {
1✔
1901
            previous = this;
1✔
1902
        } else {
1✔
1903
            previous = getLastNode(previous);
1✔
1904
        }
1905
        final NodePath oldPath = oldNode.getPath();
1✔
1906
        StreamListener listener = null;
1✔
1907
        Node newNode = null;
1✔
1908

1909
        try(final DBBroker broker = ownerDocument.getBrokerPool().getBroker()) {
1✔
1910
            final IndexController indexes = broker.getIndexController();
1✔
1911
            //May help getReindexRoot() to make some useful things
1912
            indexes.setDocument(ownerDocument);
1✔
1913
            final IStoredNode reindexRoot = broker.getIndexController().getReindexRoot(oldNode, oldPath, false);
1✔
1914
            indexes.setMode(ReindexMode.REMOVE_SOME_NODES);
1✔
1915
            if(reindexRoot == null) {
1!
1916
                listener = indexes.getStreamListener();
×
1917
            } else {
×
1918
                indexes.reindex(transaction, reindexRoot,
1✔
1919
                        ReindexMode.REMOVE_SOME_NODES);
1✔
1920
            }
1921
            broker.removeAllNodes(transaction, oldNode, oldPath, listener);
1✔
1922
            broker.endRemove(transaction);
1✔
1923
            broker.flush();
1✔
1924
            indexes.setMode(ReindexMode.STORE);
1✔
1925
            listener = indexes.getStreamListener();
1✔
1926
            newNode = appendChild(transaction, oldNode.getNodeId(), new NodeImplRef(previous),
1✔
1927
                thisPath, newChild, listener);
1✔
1928
            //Reindex if required
1929
            broker.storeXMLResource(transaction, getOwnerDocument());
1✔
1930
            broker.updateNode(transaction, this, false);
1✔
1931
            indexes.reindex(transaction, reindexRoot, ReindexMode.STORE);
1✔
1932
            broker.flush();
1✔
1933
        } catch(final EXistException e) {
×
1934
            LOG.warn("Exception while inserting node: {}", e.getMessage(), e);
×
1935
        }
1936
        //return oldChild;        // method is spec'd to return the old child, even though that's probably useless in this case
1937
        return newNode; //returning the newNode is more sensible than returning the oldNode
1✔
1938
    }
1939

1940
    private String escapeXml(final Node child) {
1941
        final String str = ((Attr) child).getValue();
1✔
1942
        StringBuilder buffer = null;
1✔
1943
        String entity;
1944
        char ch;
1945
        for(int i = 0; i < str.length(); i++) {
1✔
1946
            ch = str.charAt(i);
1✔
1947
            entity = switch (ch) {
1!
1948
                case '"' -> "&quot;";
×
1949
                case '<' -> "&lt;";
×
1950
                case '>' -> "&gt;";
×
1951
                case '\'' -> "&apos;";
×
1952
                default -> null;
1✔
1953
            };
1954

1955
            if(buffer == null) {
1!
1956
                if(entity != null) {
1!
1957
                    buffer = new StringBuilder(str.length() + 20);
×
1958
                    buffer.append(str.substring(0, i));
×
1959
                    buffer.append(entity);
×
1960
                }
1961
            } else {
×
1962
                if(entity == null) {
×
1963
                    buffer.append(ch);
×
1964
                } else {
×
1965
                    buffer.append(entity);
×
1966
                }
1967
            }
1968
        }
1969
        return buffer == null ? str : buffer.toString();
1!
1970
    }
1971

1972
    public void setPreserveSpace(final boolean preserveWS) {
1973
        this.preserveWS = preserveWS;
1✔
1974
    }
1✔
1975

1976
    public boolean preserveSpace() {
1977
        return preserveWS;
1✔
1978
    }
1979

1980
    @Override
1981
    public TypeInfo getSchemaTypeInfo() {
1982
        throw unsupported();
×
1983
    }
1984

1985
    @Override
1986
    public void setIdAttribute(final String name, final boolean isId) throws DOMException {
1987
        throw unsupported();
×
1988
    }
1989

1990
    @Override
1991
    public void setIdAttributeNS(final String namespaceURI, final String localName, final boolean isId) throws DOMException {
1992
        throw unsupported();
×
1993
    }
1994

1995
    @Override
1996
    public void setIdAttributeNode(final Attr idAttr, final boolean isId) throws DOMException {
1997
        throw unsupported();
×
1998
    }
1999

2000
    @Override
2001
    public String getBaseURI() {
2002
        final XmldbURI baseURI = calculateBaseURI();
1✔
2003
        if(baseURI != null) {
1!
2004
            return baseURI.toString();
1✔
2005
        }
2006

2007
        return ""; //UNDERSTAND: is it ok?
×
2008
    }
2009

2010
    // NOTE(AR) please keep in sync with org.exist.dom.memtree.ElementImpl
2011
    private XmldbURI calculateBaseURI() {
2012
        XmldbURI baseURI = null;
1✔
2013

2014
        final String nodeBaseURI = getAttributeNS(Namespaces.XML_NS, "base");
1✔
2015
        if (!nodeBaseURI.isEmpty()) {
1✔
2016
            baseURI = XmldbURI.create(nodeBaseURI, false);
1✔
2017
            if (baseURI.isAbsolute()) {
1✔
2018
                return baseURI;
1✔
2019
            }
2020
        }
2021

2022
        final IStoredNode<?> parent = getParentStoredNode();
1✔
2023
        if (parent != null) {
1✔
2024
            if (nodeBaseURI.isEmpty()) {
1✔
2025
                baseURI = ((ElementImpl) parent).calculateBaseURI();
1✔
2026
            } else {
1✔
2027
                final XmldbURI parentsBaseURI = ((ElementImpl) parent).calculateBaseURI();
1✔
2028
                if (parentsBaseURI.toString().endsWith("/") || !parentsBaseURI.toString().contains("/")) {
1!
2029
                    baseURI = parentsBaseURI.append(baseURI);
1✔
2030
                } else {
1✔
2031
                    // there is a filename, remove it
2032
                    baseURI = parentsBaseURI.removeLastSegment().append(baseURI);
1✔
2033
                }
2034
            }
2035
        } else {
1✔
2036
            if (nodeBaseURI.isEmpty()) {
1!
2037
                return XmldbURI.create(getOwnerDocument().getBaseURI(), false);
1✔
2038
            } else {
2039
                final String docBaseURI = getOwnerDocument().getBaseURI();
×
2040
                if (docBaseURI.endsWith("/")) {
×
2041
                    baseURI = XmldbURI.create(getOwnerDocument().getBaseURI(), false);
×
2042
                    baseURI.append(baseURI);
×
2043
                } else {
×
2044
                    baseURI = XmldbURI.create(getOwnerDocument().getBaseURI(), false);
×
2045
                    baseURI = baseURI.removeLastSegment();
×
2046
                    baseURI.append(baseURI);
×
2047
                }
2048
            }
2049
        }
2050

2051
        return baseURI;
1✔
2052
    }
2053

2054
    @Override
2055
    public boolean accept(final INodeIterator iterator, final NodeVisitor visitor) {
2056
        if(!visitor.visit(this)) {
1✔
2057
            return false;
1✔
2058
        }
2059

2060
        if(hasChildNodes() || hasAttributes()) {
1!
2061
            final int childCount = getChildCount();
1✔
2062
            for(int i = 0; i < childCount; i++) {
1✔
2063
                final IStoredNode next = iterator.next();
1✔
2064
                if(!next.accept(iterator, visitor)) {
1✔
2065
                    return false;
1✔
2066
                }
2067
            }
2068
        }
2069
        return true;
1✔
2070
    }
2071

2072
    @Override
2073
    public String lookupNamespaceURI(final String prefix) {
2074

2075
        for (Node pathNode = this; pathNode != null; pathNode = pathNode.getParentNode()) {
1✔
2076
            if (pathNode instanceof ElementImpl) {
1✔
2077
                final String namespaceForPrefix = ((ElementImpl)pathNode).getNamespaceForPrefix(prefix);
1✔
2078
                if (namespaceForPrefix != null) {
1✔
2079
                    return namespaceForPrefix;
1✔
2080
                }
2081
            }
2082
        }
2083

2084
        return XMLConstants.NULL_NS_URI;
1✔
2085
    }
2086
}
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