• 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

68.14
/exist-core/src/main/java/org/exist/collections/MutableCollection.java
1
/*
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 com.evolvedbinary.j8fu.function.BiConsumer2E;
52
import com.evolvedbinary.j8fu.function.Consumer2E;
53
import net.jcip.annotations.GuardedBy;
54
import net.jcip.annotations.NotThreadSafe;
55
import org.apache.commons.io.input.CloseShieldReader;
56
import org.exist.dom.QName;
57
import org.exist.dom.persistent.*;
58

59
import java.io.*;
60
import java.util.*;
61

62
import org.apache.commons.io.input.CloseShieldInputStream;
63
import org.apache.logging.log4j.LogManager;
64
import org.apache.logging.log4j.Logger;
65
import org.exist.Database;
66
import org.exist.EXistException;
67
import org.exist.Indexer;
68
import org.exist.collections.triggers.*;
69
import org.exist.indexing.IndexController;
70
import org.exist.indexing.StreamListener;
71
import org.exist.security.Account;
72
import org.exist.security.Permission;
73
import org.exist.security.PermissionDeniedException;
74
import org.exist.security.PermissionFactory;
75
import org.exist.security.Subject;
76
import org.exist.storage.*;
77
import org.exist.storage.io.VariableByteInput;
78
import org.exist.storage.io.VariableByteOutputStream;
79
import org.exist.storage.lock.*;
80
import org.exist.storage.lock.Lock.LockMode;
81
import org.exist.storage.lock.Lock.LockType;
82
import org.exist.storage.sync.Sync;
83
import org.exist.storage.txn.Txn;
84
import org.exist.util.Configuration;
85
import org.exist.util.LockException;
86
import org.exist.util.MimeType;
87
import org.exist.util.XMLReaderObjectFactory;
88
import org.exist.util.XMLReaderObjectFactory.VALIDATION_SETTING;
89
import org.apache.commons.io.input.UnsynchronizedByteArrayInputStream;
90
import org.exist.util.serializer.DOMStreamer;
91
import org.exist.xmldb.XmldbURI;
92
import org.exist.xquery.Constants;
93
import org.w3c.dom.DocumentType;
94
import org.w3c.dom.Node;
95
import org.xml.sax.InputSource;
96
import org.xml.sax.SAXException;
97
import org.xml.sax.XMLReader;
98

99
import javax.annotation.Nullable;
100

101
import static org.exist.storage.lock.Lock.LockMode.*;
102

103
/**
104
 * An implementation of {@link Collection} that allows
105
 * mutations to be made to the Collection object
106
 *
107
 * Locks should be taken appropriately for any mutation
108
 */
109
@NotThreadSafe
110
public class MutableCollection implements Collection {
111

112
    //TODO(AR) ultimately remove all locking internally from this class and externalise it to the callers, all methods are then internally lock free, and then finally remove `NonLocking` methods
113

114
    private static final Logger LOG = LogManager.getLogger(Collection.class);
1✔
115
    private static final int SHALLOW_SIZE = 550;
116
    private static final int DOCUMENT_SIZE = 450;
1✔
117

118
    private final int collectionId;
119
    private XmldbURI path;
120
    private final LockManager lockManager;
121

122
    /*
123
     * LinkedHashSet is used to ensure a consistent iteration order of child Documents.
124
     * The `insertion-order` of a LinkedHashSet means we effectively order by Document creation
125
     * time, i.e. oldest first.
126
     * This ordering ensures that adding new Documents does not affect the existing order of Documents,
127
     * in this manner locks acquired when iterating are always acquired and released in the same order
128
     * which gives us deadlock avoidance for Document iteration.
129
     */
130
    @GuardedBy("LockManager") private final LinkedHashMap<String, DocumentImpl> documents;
131

132
    /*
133
     * LinkedHashSet is used to ensure a consistent iteration order of sub-Collections.
134
     * The `insertion-order` of a LinkedHashSet means we effectively order by sub-Collection creation
135
     * time, i.e. oldest first.
136
     * This ordering ensures that adding new sub-Collections does not affect the existing order of sub-Collections,
137
     * in this manner locks acquired when iterating are always acquired and released in the same order
138
     * which gives us deadlock avoidance for sub-Collection iteration.
139
     */
140
    @GuardedBy("LockManager") private final LinkedHashSet<XmldbURI> subCollections;
141

142
    private long created;
143
    private volatile boolean isTempCollection;
144
    private final Permission permissions;
145
    @Deprecated private CollectionMetadata collectionMetadata = null;
1✔
146

147
    /**
148
     * Constructs a Collection Object (not yet persisted)
149
     *
150
     * @param broker The database broker
151
     * @param collectionId a unique numeric id for the collection
152
     * @param path The path of the Collection
153
     */
154
    public MutableCollection(final DBBroker broker, final int collectionId, final XmldbURI path) {
155
        this(broker, collectionId, path, null, -1, null, null);
1✔
156
    }
1✔
157

158
    /**
159
     * Constructs a Collection Object (not yet persisted)
160
     *
161
     * @param broker The database broker
162
     * @param collectionId a unique numeric id for the collection
163
     * @param path The path of the Collection
164
     * @param permissions The permissions of the collection, or null for the default
165
     * @param created The created time of the collection, or -1 for now
166
     */
167
    public MutableCollection(final DBBroker broker, final int collectionId,
168
            @EnsureLocked(mode=LockMode.READ_LOCK, type=LockType.COLLECTION) final XmldbURI path,
169
            @Nullable final Permission permissions, final long created) {
170
        this(broker, collectionId, path, permissions, created, null, null);
1✔
171
    }
1✔
172

173
    /**
174
     * Constructs a Collection Object (not yet persisted)
175
     *
176
     * @param broker The database broker
177
     * @param collectionId a unique numeric id for the collection
178
     * @param path The path of the Collection
179
     * @param permissions The permissions of the collection, or null for the default
180
     * @param created The created time of the collection, or -1 for now
181
     * @param subCollections the sub-collections
182
     * @param documents the documents in the collection
183
     */
184
    private MutableCollection(final DBBroker broker, final int collectionId,
1✔
185
            @EnsureLocked(mode=LockMode.READ_LOCK, type=LockType.COLLECTION) final XmldbURI path,
186
            @Nullable final Permission permissions, final long created,
187
            @Nullable final LinkedHashSet<XmldbURI> subCollections,
188
            @Nullable final LinkedHashMap<String, DocumentImpl> documents) {
189
        setPath(path);
1✔
190
        this.collectionId = collectionId;
1✔
191
        this.permissions = permissions != null ? permissions : PermissionFactory.getDefaultCollectionPermission(broker.getBrokerPool().getSecurityManager());
1✔
192
        this.created = created > 0 ? created : System.currentTimeMillis();
1✔
193
        this.lockManager = broker.getBrokerPool().getLockManager();
1✔
194
        this.subCollections = subCollections != null ? subCollections : new LinkedHashSet<>();
1✔
195
        this.documents = documents != null ? documents : new LinkedHashMap<>();
1✔
196
    }
1✔
197

198
    /**
199
     * Deserializes a Collection object
200
     *
201
     * Counterpart method to {@link #serialize(VariableByteOutputStream)}
202
     *
203
     * @param broker The database broker
204
     * @param path The path of the Collection
205
     * @param inputStream The input stream to deserialize the Collection from
206
     * @throws PermissionDeniedException is user does not have sufficient rights
207
     * @throws IOException if an I/O error happens
208
     * @throws LockException in case dbbroker is locked
209
     *
210
     * @return The Collection Object
211
     */
212
    public static MutableCollection load(final DBBroker broker,
213
            @EnsureLocked(mode=LockMode.WRITE_LOCK, type=LockType.COLLECTION) final XmldbURI path,
214
            final VariableByteInput inputStream) throws PermissionDeniedException, IOException, LockException {
215
        return deserialize(broker, path, inputStream);
1✔
216
    }
217

218
    @Override
219
    public final void setPath(XmldbURI path) {
220
        setPath(path, false);
1✔
221
    }
1✔
222

223
    @Override
224
    public final void setPath(XmldbURI path, final boolean updateChildren) {
225
        path = path.toCollectionPathURI();
1✔
226
        //TODO : see if the URI resolves against DBBroker.TEMP_COLLECTION
227
        this.isTempCollection = path.getRawCollectionPath().equals(XmldbURI.TEMP_COLLECTION);
1✔
228
        this.path = path;
1✔
229

230
        if (updateChildren) {
1✔
231
            for (final Map.Entry<String, DocumentImpl> docEntry : documents.entrySet()) {
1✔
232
                final XmldbURI docUri = path.append(docEntry.getKey());
1✔
233
                try (final ManagedDocumentLock documentLock = lockManager.acquireDocumentWriteLock(docUri)) {
1✔
234
                    final DocumentImpl doc = docEntry.getValue();
1✔
235
                    doc.setCollection(this);  // this will invalidate the cached `uri` in DocumentImpl
1✔
236
                } catch (final LockException e) {
×
237
                    LOG.error(e.getMessage(), e);
×
238
                    throw new IllegalStateException(e);
×
239
                }
240
            }
241
        }
242
    }
1✔
243

244
    @Override
245
    public void addCollection(final DBBroker broker, final Collection child)
246
            throws PermissionDeniedException, LockException {
247
        try(final ManagedCollectionLock collectionLock = lockManager.acquireCollectionWriteLock(path)) {
1✔
248
            if(!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.WRITE)) {
1!
249
                throw new PermissionDeniedException("Permission to write to Collection denied for " + this.getURI());
×
250
            }
251

252
            final XmldbURI childName = child.getURI().lastSegment();
1✔
253
            if (!subCollections.contains(childName)) {
1✔
254
                subCollections.add(childName);
1✔
255
            }
256
        }
257
    }
1✔
258

259
    private static <T> Iterator<T> stableIterator(final LinkedHashSet<T> set) {
260
        return new LinkedHashSet<>(set).iterator();
1✔
261
    }
262

263
    private static Iterator<DocumentImpl> stableDocumentIterator(final LinkedHashMap<String, DocumentImpl> documents) {
264
        return new ArrayList<>(documents.values()).iterator();
1✔
265
    }
266

267
    private static Iterator<String> stableDocumentNameIterator(final LinkedHashMap<String, DocumentImpl> documents) {
268
        return new ArrayList<>(documents.keySet()).iterator();
×
269
    }
270

271
    @Override
272
    public List<CollectionEntry> getEntries(final DBBroker broker) throws PermissionDeniedException, LockException, IOException {
273
        final List<CollectionEntry> list = new ArrayList<>();
×
274

275
        final Iterator<XmldbURI> subCollectionIterator;
276
        final Iterator<DocumentImpl> documentIterator;
277
        try(final ManagedCollectionLock collectionLock = lockManager.acquireCollectionReadLock(path)) {
×
278
            if(!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.READ)) {
×
279
                throw new PermissionDeniedException("Permission denied to read collection: " + path);
×
280
            }
281

282
            subCollectionIterator = stableIterator(subCollections);
×
283
            documentIterator = stableDocumentIterator(documents);
×
284
        }
285

286
        while(subCollectionIterator.hasNext()) {
×
287
            final XmldbURI subCollectionURI = subCollectionIterator.next();
×
288
            try(final ManagedCollectionLock subCollectionLock = lockManager.acquireCollectionReadLock(subCollectionURI)) {
×
289
                final CollectionEntry entry = new SubCollectionEntry(broker.getBrokerPool().getSecurityManager(),
×
290
                        subCollectionURI);
×
291
                entry.readMetadata(broker);
×
292
                list.add(entry);
×
293
            }
294
        }
295

296
        while(documentIterator.hasNext()) {
×
297
            final DocumentImpl document = documentIterator.next();
×
298
            try(final ManagedDocumentLock documentLock = lockManager.acquireDocumentReadLock(document.getURI())) {
×
299
                final DocumentEntry entry = new DocumentEntry(document);
×
300
                entry.readMetadata(broker);
×
301
                list.add(entry);
×
302
            }
303
        }
