/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.services.events;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.lang.ArrayUtils;
import org.dspace.kernel.mixins.ShutdownService;
import org.dspace.services.CachingService;
import org.dspace.services.EventService;
import org.dspace.services.RequestService;
import org.dspace.services.SessionService;
import org.dspace.services.model.Cache;
import org.dspace.services.model.CacheConfig;
import org.dspace.services.model.Event;
import org.dspace.services.model.EventListener;
import org.dspace.services.model.RequestInterceptor;
import org.dspace.services.model.Session;
import org.dspace.services.model.CacheConfig.CacheScope;
import org.dspace.services.model.Event.Scope;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
/**
* This is a placeholder until we get a real event service going.
* It does pretty much everything the service should do EXCEPT sending
* the events across a cluster.
*
* @author Aaron Zeckoski (azeckoski@gmail.com) - azeckoski - 4:02:31 PM Nov 19, 2008
*/
public final class SystemEventService implements EventService, ShutdownService {
private final Logger log = LoggerFactory.getLogger(SystemEventService.class);
private static final String QUEUE_CACHE_NAME = "eventQueueCache";
/**
* Map for holding onto the listeners which is ClassLoader safe.
*/
private Map<String, EventListener> listenersMap = new ConcurrentHashMap<String, EventListener>();
private final RequestService requestService;
private final SessionService sessionService;
private final CachingService cachingService;
private EventRequestInterceptor requestInterceptor;
@Autowired(required=true)
public SystemEventService(RequestService requestService, SessionService sessionService, CachingService cachingService) {
if (requestService == null || cachingService == null || sessionService == null) {
throw new IllegalArgumentException("requestService, cachingService, and all inputs must not be null");
}
this.requestService = requestService;
this.sessionService = sessionService;
this.cachingService = cachingService;
// register interceptor
this.requestInterceptor = new EventRequestInterceptor();
this.requestService.registerRequestInterceptor(this.requestInterceptor);
}
/* (non-Javadoc)
* @see org.dspace.kernel.mixins.ShutdownService#shutdown()
*/
public void shutdown() {
this.requestInterceptor = null; // clear the interceptor
this.listenersMap.clear();
}
/* (non-Javadoc)
* @see org.dspace.services.EventService#fireEvent(org.dspace.services.model.Event)
*/
public void fireEvent(Event event) {
validateEvent(event);
// check scopes for this event
Scope[] scopes = event.getScopes();
boolean local = ArrayUtils.contains(scopes, Scope.LOCAL);
if (local) {
fireLocalEvent(event);
}
boolean cluster = ArrayUtils.contains(scopes, Scope.CLUSTER);
if (cluster) {
fireClusterEvent(event);
}
boolean external = ArrayUtils.contains(scopes, Scope.EXTERNAL);
if (external) {
fireExternalEvent(event);
}
}
/* (non-Javadoc)
* @see org.dspace.services.EventService#queueEvent(org.dspace.services.model.Event)
*/
public void queueEvent(Event event) {
validateEvent(event);
// get the cache
Cache queueCache = this.cachingService.getCache(QUEUE_CACHE_NAME, new CacheConfig(CacheScope.REQUEST));
// put the event in the queue if this is in a request
if (requestService.getCurrentRequestId() != null) {
// create a key which is orderable and unique
String key = System.currentTimeMillis() + ":" + queueCache.size() + ":" + event.getId();
queueCache.put(key, event);
} else {
// no request so fire the event immediately
log.info("No request to queue this event ("+event+") so firing immediately");
fireEvent(event);
}
}
/* (non-Javadoc)
* @see org.dspace.services.EventService#registerEventListener(org.dspace.services.model.EventListener)
*/
public void registerEventListener(EventListener listener) {
if (listener == null) {
throw new IllegalArgumentException("Cannot register a listener that is null");
}
String key = listener.getClass().getName();
this.listenersMap.put(key, listener);
}
/**
* Fires off a local event immediately.
* This is internal so the event should have already been validated.
*
* @param event a valid event
*/
private void fireLocalEvent(Event event) {
// send event to all interested listeners
for (EventListener listener : listenersMap.values()) {
// filter the event if the listener has filter rules
if (listener != null && filterEvent(listener, event) ) {
// passed filters so send the event to this listener
try {
listener.receiveEvent(event);
} catch (Exception e) {
log.warn("Listener ("+listener+")["+listener.getClass().getName()+"] failed to recieve event ("+event+"): " + e.getMessage() + ":" + e.getCause());
}
}
}
}
/**
* Will eventually fire events to the entire cluster.
* TODO not implemented.
*
* @param event a validated event
*/
private void fireClusterEvent(Event event) {
log.debug("fireClusterEvent is not implemented yet, no support for cluster events yet, could not fire event to the cluster: " + event);
}
/**
* Will eventually fire events to external systems.
* TODO not implemented.
*
* @param event a validated event
*/
private void fireExternalEvent(Event event) {
log.debug("fireExternalEvent is not implemented yet, no support for external events yet, could not fire event to external listeners: " + event);
}
/**
* Fires all queued events for the current request.
*
* @return the number of events which were fired
*/
protected int fireQueuedEvents() {
int fired = 0;
Cache queueCache = this.cachingService.getCache(QUEUE_CACHE_NAME, new CacheConfig(CacheScope.REQUEST));
List<String> eventIds = queueCache.getKeys();
Collections.sort(eventIds); // put it in the order they were added (hopefully)
if (eventIds.size() > 0) {
for (String eventId : eventIds) {
Event event = (Event) queueCache.get(eventId);
fireEvent(event);
fired++;
}
}
queueCache.clear();
return fired;
}
/**
* Clears all events for the current request.
*
* @return the number of events that were cleared
*/
protected int clearQueuedEvents() {
Cache queueCache = this.cachingService.getCache(QUEUE_CACHE_NAME, new CacheConfig(CacheScope.REQUEST));
int cleared = queueCache.size();
queueCache.clear();
return cleared;
}
/**
* This will validate the event object and set any values which are
* unset but can be figured out.
*
* @param event the event which is being sent into the system
*/
private void validateEvent(Event event) {
if (event == null) {
throw new IllegalArgumentException("Cannot fire null events");
}
if (event.getName() == null || "".equals(event.getName()) ) {
throw new IllegalArgumentException("Event name must be set");
}
if (event.getId() == null || "".equals(event.getId()) ) {
// generate an id then
event.setId(makeEventId());
}
if (event.getUserId() == null || "".equals(event.getUserId()) ) {
// set to the current user
String userId = this.sessionService.getCurrentUserId();
event.setUserId(userId);
}
if (event.getScopes() == null) {
// set to local/cluster scope
event.setScopes( new Event.Scope[] {Scope.LOCAL, Scope.CLUSTER});
}
}
/**
* Checks to see if the filtering in the given listener allows the
* event to be received.
*
* @param listener an event listener
* @param event an event
* @return true if the event should be received, false if the event is filtered out
*/
private boolean filterEvent(EventListener listener, Event event) {
if (listener == null || event == null) {
return false;
}
// filter the event if the listener has filter rules
boolean allowName = true;
try {
String[] namePrefixes = listener.getEventNamePrefixes();
if (namePrefixes != null && namePrefixes.length > 0) {
allowName = false;
for (String namePrefix : namePrefixes) {
String eventName = event.getName();
if (namePrefix != null && namePrefix.length() > 0 && eventName.startsWith(namePrefix)) {
allowName = true;
break;
}
}
}
} catch (Exception e1) {
log.warn("Listener ("+listener+")["+listener.getClass().getName()+"] failure calling getEventNamePrefixes: " + e1.getMessage() + ":" + e1.getCause());
}
boolean allowResource = true;
try {
String resourcePrefix = listener.getResourcePrefix();
if (resourcePrefix != null && resourcePrefix.length() > 0) {
allowResource = false;
String resRef = event.getResourceReference();
if (resRef == null) {
// null references default to unfiltered
allowResource = true;
} else {
if (resRef.startsWith(resourcePrefix)) {
allowResource = true;
}
}
}
} catch (Exception e1) {
log.warn("Listener ("+listener+")["+listener.getClass().getName()+"] failure calling getResourcePrefix: " + e1.getMessage() + ":" + e1.getCause());
}
return allowName && allowResource;
}
private Random random = new Random();
/**
* Generate an event ID used to identify and track this event uniquely.
*
* @return event Id
*/
private String makeEventId() {
return "event-" + random.nextInt(1000) + "-" + System.currentTimeMillis();
}
/**
* The request interceptor for the event service.
* This will take care of firing queued events at the end of the request.
*
* @author Aaron Zeckoski (azeckoski@gmail.com) - azeckoski - 10:24:58 AM Nov 20, 2008
*/
public final class EventRequestInterceptor implements RequestInterceptor {
/* (non-Javadoc)
* @see org.dspace.services.model.RequestInterceptor#onStart(java.lang.String, org.dspace.services.model.Session)
*/
public void onStart(String requestId, Session session) {
// nothing to really do here unless we decide we should purge out any existing events? -AZ
}
/* (non-Javadoc)
* @see org.dspace.services.model.RequestInterceptor#onEnd(java.lang.String, org.dspace.services.model.Session, boolean, java.lang.Exception)
*/
public void onEnd(String requestId, Session session, boolean succeeded, Exception failure) {
if (succeeded) {
int fired = fireQueuedEvents();
log.debug("Fired "+fired+" events at the end of the request ("+requestId+")");
} else {
int cleared = clearQueuedEvents();
log.debug("Cleared/cancelled "+cleared+" events at the end of the failed request ("+requestId+")");
}
}
/* (non-Javadoc)
* @see org.dspace.kernel.mixins.OrderedService#getOrder()
*/
public int getOrder() {
return 20; // this should fire pretty late
}
}
}