#510 - Added Spring Integration dynamic service activator based on PluginRegistry.

See the ticket or test config file for usage.
This commit is contained in:
Oliver Gierke
2011-05-25 18:25:34 +02:00
parent f0b5ff07a8
commit c7bfd2b214
17 changed files with 668 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<beansProjectDescription>
<version>1</version>
<pluginVersion><![CDATA[2.6.0.201104111100-PATCH]]></pluginVersion>
<configSuffixes>
<configSuffix><![CDATA[xml]]></configSuffix>
</configSuffixes>
<enableImports><![CDATA[false]]></enableImports>
<configs>
</configs>
<configSets>
</configSets>
</beansProjectDescription>

58
org.synyx.hera.si/pom.xml Normal file
View File

@@ -0,0 +1,58 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>org.synyx.hera</artifactId>
<groupId>org.synyx.hera</groupId>
<version>0.7.0.BUILD-SNAPSHOT</version>
</parent>
<groupId>org.synyx.hera</groupId>
<artifactId>org.synyx.hera.si</artifactId>
<version>0.7.0.BUILD-SNAPSHOT</version>
<name>Hera Spring Integration integration</name>
<properties>
<spring.integration.version>2.0.3.RELEASE</spring.integration.version>
<spring.version>3.0.5.RELEASE</spring.version>
</properties>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>org.synyx.hera.core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-core</artifactId>
<version>${spring.integration.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,204 @@
/*
* 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;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.springframework.core.GenericTypeResolver;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.integration.Message;
import org.springframework.integration.MessageHandlingException;
import org.springframework.integration.handler.AbstractReplyProducingMessageHandler;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ReflectionUtils;
import org.synyx.hera.core.Plugin;
import org.synyx.hera.core.PluginRegistry;
/**
* Dynamic service activator that uses a {@link PluginRegistry} to delegate execution to one or more plugins matching a
* delimiter.
*
* @author Oliver Gierke
*/
public class PluginRegistryAwareMessageHandler extends AbstractReplyProducingMessageHandler {
private enum InvocationMethod {
ONE, ALL;
}
private final PluginRegistry<? extends Plugin<?>, Object> registry;
private final Class<? extends Plugin<?>> pluginType;
private final Class<?> delimitzerType;
private final SpelExpressionParser parser = new SpelExpressionParser();
private Expression delimiterExpression;
private Expression invocationArgumentsExpression;
private String serviceMethodName;
private InvocationMethod invocationMethod = InvocationMethod.ONE;
/**
* Creates a new {@link PluginRegistryAwareMessageHandler} for the given {@link PluginRegistry}, pluginType and a
* method name to call.
*
* @param registry
* @param pluginType
* @param serviceMethodName
*/
@SuppressWarnings("unchecked")
public PluginRegistryAwareMessageHandler(PluginRegistry<? extends Plugin<?>, ?> registry,
Class<? extends Plugin<?>> pluginType, String serviceMethodName) {
Assert.notNull(registry);
Assert.notNull(pluginType);
Assert.hasText(serviceMethodName);
this.registry = (PluginRegistry<? extends Plugin<?>, Object>) registry;
this.serviceMethodName = serviceMethodName;
this.pluginType = pluginType;
this.delimitzerType = GenericTypeResolver.resolveTypeArgument(pluginType, Plugin.class);
}
/**
* Sets the SpEL expression to extract the delimiter from the {@link Message}.
*
* @param delimiterExpression the delimiterExpression to set
*/
public void setDelimiterExpression(String expression) {
Assert.hasText(expression);
this.delimiterExpression = parser.parseExpression(expression);
}
/**
* Sets the SpEL expression to extract the method arguments for the actual plugin method invocation from the
* {@link Message}.
*
* @param invocationArgumentsExpression the invocationArgumentsExpression to set
*/
public void setInvocationArgumentsExpression(String expression) {
Assert.hasText(expression);
this.invocationArgumentsExpression = parser.parseExpression(expression);
}
/*
* (non-Javadoc)
* @see org.springframework.integration.handler.AbstractReplyProducingMessageHandler#handleRequestMessage(org.springframework.integration.Message)
*/
@SuppressWarnings("unchecked")
@Override
protected Object handleRequestMessage(Message<?> requestMessage) {
Object delimiter = getDelimiter(requestMessage);
switch (invocationMethod) {
case ALL:
return invokePlugins(registry.getPluginsFor(delimiter), requestMessage);
case ONE:
default:
List<Object> results = invokePlugins(Arrays.asList(registry.getPluginFor(delimiter)), requestMessage);
return results.isEmpty() ? null : results.get(0);
}
}
private List<Object> invokePlugins(Iterable<? extends Plugin<?>> plugins, Message<?> message) {
List<Object> results = new ArrayList<Object>();
for (Plugin<?> plugin : plugins) {
Object[] invocationArguments = getInvocationArguments(message);
Class<?>[] types = getTypes(invocationArguments);
Method businessMethod = ReflectionUtils.findMethod(pluginType, serviceMethodName, types);
if (businessMethod == null) {
throw new MessageHandlingException(message, String.format(
"Did not find a method %s on %s taking the following parameters %s", serviceMethodName,
pluginType.getName(), Arrays.toString(types)));
}
Object result = ReflectionUtils.invokeMethod(businessMethod, plugin, invocationArguments);
if (!businessMethod.getReturnType().equals(void.class)) {
results.add(result);
}
}
return results;
}
/**
* Returns the delimiter object to be used for the given {@link Message}. Will use the configured delimiter expression
* if configured.
*
* @param message
* @return
*/
private Object getDelimiter(Message<?> message) {
Object delimiter = message;
if (delimiterExpression != null) {
StandardEvaluationContext context = new StandardEvaluationContext(message);
delimiter = delimiterExpression.getValue(context);
}
Assert.isInstanceOf(delimitzerType, 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));
return delimiter;
}
/**
* Returns the actual arguments to be used for the plugin method invocation. Will apply the configured invocation
* argument expression to the given {@link Message}.
*
* @param message
* @return
*/
private Object[] getInvocationArguments(Message<?> message) {
if (invocationArgumentsExpression == null) {
return new Object[] { message };
}
StandardEvaluationContext context = new StandardEvaluationContext(message);
Object result = delimiterExpression.getValue(context);
return ObjectUtils.isArray(result) ? ObjectUtils.toObjectArray(result) : new Object[] { result };
}
/**
* Returns an array of types for the given objects. Inspects each element of the array for its type. will return
* {@literal null} for {@literal null} source values.
*
* @param source
* @return
*/
private Class<?>[] getTypes(Object[] source) {
Class<?>[] result = new Class<?>[source.length];
for (int i = 0; i < source.length; i++) {
Object sourceElement = source[i];
result[i] = sourceElement == null ? null : sourceElement.getClass();
}
return result;
}
}

