package org.exist.fluent;
import java.lang.ref.*;
import java.util.*;
import org.apache.log4j.Logger;
import org.exist.collections.*;
import org.exist.collections.Collection;
import org.exist.collections.triggers.*;
import org.exist.dom.DocumentImpl;
import org.exist.storage.DBBroker;
import org.exist.storage.txn.Txn;
import org.exist.xmldb.XmldbURI;
import org.xml.sax.*;
import org.xml.sax.ext.LexicalHandler;
/**
* 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 event='store update remove create-collection rename-collection delete-collection' 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:
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:
case org.exist.collections.triggers.Trigger.RENAME_COLLECTION_EVENT:
return before ? Trigger.BEFORE_UPDATE : Trigger.AFTER_UPDATE;
case org.exist.collections.triggers.Trigger.REMOVE_DOCUMENT_EVENT:
case org.exist.collections.triggers.Trigger.DELETE_COLLECTION_EVENT:
return before ? Trigger.BEFORE_DELETE : Trigger.AFTER_DELETE;
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 implements DocumentTrigger, CollectionTrigger {
private static final Logger LOG = Logger.getLogger(TriggerDispatcher.class);
private boolean validating;
private ContentHandler contentHandler;
private LexicalHandler lexicalHandler;
@SuppressWarnings("unchecked")
public void configure(DBBroker broker, org.exist.collections.Collection parent, Map parameters) throws CollectionConfigurationException {
// nothing to do
}
public void prepare(int event, DBBroker broker, Txn txn, XmldbURI documentPath, DocumentImpl existingDocument) throws TriggerException {
EventKey key = new EventKey(documentPath.getCollectionPath(), event, true);
INSTANCE.fire(key, existingDocument);
}
public void finish(int event, DBBroker broker, Txn txn, XmldbURI documentPath, DocumentImpl document) {
EventKey key = new EventKey(documentPath.getCollectionPath(), event, false);
INSTANCE.fire(key, key.trigger == Trigger.AFTER_DELETE ? null : document);
}
public void prepare(int event, DBBroker broker, Txn txn, org.exist.collections.Collection collection, String newName) throws TriggerException {
EventKey key = new EventKey(newName, event, true);
INSTANCE.fire(key, collection);
}
public void finish(int event, DBBroker broker, Txn txn, org.exist.collections.Collection collection, String newName) {
EventKey key = new EventKey(newName, event, false);
INSTANCE.fire(key, key.trigger == Trigger.AFTER_DELETE ? null : collection);
}
public boolean isValidating() {
return validating;
}
public void setValidating(boolean validating) {
this.validating = validating;
}
public void setOutputHandler(ContentHandler handler) {
this.contentHandler = handler;
}
public void setLexicalOutputHandler(LexicalHandler handler) {
this.lexicalHandler = handler;
}
public ContentHandler getOutputHandler() {
return contentHandler;
}
public ContentHandler getInputHandler() {
return this;
}
public LexicalHandler getLexicalOutputHandler() {
return lexicalHandler;
}
public LexicalHandler getLexicalInputHandler() {
return this;
}
public Logger getLogger() {
return LOG;
}
public void characters(char[] ch, int start, int length) throws SAXException {
contentHandler.characters(ch, start, length);
}
public void endDocument() throws SAXException {
contentHandler.endDocument();
}
public void endElement(String uri, String localName, String qName) throws SAXException {
contentHandler.endElement(uri, localName, qName);
}
public void endPrefixMapping(String prefix) throws SAXException {
contentHandler.endPrefixMapping(prefix);
}
public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException {
contentHandler.ignorableWhitespace(ch, start, length);
}
public void processingInstruction(String target, String data) throws SAXException {
contentHandler.processingInstruction(target, data);
}
public void setDocumentLocator(Locator locator) {
contentHandler.setDocumentLocator(locator);
}
public void skippedEntity(String name) throws SAXException {
contentHandler.skippedEntity(name);
}
public void startDocument() throws SAXException {
contentHandler.startDocument();
}
public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
contentHandler.startElement(uri, localName, qName, atts);
}
public void startPrefixMapping(String prefix, String uri) throws SAXException {
contentHandler.startPrefixMapping(prefix, uri);
}
public void comment(char[] ch, int start, int length) throws SAXException {
lexicalHandler.comment(ch, start, length);
}
public void endCDATA() throws SAXException {
lexicalHandler.endCDATA();
}
public void endDTD() throws SAXException {
lexicalHandler.endDTD();
}
public void endEntity(String name) throws SAXException {
lexicalHandler.endEntity(name);
}
public void startCDATA() throws SAXException {
lexicalHandler.startCDATA();
}
public void startDTD(String name, String publicId, String systemId) throws SAXException {
lexicalHandler.startDTD(name, publicId, systemId);
}
public void startEntity(String name) throws SAXException {
lexicalHandler.startEntity(name);
}
}
}