304
        return list;
×
305
    }
306

307
    @Override
308
    public CollectionEntry getChildCollectionEntry(final DBBroker broker, final String name)
309
            throws PermissionDeniedException, LockException, IOException {
310
        final XmldbURI subCollectionURI = getURI().append(name);
1✔
311
        final CollectionEntry entry;
312
        try(final ManagedCollectionLock subCollectionLock = lockManager.acquireCollectionReadLock(subCollectionURI)) {
1✔
313
            if(!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.READ)) {
1!
314
                throw new PermissionDeniedException("Permission denied to read collection: " + path);
×
315
            }
316

317
            entry = new SubCollectionEntry(broker.getBrokerPool().getSecurityManager(),
1✔
318
                    subCollectionURI);
1✔
319
            entry.readMetadata(broker);
1✔
320
        }
321
        return entry;
1✔
322
    }
323

324
    @Override
325
    public CollectionEntry getResourceEntry(final DBBroker broker, final String name)
326
            throws PermissionDeniedException, LockException, IOException {
327
        if(!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.READ)) {
1!
328
            throw new PermissionDeniedException("Permission denied to read collection: " + path);
×
329
        }
330

331
        final CollectionEntry entry;
332
        try(final ManagedCollectionLock collectionLock = lockManager.acquireCollectionReadLock(path)) {
1✔
333
            final DocumentImpl doc = documents.get(name);
1✔
334

335
            try(final ManagedDocumentLock docLock = lockManager.acquireDocumentReadLock(doc.getURI())) {
1✔
336

337
                // NOTE: early release of Collection lock inline with Asymmetrical Locking scheme
338
                collectionLock.close();
1✔
339

340
                entry = new DocumentEntry(doc);
1✔
341
                entry.readMetadata(broker);
1✔
342
            }
343
        }
344

345
        return entry;
1✔
346
    }
347

348
    @Override
349
    public boolean isTempCollection() {
350
        return isTempCollection;
1✔
351
    }
352

353
    @Override
354
    public void addDocument(final Txn transaction, final DBBroker broker, final DocumentImpl doc)
355
            throws PermissionDeniedException, LockException {
356
        addDocument(transaction, broker, doc, null);
1✔
357
    }
1✔
358
    
359
    /**
360
     * @param oldDoc if not null, then this document is replacing another and so WRITE access on the collection is not required,
361
     * just WRITE access on the old document
362
     */
363
    private void addDocument(final Txn transaction, final DBBroker broker, final DocumentImpl doc,
364
            final DocumentImpl oldDoc) throws PermissionDeniedException, LockException {
365

366
        try(final ManagedCollectionLock collectionLock = lockManager.acquireCollectionWriteLock(path)) {
1✔
367

368
            if (oldDoc == null) {
1✔
369

370
                /* create */
371
                if (!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.WRITE)) {
1!
372
                    throw new PermissionDeniedException("Permission to write to Collection denied for " + this.getURI());
×
373
                }
374
            } else {
375
                /* update-replace */
376
                try (final ManagedDocumentLock oldDocLock = lockManager.acquireDocumentReadLock(oldDoc.getURI())) {
1✔
377
                    if (!oldDoc.getPermissions().validate(broker.getCurrentSubject(), Permission.WRITE)) {
1!
378

379
                        // NOTE: early release of Collection lock inline with Asymmetrical Locking scheme
380
                        collectionLock.close();
×
381

382
                        throw new PermissionDeniedException("Permission to write to overwrite document: " + oldDoc.getURI());
×
383
                    }
384
                }
385
            }
386

387
            try (final ManagedDocumentLock docLock = lockManager.acquireDocumentWriteLock(doc.getURI())) {
1✔
388

389
                // NOTE: early release of Collection lock inline with Asymmetrical Locking scheme
390
                collectionLock.close();
1✔
391

392
                documents.put(doc.getFileURI().lastSegmentString(), doc);
1✔
393
            }
394
        }
395
    }
1✔
396

397
    @Override
398
    public void unlinkDocument(final DBBroker broker, final DocumentImpl doc) throws PermissionDeniedException,
399
            LockException {
400
        try(final ManagedCollectionLock collectionLock = lockManager.acquireCollectionWriteLock(path)) {
1✔
401
            if(!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.WRITE)) {
1!
402
                throw new PermissionDeniedException("Permission denied to remove document from collection: " + path);
×
403
            }
404

405
            documents.remove(doc.getFileURI().lastSegmentString());
1✔
406
        }
407
    }
1✔
408

409
    @Override
410
    public Iterator<XmldbURI> collectionIterator(final DBBroker broker) throws PermissionDeniedException, LockException {
411
        try(final ManagedCollectionLock collectionLock = lockManager.acquireCollectionReadLock(path)) {
1✔
412
            if(!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.READ)) {
1✔
413
                throw new PermissionDeniedException("Permission to list sub-collections denied on " + this.getURI());
1✔
414
            }
415

416
            return stableIterator(subCollections);
1✔
417
        }
418
    }
419

420
    @Override
421
    public Iterator<XmldbURI> collectionIteratorNoLock(final DBBroker broker) throws PermissionDeniedException {
422
        if(!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.READ)) {
1!
423
            throw new PermissionDeniedException("Permission to list sub-collections denied on " + this.getURI());
×
424
        }
425
        return stableIterator(subCollections);
1✔
426
    }
427

428
    @Override
429
    public List<Collection> getDescendants(final DBBroker broker, final Subject user) throws PermissionDeniedException {
430
        final ArrayList<Collection> collectionList = new ArrayList<>();
×
431
        final Iterator<XmldbURI> i;
432
        try(final ManagedCollectionLock collectionLock = lockManager.acquireCollectionReadLock(path)) {
×
433
            if(!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.READ)) {
×
434
                throw new PermissionDeniedException("Permission to list sub-collections denied on " + this.getURI());
×
435
            }
436

437
            collectionList.ensureCapacity(subCollections.size());
×
438
            i = stableIterator(subCollections);
×
439
        } catch(final LockException e) {
×
440
            LOG.error(e.getMessage(), e);
×
441
            return Collections.emptyList();
×
442
        }
443

444
        while(i.hasNext()) {
×
445
            final XmldbURI childName = i.next();
×
446
            //TODO : resolve URI !
447
            final Collection child = broker.getCollection(path.append(childName));
×
448
            if(getPermissions().validate(user, Permission.READ)) {
×
449
                collectionList.add(child);
×
450
                if(child.getChildCollectionCount(broker) > 0) {
×
451
                    //Recursive call
452
                    collectionList.addAll(child.getDescendants(broker, user));
×
453
                }
454
            }
455
        }
456

457
        return collectionList;
×
458
    }
459

460
    @Override
461
    public MutableDocumentSet allDocs(final DBBroker broker, final MutableDocumentSet docs, final boolean recursive)
462
            throws PermissionDeniedException, LockException {
463
        return allDocs(broker, docs, recursive, null);
1✔
464
    }
465

466
    @Override
467
    public MutableDocumentSet allDocs(final DBBroker broker, final MutableDocumentSet docs, final boolean recursive,
468
            final LockedDocumentMap lockMap) throws PermissionDeniedException, LockException {
469
        XmldbURI[] subColls = null;
1✔
470
        try(final ManagedCollectionLock collectionLock = lockManager.acquireCollectionReadLock(path)) {
1✔
471
            if (getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.READ)) {
1!
472
                //Add all docs in this collection to the returned set
473
                getDocuments(broker, docs);
1✔
474
                //Get a list of sub-collection URIs. We will process them
475
                //after unlocking this collection. otherwise we may deadlock ourselves
476
                subColls = subCollections.stream()
1✔
477
                        .map(path::appendInternal)
1✔
478
                        .toArray(XmldbURI[]::new);
1✔
479
            }
480
        }
481

482
        if(recursive && subColls != null) {
1!
483
            // process the child collections
484
            for(final XmldbURI subCol : subColls) {
1✔
485
                try(final Collection child = broker.openCollection(subCol, NO_LOCK)) {      // NOTE: the recursive call below to child.addDocs will take a lock
1✔
486
                    //A collection may have been removed in the meantime, so check first
487
                    if(child != null) {
1!
488
                        child.allDocs(broker, docs, recursive, lockMap);
1✔
489
                    }
490
                } catch(final PermissionDeniedException pde) {
1✔
491
                    //SKIP to next collection
492
                    //TODO create an audit log??!
493
                }
494
            }
495
        }
496
        return docs;
1✔
497
    }
498

499
    @Override
500
    public DocumentSet allDocs(final DBBroker broker, final MutableDocumentSet docs, final boolean recursive,
501
            final LockedDocumentMap lockMap, final LockMode lockType) throws LockException, PermissionDeniedException {
502
        XmldbURI[] uris = null;
1✔
503

504
        try(final ManagedCollectionLock collectionLock = lockManager.acquireCollectionReadLock(path)) {
1✔
505
            if (getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.READ)) {
1!
506
                //Add all documents in this collection to the returned set
507
                getDocuments(broker, docs, lockMap, lockType);
1✔
508
                //Get a list of sub-collection URIs. We will process them
509
                //after unlocking this collection.
510
                //otherwise we may deadlock ourselves
511
                uris = subCollections.stream()
1✔
512
                        .map(path::appendInternal)
1✔
513
                        .toArray(XmldbURI[]::new);
1✔
514
            }
515
        }
516

517
        if(recursive && uris != null) {
1!
518
            //Process the child collections
519
            for (final XmldbURI uri : uris) {
1✔
520
                try(final Collection child = broker.openCollection(uri, NO_LOCK)) {     // NOTE: the recursive call below to child.addDocs will take a lock
1✔
521
                    // a collection may have been removed in the meantime, so check first
522
                    if (child != null) {
1!
523
                        child.allDocs(broker, docs, recursive, lockMap, lockType);
1✔
524
                    }
525
                } catch (final PermissionDeniedException pde) {
×
526
                    //SKIP to next collection
527
                    //TODO create an audit log??!
528
                }
529
            }
530
        }
531
        return docs;
1✔
532
    }
533

534
    @Override
535
    public DocumentSet
536
    getDocuments(final DBBroker broker, final MutableDocumentSet docs)
537
            throws PermissionDeniedException, LockException {
538
        final Iterator<DocumentImpl> documentIterator;
539
        try(final ManagedCollectionLock collectionLock = lockManager.acquireCollectionReadLock(path)) {
1✔
540
            if(!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.READ)) {
1✔
541
                throw new PermissionDeniedException("Permission denied to read collection: " + path);
1✔
542
            }
543
            documentIterator = stableDocumentIterator(documents);
1✔
544
            docs.addCollection(this);
1✔
545
        }
546
        addDocumentsToSet(broker, documentIterator, docs);
1✔
547
        
548
        return docs;
1✔
549
    }
550

551
    @Override
552
    public DocumentSet getDocumentsNoLock(final DBBroker broker, final MutableDocumentSet docs) {
553
        final Iterator<DocumentImpl> documentIterator = stableDocumentIterator(documents);
1✔
554
        docs.addCollection(this);
1✔
555
        addDocumentsToSet(broker, documentIterator, docs);
1✔
556
        return docs;
1✔
557
    }
558

559
    @Override
560
    public DocumentSet getDocuments(final DBBroker broker, final MutableDocumentSet docs,
561
            final LockedDocumentMap lockMap, final LockMode lockType) throws LockException, PermissionDeniedException {
562
        final Iterator<DocumentImpl> documentIterator;
563
        try(final ManagedCollectionLock collectionLock = lockManager.acquireCollectionReadLock(path)) {
1✔
564
            if(!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.READ)) {
1!
565
                throw new PermissionDeniedException("Permission denied to read collection: " + path);
×
566
            }
567
            documentIterator = stableDocumentIterator(documents);
1✔
568
            docs.addCollection(this);
1✔
569
        }
570
        addDocumentsToSet(broker, documentIterator, docs, lockMap, lockType);
1✔
571

572
        return docs;
1✔
573
    }