View File

@@ -0,0 +1,87 @@
/*
* 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.config;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.BeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.integration.config.xml.AbstractConsumerEndpointParser;
import org.springframework.util.StringUtils;
import org.synyx.hera.core.support.PluginRegistryFactoryBean;
import org.synyx.hera.si.PluginRegistryAwareMessageHandler;
import org.w3c.dom.Element;
/**
* {@link BeanDefinitionParser} to create {@link PluginRegistryAwareMessageHandler} beans.
*
* @author Oliver Gierke
*/
public class DynamicServiceActivatorParser extends AbstractConsumerEndpointParser {
/*
* (non-Javadoc)
* @see org.springframework.integration.config.xml.AbstractConsumerEndpointParser#parseHandler(org.w3c.dom.Element, org.springframework.beans.factory.xml.ParserContext)
*/
@Override
protected BeanDefinitionBuilder parseHandler(Element element, ParserContext parserContext) {
Object source = parserContext.extractSource(element);
String pluginType = element.getAttribute("plugin-type");
String method = element.getAttribute("method");
BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(PluginRegistryAwareMessageHandler.class);
builder.addConstructorArgValue(getRegistryBeanDefinition(pluginType, source));
builder.addConstructorArgValue(pluginType);
builder.addConstructorArgValue(method);
String delimiter = element.getAttribute("delimiter");
if (StringUtils.hasText(delimiter)) {
builder.addPropertyValue("delimiterExpression", delimiter);
}
String invocationArguments = element.getAttribute("invocation-arguments");
if (StringUtils.hasText(invocationArguments)) {
builder.addPropertyValue("invocationArgumentsExpression", invocationArguments);
}
AbstractBeanDefinition definition = builder.getBeanDefinition();
definition.setSource(source);
return builder;
}
/**
* Creates a {@link BeanDefinition} for a {@link PluginRegistryFactoryBean}.
*
* @param pluginType
* @param source
* @return
*/
private AbstractBeanDefinition getRegistryBeanDefinition(String pluginType, Object source) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(PluginRegistryFactoryBean.class);
builder.addPropertyValue("type", pluginType);
AbstractBeanDefinition definition = builder.getBeanDefinition();
definition.setSource(source);
return definition;
}
}

