diff --git a/pom.xml b/pom.xml index 7c20883..dbf4875 100644 --- a/pom.xml +++ b/pom.xml @@ -1,261 +1,278 @@ - - - - org.sonatype.oss - oss-parent - 7 - - - 4.0.0 - net.engio - mbassador - 1.1.11-SNAPSHOT - bundle - mbassador - - Mbassador is a fast and flexible message bus system following the publish subscribe pattern. - It is designed for ease of use and aims to be feature rich and extensible - while preserving resource efficiency and performance. - - It features: - declarative handler definition via annotations, - sync and/or async message delivery, - weak-references, - message filtering, - ordering of message handlers etc. - - - https://github.com/bennidi/mbassador - - - MIT license - http://www.opensource.org/licenses/mit-license.php - - - - git@github.com:bennidi/mbassador.git - scm:git:git@github.com:bennidi/mbassador.git - mbassador-1.1.4 - scm:git:git@github.com:bennidi/mbassador.git - - - - - bennidi - Benjamin Diedrichsen - +1 - b.diedrichsen@googlemail.com - - - - - 2.0.1 - 1.6 - 3.0.1 - - UTF-8 - 1.6 - file://${project.basedir}/mvn-local-repo - - - - - - junit - junit - 4.10 - test - - - - org.slf4j - slf4j-api - 1.7.5 - test - - - - org.slf4j - slf4j-log4j12 - 1.7.5 - test - - - - - - - - - - - - org.apache.felix - maven-bundle-plugin - 2.3.7 - true - - - ${project.groupId}.${project.artifactId} - {local-packages} - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - ${project.build.java.version} - ${project.build.java.version} - - - - - org.apache.maven.plugins - maven-release-plugin - 2.4 - - forked-path - - - - - org.apache.maven.plugins - maven-surefire-plugin - - false - - - AllTests.java - - - - - - - org.apache.maven.plugins - maven-source-plugin - - - attach-sources - - jar - - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - - - attach-javadocs - - jar - - - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 2.9.1 - - true - public - true -
mbassador, ${project.version}
-
mbassador, ${project.version}
- mbassador, ${project.version} -
-
- - org.apache.maven.plugins - maven-scm-publish-plugin - 1.0-beta-2 - - ${project.build.directory}/scmpublish - Publishing javadoc for ${project.artifactId}:${project.version} - ${project.reporting.outputDirectory}/apidocs - true - scm:git:git@github.com:bennidi/mbassador.git - gh-pages - - -
-
- - - - release-sign-artifacts - - - performRelease - true - - - - - - org.apache.maven.plugins - maven-gpg-plugin - 1.4 - - - sign-artifacts - verify - - sign - - - - - - - - -
+ + + + org.sonatype.oss + oss-parent + 7 + + + 4.0.0 + net.engio + mbassador + 1.1.11-SNAPSHOT + bundle + mbassador + + Mbassador is a fast and flexible message bus system following the publish subscribe pattern. + It is designed for ease of use and aims to be feature rich and extensible + while preserving resource efficiency and performance. + + It features: + declarative handler definition via annotations, + sync and/or async message delivery, + weak-references, + message filtering, + ordering of message handlers etc. + + + https://github.com/bennidi/mbassador + + + MIT license + http://www.opensource.org/licenses/mit-license.php + + + + git@github.com:bennidi/mbassador.git + scm:git:git@github.com:bennidi/mbassador.git + mbassador-1.1.4 + scm:git:git@github.com:bennidi/mbassador.git + + + + + bennidi + Benjamin Diedrichsen + +1 + b.diedrichsen@googlemail.com + + + + + 2.0.1 + 1.6 + 3.0.1 + + UTF-8 + 1.6 + file://${project.basedir}/mvn-local-repo + + + + + + junit + junit + 4.10 + test + + + + org.slf4j + slf4j-api + 1.7.5 + test + + + + org.slf4j + slf4j-log4j12 + 1.7.5 + test + + + + javax.el + el-api + 2.2 + + + de.odysseus.juel + juel-impl + 2.2.7 + runtime + + + de.odysseus.juel + juel-spi + 2.2.7 + runtime + + + + + + + + + + + org.apache.felix + maven-bundle-plugin + 2.3.7 + true + + + ${project.groupId}.${project.artifactId} + {local-packages} + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${project.build.java.version} + ${project.build.java.version} + + + + + org.apache.maven.plugins + maven-release-plugin + 2.4 + + forked-path + + + + + org.apache.maven.plugins + maven-surefire-plugin + + false + + + AllTests.java + + + + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + attach-javadocs + + jar + + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.9.1 + + true + public + true +
mbassador, ${project.version}
+
mbassador, ${project.version}
+ mbassador, ${project.version} +
+
+ + org.apache.maven.plugins + maven-scm-publish-plugin + 1.0-beta-2 + + ${project.build.directory}/scmpublish + Publishing javadoc for ${project.artifactId}:${project.version} + ${project.reporting.outputDirectory}/apidocs + true + scm:git:git@github.com:bennidi/mbassador.git + gh-pages + + +
+
+ + + + release-sign-artifacts + + + performRelease + true + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.4 + + + sign-artifacts + verify + + sign + + + + + + + + +
diff --git a/src/main/java/net/engio/mbassy/dispatch/FilteredMessageDispatcher.java b/src/main/java/net/engio/mbassy/dispatch/FilteredMessageDispatcher.java index 0e4bcc7..89efa9c 100644 --- a/src/main/java/net/engio/mbassy/dispatch/FilteredMessageDispatcher.java +++ b/src/main/java/net/engio/mbassy/dispatch/FilteredMessageDispatcher.java @@ -1,45 +1,57 @@ -package net.engio.mbassy.dispatch; - -import net.engio.mbassy.bus.MessagePublication; -import net.engio.mbassy.listener.IMessageFilter; - -/** - * A dispatcher that implements message filtering based on the filter configuration - * of the associated message handler. It will delegate message delivery to another - * message dispatcher after having performed the filtering logic. - * - * @author bennidi - * Date: 11/23/12 - */ -public class FilteredMessageDispatcher extends DelegatingMessageDispatcher { - - private final IMessageFilter[] filter; - - public FilteredMessageDispatcher(IMessageDispatcher dispatcher) { - super(dispatcher); - this.filter = dispatcher.getContext().getHandlerMetadata().getFilter(); - } - - private boolean passesFilter(Object message) { - - if (filter == null) { - return true; - } else { - for (IMessageFilter aFilter : filter) { - if (!aFilter.accepts(message, getContext().getHandlerMetadata())) { - return false; - } - } - return true; - } - } - - - @Override - public void dispatch(MessagePublication publication, Object message, Iterable listeners){ - if (passesFilter(message)) { - getDelegate().dispatch(publication, message, listeners); - } - } - -} +package net.engio.mbassy.dispatch; + +import net.engio.mbassy.bus.MessagePublication; +import net.engio.mbassy.dispatch.el.ElFilter; +import net.engio.mbassy.listener.IMessageFilter; + +/** + * A dispatcher that implements message filtering based on the filter configuration + * of the associated message handler. It will delegate message delivery to another + * message dispatcher after having performed the filtering logic. + * + * @author bennidi + * Date: 11/23/12 + */ +public class FilteredMessageDispatcher extends DelegatingMessageDispatcher { + + private final IMessageFilter[] filter; + + public FilteredMessageDispatcher(IMessageDispatcher dispatcher) { + super(dispatcher); + this.filter = dispatcher.getContext().getHandlerMetadata().getFilter(); + } + + private boolean passesFilter(Object message) { + + if (filter == null) { + return true; + } else { + for (IMessageFilter aFilter : filter) { + if (!aFilter.accepts(message, getContext().getHandlerMetadata())) { + return false; + } + } + return true; + } + } + + + @Override + public void dispatch(MessagePublication publication, Object message, Iterable listeners){ + if (passesFilter(message) && passesELFilter(message)) { + getDelegate().dispatch(publication, message, listeners); + } + } + + /************************************************************************* + * This will test the EL expression defined on the Handler annotation. + * This is like a "parameterizable" filter. + * @param me the message object to filter with the EL expression if there is one. + * @return true if the event is allowed, false if it is rejected. + ************************************************************************/ + + private boolean passesELFilter(Object message) { + ElFilter filter = ElFilter.getInstance(); + return filter != null && filter.accepts(message, getContext().getHandlerMetadata()); + } +} diff --git a/src/main/java/net/engio/mbassy/dispatch/el/ElFilter.java b/src/main/java/net/engio/mbassy/dispatch/el/ElFilter.java new file mode 100644 index 0000000..14f9163 --- /dev/null +++ b/src/main/java/net/engio/mbassy/dispatch/el/ElFilter.java @@ -0,0 +1,113 @@ +package net.engio.mbassy.dispatch.el; + +import javax.el.ExpressionFactory; +import javax.el.ValueExpression; + +import net.engio.mbassy.listener.IMessageFilter; +import net.engio.mbassy.listener.MessageHandler; + +/***************************************************************************** + * A filter that will use a expression from the handler annotation and + * parse it as EL. + ****************************************************************************/ + +public class ElFilter implements IMessageFilter { + + private static ElFilter instance; + + static { + try { + instance = new ElFilter(); + } catch (Exception e) { + // Most likely the javax.el package is not available. + instance = null; + } + } + + private ExpressionFactory elFactory; + + /************************************************************************* + * Constructor + ************************************************************************/ + + private ElFilter() { + super(); + initELFactory(); + } + + /************************************************************************* + * Get an implementation of the ExpressionFactory. This uses the + * Java service lookup mechanism to find a proper implementation. + * If none if available we do not support EL filters. + ************************************************************************/ + + private void initELFactory() { + try { + this.elFactory = ExpressionFactory.newInstance(); + } catch (RuntimeException e) { + // No EL implementation on the class path. + elFactory = null; + } + } + + /************************************************************************* + * accepts + * @see net.engio.mbassy.listener.IMessageFilter#accepts(java.lang.Object, net.engio.mbassy.listener.MessageHandler) + ************************************************************************/ + @Override + public boolean accepts(Object message, MessageHandler metadata) { + String expression = metadata.getCondition(); + if (expression == null || expression.trim().length() == 0) { + return true; + } + if (elFactory == null) { + // TODO should we test this some where earlier? Perhaps in MessageHandler.validate() ? + throw new IllegalStateException("A handler uses an EL filter but no EL implementation is available."); + } + + expression = cleanupExpression(expression); + + EventContext context = new EventContext(); + context.bindToEvent(message); + + return evalExpression(expression, context); + } + + /************************************************************************* + * @param expression + * @param context + * @return + ************************************************************************/ + + private boolean evalExpression(String expression, EventContext context) { + ValueExpression ve = elFactory.createValueExpression(context, expression, Boolean.class); + Object result = ve.getValue(context); + if (!(result instanceof Boolean)) { + throw new IllegalStateException("A handler uses an EL filter but the output is not \"true\" or \"false\"."); + } + return (Boolean)result; + } + + /************************************************************************* + * Make it a valid expression because the parser expects it like this. + * @param expression + * @return + ************************************************************************/ + + private String cleanupExpression(String expression) { + + if (!expression.trim().startsWith("${") && !expression.trim().startsWith("#{")) { + expression = "${"+expression+"}"; + } + return expression; + } + + /************************************************************************* + * @return the one and only + ************************************************************************/ + + public static synchronized ElFilter getInstance() { + return instance; + } + +} diff --git a/src/main/java/net/engio/mbassy/dispatch/el/EventContext.java b/src/main/java/net/engio/mbassy/dispatch/el/EventContext.java new file mode 100644 index 0000000..41adb2c --- /dev/null +++ b/src/main/java/net/engio/mbassy/dispatch/el/EventContext.java @@ -0,0 +1,102 @@ +package net.engio.mbassy.dispatch.el; + +import java.lang.reflect.Method; + +import javax.el.BeanELResolver; +import javax.el.CompositeELResolver; +import javax.el.ELContext; +import javax.el.ELResolver; +import javax.el.FunctionMapper; +import javax.el.ValueExpression; +import javax.el.VariableMapper; + +/***************************************************************************** + * An EL context that knows how to resolve everything from a + * given message but event. + ****************************************************************************/ + +public class EventContext extends ELContext { + + private final CompositeELResolver resolver; + private final FunctionMapper functionMapper; + private final VariableMapper variableMapper; + private RootResolver rootResolver; + + /************************************************************************* + * Constructor + * + * @param me + ************************************************************************/ + + public EventContext() { + super(); + this.functionMapper = new NoopFunctionMapper(); + this.variableMapper = new NoopMapperImpl(); + + this.resolver = new CompositeELResolver(); + this.rootResolver = new RootResolver(); + this.resolver.add(rootResolver); + this.resolver.add(new BeanELResolver(true)); + } + + /************************************************************************* + * Binds an event object with the EL expression. This will allow access + * to all properties of a given event. + * @param event to bind. + ************************************************************************/ + + public void bindToEvent(Object event) { + this.rootResolver.setRoot(event); + } + + /************************************************************************* + * The resolver for the event object. + * @see javax.el.ELContext#getELResolver() + ************************************************************************/ + @Override + public ELResolver getELResolver() { + return this.resolver; + } + + /************************************************************************* + * @see javax.el.ELContext#getFunctionMapper() + ************************************************************************/ + @Override + public FunctionMapper getFunctionMapper() { + return this.functionMapper; + } + + /************************************************************************* + * @see javax.el.ELContext#getVariableMapper() + ************************************************************************/ + @Override + public VariableMapper getVariableMapper() { + return this.variableMapper; + } + + /***************************************************************************** + * Dummy implementation. + ****************************************************************************/ + + private class NoopMapperImpl extends VariableMapper { + public ValueExpression resolveVariable(String s) { + return null; + } + + public ValueExpression setVariable(String s, + ValueExpression valueExpression) { + return null; + } + } + + /***************************************************************************** + * Dummy implementation. + ****************************************************************************/ + + private class NoopFunctionMapper extends FunctionMapper { + public Method resolveFunction(String s, String s1) { + return null; + } + } + +} diff --git a/src/main/java/net/engio/mbassy/dispatch/el/RootResolver.java b/src/main/java/net/engio/mbassy/dispatch/el/RootResolver.java new file mode 100644 index 0000000..c9d6fdb --- /dev/null +++ b/src/main/java/net/engio/mbassy/dispatch/el/RootResolver.java @@ -0,0 +1,89 @@ +package net.engio.mbassy.dispatch.el; + +import java.beans.FeatureDescriptor; +import java.util.Iterator; + +import javax.el.ELContext; +import javax.el.ELResolver; + +/***************************************************************************** + * A resolver that will resolve the "msg" variable to the event object that + * is posted. + ****************************************************************************/ + +public class RootResolver extends ELResolver { + + private static final String ROOT_VAR_NAME = "msg"; + public Object rootObject; + + /************************************************************************* + * @param rootObject + ************************************************************************/ + + public void setRoot(Object rootObject) { + this.rootObject = rootObject; + } + + /************************************************************************* + * getValue + * @see javax.el.ELResolver#getValue(javax.el.ELContext, java.lang.Object, java.lang.Object) + ************************************************************************/ + @Override + public Object getValue(ELContext context, Object base, Object property) { + if (context == null) { + throw new NullPointerException(); + } + if (base == null && ROOT_VAR_NAME.equals(property)) { + context.setPropertyResolved(true); + return this.rootObject; + } + return null; + } + + + /************************************************************************* + * getCommonPropertyType + * @see javax.el.ELResolver#getCommonPropertyType(javax.el.ELContext, java.lang.Object) + ************************************************************************/ + @Override + public Class getCommonPropertyType(ELContext context, Object base) { + return String.class; + } + + /************************************************************************* + * getFeatureDescriptors + * @see javax.el.ELResolver#getFeatureDescriptors(javax.el.ELContext, java.lang.Object) + ************************************************************************/ + @Override + public Iterator getFeatureDescriptors(ELContext context, Object base) { + return null; + } + + /************************************************************************* + * getType + * @see javax.el.ELResolver#getType(javax.el.ELContext, java.lang.Object, java.lang.Object) + ************************************************************************/ + @Override + public Class getType(ELContext context, Object base, Object property) { + return null; + } + + /************************************************************************* + * isReadOnly + * @see javax.el.ELResolver#isReadOnly(javax.el.ELContext, java.lang.Object, java.lang.Object) + ************************************************************************/ + @Override + public boolean isReadOnly(ELContext context, Object base, Object property) { + return true; + } + + /************************************************************************* + * setValue + * @see javax.el.ELResolver#setValue(javax.el.ELContext, java.lang.Object, java.lang.Object, java.lang.Object) + ************************************************************************/ + @Override + public void setValue(ELContext context, Object base, Object property, Object value) { + // Do nothing + } + +} diff --git a/src/main/java/net/engio/mbassy/listener/Handler.java b/src/main/java/net/engio/mbassy/listener/Handler.java index bfceecd..785e33c 100644 --- a/src/main/java/net/engio/mbassy/listener/Handler.java +++ b/src/main/java/net/engio/mbassy/listener/Handler.java @@ -1,66 +1,76 @@ -package net.engio.mbassy.listener; - -import net.engio.mbassy.dispatch.HandlerInvocation; -import net.engio.mbassy.dispatch.ReflectiveHandlerInvocation; - -import java.lang.annotation.*; - -/** - * Mark any method of any class(=listener) as a message handler and configure the handler - * using different properties. - * - * @author bennidi - * Date: 2/8/12 - */ -@Retention(value = RetentionPolicy.RUNTIME) -@Inherited -@Target(value = {ElementType.METHOD}) -public @interface Handler { - - /** - * Add any numbers of filters to the handler. All filters are evaluated before the handler - * is actually invoked, which is only if all the filters accept the message. - */ - Filter[] filters() default {}; - - /** - * Define the mode in which a message is delivered to each listener. Listeners can be notified - * sequentially or concurrently. - */ - Invoke delivery() default Invoke.Synchronously; - - /** - * Handlers are ordered by priority and handlers with higher priority are processed before - * those with lower priority, i.e. Influence the order in which different handlers that consume - * the same message type are invoked. - */ - int priority() default 0; - - /** - * Define whether or not the handler accepts sub types of the message type it declares in its - * signature. - */ - boolean rejectSubtypes() default false; - - - /** - * Enable or disable the handler. Disabled handlers do not receive any messages. - * This property is useful for quick changes in configuration and necessary to disable - * handlers that have been declared by a superclass but do not apply to the subclass - */ - boolean enabled() default true; - - - /** - * Each handler call is implemented as an invocation object that implements the invocation mechanism. - * The basic implementation uses reflection and is the default. It is possible though to provide a custom - * invocation to add additional logic. - * - * Note: Providing a custom invocation will most likely reduce performance, since the JIT-Compiler - * can not do some of its sophisticated byte code optimizations. - * - */ - Class invocation() default ReflectiveHandlerInvocation.class; - - -} +package net.engio.mbassy.listener; + +import net.engio.mbassy.dispatch.HandlerInvocation; +import net.engio.mbassy.dispatch.ReflectiveHandlerInvocation; + +import java.lang.annotation.*; + +/** + * Mark any method of any class(=listener) as a message handler and configure the handler + * using different properties. + * + * @author bennidi + * Date: 2/8/12 + */ +@Retention(value = RetentionPolicy.RUNTIME) +@Inherited +@Target(value = {ElementType.METHOD}) +public @interface Handler { + + /** + * Add any numbers of filters to the handler. All filters are evaluated before the handler + * is actually invoked, which is only if all the filters accept the message. + */ + Filter[] filters() default {}; + + + /** + * Defines a filter condition as Expression Language. This can be used to filter the events based on + * attributes of the event object. Note that the expression must resolve to either + * true to allow the event or false to block it from delivery to the handler. + * The message itself is available as "msg" variable. + * @return the condition in EL syntax. + */ + String condition() default ""; + + /** + * Define the mode in which a message is delivered to each listener. Listeners can be notified + * sequentially or concurrently. + */ + Invoke delivery() default Invoke.Synchronously; + + /** + * Handlers are ordered by priority and handlers with higher priority are processed before + * those with lower priority, i.e. Influence the order in which different handlers that consume + * the same message type are invoked. + */ + int priority() default 0; + + /** + * Define whether or not the handler accepts sub types of the message type it declares in its + * signature. + */ + boolean rejectSubtypes() default false; + + + /** + * Enable or disable the handler. Disabled handlers do not receive any messages. + * This property is useful for quick changes in configuration and necessary to disable + * handlers that have been declared by a superclass but do not apply to the subclass + */ + boolean enabled() default true; + + + /** + * Each handler call is implemented as an invocation object that implements the invocation mechanism. + * The basic implementation uses reflection and is the default. It is possible though to provide a custom + * invocation to add additional logic. + * + * Note: Providing a custom invocation will most likely reduce performance, since the JIT-Compiler + * can not do some of its sophisticated byte code optimizations. + * + */ + Class invocation() default ReflectiveHandlerInvocation.class; + + +} diff --git a/src/main/java/net/engio/mbassy/listener/MessageHandler.java b/src/main/java/net/engio/mbassy/listener/MessageHandler.java index 4ce8b39..4008273 100644 --- a/src/main/java/net/engio/mbassy/listener/MessageHandler.java +++ b/src/main/java/net/engio/mbassy/listener/MessageHandler.java @@ -1,183 +1,194 @@ -package net.engio.mbassy.listener; - -import net.engio.mbassy.dispatch.HandlerInvocation; - -import java.lang.reflect.Method; -import java.util.HashMap; -import java.util.Map; - -/** - * Any method in any class annotated with the @Handler annotation represents a message handler. The class that contains - * the handler is called a message listener and more generally, any class containing a message handler in its class hierarchy - * defines such a message listener. - * - * @author bennidi - * Date: 11/14/12 - */ -public class MessageHandler { - - public static final class Properties{ - - public static final String HandlerMethod = "handler"; - public static final String InvocationMode = "invocationMode"; - public static final String Filter = "filter"; - public static final String Enveloped = "envelope"; - public static final String HandledMessages = "messages"; - public static final String IsSynchronized = "synchronized"; - public static final String Listener = "listener"; - public static final String AcceptSubtypes = "subtypes"; - public static final String Priority = "priority"; - public static final String Invocation = "invocation"; - - /** - * Create the property map for the {@link MessageHandler} constructor using the default objects. - * - * @param handler The handler annotated method of the listener - * @param handlerConfig The annotation that configures the handler - * @param filter The set of preconfigured filters if any - * @param listenerConfig The listener metadata - * @return A map of properties initialized from the given parameters that will conform to the requirements of the - * {@link MessageHandler} constructor. See {@see MessageHandler.validate()} for more details. - */ - public static final Map Create(Method handler, Handler handlerConfig, IMessageFilter[] filter, MessageListener listenerConfig){ - if(handler == null){ - throw new IllegalArgumentException("The message handler configuration may not be null"); - } - net.engio.mbassy.listener.Enveloped enveloped = handler.getAnnotation(Enveloped.class); - Class[] handledMessages = enveloped != null - ? enveloped.messages() - : handler.getParameterTypes(); - handler.setAccessible(true); - Map properties = new HashMap(); - properties.put(HandlerMethod, handler); - properties.put(Filter, filter != null ? filter : new IMessageFilter[]{}); - properties.put(Priority, handlerConfig.priority()); - properties.put(Invocation, handlerConfig.invocation()); - properties.put(InvocationMode, handlerConfig.delivery()); - properties.put(Enveloped, enveloped != null); - properties.put(AcceptSubtypes, !handlerConfig.rejectSubtypes()); - properties.put(Listener, listenerConfig); - properties.put(IsSynchronized, handler.getAnnotation(Synchronized.class) != null); - properties.put(HandledMessages, handledMessages); - return properties; - } - } - - - private final Method handler; - - private final IMessageFilter[] filter; - - private final int priority; - - private final Class invocation; - - private final Invoke invocationMode; - - private final boolean isEnvelope; - - private final Class[] handledMessages; - - private final boolean acceptsSubtypes; - - private final MessageListener listenerConfig; - - private final boolean isSynchronized; - - public MessageHandler(Map properties){ - super(); - validate(properties); - this.handler = (Method)properties.get(Properties.HandlerMethod); - this.filter = (IMessageFilter[])properties.get(Properties.Filter); - this.priority = (Integer)properties.get(Properties.Priority); - this.invocation = (Class)properties.get(Properties.Invocation); - this.invocationMode = (Invoke)properties.get(Properties.InvocationMode); - this.isEnvelope = (Boolean)properties.get(Properties.Enveloped); - this.acceptsSubtypes = (Boolean)properties.get(Properties.AcceptSubtypes); - this.listenerConfig = (MessageListener)properties.get(Properties.Listener); - this.isSynchronized = (Boolean)properties.get(Properties.IsSynchronized); - this.handledMessages = (Class[])properties.get(Properties.HandledMessages); - } - - private void validate(Map properties){ - Object[][] expectedProperties = new Object[][]{ - new Object[]{Properties.HandlerMethod, Method.class }, - new Object[]{Properties.Priority, Integer.class }, - new Object[]{Properties.Invocation, Class.class }, - new Object[]{Properties.Filter, IMessageFilter[].class }, - new Object[]{Properties.Enveloped, Boolean.class }, - new Object[]{Properties.HandledMessages, Class[].class }, - new Object[]{Properties.IsSynchronized, Boolean.class }, - new Object[]{Properties.Listener, MessageListener.class }, - new Object[]{Properties.AcceptSubtypes, Boolean.class } - }; - for(Object[] property : expectedProperties){ - if (properties.get(property[0]) == null || !((Class)property[1]).isAssignableFrom(properties.get(property[0]).getClass())) - throw new IllegalArgumentException("Property " + property[0] + " was expected to be not null and of type " + property[1] - + " but was: " + properties.get(property[0])); - } - - - } - - public boolean isSynchronized(){ - return isSynchronized; - } - - public boolean useStrongReferences(){ - return listenerConfig.useStrongReferences(); - } - - public boolean isFromListener(Class listener){ - return listenerConfig.isFromListener(listener); - } - - public boolean isAsynchronous() { - return invocationMode.equals(Invoke.Asynchronously); - } - - public boolean isFiltered() { - return filter.length > 0; - } - - public int getPriority() { - return priority; - } - - public Method getHandler() { - return handler; - } - - public IMessageFilter[] getFilter() { - return filter; - } - - public Class[] getHandledMessages() { - return handledMessages; - } - - public boolean isEnveloped() { - return isEnvelope; - } - - public Class getHandlerInvocation(){ - return invocation; - } - - public boolean handlesMessage(Class messageType) { - for (Class handledMessage : handledMessages) { - if (handledMessage.equals(messageType)) { - return true; - } - if (handledMessage.isAssignableFrom(messageType) && acceptsSubtypes()) { - return true; - } - } - return false; - } - - public boolean acceptsSubtypes() { - return acceptsSubtypes; - } - -} +package net.engio.mbassy.listener; + +import net.engio.mbassy.dispatch.HandlerInvocation; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +/** + * Any method in any class annotated with the @Handler annotation represents a message handler. The class that contains + * the handler is called a message listener and more generally, any class containing a message handler in its class hierarchy + * defines such a message listener. + * + * @author bennidi + * Date: 11/14/12 + */ +public class MessageHandler { + + public static final class Properties{ + + public static final String HandlerMethod = "handler"; + public static final String InvocationMode = "invocationMode"; + public static final String Filter = "filter"; + public static final String Condition = "condition"; + public static final String Enveloped = "envelope"; + public static final String HandledMessages = "messages"; + public static final String IsSynchronized = "synchronized"; + public static final String Listener = "listener"; + public static final String AcceptSubtypes = "subtypes"; + public static final String Priority = "priority"; + public static final String Invocation = "invocation"; + + /** + * Create the property map for the {@link MessageHandler} constructor using the default objects. + * + * @param handler The handler annotated method of the listener + * @param handlerConfig The annotation that configures the handler + * @param filter The set of preconfigured filters if any + * @param listenerConfig The listener metadata + * @return A map of properties initialized from the given parameters that will conform to the requirements of the + * {@link MessageHandler} constructor. See {@see MessageHandler.validate()} for more details. + */ + public static final Map Create(Method handler, Handler handlerConfig, IMessageFilter[] filter, MessageListener listenerConfig){ + if(handler == null){ + throw new IllegalArgumentException("The message handler configuration may not be null"); + } + net.engio.mbassy.listener.Enveloped enveloped = handler.getAnnotation(Enveloped.class); + Class[] handledMessages = enveloped != null + ? enveloped.messages() + : handler.getParameterTypes(); + handler.setAccessible(true); + Map properties = new HashMap(); + properties.put(HandlerMethod, handler); + properties.put(Filter, filter != null ? filter : new IMessageFilter[]{}); + properties.put(Condition, handlerConfig.condition()); + properties.put(Priority, handlerConfig.priority()); + properties.put(Invocation, handlerConfig.invocation()); + properties.put(InvocationMode, handlerConfig.delivery()); + properties.put(Enveloped, enveloped != null); + properties.put(AcceptSubtypes, !handlerConfig.rejectSubtypes()); + properties.put(Listener, listenerConfig); + properties.put(IsSynchronized, handler.getAnnotation(Synchronized.class) != null); + properties.put(HandledMessages, handledMessages); + return properties; + } + } + + + private final Method handler; + + private final IMessageFilter[] filter; + + private String condition; + + private final int priority; + + private final Class invocation; + + private final Invoke invocationMode; + + private final boolean isEnvelope; + + private final Class[] handledMessages; + + private final boolean acceptsSubtypes; + + private final MessageListener listenerConfig; + + private final boolean isSynchronized; + + + public MessageHandler(Map properties){ + super(); + validate(properties); + this.handler = (Method)properties.get(Properties.HandlerMethod); + this.filter = (IMessageFilter[])properties.get(Properties.Filter); + this.condition = (String)properties.get(Properties.Condition); + this.priority = (Integer)properties.get(Properties.Priority); + this.invocation = (Class)properties.get(Properties.Invocation); + this.invocationMode = (Invoke)properties.get(Properties.InvocationMode); + this.isEnvelope = (Boolean)properties.get(Properties.Enveloped); + this.acceptsSubtypes = (Boolean)properties.get(Properties.AcceptSubtypes); + this.listenerConfig = (MessageListener)properties.get(Properties.Listener); + this.isSynchronized = (Boolean)properties.get(Properties.IsSynchronized); + this.handledMessages = (Class[])properties.get(Properties.HandledMessages); + } + + private void validate(Map properties){ + Object[][] expectedProperties = new Object[][]{ + new Object[]{Properties.HandlerMethod, Method.class }, + new Object[]{Properties.Priority, Integer.class }, + new Object[]{Properties.Invocation, Class.class }, + new Object[]{Properties.Filter, IMessageFilter[].class }, + new Object[]{Properties.Condition, String.class }, + new Object[]{Properties.Enveloped, Boolean.class }, + new Object[]{Properties.HandledMessages, Class[].class }, + new Object[]{Properties.IsSynchronized, Boolean.class }, + new Object[]{Properties.Listener, MessageListener.class }, + new Object[]{Properties.AcceptSubtypes, Boolean.class } + }; + for(Object[] property : expectedProperties){ + if (properties.get(property[0]) == null || !((Class)property[1]).isAssignableFrom(properties.get(property[0]).getClass())) + throw new IllegalArgumentException("Property " + property[0] + " was expected to be not null and of type " + property[1] + + " but was: " + properties.get(property[0])); + } + + + } + + public boolean isSynchronized(){ + return isSynchronized; + } + + public boolean useStrongReferences(){ + return listenerConfig.useStrongReferences(); + } + + public boolean isFromListener(Class listener){ + return listenerConfig.isFromListener(listener); + } + + public boolean isAsynchronous() { + return invocationMode.equals(Invoke.Asynchronously); + } + + public boolean isFiltered() { + return filter.length > 0 || (condition != null && condition.trim().length() > 0); + } + + public int getPriority() { + return priority; + } + + public Method getHandler() { + return handler; + } + + public IMessageFilter[] getFilter() { + return filter; + } + + public String getCondition() { + return this.condition; + } + + public Class[] getHandledMessages() { + return handledMessages; + } + + public boolean isEnveloped() { + return isEnvelope; + } + + public Class getHandlerInvocation(){ + return invocation; + } + + public boolean handlesMessage(Class messageType) { + for (Class handledMessage : handledMessages) { + if (handledMessage.equals(messageType)) { + return true; + } + if (handledMessage.isAssignableFrom(messageType) && acceptsSubtypes()) { + return true; + } + } + return false; + } + + public boolean acceptsSubtypes() { + return acceptsSubtypes; + } + +} diff --git a/src/test/java/net/engio/mbassy/ConditionTest.java b/src/test/java/net/engio/mbassy/ConditionTest.java new file mode 100644 index 0000000..988df63 --- /dev/null +++ b/src/test/java/net/engio/mbassy/ConditionTest.java @@ -0,0 +1,133 @@ +package net.engio.mbassy; + +import net.engio.mbassy.bus.MBassador; +import net.engio.mbassy.bus.config.BusConfiguration; +import net.engio.mbassy.common.MessageBusTest; +import net.engio.mbassy.listener.Handler; + +import org.junit.Test; + +/***************************************************************************** + * Some unit tests for the "condition" filter. + ****************************************************************************/ + +public class ConditionTest extends MessageBusTest { + + public static class TestEvent { + + public Object result; + private String type; + private int size; + + public TestEvent(String type, int size) { + super(); + this.type = type; + this.size = size; + } + + public String getType() { + return type; + } + + public int getSize() { + return size; + } + + } + + public static class ConditionalMessageListener { + + @Handler(condition = "msg.type == \"TEST\"") + public void handleTypeMessage(TestEvent message) { + message.result = "handleTypeMessage"; + } + + @Handler(condition = "msg.size > 4") + public void handleSizeMessage(TestEvent message) { + message.result = "handleSizeMessage"; + } + + @Handler(condition = "msg.size > 2 && msg.size < 4") + public void handleCombinedEL(TestEvent message) { + message.result = "handleCombinedEL"; + } + + @Handler(condition = "msg.getType().equals(\"XYZ\") && msg.getSize() == 1") + public void handleMethodAccessEL(TestEvent message) { + message.result = "handleMethodAccessEL"; + } + + + } + + /************************************************************************* + * @throws Exception + ************************************************************************/ + @Test + public void testSimpleStringCondition() throws Exception { + MBassador bus = getBus(BusConfiguration.Default()); + bus.subscribe(new ConditionalMessageListener()); + + TestEvent message = new TestEvent("TEST", 0); + bus.publish(message); + + assertEquals("handleTypeMessage", message.result); + } + + /************************************************************************* + * @throws Exception + ************************************************************************/ + @Test + public void testSimpleNumberCondition() throws Exception { + MBassador bus = getBus(BusConfiguration.Default()); + bus.subscribe(new ConditionalMessageListener()); + + TestEvent message = new TestEvent("", 5); + bus.publish(message); + + assertEquals("handleSizeMessage", message.result); + } + + /************************************************************************* + * @throws Exception + ************************************************************************/ + @Test + public void testHandleCombinedEL() throws Exception { + MBassador bus = getBus(BusConfiguration.Default()); + bus.subscribe(new ConditionalMessageListener()); + + TestEvent message = new TestEvent("", 3); + bus.publish(message); + + assertEquals("handleCombinedEL", message.result); + } + + /************************************************************************* + * @throws Exception + ************************************************************************/ + @Test + public void testNotMatchingAnyCondition() throws Exception { + MBassador bus = getBus(BusConfiguration.Default()); + bus.subscribe(new ConditionalMessageListener()); + + TestEvent message = new TestEvent("", 0); + bus.publish(message); + + assertTrue(message.result == null); + } + + /************************************************************************* + * @throws Exception + ************************************************************************/ + @Test + public void testHandleMethodAccessEL() throws Exception { + MBassador bus = getBus(BusConfiguration.Default()); + bus.subscribe(new ConditionalMessageListener()); + + TestEvent message = new TestEvent("XYZ", 1); + bus.publish(message); + + assertEquals("handleMethodAccessEL", message.result); + } + +}