574

575
    private void addDocumentsToSet(final DBBroker broker, final Iterator<DocumentImpl> documentIterator, final MutableDocumentSet docs, final LockedDocumentMap lockMap, final LockMode lockType) throws LockException {
576
        final int requiredPermission;
577
        if(lockType == LockMode.READ_LOCK) {
1!
578
            requiredPermission = Permission.READ;
×
579
        } else {
×
580
            requiredPermission = Permission.WRITE;
1✔
581
        }
582

583
        while(documentIterator.hasNext()) {
1✔
584
            final DocumentImpl doc = documentIterator.next();
1✔
585
            if(doc.getPermissions().validate(broker.getCurrentSubject(), requiredPermission)) {
1!
586
                final ManagedDocumentLock documentLock = switch (lockType) {
1!
587
                    case WRITE_LOCK -> lockManager.acquireDocumentWriteLock(doc.getURI());
1✔
588
                    case READ_LOCK -> lockManager.acquireDocumentReadLock(doc.getURI());
×
589
                    default -> ManagedSingleLockDocumentLock.notLocked(doc.getURI());
×
590
                };
591

592
                docs.add(doc);
1✔
593
                lockMap.add(new LockedDocument(documentLock, doc));
1✔
594
            }
595
            }
596
    }
1✔
597
    
598
    private void addDocumentsToSet(final DBBroker broker, final Iterator<DocumentImpl> documentIterator, final MutableDocumentSet docs) {
599
        while (documentIterator.hasNext()) {
1✔
600
            final DocumentImpl doc = documentIterator.next();
1✔
601
            try(final ManagedDocumentLock lockedDoc = lockManager.acquireDocumentReadLock(doc.getURI())) {
1✔
602
                if(doc.getPermissions().validate(broker.getCurrentSubject(), Permission.READ)) {
1✔
603
                    docs.add(doc);
1✔
604
                }
605
            } catch (final LockException e) {
×
606
                LOG.error(e.getMessage(), e);
×
607
            }
608
        }
609
    }
1✔
610

611
    @Override
612
    @EnsureContainerLocked(mode=READ_LOCK)
613
    public int compareTo(@EnsureLocked(mode=READ_LOCK) final Collection other) {
614
        Objects.requireNonNull(other);
×
615

616
        if(collectionId == other.getId()) {
×
617
            return Constants.EQUAL;
×
618
        } else if(collectionId < other.getId()) {
×
619
            return Constants.INFERIOR;
×
620
        } else {
621
            return Constants.SUPERIOR;
×
622
        }
623
    }
624

625
    @Override
626
    @EnsureContainerLocked(mode=READ_LOCK) public boolean equals(@Nullable @EnsureLocked(mode=READ_LOCK) final Object obj) {
627
        if(obj == null || !(obj instanceof Collection)) {
1!
628
            return false;
1✔
629
        }
630

631
        return ((Collection) obj).getId() == collectionId;
1✔
632
    }
633

634
    @Override
635
    public int getMemorySize() {
636
        try(final ManagedCollectionLock collectionLock = lockManager.acquireCollectionReadLock(path)) {
×
637
            return SHALLOW_SIZE + (documents.size() * DOCUMENT_SIZE);
×
638
        } catch(final LockException e) {
×
639
            LOG.error(e);
×
640
            return -1;
×
641
        }
642
    }
643

644
    @Override
645
    public int getMemorySizeNoLock() {
646
        return SHALLOW_SIZE + (documents.size() * DOCUMENT_SIZE);
1✔
647
    }
648

649
    @Override
650
    public int getChildCollectionCount(final DBBroker broker) throws PermissionDeniedException {
651
        try(final ManagedCollectionLock collectionLock = lockManager.acquireCollectionReadLock(path)) {
1✔
652
            if(!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.READ)) {
1✔
653
                throw new PermissionDeniedException("Permission denied to read collection: " + path);
1✔
654
            }
655

656
            return subCollections.size();
1✔
657
        } catch(final LockException e) {
×
658
            LOG.error(e.getMessage(), e);
×
659
            return 0;
×
660
        }
661
    }
662

663
    @Override
664
    public boolean isEmpty(final DBBroker broker) throws PermissionDeniedException {
665
        try(final ManagedCollectionLock collectionLock = lockManager.acquireCollectionReadLock(path)) {
1✔
666
            if(!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.READ)) {
1!
667
                throw new PermissionDeniedException("Permission denied to read collection: " + path);
×
668
            }
669

670
            return documents.isEmpty() && subCollections.isEmpty();
1✔
671
        } catch(final LockException e) {
×
672
            LOG.error(e.getMessage(), e);
×
673
            return false;
×
674
        }
675
    }
676

677
    @Override
678
    public DocumentImpl getDocument(final DBBroker broker, final XmldbURI name) throws PermissionDeniedException {
679
        try(final ManagedCollectionLock collectionLock = lockManager.acquireCollectionReadLock(path)) {
1✔
680

681
            try(final ManagedDocumentLock docLock = lockManager.acquireDocumentReadLock(getURI().append(name.lastSegment()))) {
1✔
682
                final DocumentImpl doc = documents.get(name.lastSegmentString());
1✔
683

684
                // NOTE: early release of Collection lock inline with Asymmetrical Locking scheme
685
                collectionLock.close();
1✔
686

687
                if (doc != null) {
1✔
688
                    if (!doc.getPermissions().validate(broker.getCurrentSubject(), Permission.READ)) {
1✔
689
                        throw new PermissionDeniedException("Permission denied to read document: " + name.toString());
1✔
690
                    }
691
                } else {
692
                    if(LOG.isDebugEnabled()) {
1!
693
                        LOG.debug("Document {} not found!", name);
×
694
                    }
695
                }
696

697
                return doc;
1✔
698
            }
699
        } catch(final LockException e) {
×
700
            LOG.error(e.getMessage(), e);
×
701
            return null;
×
702
        }
703
    }
704

705
    @Override
706
    public LockedDocument getDocumentWithLock(final DBBroker broker, final XmldbURI name) throws LockException, PermissionDeniedException {
707
            return getDocumentWithLock(broker, name, READ_LOCK);
×
708
    }
709

710
    @Override
711
    public LockedDocument getDocumentWithLock(final DBBroker broker, final XmldbURI name, final LockMode lockMode) throws LockException, PermissionDeniedException {
712
        try(final ManagedCollectionLock collectionLock = lockManager.acquireCollectionReadLock(path)) {
1✔
713

714
            // lock the document
715
            final ManagedDocumentLock documentLock;
716
            final Runnable unlockFn = switch (lockMode) {
1✔
717
                case WRITE_LOCK -> {
718
                    documentLock = lockManager.acquireDocumentWriteLock(getURI().append(name.lastSegment()));
1✔
719
                    yield documentLock::close;
1✔
720
                }
721
                case READ_LOCK -> {
722
                    documentLock = lockManager.acquireDocumentReadLock(getURI().append(name.lastSegment()));
1✔
723
                    yield documentLock::close;
1✔
724
                }
725
                default -> {
726
                    documentLock = ManagedSingleLockDocumentLock.notLocked(getURI().append(name.lastSegment()));
1✔
727
                    yield () -> {
1✔
728
                    };
×
729
                }
730
            };    // we unlock on error, or if there is no Collection
731

732

733
            final DocumentImpl doc = documents.get(name.lastSegmentString());
1✔
734

735
            // NOTE: early release of Collection lock inline with Asymmetrical Locking scheme
736
            collectionLock.close();
1✔
737

738
            if(doc == null) {
1✔
739
                unlockFn.run();
1✔
740
                return null;
1✔
741
            } else {
742
                if(!doc.getPermissions().validate(broker.getCurrentSubject(), Permission.READ)) {
1✔
743
                    unlockFn.run();
1✔
744
                    throw new PermissionDeniedException("Permission denied to read + document: " + name.toString());
1✔
745
                }
746

747
                return new LockedDocument(documentLock, doc);
1✔
748
            }
749
        }
750
    }
751

752
    @Override
753
    public DocumentImpl getDocumentNoLock(final DBBroker broker, final String rawPath) throws PermissionDeniedException {
754
        final DocumentImpl doc = documents.get(rawPath);
×
755
        if(doc != null) {
×
756
            if(!doc.getPermissions().validate(broker.getCurrentSubject(), Permission.READ)) {
×
757
                throw new PermissionDeniedException("Permission denied to read document: " + rawPath);
×
758
            }
759
        }
760
        return doc;
×
761
    }
762

763
    @Override
764
    public int getDocumentCount(final DBBroker broker) throws PermissionDeniedException {
765
        try(final ManagedCollectionLock collectionLock = lockManager.acquireCollectionReadLock(path)) {
1✔
766
            if(!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.READ)) {
1!
767
                throw new PermissionDeniedException("Permission denied to read collection: " + path);
×
768
            }
769

770
            return documents.size();
1✔
771
        } catch(final LockException e) {
×
772
            LOG.warn(e.getMessage(), e);
×
773
            return -1;
×
774
        }
775
    }
776

777
    @Override
778
    public int getDocumentCountNoLock(final DBBroker broker) throws PermissionDeniedException {
779
        if(!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.READ)) {
1!
780
            throw new PermissionDeniedException("Permission denied to read collection: " + path);
×
781
        }
782
        return documents.size();
1✔
783
    }
784

785
    @Override
786
    public int getId() {
787
        return collectionId;
1✔
788
    }
789

790
    @Override
791
    public XmldbURI getURI() {
792
        return path;    //TODO(AR) we should have a READ_LOCK on here! but we can't as we need the URI to get the READ_LOCK... urgh!
1✔
793
    }
794

795
    /**
796
     * Returns the parent-collection.
797
     *
798
     * @return The parent-collection or null if this is the root collection.
799
     */
800
    @Override
801
    public XmldbURI getParentURI() {
802
        if(path.equals(XmldbURI.ROOT_COLLECTION_URI)) {
1✔
803
            return null;
1✔
804
        }
805
        //TODO : resolve URI against ".." !
806
         return path.removeLastSegment();
1✔
807
    }
808

809
    @Override
810
    final public Permission getPermissions() {
811
        try(final ManagedCollectionLock collectionLock = lockManager.acquireCollectionReadLock(path)) {
1✔
812
            return permissions;
1✔
813
        } catch(final LockException e) {
×
814
            LOG.error(e.getMessage(), e);
×
815
            return permissions;
×
816
        }
817
    }
818

819
    @Override
820
    public Permission getPermissionsNoLock() {
821
        return permissions;
1✔
822
    }
823

824
    @Deprecated
825
    @Override
826
    public CollectionMetadata getMetadata() {
827
        if (collectionMetadata == null) {
×
828
            collectionMetadata = new CollectionMetadata(this);
×
829
        }
830
        return collectionMetadata;
×
831
    }
832

833
    @Override
834
    public boolean hasDocument(final DBBroker broker, final XmldbURI name) throws PermissionDeniedException {
835
        try(final ManagedCollectionLock collectionLock = lockManager.acquireCollectionReadLock(path)) {
1✔
836
            if(!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.READ)) {
1!
837
                throw new PermissionDeniedException("Permission denied to read collection: " + path);
×
838
            }
839

840
            return documents.containsKey(name.lastSegmentString());
1✔
841
        } catch(final LockException e) {
×
842
            LOG.warn(e.getMessage(), e);
×
843
            //TODO : ouch ! Should we return at any price ? Without even logging ? -pb
844
            return documents.containsKey(name.lastSegmentString());
×
845
        }
846
    }
847

848
    @Override
849
    public boolean hasChildCollection(final DBBroker broker, final XmldbURI name) throws PermissionDeniedException, LockException {
850
        try(final ManagedCollectionLock collectionLock = lockManager.acquireCollectionReadLock(path)) {
1✔
851
            if(!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.READ)) {
1!
852
                throw new PermissionDeniedException("Permission denied to read collection: " + path);
×
853
            }
854

855
            return subCollections.contains(name);
1✔
856
        }
857
    }
858

859
    @Override
