Configuring Database Triggers

(1Q22)


eXist-db supports triggers: code that runs when certain events in the database occur. This article will tell you how to use this facility.

Overview

Triggers can be configured to respond to document and/or collection events. There are five types of events triggers can respond to:

create

Fired when a collection or document is created in a collection

update

Fired when a document is updated (there is no collection update)

copy

Fired when a collection or document is copied

move

Fired when a collection or document is moved (a rename operation is considered a move, not an update)

delete

Fired when a collection or document is deleted

Triggers can be defined for running both before and after the event.

Triggers may be chained together into a pipeline like approach. For example, if a document is stored into Collection A (configured with Trigger A), Trigger A may process the document and in response store a new document into Collection B (configured with Trigger B), which will in turn cause the document to be processed by Trigger B and so on.

Trigger Types

Triggers can be written in XQuery or Java.

XQuery Triggers

The XQuery code to execute when the trigger is fired can be placed in the collection configuration collection.xconf file or referenced by a URL.

The XQuery functions mapped to trigger event:

  • trigger:before-create-collection($uri as xs:anyURI)

  • trigger:after-create-collection($uri as xs:anyURI)

  • trigger:before-copy-collection($uri as xs:anyURI, $new-uri as xs:anyURI)

  • trigger:after-copy-collection($new-uri as xs:anyURI, $uri as xs:anyURI)

  • trigger:before-move-collection($uri as xs:anyURI, $new-uri as xs:anyURI)

  • trigger:after-move-collection($new-uri as xs:anyURI, $uri as xs:anyURI)

  • trigger:before-delete-collection($uri as xs:anyURI)

  • trigger:after-delete-collection($uri as xs:anyURI)

  • trigger:before-create-document($uri as xs:anyURI)

  • trigger:after-create-document($uri as xs:anyURI)

  • trigger:before-update-document($uri as xs:anyURI)

  • trigger:after-update-document($uri as xs:anyURI)

  • trigger:before-copy-document($uri as xs:anyURI, $new-uri as xs:anyURI)

  • trigger:after-copy-document($new-uri as xs:anyURI, $uri as xs:anyURI)

  • trigger:before-move-document($uri as xs:anyURI, $new-uri as xs:anyURI)

  • trigger:after-move-document($new-uri as xs:anyURI, $uri as xs:anyURI)

  • trigger:before-delete-document($uri as xs:anyURI)

  • trigger:after-delete-document($uri as xs:anyURI)

The trigger: prefix must be mapped to http://exist-db.org/xquery/trigger namespace.

Java Triggers

Triggers written in Java must implement the org.exist.collections.triggers.CollectionTrigger or org.exist.collections.triggers.DocumentTrigger interface. The Java class for your trigger must be available on the class path. lib/user is a good place to store the your custom trigger.

The DocumentTrigger interface provides a convenient starting place and provides the methods.

Configuring Triggers

Triggers are configured using the collection-specific configuration files called collection.xconf. These files are stored as standard XML documents in the system collection /db/system/config. collection.xconf is used also to define other collection specific settings such as indexes or default permissions.

The hierarchy of the collection /db/system/config mirrors the hierarchical structure of the main database collection.

Configurations are inherited by descendants in this hierarchy, (the configuration settings for a child collection are added to or override those set for its parent). With this it is possible for each collection to have its own trigger creation policy defined by a configuration file.

To configure triggers for a given collection, for example: /db/foo, create a new collection configuration file collection.xconf and store it in the system collection (for instance as /db/system/config/db/foo/collection.xconf). Since sub-collections inherit the configuration policy of their parents, you are not required to specify a configuration for its sub-collections.

Configuration Structure and Syntax

Trigger configuration files are standard XML documents that have their elements defined in the eXist namespace http://exist-db.org/collection-config/1.0.

All configuration documents have a <collection> root element. The triggers child element encloses the trigger configuration. Only a single <triggers> element is permitted. In the <triggers> element are <trigger> elements that define each trigger and the event(s) that it is fired for.

Each <trigger> element has two attributes, event and class.

event

A comma separated list of events to fire on:

  • create

  • update

  • copy

  • move

  • delete

If for an XQuery trigger the event attribute is not present, the code will never be invoked.