View File

@@ -0,0 +1,36 @@
/*
* 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.config;
import org.springframework.beans.factory.xml.BeanDefinitionParser;
import org.springframework.beans.factory.xml.NamespaceHandler;
import org.springframework.beans.factory.xml.NamespaceHandlerSupport;
/**
* {@link NamespaceHandler} to register {@link BeanDefinitionParser}s.
*
* @author Oliver Gierke
*/
public class HeraSpringIntegrationNamespaceHandler extends NamespaceHandlerSupport {
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.xml.NamespaceHandler#init()
*/
public void init() {
registerBeanDefinitionParser("dynamic-service-activator", new DynamicServiceActivatorParser());
}
}

View File

@@ -0,0 +1 @@
http\://schemas.synyx.org/hera/si=org.synyx.hera.si.config.HeraSpringIntegrationNamespaceHandler

View File

@@ -0,0 +1 @@
http\://schemas.synyx.org/hera/si/hera-si.xsd=org/synyx/hera/si/config/hera-si.xsd

View File

@@ -0,0 +1,4 @@
# Tooling related information for the Hera namespace
http\://schemas.synyx.org/hera/si@name=Hera Spring Integration Namespace
http\://schemas.synyx.org/hera/si@prefix=int-hera
http\://schemas.synyx.org/hera/si@icon=org/springframework/beans/factory/xml/spring-beans.gif

View File

@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8" ?>
<xsd:schema xmlns="http://schemas.synyx.org/hera/si" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:tool="http://www.springframework.org/schema/tool"
targetNamespace="http://schemas.synyx.org/hera/si" elementFormDefault="qualified"
attributeFormDefault="unqualified">
<xsd:import namespace="http://www.springframework.org/schema/tool" />
<xsd:element name="dynamic-service-activator">
<xsd:annotation>
<xsd:appinfo>
<xsd:documentation>
This service activator will be backed by a Hera Plugin registry that
allows dynamic invocation of Spring beans implementing the plugin
interface defined in "plugin-type". We will dynamically pick up all
Spring beans implementing that interface and create a PluginRegistry
of those. See org.synyx.hera.si.PluginRegistryAwareMessageHandler for
details.
</xsd:documentation>
</xsd:appinfo>
</xsd:annotation>
<xsd:complexType>
<xsd:attribute name="input-channel" type="xsd:string" use="required">
<xsd:annotation>
<xsd:documentation>The input channel to listen to.</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="plugin-type" type="xsd:string" use="required">
<xsd:annotation>
<xsd:documentation>The type of Spring beans to dynamically pick up.</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="method" type="xsd:string" use="required">
<xsd:annotation>
<xsd:documentation>The method to be invoked on the plugin(s) selected.</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="output-channel" type="xsd:string">
<xsd:annotation>
<xsd:documentation>The output channel to publish invocation results to.</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="delimiter" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
A SpEL expression to extract the delimiter to be used from the
incoming Message. If not set the entire Message will be used as
delimiter.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="invocation-arguments" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
A SpEL expression to extract the arguments for the actual method
invocation.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
</xsd:element>
</xsd:schema>

View File