860
    public boolean hasChildCollectionNoLock(final DBBroker broker, final XmldbURI name) throws PermissionDeniedException {
861
        if(!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.READ)) {
×
862
            throw new PermissionDeniedException("Permission denied to read collection: " + path);
×
863
        }
864

865
        return subCollections.contains(name);
×
866
    }
867

868
    @Override
869
    public Iterator<DocumentImpl> iterator(final DBBroker broker) throws PermissionDeniedException, LockException {
870
        return getDocuments(broker, new DefaultDocumentSet()).getDocumentIterator();
1✔
871
    }
872

873
    @Override
874
    public Iterator<DocumentImpl> iteratorNoLock(final DBBroker broker) throws PermissionDeniedException {
875
        if(!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.READ)) {
1!
876
            throw new PermissionDeniedException("Permission denied to read collection: " + path);
×
877
        }
878
        
879
        return getDocumentsNoLock(broker, new DefaultDocumentSet()).getDocumentIterator();
1✔
880
    }
881

882
    /**
883
     * Serializes the Collection to a byte representation
884
     *
885
     * Counterpart method to {@link #deserialize(DBBroker, XmldbURI, VariableByteInput)}
886
     *
887
     * @param outputStream The output stream to write the collection contents to
888
     */
889
    @Override
890
    public void serialize(final VariableByteOutputStream outputStream) throws IOException, LockException {
891
        outputStream.writeInt(collectionId);
1✔
892

893
        final int size;
894
        final Iterator<XmldbURI> i;
895

896
        //TODO(AR) should we READ_LOCK the Collection to stop it being modified concurrently? see NativeBroker#saveCollection line 1801 - already has WRITE_LOCK ;-)
897
//        try(final ManagedCollectionLock collectionLock = lockManager.acquireCollectionReadLock(path)) {
898
            size = subCollections.size();
1✔
899
//            i = subCollections.stableIterator();
900
              i = subCollections.iterator();
1✔
901
//        }
902

903
        outputStream.writeInt(size);
1✔
904
        while(i.hasNext()) {
1✔
905
            final XmldbURI childCollectionURI = i.next();
1✔
906
            outputStream.writeUTF(childCollectionURI.toString());
1✔
907
        }
908
        permissions.write(outputStream);
1✔
909
        outputStream.writeLong(created);
1✔
910
    }
1✔
911

912
    @Override
913
    public void close() {
914
        //no-op
915
    }
1✔
916

917
    /**
918
     * Read collection contents from the stream
919
     *
920
     * Counterpart method to {@link #serialize(VariableByteOutputStream)}
921
     *
922
     * @param broker The database broker
923
     * @param path The path of the Collection
924
     * @param istream The input stream to deserialize the Collection from
925
     */
926
    private static MutableCollection deserialize(final DBBroker broker, final XmldbURI path, final VariableByteInput istream)
927
            throws IOException, PermissionDeniedException, LockException {
928
        final int collectionId = istream.readInt();
1✔
929
        if (collectionId < 0) {
1!
930
            throw new IOException("Internal error reading collection: invalid collection id");
×
931
        }
932

933
        final int collLen = istream.readInt();
1✔
934

935
        //TODO(AR) should we WRITE_LOCK the Collection to stop it being loaded from disk concurrently? see NativeBroker#openCollection line 1030 - already has READ_LOCK ;-)
936
//        try(final ManagedCollectionLock collectionLock = lockManager.acquireCollectionWriteLock(path, false)) {
937
            final LinkedHashSet<XmldbURI> subCollections = new LinkedHashSet<>(Math.max(16, collLen));
1✔
938
            for (int i = 0; i < collLen; i++) {
1✔
939
                subCollections.add(XmldbURI.create(istream.readUTF()));
1✔
940
            }
941

942
            final Permission permission = PermissionFactory.getDefaultCollectionPermission(broker.getBrokerPool().getSecurityManager());
1✔
943
            permission.read(istream);
1✔
944

945
            if (!permission.validate(broker.getCurrentSubject(), Permission.EXECUTE)) {
1!
946
                throw new PermissionDeniedException("Permission denied to open the Collection " + path);
×
947
            }
948

949
            final long created = istream.readLong();
1✔
950

951
            final LinkedHashMap<String, DocumentImpl> documents = new LinkedHashMap<>();
1✔
952

953
            final MutableCollection collection =
1✔
954
                new MutableCollection(broker, collectionId, path, permission, created,subCollections, documents);
1✔
955

956
            broker.getCollectionResources(new InternalAccess() {
1✔
957
                @Override
958
                public void addDocument(final DocumentImpl doc) throws EXistException {
959
                    doc.setCollection(collection);
1✔
960

961
                    if (doc.getDocId() == DocumentImpl.UNKNOWN_DOCUMENT_ID) {
1!
962
                        LOG.error("Document must have ID. [{}]", doc);
×
963
                        throw new EXistException("Document must have ID.");
×
964
                    }
965

966
                    documents.put(doc.getFileURI().lastSegmentString(), doc);
1✔
967
                }
1✔
968

969
                @Override
970
                public int getId() {
971
                    return collectionId;
1✔
972
                }
973
            });
974

975
            return collection;
1✔
976
//        }
977
    }
978

979
    @Override
980
    public void removeCollection(final DBBroker broker, final XmldbURI name)
981
            throws LockException, PermissionDeniedException {
982
        try(final ManagedCollectionLock collectionLock = lockManager.acquireCollectionWriteLock(path)) {
1✔
983
            if(!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.WRITE)) {
1!
984
                throw new PermissionDeniedException("Permission denied to read collection: " + path);
×
985
            }
986

987
            subCollections.remove(name);
1✔
988
        }
989
    }
1✔
990

991
    @Override
992
    public void removeResource(final Txn transaction, final DBBroker broker, final DocumentImpl doc)
993
            throws PermissionDeniedException, LockException, IOException, TriggerException {
994
        if (doc.getCollection() != this) {
1!
995
            throw new IOException("Document '" + doc.getURI() + "' does not belong to Collection '" + getURI() + "'.");
×
996
        }
997

998
        if(doc.getResourceType() == DocumentImpl.BINARY_FILE) {
1✔
999
            removeBinaryResource(transaction, broker, doc);
1✔
1000
        } else {
1✔
1001
            removeXMLResource(transaction, broker, doc.getFileURI());
1✔
1002
        }
1003
    }
1✔
1004

1005
    @Override
1006
    public void removeXMLResource(final Txn transaction, final DBBroker broker, final XmldbURI name)
1007
            throws PermissionDeniedException, TriggerException, LockException, IOException {
1008
        final BrokerPool db = broker.getBrokerPool();
1✔
1009

1010
        db.getProcessMonitor().startJob(ProcessMonitor.ACTION_REMOVE_XML, name);
1✔
1011
        try(final ManagedCollectionLock collectionLock = lockManager.acquireCollectionWriteLock(path)) {
1✔
1012
            if(!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.WRITE)) {
1✔
1013
                throw new PermissionDeniedException("Permission denied to write collection: " + path);
1✔
1014
            }
1015

1016
            try(final ManagedDocumentLock docUpdateLock = lockManager.acquireDocumentWriteLock(path.append(name.lastSegment()))) {
1✔
1017

1018
                final DocumentImpl doc = documents.get(name.lastSegmentString());
1✔
1019

1020
                if (doc == null) {
1!
1021
                    // NOTE: early release of Collection lock inline with Asymmetrical Locking scheme
1022
                    collectionLock.close();
×
1023

1024
                    return; //TODO should throw an exception!!! Otherwise we dont know if the document was removed
×
1025
                }
1026

1027
                try {
1028
                    boolean useTriggers = broker.isTriggersEnabled();
1✔
1029
                    if (CollectionConfiguration.DEFAULT_COLLECTION_CONFIG_FILE_URI.equals(name)) {
1✔
1030
                        // we remove a collection.xconf configuration file: tell the configuration manager to
1031
                        // reload the configuration.
1032
                        useTriggers = false;
1✔
1033
                        final CollectionConfigurationManager confMgr = broker.getBrokerPool().getConfigurationManager();
1✔
1034
                        if (confMgr != null) {
1!
1035
                            confMgr.invalidate(getURI(), broker.getBrokerPool());
1✔
1036
                        }
1037
                    }
1038

1039
                    final DocumentTriggers trigger = new DocumentTriggers(broker, transaction, null, this, useTriggers ? getConfiguration(broker) : null);
1✔
1040

1041
                    trigger.beforeDeleteDocument(broker, transaction, doc);
1✔
1042

1043
                    broker.removeXMLResource(transaction, doc);
1✔
1044
                    documents.remove(name.lastSegmentString());
1✔
1045

1046
                    trigger.afterDeleteDocument(broker, transaction, getURI().append(name));
1✔
1047

1048
                    broker.getBrokerPool().getNotificationService().notifyUpdate(doc, UpdateListener.REMOVE);
1✔
1049

1050
                } finally {
1✔
1051
                    broker.getBrokerPool().getProcessMonitor().endJob();
1✔
1052
                }
1053

1054
                // NOTE: early release of Collection lock inline with Asymmetrical Locking scheme
1055
                collectionLock.close();
1✔
1056
            }
1057
        }
1058
    }
1✔
1059

1060
    @Override
1061
    public void removeBinaryResource(final Txn transaction, final DBBroker broker, final XmldbURI name)
1062
            throws PermissionDeniedException, LockException, TriggerException {
1063
        try(final ManagedCollectionLock collectionLock = lockManager.acquireCollectionWriteLock(path)) {
1✔
1064
            if(!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.WRITE)) {
1!
1065
                throw new PermissionDeniedException("Permission denied to write collection: " + path);
×
1066
            }
1067

1068
            try(final ManagedDocumentLock docLock = lockManager.acquireDocumentWriteLock(path.append(name.lastSegment()))) {
1✔
1069
                final DocumentImpl doc = getDocument(broker, name);
1✔
1070
                removeBinaryResource(transaction, broker, doc);
1✔
1071

1072
                // NOTE: early release of Collection lock inline with Asymmetrical Locking scheme
1073
                collectionLock.close();
1✔
1074
            }
1075
        }
1076
    }
1✔
1077

1078
    @Override
1079
    public void removeBinaryResource(final Txn transaction, final DBBroker broker, final DocumentImpl doc)
1080
            throws PermissionDeniedException, LockException, TriggerException {
1081

1082
        if(doc == null) {
1!
1083
            return;  //TODO should throw an exception!!! Otherwise we dont know if the document was removed
×
1084
        }
1085

1086
        broker.getBrokerPool().getProcessMonitor().startJob(ProcessMonitor.ACTION_REMOVE_BINARY, doc.getFileURI());
1✔
1087
        try(final ManagedCollectionLock collectionLock = lockManager.acquireCollectionWriteLock(path)) {
1✔
1088
            if(!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.WRITE)) {
1!
1089
                throw new PermissionDeniedException("Permission denied to write collection: " + path);
×
1090
            }
1091

1092
            if (doc.getResourceType() != DocumentImpl.BINARY_FILE) {
1!
1093
                throw new PermissionDeniedException("document " + doc.getFileURI() + " is not a binary object");
×
1094
            }
1095

1096
            try(final ManagedDocumentLock docUpdateLock = lockManager.acquireDocumentWriteLock(doc.getURI())) {
1✔
1097
                try {
1098
                    final DocumentTriggers trigger = new DocumentTriggers(broker, transaction, null, this, broker.isTriggersEnabled() ? getConfiguration(broker) : null);
1!
1099

1100
                    trigger.beforeDeleteDocument(broker, transaction, doc);
1✔
1101

1102
                    final IndexController indexController = broker.getIndexController();
1✔
1103
                    final StreamListener listener = indexController.getStreamListener(doc, StreamListener.ReindexMode.REMOVE_BINARY);
1✔
1104
                    try {
1105
                        indexController.startIndexDocument(transaction, listener);
1✔
1106

1107
                        try {
1108
                            broker.removeBinaryResource(transaction, (BinaryDocument) doc);
1✔
1109
                        } catch (final IOException ex) {
1✔
1110
                            throw new PermissionDeniedException("Cannot delete file: " + doc.getURI().toString() + ": " + ex.getMessage(), ex);
×
1111
                        }
1112
                        documents.remove(doc.getFileURI().lastSegmentString());
1✔
1113
                    } finally {
1✔
1114
                        indexController.endIndexDocument(transaction, listener);
1✔
1115
                    }
1116

1117
                    trigger.afterDeleteDocument(broker, transaction, doc.getURI());
1✔
1118

1119
                } finally {
1✔
1120
                    broker.getBrokerPool().getProcessMonitor().endJob();
1✔
1121
                }
1122

1123
                // NOTE: early release of Collection lock inline with Asymmetrical Locking scheme
1124
                collectionLock.close();
1✔
1125
            }
1126
        }
