• 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

50.23
/exist-core/src/main/java/org/exist/collections/CollectionConfigurationManager.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.collections;
50

51
import org.apache.logging.log4j.LogManager;
52
import org.apache.logging.log4j.Logger;
53
import org.exist.EXistException;
54
import org.exist.Namespaces;
55
import org.exist.collections.triggers.TriggerException;
56
import org.exist.dom.persistent.BinaryDocument;
57
import org.exist.dom.persistent.DocumentImpl;
58
import org.exist.dom.memtree.SAXAdapter;
59
import org.exist.security.PermissionDeniedException;
60
import org.exist.storage.*;
61
import org.exist.storage.lock.Lock.LockMode;
62
import org.exist.storage.lock.ManagedLock;
63
import org.exist.storage.txn.TransactionManager;
64
import org.exist.storage.txn.Txn;
65
import org.exist.util.LockException;
66
import org.exist.util.MimeType;
67
import org.exist.util.StringInputSource;
68
import org.exist.util.XMLReaderPool;
69
import org.exist.util.sanity.SanityCheck;
70
import org.exist.xmldb.XmldbURI;
71
import org.exist.xquery.Expression;
72
import org.w3c.dom.Document;
73
import org.xml.sax.InputSource;
74
import org.xml.sax.XMLReader;
75

76
import java.io.IOException;
77
import java.io.StringReader;
78
import java.util.*;
79
import java.util.concurrent.locks.ReadWriteLock;
80

81
/**
82
 * Manages index configurations. Index configurations are stored in a collection
83
 * hierarchy below /db/system/config. CollectionConfigurationManager is called
84
 * by {@link org.exist.collections.Collection} to retrieve the
85
 * {@link org.exist.collections.CollectionConfiguration} instance for a given
86
 * collection.
87
 * 
88
 * @author wolf
89
 */
