/*
* eXist Open Source Native XML Database
* Copyright (C) 2001-04 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 program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*
* $Id$
*/
package org.exist.collections;
import org.apache.log4j.Logger;
import org.exist.EXistException;
import org.exist.collections.triggers.TriggerException;
import org.exist.dom.DocumentImpl;
import org.exist.memtree.SAXAdapter;
import org.exist.security.PermissionDeniedException;
import org.exist.storage.BrokerPool;
import org.exist.storage.DBBroker;
import org.exist.storage.lock.Lock;
import org.exist.storage.txn.TransactionManager;
import org.exist.storage.txn.Txn;
import org.exist.util.LockException;
import org.exist.util.sanity.SanityCheck;
import org.exist.xmldb.XmldbURI;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.IOException;
import java.io.StringReader;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
* Manages index configurations. Index configurations are stored in a collection
* hierarchy below /db/system/config. CollectionConfigurationManager is called
* by {@link org.exist.collections.Collection} to retrieve the
* {@link org.exist.collections.CollectionConfiguration} instance for a given collection.
*
* @author wolf
*/
public class CollectionConfigurationManager {
private static final Logger LOG = Logger.getLogger(CollectionConfigurationManager.class);
public final static String CONFIG_COLLECTION = DBBroker.SYSTEM_COLLECTION + "/config";
public final static String COLLECTION_CONFIG_FILENAME = "collection.xconf";
public final static CollectionURI COLLECTION_CONFIG_PATH = new CollectionURI(XmldbURI.CONFIG_COLLECTION_URI.getRawCollectionPath());
private Map configurations = new HashMap();
private Object latch;
private CollectionConfiguration defaultConfig;
private BrokerPool pool;
public CollectionConfigurationManager(DBBroker broker) throws EXistException, CollectionConfigurationException {
this.pool = broker.getBrokerPool();
this.latch = pool.getCollectionsCache();
checkConfigCollection(broker);
loadAllConfigurations(broker);
defaultConfig = new CollectionConfiguration(broker.getBrokerPool());
defaultConfig.setIndexConfiguration(broker.getIndexConfiguration());
}
/**
* Add a new collection configuration. The XML document is passed as a string.
*
* @param transaction The transaction that will hold the WRITE locks until they are released by commit()/abort()
* @param broker
* @param collection the collection to which the configuration applies.
* @param config the xconf document as a String.
* @throws CollectionConfigurationException
*/
public void addConfiguration(Txn transaction, DBBroker broker, Collection collection, String config)
throws CollectionConfigurationException {
try {
//TODO : use XmldbURI.resolve() !
XmldbURI path = XmldbURI.CONFIG_COLLECTION_URI.append(collection.getURI());
Collection confCol = broker.getOrCreateCollection(transaction, path);
if(confCol == null)
throw new CollectionConfigurationException("Failed to create config collection: " + path);
XmldbURI configurationDocumentName = null;
//Replaces the current configuration file if there is one
CollectionConfiguration conf = getConfiguration(broker, collection);
if (conf != null) {
configurationDocumentName = conf.getDocName();
if (configurationDocumentName != null)
LOG.warn("Replacing current configuration file '" + configurationDocumentName + "'");
}
if (configurationDocumentName == null)
configurationDocumentName = CollectionConfiguration.DEFAULT_COLLECTION_CONFIG_FILE_URI;
broker.saveCollection(transaction, confCol);
IndexInfo info = confCol.validateXMLResource(transaction, broker, configurationDocumentName, config);
//TODO : unlock the collection here ?
confCol.store(transaction, broker, info, config, false);
//broker.sync(Sync.MAJOR_SYNC);
synchronized (latch) {
configurations.remove(new CollectionURI(path.getRawCollectionPath()));
loadConfiguration(broker, confCol);
}
} catch (IOException e) {
throw new CollectionConfigurationException("Failed to store collection configuration: " + e.getMessage(), e);
} catch (PermissionDeniedException e) {
throw new CollectionConfigurationException("Failed to store collection configuration: " + e.getMessage(), e);
} catch (EXistException e) {
throw new CollectionConfigurationException("Failed to store collection configuration: " + e.getMessage(), e);
} catch (TriggerException e) {
throw new CollectionConfigurationException("Failed to store collection configuration: " + e.getMessage(), e);
} catch (SAXException e) {
throw new CollectionConfigurationException("Failed to store collection configuration: " + e.getMessage(), e);
} catch (LockException e) {
throw new CollectionConfigurationException("Failed to store collection configuration: " + e.getMessage(), e);
}
}
/**
* Check the passed collection configuration. Throws an exception if errors are detected in the
* configuration document. Note: some configuration settings depend on the current environment, in particular
* the availability of trigger or index classes.
*
* @param broker DBBroker
* @param config the configuration to test
* @throws CollectionConfigurationException if errors were detected
*/
public void testConfiguration(DBBroker broker, String config) throws CollectionConfigurationException {
try {
SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setNamespaceAware(true);
InputSource src = new InputSource(new StringReader(config));
SAXParser parser = factory.newSAXParser();
XMLReader reader = parser.getXMLReader();
SAXAdapter adapter = new SAXAdapter();
reader.setContentHandler(adapter);
reader.parse(src);
Document doc = adapter.getDocument();
CollectionConfiguration conf = new CollectionConfiguration(broker.getBrokerPool());
conf.read(broker, doc, true, null, null);
} catch (ParserConfigurationException e) {
throw new CollectionConfigurationException(e.getMessage(), e);
} catch (SAXException e) {
throw new CollectionConfigurationException(e.getMessage(), e);
} catch (IOException e) {
throw new CollectionConfigurationException(e.getMessage(), e);
}
}
/**
* Retrieve the collection configuration instance for the given collection. This
* creates a new CollectionConfiguration object and recursively scans the collection
* hierarchy for available configurations.
*
* @param broker
* @param collection
* @return The collection configuration
* @throws CollectionConfigurationException
*/
protected CollectionConfiguration getConfiguration(DBBroker broker, Collection collection)
throws CollectionConfigurationException {
CollectionURI path = new CollectionURI(COLLECTION_CONFIG_PATH);
path.append(collection.getURI().getRawCollectionPath());
/*
* This used to go from the root collection (/db), and continue all the
* way to the end of the path, checking each collection on the way. I
* modified it to start at the collection path and work its way back to
* the root, stopping at the first config file it finds. This should be
* more efficient, and fit more appropriately will the XmldbURI api
*/
CollectionConfiguration conf;
synchronized (latch) {
while(!path.equals(COLLECTION_CONFIG_PATH)) {
conf = (CollectionConfiguration) configurations.get(path);
if (conf != null)
return conf;
path.removeLastSegment();
}
}
// use default configuration
return defaultConfig;
}
protected void loadAllConfigurations(DBBroker broker) throws CollectionConfigurationException {
Collection root = broker.getCollection(XmldbURI.CONFIG_COLLECTION_URI);
loadAllConfigurations(broker, root);
}
protected void loadAllConfigurations(DBBroker broker, Collection configCollection) throws CollectionConfigurationException {
if (configCollection == null)
return;
loadConfiguration(broker, configCollection);
XmldbURI path = configCollection.getURI();
for (Iterator i = configCollection.collectionIterator(); i.hasNext(); ) {
XmldbURI childName = (XmldbURI) i.next();
Collection child = broker.getCollection(path.appendInternal(childName));
if (child == null)
LOG.error("Collection is registered but could not be loaded: " + childName);
loadAllConfigurations(broker, child);
}
}
protected void loadConfiguration(DBBroker broker, Collection configCollection) throws CollectionConfigurationException {
if (configCollection != null && configCollection.getDocumentCount() > 0) {
for(Iterator i = configCollection.iterator(broker); i.hasNext(); ) {
DocumentImpl confDoc = (DocumentImpl) i.next();
if(confDoc.getFileURI().endsWith(CollectionConfiguration.COLLECTION_CONFIG_SUFFIX_URI)) {
if (LOG.isTraceEnabled())
LOG.trace("Reading collection configuration from '" + confDoc.getURI() + "'");
CollectionConfiguration conf = new CollectionConfiguration(broker.getBrokerPool());
// TODO DWES Temporary workaround for bug
// [ 1807744 ] Invalid collection.xconf causes a non startable database
// http://sourceforge.net/tracker/index.php?func=detail&aid=1807744&group_id=17691&atid=117691
try {
conf.read(broker, confDoc, false, configCollection.getURI(), confDoc.getFileURI());
} catch (CollectionConfigurationException e) {
String message = "Failed to read configuration document " + confDoc.getFileURI() + " in "
+ configCollection.getURI() + ". "
+ e.getMessage();
LOG.error(message);
System.out.println(message);
}
synchronized (latch) {
configurations.put(new CollectionURI(configCollection.getURI().getRawCollectionPath()), conf);
}
//Allow just one configuration document per collection
//TODO : do not break if a system property allows several ones -pb
break;
}
}
}
}
/**
* Notify the manager that a collection.xconf file has changed. All cached configurations
* for the corresponding collection and its sub-collections will be cleared.
*
* @param collectionPath
*/
public void invalidateAll(XmldbURI collectionPath) {
//TODO : use XmldbURI.resolve !
if (!collectionPath.startsWith(XmldbURI.CONFIG_COLLECTION_URI))
return;
synchronized (latch) {
LOG.debug("Invalidating collection " + collectionPath);
configurations.remove(new CollectionURI(collectionPath.getRawCollectionPath()));
}
}
/**
* Called by the collection cache if a collection is removed from the cache.
* This will delete the cached configuration instance for this collection.
*
* @param collectionPath
*/
protected void invalidate(XmldbURI collectionPath) {
//TODO : use XmldbURI.resolve !
// if (!collectionPath.startsWith(XmldbURI.CONFIG_COLLECTION_URI))
// return;
// collectionPath = collectionPath.trimFromBeginning(XmldbURI.CONFIG_COLLECTION_URI);
// CollectionCache collectionCache = pool.getCollectionsCache();
// synchronized (collectionCache) {
// CollectionConfiguration config = (CollectionConfiguration) cache.get(collectionPath);
// if (config != null) {
// config.getCollection().invalidateConfiguration();
// cache.remove(collectionPath);
// }
// }
}
/**
* Check if the config collection exists below the system collection. If not, create it.
*
* @param broker
* @throws EXistException
*/
private void checkConfigCollection(DBBroker broker) throws EXistException {
TransactionManager transact = pool.getTransactionManager();
Txn txn = null;
try {
Collection root = broker.getCollection(XmldbURI.CONFIG_COLLECTION_URI);
if(root == null) {
txn = transact.beginTransaction();
root = broker.getOrCreateCollection(txn, XmldbURI.CONFIG_COLLECTION_URI);
SanityCheck.THROW_ASSERT(root != null);
broker.saveCollection(txn, root);
transact.commit(txn);
}
} catch (IOException e) {
transact.abort(txn);
throw new EXistException("Failed to initialize '" + CONFIG_COLLECTION + "' : " + e.getMessage());
} catch (PermissionDeniedException e) {
transact.abort(txn);
throw new EXistException("Failed to initialize '" + CONFIG_COLLECTION + "' : " + e.getMessage());
}
}
public void checkRootCollectionConfigCollection(DBBroker broker) throws EXistException {
TransactionManager transact = pool.getTransactionManager();
Txn txn = null;
try {
//Create a configuration collection for the root collection
Collection rootCollectionConfiguration = broker.getCollection(XmldbURI.ROOT_COLLECTION_CONFIG_URI);
if(rootCollectionConfiguration == null) {
txn = transact.beginTransaction();
rootCollectionConfiguration = broker.getOrCreateCollection(txn, XmldbURI.ROOT_COLLECTION_CONFIG_URI);
SanityCheck.THROW_ASSERT(rootCollectionConfiguration != null);
broker.saveCollection(txn, rootCollectionConfiguration);
transact.commit(txn);
}
} catch (IOException e) {
transact.abort(txn);
throw new EXistException("Failed to initialize '" + CONFIG_COLLECTION + "' : " + e.getMessage());
} catch (PermissionDeniedException e) {
transact.abort(txn);
throw new EXistException("Failed to initialize '" + CONFIG_COLLECTION + "' : " + e.getMessage());
}
}
/** Create a stored default configuration document for the root collection
* @param broker The broker which will do the operation
* @throws EXistException
*/
public void checkRootCollectionConfig(DBBroker broker) throws EXistException {
String configuration =
"<collection xmlns=\"http://exist-db.org/collection-config/1.0\">" +
" <index>" +
//Copied from the legacy conf.xml in order to make the test suite work
//TODO : backward compatibility could be ensured by copying the relevant parts of conf.xml
" <fulltext attributes=\"true\" default=\"all\">" +
" <exclude path=\"/auth\" />" +
" </fulltext>" +
" </index>" +
"</collection>";
TransactionManager transact = pool.getTransactionManager();
Txn transaction = transact.beginTransaction();
try {
Collection collection = null;
try {
collection = broker.openCollection(XmldbURI.ROOT_COLLECTION_URI, Lock.READ_LOCK);
if (collection == null) {
transact.abort(transaction);
throw new EXistException("collection " + XmldbURI.ROOT_COLLECTION_URI + " not found!");
}
CollectionConfiguration conf = getConfiguration(broker, collection);
if (conf != null) {
//We already have a configuration document : do not erase it
if (conf.getDocName() != null) {
transact.abort(transaction);
return;
}
}
} finally {
if (collection != null)
collection.release(Lock.READ_LOCK);
}
//Configure the root collection
addConfiguration(transaction, broker, collection, configuration);
transact.commit(transaction);
LOG.info("Configured '" + collection.getURI() + "'");
} catch (CollectionConfigurationException e) {
transact.abort(transaction);
throw new EXistException(e.getMessage());
}
}
private void debugCache() {
StringBuilder buf = new StringBuilder();
for (Iterator i = configurations.keySet().iterator(); i.hasNext(); ) {
buf.append(i.next()).append(' ');
}
LOG.debug(buf.toString());
}
}