1127
    }
1✔
1128

1129
    @Override
1130
    public void storeDocument(final Txn transaction, final DBBroker broker, final XmldbURI name, final InputSource source, @Nullable MimeType mimeType) throws EXistException, PermissionDeniedException, SAXException, LockException, IOException {
1131
        storeDocument(transaction, broker, name, source, mimeType, null, null, null, null, null);
1✔
1132
    }
1✔
1133

1134
    @Override
1135
    public void storeDocument(final Txn transaction, final DBBroker broker, final XmldbURI name, final InputSource source, @Nullable MimeType mimeType, final @Nullable Date createdDate, final @Nullable Date lastModifiedDate, final @Nullable Permission permission, final @Nullable DocumentType documentType, @Nullable final XMLReader xmlReader) throws EXistException, PermissionDeniedException, SAXException, LockException, IOException {
1136
        if (mimeType == null) {
1✔
1137
            mimeType = MimeType.BINARY_TYPE;
1✔
1138
        }
1139

1140
        if (mimeType.isXMLType()) {
1✔
1141
            // Store XML Document
1142

1143
            final BiConsumer2E<XMLReader, IndexInfo, SAXException, EXistException> validatorFn = (xmlReader1, validateIndexInfo) -> {
1✔
1144
                validateIndexInfo.setReader(xmlReader1, null);
1✔
1145
                try {
1146
                      xmlReader1.parse(source);
1✔
1147
                } catch(final SAXException e) {
1✔
1148
                    throw new SAXException("The XML parser reported a problem: " + e.getMessage(), e);
1✔
1149
                } catch(final IOException e) {
×
1150
                    throw new EXistException(e);
×
1151
                }
1152
            };
1✔
1153

1154
            final BiConsumer2E<XMLReader, IndexInfo, SAXException, EXistException> parserFn = (xmlReader1, storeIndexInfo) -> {
1✔
1155
                try {
1156
                    storeIndexInfo.setReader(xmlReader1, null);
1✔
1157
                    xmlReader1.parse(source);
1✔
1158
                } catch(final IOException e) {
1✔
1159
                    throw new EXistException(e);
×
1160
                }
1161
            };
1✔
1162

1163
            storeXmlDocument(transaction, broker, name, mimeType, createdDate, lastModifiedDate, permission, documentType, xmlReader, validatorFn, parserFn);
1✔
1164

1165
        } else {
1✔
1166
            // Store Binary Document
1167
            try (final InputStream is = source.getByteStream()) {
1✔
1168
                if (is == null) {
1!
1169
                    throw new IOException("storeDocument received a null InputStream when trying to store a Binary Document");
×
1170
                }
1171
                addBinaryResource(transaction, broker, name, is, mimeType.getName(), -1, createdDate, lastModifiedDate, permission);
1✔
1172
            }
1173
        }
1174
    }
1✔
1175

1176
    @Override
1177
    public void storeDocument(final Txn transaction, final DBBroker broker, final XmldbURI name, final Node node, @Nullable MimeType mimeType) throws EXistException, PermissionDeniedException, SAXException, LockException, IOException {
1178
        storeDocument(transaction, broker, name, node, mimeType, null, null, null, null, null);
1✔
1179
    }
1✔
1180

1181
    @Override
1182
    public void storeDocument(final Txn transaction, final DBBroker broker, final XmldbURI name, final Node node, @Nullable MimeType mimeType, final @Nullable Date createdDate, final @Nullable Date lastModifiedDate, final @Nullable Permission permission, final @Nullable DocumentType documentType, @Nullable final XMLReader xmlReader) throws EXistException, PermissionDeniedException, SAXException, LockException, IOException {
1183
        if (mimeType == null) {
1!
1184
            mimeType = MimeType.BINARY_TYPE;
×
1185
        }
1186

1187
        if (mimeType.isXMLType()) {
1!
1188
            // Store XML Document
1189
            final BiConsumer2E<XMLReader, IndexInfo, SAXException, EXistException> validatorFn = (xmlReader1, validateIndexInfo) -> {
1✔
1190
                validateIndexInfo.setReader(xmlReader1, null);
1✔
1191
                validateIndexInfo.setDOMStreamer(new DOMStreamer());
1✔
1192
                validateIndexInfo.getDOMStreamer().serialize(node, true);
1✔
1193
            };
1✔
1194

1195
            final BiConsumer2E<XMLReader, IndexInfo, SAXException, EXistException> parserFn = (xmlReader1, storeIndexInfo) -> {
1✔
1196
                storeIndexInfo.setReader(xmlReader1, null);
1✔
1197
                storeIndexInfo.getDOMStreamer().serialize(node, true);
1✔
1198
            };
1✔
1199

1200
            storeXmlDocument(transaction, broker, name, mimeType, createdDate, lastModifiedDate, permission, documentType, xmlReader, validatorFn, parserFn);
1✔
1201

1202
        } else {
1✔
1203
            throw new EXistException("Cannot store DOM Node as a Binary Document to URI: " + getURI().append(name));
×
1204
        }
1205
    }
1✔
1206

1207
    private void storeXmlDocument(final Txn transaction, final DBBroker broker, final XmldbURI name, final MimeType mimeType, final @Nullable Date createdDate, final @Nullable Date lastModifiedDate, final @Nullable Permission permission, final @Nullable DocumentType documentType, @Nullable final XMLReader xmlReader, final BiConsumer2E<XMLReader, IndexInfo, SAXException, EXistException> validatorFn, final BiConsumer2E<XMLReader, IndexInfo, SAXException, EXistException> parserFn) throws EXistException, PermissionDeniedException, SAXException, LockException, IOException {
1208
        final CollectionConfiguration colconf = getConfiguration(broker);
1✔
1209

1210
        // borrow a default XML Reader if needed
1211
        boolean borrowed = false;
1✔
1212
        final XMLReader xmlReader1;
1213
        if (xmlReader != null) {
1!
1214
            xmlReader1 = xmlReader;
×
1215
        } else {
×
1216
            xmlReader1 = getReader(broker, true, colconf);
1✔
1217
            borrowed = true;
1✔
1218
        }
1219

1220
        try {
1221
            // Phase 1 of 3 - Validate the Document
1222
            final IndexInfo indexInfo = validateXMLResourceInternal(transaction, broker, name, colconf, validatorIndexInfo -> validatorFn.accept(xmlReader1, validatorIndexInfo));
1✔
1223

1224
            // Phase 2 of 3 - Set the metadata for the document
1225
            final DocumentImpl document = indexInfo.getDocument();
1✔
1226
            document.setMimeType(mimeType.getName());
1✔
1227
            if (createdDate != null) {
1✔
1228
                document.setCreated(createdDate.getTime());
1✔
1229
                if (lastModifiedDate == null) {
1!
1230
                    document.setLastModified(createdDate.getTime());
×
1231
                }
1232
            }
1233
            if (lastModifiedDate != null) {
1✔
1234
                document.setLastModified(lastModifiedDate.getTime());
1✔
1235
            }
1236
            if (permission != null) {
1✔
1237
                document.setPermissions(permission);
1✔
1238
            }
1239
            if (documentType != null) {
1!
1240
                document.setDocumentType(documentType);
×
1241
            }
1242

1243
            // Phase 3 of 3 - Store the Document
1244
            storeXMLInternal(transaction, broker, indexInfo, storeIndexInfo -> parserFn.accept(xmlReader1, storeIndexInfo));
1✔
1245
        } finally {
1✔
1246
            if (borrowed) {
1!
1247
                releaseReader(broker, xmlReader1);
1✔
1248
            }
1249
        }
1250
    }
1✔
1251

1252
    @Deprecated
1253
    @Override
1254
    public void store(final Txn transaction, final DBBroker broker, final IndexInfo info, final InputSource source)
1255
            throws EXistException, PermissionDeniedException, TriggerException, SAXException, LockException {
1256
        final XMLReader reader = getReader(broker, false, info.getCollectionConfig());
×
1257
        try {
1258
            store(transaction, broker, info, source, reader);
×
1259
        } finally {
×
1260
            releaseReader(broker, reader);
×
1261
        }
1262
    }
×
1263

1264
    @Deprecated
1265
    @Override
1266
    public void store(final Txn transaction, final DBBroker broker, final IndexInfo info, final InputSource source, final XMLReader reader)
1267
            throws EXistException, PermissionDeniedException, TriggerException, SAXException, LockException {
1268
        storeXMLInternal(transaction, broker, info, storeInfo -> {
×
1269
            try {
1270
                final InputStream is = source.getByteStream();
×
1271
                if(is != null && is.markSupported()) {
×
1272
                    is.reset();
×
1273
                } else {
×
1274
                    final Reader cs = source.getCharacterStream();
×
1275
                    if(cs != null && cs.markSupported()) {
×
1276
                        cs.reset();
×
1277
                    }
1278
                }
1279
            } catch(final IOException e) {
×
1280
                // mark is not supported: exception is expected, do nothing
1281
                LOG.debug("InputStream or CharacterStream underlying the InputSource does not support marking and therefore cannot be re-read.");
×
1282
            }
1283
            storeInfo.setReader(reader, null);
×
1284
            try {
1285
                reader.parse(source);
×
1286
            } catch(final IOException e) {
×
1287
                throw new EXistException(e);
×
1288
            }
1289
        });
×
1290
    }
×
1291

1292
    @Deprecated
1293
    @Override
1294
    public void store(final Txn transaction, final DBBroker broker, final IndexInfo info, final String data)
1295
            throws EXistException, PermissionDeniedException, TriggerException, SAXException, LockException {
1296
        storeXMLInternal(transaction, broker, info, storeInfo -> {
×
1297
            final CollectionConfiguration colconf = storeInfo.getDocument().getCollection().getConfiguration(broker);
×
1298
            final XMLReader reader = getReader(broker, false, colconf);
×
1299
            try {
1300
                storeInfo.setReader(reader, null);
×
1301
                reader.parse(new InputSource(new StringReader(data)));
×
1302
            } catch(final IOException e) {
×
1303
                throw new EXistException(e);
×
1304
            } finally {
1305
                releaseReader(broker, reader);
×
1306
            }
1307
        });
×
1308
    }
×
1309

1310
    @Deprecated
1311
    @Override
1312
    public void store(final Txn transaction, final DBBroker broker, final IndexInfo info, final Node node)
1313
            throws EXistException, PermissionDeniedException, TriggerException, SAXException, LockException {
1314
        if(!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.WRITE)) {
×
1315
            throw new PermissionDeniedException("Permission denied to write collection: " + path);
×
1316
        }
1317

1318
        storeXMLInternal(transaction, broker, info, storeInfo -> storeInfo.getDOMStreamer().serialize(node, true));
×
1319
    }
×
1320

1321
    /** 
1322
     * Stores an XML document in the database. {@link #validateXMLResourceInternal(Txn, DBBroker, XmldbURI,
1323
     * CollectionConfiguration, Consumer2E)}should have been called previously in order to acquire a write lock
1324
     * for the document. Launches the finish trigger.
1325
     *
1326
     * @param transaction The database transaction
1327
     * @param broker      The database broker
1328
     * @param info        Tracks information between validate and store phases
1329
     * @param parserFn    A function which parses the XML document
1330
     */
