package org.exist.collections.triggers; import java.io.IOException; import java.io.InputStream; import java.util.Iterator; import java.util.Map; import java.util.Properties; import org.exist.collections.Collection; import org.exist.collections.CollectionConfigurationException; import org.exist.dom.BinaryDocument; import org.exist.dom.DocumentImpl; import org.exist.dom.NodeSet; import org.exist.memtree.SAXAdapter; import org.exist.security.xacml.AccessContext; import org.exist.source.Source; import org.exist.source.SourceFactory; import org.exist.source.StringSource; import org.exist.storage.DBBroker; import org.exist.storage.txn.Txn; import org.exist.xmldb.XmldbURI; import org.exist.xquery.CompiledXQuery; import org.exist.xquery.XPathException; import org.exist.xquery.XQuery; import org.exist.xquery.XQueryContext; import org.exist.xquery.value.AnyURIValue; import org.exist.xquery.value.Base64Binary; import org.exist.xquery.value.Sequence; import org.exist.xquery.value.StringValue; import org.xml.sax.ContentHandler; import org.xml.sax.SAXException; /** * A trigger that executes a user XQuery statement when invoked. * * The XQuery source executed is the value of the parameter named "query" or the * query at the URL indicated by the parameter named "url". * * Any additional parameters will be declared as external variables with the type xs:string * * These external variables for the Trigger are accessible to the user XQuery statement * <code>xxx:eventType</code> : the type of event for the Trigger. Either "prepare" or "finish" * <code>xxx:collectionName</code> : the name of the collection from which the event is triggered * <code>xxx:documentName</code> : the name of the document from which the event is triggered * <code>xxx:triggerEvent</code> : the kind of triggered event * <code>xxx:document</code> : the document from which the event is triggered * xxx is the namespace prefix within the XQuery, can be set by the variable "bindingPrefix" * * @author Pierrick Brihaye <pierrick.brihaye@free.fr> * @author Adam Retter <adam.retter@devon.gov.uk> */ public class XQueryTrigger extends FilteringTrigger { private final static String EVENT_TYPE_PREPARE = "prepare"; private final static String EVENT_TYPE_FINISH = "finish"; private final static String DEFAULT_BINDING_PREFIX = "local:"; private SAXAdapter adapter; private Collection collection = null; private String strQuery = null; private String urlQuery = null; private Properties userDefinedVariables = new Properties(); /** Namespace prefix associated to trigger */ private String bindingPrefix = null; private XQuery service; private ContentHandler originalOutputHandler; public final static String PEPARE_EXCEIPTION_MESSAGE = "Error during trigger prepare"; public XQueryTrigger() { adapter = new SAXAdapter(); } /** * @link org.exist.collections.Trigger#configure(org.exist.storage.DBBroker, org.exist.collections.Collection, java.util.Map) */ public void configure(DBBroker broker, Collection parent, Map parameters) throws CollectionConfigurationException { this.collection = parent; //for an XQuery trigger there must be at least //one parameter to specify the XQuery if(parameters != null) { urlQuery = (String) parameters.get("url"); strQuery = (String) parameters.get("query"); for(Iterator itParamName = parameters.keySet().iterator(); itParamName.hasNext();) { String paramName = (String)itParamName.next(); //get the binding prefix (if any) if(paramName.equals("bindingPrefix")) { String bindingPrefix = (String)parameters.get("bindingPrefix"); if(bindingPrefix != null && !"".equals(bindingPrefix.trim())) { this.bindingPrefix = bindingPrefix.trim() + ":"; } } //get the URL of the query (if any) else if(paramName.equals("url")) { urlQuery = (String)parameters.get("url"); } //get the query (if any) else if(paramName.equals("query")) { strQuery = (String)parameters.get("query"); } //make any other parameters available as external variables for the query else { userDefinedVariables.put(paramName, parameters.get(paramName)); } } //set a default binding prefix if none was specified if(this.bindingPrefix == null) { this.bindingPrefix = DEFAULT_BINDING_PREFIX; } //old if(urlQuery != null || strQuery != null) { service = broker.getXQueryService(); return; } } //no query to execute LOG.error("XQuery Trigger for: '" + parent.getURI() + "' is missing its XQuery parameter"); } /** * Get's a Source for the Trigger's XQuery * * @param the database broker * * @return the Source for the XQuery */ private Source getQuerySource(DBBroker broker) { Source querySource = null; //try and get the XQuery from a URL if(urlQuery != null) { try { querySource = SourceFactory.getSource(broker, null, urlQuery, false); } catch(Exception e) { LOG.error(e); } } //try and get the XQuery from a string else if(strQuery != null) { querySource = new StringSource(strQuery); } return querySource; } /** * @link org.exist.collections.Trigger#prepare(java.lang.String, org.w3c.dom.Document) */ public void prepare(int event, DBBroker broker, Txn transaction, XmldbURI documentPath, DocumentImpl existingDocument) throws TriggerException { LOG.debug("Preparing " + eventToString(event) + "XQuery trigger for document: '" + documentPath + "'"); //get the query Source query = getQuerySource(broker); if(query == null) return; // avoid infinite recursion by allowing just one trigger per thread if(!TriggerStatePerThread.verifyUniqueTriggerPerThreadBeforePrepare(this, documentPath)) { return; } TriggerStatePerThread.setTransaction(transaction); XQueryContext context = service.newContext(AccessContext.TRIGGER); //TODO : further initialisations ? CompiledXQuery compiledQuery; try { //compile the XQuery compiledQuery = service.compile(context, query); //declare external variables context.declareVariable(bindingPrefix + "eventType", EVENT_TYPE_PREPARE); context.declareVariable(bindingPrefix + "collectionName", new AnyURIValue(collection.getURI())); context.declareVariable(bindingPrefix + "documentName", new AnyURIValue(documentPath)); context.declareVariable(bindingPrefix + "triggerEvent", new StringValue(eventToString(event))); //declare user defined parameters as external variables for(Iterator itUserVarName = userDefinedVariables.keySet().iterator(); itUserVarName.hasNext();) { String varName = (String)itUserVarName.next(); String varValue = userDefinedVariables.getProperty(varName); context.declareVariable(bindingPrefix + varName, new StringValue(varValue)); } if(existingDocument == null) context.declareVariable(bindingPrefix + "document", Sequence.EMPTY_SEQUENCE); else if (existingDocument instanceof BinaryDocument) { //binary document BinaryDocument bin = (BinaryDocument)existingDocument; InputStream is = broker.getBinaryResource(bin); byte [] data = new byte[(int)broker.getBinaryResourceSize(bin)]; is.read(data); is.close(); context.declareVariable(bindingPrefix + "document", new Base64Binary(data)); } else { //XML document context.declareVariable(bindingPrefix + "document", (DocumentImpl)existingDocument); } } catch(XPathException e) { TriggerStatePerThread.setTriggerRunningState(TriggerStatePerThread.NO_TRIGGER_RUNNING, this, null); TriggerStatePerThread.setTransaction(null); throw new TriggerException(PEPARE_EXCEIPTION_MESSAGE, e); } catch(IOException e) { TriggerStatePerThread.setTriggerRunningState(TriggerStatePerThread.NO_TRIGGER_RUNNING, this, null); TriggerStatePerThread.setTransaction(null); throw new TriggerException(PEPARE_EXCEIPTION_MESSAGE, e); } //execute the XQuery try { //TODO : should we provide another contextSet ? NodeSet contextSet = NodeSet.EMPTY_SET; service.execute(compiledQuery, contextSet); //TODO : should we have a special processing ? LOG.debug("Trigger fired for prepare"); } catch(XPathException e) { TriggerStatePerThread.setTriggerRunningState(TriggerStatePerThread.NO_TRIGGER_RUNNING, this, null); TriggerStatePerThread.setTransaction(null); throw new TriggerException(PEPARE_EXCEIPTION_MESSAGE, e); } } /** * @link org.exist.collections.triggers.DocumentTrigger#finish(int, org.exist.storage.DBBroker, java.lang.String, org.w3c.dom.Document) */ public void finish(int event, DBBroker broker, Txn transaction, XmldbURI documentPath, DocumentImpl document) { LOG.debug("Finishing " + eventToString(event) + " XQuery trigger for document : '" + documentPath + "'"); //get the query Source query = getQuerySource(broker); if (query == null) return; // avoid infinite recursion by allowing just one trigger per thread if(!TriggerStatePerThread.verifyUniqueTriggerPerThreadBeforeFinish(this, documentPath)) { return; } XQueryContext context = service.newContext(AccessContext.TRIGGER); CompiledXQuery compiledQuery = null; try { //compile the XQuery compiledQuery = service.compile(context, query); //declare external variables context.declareVariable(bindingPrefix + "eventType", EVENT_TYPE_FINISH); context.declareVariable(bindingPrefix + "collectionName", new AnyURIValue(collection.getURI())); context.declareVariable(bindingPrefix + "documentName", new AnyURIValue(documentPath)); context.declareVariable(bindingPrefix + "triggerEvent", new StringValue(eventToString(event))); //declare user defined parameters as external variables for(Iterator itUserVarName = userDefinedVariables.keySet().iterator(); itUserVarName.hasNext();) { String varName = (String)itUserVarName.next(); String varValue = userDefinedVariables.getProperty(varName); context.declareVariable(bindingPrefix + varName, new StringValue(varValue)); } if(event == REMOVE_DOCUMENT_EVENT) { //Document does not exist any more -> Sequence.EMPTY_SEQUENCE context.declareVariable(bindingPrefix + "document", Sequence.EMPTY_SEQUENCE); } else if (document instanceof BinaryDocument) { //Binary document BinaryDocument bin = (BinaryDocument)document; InputStream is = broker.getBinaryResource(bin); byte [] data = new byte[(int)broker.getBinaryResourceSize(bin)]; is.read(data); is.close(); context.declareVariable(bindingPrefix + "document", new Base64Binary(data)); } else { //XML document context.declareVariable(bindingPrefix + "document", (DocumentImpl)document); } } catch(XPathException e) { //Should never be reached LOG.error(e); } catch(IOException e) { //Should never be reached LOG.error(e); } //execute the XQuery try { //TODO : should we provide another contextSet ? NodeSet contextSet = NodeSet.EMPTY_SET; service.execute(compiledQuery, contextSet); //TODO : should we have a special processing ? } catch (XPathException e) { //Should never be reached LOG.error("Error during trigger finish", e); } TriggerStatePerThread.setTriggerRunningState(TriggerStatePerThread.NO_TRIGGER_RUNNING, this, null); TriggerStatePerThread.setTransaction(null); LOG.debug("Trigger fired for finish"); } public void startDocument() throws SAXException { originalOutputHandler = getOutputHandler(); //TODO : uncomment when it works /* if (isValidating()) setOutputHandler(adapter); */ super.startDocument(); } public void endDocument() throws SAXException { super.endDocument(); setOutputHandler(originalOutputHandler); //if (!isValidating()) // return; //XQueryContext context = service.newContext(AccessContext.TRIGGER); //TODO : futher initializations ? // CompiledXQuery compiledQuery; //try { // compiledQuery = //service.compile(context, query); //context.declareVariable(bindingPrefix + "validating", new BooleanValue(isValidating())); //if (adapter.getDocument() == null) //context.declareVariable(bindingPrefix + "document", Sequence.EMPTY_SEQUENCE); //TODO : find the right method ;-) /* else context.declareVariable(bindingPrefix + "document", (DocumentImpl)adapter.getDocument()); */ //} catch (XPathException e) { //query = null; //prevents future use // throw new SAXException("Error during endDocument", e); //} catch (IOException e) { //query = null; //prevents future use // throw new SAXException("Error during endDocument", e); //} //TODO : uncomment when it works /* try { //TODO : should we provide another contextSet ? NodeSet contextSet = NodeSet.EMPTY_SET; //Sequence result = service.execute(compiledQuery, contextSet); //TODO : should we have a special processing ? LOG.debug("done."); } catch (XPathException e) { query = null; //prevents future use throw new SAXException("Error during endDocument", e); } */ //TODO : check that result is a document node //TODO : Stream result to originalOutputHandler } /** * Returns a String representation of the Trigger event * * @param event The Trigger event * * @return The String representation */ public static String eventToString(int event) { switch (event) { case STORE_DOCUMENT_EVENT : return "STORE"; case UPDATE_DOCUMENT_EVENT : return "UPDATE"; case REMOVE_DOCUMENT_EVENT : return "REMOVE"; case CREATE_COLLECTION_EVENT : return "CREATE"; case RENAME_COLLECTION_EVENT : return "RENAME"; case DELETE_COLLECTION_EVENT : return "DELETE"; default : return null; } } /*public String toString() { return "collection=" + collection + "\n" + "modifiedDocument=" + TriggerStatePerThread.getModifiedDocument() + "\n" + ( query != null ? query.substring(0, 40 ) : null ); }*/ }