From 64432821769af0c575a90ffab5ee67aa466626be Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Mon, 30 May 2011 12:08:01 +0200 Subject: [PATCH] #510 - Further improvements to PluginRegistryAwareMessageHandler. Fixed typo in property name. Added unit test to reject invalid method name on construction already. Added logging output in debug log level. Tightened XSD to hint plugin-type attribute to be a class. --- .../si/PluginRegistryAwareMessageHandler.java | 107 ++++++++++++++++-- .../org/synyx/hera/si/config/hera-si.xsd | 28 ++++- ...inRegistryAwareMessageHandlerUnitTest.java | 45 +++++++- .../hera/si/sample/FirstSamplePluginImpl.java | 18 ++- .../synyx/hera/si/sample/SamplePlugin.java | 17 ++- .../si/sample/SecondSamplePluginImpl.java | 22 +++- .../hera/si/sample/ThirdSamplePluginImpl.java | 42 +++++++ 7 files changed, 259 insertions(+), 20 deletions(-) create mode 100644 org.synyx.hera.si/src/test/java/org/synyx/hera/si/sample/ThirdSamplePluginImpl.java diff --git a/org.synyx.hera.si/src/main/java/org/synyx/hera/si/PluginRegistryAwareMessageHandler.java b/org.synyx.hera.si/src/main/java/org/synyx/hera/si/PluginRegistryAwareMessageHandler.java index 91c8810..0bf8f56 100644 --- a/org.synyx.hera.si/src/main/java/org/synyx/hera/si/PluginRegistryAwareMessageHandler.java +++ b/org.synyx.hera.si/src/main/java/org/synyx/hera/si/PluginRegistryAwareMessageHandler.java @@ -18,9 +18,14 @@ package org.synyx.hera.si; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.List; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.core.GenericTypeResolver; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import org.springframework.expression.Expression; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; @@ -30,6 +35,8 @@ import org.springframework.integration.handler.AbstractReplyProducingMessageHand import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; +import org.synyx.hera.core.OrderAwarePluginRegistry; import org.synyx.hera.core.Plugin; import org.synyx.hera.core.PluginRegistry; @@ -41,19 +48,17 @@ import org.synyx.hera.core.PluginRegistry; */ public class PluginRegistryAwareMessageHandler extends AbstractReplyProducingMessageHandler { - private enum InvocationMethod { - ONE, ALL; - } + private static final Log LOG = LogFactory.getLog(PluginRegistryAwareMessageHandler.class); private final PluginRegistry, Object> registry; private final Class> pluginType; - private final Class delimitzerType; + private final Class delimiterType; private final SpelExpressionParser parser = new SpelExpressionParser(); private Expression delimiterExpression; private Expression invocationArgumentsExpression; private String serviceMethodName; - private InvocationMethod invocationMethod = InvocationMethod.ONE; + private PluginLookupMethod pluginLookupMethod = PluginLookupMethod.getDefault(); /** * Creates a new {@link PluginRegistryAwareMessageHandler} for the given {@link PluginRegistry}, pluginType and a @@ -74,7 +79,26 @@ public class PluginRegistryAwareMessageHandler extends AbstractReplyProducingMes this.registry = (PluginRegistry, Object>) registry; this.serviceMethodName = serviceMethodName; this.pluginType = pluginType; - this.delimitzerType = GenericTypeResolver.resolveTypeArgument(pluginType, Plugin.class); + this.delimiterType = GenericTypeResolver.resolveTypeArgument(pluginType, Plugin.class); + + verify(); + } + + private final void verify() { + + boolean methodFound = false; + + for (Method candidate : pluginType.getMethods()) { + if (candidate.getName().equals(serviceMethodName)) { + methodFound = true; + break; + } + } + + if (!methodFound) { + throw new IllegalArgumentException(String.format("Not method %s found for type %s!", serviceMethodName, + pluginType)); + } } /** @@ -98,6 +122,16 @@ public class PluginRegistryAwareMessageHandler extends AbstractReplyProducingMes this.invocationArgumentsExpression = parser.parseExpression(expression); } + /** + * Configures the method to be used when looking up plugins to invoke. + * + * @see PluginLookupMethod + * @param pluginLookupMethod the invocationMethod to set + */ + public void setPluginLookupMethod(PluginLookupMethod pluginLookupMethod) { + this.pluginLookupMethod = pluginLookupMethod == null ? PluginLookupMethod.getDefault() : pluginLookupMethod; + } + /* * (non-Javadoc) * @see org.springframework.integration.handler.AbstractReplyProducingMessageHandler#handleRequestMessage(org.springframework.integration.Message) @@ -108,18 +142,32 @@ public class PluginRegistryAwareMessageHandler extends AbstractReplyProducingMes Object delimiter = getDelimiter(requestMessage); - switch (invocationMethod) { + switch (pluginLookupMethod) { + case ALL: + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("Looking up plugins for delimiter %s", delimiter)); + } return invokePlugins(registry.getPluginsFor(delimiter), requestMessage); + case ONE: - default: + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("Looking up plugin for delimiter %s", delimiter)); + } List results = invokePlugins(Arrays.asList(registry.getPluginFor(delimiter)), requestMessage); return results.isEmpty() ? null : results.get(0); + + default: + throw new IllegalStateException(String.format("Unsupported plugin lookup method %s!", pluginLookupMethod)); } } - private List invokePlugins(Iterable> plugins, Message message) { + private List invokePlugins(Collection> plugins, Message message) { List results = new ArrayList(); + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("Invoking plugin(s) %s with message %s", + StringUtils.collectionToCommaDelimitedString(plugins), message)); + } for (Plugin plugin : plugins) { @@ -134,6 +182,11 @@ public class PluginRegistryAwareMessageHandler extends AbstractReplyProducingMes pluginType.getName(), Arrays.toString(types))); } + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("Invoke plugin method %s using arguments %s", businessMethod, + Arrays.toString(invocationArguments))); + } + Object result = ReflectionUtils.invokeMethod(businessMethod, plugin, invocationArguments); if (!businessMethod.getReturnType().equals(void.class)) { @@ -160,9 +213,9 @@ public class PluginRegistryAwareMessageHandler extends AbstractReplyProducingMes delimiter = delimiterExpression.getValue(context); } - Assert.isInstanceOf(delimitzerType, delimiter, String.format("Delimiter expression did " + Assert.isInstanceOf(delimiterType, delimiter, String.format("Delimiter expression did " + "not return a suitable delimiter! Make sure the expression evaluates to a suitable " - + "type! Got %s but need %s", delimiter.getClass(), delimitzerType)); + + "type! Got %s but need %s", delimiter.getClass(), delimiterType)); return delimiter; } @@ -201,4 +254,36 @@ public class PluginRegistryAwareMessageHandler extends AbstractReplyProducingMes } return result; } + + + /** + * Lookup methods for plugins. + * + * @author Oliver Gierke + */ + private enum PluginLookupMethod { + + /** + * The first plugin supporting a given delimiter found will be invoked. + */ + ONE, + + /** + * All plugins supporting a given delimiter will be invoked. Plugin order will be considered. + * + * @see OrderAwarePluginRegistry + * @see Order + * @see Ordered + */ + ALL; + + /** + * Returns the default {@link PluginLookupMethod}. + * + * @return + */ + static PluginLookupMethod getDefault() { + return ONE; + } + } } diff --git a/org.synyx.hera.si/src/main/resources/org/synyx/hera/si/config/hera-si.xsd b/org.synyx.hera.si/src/main/resources/org/synyx/hera/si/config/hera-si.xsd index fc70ee8..2df6932 100644 --- a/org.synyx.hera.si/src/main/resources/org/synyx/hera/si/config/hera-si.xsd +++ b/org.synyx.hera.si/src/main/resources/org/synyx/hera/si/config/hera-si.xsd @@ -26,7 +26,7 @@ The input channel to listen to. - + The type of Spring beans to dynamically pick up. @@ -58,7 +58,33 @@ + + + + Defines whether to invoke the first plugin matching the delimiter + (default) or all found. Order of the plugins will be considered. + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/org.synyx.hera.si/src/test/java/org/synyx/hera/si/PluginRegistryAwareMessageHandlerUnitTest.java b/org.synyx.hera.si/src/test/java/org/synyx/hera/si/PluginRegistryAwareMessageHandlerUnitTest.java index adfcc00..f47c01c 100644 --- a/org.synyx.hera.si/src/test/java/org/synyx/hera/si/PluginRegistryAwareMessageHandlerUnitTest.java +++ b/org.synyx.hera.si/src/test/java/org/synyx/hera/si/PluginRegistryAwareMessageHandlerUnitTest.java @@ -14,11 +14,21 @@ * limitations under the License. */ package org.synyx.hera.si; +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; import java.util.Arrays; +import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; import org.springframework.integration.Message; +import org.springframework.integration.MessageChannel; import org.springframework.integration.MessageHandlingException; import org.springframework.integration.support.MessageBuilder; import org.synyx.hera.core.OrderAwarePluginRegistry; @@ -32,27 +42,52 @@ import org.synyx.hera.si.sample.SecondSamplePluginImpl; * * @author Oliver Gierke */ +@RunWith(MockitoJUnitRunner.class) public class PluginRegistryAwareMessageHandlerUnitTest { - PluginRegistry registry = OrderAwarePluginRegistry.create(Arrays.asList( - new FirstSamplePluginImpl(), new SecondSamplePluginImpl())); - PluginRegistryAwareMessageHandler handler = new PluginRegistryAwareMessageHandler(registry, SamplePlugin.class, - "myBusinessMethod"); + PluginRegistry registry; + PluginRegistryAwareMessageHandler handler; + + @Mock + MessageChannel outputChannel; + + @Before + public void setUp() { + registry = OrderAwarePluginRegistry + .create(Arrays.asList(new FirstSamplePluginImpl(), new SecondSamplePluginImpl())); + + handler = new PluginRegistryAwareMessageHandler(registry, SamplePlugin.class, "myBusinessMethod"); + handler.setOutputChannel(outputChannel); + } @Test + @SuppressWarnings("rawtypes") public void routesInvocationToFirstpluginIfConfiguredToDoSo() { handler.setDelimiterExpression("payload"); handler.setInvocationArgumentsExpression("payload"); + handler.afterPropertiesSet(); Message message = MessageBuilder.withPayload("FOO").build(); + when(outputChannel.send(Mockito.any(Message.class))).thenReturn(true); + handler.handleMessage(message); + + ArgumentCaptor resultMessage = ArgumentCaptor.forClass(Message.class); + verify(outputChannel).send(resultMessage.capture()); + assertThat(resultMessage.getValue().getPayload().toString(), is("First")); } - @Test(expected=MessageHandlingException.class) + @Test(expected = MessageHandlingException.class) public void failsHandlingMessageIfDelimiterTypeDoesNotMatch() { Message message = MessageBuilder.withPayload("FOO").build(); handler.handleMessage(message); } + + @Test(expected = IllegalArgumentException.class) + public void rejectsInvalidMethodName() { + + new PluginRegistryAwareMessageHandler(registry, SamplePlugin.class, "foo"); + } } diff --git a/org.synyx.hera.si/src/test/java/org/synyx/hera/si/sample/FirstSamplePluginImpl.java b/org.synyx.hera.si/src/test/java/org/synyx/hera/si/sample/FirstSamplePluginImpl.java index 1e562c5..6c71cb1 100644 --- a/org.synyx.hera.si/src/test/java/org/synyx/hera/si/sample/FirstSamplePluginImpl.java +++ b/org.synyx.hera.si/src/test/java/org/synyx/hera/si/sample/FirstSamplePluginImpl.java @@ -1,3 +1,18 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.synyx.hera.si.sample; /** @@ -16,7 +31,8 @@ public class FirstSamplePluginImpl implements SamplePlugin { /* (non-Javadoc) * @see org.synyx.hera.si.sample.SamplePlugin#myBusinessMethod() */ - public void myBusinessMethod(String message) { + public String myBusinessMethod(String message) { System.out.println("First plugin invoked! " + message); + return "First"; } } diff --git a/org.synyx.hera.si/src/test/java/org/synyx/hera/si/sample/SamplePlugin.java b/org.synyx.hera.si/src/test/java/org/synyx/hera/si/sample/SamplePlugin.java index 05a19d9..d30b1c8 100644 --- a/org.synyx.hera.si/src/test/java/org/synyx/hera/si/sample/SamplePlugin.java +++ b/org.synyx.hera.si/src/test/java/org/synyx/hera/si/sample/SamplePlugin.java @@ -1,3 +1,18 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.synyx.hera.si.sample; import org.synyx.hera.core.Plugin; @@ -8,5 +23,5 @@ import org.synyx.hera.core.Plugin; */ public interface SamplePlugin extends Plugin { - void myBusinessMethod(String message); + String myBusinessMethod(String message); } diff --git a/org.synyx.hera.si/src/test/java/org/synyx/hera/si/sample/SecondSamplePluginImpl.java b/org.synyx.hera.si/src/test/java/org/synyx/hera/si/sample/SecondSamplePluginImpl.java index 36c2d14..637abe8 100644 --- a/org.synyx.hera.si/src/test/java/org/synyx/hera/si/sample/SecondSamplePluginImpl.java +++ b/org.synyx.hera.si/src/test/java/org/synyx/hera/si/sample/SecondSamplePluginImpl.java @@ -1,9 +1,28 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.synyx.hera.si.sample; +import org.springframework.core.annotation.Order; + /** + * Sample implementation of {@link SamplePlugin} supporting {@code BAR} delimiter. * * @author Oliver Gierke */ +@Order(10) public class SecondSamplePluginImpl implements SamplePlugin { /* (non-Javadoc) @@ -16,7 +35,8 @@ public class SecondSamplePluginImpl implements SamplePlugin { /* (non-Javadoc) * @see org.synyx.hera.si.sample.SamplePlugin#myBusinessMethod() */ - public void myBusinessMethod(String message) { + public String myBusinessMethod(String message) { System.out.println("Second plugin invoked! " + message); + return "Second"; } } diff --git a/org.synyx.hera.si/src/test/java/org/synyx/hera/si/sample/ThirdSamplePluginImpl.java b/org.synyx.hera.si/src/test/java/org/synyx/hera/si/sample/ThirdSamplePluginImpl.java new file mode 100644 index 0000000..4a59d89 --- /dev/null +++ b/org.synyx.hera.si/src/test/java/org/synyx/hera/si/sample/ThirdSamplePluginImpl.java @@ -0,0 +1,42 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.synyx.hera.si.sample; + +import org.springframework.core.annotation.Order; + +/** + * Third sample implementation of {@link SamplePlugin} also supporting {@code BAR) but with lower precendence. + * + * @author Oliver Gierke + */ +@Order(20) +class ThirdSamplePluginImpl implements SamplePlugin { + + /* (non-Javadoc) + * @see org.synyx.hera.core.Plugin#supports(java.lang.Object) + */ + public boolean supports(String delimiter) { + return "BAR".equals(delimiter); + } + + /* (non-Javadoc) + * @see org.synyx.hera.si.sample.SamplePlugin#myBusinessMethod() + */ + public String myBusinessMethod(String message) { + System.out.println("Second plugin invoked! " + message); + return "Third"; + } +}