For Java triggers the event attribute may or may not have any effect depending on the implementation of the configure() method. See the examples below.

class

The name of the Java Class to fire when an event occurs. XQuery triggers are handled by the built-in Java trigger org.exist.collections.triggers.XQueryTrigger.

The <trigger> element can in addition contain zero or more <parameter> child-elements, defining any parameters to send to the trigger.

Configuring an XQuery Trigger

For XQuery triggers the following parameters apply:

url

The URL of the XQuery to execute

query

Can be used instead of url. Contains the XQuery itself.

The following example shows two XQuery Triggers configured in collection.xconf. The first executes an XQuery stored in the database, the second an XQuery placed inline:

<collection xmlns="http://exist-db.org/collection-config/1.0">
  <triggers>
    <trigger event="update" class="org.exist.collections.triggers.XQueryTrigger">
      <parameter name="url" value="xmldb:exist:///db/myTrigger.xql"/>
    </trigger>
    <trigger event="create" class="org.exist.collections.triggers.XQueryTrigger">
      <parameter name="query" value="module namespace trigger='http://exist-db.org/xquery/trigger';      declare function trigger:before-create-collection($uri as xs:anyURI) {              util:log('debug', concat('Trigger fired at ', current-dateTime()))           };"/>
    </trigger>
  </triggers>
</collection>

Configuring a Java Trigger

When configuring a Java Trigger, parameters are passed in a named map to the configure() function of the trigger.

The following example shows a Java trigger configuration:

<collection xmlns="http://exist-db.org/collection-config/1.0">
  <triggers>
    <trigger class="my.domain.testTrigger">
      <parameter name="myParam" value="myValue"/>
    </trigger>
  </triggers>
</collection>

Example Triggers

Here are trigger examples:

Simple logging Trigger in XQuery

xquery version "1.0";

(:
    A simple XQuery for an XQueryTrigger that
    logs all trigger events for which it is executed
    in the file /db/triggers-log.xml
:)

module namespace trigger="http://exist-db.org/xquery/trigger";

declare namespace xmldb="http://exist-db.org/xquery/xmldb";

declare function trigger:before-create-collection($uri as xs:anyURI) {
    local:log-event("before", "create", "collection", $uri)
};

declare function trigger:after-create-collection($uri as xs:anyURI) {
    local:log-event("after", "create", "collection", $uri)
};

declare function trigger:before-copy-collection($uri as xs:anyURI, $new-uri as xs:anyURI) {
    local:log-event("before", "copy", "collection", concat("from: ", $uri, " to: ", $new-uri))
};

declare function trigger:after-copy-collection($new-uri as xs:anyURI, $uri as xs:anyURI) {
    local:log-event("after", "copy", "collection", concat("from: ", $uri, " to: ", $new-uri))
};

declare function trigger:before-move-collection($uri as xs:anyURI, $new-uri as xs:anyURI) {
    local:log-event("before", "move", "collection", concat("from: ", $uri, " to: ", $new-uri))
};

declare function trigger:after-move-collection($new-uri as xs:anyURI, $uri as xs:anyURI) {
    local:log-event("after", "move", "collection", concat("from: ", $uri, " to: ", $new-uri))
};

declare function trigger:before-delete-collection($uri as xs:anyURI) {
    local:log-event("before", "delete", "collection", $uri)
};

declare function trigger:after-delete-collection($uri as xs:anyURI) {
    local:log-event("after", "delete", "collection", $uri)
};

declare function trigger:before-create-document($uri as xs:anyURI) {
    local:log-event("before", "create", "document", $uri)
};

declare function trigger:after-create-document($uri as xs:anyURI) {
    local:log-event("after", "create", "document", $uri)
};

declare function trigger:before-update-document($uri as xs:anyURI) {
    local:log-event("before", "update", "document", $uri)
};

declare function trigger:after-update-document($uri as xs:anyURI) {
    local:log-event("after", "update", "document", $uri)
};

declare function trigger:before-copy-document($uri as xs:anyURI, $new-uri as xs:anyURI) {
    local:log-event("before", "copy", "document", concat("from: ", $uri, " to: ", $new-uri))
};

