/* * eXist Open Source Native XML Database * Copyright (C) 2001-2014 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 org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.exist.EXistException; import org.exist.dom.persistent.DocumentImpl; import org.exist.dom.memtree.SAXAdapter; import org.exist.security.PermissionDeniedException; import org.exist.storage.*; import org.exist.storage.lock.Lock.LockMode; import org.exist.storage.lock.Locked; 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.XMLReader; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import java.io.StringReader; import java.util.*; import java.util.Map.Entry; import java.util.concurrent.Callable; /** * 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 implements BrokerPoolService { private static final Logger LOG = LogManager.getLogger(CollectionConfigurationManager.class); public final static String CONFIG_COLLECTION = XmldbURI.SYSTEM_COLLECTION + "/config"; /** /db/system/config **/ public final static XmldbURI CONFIG_COLLECTION_URI = XmldbURI.create(CONFIG_COLLECTION); /** /db/system/config/db **/ public final static XmldbURI ROOT_COLLECTION_CONFIG_URI = CONFIG_COLLECTION_URI.append(XmldbURI.ROOT_COLLECTION_NAME); public final static String COLLECTION_CONFIG_FILENAME = "collection.xconf"; public final static CollectionURI COLLECTION_CONFIG_PATH = new CollectionURI(CONFIG_COLLECTION_URI.getRawCollectionPath()); private Map<CollectionURI, CollectionConfiguration> configurations = new HashMap<CollectionURI, CollectionConfiguration>(); private Locked latch = new Locked(); private CollectionConfiguration defaultConfig; public CollectionConfigurationManager(final BrokerPool brokerPool) { this.defaultConfig = new CollectionConfiguration(brokerPool); } @Override public void startSystem(final DBBroker systemBroker) throws BrokerPoolServiceException { try { checkCreateCollection(systemBroker, CONFIG_COLLECTION_URI); checkCreateCollection(systemBroker, ROOT_COLLECTION_CONFIG_URI); loadAllConfigurations(systemBroker); defaultConfig.setIndexConfiguration(systemBroker.getIndexConfiguration()); } catch(final EXistException | CollectionConfigurationException | PermissionDeniedException | LockException e) { throw new BrokerPoolServiceException(e); } } /** * Add a new collection configuration. The XML document is passed as a * string. * * @param txn * 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 txn, final DBBroker broker, Collection collection, String config) throws CollectionConfigurationException { try { final XmldbURI path = CONFIG_COLLECTION_URI.append(collection.getURI()); final Collection confCol = broker.getOrCreateCollection(txn, 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 final 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(txn, confCol); final IndexInfo info = confCol.validateXMLResource(txn, broker, configurationDocumentName, config); // TODO : unlock the collection here ? confCol.store(txn, broker, info, config); // broker.sync(Sync.MAJOR_SYNC); } catch (final CollectionConfigurationException e) { throw e; } catch (final Exception 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 { final SAXParserFactory factory = SAXParserFactory.newInstance(); factory.setNamespaceAware(true); final InputSource src = new InputSource(new StringReader(config)); final SAXParser parser = factory.newSAXParser(); final XMLReader reader = parser.getXMLReader(); final SAXAdapter adapter = new SAXAdapter(); reader.setContentHandler(adapter); reader.parse(src); final Document doc = adapter.getDocument(); final CollectionConfiguration conf = new CollectionConfiguration(broker.getBrokerPool()); conf.read(broker, doc, true, null, null); } catch (final Exception e) { throw new CollectionConfigurationException(e); } } public List<Object> getCustomIndexSpecs(final String customIndexId) { return latch.read(new Callable<List<Object>>() { @Override public List<Object> call() throws Exception { List<Object> configs = new ArrayList<Object>(10); for (CollectionConfiguration config: configurations.values()) { IndexSpec spec = config.getIndexConfiguration(); if (spec != null) { Object customConfig = spec.getCustomIndexSpec(customIndexId); if (customConfig != null) { configs.add(customConfig); } } } return configs; } }); } /** * 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 { final 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 */ return latch.read(new Callable<CollectionConfiguration>() { @Override public CollectionConfiguration call() throws Exception { CollectionConfiguration conf = null; while(!path.equals(COLLECTION_CONFIG_PATH)) { conf = configurations.get(path); if (conf != null) { return conf; } path.removeLastSegment(); } // use default configuration return defaultConfig; } }); } protected void loadAllConfigurations(DBBroker broker) throws CollectionConfigurationException, PermissionDeniedException, LockException { final Collection root = broker.getCollection(CONFIG_COLLECTION_URI); loadAllConfigurations(broker, root); } protected void loadAllConfigurations(DBBroker broker, Collection configCollection) throws CollectionConfigurationException, PermissionDeniedException, LockException { if (configCollection == null) { return; } loadConfiguration(broker, configCollection); final XmldbURI path = configCollection.getURI(); for (final Iterator<XmldbURI> i = configCollection.collectionIterator(broker); i.hasNext();) { final XmldbURI childName = i.next(); final 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, final Collection configCollection) throws CollectionConfigurationException, PermissionDeniedException, LockException { if (configCollection != null && configCollection.getDocumentCount(broker) > 0) { for (final Iterator<DocumentImpl> i = configCollection.iterator(broker); i.hasNext();) { final DocumentImpl confDoc = i.next(); if (confDoc.getFileURI().endsWith(CollectionConfiguration.COLLECTION_CONFIG_SUFFIX_URI)) { if (LOG.isTraceEnabled()) { LOG.trace("Reading collection configuration from '" + confDoc.getURI() + "'"); } final CollectionConfiguration conf = new CollectionConfiguration(broker.getBrokerPool()); // [ 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 (final CollectionConfigurationException e) { final String message = "Failed to read configuration document " + confDoc.getFileURI() + " in " + configCollection.getURI() + ". " + e.getMessage(); LOG.error(message); } latch.write(new Callable<Void>() { @Override public Void call() throws Exception { configurations.put(new CollectionURI(configCollection.getURI().getRawCollectionPath()), conf); return null; } }); // Allow just one configuration document per collection // TODO : do not break if a system property allows several ones -pb break; } } } } public CollectionConfiguration getOrCreateCollectionConfiguration(final DBBroker broker, Collection collection) { final CollectionURI path = new CollectionURI(COLLECTION_CONFIG_PATH); path.append(collection.getURI().getRawCollectionPath()); CollectionConfiguration conf = latch.read(new Callable<CollectionConfiguration>() { @Override public CollectionConfiguration call() { return configurations.get(path); } }); if (conf != null) { return conf; } return latch.write(new Callable<CollectionConfiguration>() { @Override public CollectionConfiguration call() { CollectionConfiguration conf = configurations.get(path); if (conf != null) { return conf; } conf = new CollectionConfiguration(broker.getBrokerPool()); configurations.put(path, conf); return conf; } }); } /** * 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(final XmldbURI collectionPath) { if (!collectionPath.startsWith(CONFIG_COLLECTION_URI)) { return; } latch.write(new Callable<Void>() { @Override public Void call() { if (LOG.isDebugEnabled()) { LOG.debug("Invalidating collection " + collectionPath + " and subcollections"); } CollectionURI uri = new CollectionURI(collectionPath.getRawCollectionPath()); configurations.remove(uri); String str = uri.toString(); Iterator<Entry<CollectionURI, CollectionConfiguration>> it = configurations.entrySet().iterator(); while (it.hasNext()) { Entry<CollectionURI, CollectionConfiguration> entry = it.next(); if (entry.getKey().toString().startsWith(str)) { it.remove(); } } return null; } }); } /** * 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 * @param pool if not null: clear query cache */ public void invalidate(final XmldbURI collectionPath, final BrokerPool pool) { if (!collectionPath.startsWith(CONFIG_COLLECTION_URI)) { return; } latch.write(new Callable<Void>() { @Override public Void call() { if (LOG.isDebugEnabled()) { LOG.debug("Invalidating collection " + collectionPath); } configurations.remove(new CollectionURI(collectionPath.getRawCollectionPath())); return null; } }); } /** * Check if the collection exists below the system collection. If not, * create it. * * @param broker * @param uri * @throws EXistException */ private void checkCreateCollection(DBBroker broker, XmldbURI uri) throws EXistException { final TransactionManager transact = broker.getDatabase().getTransactionManager(); try(final Txn txn = transact.beginTransaction()) { Collection collection = broker.getCollection(uri); if (collection == null) { collection = broker.getOrCreateCollection(txn, uri); SanityCheck.THROW_ASSERT(collection != null); broker.saveCollection(txn, collection); } transact.commit(txn); } catch (final Exception e) { throw new EXistException("Failed to initialize '" + uri + "' : " + 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, PermissionDeniedException { // 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 final String configuration = "<collection xmlns=\"http://exist-db.org/collection-config/1.0\">" + " <index>" + " </index>" + "</collection>"; final TransactionManager transact = broker.getDatabase().getTransactionManager(); try(final Txn txn = transact.beginTransaction()) { Collection collection = null; try { collection = broker.openCollection(XmldbURI.ROOT_COLLECTION_URI, LockMode.READ_LOCK); if (collection == null) { transact.abort(txn); throw new EXistException("collection " + XmldbURI.ROOT_COLLECTION_URI + " not found!"); } final CollectionConfiguration conf = getConfiguration(broker, collection); if (conf != null) { // We already have a configuration document : do not erase // it if (conf.getDocName() != null) { transact.abort(txn); return; } } } finally { if (collection != null) { collection.release(LockMode.READ_LOCK); } } // Configure the root collection addConfiguration(txn, broker, collection, configuration); transact.commit(txn); LOG.info("Configured '" + collection.getURI() + "'"); } catch (final CollectionConfigurationException e) { 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()); } */ }