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

eXist-db / exist / 40657517912

28 Oct 2026 08:32AM UTC coverage: 58.121% (-0.08%) from 58.196%
40657517912

push

github

web-flow
Merge pull request #6938 from eXist-db/dependabot/github_actions/actions-0c219a018e

Bump docker/login-action from 4.6.0 to 4.7.0 in the actions group

32462 of 61752 branches covered (52.57%)

Branch coverage included in aggregate %.

85521 of 141244 relevant lines covered (60.55%)

2.72 hits per line

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

77.56
/exist-core/src/main/java/org/exist/storage/NodePath.java
1
/*
2
 * eXist-db Open Source Native XML Database
3
 * Copyright (C) 2001 The eXist-db Authors
4
 *
5
 * info@exist-db.org
6
 * http://www.exist-db.org
7
 *
8
 * This library is free software; you can redistribute it and/or
9
 * modify it under the terms of the GNU Lesser General Public
10
 * License as published by the Free Software Foundation; either
11
 * version 2.1 of the License, or (at your option) any later version.
12
 *
13
 * This library is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16
 * Lesser General Public License for more details.
17
 *
18
 * You should have received a copy of the GNU Lesser General Public
19
 * License along with this library; if not, write to the Free Software
20
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
21
 */
22
package org.exist.storage;
23

24
import java.util.Arrays;
25
import java.util.Map;
26

27
import org.apache.logging.log4j.LogManager;
28
import org.apache.logging.log4j.Logger;
29
import org.exist.dom.QName;
30

31
import javax.annotation.Nullable;
32
import javax.xml.XMLConstants;
33

34

35
/**
36
 * Represents a Node Path.
37
 *
38
 * Internally the node path is held as an array of {@link QName} components.
39
 * Upon construction the array of `components` is sized such that it will be exactly
40
 * what is required to represent the Node Path.
41
 * Mutation operations however will over-allocate the array as an optmisation, so that
42
 * we need not allocate/free memory on every mutation operation, but only
43
 * every {@link #DEFAULT_NODE_PATH_SIZE} operations.
44
 *
45
 * @author <a href="mailto:adam@evolvedbinary.com">Adam Retter</a>
46
 * @author wolf
47
 */