@@ -0,0 +1,58 @@
/*
* 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;
import java.util.Arrays;
import org.junit.Test;
import org.springframework.integration.Message;
import org.springframework.integration.MessageHandlingException;
import org.springframework.integration.support.MessageBuilder;
import org.synyx.hera.core.OrderAwarePluginRegistry;
import org.synyx.hera.core.PluginRegistry;
import org.synyx.hera.si.sample.FirstSamplePluginImpl;
import org.synyx.hera.si.sample.SamplePlugin;
import org.synyx.hera.si.sample.SecondSamplePluginImpl;
/**
* Unit tests for {@link PluginRegistryAwareMessageHandler}.
*
* @author Oliver Gierke
*/
public class PluginRegistryAwareMessageHandlerUnitTest {
PluginRegistry<SamplePlugin, String> registry = OrderAwarePluginRegistry.create(Arrays.asList(
new FirstSamplePluginImpl(), new SecondSamplePluginImpl()));
PluginRegistryAwareMessageHandler handler = new PluginRegistryAwareMessageHandler(registry, SamplePlugin.class,
"myBusinessMethod");
@Test
public void routesInvocationToFirstpluginIfConfiguredToDoSo() {
handler.setDelimiterExpression("payload");
handler.setInvocationArgumentsExpression("payload");
Message<String> message = MessageBuilder.withPayload("FOO").build();
handler.handleMessage(message);
}
@Test(expected=MessageHandlingException.class)
public void failsHandlingMessageIfDelimiterTypeDoesNotMatch() {
Message<String> message = MessageBuilder.withPayload("FOO").build();
handler.handleMessage(message);
}
}

View File

@@ -0,0 +1,47 @@
/*
* 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.config;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.integration.Message;
import org.springframework.integration.MessageChannel;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* Integration test for the namespace.
*
* @author Oliver Gierke
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:dynamic-service-activator-test-context.xml")
public class DynamicServiceActivatorNamespaceIntegrationTest {
@Autowired
@Qualifier("sampleChannel")
MessageChannel channel;
@Test
public void invokesPluginBasedOnPayload() {
Message<String> message = MessageBuilder.withPayload("FOO").build();
channel.send(message);
}
}

View File

@@ -0,0 +1,22 @@
package org.synyx.hera.si.sample;
/**
*
* @author Oliver Gierke
*/
public class FirstSamplePluginImpl implements SamplePlugin {
/* (non-Javadoc)
* @see org.synyx.hera.core.Plugin#supports(java.lang.Object)
*/
public boolean supports(String delimiter) {
return "FOO".equals(delimiter);
}
/* (non-Javadoc)
* @see org.synyx.hera.si.sample.SamplePlugin#myBusinessMethod()
*/
public void myBusinessMethod(String message) {
System.out.println("First plugin invoked! " + message);
}
}

View File

@@ -0,0 +1,12 @@
package org.synyx.hera.si.sample;
import org.synyx.hera.core.Plugin;
/**
*
* @author Oliver Gierke
*/
public interface SamplePlugin extends Plugin<String> {
void myBusinessMethod(String message);
}

View File

@@ -0,0 +1,22 @@
package org.synyx.hera.si.sample;
/**
*
* @author Oliver Gierke
*/
public class SecondSamplePluginImpl 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 void myBusinessMethod(String message) {
System.out.println("Second plugin invoked! " + message);
}
}

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:int="http://www.springframework.org/schema/integration"
xmlns:int-hera="http://schemas.synyx.org/hera/si"
xsi:schemaLocation="http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration-2.0.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://schemas.synyx.org/hera/si http://schemas.synyx.org/hera/si/hera-si.xsd">
<bean class="org.synyx.hera.si.sample.FirstSamplePluginImpl" />
<bean class="org.synyx.hera.si.sample.SecondSamplePluginImpl" />
<int-hera:dynamic-service-activator
input-channel="sampleChannel"
output-channel="foo"
plugin-type="org.synyx.hera.si.sample.SamplePlugin"
method="myBusinessMethod"
delimiter="payload"
invocation-arguments="payload" />
<int:channel id="sampleChannel" />
<int:channel id="foo" />
</beans>

View File

@@ -0,0 +1,13 @@
Bundle-ManifestVersion: 2
Bundle-SymbolicName: ${project.artifactId}
Bundle-Name: ${project.name}
Bundle-Vendor: Synyx GmbH & Co. KG
Bundle-Version: ${project.version}
Bundle-RequiredExecutionEnvironment: J2SE-1.5
Export-Template:
${project.artifactId}.*;version="${project.version}"
Import-Template:
org.synyx.hera.core.*;version="${project.version:[=.=.=.=,+1.0.0)}",
org.springframework.*;version="${spring.version:[=.=.=.=,+2.0.0)}",
org.springframework.integration.*;version="${spring.integration.version:[=.=.=.=,+1.0.0)}",
org.w3c.dom.*;version="0"

View File

@@ -17,6 +17,7 @@
<modules>
<module>core</module>
<module>metadata</module>
<module>org.synyx.hera.si</module>
</modules>
<properties>