org.exist.collections.MutableCollection.java Source code

Java tutorial

Introduction

Here is the source code for org.exist.collections.MutableCollection.java

Source

/*
 *  eXist Open Source Native XML Database
 *  Copyright (C) 2001-2015 The eXist Project
 *  http://exist-db.org
 *
 *  This program is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public License
 *  as published by the Free Software Foundation; either version 2
 *  of the License, or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */
package org.exist.collections;

import com.evolvedbinary.j8fu.function.Consumer2E;
import net.jcip.annotations.GuardedBy;
import net.jcip.annotations.NotThreadSafe;
import org.exist.dom.QName;
import org.exist.dom.persistent.DocumentMetadata;
import org.exist.dom.persistent.DocumentSet;
import org.exist.dom.persistent.DocumentImpl;
import org.exist.dom.persistent.MutableDocumentSet;
import org.exist.dom.persistent.BinaryDocument;
import org.exist.dom.persistent.DefaultDocumentSet;
import java.io.*;
import java.util.*;
import java.util.function.Consumer;

import org.apache.commons.io.input.CloseShieldInputStream;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.exist.Database;
import org.exist.EXistException;
import org.exist.Indexer;
import org.exist.collections.triggers.*;
import org.exist.indexing.IndexController;
import org.exist.indexing.StreamListener;
import org.exist.security.Account;
import org.exist.security.Permission;
import org.exist.security.PermissionDeniedException;
import org.exist.security.PermissionFactory;
import org.exist.security.Subject;
import org.exist.storage.*;
import org.exist.storage.index.BFile;
import org.exist.storage.io.VariableByteInput;
import org.exist.storage.io.VariableByteOutputStream;
import org.exist.storage.lock.*;
import org.exist.storage.lock.Lock.LockMode;
import org.exist.storage.sync.Sync;
import org.exist.storage.txn.Txn;
import org.exist.util.Configuration;
import org.exist.util.LockException;
import org.exist.util.MimeType;
import org.exist.util.SyntaxException;
import org.exist.util.XMLReaderObjectFactory;
import org.exist.util.XMLReaderObjectFactory.VALIDATION_SETTING;
import org.exist.util.hashtable.ObjectHashSet;
import org.exist.util.serializer.DOMStreamer;
import org.exist.xmldb.XmldbURI;
import org.exist.xquery.Constants;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;

/**
 * An implementation of {@link Collection} that allows
 * mutations to be made to the Collection object
 *
 * Locks should be taken appropriately for any mutation
 */
@NotThreadSafe
public class MutableCollection implements Collection {

    private static final Logger LOG = LogManager.getLogger(Collection.class);
    private static final int SHALLOW_SIZE = 550;
    private static final int DOCUMENT_SIZE = 450;
    private static final int POOL_PARSER_THRESHOLD = 500;

    private int collectionId = UNKNOWN_COLLECTION_ID;
    private XmldbURI path;
    private final Lock lock;
    @GuardedBy("lock")
    private final Map<String, DocumentImpl> documents = new TreeMap<>();
    @GuardedBy("lock")
    private ObjectHashSet<XmldbURI> subCollections = new ObjectHashSet<>(19);
    private long address = BFile.UNKNOWN_ADDRESS; // Storage address of the collection in the BFile
    private long created = 0;
    private volatile boolean collectionConfigEnabled = true;
    private boolean triggersEnabled = true;
    private XMLReader userReader;
    private boolean isTempCollection;
    private Permission permissions;
    private final CollectionMetadata collectionMetadata;
    private final ObservaleMutableCollection observable = new ObservaleMutableCollection();

    // fields required by the collections cache
    private int refCount;
    private int timestamp;

    /**
     * Constructs a Collection Object (not yet persisted)
     *
     * @param broker The database broker
     * @param path The path of the Collection
     */
    public MutableCollection(final DBBroker broker, final XmldbURI path) {
        //The permissions assigned to this collection
        permissions = PermissionFactory.getDefaultCollectionPermission(broker.getBrokerPool().getSecurityManager());

        setPath(path);
        lock = new ReentrantReadWriteLock(path);
        this.collectionMetadata = new CollectionMetadata(this);
    }

    /**
     * Deserializes a Collection object
     *
     * Counterpart method to {@link #serialize(VariableByteOutputStream)}
     *
     * @param broker The database broker
     * @param path The path of the Collection
     * @param inputStream The input stream to deserialize the Collection from
     *
     * @return The Collection Object
     */
    public static MutableCollection load(final DBBroker broker, final XmldbURI path,
            final VariableByteInput inputStream) throws PermissionDeniedException, IOException, LockException {
        final MutableCollection collection = new MutableCollection(broker, path);
        collection.deserialize(broker, inputStream);
        return collection;
    }

    @Override
    public boolean isTriggersEnabled() {
        return triggersEnabled;
    }

    @Override
    public final void setPath(XmldbURI path) {
        path = path.toCollectionPathURI();
        //TODO : see if the URI resolves against DBBroker.TEMP_COLLECTION
        isTempCollection = path.getRawCollectionPath().equals(XmldbURI.TEMP_COLLECTION);
        this.path = path;
    }

    @Override
    public Lock getLock() {
        return lock;
    }

    @Override
    public void addCollection(final DBBroker broker, final Collection child, final boolean isNew)
            throws PermissionDeniedException, LockException {
        if (!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.WRITE)) {
            throw new PermissionDeniedException("Permission to write to Collection denied for " + this.getURI());
        }

        final XmldbURI childName = child.getURI().lastSegment();

        getLock().acquire(LockMode.WRITE_LOCK);
        try {
            if (!subCollections.contains(childName)) {
                subCollections.add(childName);
            }
        } finally {
            getLock().release(LockMode.WRITE_LOCK);
        }

