Merge pull request #70 from Rossi1337/master

Support Message filtering per Expression Language
This commit is contained in:
Benjamin Diedrichsen 2014-05-22 10:01:05 +02:00
commit 86bdcad336
8 changed files with 1044 additions and 555 deletions

19
pom.xml
View File

@ -81,6 +81,25 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.el</groupId>
<artifactId>el-api</artifactId>
<version>2.2</version>
</dependency>
<dependency>
<groupId>de.odysseus.juel</groupId>
<artifactId>juel-impl</artifactId>
<version>2.2.7</version>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>de.odysseus.juel</groupId>
<artifactId>juel-spi</artifactId>
<version>2.2.7</version>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
</dependencies>
<!-- Local repository (for testing)

View File

@ -1,6 +1,7 @@
package net.engio.mbassy.dispatch;
import net.engio.mbassy.bus.MessagePublication;
import net.engio.mbassy.dispatch.el.ElFilter;
import net.engio.mbassy.listener.IMessageFilter;
/**
@ -37,9 +38,20 @@ public class FilteredMessageDispatcher extends DelegatingMessageDispatcher {
@Override
public void dispatch(MessagePublication publication, Object message, Iterable listeners){
if (passesFilter(message)) {
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());
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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<FeatureDescriptor> 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
}
}

View File

@ -23,6 +23,16 @@ public @interface Handler {
*/
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
* <code>true</code> to allow the event or <code>false</code> 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.

View File

@ -21,6 +21,7 @@ public class MessageHandler {
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";
@ -51,6 +52,7 @@ public class MessageHandler {
Map<String, Object> properties = new HashMap<String, Object>();
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());
@ -68,6 +70,8 @@ public class MessageHandler {
private final IMessageFilter[] filter;
private String condition;
private final int priority;
private final Class<? extends HandlerInvocation> invocation;
@ -84,11 +88,13 @@ public class MessageHandler {
private final boolean isSynchronized;
public MessageHandler(Map<String, Object> 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<? extends HandlerInvocation>)properties.get(Properties.Invocation);
this.invocationMode = (Invoke)properties.get(Properties.InvocationMode);
@ -105,6 +111,7 @@ public class MessageHandler {
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 },
@ -137,7 +144,7 @@ public class MessageHandler {
}
public boolean isFiltered() {
return filter.length > 0;
return filter.length > 0 || (condition != null && condition.trim().length() > 0);
}
public int getPriority() {
@ -152,6 +159,10 @@ public class MessageHandler {
return filter;
}
public String getCondition() {
return this.condition;
}
public Class[] getHandledMessages() {
return handledMessages;
}

View File

@ -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);
}
}