diff --git a/mbassador.iml b/mbassador.iml new file mode 100644 index 0000000..f6a4b36 --- /dev/null +++ b/mbassador.iml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..8b6e0bd --- /dev/null +++ b/pom.xml @@ -0,0 +1,49 @@ + + + 4.0.0 + org.mbassy + mbassador + 1.0.0.RC + jar + mbassador + Library for simple implementation of bidirectional conversions + + + UTF-8 + 1.6 + + + + + + junit + junit + 4.10 + compile + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${project.build.java.version} + ${project.build.java.version} + + + + + org.apache.maven.plugins + maven-surefire-plugin + + false + + + + + + diff --git a/src/main/java/org/mbassy/Listener.java b/src/main/java/org/mbassy/Listener.java new file mode 100644 index 0000000..b5f2985 --- /dev/null +++ b/src/main/java/org/mbassy/Listener.java @@ -0,0 +1,27 @@ +package org.mbassy; + +import org.mbassy.filter.Filter; + +import java.lang.annotation.*; + +/** + * TODO. Insert class description here + *

+ * User: benni + * Date: 2/8/12 + * Time: 3:35 PM + */ +@Retention(value = RetentionPolicy.RUNTIME) +@Inherited +@Target(value = {ElementType.METHOD}) +public @interface Listener { + + Filter[] value() default {}; // no filters by default + + Dispatch mode() default Dispatch.Synchronous; + + public static enum Dispatch{ + Synchronous,Asynchronous + } + +} diff --git a/src/main/java/org/mbassy/MBassador.java b/src/main/java/org/mbassy/MBassador.java new file mode 100644 index 0000000..3be67bc --- /dev/null +++ b/src/main/java/org/mbassy/MBassador.java @@ -0,0 +1,427 @@ +package org.mbassy; + +import org.mbassy.filter.Filter; +import org.mbassy.filter.MessageFilter; +import org.mbassy.common.*; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.*; +import java.util.concurrent.*; + +/** + * + * A message bus offers facilities for publishing messages to registered listeners. Messages can be dispatched + * synchronously or asynchronously and may be of any type that is a valid sub type of the type parameter T. + * The dispatch mechanism can by controlled for each concrete message publication. + * A message publication is the publication of any message using one of the bus' publish(..) methods. + *

+ * Each message publication is isolated from all other running publications such that it does not interfere with them. + * Hence, the bus expects message handlers to be stateless as it may invoke them concurrently if multiple + * messages get published asynchronously. + *

+ * A listener is any object that defines at least one message handler and that has been subscribed to at least + * one message bus. A message handler can be any method that accepts exactly one parameter (the message) and is marked + * as a message handler using the @Listener annotation. + *

+ * The bus uses weak references to all listeners such that registered listeners do not need to + * be explicitly unregistered to be eligible for garbage collection. Dead (garbage collected) listeners are + * removed on-the-fly as messages get dispatched. + *

+ * Generally message handlers will be invoked in inverse sequence of insertion (subscription) but any + * class using this bus should not rely on this assumption. The basic contract of the bus is that it will deliver + * a specific message exactly once to each of the subscribed message handlers. + *

+ * Messages are dispatched to all listeners that accept the type or supertype of the dispatched message. Additionally + * a message handler may define filters to narrow the set of messages that it accepts. + *

+ * Subscribed message handlers are available to all pending message publications that have not yet started processing. + * Any messageHandler may only be subscribed once (subsequent subscriptions of an already subscribed messageHandler will be silently ignored) + *

+ * Removing a listener means removing all subscribed message handlers of that object. This remove operation + * immediately takes effect and on all running dispatch processes. A removed listener (a listener + * is considered removed after the remove(Object) call returned) will under no circumstances receive any message publications. + * + * NOTE: Generic type parameters of messages will not be taken into account, e.g. a List will + * get dispatched to all message handlers that take an instance of List as their parameter + * + * @Author bennidi + * Date: 2/8/12 + */ +public class MBassador{ + + + // This predicate is used to find all message listeners (methods annotated with @Listener) + private static final IPredicate AllMessageListeners = new IPredicate() { + @Override + public boolean apply(Method target) { + return target.getAnnotation(Listener.class) != null; + } + }; + + // This is the default error handler it will simply log to standard out and + // print stack trace if available + protected static final class ConsoleLogger implements IPublicationErrorHandler { + @Override + public void handleError(PublicationError error) { + System.out.println(error); + if (error.getCause() != null) error.getCause().printStackTrace(); + } + }; + + // executor for asynchronous listeners using unbound queuing strategy to ensure that no events get lost + private ExecutorService executor = new ThreadPoolExecutor(5, 50, 1, TimeUnit.MINUTES, new LinkedBlockingQueue()); + + // cache already created filter instances + private final Map, MessageFilter> filterCache = new HashMap, MessageFilter>(); + + // all subscriptions per message type + // this is the primary list for dispatching a specific message + private final Map> subscriptionsPerMessage = new HashMap(50); + + // all subscriptions per messageHandler type + // this list provides access for subscribing and unsubsribing + private final Map> subscriptionsPerListener = new HashMap(50); + + // remember already processed classes that do not contain any listeners + private final Collection nonListeners = new HashSet(); + + // this handler will receive all errors that occur during message dispatch or message handling + private IPublicationErrorHandler errorHandler = new ConsoleLogger(); + + + // all threads that are available for asynchronous message dispatching + private final CopyOnWriteArrayList dispatchers = new CopyOnWriteArrayList(); + + // all pending messages scheduled for asynchronous dispatch are queued here + private final LinkedBlockingQueue pendingMessages = new LinkedBlockingQueue(); + + // initialize the dispatch workers + private void initDispatcherThreads(int numberOfThreads) { + for (int i = 0; i < numberOfThreads; i++) { + // each thread will run forever and process incoming + //dispatch requests + Thread dispatcher = new Thread(new Runnable() { + public void run() { + while (true) { + try { + publish(pendingMessages.take()); + } catch (InterruptedException e) { + errorHandler.handleError(new PublicationError(e, "Asnchronous publication interupted", null, null, null)); + return; + } + } + } + }); + dispatchers.add(dispatcher); + dispatcher.start(); + } + } + + public MBassador(){ + initDispatcherThreads(2); + } + + public MBassador(int dispatcherThreadCount){ + initDispatcherThreads(dispatcherThreadCount > 0 ? dispatcherThreadCount : 2); + } + + + public void publishAsync(T message){ + pendingMessages.offer(message); + } + + + /** + * Synchronously publish a message to all registered listeners (this includes listeners defined for super types) + * The call blocks until every messageHandler has processed the message. + * + * @param message + */ + public void publish(T message){ + try { + final Collection subscriptions = getSubscriptionsByMessageType(message.getClass()); + for (Subscription subscription : subscriptions) subscription.publish(message); + } catch (Throwable e) { + handlePublicationError(new PublicationError() + .setMessage("Error during publication of message") + .setCause(e) + .setPublishedObject(message)); + } + + } + + /** + * Immediately unsubscribe all registered message handlers (if any) of the given listener. When this call returns + * have effectively been removed and will not receive any message publications (including asynchronously scheduled + * publications that have been published when the messageHandler was still subscribed). + * A call to this method passing null, an already subscribed message or any message that does not define any listeners + * will not have any effect. + * + * @param listener + */ + public void unsubscribe(Object listener){ + if (listener == null) return; + Collection subscriptions = subscriptionsPerListener.get(listener.getClass()); + for (Subscription subscription : subscriptions) { + subscription.unsubscribe(listener); + } + } + + + /** + * Subscribe all listeners of the given message to receive message publications. + * Any message may only be subscribed once (subsequent subscriptions of an already subscribed + * message will be silently ignored) + * + * @param listener + */ + public void subscribe(Object listener){ + Class listeningClass = listener.getClass(); + if (nonListeners.contains(listeningClass)) + return; // early reject of known classes that do not participate in eventing + Collection subscriptionsByListener = subscriptionsPerListener.get(listeningClass); + if (subscriptionsByListener == null) { // if the type is registered for the first time + synchronized (this) { // new subscriptions must be processed sequentially for each class + subscriptionsByListener = subscriptionsPerListener.get(listeningClass); + if (subscriptionsByListener == null) { // double check (a bit ugly but works here) + List messageHandlers = getListeners(listeningClass); // get all methods with subscriptions + subscriptionsByListener = new ArrayList(messageHandlers.size()); // it's safe to use non-concurrent collection here (read only) + if (messageHandlers.isEmpty()) { // remember the class as non listening class + nonListeners.add(listeningClass); + return; + } + // create subscriptions for all detected listeners + for (Method messageHandler : messageHandlers) { + if (!isValidMessageHandler(messageHandler)) continue; // ignore invalid listeners + MessageFilter[] filter = getFilter(messageHandler.getAnnotation(Listener.class)); + Class eventType = getMessageType(messageHandler); + Subscription subscription = new Subscription(messageHandler, filter); + subscription.subscribe(listener); + addMessageTypeSubscription(eventType, subscription); + subscriptionsByListener.add(subscription); + //updateMessageTypeHierarchy(eventType); + } + subscriptionsPerListener.put(listeningClass, subscriptionsByListener); + } + } + } + // register the message to the existing subscriptions + for (Subscription sub : subscriptionsByListener) sub.subscribe(listener); + } + + + public void setErrorHandler(IPublicationErrorHandler handler){ + this.errorHandler = handler; + } + + + + // obtain the set of subscriptions for the given message type + private Collection getSubscriptionsByMessageType(Class messageType) { + // TODO improve with cache + Collection subscriptions = new LinkedList(); + + if(subscriptionsPerMessage.get(messageType) != null) { + subscriptions.addAll(subscriptionsPerMessage.get(messageType)); + } + for (Class eventSuperType : getSuperclasses(messageType)){ + if(subscriptionsPerMessage.get(eventSuperType) != null){ + subscriptions.addAll(subscriptionsPerMessage.get(eventSuperType)); + } + } + + return subscriptions; + } + + private Collection getSuperclasses(Class from){ + Collection superclasses = new LinkedList(); + while(!from.equals(Object.class)){ + superclasses.add(from.getSuperclass()); + from = from.getSuperclass(); + } + return superclasses; + } + + // associate a suscription with a message type + private void addMessageTypeSubscription(Class messageType, Subscription subscription) { + Collection subscriptions = subscriptionsPerMessage.get(messageType); + if (subscriptions == null) { + subscriptions = new CopyOnWriteArraySet(); + subscriptionsPerMessage.put(messageType, subscriptions); + } + subscriptions.add(subscription); + } + + /* + private void updateMessageTypeHierarchy(Class messageType) { + for (Class existingEventType : subscriptionsPerMessage.keySet()) { + if (existingEventType.equals(messageType)) continue; + if (messageType.isAssignableFrom(existingEventType)) //message is super type of existing + messageTypeHierarchy.put(existingEventType, messageType); + else if (existingEventType.isAssignableFrom(messageType)) { // message is sub type of existing + messageTypeHierarchy.put(messageType, existingEventType); // add direct super type + messageTypeHierarchy.putAll(messageType, messageTypeHierarchy.get(existingEventType)); // add all super types of super type + } + } + }*/ + + + private boolean isValidMessageHandler(Method handler) { + if (handler.getParameterTypes().length != 1) { + // a messageHandler only defines one parameter (the message) + System.out.println("Found nono or more than one parameter in messageHandler [" + handler.getName() + + "]. A messageHandler must define exactly one parameter"); + return false; + } + return true; + } + + private static Class getMessageType(Method listener) { + return listener.getParameterTypes()[0]; + } + + // get all listeners defined by the given class (includes + // listeners defined in super classes) + private static List getListeners(Class target) { + return ReflectionUtils.getMethods(AllMessageListeners, target); + } + + // retrieve all instances of filters associated with the given subscription + private MessageFilter[] getFilter(Listener subscription) { + if (subscription.value().length == 0) return null; + MessageFilter[] filters = new MessageFilter[subscription.value().length]; + int i = 0; + for (Filter filterDef : subscription.value()) { + MessageFilter filter = filterCache.get(filterDef.value()); + if (filter == null) { + try { + filter = filterDef.value().newInstance(); + filterCache.put(filterDef.value(), filter); + } catch (Throwable e) { + handlePublicationError(new PublicationError() + .setMessage("Error retrieving filter")); + } + + } + filters[i] = filter; + i++; + } + return filters; + } + + + + private void handlePublicationError(PublicationError error) { + errorHandler.handleError(error); + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + for(Thread dispatcher : dispatchers){ + dispatcher.interrupt(); + } + } + + /** + * Subscription is a thread safe container for objects that contain message handlers + * + */ + private class Subscription { + + private final MessageFilter[] filter; + + private final Method messageHandler; + + private ConcurrentLinkedBag listeners = new ConcurrentLinkedBag(); + + private boolean isAynchronous; + + private Subscription(Method messageHandler, MessageFilter[] filter) { + this.messageHandler = messageHandler; + this.filter = filter; + this.messageHandler.setAccessible(true); + this.isAynchronous = messageHandler.getAnnotation(Listener.class).mode().equals(Listener.Dispatch.Asynchronous); + } + + + public void subscribe(Object o) { + listeners.add(o); + + } + + private void dispatch(final Object message, final Object listener){ + if(isAynchronous){ + MBassador.this.executor.execute(new Runnable() { + @Override + public void run() { + invokeHandler(message, listener); + } + }); + } + else{ + invokeHandler(message, listener); + } + } + + private void invokeHandler(final Object message, final Object listener){ + try { + messageHandler.invoke(listener, message); + }catch(IllegalAccessException e){ + MBassador.this.handlePublicationError( + new PublicationError(e, "Error during messageHandler notification. " + + "The class or method is not accessible", + messageHandler, listener, message)); + } + catch(IllegalArgumentException e){ + MBassador.this.handlePublicationError( + new PublicationError(e, "Error during messageHandler notification. " + + "Wrong arguments passed to method. Was: " + message.getClass() + + "Expected: " + messageHandler.getParameterTypes()[0], + messageHandler, listener, message)); + } + catch (InvocationTargetException e) { + MBassador.this.handlePublicationError( + new PublicationError(e, "Error during messageHandler notification. " + + "Message handler threw exception", + messageHandler, listener, message)); + } + catch (Throwable e) { + MBassador.this.handlePublicationError( + new PublicationError(e, "Error during messageHandler notification. " + + "Unexpected exception", + messageHandler, listener, message)); + } + } + + public void publish(Object message) { + + Iterator iterator = listeners.iterator(); + Object listener = null; + while ((listener = iterator.next()) != null) { + if(passesFilter(message, listener)) { + dispatch(message, listener); + } + } + } + + private boolean passesFilter(Object message, Object listener) { + + if (filter == null) { + return true; + } + else { + for (int i = 0; i < filter.length; i++) { + if (!filter[i].accepts(message, listener)) return false; + } + return true; + } + } + + public void unsubscribe(Object existingListener) { + listeners.remove(existingListener); + } + } + +} diff --git a/src/main/java/org/mbassy/common/ConcurrentLinkedBag.java b/src/main/java/org/mbassy/common/ConcurrentLinkedBag.java new file mode 100644 index 0000000..1483ca7 --- /dev/null +++ b/src/main/java/org/mbassy/common/ConcurrentLinkedBag.java @@ -0,0 +1,159 @@ +package org.mbassy.common; + + +import java.lang.ref.WeakReference; +import java.util.Iterator; +import java.util.WeakHashMap; + +/** + * This data structure is optimized for non-blocking reads even when write operations occur. + * Running read iterators will not be affected by add operations since writes always insert at the head of the + * structure. Remove operations can affect any running iterator such that a removed element that has not yet + * been reached by the iterator will not appear in that iterator anymore. + * + * The structure uses weak references to the elements. Iterators automatically perform cleanups of + * garbace collect objects during iteration. + * No dedicated maintenance operations need to be called or run in background. + * + * + *

+ * @author bennidi + * Date: 2/12/12 + */ +public class ConcurrentLinkedBag implements Iterable { + + + private WeakHashMap> entries = new WeakHashMap>(); // maintain a map of entries for O(log n) lookup + + private ListEntry head; // reference to the first element + + public ConcurrentLinkedBag add(T element) { + if (element == null || entries.containsKey(element)) return this; + synchronized (this) { + insert(element); + } + return this; + } + + private void insert(T element) { + if(head == null){ + head = new ListEntry(element); + } + else{ + head = new ListEntry(element, head); + } + entries.put(element, head); + } + + public ConcurrentLinkedBag addAll(Iterable elements) { + for (T element : elements) { + if (element == null || entries.containsKey(element)) return this; + synchronized (this) { + insert(element); + } + } + return this; + } + + public ConcurrentLinkedBag remove(T element) { + if (!entries.containsKey(element)) return this; + synchronized (this) { + ListEntry listelement = entries.get(element); + if(listelement != head){ + listelement.remove(); + } + else{ + head = head.next(); + } + entries.remove(element); + } + return this; + } + + public Iterator iterator() { + return new Iterator() { + + private ListEntry current = head; + + public boolean hasNext() { + if(current == null) return false; + T value = current.getValue(); + if(value == null){ // auto-removal of orphan references + remove(); + return hasNext(); + } + else{ + return true; + } + } + + public T next() { + if(current == null) return null; + T value = current.getValue(); + if(value == null){ // auto-removal of orphan references + remove(); + return next(); + } + else{ + current = current.next(); + return value; + } + } + + public void remove() { + if(current == null)return; + synchronized (ConcurrentLinkedBag.this){ + current.remove(); + current = current.next();} + } + }; + } + + + public class ListEntry { + + private WeakReference value; + + private ListEntry next; + + private ListEntry predecessor; + + + private ListEntry(T value) { + this.value = new WeakReference(value); + } + + private ListEntry(T value, ListEntry next) { + this(value); + this.next = next; + next.predecessor = this; + } + + public T getValue() { + return value.get(); + } + + public void remove(){ + if(predecessor != null){ + predecessor.setNext(next()); + } + else if(next() != null){ + next.predecessor = null; + } + } + + public void setNext(ListEntry element) { + this.next = element; + if(element != null)element.predecessor = this; + } + + public ListEntry next() { + return next; + } + + public boolean hasNext() { + return next() != null; + } + + } +} diff --git a/src/main/java/org/mbassy/common/IPredicate.java b/src/main/java/org/mbassy/common/IPredicate.java new file mode 100644 index 0000000..7598643 --- /dev/null +++ b/src/main/java/org/mbassy/common/IPredicate.java @@ -0,0 +1,14 @@ +package org.mbassy.common; + +/** + * Created with IntelliJ IDEA. + * User: benni + * Date: 10/22/12 + * Time: 9:33 AM + * To change this template use File | Settings | File Templates. + */ +public interface IPredicate { + + public boolean apply(T target); + +} diff --git a/src/main/java/org/mbassy/common/IPublicationErrorHandler.java b/src/main/java/org/mbassy/common/IPublicationErrorHandler.java new file mode 100644 index 0000000..1789961 --- /dev/null +++ b/src/main/java/org/mbassy/common/IPublicationErrorHandler.java @@ -0,0 +1,13 @@ +package org.mbassy.common; + +/** + * TODO. Insert class description here + *

+ * User: benni + * Date: 2/22/12 + * Time: 5:03 PM + */ +public interface IPublicationErrorHandler { + + public void handleError(PublicationError error); +} diff --git a/src/main/java/org/mbassy/common/PublicationError.java b/src/main/java/org/mbassy/common/PublicationError.java new file mode 100644 index 0000000..e117571 --- /dev/null +++ b/src/main/java/org/mbassy/common/PublicationError.java @@ -0,0 +1,98 @@ +package org.mbassy.common; + +import java.lang.reflect.Method; + +/** + * Publication errors are created when object publication fails for some reason and contain details + * as to the cause and location where they occured. + *

+ * User: benni + * Date: 2/22/12 + * Time: 4:59 PM + */ +public class PublicationError { + + private Throwable cause; + + private String message; + + private Method listener; + + private Object listeningObject; + + private Object publishedObject; + + + public PublicationError(Throwable cause, String message, Method listener, Object listeningObject, Object publishedObject) { + this.cause = cause; + this.message = message; + this.listener = listener; + this.listeningObject = listeningObject; + this.publishedObject = publishedObject; + } + + public PublicationError(){ + super(); + } + + public Throwable getCause() { + return cause; + } + + public PublicationError setCause(Throwable cause) { + this.cause = cause; + return this; + } + + public String getMessage() { + return message; + } + + public PublicationError setMessage(String message) { + this.message = message; + return this; + } + + public Method getListener() { + return listener; + } + + public PublicationError setListener(Method listener) { + this.listener = listener; + return this; + } + + public Object getListeningObject() { + return listeningObject; + } + + public PublicationError setListeningObject(Object listeningObject) { + this.listeningObject = listeningObject; + return this; + } + + public Object getPublishedObject() { + return publishedObject; + } + + public PublicationError setPublishedObject(Object publishedObject) { + this.publishedObject = publishedObject; + return this; + } + + @Override + public String toString() { + return "PublicationError{" + + "\n" + + "\tcause=" + cause + + "\n" + + "\tmessage='" + message + '\'' + + "\n" + + "\tlistener=" + listener + + "\n" + + "\tlisteningObject=" + listeningObject + + "\n" + + "\tpublishedObject=" + publishedObject + + '}'; + } +} diff --git a/src/main/java/org/mbassy/common/ReflectionUtils.java b/src/main/java/org/mbassy/common/ReflectionUtils.java new file mode 100644 index 0000000..b61c764 --- /dev/null +++ b/src/main/java/org/mbassy/common/ReflectionUtils.java @@ -0,0 +1,70 @@ +package org.mbassy.common; + +import com.google.common.base.Predicate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.LinkedList; +import java.util.List; + +/** + * User: benni + * Date: 2/16/12 + * Time: 12:14 PM + */ +public class ReflectionUtils { + + private static final Logger logger = LoggerFactory.getLogger(ReflectionUtils.class); + + public static List getMethods(IPredicate condition, Class target) { + List methods = new LinkedList(); + try { + for (Method method : target.getDeclaredMethods()) { + if (condition.apply(method)) { + methods.add(method); + } + } + } catch (Exception e) { + //nop + } + if (!target.equals(Object.class)) { + methods.addAll(getMethods(condition, target.getSuperclass())); + } + return methods; + } + + public static List getFields(Predicate condition, Class target) { + List methods = new LinkedList(); + try { + for (Field method : target.getDeclaredFields()) { + if (condition.apply(method)) { + methods.add(method); + } + } + } catch (Exception e) { + //nop + } + if (!target.equals(Object.class)) { + methods.addAll(getFields(condition, target.getSuperclass())); + } + return methods; + } + + public static Object callMethod(Object o, final String methodName, Object... args) { + + if(o == null || methodName == null) { + return null; + } + + Object res = null; + try { + Method m = o.getClass().getMethod(methodName); + res = m.invoke(o, args); + } catch (Exception e) { + //logger.warn("Not possible to get value", e); + } + return res; + } +} diff --git a/src/main/java/org/mbassy/filter/Filter.java b/src/main/java/org/mbassy/filter/Filter.java new file mode 100644 index 0000000..6da4e42 --- /dev/null +++ b/src/main/java/org/mbassy/filter/Filter.java @@ -0,0 +1,23 @@ +package org.mbassy.filter; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * THe filter annotation is used to add filters to message listeners. + * It references a class that implements the MessageFilter interface. + * The object filter will be used to check whether a message should be delivered + * to the message listener or not. + * + *

+ * @author benni + * Date: 2/14/12 + */ +@Retention(value = RetentionPolicy.RUNTIME) +@Target(value = {ElementType.ANNOTATION_TYPE}) +public @interface Filter { + + Class value(); +} diff --git a/src/main/java/org/mbassy/filter/MessageFilter.java b/src/main/java/org/mbassy/filter/MessageFilter.java new file mode 100644 index 0000000..0169ccf --- /dev/null +++ b/src/main/java/org/mbassy/filter/MessageFilter.java @@ -0,0 +1,31 @@ +package org.mbassy.filter; + +/** + * Object filters can be used to prevent certain messages to be delivered to a specific listener. + * If a filter is used the message will only be delivered if it passes the filter(s) + * + * User: benni + * Date: 2/8/12 + */ +public interface MessageFilter { + + /** + * Evaluate the message and listener to ensure that the message should be handled by the listener + * + * + * @param event the event to be delivered + * @param listener the listener instance that would receive the event if it passes the filter + * @return + */ + public boolean accepts(Object event, Object listener); + + + public static final class All implements MessageFilter { + + @Override + public boolean accepts(Object event, Object listener) { + return true; + } + } + +} diff --git a/src/test/java/org/mbassy/ConcurrentExecutor.java b/src/test/java/org/mbassy/ConcurrentExecutor.java new file mode 100644 index 0000000..1bf34f3 --- /dev/null +++ b/src/test/java/org/mbassy/ConcurrentExecutor.java @@ -0,0 +1,68 @@ +package org.mbassy; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.*; + +/** + * Run various tests concurrently. A given instance of runnable will be used to spawn and start + * as many threads as specified by an additional parameter or (if multiple runnables have been + * passed to the method) one thread for each runnable. + *

+ * Date: 2/14/12 + * + * @Author bennidi + */ +public class ConcurrentExecutor { + + + public static void runConcurrent(final Runnable unit, int numberOfConcurrentExecutions) { + Runnable[] units = new Runnable[numberOfConcurrentExecutions]; + // create the tasks and schedule for execution + for (int i = 0; i < numberOfConcurrentExecutions; i++) { + units[i] = unit; + } + runConcurrent(units); + + } + + public static void runConcurrent(final Runnable... units) { + ExecutorService executor = Executors.newCachedThreadPool(); + List> returnValues = new ArrayList>(); + + // create the tasks and schedule for execution + for (final Runnable unit : units) { + Callable wrapper = new Callable() { + @Override + public Long call() throws Exception { + long start = System.currentTimeMillis(); + unit.run(); + return System.currentTimeMillis() - start; + } + }; + returnValues.add(executor.submit(wrapper)); + } + + // wait until all tasks have been executed + try { + executor.shutdown();// tells the thread pool to execute all waiting tasks + executor.awaitTermination(5, TimeUnit.MINUTES); + } catch (InterruptedException e) { + // unlikely that this will happen + e.printStackTrace(); + } + + // print results + for (Future result : returnValues) + try { + System.out.println("Execution of unit of work to " + result.get() + "ms."); + } catch (Exception e) { + //should never happen + // since the code waits until all tasks are processed + e.printStackTrace(); + } + + } + + +} diff --git a/src/test/java/org/mbassy/MBassadorTest.java b/src/test/java/org/mbassy/MBassadorTest.java new file mode 100644 index 0000000..6103e41 --- /dev/null +++ b/src/test/java/org/mbassy/MBassadorTest.java @@ -0,0 +1,167 @@ +package org.mbassy; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Test synchronous and asynchronous dispatch in single and multi-threaded scenario. + * + * @author bennidi + * Date: 2/8/12 + */ +public class MBassadorTest { + + + @Test + public void testAsynchronous() throws InterruptedException { + + MBassador bus = new MBassador(); + int listenerCount = 1000; + List persistentReferences = new ArrayList(); + + for (int i = 1; i <= listenerCount; i++) { + EventingTestBean bean = new EventingTestBean(); + persistentReferences.add(bean); + bus.subscribe(bean); + } + + TestEvent event = new TestEvent(); + TestEvent subEvent = new SubTestEvent(); + + bus.publishAsync(event); + bus.publishAsync(subEvent); + + Thread.sleep(2000); + + Assert.assertTrue(event.counter.get() == 1000); + Assert.assertTrue(subEvent.counter.get() == 1000 * 2); + + } + + @Test + public void testSynchronous() throws InterruptedException { + + MBassador bus = new MBassador(); + int listenerCount = 100; + List persistentReferences = new ArrayList(); + for (int i = 1; i <= listenerCount; i++) { + + + EventingTestBean bean = new EventingTestBean(); + persistentReferences.add(bean); + bus.subscribe(bean); + + TestEvent event = new TestEvent(); + TestEvent subEvent = new SubTestEvent(); + + bus.publish(event); + bus.publish(subEvent); + + Assert.assertEquals(i, event.counter.get()); + + try { + Thread.sleep(10); + } catch (InterruptedException e) { + e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates. + } + + Assert.assertEquals(i * 2, subEvent.counter.get()); + + } + + } + + @Test + public void testConcurrentPublication() { + final MBassador bus = new MBassador(); + final int listenerCount = 100; + final int concurenny = 20; + final CopyOnWriteArrayList testEvents = new CopyOnWriteArrayList(); + final CopyOnWriteArrayList subtestEvents = new CopyOnWriteArrayList(); + final CopyOnWriteArrayList persistentReferences = new CopyOnWriteArrayList(); + + ConcurrentExecutor.runConcurrent(new Runnable() { + @Override + public void run() { + long start = System.currentTimeMillis(); + for (int i = 0; i < listenerCount; i++) { + EventingTestBean bean = new EventingTestBean(); + persistentReferences.add(bean); + bus.subscribe(bean); + } + + long end = System.currentTimeMillis(); + System.out.println("MBassador: Creating " + listenerCount + " listeners took " + (end - start) + " ms"); + } + }, concurenny); + + ConcurrentExecutor.runConcurrent(new Runnable() { + @Override + public void run() { + long start = System.currentTimeMillis(); + for (int i = 0; i < listenerCount; i++) { + TestEvent event = new TestEvent(); + SubTestEvent subEvent = new SubTestEvent(); + testEvents.add(event); + subtestEvents.add(subEvent); + + bus.publishAsync(event); + bus.publish(subEvent); + } + + long end = System.currentTimeMillis(); + System.out.println("MBassador: Publishing " + 2 * listenerCount + " events took " + (end - start) + " ms"); + } + }, concurenny); + + try { + Thread.sleep(3000); + } catch (InterruptedException e) { + e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates. + } + + for(TestEvent event : testEvents){ + Assert.assertEquals(listenerCount * concurenny, event.counter.get()); + } + + for(SubTestEvent event : subtestEvents){ + Assert.assertEquals(listenerCount * concurenny * 2, event.counter.get()); + } + + } + + + public static class TestEvent { + + public AtomicInteger counter = new AtomicInteger(); + + } + + public static class SubTestEvent extends TestEvent { + + } + + + public class EventingTestBean { + + + @Listener + public void handleTestEvent(TestEvent event) { + event.counter.incrementAndGet(); + } + + @Listener(mode = Listener.Dispatch.Asynchronous) + public void handleSubTestEvent(SubTestEvent event) { + event.counter.incrementAndGet(); + } + + + } + + +} diff --git a/target/classes/org/mbassy/Listener$Dispatch.class b/target/classes/org/mbassy/Listener$Dispatch.class new file mode 100644 index 0000000..85ee3e7 Binary files /dev/null and b/target/classes/org/mbassy/Listener$Dispatch.class differ diff --git a/target/classes/org/mbassy/Listener.class b/target/classes/org/mbassy/Listener.class new file mode 100644 index 0000000..055de69 Binary files /dev/null and b/target/classes/org/mbassy/Listener.class differ diff --git a/target/classes/org/mbassy/MBassador$1.class b/target/classes/org/mbassy/MBassador$1.class new file mode 100644 index 0000000..87921e3 Binary files /dev/null and b/target/classes/org/mbassy/MBassador$1.class differ diff --git a/target/classes/org/mbassy/MBassador$2.class b/target/classes/org/mbassy/MBassador$2.class new file mode 100644 index 0000000..afaed7b Binary files /dev/null and b/target/classes/org/mbassy/MBassador$2.class differ diff --git a/target/classes/org/mbassy/MBassador$ConsoleLogger.class b/target/classes/org/mbassy/MBassador$ConsoleLogger.class new file mode 100644 index 0000000..edfa58a Binary files /dev/null and b/target/classes/org/mbassy/MBassador$ConsoleLogger.class differ diff --git a/target/classes/org/mbassy/MBassador$Subscription$1.class b/target/classes/org/mbassy/MBassador$Subscription$1.class new file mode 100644 index 0000000..966c5c6 Binary files /dev/null and b/target/classes/org/mbassy/MBassador$Subscription$1.class differ diff --git a/target/classes/org/mbassy/MBassador$Subscription.class b/target/classes/org/mbassy/MBassador$Subscription.class new file mode 100644 index 0000000..02d43e4 Binary files /dev/null and b/target/classes/org/mbassy/MBassador$Subscription.class differ diff --git a/target/classes/org/mbassy/MBassador.class b/target/classes/org/mbassy/MBassador.class new file mode 100644 index 0000000..dce8a7d Binary files /dev/null and b/target/classes/org/mbassy/MBassador.class differ diff --git a/target/classes/org/mbassy/common/ConcurrentLinkedBag$1.class b/target/classes/org/mbassy/common/ConcurrentLinkedBag$1.class new file mode 100644 index 0000000..5474699 Binary files /dev/null and b/target/classes/org/mbassy/common/ConcurrentLinkedBag$1.class differ diff --git a/target/classes/org/mbassy/common/ConcurrentLinkedBag$ListEntry.class b/target/classes/org/mbassy/common/ConcurrentLinkedBag$ListEntry.class new file mode 100644 index 0000000..70f231f Binary files /dev/null and b/target/classes/org/mbassy/common/ConcurrentLinkedBag$ListEntry.class differ diff --git a/target/classes/org/mbassy/common/ConcurrentLinkedBag.class b/target/classes/org/mbassy/common/ConcurrentLinkedBag.class new file mode 100644 index 0000000..d692cd9 Binary files /dev/null and b/target/classes/org/mbassy/common/ConcurrentLinkedBag.class differ diff --git a/target/classes/org/mbassy/common/IPredicate.class b/target/classes/org/mbassy/common/IPredicate.class new file mode 100644 index 0000000..ee24e04 Binary files /dev/null and b/target/classes/org/mbassy/common/IPredicate.class differ diff --git a/target/classes/org/mbassy/common/IPublicationErrorHandler.class b/target/classes/org/mbassy/common/IPublicationErrorHandler.class new file mode 100644 index 0000000..51ad920 Binary files /dev/null and b/target/classes/org/mbassy/common/IPublicationErrorHandler.class differ diff --git a/target/classes/org/mbassy/common/PublicationError.class b/target/classes/org/mbassy/common/PublicationError.class new file mode 100644 index 0000000..826fda4 Binary files /dev/null and b/target/classes/org/mbassy/common/PublicationError.class differ diff --git a/target/classes/org/mbassy/common/ReflectionUtils.class b/target/classes/org/mbassy/common/ReflectionUtils.class new file mode 100644 index 0000000..c1b4f82 Binary files /dev/null and b/target/classes/org/mbassy/common/ReflectionUtils.class differ diff --git a/target/classes/org/mbassy/filter/Filter.class b/target/classes/org/mbassy/filter/Filter.class new file mode 100644 index 0000000..3a1be1f Binary files /dev/null and b/target/classes/org/mbassy/filter/Filter.class differ diff --git a/target/classes/org/mbassy/filter/MessageFilter$All.class b/target/classes/org/mbassy/filter/MessageFilter$All.class new file mode 100644 index 0000000..bf1b2d8 Binary files /dev/null and b/target/classes/org/mbassy/filter/MessageFilter$All.class differ diff --git a/target/classes/org/mbassy/filter/MessageFilter.class b/target/classes/org/mbassy/filter/MessageFilter.class new file mode 100644 index 0000000..42cd6e3 Binary files /dev/null and b/target/classes/org/mbassy/filter/MessageFilter.class differ diff --git a/target/test-classes/org/mbassy/ConcurrentExecutor$1.class b/target/test-classes/org/mbassy/ConcurrentExecutor$1.class new file mode 100644 index 0000000..4c36aae Binary files /dev/null and b/target/test-classes/org/mbassy/ConcurrentExecutor$1.class differ diff --git a/target/test-classes/org/mbassy/ConcurrentExecutor.class b/target/test-classes/org/mbassy/ConcurrentExecutor.class new file mode 100644 index 0000000..9b28e0c Binary files /dev/null and b/target/test-classes/org/mbassy/ConcurrentExecutor.class differ diff --git a/target/test-classes/org/mbassy/MBassadorTest$1.class b/target/test-classes/org/mbassy/MBassadorTest$1.class new file mode 100644 index 0000000..8edf17c Binary files /dev/null and b/target/test-classes/org/mbassy/MBassadorTest$1.class differ diff --git a/target/test-classes/org/mbassy/MBassadorTest$2.class b/target/test-classes/org/mbassy/MBassadorTest$2.class new file mode 100644 index 0000000..2dc027d Binary files /dev/null and b/target/test-classes/org/mbassy/MBassadorTest$2.class differ diff --git a/target/test-classes/org/mbassy/MBassadorTest$EventingTestBean.class b/target/test-classes/org/mbassy/MBassadorTest$EventingTestBean.class new file mode 100644 index 0000000..a2ad50f Binary files /dev/null and b/target/test-classes/org/mbassy/MBassadorTest$EventingTestBean.class differ diff --git a/target/test-classes/org/mbassy/MBassadorTest$SubTestEvent.class b/target/test-classes/org/mbassy/MBassadorTest$SubTestEvent.class new file mode 100644 index 0000000..d813302 Binary files /dev/null and b/target/test-classes/org/mbassy/MBassadorTest$SubTestEvent.class differ diff --git a/target/test-classes/org/mbassy/MBassadorTest$TestEvent.class b/target/test-classes/org/mbassy/MBassadorTest$TestEvent.class new file mode 100644 index 0000000..c4b3f66 Binary files /dev/null and b/target/test-classes/org/mbassy/MBassadorTest$TestEvent.class differ diff --git a/target/test-classes/org/mbassy/MBassadorTest.class b/target/test-classes/org/mbassy/MBassadorTest.class new file mode 100644 index 0000000..ca58d08 Binary files /dev/null and b/target/test-classes/org/mbassy/MBassadorTest.class differ