/* * eXist Open Source Native XML Database * Copyright (C) 2001-06, Wolfgang M. Meier (meier@ifs.tu-darmstadt.de) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This library 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 Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * * $Id$ */ package org.exist.collections; import java.util.HashMap; import java.util.Map; import java.util.StringTokenizer; import org.apache.log4j.Logger; import org.exist.collections.triggers.Trigger; import org.exist.dom.DocumentImpl; import org.exist.security.SecurityManager; import org.exist.security.User; import org.exist.storage.BrokerPool; import org.exist.storage.DBBroker; import org.exist.storage.IndexSpec; import org.exist.util.DatabaseConfigurationException; 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; 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 final static String ROOT_ELEMENT = "collection"; /** First level element in a collection configuration document */ private final static String TRIGGERS_ELEMENT = "triggers"; private final static String EVENT_ATTRIBUTE = "event"; private final static String CLASS_ATTRIBUTE = "class"; private final static String PARAMETER_ELEMENT = "parameter"; private final static String PARAM_NAME_ATTRIBUTE = "name"; private final static String PARAM_VALUE_ATTRIBUTE = "value"; /** First level element in a collection configuration document */ private final static String INDEX_ELEMENT = "index"; private final static String PERMISSIONS_ELEMENT = "default-permissions"; private final static String GROUP_ELEMENT = "default-group"; private final static String RESOURCE_ATTR = "resource"; private final static String COLLECTION_ATTR = "collection"; private final static String VALIDATION_ELEMENT = "validation"; private final static String VALIDATION_MODE_ATTR = "mode"; private static final Logger LOG = Logger.getLogger(CollectionConfiguration.class); private TriggerConfig[] triggers = new TriggerConfig[6]; private IndexSpec indexSpec = null; private XmldbURI docName = null; private XmldbURI srcCollectionURI; private int defCollPermissions; private int defResPermissions; private String defCollGroup = null; private String defResGroup = null; private int validationMode=XMLReaderObjectFactory.VALIDATION_UNKNOWN; private SecurityManager secman; public CollectionConfiguration(BrokerPool pool) { secman = pool.getSecurityManager(); defResPermissions = secman.getResourceDefaultPerms(); defCollPermissions = secman.getCollectionDefaultPerms(); } public static boolean isCollectionConfigDocument(XmldbURI docName) { return docName.endsWith(CollectionConfiguration.COLLECTION_CONFIG_SUFFIX_URI); } public static boolean isCollectionConfigDocument(DocumentImpl doc ) { XmldbURI docName = doc.getURI(); return isCollectionConfigDocument( docName ); } /** * @param broker * @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 * @param doc collection configuration document * @throws CollectionConfigurationException */ protected void read(DBBroker broker, Document doc, boolean checkOnly, XmldbURI srcCollectionURI, XmldbURI docName) throws CollectionConfigurationException { if (!checkOnly) { this.docName = docName; this.srcCollectionURI = srcCollectionURI; } 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; } NodeList childNodes = root.getChildNodes(); Node node; for(int i = 0; i < childNodes.getLength(); i++) { node = childNodes.item(i); if(NAMESPACE.equals(node.getNamespaceURI())) { if(TRIGGERS_ELEMENT.equals(node.getLocalName())) { NodeList triggers = node.getChildNodes(); for(int j = 0; j < triggers.getLength(); j++) { node = triggers.item(j); if(node.getNodeType() == Node.ELEMENT_NODE) createTrigger(broker, (Element)node, checkOnly); } } else if(INDEX_ELEMENT.equals(node.getLocalName())) { Element elem = (Element) node; try { if(indexSpec == null) indexSpec = new IndexSpec(broker, elem); else indexSpec.read(broker, elem); } catch (DatabaseConfigurationException e) { if (checkOnly) throw new CollectionConfigurationException(e.getMessage(), e); else LOG.warn(e.getMessage(), e); } } else if (PERMISSIONS_ELEMENT.equals(node.getLocalName())) { 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 (NumberFormatException e) { if (checkOnly) throw new CollectionConfigurationException("Ilegal 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 (NumberFormatException e) { if (checkOnly) throw new CollectionConfigurationException("Ilegal 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())) { Element elem = (Element) node; String groupOpt = elem.getAttribute(RESOURCE_ATTR); if (groupOpt != null && groupOpt.length() > 0) { LOG.debug("RESOURCE: " + groupOpt); if (secman.getGroup(groupOpt)!=null){ defResGroup = groupOpt; } else { 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 (secman.getGroup(groupOpt)!=null){ defCollGroup = groupOpt; } else { 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())) { Element elem = (Element) node; String mode = elem.getAttribute(VALIDATION_MODE_ATTR); if(mode==null){ LOG.debug("Unable to determine validation mode in "+srcCollectionURI); validationMode=XMLReaderObjectFactory.VALIDATION_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(String message, boolean throwExceptions) throws CollectionConfigurationException { if (throwExceptions) throw new CollectionConfigurationException(message); else LOG.warn(message); } public XmldbURI getDocName() { return docName; } protected void setIndexConfiguration(IndexSpec spec) { this.indexSpec = spec; } public XmldbURI getSourceCollectionURI() { return srcCollectionURI; } public int getDefCollPermissions() { return defCollPermissions; } public int getDefResPermissions() { return defResPermissions; } public String getDefCollGroup(User user) { return (defCollGroup != null) ? defCollGroup : user.getPrimaryGroup(); } public String getDefResGroup(User user) { return (defResGroup != null) ? defResGroup : user.getPrimaryGroup(); } public int getValidationMode() { return validationMode; } public IndexSpec getIndexConfiguration() { return indexSpec; } public Trigger newTrigger(int eventType, DBBroker broker, Collection collection) throws org.exist.collections.CollectionConfigurationException { TriggerConfig config = getTriggerConfiguration(eventType); if (config != null) return config.newInstance(broker, collection); return null; } public TriggerConfig getTriggerConfiguration(int eventType) { return triggers[eventType]; } public boolean triggerRegistered(Class triggerClass) { for (int i = 0; i < triggers.length; i++) { if (triggers[i] != null && triggers[i].getTriggerClass() == triggerClass) return true; } return false; } private void createTrigger(DBBroker broker, Element node, boolean testConfig) throws CollectionConfigurationException { String eventAttr = node.getAttribute(EVENT_ATTRIBUTE); if(eventAttr == null) { throwOrLog("'" + node.getNodeName() + "' requires an attribute '"+ EVENT_ATTRIBUTE + "'", testConfig); return; } String classAttr = node.getAttribute(CLASS_ATTRIBUTE); if(classAttr == null) { throwOrLog("'" + node.getNodeName() + "' requires an attribute '"+ CLASS_ATTRIBUTE + "'", testConfig); return; } TriggerConfig trigger = instantiate(broker, node, classAttr, testConfig); if (!testConfig) { StringTokenizer tok = new StringTokenizer(eventAttr, ", "); String event; while(tok.hasMoreTokens()) { event = tok.nextToken(); LOG.debug("Registering trigger '" + classAttr + "' for event '" + event + "'"); if(event.equalsIgnoreCase("store")) { if (triggers[Trigger.STORE_DOCUMENT_EVENT] != null) LOG.warn("Trigger '" + classAttr + "' already registered"); triggers[Trigger.STORE_DOCUMENT_EVENT] = trigger; } else if(event.equalsIgnoreCase("update")) { if (triggers[Trigger.UPDATE_DOCUMENT_EVENT] != null) LOG.warn("Trigger '" + classAttr + "' already registered"); triggers[Trigger.UPDATE_DOCUMENT_EVENT] = trigger; } else if(event.equalsIgnoreCase("remove")) { if (triggers[Trigger.REMOVE_DOCUMENT_EVENT] != null) LOG.warn("Trigger '" + classAttr + "' already registered"); triggers[Trigger.REMOVE_DOCUMENT_EVENT] = trigger; } else if(event.equalsIgnoreCase("create-collection")) { if (triggers[Trigger.CREATE_COLLECTION_EVENT] != null) LOG.warn("Trigger '" + classAttr + "' already registered"); triggers[Trigger.CREATE_COLLECTION_EVENT] = trigger; } else if(event.equalsIgnoreCase("rename-collection")) { if (triggers[Trigger.RENAME_COLLECTION_EVENT] != null) LOG.warn("Trigger '" + classAttr + "' already registered"); triggers[Trigger.RENAME_COLLECTION_EVENT] = trigger; } else if(event.equalsIgnoreCase("delete-collection")) { if (triggers[Trigger.DELETE_COLLECTION_EVENT] != null) LOG.warn("Trigger '" + classAttr + "' already registered"); triggers[Trigger.DELETE_COLLECTION_EVENT] = trigger; } else LOG.warn("Unknown event type '" + event + "' in trigger '" + classAttr + "'"); } } } private TriggerConfig instantiate(DBBroker broker, Element node, String classname, boolean testOnly) throws CollectionConfigurationException { try { Class clazz = Class.forName(classname); if(!Trigger.class.isAssignableFrom(clazz)) { throwOrLog("Trigger's class '" + classname + "' is not assignable from '" + Trigger.class + "'", testOnly); return null; } TriggerConfig triggerConf = new TriggerConfig(clazz); NodeList nodes = node.getElementsByTagNameNS(NAMESPACE, PARAMETER_ELEMENT); //TODO : rely on schema-driven validation -pb if (nodes.getLength() > 0) { Map parameters = new HashMap(nodes.getLength()); for (int i = 0 ; i < nodes.getLength(); i++) { Element param = (Element)nodes.item(i); //TODO : rely on schema-driven validation -pb String name = param.getAttribute(PARAM_NAME_ATTRIBUTE); if(name == null) { throwOrLog("Expected attribute '" + PARAM_NAME_ATTRIBUTE + "' for element '" + PARAMETER_ELEMENT + "' in trigger's configuration.", testOnly); } else { String value = param.getAttribute(PARAM_VALUE_ATTRIBUTE); if(value == null) { throwOrLog("Expected attribute '" + PARAM_VALUE_ATTRIBUTE + "' for element '" + PARAMETER_ELEMENT + "' in trigger's configuration.", testOnly); } else parameters.put(name, value); } } triggerConf.setParameters(parameters); } return triggerConf; } catch (ClassNotFoundException e) { if (testOnly) throw new CollectionConfigurationException(e.getMessage(), e); else LOG.warn("Trigger class not found: " + e.getMessage(), e); } return null; } public String toString() { StringBuilder result = new StringBuilder(); if (indexSpec != null) result.append(indexSpec.toString()).append('\n'); for (int i = 0 ; i < triggers.length; i++) { TriggerConfig trigger = triggers[i]; if (trigger != null) { switch (i) { case Trigger.STORE_DOCUMENT_EVENT : result.append("store document trigger"); case Trigger.UPDATE_DOCUMENT_EVENT : result.append("update document trigger"); case Trigger.REMOVE_DOCUMENT_EVENT : result.append("remove document trigger"); case Trigger.CREATE_COLLECTION_EVENT : result.append("create collection trigger"); case Trigger.RENAME_COLLECTION_EVENT : result.append("rename collection trigger"); case Trigger.DELETE_COLLECTION_EVENT : result.append("delete collection trigger"); } result.append('\t').append(trigger.toString()).append('\n'); } } return result.toString(); } public static class TriggerConfig { private Class clazz; private Map parameters; public TriggerConfig(Class clazz) { this.clazz = clazz; } public Trigger newInstance(DBBroker broker, Collection collection) throws CollectionConfigurationException { try { Trigger trigger = (Trigger) clazz.newInstance(); trigger.configure(broker, collection, parameters); return trigger; } catch (InstantiationException e) { throw new CollectionConfigurationException(e.getMessage(), e); } catch (IllegalAccessException e) { throw new CollectionConfigurationException(e.getMessage(), e); } } public Map getParameters() { return parameters; } public void setParameters(Map parameters) { this.parameters = parameters; } public Class getTriggerClass() { return clazz; } } }