/* * 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 java.util.ArrayList; import java.util.List; import java.util.Map; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.exist.collections.triggers.CollectionTrigger; import org.exist.collections.triggers.CollectionTriggerProxy; import org.exist.collections.triggers.DocumentTrigger; import org.exist.collections.triggers.DocumentTriggerProxy; import org.exist.collections.triggers.Trigger; import org.exist.collections.triggers.TriggerException; import org.exist.collections.triggers.TriggerProxy; import org.exist.config.annotation.ConfigurationClass; import org.exist.security.Account; import org.exist.security.Permission; import org.exist.storage.BrokerPool; import org.exist.storage.DBBroker; import org.exist.storage.IndexSpec; import org.exist.util.DatabaseConfigurationException; import org.exist.util.ParametersExtractor; import org.exist.util.XMLReaderObjectFactory; import org.exist.xmldb.XmldbURI; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; @ConfigurationClass("collection") public class CollectionConfiguration { public final static String COLLECTION_CONFIG_SUFFIX = ".xconf"; public final static XmldbURI COLLECTION_CONFIG_SUFFIX_URI = XmldbURI.create(COLLECTION_CONFIG_SUFFIX); public final static String DEFAULT_COLLECTION_CONFIG_FILE = "collection" + COLLECTION_CONFIG_SUFFIX; public final static XmldbURI DEFAULT_COLLECTION_CONFIG_FILE_URI = XmldbURI.create(DEFAULT_COLLECTION_CONFIG_FILE); public final static String NAMESPACE = "http://exist-db.org/collection-config/1.0"; private static final String ROOT_ELEMENT = "collection"; /** * First level element in a collection configuration document */ private static final String TRIGGERS_ELEMENT = "triggers"; private static final String TRIGGER_ELEMENT = "trigger"; private static final String CLASS_ATTRIBUTE = "class"; private static final String PARAMETER_ELEMENT = "parameter"; /** * First level element in a collection configuration document */ private static final String INDEX_ELEMENT = "index"; private static final String PERMISSIONS_ELEMENT = "default-permissions"; private static final String GROUP_ELEMENT = "default-group"; private static final String RESOURCE_ATTR = "resource"; private static final String COLLECTION_ATTR = "collection"; private static final String VALIDATION_ELEMENT = "validation"; private static final String VALIDATION_MODE_ATTR = "mode"; private static final Logger LOG = LogManager.getLogger(CollectionConfiguration.class); private final List<TriggerProxy<? extends CollectionTrigger>> colTriggers = new ArrayList<>(); private final List<TriggerProxy<? extends DocumentTrigger>> docTriggers = new ArrayList<>(); private IndexSpec indexSpec = null; private XmldbURI docName = null; private XmldbURI srcCollectionURI; private int defCollPermissions = Permission.DEFAULT_COLLECTION_PERM; private int defResPermissions = Permission.DEFAULT_RESOURCE_PERM; private String defCollGroup = null; private String defResGroup = null; private XMLReaderObjectFactory.VALIDATION_SETTING validationMode = XMLReaderObjectFactory.VALIDATION_SETTING.UNKNOWN; private final BrokerPool pool; public CollectionConfiguration(final BrokerPool pool) { this.pool = pool; } /** * @param broker the database broker * @param doc collection configuration document * @param checkOnly true to only check * @param srcCollectionURI The collection from which the document is being read. This * is not necessarily the same as this.collection.getURI() because the * source document may have come from a parent collection. * @param docName The name of the document being read * @throws CollectionConfigurationException if an error occurs whilst reading the collection configuration */ protected void read(final DBBroker broker, final Document doc, final boolean checkOnly, final XmldbURI srcCollectionURI, final XmldbURI docName) throws CollectionConfigurationException { if (!checkOnly) { this.docName = docName; this.srcCollectionURI = srcCollectionURI; } final Element root = doc.getDocumentElement(); if (root == null) { throwOrLog("Configuration document can not be parsed", checkOnly); return; } if (!ROOT_ELEMENT.equals(root.getLocalName())) { throwOrLog("Expected element '" + ROOT_ELEMENT + "' in configuration document. Got element '" + root.getLocalName() + "'", checkOnly); return; } if (!NAMESPACE.equals(root.getNamespaceURI())) { throwOrLog("Expected namespace '" + NAMESPACE + "' for element '" + PARAMETER_ELEMENT + "' in configuration document. Got '" + root.getNamespaceURI() + "'", checkOnly); return; } final NodeList childNodes = root.getChildNodes(); for (int i = 0; i < childNodes.getLength(); i++) { Node node = childNodes.item(i); if (NAMESPACE.equals(node.getNamespaceURI())) { if (TRIGGERS_ELEMENT.equals(node.getLocalName())) { final NodeList triggers = node.getChildNodes(); for (int j = 0; j < triggers.getLength(); j++) { node = triggers.item(j); if (node.getNodeType() == Node.ELEMENT_NODE && node.getLocalName().equals(TRIGGER_ELEMENT)) { configureTrigger((Element) node, srcCollectionURI, checkOnly); } } } else if (INDEX_ELEMENT.equals(node.getLocalName())) { final Element elem = (Element) node; try { if (indexSpec == null) { indexSpec = new IndexSpec(broker, elem); } else { indexSpec.read(broker, elem); } } catch (final DatabaseConfigurationException e) { if (checkOnly) { throw new CollectionConfigurationException(e.getMessage(), e); } else { LOG.warn(e.getMessage(), e); } } } else if (PERMISSIONS_ELEMENT.equals(node.getLocalName())) { final Element elem = (Element) node; String permsOpt = elem.getAttribute(RESOURCE_ATTR); if (permsOpt != null && permsOpt.length() > 0) { LOG.debug("RESOURCE: " + permsOpt); try { defResPermissions = Integer.parseInt(permsOpt, 8); } catch (final NumberFormatException e) { if (checkOnly) { throw new CollectionConfigurationException( "Illegal value for permissions in " + "configuration document : " + e.getMessage(), e); } else { LOG.warn("Ilegal value for permissions in " + "configuration document : " + e.getMessage(), e); } } } permsOpt = elem.getAttribute(COLLECTION_ATTR); if (permsOpt != null && permsOpt.length() > 0) { LOG.debug("COLLECTION: " + permsOpt); try { defCollPermissions = Integer.parseInt(permsOpt, 8); } catch (final NumberFormatException e) { if (checkOnly) { throw new CollectionConfigurationException( "Illegal value for permissions in configuration " + "document : " + e.getMessage(), e); } else { LOG.warn("Ilegal value for permissions in configuration " + "document : " + e.getMessage(), e); } } } } else if (GROUP_ELEMENT.equals(node.getLocalName())) { final Element elem = (Element) node; String groupOpt = elem.getAttribute(RESOURCE_ATTR); if (groupOpt != null && groupOpt.length() > 0) { LOG.debug("RESOURCE: " + groupOpt); if (pool.getSecurityManager().getGroup(groupOpt) != null) { defResGroup = groupOpt; } else { //? Seems inconsistent : what does "checkOnly" means then ? if (checkOnly) { throw new CollectionConfigurationException("Ilegal value " + "for group in configuration document : " + groupOpt); } else { LOG.warn("Ilegal value for group in configuration document : " + groupOpt); } } } groupOpt = elem.getAttribute(COLLECTION_ATTR); if (groupOpt != null && groupOpt.length() > 0) { LOG.debug("COLLECTION: " + groupOpt); if (pool.getSecurityManager().getGroup(groupOpt) != null) { defCollGroup = groupOpt; } else { //? Seems inconsistent : what does "checkOnly" means then ? if (checkOnly) { throw new CollectionConfigurationException("Ilegal value " + "for group in configuration document : " + groupOpt); } else { LOG.warn("Ilegal value for group in configuration document : " + groupOpt); } } } } else if (VALIDATION_ELEMENT.equals(node.getLocalName())) { final Element elem = (Element) node; final String mode = elem.getAttribute(VALIDATION_MODE_ATTR); if (mode == null) { LOG.debug("Unable to determine validation mode in " + srcCollectionURI); validationMode = XMLReaderObjectFactory.VALIDATION_SETTING.UNKNOWN; } else { LOG.debug(srcCollectionURI + " : Validation mode=" + mode); validationMode = XMLReaderObjectFactory.convertValidationMode(mode); } } else { throwOrLog("Ignored node '" + node.getLocalName() + "' in configuration document", checkOnly); //TODO : throw an exception like above ? -pb } } else if (node.getNodeType() == Node.ELEMENT_NODE) { throwOrLog("Ignored node '" + node.getLocalName() + "' in namespace '" + node.getNamespaceURI() + "' in configuration document", checkOnly); } } } private void throwOrLog(final String message, final boolean throwExceptions) throws CollectionConfigurationException { if (throwExceptions) { throw new CollectionConfigurationException(message); } else { LOG.warn(message); } } public XmldbURI getDocName() { return docName; } protected void setIndexConfiguration(final IndexSpec spec) { this.indexSpec = spec; } public XmldbURI getSourceCollectionURI() { return srcCollectionURI; } public int getDefCollPermissions() { return defCollPermissions; } public int getDefResPermissions() { return defResPermissions; } public String getDefCollGroup(final Account user) { return defCollGroup != null ? defCollGroup : user.getPrimaryGroup(); } public String getDefResGroup(final Account user) { return (defResGroup != null) ? defResGroup : user.getPrimaryGroup(); } public XMLReaderObjectFactory.VALIDATION_SETTING getValidationMode() { return validationMode; } public IndexSpec getIndexConfiguration() { return indexSpec; } private void configureTrigger(final Element triggerElement, final XmldbURI collectionConfigurationURI, final boolean testOnly) throws CollectionConfigurationException { //TODO : rely on schema-driven validation -pb final String classname = triggerElement.getAttributes().getNamedItem(CLASS_ATTRIBUTE).getNodeValue(); try { final Class clazz = Class.forName(classname); if (!Trigger.class.isAssignableFrom(clazz)) { throwOrLog("Trigger's class '" + classname + "' is not assignable from '" + Trigger.class + "'", testOnly); return; } final NodeList nlParameter = triggerElement.getElementsByTagNameNS(NAMESPACE, PARAMETER_ELEMENT); final Map<String, List<? extends Object>> parameters = ParametersExtractor.extract(nlParameter); boolean added = false; if (DocumentTrigger.class.isAssignableFrom(clazz)) { docTriggers.add(new DocumentTriggerProxy((Class<? extends DocumentTrigger>) clazz, parameters)); added = true; } if (CollectionTrigger.class.isAssignableFrom(clazz)) { colTriggers.add(new CollectionTriggerProxy((Class<? extends CollectionTrigger>) clazz, parameters)); added = true; } if (!added) { throw new TriggerException("Unknown Trigger class type: " + clazz.getName()); } } catch (final ClassNotFoundException | TriggerException e) { if (testOnly) { throw new CollectionConfigurationException(e.getMessage(), e); } else { LOG.warn("Trigger class not found: " + e.getMessage(), e); } } } public List<TriggerProxy<? extends CollectionTrigger>> collectionTriggers() { return colTriggers; } public List<TriggerProxy<? extends DocumentTrigger>> documentTriggers() { return docTriggers; } public boolean triggerRegistered(final Class<?> triggerClass) { if(DocumentTrigger.class.isAssignableFrom(triggerClass)) { if(hasTriggerProxy(docTriggers, (Class<? extends DocumentTrigger>)triggerClass)) { return true; } } if(CollectionTrigger.class.isAssignableFrom(triggerClass)) { if(hasTriggerProxy(colTriggers, (Class<? extends CollectionTrigger>)triggerClass)) { return true; } } return false; } private <T> boolean hasTriggerProxy(final List<TriggerProxy<? extends T>> triggerProxies, final Class<? extends T> triggerProxyClazz) { for(final TriggerProxy<? extends T> triggerProxy : triggerProxies) { if(triggerProxy.getClazz() == triggerProxyClazz) { return true; } } return false; } @Override public String toString() { final StringBuilder result = new StringBuilder(); if (indexSpec != null) { result.append(indexSpec.toString()).append(System.getProperty("line.separator")); } return result.toString(); } }