declare function trigger:after-copy-document($new-uri as xs:anyURI, $uri as xs:anyURI) {
    local:log-event("after", "copy", "document", concat("from: ", $uri, " to: ", $new-uri))
};

declare function trigger:before-move-document($uri as xs:anyURI, $new-uri as xs:anyURI) {
    local:log-event("before", "move", "document", concat("from: ", $uri, " to: ", $new-uri))
};

declare function trigger:after-move-document($new-uri as xs:anyURI, $uri as xs:anyURI) {
    local:log-event("after", "move", "document", concat("from: ", $uri, " to: ", $new-uri))
};

declare function trigger:before-delete-document($uri as xs:anyURI) {
    local:log-event("before", "delete", "document", $uri)
};

declare function trigger:after-delete-document($uri as xs:anyURI) {
    local:log-event("after", "delete", "document", $uri)
};

declare function local:log-event($type as xs:string, $event as xs:string, $object-type as xs:string, $uri as xs:string) {
    let $log-collection := "/db"
    let $log := "triggers-log.xml"
    let $log-uri := concat($log-collection, "/", $log)
    return
        (
        (: create the log file if it does not exist :)
        if (not(doc-available($log-uri))) then
            xmldb:store($log-collection, $log, <triggers/>)
        else ()
        ,
        (: log the trigger details to the log file :)
        update insert <trigger event="{string-join(($type, $event, $object-type), "-")}" uri="{$uri}" timestamp="{current-dateTime()}"/> into doc($log-uri)/triggers
        )
};

Simple Logging Trigger in Java

import java.io.File;
import java.io.FileOutputStream;

import org.exist.collections.triggers.FilteringTrigger;
import org.exist.collections.triggers.TriggerException;
import org.exist.dom.DocumentImpl;
import org.exist.storage.DBBroker;
import org.exist.storage.txn.Txn;
import org.exist.xmldb.XmldbURI;
import org.exist.xquery.value.DateTimeValue;

/**
 	A simple Java Trigger that
	logs all trigger events for which it is executed
	in the file triggersLog.xml in the systems temporary folder
*/

public class LoggingTrigger extends FilteringTrigger implements DocumentTrigger
{
    private final static String TEMPLATE = "<?xml version=\"1.0\"?><events></events>";

    private DocumentImpl doc;

    public void configure(DBBroker broker, org.exist.collections.Collection parent, Map<String, List<?>> parameters) throws CollectionConfigurationException {
        super.configure(broker, parent, parameters);
        XmldbURI docPath = XmldbURI.create("messages.xml");
        System.out.println("TestTrigger prepares");
        this.doc = parent.getDocument(broker, docPath);
        if (this.doc == null) {
            TransactionManager transactMgr = broker.getBrokerPool().getTransactionManager();
            Txn transaction = transactMgr.beginTransaction();
            try {
                getLogger().debug("creating new file for collection contents");

                // IMPORTANT: temporarily disable triggers on the collection.
                // We would end up in infinite recursion if we don't do that
                parent.setTriggersEnabled(false);
                IndexInfo info = parent.validateXMLResource(transaction, broker, docPath, TEMPLATE);

                parent.store(transaction, broker, info, TEMPLATE, false);
                this.doc = info.getDocument();

                transactMgr.commit(transaction);
            } catch (Exception e) {
                transactMgr.abort(transaction);
                throw new CollectionConfigurationException(e.getMessage(), e);
            } finally {
                parent.setTriggersEnabled(true);
            }
        }
    }

	@Deprecated
    public void prepare(int event, DBBroker broker, Txn transaction, XmldbURI documentPath, DocumentImpl existingDocument) throws TriggerException {
    }

	@Deprecated
	public void finish(int event, DBBroker broker, Txn transaction, XmldbURI documentPath, DocumentImpl document) {
	}
	
