• 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

83.48
/extensions/indexes/range/src/main/java/org/exist/indexing/range/RangeIndexConfigElement.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.indexing.range;
50

51
import org.apache.lucene.analysis.Analyzer;
52
import org.apache.lucene.document.*;
53
import org.apache.lucene.util.BytesRef;
54
import org.apache.lucene.util.BytesRefBuilder;
55
import org.apache.lucene.util.NumericUtils;
56
import org.exist.dom.QName;
57
import org.exist.indexing.lucene.LuceneIndexConfig;
58
import org.exist.storage.NodePath;
59
import org.exist.util.DatabaseConfigurationException;
60
import org.exist.util.XMLString;
61
import org.exist.xquery.XPathException;
62
import org.exist.xquery.value.*;
63
import org.w3c.dom.Element;
64
import org.w3c.dom.Node;
65

66
import javax.xml.datatype.XMLGregorianCalendar;
67
import java.io.IOException;
68
import java.util.Map;
69

70
import static org.exist.indexing.lucene.LuceneIndexConfig.MATCH_ATTR;
71
import static org.exist.indexing.lucene.LuceneIndexConfig.QNAME_ATTR;
72
import static org.exist.indexing.lucene.LuceneIndexConfig.TYPE_ATTR;
73

74
public class RangeIndexConfigElement {
75

76
    protected final static String FILTER_ELEMENT = "filter";
77

78
    protected NodePath path = null;
1✔
79
    private int type = Type.STRING;
1✔
80
    private RangeIndexConfigElement nextConfig = null;
1✔
81
    protected boolean isQNameIndex = false;
1✔
82
    protected RangeIndexAnalyzer analyzer = new RangeIndexAnalyzer();
1✔
83
    protected boolean includeNested = false;
1✔
84
    protected boolean caseSensitive = true;
1✔
85
    protected boolean usesCollation = false;
1✔
86
    protected int wsTreatment = XMLString.SUPPRESS_NONE;
1✔
87
    private org.exist.indexing.range.conversion.TypeConverter typeConverter = null;
1✔
88

89
    public RangeIndexConfigElement(Element node, Map<String, String> namespaces) throws DatabaseConfigurationException {
1✔
90
        String match = node.getAttribute(MATCH_ATTR);
1✔
91
        if (!match.isEmpty()) {
1✔
92
            try {
93
                path = new NodePath(namespaces, match);
1✔
94
                if (path.length() == 0)
1!
95
                    throw new DatabaseConfigurationException("Range index module: Invalid match path in collection config: " + match);
×
96
            } catch (IllegalArgumentException e) {
×
97
                throw new DatabaseConfigurationException("Range index module: invalid qname in configuration: " + e.getMessage());
×
98
            }
1✔
99
        } else if (node.hasAttribute(QNAME_ATTR)) {
1!
100
            QName qname = LuceneIndexConfig.parseQName(node, namespaces);
1✔
101
            path = new NodePath(NodePath.SKIP);
1✔
102
            path.addComponent(qname);
1✔
103
            isQNameIndex = true;
1✔
104
        }
105
        String typeStr = node.getAttribute(TYPE_ATTR);
1✔
106
        if (!typeStr.isEmpty()) {
1✔
107
            try {
108
                this.type = Type.getType(typeStr);
1✔
109
            } catch (XPathException e) {
×
110
                throw new DatabaseConfigurationException("Invalid type declared for range index on " + match + ": " + typeStr);
×
111
            }
1✔
112
        }
113

114
        parseChildren(node);
1✔
115

116
        String collation = node.getAttribute("collation");
1✔
117
        if (!collation.isEmpty()) {
1✔
118
            analyzer.addCollation(collation);
1✔
119
            usesCollation = true;
1✔
120
        }
121
        String nested = node.getAttribute("nested");
1✔
122
        includeNested = nested.isEmpty() || nested.equalsIgnoreCase("yes");
1!
123

124
        // normalize whitespace if whitespace="normalize"
125
        String whitespace = node.getAttribute("whitespace");
1✔
126
        if ("trim".equalsIgnoreCase(whitespace)) {
1!
127
            wsTreatment = XMLString.SUPPRESS_BOTH;
×
128
        } else if ("normalize".equalsIgnoreCase(whitespace)) {
1✔
129
            wsTreatment = XMLString.NORMALIZE;
1✔
130
        }
131

132
        String caseStr = node.getAttribute("case");
1✔
133
        if (!caseStr.isEmpty()) {
1✔
134
            caseSensitive = caseStr.equalsIgnoreCase("yes");
1✔
135
        }
136
        String custom = node.getAttribute("converter");
1✔
137
        if (!custom.isEmpty()) {
1✔
138
            try {
139
                Class customClass = Class.forName(custom);
1✔
140
                typeConverter = (org.exist.indexing.range.conversion.TypeConverter) customClass.newInstance();
1✔
141
            } catch (ClassNotFoundException e) {
×
142
                RangeIndex.LOG.warn("Class for custom-type not found: {}", custom);
×
143
            } catch (InstantiationException | IllegalAccessException e) {
×
144
                RangeIndex.LOG.warn("Failed to initialize custom-type: {}", custom, e);
×
145
            }
1✔
146
        }
147
    }
1✔
148

149
    private void parseChildren(Node root) throws DatabaseConfigurationException {
150
        Node child = root.getFirstChild();
1✔
151
        while (child != null) {
1✔
152
            if (child.getNodeType() == Node.ELEMENT_NODE) {
1!
153
                if (FILTER_ELEMENT.equals(child.getLocalName())) {
1✔
154
                    analyzer.addFilter((Element) child);
1✔
155
                }
156
            }
157
            child = child.getNextSibling();
1✔
158
        }
159
    }
1✔
160

161
    public Field convertToField(String fieldName, String content) throws IOException {
162
        // check if a converter is defined for this index to handle on-the-fly conversions
163
        final org.exist.indexing.range.conversion.TypeConverter custom = getTypeConverter(fieldName);
1✔
164
        if (custom != null) {
1✔
165
            return custom.toField(fieldName, content);
1✔
166
        }
167
        // no converter: handle default types
168
        final int fieldType = getType(fieldName);
1✔
169
        try {
170
            switch (fieldType) {
1!
171
                case Type.INTEGER:
172
                case Type.LONG:
173
                case Type.UNSIGNED_LONG:
174
                    long lvalue = Long.parseLong(content);
1✔
175
                    return new LongField(fieldName, lvalue, LongField.TYPE_NOT_STORED);
1✔
176
                case Type.INT:
177
                case Type.UNSIGNED_INT:
178
                case Type.SHORT:
179
                case Type.UNSIGNED_SHORT:
180
                    int ivalue = Integer.parseInt(content);
1✔
181
                    return new IntField(fieldName, ivalue, IntField.TYPE_NOT_STORED);
1✔
182
                case Type.DECIMAL:
183
                case Type.DOUBLE:
184
                    double dvalue = Double.parseDouble(content);
×
185
                    return new DoubleField(fieldName, dvalue, DoubleField.TYPE_NOT_STORED);
×
186
                case Type.FLOAT:
187
                    float fvalue = Float.parseFloat(content);
×
188
                    return new FloatField(fieldName, fvalue, FloatField.TYPE_NOT_STORED);
×
189
                case Type.DATE:
190
                    DateValue dv = new DateValue(content);
1✔
191
                    long dl = dateToLong(dv);
1✔
192
                    return new LongField(fieldName, dl, LongField.TYPE_NOT_STORED);
1✔
193
                case Type.TIME:
194
                    TimeValue tv = new TimeValue(content);
1✔
195
                    long tl = timeToLong(tv);
1✔
196
                    return new LongField(fieldName, tl, LongField.TYPE_NOT_STORED);
1✔
197
                case Type.DATE_TIME:
198
                    DateTimeValue dtv = new DateTimeValue(content);
1✔
199
                    String dateStr = dateTimeToString(dtv);
1✔
200
                    return new TextField(fieldName, dateStr, Field.Store.NO);
1✔
201
                default:
202
                    return new TextField(fieldName, content, Field.Store.NO);
1✔
203
            }
204
        } catch (NumberFormatException | XPathException e) {
×
205
            // wrong type: ignore
206
        }
207
        return null;
×
208
    }
209

210
    public static BytesRef convertToBytes(final AtomicValue content) throws XPathException {
211
        final BytesRefBuilder bytes = new BytesRefBuilder();
1✔
212
        switch(content.getType()) {
1!
213
            case Type.INTEGER:
214
            case Type.LONG:
215
            case Type.UNSIGNED_LONG:
216
                NumericUtils.longToPrefixCoded(((IntegerValue)content).getLong(), 0, bytes);
1✔
217
                break;
1✔
218

219
            case Type.SHORT:
220
            case Type.UNSIGNED_SHORT:
221
            case Type.INT:
222
            case Type.UNSIGNED_INT:
223
                NumericUtils.intToPrefixCoded(((IntegerValue)content).getInt(), 0, bytes);
1✔
224
                break;
1✔
225

226
            case Type.DECIMAL:
227
                final long dv = NumericUtils.doubleToSortableLong(((DecimalValue)content).getDouble());
×
228
                NumericUtils.longToPrefixCoded(dv, 0, bytes);
×
229
                break;
×
230

231
            case Type.DOUBLE:
232
                final long lv = NumericUtils.doubleToSortableLong(((DoubleValue)content).getDouble());
×
233
                NumericUtils.longToPrefixCoded(lv, 0, bytes);
×
234
                break;
×
235

236
            case Type.FLOAT:
237
                final int iv = NumericUtils.floatToSortableInt(((FloatValue)content).getValue());
×
238
                NumericUtils.longToPrefixCoded(iv, 0, bytes);
×
239
                break;
×
240

241
            case Type.DATE:
242
                final long dl = dateToLong((DateValue)content);
1✔
243
                NumericUtils.longToPrefixCoded(dl, 0, bytes);
1✔
244
                break;
1✔
245

246
            case Type.TIME:
247
                final long tl = timeToLong((TimeValue) content);
1✔
248
                NumericUtils.longToPrefixCoded(tl, 0, bytes);
1✔
249
                break;
1✔
250

251
            case Type.DATE_TIME:
252
                final String dt = dateTimeToString((DateTimeValue) content);
1✔
253
                bytes.copyChars(dt);
1✔
254
                break;
1✔
255

256
            default:
257
                bytes.copyChars(content.getStringValue());
×
258
        }
259
        return bytes.toBytesRef();
1✔
260
    }
261

262
    public static long dateToLong(DateValue date) {
263
        final XMLGregorianCalendar utccal = date.calendar.normalize();
1✔
264
        return ((long)utccal.getYear() << 16) + ((long)utccal.getMonth() << 8) + ((long)utccal.getDay());
1✔
265
    }
266

267
    public static long timeToLong(TimeValue time) {
268
        return time.getTimeInMillis();
1✔
269
    }
270

271
    public static String dateTimeToString(DateTimeValue dtv) {
272
        final XMLGregorianCalendar utccal = dtv.calendar.normalize();
1✔
273
        final StringBuilder sb = new StringBuilder();
1✔
274
        formatNumber(utccal.getMillisecond(), 3, sb);
1✔
275
        formatNumber(utccal.getSecond(), 2, sb);
1✔
276
        formatNumber(utccal.getMinute(), 2, sb);
1✔
277
        formatNumber(utccal.getHour(), 2, sb);
1✔
278
        formatNumber(utccal.getDay(), 2, sb);
1✔
279
        formatNumber(utccal.getMonth(), 2, sb);
1✔
280
        formatNumber(utccal.getYear(), 4, sb);
1✔
281
        return sb.toString();
1✔
282
    }
283

284
    public static void formatNumber(int number, int digits, StringBuilder sb) {
285
        int count = 0;
1✔
286
        long n = number;
1✔
287
        while (n > 0) {
1✔
288
            final int digit = '0' + (int)n % 10;
1✔
289
            sb.insert(0, (char)digit);
1✔
290
            count++;
1✔
291
            if (count == digits) {
1✔
292
                break;
1✔
293
            }
294
            n = n / 10;
1✔
295
        }
1✔
296
        if (count < digits) {
1✔
297
            for (int i = count; i < digits; i++) {
1✔
298
                sb.insert(0, '0');
1✔
299
            }
300
        }
301
    }
1✔
302

303
    public TextCollector getCollector(NodePath path) {
304
        return new SimpleTextCollector(this, includeNested, wsTreatment, caseSensitive);
1✔
305
    }
306

307
    public Analyzer getAnalyzer() {
308
        return analyzer;
1✔
309
    }
310

311
    public Analyzer getAnalyzer(String field) {
312
        return analyzer;
1✔
313
    }
314

315
    public boolean isCaseSensitive(String fieldName) {
316
        return caseSensitive;
1✔
317
    }
318

319
    public boolean usesCollation() {
320
        return usesCollation;
1✔
321
    }
322

323
    public boolean isComplex() {
324
        return false;
1✔
325
    }
326

327
    public int getType(String fieldName) {
328
        // no fields: return type
329
        return type;
1✔
330
    }
331

332
    public int getType() {
333
        return type;
1✔
334
    }
335

336
    public org.exist.indexing.range.conversion.TypeConverter getTypeConverter(String fieldName) {
337
        return typeConverter;
1✔
338
    }
339

340
    public NodePath getNodePath() {
341
        return path;
1✔
342
    }
343

344
    public void add(RangeIndexConfigElement config) {
345
        if (nextConfig == null)
1✔
346
            nextConfig = config;
1✔
347
        else
348
            nextConfig.add(config);
1✔
349
    }
1✔
350

351
    public RangeIndexConfigElement getNext() {
352
        return nextConfig;
1✔
353
    }
354

355
    public boolean match(NodePath other) {
356
        if (isQNameIndex) {
1✔
357
            final QName qn1 = path.getLastComponent();
1✔
358
            final QName qn2 = other.getLastComponent();
1✔
359
            return qn1.getNameType() == qn2.getNameType() && qn2.equals(qn1);
1✔
360
        }
361
        return other.match(path);
1✔
362
    }
363

364
    public boolean find(NodePath other) {
365
        return match(other);
1✔
366
    }
367
}
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

© 2026 Coveralls, Inc