1331
    private void storeXMLInternal(final Txn transaction, final DBBroker broker, final IndexInfo info,
1332
            final Consumer2E<IndexInfo, EXistException, SAXException> parserFn)
1333
            throws EXistException, SAXException, PermissionDeniedException {
1334
        
1335
        final DocumentImpl document = info.getIndexer().getDocument();
1✔
1336
        
1337
        final Database db = broker.getBrokerPool();
1✔
1338
        
1339
        try {
1340
            /* TODO
1341
             * 
1342
             * These security checks are temporarily disabled because throwing an exception
1343
             * here may cause the database to corrupt.
1344
             * Why would the database corrupt? Because validateXMLInternal that is typically
1345
             * called before this method actually modifies the database and this collection,
1346
             * so throwing an exception here leaves the database in an inconsistent state
1347
             * with data 1/2 added/updated.
1348
             * 
1349
             * The downside of disabling these checks here is that: this collection is not locked
1350
             * between the call to validateXmlInternal and storeXMLInternal, which means that if
1351
             * UserA in ThreadA calls validateXmlInternal and is permitted access to store a resource,
1352
             * and then UserB in ThreadB modifies the permissions of this collection to prevent UserA
1353
             * from storing the document, when UserA reaches here (storeXMLInternal) they will still
1354
             * be allowed to store their document. However the next document that UserA attempts to store
1355
             * will be forbidden by validateXmlInternal and so the security transgression whilst not ideal
1356
             * is short-lived.
1357
             * 
1358
             * To fix the issue we need to refactor validateXMLInternal and move any document/database/collection
1359
             * modification code into storeXMLInternal after the commented out permissions checks below.
1360
             * 
1361
             * Noted by Adam Retter 2012-02-01T19:18
1362
             */
1363
            
1364
            /*
1365
            if(info.isCreating()) {
1366
                // create
1367
                * 
1368
                if(!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.WRITE)) {
1369
                    throw new PermissionDeniedException("Permission denied to write collection: " + path);
1370
                }
1371
            } else {
1372
                // update
1373

1374
                final Permission oldDocPermissions = info.getOldDocPermissions();
1375
                if(!((oldDocPermissions.getOwner().getId() != broker.getCurrentSubject().getId()) | (oldDocPermissions.validate(broker.getCurrentSubject(), Permission.WRITE)))) {
1376
                    throw new PermissionDeniedException("A resource with the same name already exists in the target collection '" + path + "', and you do not have write access on that resource.");
1377
                }
1378
            }
1379
            */
1380

1381
            if(LOG.isDebugEnabled()) {
1!
1382
                LOG.debug("storing document {} ...", document.getDocId());
×
1383
            }
1384

1385
            //Sanity check
1386
            if(!lockManager.isDocumentLockedForWrite(document.getURI())) {
1✔
1387
                LOG.warn("document is not locked for write !");
1✔
1388
            }
1389
            
1390
            db.getProcessMonitor().startJob(ProcessMonitor.ACTION_STORE_DOC, document.getFileURI());
1✔
1391
            parserFn.accept(info);
1✔
1392
            broker.storeXMLResource(transaction, document);
1✔
1393
            broker.flush();
1✔
1394
            broker.closeDocument();
1✔
1395
            //broker.checkTree(document);
1396
            LOG.debug("document stored.");
1✔
1397
        } finally {
1✔
1398
            //This lock has been acquired in validateXMLResourceInternal()
1399
            info.getDocumentLock().close();
1✔
1400
            broker.getBrokerPool().getProcessMonitor().endJob();
1✔
1401
        }
1402
        
1403
        if(info.isCreating()) {
1✔
1404
            info.getTriggers().afterCreateDocument(broker, transaction, document);
1✔
1405
        } else {
1✔
1406
            final StreamListener listener = broker.getIndexController().getStreamListener();
1✔
1407
            listener.endReplaceDocument(transaction);
1✔
1408

1409
            info.getTriggers().afterUpdateDocument(broker, transaction, document);
1✔
1410
        }
1411
        
1412
        db.getNotificationService().notifyUpdate(document, (info.isCreating() ? UpdateListener.ADD : UpdateListener.UPDATE));
1✔
1413
        //Is it a collection configuration file ?
1414
        final XmldbURI docName = document.getFileURI();
1✔
1415
        //WARNING : there is no reason to lock the collection since setPath() is normally called in a safe way
1416
        //TODO: *resolve* URI against CollectionConfigurationManager.CONFIG_COLLECTION_URI 
1417
        if (getURI().startsWith(XmldbURI.CONFIG_COLLECTION_URI)
1✔
1418
                && docName.endsWith(CollectionConfiguration.COLLECTION_CONFIG_SUFFIX_URI)) {
1!
1419
            broker.sync(Sync.MAJOR);
1✔
1420
            final CollectionConfigurationManager manager = broker.getBrokerPool().getConfigurationManager();
1✔
1421
            if(manager != null) {
1!
1422
                try {
1423
                    manager.invalidate(getURI(), broker.getBrokerPool());
1✔
1424
                    manager.loadConfiguration(broker, this);
1✔
1425
                } catch(final PermissionDeniedException | LockException pde) {
1✔
1426
                    throw new EXistException(pde.getMessage(), pde);
×
1427
                } catch(final CollectionConfigurationException e) {
×
1428
                    // DIZ: should this exception really been thrown? bugid=1807744
1429
                    throw new EXistException("Error while reading new collection configuration: " + e.getMessage(), e);
×
1430
                }
1431
            }
1432
        }
1433
    }
1✔
1434

1435
    @Deprecated
1436
    @Override
1437
    public IndexInfo validateXMLResource(final Txn transaction, final DBBroker broker, final XmldbURI name, final String data) throws EXistException, PermissionDeniedException, TriggerException, SAXException, LockException, IOException {
1438
        return validateXMLResource(transaction, broker, name, new InputSource(new StringReader(data)));
×
1439
    }
1440

1441
    @Deprecated
1442
    @Override
1443
    public IndexInfo validateXMLResource(final Txn transaction, final DBBroker broker, final XmldbURI name, final InputSource source) throws EXistException, PermissionDeniedException, TriggerException, SAXException, LockException, IOException {
1444
        final CollectionConfiguration colconf = getConfiguration(broker);
×
1445
        final XMLReader reader = getReader(broker, true, colconf);
×
1446
        try {
1447
            return validateXMLResource(transaction, broker, name, colconf, source, reader);
×
1448
        } finally {
1449
            releaseReader(broker, reader);
×
1450
        }
1451
    }
1452

1453
    @Deprecated
1454
    @Override
1455
    public IndexInfo validateXMLResource(final Txn transaction, final DBBroker broker, final XmldbURI name, final InputSource source, final XMLReader reader) throws EXistException, PermissionDeniedException, TriggerException, SAXException, LockException, IOException {
1456
        final CollectionConfiguration colconf = getConfiguration(broker);
×
1457
        return validateXMLResource(transaction, broker, name, colconf, source, reader);
×
1458
    }
1459

1460
    private IndexInfo validateXMLResource(final Txn transaction, final DBBroker broker, final XmldbURI name, final CollectionConfiguration colconf, final InputSource source, final XMLReader reader) throws EXistException, PermissionDeniedException, TriggerException, SAXException, LockException, IOException {
1461
        return validateXMLResourceInternal(transaction, broker, name, colconf, (info) -> {
×
1462
            info.setReader(reader, null);
×
1463
            try {
1464

1465
                /*
1466
                 * Note - we must close shield the input source,
1467
                 * else it can be closed by the Reader, so subsequently
1468
                 * when we try and read it in storeXmlInternal we will get
1469
                 * an exception.
1470
                 */
1471
                final InputSource closeShieldedInputSource = closeShieldInputSource(source);
×
1472

1473
                reader.parse(closeShieldedInputSource);
×
1474
            } catch(final SAXException e) {
×
1475
                throw new SAXException("The XML parser reported a problem: " + e.getMessage(), e);
×
1476
            } catch(final IOException e) {
×
1477
                throw new EXistException(e);
×
1478
            }
1479
        });
×
1480
    }
1481

1482
    /**
1483
     * Creates a new InputSource that prevents the streams
1484
     * and readers of the InputSource from being closed.
1485
     *
1486
     * @param source the input source
1487
     *
1488
     * @return a new input source
1489
     */
1490
    private InputSource closeShieldInputSource(final InputSource source) {
1491
        final InputSource protectedInputSource = new InputSource();
×
1492
        protectedInputSource.setEncoding(source.getEncoding());
×
1493
        protectedInputSource.setSystemId(source.getSystemId());
×
1494
        protectedInputSource.setPublicId(source.getPublicId());
×
1495
        
1496
        if (source.getByteStream() != null) {
×
1497
            //TODO(AR) consider AutoCloseInputStream
1498
            final InputStream closeShieldByteStream = CloseShieldInputStream.wrap(source.getByteStream());
×
1499
            protectedInputSource.setByteStream(closeShieldByteStream);
×
1500
        }
1501
        
1502
        if (source.getCharacterStream() != null) {
×
1503
            //TODO(AR) consider AutoCloseReader
1504
            final Reader closeShieldReader = CloseShieldReader.wrap(source.getCharacterStream());
×
1505
            protectedInputSource.setCharacterStream(closeShieldReader);
×
1506
        }
1507
        
1508
        return protectedInputSource;
×
1509
    }
1510

1511
    @Deprecated
1512
    @Override
1513
    public IndexInfo validateXMLResource(final Txn transaction, final DBBroker broker, final XmldbURI name, final Node node) throws EXistException, PermissionDeniedException, TriggerException, SAXException, LockException, IOException {
1514
        return validateXMLResourceInternal(transaction, broker, name, getConfiguration(broker), (info) -> {
×
1515
                info.setDOMStreamer(new DOMStreamer());
×
1516
                info.getDOMStreamer().serialize(node, true);
×
1517
        });
×
1518
    }
1519

1520
    /** 
1521
     * Validates an XML document et prepares it for further storage. Launches prepare and postValidate triggers.
1522
     * Since the process is dependant from the collection configuration, the collection acquires a write lock during
1523
     * the process.
1524
     *
1525
     * @param transaction The database transaction
1526
     * @param broker      The database broker
1527
     * @param name        the name (without path) of the document
1528
     * @param validator   A function which validates the document of throws an Exception
1529
     * 
1530
     * @return An {@link IndexInfo} with a write lock on the document.
1531
     */
1532
    private IndexInfo validateXMLResourceInternal(final Txn transaction, final DBBroker broker, final XmldbURI name,
1533
            final CollectionConfiguration config, final Consumer2E<IndexInfo, SAXException, EXistException> validator)
1534
            throws EXistException, PermissionDeniedException, TriggerException, SAXException, LockException,