	private void addRecord(DBBroker broker, String xupdate) throws TriggerException {
        MutableDocumentSet docs = new DefaultDocumentSet();
        docs.add(doc);
        try {
            // IMPORTANT: temporarily disable triggers on the collection.
            // We would end up in infinite recursion if we don't do that
            getCollection().setTriggersEnabled(false);
            // create the XUpdate processor
            XUpdateProcessor processor = new XUpdateProcessor(broker, docs, AccessContext.TRIGGER);
            // process the XUpdate
            Modification modifications[] = processor.parse(new InputSource(new StringReader(xupdate)));
            for (int i = 0; i < modifications.length; i++)
                modifications[i].process(null);
            broker.flush();
        } catch (Exception e) {
            e.printStackTrace();
            throw new TriggerException(e.getMessage(), e);
        } finally {
            // IMPORTANT: reenable trigger processing for the collection.
            getCollection().setTriggersEnabled(true);
        }

	}

	@Override
	public void beforeCreateDocument(DBBroker broker, Txn transaction, XmldbURI uri) throws TriggerException {
        String xupdate = "<?xml version=\"1.0\"?>" +
        "<xu:modifications version=\"1.0\" xmlns:xu=\"" + XUpdateProcessor.XUPDATE_NS + "\">" +
        "   <xu:append select='/events'>" +
        "       <xu:element name='event'>" +
        "           <xu:attribute name='id'>STORE-DOCUMENT</xu:attribute>" +
        "           <xu:attribute name='collection'>" + doc.getCollection().getURI() + "</xu:attribute>" +
        "       </xu:element>" +
        "   </xu:append>" +
        "</xu:modifications>";

        addRecord(broker, xupdate);
	}

	@Override
	public void afterCreateDocument(DBBroker broker, Txn transaction, DocumentImpl document) {
		//ignore this event
	}

	@Override
	public void beforeUpdateDocument(DBBroker broker, Txn transaction, DocumentImpl document) throws TriggerException {
		//ignore this event
	}

	@Override
	public void afterUpdateDocument(DBBroker broker, Txn transaction, DocumentImpl document) {
		//ignore this event
	}

	@Override
	public void beforeCopyDocument(DBBroker broker, Txn transaction, DocumentImpl document, XmldbURI newUri) throws TriggerException {
		//ignore this event
	}

	@Override
	public void afterCopyDocument(DBBroker broker, Txn transaction, DocumentImpl document, XmldbURI newUri) {
		//ignore this event
	}

	@Override
	public void beforeMoveDocument(DBBroker broker, Txn transaction, DocumentImpl document, XmldbURI newUri) throws TriggerException {
		//ignore this event
	}

	@Override
	public void afterMoveDocument(DBBroker broker, Txn transaction, DocumentImpl document, XmldbURI newUri) {
		//ignore this event
	}

	@Override
	public void beforeDeleteDocument(DBBroker broker, Txn transaction, DocumentImpl document) throws TriggerException {
        String xupdate = "<?xml version=\"1.0\"?>" +
        "<xu:modifications version=\"1.0\" xmlns:xu=\"" + XUpdateProcessor.XUPDATE_NS + "\">" +
        "   <xu:append select='/events'>" +
        "       <xu:element name='event'>" +
        "           <xu:attribute name='id'>REMOVE-DOCUMENT</xu:attribute>" +
        "           <xu:attribute name='collection'>" + doc.getCollection().getURI() + "</xu:attribute>" +
        "       </xu:element>" +
        "   </xu:append>" +
        "</xu:modifications>";
        
        addRecord(broker, xupdate);
	}

	@Override
	public void afterDeleteDocument(DBBroker broker, Txn transaction, XmldbURI uri) {
	}
}

Provided Triggers

eXist provides some triggers that might be useful:

HistoryTrigger

This collection trigger will save all old versions of documents before they are overwritten or removed. The old versions are kept in the 'history root' which is by default /db/history. This can be changed with the parameter root. You need to configure this trigger for every collection whose history you want to preserve.

The event attribute is ignored by HistoryTrigger

<collection xmlns="http://exist-db.org/collection-config/1.0">
  <triggers>
    <trigger class="org.exist.collections.triggers.HistoryTrigger"/>
  </triggers>
</collection>
STX Transformer Trigger

STXTransformerTrigger applies an STX stylesheet to the input SAX stream, using Joost. The stylesheet location is identified by parameter src. The src parameter is just a path, the stylesheet will be loaded from the database, otherwise, it is interpreted as an URI.

The event attribute is ignored by STXTransformerTrigger