48
public class NodePath implements Comparable<NodePath> {
49

50
    static final int DEFAULT_NODE_PATH_SIZE = 4;
51
    static final int MAX_OVER_ALLOCATION_FACTOR = 2;
52

53
    private static final Logger LOG = LogManager.getLogger(NodePath.class);
3 ✔
54

55
    /**
56
     * (Illegal) QNames used as a marker for arbitrary path steps.
57
     */
58
    public static final QName SKIP = new QName("//", "");
6 ✔
59
    public static final QName WILDCARD = new QName("*", "");
7 ✔
60

61
    private QName[] components = new QName[DEFAULT_NODE_PATH_SIZE];
16 ✔
62
    private int pos = 0;
12 ✔
63
    private boolean includeDescendants = true;
12 ✔
64

65
    public NodePath() {
2 ✔
66
        //Nothing to do
67
    }
1 ✔
68

69
    public NodePath(final NodePath other) {
70
        this(other, other.includeDescendants);
5 ✔
71
    }
1 ✔
72

73
    public NodePath(final NodePath other, final boolean includeDescendants) {
2 ✔
74
        components = new QName[other.components.length];
6 ✔
75
        System.arraycopy(other.components, 0, components, 0, other.components.length);
10 ✔
76
        pos = other.pos;
4 ✔
77
        this.includeDescendants = includeDescendants;
3 ✔
78
    }
1 ✔
79

80
    public NodePath(@Nullable final Map<String, String> namespaces, final String path) {
2 ✔
81
        init(namespaces, path);
4 ✔
82
    }
1 ✔
83

84
    public NodePath(@Nullable final Map<String, String> namespaces, final String path, final boolean includeDescendants) {
2 ✔
85
        this.includeDescendants = includeDescendants;
3 ✔
86
        init(namespaces, path);
4 ✔
87
    }
1 ✔
88

89
    public NodePath(final QName qname) {
×
90
            addComponent(qname);
×
91
    }
×
92

93
    public void setIncludeDescendants(final boolean includeDescendants) {
94
        this.includeDescendants = includeDescendants;
×
95
    }
×
96

97
    public boolean includeDescendants() {
98
        return includeDescendants;
×
99
    }
100

101
    public void append(final NodePath other) {
102
        // do we have enough space to accommodate the components from `other`
103
        final int numOtherComponentsToAppend = other.length();
3 ✔
104
        allocateIfNeeded(numOtherComponentsToAppend);
3 ✔
105

106
        // at this point we have enough space, append the components from `other`
107
        System.arraycopy(other.components, 0, components, pos, numOtherComponentsToAppend);
9 ✔
108
        this.pos += numOtherComponentsToAppend;
6 ✔
109
    }
1 ✔
110

111
    public void addComponent(final QName component) {
112
        // do we have enough space to add the component
113
        allocateIfNeeded(1);
3 ✔
114

115
        // at this point we have enough space, add the component
116
        components[pos++] = component;
11 ✔
117
    }
1 ✔
118

119
    private void allocateIfNeeded(final int numOtherComponentsToAppend) {
120
        final int available = components.length - pos;
7 ✔
121
        if (available < numOtherComponentsToAppend) {
3 ✔
122
            // we need more space, allocate a multiple of DEFAULT_NODE_PATH_SIZE
123
            final int allocationFactor = (int) Math.ceil((numOtherComponentsToAppend - available + components.length) / ((float) DEFAULT_NODE_PATH_SIZE));
14 ✔
124
            final int newSize = allocationFactor * DEFAULT_NODE_PATH_SIZE;
4 ✔
125
            this.components = Arrays.copyOf(components, newSize);
7 ✔
126
        }
127
    }
1 ✔
128

129
    /**
130
     * Remove the Last Component from the NodePath.
131
     *
132
     * NOTE: this does not shrink the storage requirements
133
     * of the NodePath
134
     */
135
    public void removeLastComponent() {
136
        if (pos > 0) {
3 !
137
            components[--pos] = null;
11 ✔
138
        }
139
    }
1 ✔
140

141
    public void reset() {
142
        // when resetting if this object has more than twice the capacity of a new object, then set it back to the default capacity
143
        if (pos > DEFAULT_NODE_PATH_SIZE * MAX_OVER_ALLOCATION_FACTOR) {
4 ✔
144
            components = new QName[DEFAULT_NODE_PATH_SIZE];
4 ✔
145
        } else {
1 ✔
146
            Arrays.fill(components, null);
4 ✔
147
        }
148
        pos = 0;
3 ✔
149
    }
1 ✔
150

151
    public int length() {
152
        return pos;
3 ✔
153
    }
154

155
    /**
156
     * Return the size of the components array.
157
     *
158
     * This function is intentionally marked as package-private
159
     * so that it may only be called for testing purposes!
160
     *
161
     * @return the size of the components array.
162
     */
163
    int componentsSize() {
164
        return components.length;
4 ✔
165
    }
166

167
    protected void reverseComponents() {
168
        for (int i = 0; i < pos / 2; ++i) {
10 ✔
169
            QName tmp = components[i];
5 ✔
170
            components[i] = components[pos - 1 - i];
13 ✔
171
            components[pos - 1 - i] = tmp;
10 ✔
172
        }
173
    }
1 ✔
174

175
    public QName getComponent(final int at) {
176
        if (at < 0 || at >= pos) {
×
177
            throw new ArrayIndexOutOfBoundsException(at);
×
178
        }
179
        return components[at];
×
180
    }
181

182
    public QName getLastComponent() {
183
        if (pos > 0) {
×
184
            return components[pos - 1];
×
185
        }
186
        return null;
×
187
    }
188

189
    public boolean hasWildcard() {
190
        for (int i = 0; i < pos; i++) {
×
191
            if (components[i] == WILDCARD) {
×
192
                return true;
×
193
            }
194
        }
195
        return false;
×
196
    }
197

198
    public boolean match(final QName qname) {
199
        if (pos > 0) {
×
200
            return components[pos - 1].equals(qname);
×
201
        }
202
        return false;
×
203
    }
204

205
    public final boolean match(final NodePath other) {
206
            return match(other, 0);
5 ✔
207
    }
208

209
    public final boolean match(final NodePath other, int j) {
210
        boolean skip = false;
2 ✔
211
        int i = 0;
2 ✔
212
        for ( ; j < other.pos; j++) {
6 ✔
213
            if (i == pos) {
4 ✔
214
                if (includeDescendants) {
3 ✔
215
                    return true;
2 ✔
216
                }
217
                return j == other.pos;
6 !
218
            }
219
            if (components[i] == SKIP) {
6 ✔
220
                ++i;
1 ✔
221
                skip = true;
2 ✔
222
            }
223
            if ((components[i] == WILDCARD || other.components[j].compareTo(components[i]) == 0) &&
16 ✔
224
                    (!skip || j + 1 == other.pos || other.components[j + 1].compareTo(components[i]) != 0)) {
20 ✔
225
                ++i;
1 ✔
226
                skip = false;
2 ✔
227
            } else if (skip) {
3 ✔
228
                continue;
1 ✔
229
            } else {
230
                return false;
2 ✔
231
            }
232
        }
233
        if (i == pos) {
4 ✔
234
            if (includeDescendants) {
3 ✔
235
                return true;
2 ✔
236
            }
237
            return j == other.pos;
6 !
238
        }
239
        return false;
2 ✔
240
    }
241

242
    @Override
243
    public String toString() {
244
        final StringBuilder buf = new StringBuilder();
4 ✔
245
        for (int i = 0; i < pos; i++) {
8 ✔
246

247
            if (!(SKIP.equals(components[i]) || (i > 0 && SKIP.equals(components[i - 1])))) {
18 !
248
                buf.append("/");
4 ✔
249
            }
250

251
            if (components[i].getNameType() == ElementValue.ATTRIBUTE) {
7 !
252
                buf.append("@");
×
253
            }
254

255
            buf.append(components[i]);
7 ✔
256
        }
257
        return buf.toString();
3 ✔
258
    }
259

260
    public void addComponent(@Nullable final Map<String, String> namespaces, final String origComponent) {
261
        String component = origComponent;
2 ✔
262
        boolean isAttribute = false;
2 ✔
263
        if (component.startsWith("@")) {
4 ✔
264
            isAttribute = true;
2 ✔
265
            component = component.substring(1);
4 ✔
266
        }
267
        try {
268
            String prefix = QName.extractPrefix(component);
3 ✔
269
            final String localName = QName.extractLocalName(component);
3 ✔
270
            String namespaceURI = null;
2 ✔
271

272
            //TODO simplify this code (AR)
273
            if (prefix != null) {
2 ✔
274
                namespaceURI = namespaces.get(prefix);
5 ✔
275
                if (namespaceURI == null) {
2 !
276
                    LOG.error("No namespace URI defined for prefix: {}", prefix);
×
277
                    //TODO : throw exception ? -pb
278
                    prefix = null;
×
279
                    namespaceURI = "";
×
280
                }
281
            } else if (namespaces != null) {
2 ✔
282
                namespaceURI = namespaces.get(XMLConstants.DEFAULT_NS_PREFIX);
5 ✔
283
            }
284

285
            final QName qn;
286
            if (isAttribute) {
2 ✔
287
                qn = new QName(localName, namespaceURI, prefix, ElementValue.ATTRIBUTE);
8 ✔
288
            } else {
1 ✔
289
                qn = new QName(localName, namespaceURI, prefix);
7 ✔
290
            }
291
            LOG.debug("URI = {}", qn.getNamespaceURI());
5 ✔
292
            addComponent(qn);
3 ✔
293
        } catch (final QName.IllegalQNameException e) {
1 ✔
294
            throw new IllegalArgumentException(e);
×
295
        }
296
    }
1 ✔
297

298
    private void init(@Nullable final Map<String, String> namespaces, final String path) {
299
        //TODO : compute better length
300
        final StringBuilder token = new StringBuilder(path.length());
6 ✔
301
        int pos = 0;
2 ✔
302
        while (pos < path.length()) {
5 ✔
303
            final char ch = path.charAt(pos);
4 ✔
304
            switch (ch) {
2 ✔
305
            case '*':
306
                addComponent(WILDCARD);
3 ✔
307
                token.setLength(0);
3 ✔
308
                pos++;
1 ✔
309
                break;
1 ✔
310
            case '/':
311
                final String next = token.toString();
3 ✔
312
                token.setLength(0);
3 ✔
313
                if (!next.isEmpty()) {
3 ✔
314
                    addComponent(namespaces, next);
4 ✔
315
                }
316
                if (path.charAt(++pos ) == '/') {
6 ✔
317
                    addComponent(SKIP);
3 ✔
318
                }
319
                break;
1 ✔
320
            default:
321
                token.append(ch);
4 ✔
322
                pos++;
1 ✔
323
            }
324
        }
325
        if (!token.isEmpty()) {
3 ✔
326
            addComponent(namespaces, token.toString());
5 ✔
327
        }
328
    }
1 ✔
329

330
    @Override
331
    public boolean equals(final Object obj) {
332
        if (this == obj) {
3 !
333
            return true;
×
334
        }
335

336
        if (obj instanceof NodePath otherNodePath) {
6 !
337

338
            // NOTE(AR) we cannot use Array.equals on the components of the NodePaths as they may be over-allocated!
339
            if (pos == otherNodePath.pos) {
5 !
340
                for (int i = 0; i < pos; i++) {
8 ✔
341
                    if (!components[i].equals(otherNodePath.components[i])) {
10 !
342
                        return false;
×
343
                    }
344
                }
345
                return true;
2 ✔
346
            }
347
        }
348

349
        return false;
×
350
    }
351

352
    @Override
353
    public int hashCode() {
354
        int h = 0;
×
355
        for (int i = 0; i < pos; i++) {
×
356
            h = 31 * h + components[i].hashCode();
×
357
        }
358
        return h;
×
359
    }
360

361
    @Override
362
    public int compareTo(final NodePath other) {
363
        return toString().compareTo(other.toString()); //TODO: optimize
×
364
    }
365
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc