package org.exist.fluent;
import java.lang.ref.*;
import java.util.*;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.exist.collections.Collection;
import org.exist.collections.triggers.CollectionTrigger;
import org.exist.collections.triggers.DocumentTrigger;
import org.exist.collections.triggers.SAXTrigger;
import org.exist.collections.triggers.TriggerException;
import org.exist.dom.persistent.DocumentImpl;
import org.exist.storage.DBBroker;
import org.exist.storage.txn.Txn;
import org.exist.xmldb.XmldbURI;
/**
* Internal class not for public use; needs to be public due to external instantiation requirements.
* Mediates between native eXist triggers and db listeners.
*
* @author <a href="mailto:piotr@ideanest.com">Piotr Kaminski</a>
*/
public class ListenerManager {
static String getTriggerConfigXml() {
return "<triggers><trigger class='org.exist.fluent.ListenerManager$TriggerDispatcher'/></triggers>";
}
static class EventKey implements Comparable<EventKey> {
final String path;
final Trigger trigger;
EventKey(String path, Trigger trigger) {
this.path = Database.normalizePath(path);
this.trigger = trigger;
}
EventKey(String path, int existEventCode, boolean before) {
this(path, toTrigger(existEventCode, before));
}
private static Trigger toTrigger(int code, boolean before) {
switch(code) {
case org.exist.collections.triggers.Trigger.STORE_DOCUMENT_EVENT:
return before ? Trigger.BEFORE_STORE : Trigger.AFTER_STORE;
case org.exist.collections.triggers.Trigger.CREATE_COLLECTION_EVENT:
return before ? Trigger.BEFORE_CREATE : Trigger.AFTER_CREATE;
case org.exist.collections.triggers.Trigger.UPDATE_DOCUMENT_EVENT:
return before ? Trigger.BEFORE_UPDATE : Trigger.AFTER_UPDATE;
case org.exist.collections.triggers.Trigger.RENAME_DOCUMENT_EVENT:
case org.exist.collections.triggers.Trigger.RENAME_COLLECTION_EVENT:
return before ? Trigger.BEFORE_RENAME : Trigger.AFTER_RENAME;
case org.exist.collections.triggers.Trigger.MOVE_DOCUMENT_EVENT:
case org.exist.collections.triggers.Trigger.MOVE_COLLECTION_EVENT:
return before ? Trigger.BEFORE_MOVE : Trigger.AFTER_MOVE;
case org.exist.collections.triggers.Trigger.REMOVE_DOCUMENT_EVENT:
case org.exist.collections.triggers.Trigger.REMOVE_COLLECTION_EVENT:
return before ? Trigger.BEFORE_REMOVE : Trigger.AFTER_REMOVE;
default:
throw new IllegalArgumentException("unknown exist trigger code " + code);
}
}
@Override
public boolean equals(Object o) {
if (o instanceof EventKey) {
EventKey that = (EventKey) o;
return
path.equals(that.path) &&
trigger == that.trigger;
}
return false;
}
@Override
public int hashCode() {
return path.hashCode() * 37 + trigger.hashCode();
}
public int compareTo(EventKey that) {
int r = this.trigger.compareTo(that.trigger);
if (r == 0) r = that.path.compareTo(this.path); // reverse order on purpose
return r;
}
boolean matchesAsPrefix(EventKey that) {
return
this.trigger == that.trigger &&
that.path.startsWith(this.path) &&
(this.path.equals("/") || this.path.length() == that.path.length() || that.path.charAt(this.path.length()) == '/');
}
}
private static class ListenerWrapper {
final Reference<Listener> refListener;
final Resource origin;
ListenerWrapper(Listener listener, Resource origin) {
this.refListener = new WeakReference<Listener>(listener);
this.origin = origin;
}
private Document wrap(DocumentImpl doc) {
return doc == null ? null : Document.newInstance(doc, origin);
}
private Folder wrap(org.exist.collections.Collection col) {
if (col == null) return null;
return new Folder(col.getURI().getCollectionPath(), false, origin);
}
boolean sameOrNull(Listener listener) {
Listener x = refListener.get();
return x == null || x == listener;
}
boolean isAlive() {
return refListener.get() != null;
}
void fireDocumentEvent(EventKey key, DocumentImpl doc) {
Listener listener = refListener.get();
if (listener instanceof Document.Listener) ((Document.Listener) listener).handle(new Document.Event(key, wrap(doc)));
}
void fireFolderEvent(EventKey key, Collection col) {
Listener listener = refListener.get();
if (listener instanceof Folder.Listener) ((Folder.Listener) listener).handle(new Folder.Event(key, wrap(col)));
}
}
enum Depth {
/**
* Targets matching the given path exactly.
*/
ZERO,
/**
* Targets one level below the given path (i.e. inside the given folder).
*/
ONE,
/**
* Targets matching or at any level below the given path.
*/
MANY}
private final Map<EventKey,List<ListenerWrapper>>[] listenerMaps;
@SuppressWarnings("unchecked")
private ListenerManager() {
listenerMaps = new Map[Depth.values().length];
listenerMaps[Depth.ZERO.ordinal()] = Collections.synchronizedMap(new HashMap<EventKey,List<ListenerWrapper>>());
listenerMaps[Depth.ONE.ordinal()] = Collections.synchronizedMap(new HashMap<EventKey,List<ListenerWrapper>>());
listenerMaps[Depth.MANY.ordinal()] = Collections.synchronizedSortedMap(new TreeMap<EventKey,List<ListenerWrapper>>());
}
private void checkListenerType(Listener listener) {
if (!(listener instanceof Document.Listener || listener instanceof Folder.Listener))
throw new IllegalArgumentException("invalid listener type " + listener.getClass().getName());
}
void add(String pathPrefix, Depth depth, Set<Trigger> triggers, Listener listener, Resource origin) {
checkListenerType(listener);
if (triggers.isEmpty()) throw new IllegalArgumentException("cannot add listener with empty set of triggers");
for (Trigger trigger : triggers) {
EventKey key = new EventKey(pathPrefix, trigger);
List<ListenerWrapper> list;
synchronized(listenerMaps[depth.ordinal()]) {
list = listenerMaps[depth.ordinal()].get(key);
if (list == null) {
list = new LinkedList<ListenerWrapper>();
listenerMaps[depth.ordinal()].put(key, list);
}
}
synchronized(list) {
list.add(new ListenerWrapper(listener, origin));
}
}
}
void remove(String pathPrefix, Depth depth, Listener listener) {
remove(pathPrefix, listenerMaps[depth.ordinal()], listener);
}
void remove(Listener listener) {
for (Map<EventKey,List<ListenerWrapper>> map : listenerMaps) remove(null, map, listener);
}
private void remove(String path, Map<EventKey,List<ListenerWrapper>> map, Listener listener) {
checkListenerType(listener);
synchronized(map) {
for (Iterator<Map.Entry<EventKey,List<ListenerWrapper>>> it = map.entrySet().iterator(); it.hasNext(); ) {
Map.Entry<EventKey,List<ListenerWrapper>> entry = it.next();
if (path != null && !entry.getKey().path.equals(path)) continue;
synchronized(entry.getValue()) {
for (Iterator<ListenerWrapper> it2 = entry.getValue().iterator(); it2.hasNext(); ) {
if (it2.next().sameOrNull(listener)) it2.remove();
}
}
if (entry.getValue().isEmpty()) it.remove();
}
}
}
void fire(EventKey key, DocumentImpl doc) {
fire(key, doc, null, true);
}
void fire(EventKey key, org.exist.collections.Collection col) {
fire(key, null, col, false);
}
private void fire(EventKey key, DocumentImpl doc, org.exist.collections.Collection col, boolean documentEvent) {
fire(listenerMaps[Depth.ZERO.ordinal()].get(key), key, doc, col, documentEvent);
int k = key.path.lastIndexOf('/');
assert k != -1;
if (k > 0) {
EventKey trimmedKey = new EventKey(key.path.substring(0, k), key.trigger);
fire(listenerMaps[Depth.ONE.ordinal()].get(trimmedKey), key, doc, col, documentEvent);
}
SortedMap<EventKey,List<ListenerWrapper>> map =
(SortedMap<EventKey,List<ListenerWrapper>>) listenerMaps[Depth.MANY.ordinal()];
SortedMap<EventKey,List<ListenerWrapper>> tailMap;
synchronized(map) {
tailMap = new TreeMap<EventKey,List<ListenerWrapper>>(map.tailMap(key));
}
for (Map.Entry<EventKey,List<ListenerWrapper>> entry : tailMap.entrySet()) {
EventKey target = entry.getKey();
if (!target.matchesAsPrefix(key)) break;
fire(entry.getValue(), key, doc, col, documentEvent);
}
}
private void fire(List<ListenerWrapper> list, EventKey key, DocumentImpl doc, org.exist.collections.Collection col, boolean documentEvent) {
if (list == null) return;
List<ListenerWrapper> listCopy;
synchronized(list) {
for (Iterator<ListenerWrapper> it = list.iterator(); it.hasNext(); ) {
if (!it.next().isAlive()) it.remove();
}
listCopy = new ArrayList<ListenerWrapper>(list);
}
if (documentEvent) {
for (ListenerWrapper wrap : listCopy) wrap.fireDocumentEvent(key, doc);
} else {
for (ListenerWrapper wrap : listCopy) wrap.fireFolderEvent(key, col);
}
}
static final ListenerManager INSTANCE = new ListenerManager();
/**
* A centralized trigger listener for eXist that dispatches back to the singleton
* <code>ListenerManager</code>. Public only because it needs to be instantiated
* via reflection; for internal use only.
*
* @author <a href="mailto:piotr@ideanest.com">Piotr Kaminski</a>
*/
public static class TriggerDispatcher extends SAXTrigger implements DocumentTrigger, CollectionTrigger {
private static final Logger LOG = LogManager.getLogger(TriggerDispatcher.class);
public void configure(DBBroker broker, org.exist.collections.Collection parent, Map<String, List<? extends Object>> parameters) {
// nothing to do
}
public Logger getLogger() {
return LOG;
}
@Override
public void beforeCreateCollection(DBBroker broker, Txn transaction, XmldbURI uri) throws TriggerException {
EventKey key = new EventKey(uri.toString(), Trigger.BEFORE_CREATE);
INSTANCE.fire(key, null, null, false);
}
@Override
public void afterCreateCollection(DBBroker broker, Txn transaction, Collection collection) throws TriggerException {
EventKey key = new EventKey(collection.getURI().toString(), Trigger.AFTER_CREATE);
INSTANCE.fire(key, null, collection, false);
}
@Override
public void beforeCopyCollection(DBBroker broker, Txn transaction, Collection collection, XmldbURI newUri) throws TriggerException {
EventKey key = new EventKey(newUri.toString(), Trigger.BEFORE_CREATE);
INSTANCE.fire(key, null, null, false);
}
@Override
public void afterCopyCollection(DBBroker broker, Txn transaction, Collection collection, XmldbURI newUri) throws TriggerException {
EventKey key = new EventKey(newUri.toString(), Trigger.AFTER_CREATE);
INSTANCE.fire(key, null, collection, false);
}
@Override
public void beforeMoveCollection(DBBroker broker, Txn transaction, Collection collection, XmldbURI newUri) throws TriggerException {
EventKey key = new EventKey(collection.getURI().toString(), Trigger.BEFORE_MOVE);
INSTANCE.fire(key, null, collection, false);
}
@Override
public void afterMoveCollection(DBBroker broker, Txn transaction, Collection collection, XmldbURI newUri) throws TriggerException {
EventKey key = new EventKey(collection.getURI().toString(), Trigger.AFTER_MOVE);
INSTANCE.fire(key, null, collection, false);
}
@Override
public void beforeDeleteCollection(DBBroker broker, Txn transaction, Collection collection) throws TriggerException {
EventKey key = new EventKey(collection.getURI().toString(), Trigger.BEFORE_REMOVE);
INSTANCE.fire(key, null, collection, false);
}
@Override
public void afterDeleteCollection(DBBroker broker, Txn transaction, XmldbURI uri) throws TriggerException {
EventKey key = new EventKey(uri.toString(), Trigger.AFTER_REMOVE);
INSTANCE.fire(key, (DocumentImpl)null, null, false);
}
@Override
public void beforeCreateDocument(DBBroker broker, Txn transaction, XmldbURI uri) throws TriggerException {
EventKey key = new EventKey(uri.toString(), Trigger.BEFORE_CREATE);
INSTANCE.fire(key, (DocumentImpl)null, (org.exist.collections.Collection)null, true);
}
@Override
public void afterCreateDocument(DBBroker broker, Txn transaction, DocumentImpl document) { //throws TriggerException {
EventKey key = new EventKey(document.getURI().toString(), Trigger.AFTER_CREATE);
INSTANCE.fire(key, document, null, true);
}
@Override
public void beforeUpdateDocument(DBBroker broker, Txn transaction, DocumentImpl document) throws TriggerException {
EventKey key = new EventKey(document.getURI().toString(), Trigger.BEFORE_UPDATE);
INSTANCE.fire(key, document, null, true);
}
@Override
public void afterUpdateDocument(DBBroker broker, Txn transaction, DocumentImpl document) { //throws TriggerException {
EventKey key = new EventKey(document.getURI().toString(), Trigger.AFTER_UPDATE);
INSTANCE.fire(key, document, null, true);
}
@Override
public void beforeCopyDocument(DBBroker broker, Txn transaction, DocumentImpl document, XmldbURI newUri) throws TriggerException {
EventKey key = new EventKey(newUri.toString(), Trigger.BEFORE_CREATE);
INSTANCE.fire(key, document, null, true);
}
@Override
public void afterCopyDocument(DBBroker broker, Txn transaction, DocumentImpl document, XmldbURI newUri) { //throws TriggerException {
EventKey key = new EventKey(document.getURI().toString(), Trigger.AFTER_CREATE);
INSTANCE.fire(key, document, null, true);
}
@Override
public void beforeMoveDocument(DBBroker broker, Txn transaction, DocumentImpl document, XmldbURI newUri) throws TriggerException {
EventKey key = new EventKey(document.getURI().toString(), Trigger.BEFORE_RENAME);
INSTANCE.fire(key, document, null, true);
}
@Override
public void afterMoveDocument(DBBroker broker, Txn transaction, DocumentImpl document, XmldbURI newUri) { //throws TriggerException {
EventKey key = new EventKey(document.getURI().toString(), Trigger.AFTER_RENAME);
INSTANCE.fire(key, document, null, true);
}
@Override
public void beforeDeleteDocument(DBBroker broker, Txn transaction, DocumentImpl document) throws TriggerException {
EventKey key = new EventKey(document.getURI().toString(), Trigger.BEFORE_REMOVE);
INSTANCE.fire(key, document, null, true);
}
@Override
public void afterDeleteDocument(DBBroker broker, Txn transaction, XmldbURI uri) { //throws TriggerException {
EventKey key = new EventKey(uri.toString(), Trigger.AFTER_REMOVE);
INSTANCE.fire(key, null, null, true);
}
@Override
public void beforeUpdateDocumentMetadata(DBBroker broker, Txn txn, DocumentImpl document) throws TriggerException {
EventKey key = new EventKey(document.getURI().toString(), Trigger.BEFORE_UPDATE_META);
INSTANCE.fire(key, document, null, true);
}
@Override
public void afterUpdateDocumentMetadata(DBBroker broker, Txn txn, DocumentImpl document) throws TriggerException {
EventKey key = new EventKey(document.getURI().toString(), Trigger.AFTER_UPDATE_META);
INSTANCE.fire(key, document, null, true);
}
}
}