        if (isNew) {
            child.setCreationTime(System.currentTimeMillis());
        }
    }

    @Override
    public List<CollectionEntry> getEntries(final DBBroker broker) throws PermissionDeniedException, LockException {
        if (!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.READ)) {
            throw new PermissionDeniedException("Permission denied to read collection: " + path);
        }
        final List<CollectionEntry> list = new ArrayList<>();

        final Iterator<XmldbURI> subCollectionIterator;
        getLock().acquire(LockMode.READ_LOCK);
        try {
            subCollectionIterator = subCollections.stableIterator();
        } finally {
            getLock().release(LockMode.READ_LOCK);
        }
        while (subCollectionIterator.hasNext()) {
            final XmldbURI subCollectionURI = subCollectionIterator.next();
            final CollectionEntry entry = new SubCollectionEntry(broker.getBrokerPool().getSecurityManager(),
                    subCollectionURI);
            entry.readMetadata(broker);
            list.add(entry);
        }

        for (final DocumentImpl document : copyOfDocs()) {
            final CollectionEntry entry = new DocumentEntry(document);
            entry.readMetadata(broker);
            list.add(entry);
        }
        return list;
    }

    @Override
    public CollectionEntry getChildCollectionEntry(final DBBroker broker, final String name)
            throws PermissionDeniedException {
        if (!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.READ)) {
            throw new PermissionDeniedException("Permission denied to read collection: " + path);
        }
        final XmldbURI subCollectionURI = getURI().append(name);
        final CollectionEntry entry = new SubCollectionEntry(broker.getBrokerPool().getSecurityManager(),
                subCollectionURI);
        entry.readMetadata(broker);
        return entry;
    }

    @Override
    public CollectionEntry getResourceEntry(final DBBroker broker, final String name)
            throws PermissionDeniedException, LockException {
        if (!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.READ)) {
            throw new PermissionDeniedException("Permission denied to read collection: " + path);
        }

        final CollectionEntry entry;
        getLock().acquire(LockMode.READ_LOCK);
        try {
            entry = new DocumentEntry(documents.get(name));
        } finally {
            getLock().release(LockMode.READ_LOCK);
        }

        entry.readMetadata(broker);
        return entry;
    }

    @Override
    public boolean isTempCollection() {
        return isTempCollection;
    }

    @Override
    public void release(final LockMode mode) {
        getLock().release(mode);
    }

    @Override
    public void update(final DBBroker broker, final Collection child)
            throws PermissionDeniedException, LockException {
        final XmldbURI childName = child.getURI().lastSegment();
        getLock().acquire(LockMode.WRITE_LOCK);
        try {
            subCollections.remove(childName);
            subCollections.add(childName);
        } finally {
            getLock().release(LockMode.WRITE_LOCK);
        }
    }

    @Override
    public void addDocument(final Txn transaction, final DBBroker broker, final DocumentImpl doc)
            throws PermissionDeniedException, LockException {
        addDocument(transaction, broker, doc, null);
    }

    /**
     * @param oldDoc if not null, then this document is replacing another and so WRITE access on the collection is not required,
     * just WRITE access on the old document
     */
    private void addDocument(final Txn transaction, final DBBroker broker, final DocumentImpl doc,
            final DocumentImpl oldDoc) throws PermissionDeniedException, LockException {
        if (oldDoc == null) {

            /* create */
            if (!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.WRITE)) {
                throw new PermissionDeniedException(
                        "Permission to write to Collection denied for " + this.getURI());
            }
        } else {

            /* update-replace */
            if (!oldDoc.getPermissions().validate(broker.getCurrentSubject(), Permission.WRITE)) {
                throw new PermissionDeniedException(
                        "Permission to write to overwrite document: " + oldDoc.getURI());
            }
        }

        if (doc.getDocId() == DocumentImpl.UNKNOWN_DOCUMENT_ID) {
            try {
                doc.setDocId(broker.getNextResourceId(transaction, this));
            } catch (final EXistException e) {
                LOG.error("Collection error " + e.getMessage(), e);
                // TODO : re-raise the exception ? -pb
                return;
            }
        }

        getLock().acquire(LockMode.WRITE_LOCK);
        try {
            documents.put(doc.getFileURI().getRawCollectionPath(), doc);
        } finally {
            getLock().release(LockMode.WRITE_LOCK);
        }
    }

    @Override
    public void unlinkDocument(final DBBroker broker, final DocumentImpl doc)
            throws PermissionDeniedException, LockException {
        if (!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.WRITE)) {
            throw new PermissionDeniedException("Permission denied to remove document from collection: " + path);
        }

        getLock().acquire(LockMode.WRITE_LOCK);
        try {
            documents.remove(doc.getFileURI().getRawCollectionPath());
        } finally {
            getLock().release(LockMode.WRITE_LOCK);
        }
    }

    @Override
    public Iterator<XmldbURI> collectionIterator(final DBBroker broker)
            throws PermissionDeniedException, LockException {
        if (!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.READ)) {
            throw new PermissionDeniedException("Permission to list sub-collections denied on " + this.getURI());
        }

        getLock().acquire(LockMode.READ_LOCK);
        try {
            return subCollections.stableIterator();
        } finally {
            getLock().release(LockMode.READ_LOCK);
        }
    }

    @Override
    public Iterator<XmldbURI> collectionIteratorNoLock(final DBBroker broker) throws PermissionDeniedException {
        if (!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.READ)) {
            throw new PermissionDeniedException("Permission to list sub-collections denied on " + this.getURI());
        }
        return subCollections.stableIterator();
    }

    @Override
    public List<Collection> getDescendants(final DBBroker broker, final Subject user)
            throws PermissionDeniedException {
        if (!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.READ)) {
            throw new PermissionDeniedException("Permission to list sub-collections denied on " + this.getURI());
        }

        final ArrayList<Collection> collectionList;
        final Iterator<XmldbURI> i;
        try {
            getLock().acquire(LockMode.READ_LOCK);
            try {
                collectionList = new ArrayList<>(subCollections.size());
                i = subCollections.stableIterator();
            } finally {
                getLock().release(LockMode.READ_LOCK);
            }
        } catch (final LockException e) {
            LOG.error(e.getMessage(), e);
            return Collections.emptyList();
        }

        while (i.hasNext()) {
            final XmldbURI childName = i.next();
            //TODO : resolve URI !
            final Collection child = broker.getCollection(path.append(childName));
            if (getPermissionsNoLock().validate(user, Permission.READ)) {
                collectionList.add(child);
                if (child.getChildCollectionCount(broker) > 0) {
                    //Recursive call
                    collectionList.addAll(child.getDescendants(broker, user));
                }
            }
        }

        return collectionList;
    }

    @Override
    public MutableDocumentSet allDocs(final DBBroker broker, final MutableDocumentSet docs, final boolean recursive)
            throws PermissionDeniedException {
        return allDocs(broker, docs, recursive, null);
    }

    @Override
    public MutableDocumentSet allDocs(final DBBroker broker, final MutableDocumentSet docs, final boolean recursive,
            final LockedDocumentMap lockMap) throws PermissionDeniedException {
        List<XmldbURI> subColls = null;
        if (getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.READ)) {
            try {
                getLock().acquire(LockMode.READ_LOCK);
                try {
                    //Add all docs in this collection to the returned set
                    getDocuments(broker, docs);
                    //Get a list of sub-collection URIs. We will process them
                    //after unlocking this collection. otherwise we may deadlock ourselves
                    subColls = subCollections.keys();
                } finally {
                    getLock().release(LockMode.READ_LOCK);
                }
            } catch (final LockException e) {
                LOG.error(e.getMessage(), e);
            }
        }

        if (recursive && subColls != null) {
            // process the child collections
            for (final XmldbURI childName : subColls) {
                //TODO : resolve URI !
                try {
                    final Collection child = broker.openCollection(path.appendInternal(childName),
                            LockMode.NO_LOCK);
                    //A collection may have been removed in the meantime, so check first
                    if (child != null) {
                        child.allDocs(broker, docs, recursive, lockMap);
                    }
                } catch (final PermissionDeniedException pde) {
                    //SKIP to next collection
                    //TODO create an audit log??!
                }
            }
        }
        return docs;
    }

    @Override
    public DocumentSet allDocs(final DBBroker broker, final MutableDocumentSet docs, final boolean recursive,
            final LockedDocumentMap lockMap, LockMode lockType) throws LockException, PermissionDeniedException {

        XmldbURI uris[] = null;
        if (getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.READ)) {
            getLock().acquire(LockMode.READ_LOCK);
            try {
                //Add all documents in this collection to the returned set
                getDocuments(broker, docs, lockMap, lockType);
                //Get a list of sub-collection URIs. We will process them
                //after unlocking this collection.
                //otherwise we may deadlock ourselves
                final List<XmldbURI> subColls = subCollections.keys();
                if (subColls != null) {
                    uris = new XmldbURI[subColls.size()];
                    for (int i = 0; i < subColls.size(); i++) {
                        uris[i] = path.appendInternal(subColls.get(i));
                    }
                }
            } finally {
                getLock().release(LockMode.READ_LOCK);
            }
        }

        if (recursive && uris != null) {
            //Process the child collections
            for (XmldbURI uri : uris) {
                //TODO : resolve URI !
                try {
                    final Collection child = broker.openCollection(uri, LockMode.NO_LOCK);
                    // a collection may have been removed in the meantime, so check first
                    if (child != null) {
                        child.allDocs(broker, docs, recursive, lockMap, lockType);
                    }
                } catch (final PermissionDeniedException pde) {
                    //SKIP to next collection
                    //TODO create an audit log??!
                }
            }
        }
        return docs;
    }

    @Override
    public DocumentSet getDocuments(final DBBroker broker, final MutableDocumentSet docs)
            throws PermissionDeniedException, LockException {
        if (!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.READ)) {
            throw new PermissionDeniedException("Permission denied to read collection: " + path);
        }

        try {
            getLock().acquire(LockMode.READ_LOCK);
            docs.addCollection(this);
            addDocumentsToSet(broker, docs);
        } finally {
            getLock().release(LockMode.READ_LOCK);
        }

        return docs;
    }

    @Override
    public DocumentSet getDocumentsNoLock(final DBBroker broker, final MutableDocumentSet docs) {
        docs.addCollection(this);
        addDocumentsToSet(broker, docs);
        return docs;
    }

    @Override
    public DocumentSet getDocuments(final DBBroker broker, final MutableDocumentSet docs,
            final LockedDocumentMap lockMap, LockMode lockType) throws LockException, PermissionDeniedException {
        if (!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.READ)) {
            throw new PermissionDeniedException("Permission denied to read collection: " + path);
        }

        try {
            getLock().acquire(LockMode.READ_LOCK);
            docs.addCollection(this);
            addDocumentsToSet(broker, docs, lockMap, lockType);
        } finally {
            getLock().release(LockMode.READ_LOCK);
        }
        return docs;
    }

    /**
     * Gets a stable list of the document objects
     * from {@link #documents}
     *
     * @return A stable list of the document objects
     */
    private List<DocumentImpl> copyOfDocs() throws LockException {
        getLock().acquire(LockMode.READ_LOCK);
        try {
            return new ArrayList<>(documents.values());
        } finally {
            getLock().release(LockMode.READ_LOCK);
        }
    }

    /**
     * Gets a stable set of the the document object
     * names from {@link #documents}
     *
     * @return A stable set of the document names
     */
    private Set<String> copyOfDocNames() throws LockException {
        getLock().acquire(LockMode.READ_LOCK);
        try {
            return new TreeSet<>(documents.keySet());
        } finally {
            getLock().release(LockMode.READ_LOCK);
        }
    }

    private void addDocumentsToSet(final DBBroker broker, final MutableDocumentSet docs,
            final LockedDocumentMap lockMap, LockMode lockType) throws LockException {
        for (final DocumentImpl doc : copyOfDocs()) {
            if (doc.getPermissions().validate(broker.getCurrentSubject(), Permission.WRITE)) {
                doc.getUpdateLock().acquire(lockType);

                docs.add(doc);
                lockMap.add(doc);
            }
        }
    }

    private void addDocumentsToSet(final DBBroker broker, final MutableDocumentSet docs) {
        try {
            for (final DocumentImpl doc : copyOfDocs()) {
                if (doc.getPermissions().validate(broker.getCurrentSubject(), Permission.READ)) {
                    docs.add(doc);
                }
            }
        } catch (final LockException e) {
            LOG.error(e);
        }
    }

    @Override
    public boolean allowUnload() {
        if (getURI().startsWith(CollectionConfigurationManager.ROOT_COLLECTION_CONFIG_URI)) {
            return false;
        }

        try {
            getLock().acquire(LockMode.READ_LOCK);
            try {
                for (final DocumentImpl doc : documents.values()) {
                    if (doc.isLockedForWrite()) {
                        return false;
                    }
                }
                return true;
            } finally {
                getLock().release(LockMode.READ_LOCK);
            }
        } catch (final LockException e) {
            LOG.error(e);
            return false;
        }
    }

    @Override
    public int compareTo(final Collection other) {
        Objects.requireNonNull(other);

        if (collectionId == other.getId()) {
            return Constants.EQUAL;
        } else if (collectionId < other.getId()) {
            return Constants.INFERIOR;
        } else {
            return Constants.SUPERIOR;
        }
    }

    @Override
    public boolean equals(final Object obj) {
        if (obj == null || !(obj instanceof Collection)) {
            return false;
        }

        return ((Collection) obj).getId() == collectionId;
    }

    @Override
    public int getMemorySize() {
        try {
            getLock().acquire(LockMode.READ_LOCK);
            try {
                return SHALLOW_SIZE + documents.size() * DOCUMENT_SIZE;
            } finally {
                getLock().release(LockMode.READ_LOCK);
            }
        } catch (final LockException e) {
            LOG.error(e);
            return -1;
        }
    }

    @Override
    public int getChildCollectionCount(final DBBroker broker) throws PermissionDeniedException {
        if (!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.READ)) {
            throw new PermissionDeniedException("Permission denied to read collection: " + path);
        }

        try {
            getLock().acquire(LockMode.READ_LOCK);
            try {
                return subCollections.size();
            } finally {
                getLock().release(LockMode.READ_LOCK);
            }
        } catch (final LockException e) {
            LOG.error(e.getMessage(), e);
            return 0;
        }
    }

    @Override
    public boolean isEmpty(final DBBroker broker) throws PermissionDeniedException {
        if (!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.READ)) {
            throw new PermissionDeniedException("Permission denied to read collection: " + path);
        }

        try {
            getLock().acquire(LockMode.READ_LOCK);
            try {
                return documents.isEmpty() && subCollections.isEmpty();
            } finally {
                getLock().release(LockMode.READ_LOCK);
            }
        } catch (final LockException e) {
            LOG.error(e.getMessage(), e);
            return false;
        }
    }

    @Override
    public DocumentImpl getDocument(final DBBroker broker, final XmldbURI name) throws PermissionDeniedException {
        try {
            getLock().acquire(LockMode.READ_LOCK);
            try {
                final DocumentImpl doc = documents.get(name.getRawCollectionPath());
                if (doc != null) {
                    if (!doc.getPermissions().validate(broker.getCurrentSubject(), Permission.READ)) {
                        throw new PermissionDeniedException(
                                "Permission denied to read document: " + name.toString());
                    }
                } else {
                    LOG.debug("Document " + name + " not found!");
                }

                return doc;
            } finally {
                getLock().release(LockMode.READ_LOCK);
            }
        } catch (final LockException e) {
            LOG.error(e.getMessage(), e);
            return null;
        }
    }

    @Override
    public DocumentImpl getDocumentWithLock(final DBBroker broker, final XmldbURI name)
            throws LockException, PermissionDeniedException {
        return getDocumentWithLock(broker, name, LockMode.READ_LOCK);
    }

    @Override
    public DocumentImpl getDocumentWithLock(final DBBroker broker, final XmldbURI name, final LockMode lockMode)
            throws LockException, PermissionDeniedException {
        getLock().acquire(LockMode.READ_LOCK);
        try {
            final DocumentImpl doc = documents.get(name.getRawCollectionPath());
            if (doc != null) {
                if (!doc.getPermissions().validate(broker.getCurrentSubject(), Permission.READ)) {
                    throw new PermissionDeniedException("Permission denied to read document: " + name.toString());
                }
                doc.getUpdateLock().acquire(lockMode);
            }
            return doc;
        } finally {
            getLock().release(LockMode.READ_LOCK);
        }
    }

    @Override
    public DocumentImpl getDocumentNoLock(final DBBroker broker, final String rawPath)
            throws PermissionDeniedException {
        final DocumentImpl doc = documents.get(rawPath);
        if (doc != null) {
            if (!doc.getPermissions().validate(broker.getCurrentSubject(), Permission.READ)) {
                throw new PermissionDeniedException("Permission denied to read document: " + rawPath);
            }
        }
        return doc;
    }

    @Override
    public void releaseDocument(final DocumentImpl doc) {
        if (doc != null) {
            doc.getUpdateLock().release(LockMode.READ_LOCK);
        }
    }

    @Override
    public void releaseDocument(final DocumentImpl doc, final LockMode mode) {
        if (doc != null) {
            doc.getUpdateLock().release(mode);
        }
    }

    @Override
    public int getDocumentCount(final DBBroker broker) throws PermissionDeniedException {
        if (!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.READ)) {
            throw new PermissionDeniedException("Permission denied to read collection: " + path);
        }

        try {
            getLock().acquire(LockMode.READ_LOCK);
            try {
                return documents.size();
            } finally {
                getLock().release(LockMode.READ_LOCK);
            }
        } catch (final LockException e) {
            LOG.warn(e.getMessage(), e);
            return -1;
        }
    }

    @Override
    public int getDocumentCountNoLock(final DBBroker broker) throws PermissionDeniedException {
        if (!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.READ)) {
            throw new PermissionDeniedException("Permission denied to read collection: " + path);
        }
        return documents.size();
    }

    @Override
    public int getId() {
        return collectionId;
    }

    @Override
    public XmldbURI getURI() {
        return path;
    }

    /**
     * Returns the parent-collection.
     *
     * @return The parent-collection or null if this is the root collection.
     */
    @Override
    public XmldbURI getParentURI() {
        if (path.equals(XmldbURI.ROOT_COLLECTION_URI)) {
            return null;
        }
        //TODO : resolve URI against ".." !
        return path.removeLastSegment();
    }

    @Override
    final public Permission getPermissions() {
        try {
            getLock().acquire(LockMode.READ_LOCK);
            return permissions;
        } catch (final LockException e) {
            LOG.error(e.getMessage(), e);
            return permissions;
        } finally {
            getLock().release(LockMode.READ_LOCK);
        }
    }

    @Override
    public Permission getPermissionsNoLock() {
        return permissions;
    }

    @Override
    public CollectionMetadata getMetadata() {
        return collectionMetadata;
    }

    @Override
    public boolean hasDocument(final DBBroker broker, final XmldbURI name) throws PermissionDeniedException {
        if (!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.READ)) {
            throw new PermissionDeniedException("Permission denied to read collection: " + path);
        }

        try {
            getLock().acquire(LockMode.READ_LOCK);
            try {
                return documents.containsKey(name.getRawCollectionPath());
            } finally {
                getLock().release(LockMode.READ_LOCK);
            }
        } catch (final LockException e) {
            LOG.warn(e.getMessage(), e);
            //TODO : ouch ! Should we return at any price ? Xithout even logging ? -pb
            return documents.containsKey(name.getRawCollectionPath());
        }
    }

    @Override
    public boolean hasChildCollection(final DBBroker broker, final XmldbURI name)
            throws PermissionDeniedException, LockException {
        if (!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.READ)) {
            throw new PermissionDeniedException("Permission denied to read collection: " + path);
        }

        getLock().acquire(LockMode.READ_LOCK);
        try {
            return subCollections.contains(name);
        } finally {
            getLock().release(LockMode.READ_LOCK);
        }
    }

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

        return subCollections.contains(name);
    }

    @Override
    public Iterator<DocumentImpl> iterator(final DBBroker broker) throws PermissionDeniedException, LockException {
        if (!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.READ)) {
            throw new PermissionDeniedException("Permission denied to read collection: " + path);
        }

        return getDocuments(broker, new DefaultDocumentSet()).getDocumentIterator();
    }

    @Override
    public Iterator<DocumentImpl> iteratorNoLock(final DBBroker broker) throws PermissionDeniedException {
        if (!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.READ)) {
            throw new PermissionDeniedException("Permission denied to read collection: " + path);
        }

        return getDocumentsNoLock(broker, new DefaultDocumentSet()).getDocumentIterator();
    }

    /**
     * Serializes the Collection to a byte representation
     *
     * Counterpart method to {@link #deserialize(DBBroker, VariableByteInput)}
     *
     * @param outputStream The output stream to write the collection contents to
     */
    @Override
    public void serialize(final VariableByteOutputStream outputStream) throws IOException, LockException {
        outputStream.writeInt(collectionId);

        final int size;
        final Iterator<XmldbURI> i;

        getLock().acquire(LockMode.READ_LOCK);
        try {
            size = subCollections.size();
            i = subCollections.stableIterator();
        } finally {
            getLock().release(LockMode.READ_LOCK);
        }

        outputStream.writeInt(size);
        while (i.hasNext()) {
            final XmldbURI childCollectionURI = i.next();
            outputStream.writeUTF(childCollectionURI.toString());
        }
        permissions.write(outputStream);
        outputStream.writeLong(created);
    }

    /**
     * Read collection contents from the stream
     *
     * Counterpart method to {@link #serialize(VariableByteOutputStream)}
     *
     * @param broker the database broker
     * @param istream The input data
     */
    private void deserialize(final DBBroker broker, final VariableByteInput istream)
            throws IOException, PermissionDeniedException, LockException {
        collectionId = istream.readInt();
        if (collectionId < 0) {
            throw new IOException("Internal error reading collection: invalid collection id");
        }
        final int collLen = istream.readInt();

        getLock().acquire(LockMode.WRITE_LOCK);
        try {
            subCollections = new ObjectHashSet<>(collLen == 0 ? 19 : collLen); //TODO(AR) why is this number 19?
            for (int i = 0; i < collLen; i++) {
                subCollections.add(XmldbURI.create(istream.readUTF()));
            }

            permissions.read(istream);

            created = istream.readLong();

            if (!permissions.validate(broker.getCurrentSubject(), Permission.EXECUTE)) {
                throw new PermissionDeniedException("Permission denied to open the Collection " + path);
            }

            final Collection col = this;

            broker.getCollectionResources(new InternalAccess() {
                @Override
                public void addDocument(final DocumentImpl doc) throws EXistException {
                    doc.setCollection(col);

                    if (doc.getDocId() == DocumentImpl.UNKNOWN_DOCUMENT_ID) {
                        LOG.error("Document must have ID. [" + doc + "]");
                        throw new EXistException("Document must have ID.");
                    }

                    documents.put(doc.getFileURI().getRawCollectionPath(), doc);
                }

                @Override
                public int getId() {
                    return col.getId();
                }
            });
        } finally {
            getLock().release(LockMode.WRITE_LOCK);
        }
    }

    @Override
    public void removeCollection(final DBBroker broker, final XmldbURI name)
            throws LockException, PermissionDeniedException {
        if (!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.WRITE)) {
            throw new PermissionDeniedException("Permission denied to read collection: " + path);
        }

        getLock().acquire(LockMode.WRITE_LOCK);
        try {
            subCollections.remove(name);
        } finally {
            getLock().release(LockMode.WRITE_LOCK);
        }
    }

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

        if (doc.getResourceType() == DocumentImpl.BINARY_FILE) {
            removeBinaryResource(transaction, broker, doc);
        } else {
            removeXMLResource(transaction, broker, doc.getFileURI());
        }
    }

    @Override
    public void removeXMLResource(final Txn transaction, final DBBroker broker, final XmldbURI name)
            throws PermissionDeniedException, TriggerException, LockException, IOException {
        if (!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.WRITE)) {
            throw new PermissionDeniedException("Permission denied to write collection: " + path);
        }

        DocumentImpl doc = null;

        final BrokerPool db = broker.getBrokerPool();

        db.getProcessMonitor().startJob(ProcessMonitor.ACTION_REMOVE_XML, name);
        getLock().acquire(LockMode.WRITE_LOCK);

        try {
            doc = documents.get(name.getRawCollectionPath());

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

            doc.getUpdateLock().acquire(LockMode.WRITE_LOCK);

            boolean useTriggers = isTriggersEnabled();
            if (CollectionConfiguration.DEFAULT_COLLECTION_CONFIG_FILE_URI.equals(name)) {
                // we remove a collection.xconf configuration file: tell the configuration manager to
                // reload the configuration.
                useTriggers = false;
                final CollectionConfigurationManager confMgr = broker.getBrokerPool().getConfigurationManager();
                if (confMgr != null) {
                    confMgr.invalidate(getURI(), broker.getBrokerPool());
                }
            }

            DocumentTriggers trigger = new DocumentTriggers(broker, null, this,
                    useTriggers ? getConfiguration(broker) : null);

            trigger.beforeDeleteDocument(broker, transaction, doc);

            broker.removeXMLResource(transaction, doc);
            documents.remove(name.getRawCollectionPath());

            trigger.afterDeleteDocument(broker, transaction, getURI().append(name));

            broker.getBrokerPool().getNotificationService().notifyUpdate(doc, UpdateListener.REMOVE);
        } finally {
            broker.getBrokerPool().getProcessMonitor().endJob();
            if (doc != null) {
                doc.getUpdateLock().release(LockMode.WRITE_LOCK);
            }
            getLock().release(LockMode.WRITE_LOCK);
        }
    }

    @Override
    public void removeBinaryResource(final Txn transaction, final DBBroker broker, final XmldbURI name)
            throws PermissionDeniedException, LockException, TriggerException {
        if (!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.WRITE)) {
            throw new PermissionDeniedException("Permission denied to write collection: " + path);
        }

        try {
            getLock().acquire(LockMode.READ_LOCK);
            final DocumentImpl doc = getDocument(broker, name);

            if (doc.isLockedForWrite()) {
                throw new PermissionDeniedException("Document " + doc.getFileURI() + " is locked for write");
            }

            removeBinaryResource(transaction, broker, doc);
        } finally {
            getLock().release(LockMode.READ_LOCK);
        }
    }

    @Override
    public void removeBinaryResource(final Txn transaction, final DBBroker broker, final DocumentImpl doc)
            throws PermissionDeniedException, LockException, TriggerException {
        if (!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.WRITE)) {
            throw new PermissionDeniedException("Permission denied to write collection: " + path);
        }

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

        broker.getBrokerPool().getProcessMonitor().startJob(ProcessMonitor.ACTION_REMOVE_BINARY, doc.getFileURI());
        getLock().acquire(LockMode.WRITE_LOCK);

        try {

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

            if (doc.isLockedForWrite()) {
                throw new PermissionDeniedException("Document " + doc.getFileURI() + " is locked for write");
            }

            doc.getUpdateLock().acquire(LockMode.WRITE_LOCK);

            DocumentTriggers trigger = new DocumentTriggers(broker, null, this,
                    isTriggersEnabled() ? getConfiguration(broker) : null);

            trigger.beforeDeleteDocument(broker, transaction, doc);

            final IndexController indexController = broker.getIndexController();
            final StreamListener listener = indexController.getStreamListener(doc,
                    StreamListener.ReindexMode.REMOVE_BINARY);
            try {
                indexController.startIndexDocument(transaction, listener);

                try {
                    broker.removeBinaryResource(transaction, (BinaryDocument) doc);
                } catch (final IOException ex) {
                    throw new PermissionDeniedException(
                            "Cannot delete file: " + doc.getURI().toString() + ": " + ex.getMessage(), ex);
                }
                documents.remove(doc.getFileURI().getRawCollectionPath());
            } finally {
                indexController.endIndexDocument(transaction, listener);
            }

            trigger.afterDeleteDocument(broker, transaction, doc.getURI());

        } finally {
            broker.getBrokerPool().getProcessMonitor().endJob();
            doc.getUpdateLock().release(LockMode.WRITE_LOCK);
            getLock().release(LockMode.WRITE_LOCK);
        }
    }

    @Override
    public void store(final Txn transaction, final DBBroker broker, final IndexInfo info, final InputSource source)
            throws EXistException, PermissionDeniedException, TriggerException, SAXException, LockException {
        storeXMLInternal(transaction, broker, info, storeInfo -> {
            try {
                final InputStream is = source.getByteStream();
                if (is != null && is.markSupported()) {
                    is.reset();
                } else {
                    final Reader cs = source.getCharacterStream();
                    if (cs != null && cs.markSupported()) {
                        cs.reset();
                    }
                }
            } catch (final IOException e) {
                // mark is not supported: exception is expected, do nothing
                LOG.debug(
                        "InputStream or CharacterStream underlying the InputSource does not support marking and therefore cannot be re-read.");
            }
            final XMLReader reader = getReader(broker, false, storeInfo.getCollectionConfig());
            storeInfo.setReader(reader, null);
            try {
                reader.parse(source);
            } catch (final IOException e) {
                throw new EXistException(e);
            } finally {
                releaseReader(broker, storeInfo, reader);
            }
        });
    }

    @Override
    public void store(final Txn transaction, final DBBroker broker, final IndexInfo info, final String data)
            throws EXistException, PermissionDeniedException, TriggerException, SAXException, LockException {
        storeXMLInternal(transaction, broker, info, storeInfo -> {
            final CollectionConfiguration colconf = storeInfo.getDocument().getCollection()
                    .getConfiguration(broker);
            final XMLReader reader = getReader(broker, false, colconf);
            storeInfo.setReader(reader, null);
            try {
                reader.parse(new InputSource(new StringReader(data)));
            } catch (final IOException e) {
                throw new EXistException(e);
            } finally {
                releaseReader(broker, storeInfo, reader);
            }
        });
    }

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

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

    /** 
     * Stores an XML document in the database. {@link #validateXMLResourceInternal(Txn, DBBroker, XmldbURI,
     * CollectionConfiguration, Consumer2E)}should have been called previously in order to acquire a write lock
     * for the document. Launches the finish trigger.
     *
     * @param transaction The database transaction
     * @param broker      The database broker
     * @param info        Tracks information between validate and store phases
     * @param parserFn    A function which parses the XML document
     */
    private void storeXMLInternal(final Txn transaction, final DBBroker broker, final IndexInfo info,
            final Consumer2E<IndexInfo, EXistException, SAXException> parserFn)
            throws EXistException, SAXException, PermissionDeniedException {

        final DocumentImpl document = info.getIndexer().getDocument();

        final Database db = broker.getBrokerPool();

        try {
            /* TODO
             * 
             * These security checks are temporarily disabled because throwing an exception
             * here may cause the database to corrupt.
             * Why would the database corrupt? Because validateXMLInternal that is typically
             * called before this method actually modifies the database and this collection,
             * so throwing an exception here leaves the database in an inconsistent state
             * with data 1/2 added/updated.
             * 
             * The downside of disabling these checks here is that: this collection is not locked
             * between the call to validateXmlInternal and storeXMLInternal, which means that if
             * UserA in ThreadA calls validateXmlInternal and is permitted access to store a resource,
             * and then UserB in ThreadB modifies the permissions of this collection to prevent UserA
             * from storing the document, when UserA reaches here (storeXMLInternal) they will still
             * be allowed to store their document. However the next document that UserA attempts to store
             * will be forbidden by validateXmlInternal and so the security transgression whilst not ideal
             * is short-lived.
             * 
             * To fix the issue we need to refactor validateXMLInternal and move any document/database/collection
             * modification code into storeXMLInternal after the commented out permissions checks below.
             * 
             * Noted by Adam Retter 2012-02-01T19:18
             */

            /*
            if(info.isCreating()) {
            // create
            * 
            if(!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.WRITE)) {
                throw new PermissionDeniedException("Permission denied to write collection: " + path);
            }
            } else {
            // update
                
            final Permission oldDocPermissions = info.getOldDocPermissions();
            if(!((oldDocPermissions.getOwner().getId() != broker.getCurrentSubject().getId()) | (oldDocPermissions.validate(broker.getCurrentSubject(), Permission.WRITE)))) {
                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.");
            }
            }
            */

            if (LOG.isDebugEnabled()) {
                LOG.debug("storing document " + document.getDocId() + " ...");
            }

            //Sanity check
            if (!document.getUpdateLock().isLockedForWrite()) {
                LOG.warn("document is not locked for write !");
            }

            db.getProcessMonitor().startJob(ProcessMonitor.ACTION_STORE_DOC, document.getFileURI());
            parserFn.accept(info);
            broker.storeXMLResource(transaction, document);
            broker.flush();
            broker.closeDocument();
            //broker.checkTree(document);
            LOG.debug("document stored.");
        } finally {
            //This lock has been acquired in validateXMLResourceInternal()
            document.getUpdateLock().release(LockMode.WRITE_LOCK);
            broker.getBrokerPool().getProcessMonitor().endJob();
        }
        setCollectionConfigEnabled(true);
        broker.deleteObservers();

        if (info.isCreating()) {
            info.getTriggers().afterCreateDocument(broker, transaction, document);
        } else {
            final StreamListener listener = broker.getIndexController().getStreamListener();
            listener.endReplaceDocument(transaction);

            info.getTriggers().afterUpdateDocument(broker, transaction, document);
        }

        db.getNotificationService().notifyUpdate(document,
                (info.isCreating() ? UpdateListener.ADD : UpdateListener.UPDATE));
        //Is it a collection configuration file ?
        final XmldbURI docName = document.getFileURI();
        //WARNING : there is no reason to lock the collection since setPath() is normally called in a safe way
        //TODO: *resolve* URI against CollectionConfigurationManager.CONFIG_COLLECTION_URI 
        if (getURI().startsWith(XmldbURI.CONFIG_COLLECTION_URI)
                && docName.endsWith(CollectionConfiguration.COLLECTION_CONFIG_SUFFIX_URI)) {
            broker.sync(Sync.MAJOR);
            final CollectionConfigurationManager manager = broker.getBrokerPool().getConfigurationManager();
            if (manager != null) {
                try {
                    manager.invalidate(getURI(), broker.getBrokerPool());
                    manager.loadConfiguration(broker, this);
                } catch (final PermissionDeniedException | LockException pde) {
                    throw new EXistException(pde.getMessage(), pde);
                } catch (final CollectionConfigurationException e) {
                    // DIZ: should this exception really been thrown? bugid=1807744
                    throw new EXistException("Error while reading new collection configuration: " + e.getMessage(),
                            e);
                }
            }
        }
    }

    @Override
    public IndexInfo validateXMLResource(final Txn transaction, final DBBroker broker, final XmldbURI name,
            final String data) throws EXistException, PermissionDeniedException, TriggerException, SAXException,
            LockException, IOException {
        return validateXMLResource(transaction, broker, name, new InputSource(new StringReader(data)));
    }

    @Override
    public IndexInfo validateXMLResource(final Txn transaction, final DBBroker broker, final XmldbURI name,
            final InputSource source) throws EXistException, PermissionDeniedException, TriggerException,
            SAXException, LockException, IOException {
        final CollectionConfiguration colconf = getConfiguration(broker);

        return validateXMLResourceInternal(transaction, broker, name, colconf, (info) -> {
            final XMLReader reader = getReader(broker, true, colconf);
            info.setReader(reader, null);
            try {

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

                reader.parse(closeShieldedInputSource);
            } catch (final SAXException e) {
                throw new SAXException("The XML parser reported a problem: " + e.getMessage(), e);
            } catch (final IOException e) {
                throw new EXistException(e);
            } finally {
                releaseReader(broker, info, reader);
            }
        });
    }

    //stops streams on the input source from being closed
    private InputSource closeShieldInputSource(final InputSource source) {
        final InputSource protectedInputSource = new InputSource();
        protectedInputSource.setEncoding(source.getEncoding());
        protectedInputSource.setSystemId(source.getSystemId());
        protectedInputSource.setPublicId(source.getPublicId());

        if (source.getByteStream() != null) {
            //TODO consider AutoCloseInputStream
            final InputStream closeShieldByteStream = new CloseShieldInputStream(source.getByteStream());
            protectedInputSource.setByteStream(closeShieldByteStream);
        }

        if (source.getCharacterStream() != null) {
            //TODO consider AutoCloseReader
            final Reader closeShieldReader = new CloseShieldReader(source.getCharacterStream());
            protectedInputSource.setCharacterStream(closeShieldReader);
        }

        return protectedInputSource;
    }

    private static class CloseShieldReader extends Reader {
        private final Reader reader;

        public CloseShieldReader(final Reader reader) {
            this.reader = reader;
        }

        @Override
        public int read(final char[] cbuf, final int off, final int len) throws IOException {
            return reader.read(cbuf, off, len);
        }

        @Override
        public void close() throws IOException {
            //do nothing as we are close shield
        }
    }

    @Override
    public IndexInfo validateXMLResource(final Txn transaction, final DBBroker broker, final XmldbURI name,
            final Node node) throws EXistException, PermissionDeniedException, TriggerException, SAXException,
            LockException, IOException {
        return validateXMLResourceInternal(transaction, broker, name, getConfiguration(broker), (info) -> {
            info.setDOMStreamer(new DOMStreamer());
            info.getDOMStreamer().serialize(node, true);
        });
    }

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

        //Make the necessary operations if we process a collection configuration document
        checkConfigurationDocument(transaction, broker, name);

        final Database db = broker.getBrokerPool();

        if (db.isReadOnly()) {
            throw new IOException("Database is read-only");
        }

        DocumentImpl oldDoc = null;
        boolean oldDocLocked = false;

        db.getProcessMonitor().startJob(ProcessMonitor.ACTION_VALIDATE_DOC, name);
        getLock().acquire(LockMode.WRITE_LOCK);
        try {
            DocumentImpl document = new DocumentImpl((BrokerPool) db, this, name);
            oldDoc = documents.get(name.getRawCollectionPath());
            checkPermissionsForAddDocument(broker, oldDoc);
            checkCollectionConflict(name);
            manageDocumentInformation(oldDoc, document);
            final Indexer indexer = new Indexer(broker, transaction);

            final IndexInfo info = new IndexInfo(indexer, config);
            info.setCreating(oldDoc == null);
            info.setOldDocPermissions(oldDoc != null ? oldDoc.getPermissions() : null);
            indexer.setDocument(document, config);
            addObserversToIndexer(broker, indexer);
            indexer.setValidating(true);

            if (CollectionConfiguration.DEFAULT_COLLECTION_CONFIG_FILE_URI.equals(name)) {
                // we are updating collection.xconf. Notify configuration manager
                //CollectionConfigurationManager confMgr = broker.getBrokerPool().getConfigurationManager();
                //confMgr.invalidateAll(getURI());
                setCollectionConfigEnabled(false);
            }

            final DocumentTriggers trigger = new DocumentTriggers(broker, indexer, this,
                    isTriggersEnabled() ? config : null);
            trigger.setValidating(true);

            info.setTriggers(trigger);

            if (oldDoc == null) {
                trigger.beforeCreateDocument(broker, transaction, getURI().append(name));
            } else {
                trigger.beforeUpdateDocument(broker, transaction, oldDoc);
            }

            if (LOG.isDebugEnabled()) {
                LOG.debug("Scanning document " + getURI().append(name));
            }

            validator.accept(info);
            // new document is valid: remove old document
            if (oldDoc != null) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("removing old document " + oldDoc.getFileURI());
                }
                updateModificationTime(document);
                oldDoc.getUpdateLock().acquire(LockMode.WRITE_LOCK);
                oldDocLocked = true;

                /**
                 * Matching {@link StreamListener#endReplaceDocument(Txn)} call is in
                 * {@link #storeXMLInternal(Txn, DBBroker, IndexInfo, Consumer2E)}
                 */
                final StreamListener listener = broker.getIndexController().getStreamListener(document,
                        StreamListener.ReindexMode.REPLACE_DOCUMENT);
                listener.startReplaceDocument(transaction);

                if (oldDoc.getResourceType() == DocumentImpl.BINARY_FILE) {
                    //TODO : use a more elaborated method ? No triggers...
                    broker.removeBinaryResource(transaction, (BinaryDocument) oldDoc);
                    documents.remove(oldDoc.getFileURI().getRawCollectionPath());
                    //This lock is released in storeXMLInternal()
                    //TODO : check that we go until there to ensure the lock is released
                    //                    if (transaction != null)
                    //                       transaction.acquireLock(document.getUpdateLock(), LockMode.WRITE_LOCK);
                    //                   else
                    document.getUpdateLock().acquire(LockMode.WRITE_LOCK);

                    document.setDocId(broker.getNextResourceId(transaction, this));
                    addDocument(transaction, broker, document);
                } else {
                    //TODO : use a more elaborated method ? No triggers...
                    broker.removeXMLResource(transaction, oldDoc, false);
                    oldDoc.copyOf(document, true);
                    indexer.setDocumentObject(oldDoc);
                    //old has become new at this point
                    document = oldDoc;
                    oldDocLocked = false;
                }
                if (LOG.isDebugEnabled()) {
                    LOG.debug("removed old document " + oldDoc.getFileURI());
                }
            } else {
                //This lock is released in storeXMLInternal()
                //TODO : check that we go until there to ensure the lock is released
                //               if (transaction != null)
                //                   transaction.acquireLock(document.getUpdateLock(), LockMode.WRITE_LOCK);
                //               else
                document.getUpdateLock().acquire(LockMode.WRITE_LOCK);

                document.setDocId(broker.getNextResourceId(transaction, this));
                addDocument(transaction, broker, document);
            }

            trigger.setValidating(false);

            return info;
        } finally {
            if (oldDoc != null && oldDocLocked) {
                oldDoc.getUpdateLock().release(LockMode.WRITE_LOCK);
            }
            getLock().release(LockMode.WRITE_LOCK);

            db.getProcessMonitor().endJob();
        }
    }

    private void checkConfigurationDocument(final Txn transaction, final DBBroker broker, final XmldbURI docUri)
            throws EXistException, PermissionDeniedException, LockException {
        //Is it a collection configuration file ?
        //TODO : use XmldbURI.resolve() !
        if (!getURI().startsWith(XmldbURI.CONFIG_COLLECTION_URI)) {
            return;
        }
        if (!docUri.endsWith(CollectionConfiguration.COLLECTION_CONFIG_SUFFIX_URI)) {
            return;
        }
        //Allow just one configuration document per collection
        //TODO : do not throw the exception if a system property allows several ones -pb
        for (final Iterator<DocumentImpl> i = iterator(broker); i.hasNext();) {
            final DocumentImpl confDoc = i.next();
            final XmldbURI currentConfDocName = confDoc.getFileURI();
            if (currentConfDocName != null && !currentConfDocName.equals(docUri)) {
                throw new EXistException("Could not store configuration '" + docUri
                        + "': A configuration document with a different name (" + currentConfDocName
                        + ") already exists in this collection (" + getURI() + ")");
            }
        }
        //broker.saveCollection(transaction, this);
        //CollectionConfigurationManager confMgr = broker.getBrokerPool().getConfigurationManager();
        //if(confMgr != null)
        //try {
        //confMgr.reload(broker, this);
        // catch (CollectionConfigurationException e) {
        //throw new EXistException("An error occurred while reloading the updated collection configuration: " + e.getMessage(), e);
        //}
    }

    /**
     * Add observers to the indexer
     *
     * @param broker    The database broker
     * @param indexer   The indexer to add observers to
     */
    private void addObserversToIndexer(final DBBroker broker, final Indexer indexer) {
        broker.deleteObservers();
        observable.forEachObserver(observer -> {
            indexer.addObserver(observer);
            broker.addObserver(observer);
        });
    }

    /**
     * If an old document exists, keep information about  the document.
     *
     * @param oldDoc The old document
     * @param document The current/new document
     */
    private void manageDocumentInformation(final DocumentImpl oldDoc, final DocumentImpl document) {
        DocumentMetadata metadata = new DocumentMetadata();
        if (oldDoc != null) {
            metadata = oldDoc.getMetadata();
            metadata.setCreated(oldDoc.getMetadata().getCreated());
            document.setPermissions(oldDoc.getPermissions());
        } else {
            metadata.setCreated(System.currentTimeMillis());
        }
        document.setMetadata(metadata);
    }

    /**
     * Update the modification time of a document
     *
     * @param document The document whose modification time should be updated
     */
    private void updateModificationTime(final DocumentImpl document) {
        final DocumentMetadata metadata = document.getMetadata();
        metadata.setLastModified(System.currentTimeMillis());
        document.setMetadata(metadata);
    }

    /**
     * Check Permissions about user and document when a document is added to the database,
     * and throw exceptions if necessary.
     *
     * @param broker The database broker
     * @param oldDoc old Document existing in database prior to adding a new one with same name, or null if none exists
     */
    private void checkPermissionsForAddDocument(final DBBroker broker, final DocumentImpl oldDoc)
            throws LockException, PermissionDeniedException {

        // do we have execute permission on the collection?
        if (!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.EXECUTE)) {
            throw new PermissionDeniedException("Execute permission is not granted on the Collection.");
        }

        if (oldDoc != null) {

            /* update document */

            LOG.debug("Found old doc " + oldDoc.getDocId());

            // check if the document is locked by another user
            final Account lockUser = oldDoc.getUserLock();
            if (lockUser != null && !lockUser.equals(broker.getCurrentSubject())) {
                throw new PermissionDeniedException("The document is locked by user '" + lockUser.getName() + "'.");
            }

            // do we have write permission on the old document or are we the owner of the old document?
            if (!((oldDoc.getPermissions().getOwner().getId() == broker.getCurrentSubject().getId())
                    || (oldDoc.getPermissions().validate(broker.getCurrentSubject(), Permission.WRITE)))) {
                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.");
            }
        } else {

            /* create document */

            if (!getPermissionsNoLock().validate(broker.getCurrentSubject(), Permission.WRITE)) {
                throw new PermissionDeniedException("Write permission is not granted on the Collection.");
            }
        }
    }

    private void checkCollectionConflict(final XmldbURI docUri) throws EXistException, PermissionDeniedException {
        if (subCollections.contains(docUri.lastSegment())) {
            throw new EXistException(
                    "The collection '" + getURI() + "' already has a sub-collection named '" + docUri.lastSegment()
                            + "', you cannot create a Document with the same name as an existing collection.");
        }
    }

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

    @Override
    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 {
        return addBinaryResource(transaction, broker, name, new ByteArrayInputStream(data), mimeType, data.length,
                created, modified);
    }

    @Override
    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 {
        return addBinaryResource(transaction, broker, name, is, mimeType, size, null, null);
    }

    @Override
    public BinaryDocument addBinaryResource(final Txn transaction, final DBBroker broker, final XmldbURI name,
            final InputStream is, final String mimeType, final long size, final Date created, final Date modified)
            throws EXistException, PermissionDeniedException, LockException, TriggerException, IOException {
        final BinaryDocument blob = new BinaryDocument(broker.getBrokerPool(), this, name);
        return addBinaryResource(transaction, broker, blob, is, mimeType, size, created, modified);
    }

    @Override
    public BinaryDocument validateBinaryResource(final Txn transaction, final DBBroker broker, final XmldbURI name)
            throws PermissionDeniedException, LockException, TriggerException, IOException {
        return new BinaryDocument(broker.getBrokerPool(), this, name);
    }

    @Override
    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 {
        final Database db = broker.getBrokerPool();
        if (db.isReadOnly()) {
            throw new IOException("Database is read-only");
        }
        final XmldbURI docUri = blob.getFileURI();
        //TODO : move later, i.e. after the collection lock is acquired ?
        final DocumentImpl oldDoc = getDocument(broker, docUri);
        final DocumentTriggers trigger = new DocumentTriggers(broker, null, this,
                isTriggersEnabled() ? getConfiguration(broker) : null);

        getLock().acquire(LockMode.WRITE_LOCK);
        try {
            db.getProcessMonitor().startJob(ProcessMonitor.ACTION_STORE_BINARY, docUri);
            checkPermissionsForAddDocument(broker, oldDoc);
            checkCollectionConflict(docUri);
            manageDocumentInformation(oldDoc, blob);
            final DocumentMetadata metadata = blob.getMetadata();
            metadata.setMimeType(mimeType == null ? MimeType.BINARY_TYPE.getName() : mimeType);
            if (created != null) {
                metadata.setCreated(created.getTime());
            }
            if (modified != null) {
                metadata.setLastModified(modified.getTime());
            }
            blob.setContentLength(size);

            if (oldDoc == null) {
                trigger.beforeCreateDocument(broker, transaction, blob.getURI());
            } else {
                trigger.beforeUpdateDocument(broker, transaction, oldDoc);
            }

            if (oldDoc != null) {
                LOG.debug("removing old document " + oldDoc.getFileURI());
                updateModificationTime(blob);
                broker.removeResource(transaction, oldDoc);
            }

            broker.storeBinaryResource(transaction, blob, is);
            addDocument(transaction, broker, blob, oldDoc);

            final IndexController indexController = broker.getIndexController();
            final StreamListener listener = indexController.getStreamListener(blob,
                    StreamListener.ReindexMode.STORE);
            indexController.startIndexDocument(transaction, listener);
            try {
                broker.storeXMLResource(transaction, blob);
            } finally {
                indexController.endIndexDocument(transaction, listener);
            }

            blob.getUpdateLock().acquire(LockMode.READ_LOCK);
        } finally {
            broker.getBrokerPool().getProcessMonitor().endJob();
            getLock().release(LockMode.WRITE_LOCK);
        }
        try {
            if (oldDoc == null) {
                trigger.afterCreateDocument(broker, transaction, blob);
            } else {
                trigger.afterUpdateDocument(broker, transaction, blob);
            }
        } finally {
            blob.getUpdateLock().release(LockMode.READ_LOCK);
        }
        return blob;
    }

    @Override
    public void setId(final int id) {
        this.collectionId = id;
    }

    @Override
    public void setPermissions(final int mode) throws LockException, PermissionDeniedException {
        try {
            getLock().acquire(LockMode.WRITE_LOCK);
            permissions.setMode(mode);
        } finally {
            getLock().release(LockMode.WRITE_LOCK);
        }
    }

    @Override
    public void setPermissions(final String mode) throws SyntaxException, LockException, PermissionDeniedException {
        try {
            getLock().acquire(LockMode.WRITE_LOCK);
            permissions.setMode(mode);
        } finally {
            getLock().release(LockMode.WRITE_LOCK);
        }
    }

    @Override
    public void setPermissions(final Permission permissions) throws LockException {
        try {
            getLock().acquire(LockMode.WRITE_LOCK);
            this.permissions = permissions;
        } finally {
            getLock().release(LockMode.WRITE_LOCK);
        }
    }

    @Override
    public CollectionConfiguration getConfiguration(final DBBroker broker) {
        if (!isCollectionConfigEnabled()) {
            return null;
        }

        final CollectionConfigurationManager manager = broker.getBrokerPool().getConfigurationManager();
        if (manager == null) {
            return null;
        }
        //Attempt to get configuration
        CollectionConfiguration configuration = null;
        try {
            //TODO: AR: if a Trigger throws CollectionConfigurationException
            //from its configure() method, is the rest of the collection 
            //configuration (indexes etc.) ignored even though they might be fine?
            configuration = manager.getConfiguration(broker, this);
            setCollectionConfigEnabled(true);
        } catch (final CollectionConfigurationException e) {
            setCollectionConfigEnabled(false);
            LOG.warn("Failed to load collection configuration for '" + getURI() + "'", e);
        }
        return configuration;
    }

    @Override
    public void setCollectionConfigEnabled(final boolean collectionConfigEnabled) {
        this.collectionConfigEnabled = collectionConfigEnabled;
    }

    @Override
    public boolean isCollectionConfigEnabled() {
        return collectionConfigEnabled;
    }

    @Override
    public void setAddress(final long addr) {
        this.address = addr;
    }

    @Override
    public long getAddress() {
        return this.address;
    }

    @Override
    public void setCreationTime(final long ms) {
        created = ms;
    }

    @Override
    public long getCreationTime() {
        return created;
    }

    @Override
    public void setTriggersEnabled(final boolean enabled) {
        try {
            getLock().acquire(LockMode.WRITE_LOCK);
            this.triggersEnabled = enabled;
        } catch (final LockException e) {
            LOG.warn(e.getMessage(), e);
            //Ouch ! -pb
            this.triggersEnabled = enabled;
        } finally {
            getLock().release(LockMode.WRITE_LOCK);
        }
    }

    @Override
    public void setReader(final XMLReader reader) {
        userReader = reader;
    }

    /** 
     * Get XML Reader from ReaderPool and setup validation when needed.
     *
     * @param broker The database broker
     * @param validation true if validation should be enabled
     * @param collectionConf The configuration of the Collection
     *
     * @return An XML Reader
     */
    private XMLReader getReader(final DBBroker broker, final boolean validation,
            final CollectionConfiguration collectionConf) {
        // If user-defined Reader is set, return it;
        if (userReader != null) {
            return userReader;
        }
        // Get reader from readerpool.
        final XMLReader reader = broker.getBrokerPool().getParserPool().borrowXMLReader();

        // If Collection configuration exists (try to) get validation mode
        // and setup reader with this information.
        if (!validation) {
            XMLReaderObjectFactory.setReaderValidationMode(XMLReaderObjectFactory.VALIDATION_SETTING.DISABLED,
                    reader);

        } else if (collectionConf != null) {
            final VALIDATION_SETTING mode = collectionConf.getValidationMode();
            XMLReaderObjectFactory.setReaderValidationMode(mode, reader);
        }
        // Return configured reader.
        return reader;
    }

    /**
     * Reset validation mode of reader and return reader to reader pool.
     *
     * @param broker The database broker
     * @param info The indexing info
     * @param reader The XML Reader to release
     */
    private void releaseReader(final DBBroker broker, final IndexInfo info, final XMLReader reader) {
        if (userReader != null) {
            return;
        }

        if (info.getIndexer().getDocSize() > POOL_PARSER_THRESHOLD) {
            return;
        }

        // Get validation mode from static configuration
        final Configuration config = broker.getConfiguration();
        final String optionValue = (String) config.getProperty(XMLReaderObjectFactory.PROPERTY_VALIDATION_MODE);
        final VALIDATION_SETTING validationMode = XMLReaderObjectFactory.convertValidationMode(optionValue);

        // Restore default validation mode
        XMLReaderObjectFactory.setReaderValidationMode(validationMode, reader);

        // Return reader
        broker.getBrokerPool().getParserPool().returnXMLReader(reader);
    }

    @Override
    public IndexSpec getIndexConfiguration(final DBBroker broker) {
        final CollectionConfiguration conf = getConfiguration(broker);
        //If the collection has its own config...
        if (conf == null) {
            return broker.getIndexConfiguration();
        }
        //... otherwise return the general config (the broker's one)
        return conf.getIndexConfiguration();
    }

    @Override
    public GeneralRangeIndexSpec getIndexByPathConfiguration(final DBBroker broker, final NodePath nodePath) {
        final IndexSpec idxSpec = getIndexConfiguration(broker);
        return (idxSpec == null) ? null : idxSpec.getIndexByPath(nodePath);
    }

    @Override
    public QNameRangeIndexSpec getIndexByQNameConfiguration(final DBBroker broker, final QName nodeName) {
        final IndexSpec idxSpec = getIndexConfiguration(broker);
        return (idxSpec == null) ? null : idxSpec.getIndexByQName(nodeName);
    }

    @Override
    public Observable getObservable() {
        return observable;
    }

    @Override
    public long getKey() {
        return collectionId;
    }

    @Override
    public int getReferenceCount() {
        return refCount;
    }

    @Override
    public int incReferenceCount() {
        return ++refCount;
    }

    @Override
    public int decReferenceCount() {
        return refCount > 0 ? --refCount : 0;
    }

    @Override
    public void setReferenceCount(final int count) {
        refCount = count;
    }

    @Override
    public void setTimestamp(final int timestamp) {
        this.timestamp = timestamp;
    }

    @Override
    public int getTimestamp() {
        return timestamp;
    }

    @Override
    public boolean sync(final boolean syncJournal) {
        return false;
    }

    @Override
    public boolean isDirty() {
        return false;
    }

    @Override
    public String toString() {
        final StringBuilder buf = new StringBuilder();
        buf.append(getURI());
        buf.append("[");

        try {
            for (final Iterator<String> i = copyOfDocNames().iterator(); i.hasNext();) {
                buf.append(i.next());
                if (i.hasNext()) {
                    buf.append(", ");
                }
            }
        } catch (final LockException e) {
            LOG.error(e);
            throw new IllegalStateException(e);
        }
        buf.append("]");
        return buf.toString();
    }

    private static class ObservaleMutableCollection extends Observable {
        private Observer[] observers = null;

        @Override
        public synchronized void addObserver(final Observer o) {
            if (hasObserver(o)) {
                return;
            }
            if (observers == null) {
                observers = new Observer[1];
                observers[0] = o;
            } else {
                final Observer n[] = new Observer[observers.length + 1];
                System.arraycopy(observers, 0, n, 0, observers.length);
                n[observers.length] = o;
                observers = n;
            }
        }

        private boolean hasObserver(final Observer o) {
            if (observers == null) {
                return false;
            }
            for (Observer observer : observers) {
                if (observer == o) {
                    return true;
                }
            }
            return false;
        }

        void forEachObserver(final Consumer<Observer> consumer) {
            if (observers != null) {
                for (final Observer observer : observers) {
                    consumer.accept(observer);
                }
            }
        }

        @Override
        public synchronized void deleteObservers() {
            if (observers != null) {
                observers = null;
            }
        }
    }
}