90
public class CollectionConfigurationManager implements BrokerPoolService {
91

92
    private static final Logger LOG = LogManager.getLogger(CollectionConfigurationManager.class);
1✔
93

94
    public static final String CONFIG_COLLECTION = XmldbURI.SYSTEM_COLLECTION + "/config";
95

96
    /** /db/system/config **/
97
    public static final XmldbURI CONFIG_COLLECTION_URI = XmldbURI.create(CONFIG_COLLECTION);
1✔
98

99
    /** /db/system/config/db **/
100
    public static final XmldbURI ROOT_COLLECTION_CONFIG_URI = CONFIG_COLLECTION_URI.append(XmldbURI.ROOT_COLLECTION_NAME);
1✔
101

102
    public static final CollectionURI COLLECTION_CONFIG_PATH = new CollectionURI(CONFIG_COLLECTION_URI.getRawCollectionPath());
1✔
103

104
    private final Map<CollectionURI, CollectionConfiguration> configurations = new HashMap<>();
1✔
105

106
    private final ReadWriteLock lock = new java.util.concurrent.locks.ReentrantReadWriteLock();
1✔
107

108
    private final CollectionConfiguration defaultConfig;
109

110
    public CollectionConfigurationManager(final BrokerPool brokerPool) {
1✔
111
        this.defaultConfig = new CollectionConfiguration(brokerPool);
1✔
112
    }
1✔
113

114
    @Override
115
    public void startSystem(final DBBroker systemBroker, final Txn transaction) throws BrokerPoolServiceException {
116
        try {
117
            checkCreateCollection(systemBroker, transaction, CONFIG_COLLECTION_URI);
1✔
118
            checkCreateCollection(systemBroker, transaction, ROOT_COLLECTION_CONFIG_URI);
1✔
119
            loadAllConfigurations(systemBroker);
1✔
120
            defaultConfig.setIndexConfiguration(systemBroker.getIndexConfiguration());
1✔
121
        } catch(final EXistException | CollectionConfigurationException | PermissionDeniedException | LockException e) {
1✔
122
            throw new BrokerPoolServiceException(e);
×
123
        }
124
    }
1✔
125

126
    /**
127
     * Add a new collection configuration. The XML document is passed as a
128
     * string.
129
     * 
130
     * @param txn The transaction that will hold the WRITE locks until they are
131
     *            released by commit()/abort()
132
     * @param broker the broker
133
     * @param collection  the collection to which the configuration applies.
134
     * @param config the xconf document as a String.
135
     * @throws CollectionConfigurationException if config is invalid
136
     */
137
    public void addConfiguration(final Txn txn, final DBBroker broker, final Collection collection, final String config) throws CollectionConfigurationException {
138
        try {
139
            final XmldbURI path = CONFIG_COLLECTION_URI.append(collection.getURI());
1✔
140

141
            final Collection confCol = broker.getOrCreateCollection(txn, path);
1✔
142
            if (confCol == null) {
1!
143
                throw new CollectionConfigurationException("Failed to create config collection: " + path);
×
144
            }
145

146
            XmldbURI configurationDocumentName = null;
1✔
147
            // Replaces the current configuration file if there is one
148
            final CollectionConfiguration conf = getConfiguration(collection);
1✔
149
            if (conf != null) {
1!
150
                configurationDocumentName = conf.getDocName();
1✔
151
                if (configurationDocumentName != null) {
1✔
152
                    LOG.warn("Replacing current configuration file '{}'", configurationDocumentName);
1✔
153
                }
154
            }
155
            if (configurationDocumentName == null) {
1✔
156
                configurationDocumentName = CollectionConfiguration.DEFAULT_COLLECTION_CONFIG_FILE_URI;
1✔
157
            }
158

159
            broker.saveCollection(txn, confCol);
1✔
160

161
            broker.storeDocument(txn, configurationDocumentName, new StringInputSource(config), MimeType.XML_TYPE, confCol);
1✔
162

163
            // broker.sync(Sync.MAJOR_SYNC);
164
        } catch (final CollectionConfigurationException e) {
1✔
165
            throw e;
×
166
        } catch (final Exception e) {
×
167
            throw new CollectionConfigurationException("Failed to store collection configuration: " + e.getMessage(), e);
×
168
        }
169
    }
1✔
170

171
    /**
172
     * Check the passed collection configuration. Throws an exception if errors
173
     * are detected in the configuration document. Note: some configuration
174
     * settings depend on the current environment, in particular the
175
     * availability of trigger or index classes.
176
     * 
177
     * @param broker DBBroker
178
     * @param config the configuration to test
179
     * @throws CollectionConfigurationException if errors were detected
180
     */
181
    public void testConfiguration(DBBroker broker, String config) throws CollectionConfigurationException {
182
        try {
183
            final SAXAdapter adapter = new SAXAdapter((Expression) null);
×
184
            final InputSource src = new InputSource(new StringReader(config));
×
185
            final XMLReaderPool parserPool = broker.getBrokerPool().getParserPool();
×
186
            XMLReader reader = null;
×
187
            try {
188
                reader = parserPool.borrowXMLReader();
×
189
                reader.setContentHandler(adapter);
×
190
                reader.setProperty(Namespaces.SAX_LEXICAL_HANDLER, adapter);
×
191
                reader.parse(src);
×
192
            } finally {
×
193
                if (reader != null) {
×
194
                    parserPool.returnXMLReader(reader);
×
195
                }
196
            }
197
            final Document doc = adapter.getDocument();
×
198
            final CollectionConfiguration conf = new CollectionConfiguration(broker.getBrokerPool());
×
199
            conf.read(broker, doc, true, null, null);
×
200
        } catch (final Exception e) {
×
201
            throw new CollectionConfigurationException(e);
×
202
        }
203
    }
×
204

205
    public List<Object> getCustomIndexSpecs(final String customIndexId) {
206
        try(final ManagedLock<ReadWriteLock> readLock = ManagedLock.acquire(lock, LockMode.READ_LOCK)) {
×
207
            final List<Object> configs = new ArrayList<>(10);
×
208
            for (final CollectionConfiguration config: configurations.values()) {
×
209
                final IndexSpec spec = config.getIndexConfiguration();
×
210
                if (spec != null) {
×
211
                    final Object customConfig = spec.getCustomIndexSpec(customIndexId);
×
212
                    if (customConfig != null) {
×
213
                        configs.add(customConfig);
×
214
                    }
215
                }
216
            }
217

218
            return configs;
×
219
        }
220
    }
221

222
    /**
223
     * Retrieve the collection configuration instance for the given collection.
224
     * This creates a new CollectionConfiguration object and recursively scans
225
     * the collection hierarchy for available configurations.
226
     *
227
     * @param collection to retrieve configuration for
228
     * @return The collection configuration
229
     */
230
    protected CollectionConfiguration getConfiguration(final Collection collection) {
231

232
        final CollectionURI path = new CollectionURI(COLLECTION_CONFIG_PATH);
1✔
233
        path.append(collection.getURI().getRawCollectionPath());
1✔
234

235
        /*
236
         * This used to go from the root collection (/db), and continue all the
237
         * way to the end of the path, checking each collection on the way. I
238
         * modified it to start at the collection path and work its way back to
239
         * the root, stopping at the first config file it finds. This should be
240
         * more efficient, and fit more appropriately with the XmldbURI api
241
         */
242
        try(final ManagedLock<ReadWriteLock> readLock = ManagedLock.acquire(lock, LockMode.READ_LOCK)) {
1✔
243
            while(!path.equals(COLLECTION_CONFIG_PATH)) {
1✔
244
                final CollectionConfiguration conf = configurations.get(path);
1✔
245
                if (conf != null) {
1✔
246
                    return conf;
1✔
247
                }
248
                path.removeLastSegment();
1✔
249
            }
250

251
            // use default configuration
252
            return defaultConfig;
1✔
253
        }
254
    }
255

256
    protected void loadAllConfigurations(DBBroker broker) throws CollectionConfigurationException, PermissionDeniedException, LockException {
257
        final Collection root = broker.getCollection(CONFIG_COLLECTION_URI);
1✔
258
        loadAllConfigurations(broker, root);
1✔
259
    }
1✔
260

261
    protected void loadAllConfigurations(DBBroker broker, Collection configCollection) throws CollectionConfigurationException, PermissionDeniedException,
262
            LockException {
263
        if (configCollection == null) {
1!
264
            return;
×
265
        }
266
        loadConfiguration(broker, configCollection);
1✔
267
        final XmldbURI path = configCollection.getURI();
1✔
268
        for (final Iterator<XmldbURI> i = configCollection.collectionIterator(broker); i.hasNext();) {
1✔
269
            final XmldbURI childName = i.next();
1✔
270
            final Collection child = broker.getCollection(path.appendInternal(childName));
1✔
271
            if (child == null) {
1!
272
                LOG.error("Collection is registered but could not be loaded: {}", childName);
×
273
            }
274
            loadAllConfigurations(broker, child);
1✔
275
        }
276
    }
1✔
277

278
    protected void loadConfiguration(DBBroker broker, final Collection configCollection) throws CollectionConfigurationException, PermissionDeniedException,
279
            LockException {
280
        if (configCollection != null && configCollection.getDocumentCount(broker) > 0) {
1!
281
            for (final Iterator<DocumentImpl> i = configCollection.iterator(broker); i.hasNext();) {
1!
282
                final DocumentImpl confDoc = i.next();
1✔
283
                if (confDoc.getFileURI().endsWith(CollectionConfiguration.COLLECTION_CONFIG_SUFFIX_URI)) {
1!
284

285
                    if (confDoc instanceof BinaryDocument) {
1!
286
                        LOG.warn("Found a possible Collection configuration document: {}, however it is a Binary document! A user may have stored the document as a Binary document by mistake. Skipping...", confDoc.getURI());
×
287
                        continue;
×
288
                    }
289

290
                    if (LOG.isTraceEnabled()) {
1!
291
                        LOG.trace("Reading collection configuration from '{}'", confDoc.getURI());
×
292
                    }
293

294
                    final CollectionConfiguration conf = new CollectionConfiguration(broker.getBrokerPool());
1✔
295

296
                    // [ 1807744 ] Invalid collection.xconf causes a non startable database
297
                    // http://sourceforge.net/tracker/index.php?func=detail&aid=1807744&group_id=17691&atid=117691
298
                    try {
299
                        conf.read(broker, confDoc, false, configCollection.getURI(), confDoc.getFileURI());
1✔
300
                    } catch (final CollectionConfigurationException e) {
1✔
301
                        final String message = "Failed to read configuration document " + confDoc.getFileURI() + " in " + configCollection.getURI() + ". "
×
302
                                + e.getMessage();
×
303
                        LOG.error(message);
×
304
                    }
305

306
                    try(final ManagedLock<ReadWriteLock> writeLock = ManagedLock.acquire(lock, LockMode.WRITE_LOCK)) {
1✔
307
                        configurations.put(new CollectionURI(configCollection.getURI().getRawCollectionPath()), conf);
1✔
308
                    }
309

310
                    // Allow just one configuration document per collection
311
                    // TODO : do not break if a system property allows several ones -pb
312
                    break;
313
                }
314
            }
315
        }
316
    }
1✔
317

318
    public CollectionConfiguration getOrCreateCollectionConfiguration(final DBBroker broker, final Collection collection) {
319
        final CollectionURI path = new CollectionURI(COLLECTION_CONFIG_PATH);
1✔
320
        path.append(collection.getURI().getRawCollectionPath());
1✔
321

322
        try(final ManagedLock<ReadWriteLock> readLock = ManagedLock.acquire(lock, LockMode.READ_LOCK)) {
1✔
323
            final CollectionConfiguration conf = configurations.get(path);
1✔
324
            if(conf != null) {
1!
325
                return conf;
×
326
            }
327
        }
328

329
        try(final ManagedLock<ReadWriteLock> writeLock = ManagedLock.acquire(lock, LockMode.WRITE_LOCK)) {
1✔
330
            CollectionConfiguration conf = configurations.get(path);
1✔
331
            if (conf != null) {
1!
332
                return conf;
×
333
            }
334

335
            conf = new CollectionConfiguration(broker.getBrokerPool());
1✔
336
            configurations.put(path, conf);
1✔
337

338
            return conf;
1✔
339
        }
340
    }
341

342
    /**
343
     * Notify the manager that a collection.xconf file has changed. All cached
344
     * configurations for the corresponding collection and its sub-collections
345
     * will be cleared.
346
     * 
347
     * @param collectionPath to the collection for which configuration will be invalidated
348
     */
349
    public void invalidateAll(final XmldbURI collectionPath) {
350

351
        if (!collectionPath.startsWith(CONFIG_COLLECTION_URI)) {
×
352
            return;
×
353
        }
354

355
        try(final ManagedLock<ReadWriteLock> writeLock = ManagedLock.acquire(lock, LockMode.WRITE_LOCK)) {
×
356

357
            if (LOG.isDebugEnabled()) {
×
358
                LOG.debug("Invalidating collection {} and subcollections", collectionPath);
×
359
            }
360

361
            CollectionURI uri = new CollectionURI(collectionPath.getRawCollectionPath());
×
362

363
            configurations.remove(uri);
×
364

365
            String str = uri.toString();
×
366

367
            configurations.entrySet().removeIf(entry -> entry.getKey().toString().startsWith(str));
×
368
        }
369
    }
×
370

371
    /**
372
     * Called by the collection cache if a collection is removed from the cache.
373
     * This will delete the cached configuration instance for this collection.
374
     * 
375
     * @param collectionPath to the collection for which configuration will be invalidated
376
     * @param pool if not null: clear query cache
377
     */
378
    public void invalidate(final XmldbURI collectionPath, final BrokerPool pool) {
379
        if (!collectionPath.startsWith(CONFIG_COLLECTION_URI)) {
1✔
380
            return;
1✔
381
        }
382

383
        try(final ManagedLock<ReadWriteLock> writeLock = ManagedLock.acquire(lock, LockMode.WRITE_LOCK)) {
1✔
384
            if (LOG.isDebugEnabled()) {
1!
385
                LOG.debug("Invalidating collection {}", collectionPath);
×
386
            }
387

388
            configurations.remove(new CollectionURI(collectionPath.getRawCollectionPath()));
1✔
389
        }
390
    }
1✔
391

392
    /**
393
     * Check if the collection exists below the system collection. If not,
394
     * create it.
395
     * 
396
     * @param broker the broker
397
     * @param txn according transaction
398
     * @param uri to the collection to create
399
     * @throws EXistException if something goes wrong
400
     */
401
    private void checkCreateCollection(final DBBroker broker, final Txn txn, final XmldbURI uri) throws EXistException {
402
        try {
403
            Collection collection = broker.getCollection(uri);
1✔
404
            if (collection == null) {
1!
405
                collection = broker.getOrCreateCollection(txn, uri);
×
406
                SanityCheck.THROW_ASSERT(collection != null);
×
407
                broker.saveCollection(txn, collection);
×
408
            }
409
        } catch(final TriggerException | PermissionDeniedException | IOException e) {
×
410
            throw new EXistException("Failed to initialize '" + uri + "' : " + e.getMessage());
×
411
        }
412
    }
1✔
413

414
    /**
415
     * Create a stored default configuration document for the root collection
416
     * 
417
     * @param broker The broker which will do the operation
418
     * @throws EXistException if something goes wrong
419
     * @throws PermissionDeniedException if user does not have sufficient rights
420
     */
421
    public void checkRootCollectionConfig(DBBroker broker) throws EXistException, PermissionDeniedException {
422
        // Copied from the legacy conf.xml in order to make the test suite work
423
        // TODO : backward compatibility could be ensured by copying the
424
        // relevant parts of conf.xml
425

426
        final String configuration = 
×
427
          "<collection xmlns=\"http://exist-db.org/collection-config/1.0\">"
×
428
        + "    <index>"
429
        + "    </index>"
430
        + "</collection>";
431

432
        final TransactionManager transact = broker.getDatabase().getTransactionManager();
×
433
        try(final Txn txn = transact.beginTransaction()) {
×
434

435
            try(final Collection collection = broker.openCollection(XmldbURI.ROOT_COLLECTION_URI, LockMode.READ_LOCK)) {
×
436
                if (collection == null) {
×
437
                    transact.abort(txn);
×
438
                    throw new EXistException("collection " + XmldbURI.ROOT_COLLECTION_URI + " not found!");
×
439
                }
440
                final CollectionConfiguration conf = getConfiguration(collection);
×
441
                if (conf != null) {
×
442
                    // We already have a configuration document : do not erase
443
                    // it
444
                    if (conf.getDocName() != null) {
×
445
                        transact.abort(txn);
×
446
                        return;
×
447
                    }
448
                }
449

450
                // Configure the root collection
451
                addConfiguration(txn, broker, collection, configuration);
×
452
                LOG.info("Configured '{}'", collection.getURI());
×
453
            }
454

455
            transact.commit(txn);
×
456

457
        } catch (final CollectionConfigurationException e) {
×
458
            throw new EXistException(e.getMessage());
×
459
        }
460
    }
×
461

462
    /*
463
     * private void debugCache() { StringBuilder buf = new StringBuilder(); for
464
     * (Iterator i = configurations.keySet().iterator(); i.hasNext(); ) {
465
     * buf.append(i.next()).append(' '); } LOG.debug(buf.toString()); }
466
     */
467
}
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