1535
            IOException {
1536

1537
        //Make the necessary operations if we process a collection configuration document
1538
        checkConfigurationDocument(transaction, broker, name);
1✔
1539
        
1540
        final Database db = broker.getBrokerPool();
1✔
1541
        
1542
        if (db.isReadOnly()) {
1!
1543
            throw new IOException("Database is read-only");
×
1544
        }
1545

1546
        ManagedDocumentLock documentWriteLock = null;
1✔
1547
        DocumentImpl oldDoc = null;
1✔
1548

1549
        db.getProcessMonitor().startJob(ProcessMonitor.ACTION_VALIDATE_DOC, name);
1✔
1550
        try {
1551
            try(final ManagedCollectionLock collectionLock = lockManager.acquireCollectionWriteLock(path)) {
1✔
1552

1553
                // acquire the WRITE_LOCK on the Document, this lock is released in storeXMLInternal via IndexInfo
1554
                documentWriteLock = lockManager.acquireDocumentWriteLock(getURI().append(name.lastSegment()));
1✔
1555

1556
                oldDoc = documents.get(name.lastSegmentString());
1✔
1557
                checkPermissionsForAddDocument(broker, oldDoc);
1✔
1558

1559
                // NOTE: the new `document` object actually gets discarded in favour of the `oldDoc` below if there is an oldDoc and it is XML (so we can use -1 as the docId because it will never be used)
1560
                final int docId = (oldDoc != null && oldDoc.getResourceType() == DocumentImpl.XML_FILE) ? - 1 : broker.getNextResourceId(transaction);
1✔
1561
                DocumentImpl document = new DocumentImpl(null, (BrokerPool) db, this, docId, name);
1✔
1562

1563
                checkCollectionConflict(name);
1✔
1564
                manageDocumentInformation(oldDoc, document);
1✔
1565
                final Indexer indexer = new Indexer(broker, transaction);
1✔
1566

1567
                final IndexInfo info = new IndexInfo(indexer, config, documentWriteLock);
1✔
1568
                info.setCreating(oldDoc == null);
1✔
1569
                info.setOldDocPermissions(oldDoc != null ? oldDoc.getPermissions() : null);
1✔
1570
                indexer.setDocument(document, config);
1✔
1571
                indexer.setValidating(true);
1✔
1572

1573
                final DocumentTriggers trigger = new DocumentTriggers(broker, transaction, indexer, this, broker.isTriggersEnabled() ? config : null);
1✔
1574
                trigger.setValidating(true);
1✔
1575

1576
                info.setTriggers(trigger);
1✔
1577

1578
                if (oldDoc == null) {
1✔
1579
                    trigger.beforeCreateDocument(broker, transaction, getURI().append(name));
1✔
1580
                } else {
1✔
1581
                    trigger.beforeUpdateDocument(broker, transaction, oldDoc);
1✔
1582
                }
1583

1584
                if (LOG.isDebugEnabled()) {
1!
1585
                    LOG.debug("Scanning document {}", getURI().append(name));
×
1586
                }
1587

1588
                validator.accept(info);
1✔
1589
                // new document is valid: remove old document
1590
                if (oldDoc != null) {
1✔
1591
                    if (LOG.isDebugEnabled()) {
1!
1592
                        LOG.debug("removing old document {}", oldDoc.getFileURI());
×
1593
                    }
1594
                    updateModificationTime(document);
1✔
1595

1596
                    /**
1597
                     * Matching {@link StreamListener#endReplaceDocument(Txn)} call is in
1598
                     * {@link #storeXMLInternal(Txn, DBBroker, IndexInfo, Consumer2E)}
1599
                     */
1600
                    final StreamListener listener = broker.getIndexController().getStreamListener(document, StreamListener.ReindexMode.REPLACE_DOCUMENT);
1✔
1601
                    listener.startReplaceDocument(transaction);
1✔
1602

1603
                    if (oldDoc.getResourceType() == DocumentImpl.BINARY_FILE) {
1✔
1604
                        //TODO : use a more elaborated method ? No triggers...
1605
                        broker.removeBinaryResource(transaction, (BinaryDocument) oldDoc);
1✔
1606
                        documents.remove(oldDoc.getFileURI().lastSegmentString());
1✔
1607

1608
                        addDocument(transaction, broker, document);
1✔
1609
                    } else {
1✔
1610
                        //TODO : use a more elaborated method ? No triggers...
1611
                        broker.removeXMLResource(transaction, oldDoc, false);
1✔
1612
                        oldDoc.copyOf(broker, document, oldDoc);
1✔
1613
                        indexer.setDocumentObject(oldDoc);
1✔
1614
                        //old has become new at this point
1615
                        document = oldDoc;
1✔
1616
                    }
1617

1618
                    if (LOG.isDebugEnabled()) {
1!
1619
                        LOG.debug("removed old document {}", oldDoc.getFileURI());
×
1620
                    }
1621
                } else {
×
1622
                    addDocument(transaction, broker, document);
1✔
1623
                }
1624

1625
                trigger.setValidating(false);
1✔
1626

1627
                return info;
1✔
1628
            }
1629
        } catch(final EXistException | PermissionDeniedException | SAXException | LockException | IOException e) {
1✔
1630
            // if there is an exception and we hold the document WRITE_LOCK we must release it
1631
            if(documentWriteLock != null) {
1!
1632
                documentWriteLock.close();
1✔
1633
            }
1634
            throw e;
1✔
1635
        } finally {
1636
            db.getProcessMonitor().endJob();
1✔
1637
        }
1638
    }
1639

1640
    private void checkConfigurationDocument(final Txn transaction, final DBBroker broker, final XmldbURI docUri) throws EXistException, PermissionDeniedException, LockException {
1641
        //Is it a collection configuration file ?
1642
        //TODO : use XmldbURI.resolve() !
1643
        if (!getURI().startsWith(XmldbURI.CONFIG_COLLECTION_URI)) {
1✔
1644
            return;
1✔
1645
        }
1646
        if(!docUri.endsWith(CollectionConfiguration.COLLECTION_CONFIG_SUFFIX_URI)) {
1!
1647
            return;
×
1648
        }
1649
        //Allow just one configuration document per collection
1650
        //TODO : do not throw the exception if a system property allows several ones -pb
1651
        for(final Iterator<DocumentImpl> i = iterator(broker); i.hasNext(); ) {
1✔
1652
            final DocumentImpl confDoc = i.next();
1✔
1653
            final XmldbURI currentConfDocName = confDoc.getFileURI();
1✔
1654
            if(currentConfDocName != null && !currentConfDocName.equals(docUri)) {
1!
1655
                throw new EXistException("Could not store configuration '" + docUri + "': A configuration document with a different name ("
1✔
1656
                    + currentConfDocName + ") already exists in this collection (" + getURI() + ")");
1✔
1657
            }
1658
        }
1659
        //broker.saveCollection(transaction, this);
1660
        //CollectionConfigurationManager confMgr = broker.getBrokerPool().getConfigurationManager();
1661
        //if(confMgr != null)
1662
            //try {
1663
                //confMgr.reload(broker, this);
1664
            // catch (CollectionConfigurationException e) {
1665
                //throw new EXistException("An error occurred while reloading the updated collection configuration: " + e.getMessage(), e);
1666
        //}
1667
    }
1✔
1668

1669
    /**
1670
     * If an old document exists, keep information about  the document.
1671
     *
1672
     * @param oldDoc The old document
1673
     * @param document The current/new document
1674
     */
1675
    private void manageDocumentInformation(final DocumentImpl oldDoc, final DocumentImpl document) {
1676
        if (oldDoc != null) {
1✔
1677
            document.setCreated(oldDoc.getCreated());
1✔
1678
            document.setPermissions(oldDoc.getPermissions());
1✔
1679
        } else {
1✔
1680
            document.setCreated(System.currentTimeMillis());
1✔
1681
        }
1682
    }
1✔
1683

1684
     /**
1685
      * Update the modification time of a document
1686
      *
1687
      * @param document The document whose modification time should be updated
1688
      */
1689
    private void updateModificationTime(final DocumentImpl document) {
1690
        document.setLastModified(System.currentTimeMillis());
1✔
1691
    }
1✔
1692
    
1693
    /**
1694
     * Check Permissions about user and document when a document is added to the database,
1695
     * and throw exceptions if necessary.
1696
     *
1697
     * @param broker The database broker
1698
     * @param oldDoc old Document existing in database prior to adding a new one with same name, or null if none exists
1699
     */
1700
    private void checkPermissionsForAddDocument(final DBBroker broker, final DocumentImpl oldDoc)
1701
            throws LockException, PermissionDeniedException {
1702
        
1703
        // do we have execute permission on the collection?
1704
        if(!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.EXECUTE)) {
1!
1705
            throw new PermissionDeniedException("Execute permission is not granted on the Collection.");
×
1706
        }
1707
            
1708
        if(oldDoc != null) {   
1✔
1709
            
1710
            /* update document */
1711

1712
            LOG.debug("Found old doc {}", oldDoc.getDocId());
1✔
1713
            
1714
            // check if the document is locked by another user
1715
            final Account lockUser = oldDoc.getUserLock();
1✔
1716
            if(lockUser != null && !lockUser.equals(broker.getCurrentSubject())) {
1!
1717
                throw new PermissionDeniedException("The document is locked by user '" + lockUser.getName() + "'.");
×
1718
            }
1719
            
1720
            // do we have write permission on the old document or are we the owner of the old document?
1721
            if (!((oldDoc.getPermissions().getOwner().getId() == broker.getCurrentSubject().getId()) || (oldDoc.getPermissions().validate(broker.getCurrentSubject(), Permission.WRITE)))) {
1✔
1722
                throw new PermissionDeniedException("A resource with the same name already exists in the target collection '" + path + "', and you do not have write access on that resource.");
1✔
1723
            }
1724
        } else {
1725
            
1726
            /* create document */
1727
            
1728
            if(!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.WRITE)) {
1✔
1729
                throw new PermissionDeniedException("Write permission is not granted on the Collection.");
1✔
1730
            }
1731
        }
1732
    }
1✔
1733
    
1734
    private void checkCollectionConflict(final XmldbURI docUri) throws EXistException, PermissionDeniedException {
1735
        if(subCollections.contains(docUri.lastSegment())) {
1✔
1736
            throw new EXistException(
1✔
1737
                "The collection '" + getURI() + "' already has a sub-collection named '" + docUri.lastSegment() + "', you cannot create a Document with the same name as an existing collection."
1✔
1738
            );
1739
        }
1740
    }
1✔
1741

1742
    @Deprecated
1743
    @Override
1744
    public BinaryDocument addBinaryResource(final Txn transaction, final DBBroker broker, final XmldbURI name, final byte[] data, final String mimeType) throws EXistException, PermissionDeniedException, LockException, TriggerException,IOException {
1745
        return addBinaryResource(transaction, broker, name, data, mimeType, null, null);
×
1746
    }
1747

1748
    @Deprecated
1749
    @Override
1750
    public BinaryDocument addBinaryResource(final Txn transaction, final DBBroker broker, final XmldbURI name, final byte[] data, final String mimeType, final Date created, final Date modified) throws EXistException, PermissionDeniedException, LockException, TriggerException,IOException {
1751
        return addBinaryResource(transaction, broker, name, new UnsynchronizedByteArrayInputStream(data), mimeType, data.length, created, modified);
×
1752
    }
1753

1754
    @Deprecated
1755
    @Override
1756
    public BinaryDocument addBinaryResource(final Txn transaction, final DBBroker broker, final XmldbURI name, final InputStream is, final String mimeType, final long size) throws EXistException, PermissionDeniedException, LockException, TriggerException,IOException {
1757
        return addBinaryResource(transaction, broker, name, is, mimeType, size, null, null);
×
1758
    }
1759

1760
    @Deprecated
1761
    @Override
1762
    public BinaryDocument addBinaryResource(final Txn transaction, final DBBroker broker, final XmldbURI name,
1763
            final InputStream is, final String mimeType, final long size, final Date created, final Date modified)
1764
            throws EXistException, PermissionDeniedException, LockException, TriggerException, IOException {
1765
        return addBinaryResource(transaction, broker, name, is, mimeType, size, created, modified, null);
×
1766
    }
1767

1768
    @Deprecated
1769
    @Override
1770
    public BinaryDocument addBinaryResource(final Txn transaction, final DBBroker broker, final XmldbURI name,
1771
            final InputStream is, final String mimeType, final long size, final Date created, final Date modified,
1772
            @Nullable final Permission permission) throws EXistException, PermissionDeniedException, LockException,
1773
            TriggerException, IOException {
1774

1775
        final Database db = broker.getBrokerPool();
1✔
1776
        if (db.isReadOnly()) {
1!
1777
            throw new IOException("Database is read-only");
×
1778
        }
1779

1780
        final XmldbURI uri = getURI().append(name.lastSegment());
1✔
1781

1782
        try(final ManagedCollectionLock collectionLock = lockManager.acquireCollectionWriteLock(path);
1✔
1783
            final ManagedDocumentLock docLock = lockManager.acquireDocumentWriteLock(uri)) {
1✔
1784

1785
            final DocumentImpl oldDoc = getDocument(broker, name);
1✔
1786

1787
            final int docId = broker.getNextResourceId(transaction);
1✔
1788
            final BinaryDocument blob;
1789
            if (oldDoc != null) {
1✔
1790
                blob = new BinaryDocument(null, docId, oldDoc);
1✔
1791
            } else {
1✔
1792
                blob = new BinaryDocument(null, broker.getBrokerPool(), this, docId, name);
1✔
1793
            }
1794

1795
            return addBinaryResource(db, transaction, broker, blob, is, mimeType, size, created, modified, permission,
1✔
1796
                    DBBroker.PreserveType.DEFAULT, oldDoc, collectionLock);
1✔
1797
        }
1798
    }
1799

1800
    @Deprecated
1801
    @Override
1802
    public BinaryDocument validateBinaryResource(final Txn transaction, final DBBroker broker, final XmldbURI name) throws PermissionDeniedException, LockException, TriggerException, IOException {
1803
        try {
1804
            final int docId = broker.getNextResourceId(transaction);
×
1805
            return new BinaryDocument(null, broker.getBrokerPool(), this, docId, name);
×
1806
        } catch (final EXistException e) {
×
1807
            throw new IOException(e.getMessage(), e);
×
1808
        }
1809
    }
1810

1811
    @Deprecated
1812
    @Override
1813
    public BinaryDocument addBinaryResource(final Txn transaction, final DBBroker broker, final BinaryDocument blob, final InputStream is, final String mimeType, final long size, final Date created, final Date modified) throws EXistException, PermissionDeniedException, LockException, TriggerException, IOException {
1814
        return addBinaryResource(transaction, broker, blob, is, mimeType, size, created, modified, DBBroker.PreserveType.DEFAULT);
×
1815
    }
1816

1817
    @Deprecated
1818
    @Override
1819
    public BinaryDocument addBinaryResource(final Txn transaction, final DBBroker broker, final BinaryDocument blob, final InputStream is, final String mimeType, final long size, final Date created, final Date modified, final DBBroker.PreserveType preserve) throws EXistException, PermissionDeniedException, LockException, TriggerException, IOException {
1820
        final Database db = broker.getBrokerPool();
1✔
1821
        if (db.isReadOnly()) {
1!
1822
            throw new IOException("Database is read-only");
×
1823
        }
1824

1825
        final XmldbURI docUri = blob.getFileURI();
1✔
1826

1827
        try(final ManagedCollectionLock collectionLock = lockManager.acquireCollectionWriteLock(path);
1✔
1828
                final ManagedDocumentLock docLock = lockManager.acquireDocumentWriteLock(blob.getURI())) {
1✔
1829

1830
            final DocumentImpl oldDoc = getDocument(broker, docUri);
1✔
1831

1832
            return addBinaryResource(db, transaction, broker, blob, is, mimeType, size, created, modified, null, preserve,
1✔
1833
                    oldDoc, collectionLock);
1✔
1834
        }
1835
    }
1836

1837
    private BinaryDocument addBinaryResource(final Database db, final Txn transaction, final DBBroker broker,
1838
            final BinaryDocument blob, final InputStream is, final String mimeType, @Deprecated final long size, final Date created,
1839
            final Date modified, @Nullable final Permission permission, final DBBroker.PreserveType preserve, final DocumentImpl oldDoc,
1840
            final ManagedCollectionLock collectionLock) throws EXistException, PermissionDeniedException, LockException, TriggerException, IOException {
1841

1842
        final DocumentTriggers trigger = new DocumentTriggers(broker, transaction, null, this, broker.isTriggersEnabled() ? getConfiguration(broker) : null);
1!
1843
        final XmldbURI docUri = blob.getFileURI();
1✔
1844
        try {
1845
            db.getProcessMonitor().startJob(ProcessMonitor.ACTION_STORE_BINARY, docUri);
1✔
1846
            checkPermissionsForAddDocument(broker, oldDoc);
1✔
1847
            checkCollectionConflict(docUri);
1✔
1848
            //manageDocumentInformation(oldDoc, blob);
1849
            if (!broker.preserveOnCopy(preserve)) {
1✔
1850
                blob.copyOf(broker, blob, oldDoc);
1✔
1851
            }
1852
            blob.setMimeType(mimeType == null ? MimeType.BINARY_TYPE.getName() : mimeType);
1!
1853
            if (created != null) {
1✔
1854
                blob.setCreated(created.getTime());
1✔
1855
            }
1856
            if (modified != null) {
1✔
1857
                blob.setLastModified(modified.getTime());
1✔
1858
            }
1859
//            blob.setContentLength(size);
1860

1861
            if (oldDoc == null) {
1✔
1862
                trigger.beforeCreateDocument(broker, transaction, blob.getURI());
1✔
1863
            } else {
1✔
1864
                trigger.beforeUpdateDocument(broker, transaction, oldDoc);
1✔
1865
            }
1866

1867
            if (oldDoc != null) {
1✔
1868
                if (LOG.isDebugEnabled()) {
1!
1869
                    LOG.debug("removing old document db entry{}", oldDoc.getFileURI());
×
1870
                }
1871

1872
                if (!broker.preserveOnCopy(preserve)) {
1!
1873
                    updateModificationTime(blob);
1✔
1874
                }
1875

1876
                // remove the old document
1877
                broker.removeResource(transaction, oldDoc);
1✔
1878
            }
1879

1880
            if (permission != null) {
1✔
1881
                blob.setPermissions(permission);
1✔
1882
            }
1883

1884
            // store the binary content (create/replace)
1885
            broker.storeBinaryResource(transaction, blob, is);
1✔
1886
            addDocument(transaction, broker, blob, oldDoc);
1✔
1887

1888
            final IndexController indexController = broker.getIndexController();
1✔
1889
            final StreamListener listener = indexController.getStreamListener(blob, StreamListener.ReindexMode.STORE);
1✔
1890
            indexController.startIndexDocument(transaction, listener);
1✔
1891
            try {
1892
                broker.storeXMLResource(transaction, blob);
1✔
1893
            } finally {
1✔
1894
                indexController.endIndexDocument(transaction, listener);
1✔
1895
            }
1896

1897
            if (oldDoc == null) {
1✔
1898
                trigger.afterCreateDocument(broker, transaction, blob);
1✔
1899
            } else {
1✔
1900
                trigger.afterUpdateDocument(broker, transaction, blob);
1✔
1901
            }
1902

1903
            // NOTE: early release of Collection lock inline with Asymmetrical Locking scheme
1904
            collectionLock.close();
1✔
1905

1906
            return blob;
1✔
1907
        } finally {
1908
            broker.getBrokerPool().getProcessMonitor().endJob();
1✔
1909
        }
1910
    }
1911

1912
    @Override
1913
    public void setPermissions(final DBBroker broker, final int mode) throws LockException, PermissionDeniedException {
1914
        try(final ManagedCollectionLock collectionLock = lockManager.acquireCollectionWriteLock(path)) {
1✔
1915
            PermissionFactory.chmod(broker, this, Optional.of(mode), Optional.empty());
1✔
1916
        }
1917
    }
1✔
1918

1919
    @Override
1920
    public CollectionConfiguration getConfiguration(final DBBroker broker) {
1921
        final CollectionConfigurationManager manager = broker.getBrokerPool().getConfigurationManager();
1✔
1922
        if (manager == null) {
1!
1923
            return null;
×
1924
        }
1925
        //Attempt to get configuration
1926
        return manager.getConfiguration(this);
1✔
1927
    }
1928

1929
    @Override
1930
    public void setCreated(final long ms) {
1931
        created = ms;
×
1932
    }
×
1933

1934
    @Override
1935
    public long getCreated() {
1936
        return created;
1✔
1937
    }
1938

1939
    /** 
1940
     * Get XML Reader from ReaderPool and setup validation when needed.
1941
     *
1942
     * @param broker The database broker
1943
     * @param validation true if validation should be enabled
1944
     * @param collectionConf The configuration of the Collection
1945
     *
1946
     * @return An XML Reader
1947
     */
1948
    private XMLReader getReader(final DBBroker broker, final boolean validation, final CollectionConfiguration collectionConf) {
1949
        // Get reader from readerpool.
1950
        final XMLReader reader = broker.getBrokerPool().getXmlReaderPool().borrowXMLReader();
1✔
1951
        
1952
        // If Collection configuration exists (try to) get validation mode
1953
        // and setup reader with this information.
1954
        if (!validation) {
1!
1955
            XMLReaderObjectFactory.setReaderValidationMode(XMLReaderObjectFactory.VALIDATION_SETTING.DISABLED, reader);
×
1956
            
1957
        } else if(collectionConf != null) {
1!
1958
            final VALIDATION_SETTING mode = collectionConf.getValidationMode();
1✔
1959
            XMLReaderObjectFactory.setReaderValidationMode(mode, reader);
1✔
1960
        }
1961
        // Return configured reader.
1962
        return reader;
1✔
1963
    }
1964

1965
    /**
1966
     * Reset validation mode of reader and return reader to reader pool.
1967
     *
1968
     * @param broker The database broker
1969
     * @param reader The XML Reader to release
1970
     */    
1971
    private void releaseReader(final DBBroker broker, final XMLReader reader) {
1972
        // Get validation mode from static configuration
1973
        final Configuration config = broker.getConfiguration();
1✔
1974
        final String optionValue = (String) config.getProperty(XMLReaderObjectFactory.PROPERTY_VALIDATION_MODE);
1✔
1975
        final VALIDATION_SETTING validationMode = XMLReaderObjectFactory.VALIDATION_SETTING.fromOption(optionValue);
1✔
1976
        
1977
        // Restore default validation mode
1978
        XMLReaderObjectFactory.setReaderValidationMode(validationMode, reader);
1✔
1979
        
1980
        // Return reader
1981
        broker.getBrokerPool().getParserPool().returnXMLReader(reader);
1✔
1982
    }
1✔
1983

1984
    @Override
1985
    public IndexSpec getIndexConfiguration(final DBBroker broker) {
1986
        final CollectionConfiguration conf = getConfiguration(broker);
1✔
1987
        //If the collection has its own config...
1988
        if (conf == null) {
1!
1989
            return broker.getIndexConfiguration();
×
1990
        }
1991
        //... otherwise return the general config (the broker's one)
1992
        return conf.getIndexConfiguration();
1✔
1993
    }
1994

1995
    @Override
1996
    public GeneralRangeIndexSpec getIndexByPathConfiguration(final DBBroker broker, final NodePath nodePath) {
1997
        final IndexSpec idxSpec = getIndexConfiguration(broker);
1✔
1998
        return (idxSpec == null) ? null : idxSpec.getIndexByPath(nodePath);
1✔
1999
    }
2000

2001
    @Override
2002
    public QNameRangeIndexSpec getIndexByQNameConfiguration(final DBBroker broker, final QName nodeName) {
2003
        final IndexSpec idxSpec = getIndexConfiguration(broker);
1✔
2004
        return (idxSpec == null) ? null : idxSpec.getIndexByQName(nodeName);
1✔
2005
    }
2006

2007
    @Override
2008
    public String toString() {
2009
        final StringBuilder buf = new StringBuilder();
×
2010
        buf.append( getURI() );
×
2011
        buf.append("[");
×
2012

2013
        try {
2014
            final Iterator<String> documentNameIterator;
2015
            try (final ManagedCollectionLock collectionLock = lockManager.acquireCollectionReadLock(path)) {
×
2016
                documentNameIterator = documents.keySet().iterator();
×
2017
            }
2018

2019
            while (documentNameIterator.hasNext()) {
×
2020
                buf.append(documentNameIterator.next());
×
2021
                if (documentNameIterator.hasNext()) {
×
2022
                    buf.append(", ");
×
2023
                }
2024
            }
2025
        } catch(final LockException e) {
×
2026
            LOG.error(e);
×
2027
            throw new IllegalStateException(e);
×
2028
        }
2029
        buf.append("]");
×
2030
        return buf.toString();
×
2031
    }
2032
}
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