INT-1298: Combine some message group features and base classes

This commit is contained in:
David Syer
2010-07-29 11:14:53 +00:00
parent d92a1245d3
commit 7dda54bb79
12 changed files with 206 additions and 197 deletions

View File

@@ -13,20 +13,29 @@
package org.springframework.integration.aggregator;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.support.AopUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.integration.Message;
import org.springframework.integration.MessageHeaders;
import org.springframework.integration.annotation.Header;
import org.springframework.integration.core.MessageBuilder;
import org.springframework.integration.core.MessageChannel;
import org.springframework.integration.core.MessagingTemplate;
import org.springframework.integration.store.MessageGroup;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
/**
* Base class for MessageGroupProcessor implementations that aggregate the group of Messages into a single Message.
@@ -88,4 +97,123 @@ public abstract class AbstractAggregatingMessageGroupProcessor implements Messag
protected abstract Object aggregatePayloads(MessageGroup group);
protected MessageListProcessor getAdapter(Object candidate, Class<? extends Annotation> annotationType) {
Method method = findAggregatorMethod(candidate, annotationType);
if (method == null) {
return null;
}
return new MethodInvokingMessageListProcessor(candidate, method);
}
private Method findAggregatorMethod(Object candidate, Class<? extends Annotation> annotationType) {
Class<?> targetClass = AopUtils.getTargetClass(candidate);
if (targetClass == null) {
targetClass = candidate.getClass();
}
Method method = this.findAnnotatedMethod(targetClass, annotationType);
if (method == null) {
method = this.findSinglePublicMethod(targetClass);
}
return method;
}
private Method findAnnotatedMethod(final Class<?> targetClass, final Class<? extends Annotation> annotationType) {
final AtomicReference<Method> annotatedMethod = new AtomicReference<Method>();
ReflectionUtils.doWithMethods(targetClass, new ReflectionUtils.MethodCallback() {
public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
Annotation annotation = AnnotationUtils.findAnnotation(method, annotationType);
if (annotation != null) {
Assert.isNull(annotatedMethod.get(), "found more than one method on target class [" + targetClass
+ "] with the annotation type [" + annotationType.getName() + "]");
annotatedMethod.set(method);
}
}
});
return annotatedMethod.get();
}
private Method findSinglePublicMethod(Class<?> targetClass) {
Set<Method> methods = new HashSet<Method>();
for (Method method : targetClass.getMethods()) {
if (!method.getDeclaringClass().equals(Object.class)) {
methods.add(method);
}
}
removeListIncompatibleMethodsFrom(methods);
removeVoidMethodsFrom(methods);
removeUnfittingFrom(methods);
if (methods.size() > 1) {
throw new IllegalArgumentException("Class [" + targetClass + "] contains more than one public Method.");
}
return methods.isEmpty() ? null : methods.iterator().next();
}
private void removeListIncompatibleMethodsFrom(Set<Method> candidates) {
removeMethodsMatchingSelector(candidates, new MethodSelector() {
public boolean select(Method method) {
int found = 0;
for (Class<?> parameterClass : method.getParameterTypes()) {
if (Collection.class.isAssignableFrom(parameterClass)) {
found++;
}
}
return found != 1;
}
});
}
private void removeVoidMethodsFrom(Set<Method> candidates) {
removeMethodsMatchingSelector(candidates, new MethodSelector() {
public boolean select(Method method) {
return method.getReturnType().getName().equals("void");
}
});
}
private Set<Method> removeUnfittingFrom(Set<Method> candidates) {
return removeMethodsMatchingSelector(candidates, new MethodSelector() {
public boolean select(Method method) {
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
Class<?>[] parameterTypes = method.getParameterTypes();
return (!isFittinglyAnnotated(parameterTypes, parameterAnnotations));
}
});
}
private boolean isFittinglyAnnotated(Class<?>[] parameterTypes, Annotation[][] parameterAnnotations) {
int candidateParametersFound = 0;
for (int i = 0; i < parameterTypes.length; i++) {
Class<?> parameterType = parameterTypes[i];
if (Collection.class.isAssignableFrom(parameterType)) {
boolean headerAnnotationFound = false;
for (Annotation annotation : parameterAnnotations[i]) {
if (annotation instanceof Header) {
headerAnnotationFound = true;
}
}
if (!headerAnnotationFound) {
candidateParametersFound++;
}
}
}
return candidateParametersFound == 1;
}
private Set<Method> removeMethodsMatchingSelector(Set<Method> candidates, MethodSelector selector) {
Set<Method> removed = new HashSet<Method>();
Iterator<Method> iterator = candidates.iterator();
while (iterator.hasNext()) {
Method method = iterator.next();
if (selector.select(method)) {
iterator.remove();
removed.add(method);
}
}
return removed;
}
private interface MethodSelector {
boolean select(Method method);
}
}

View File

@@ -1,7 +1,8 @@
package org.springframework.integration.aggregator;
import org.springframework.integration.core.MessageBuilder;
import org.springframework.integration.core.MessageChannel;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.core.convert.ConversionService;
import org.springframework.integration.core.MessagingTemplate;
import org.springframework.integration.store.MessageGroup;
@@ -14,20 +15,33 @@ import org.springframework.integration.store.MessageGroup;
* @author Dave Syer
*
*/
public class ExpressionEvaluatingMessageGroupProcessor extends AbstractExpressionEvaluatingMessageListProcessor
implements MessageGroupProcessor {
public class ExpressionEvaluatingMessageGroupProcessor extends AbstractAggregatingMessageGroupProcessor implements BeanFactoryAware {
private final ExpressionEvaluatingMessageListProcessor processor;
public void setBeanFactory(BeanFactory beanFactory) {
processor.setBeanFactory(beanFactory);
}
public void setConversionService(ConversionService conversionService) {
processor.setConversionService(conversionService);
}
public void setExpectedType(Class<?> expectedType) {
processor.setExpectedType(expectedType);
}
public ExpressionEvaluatingMessageGroupProcessor(String expression) {
super(expression);
processor = new ExpressionEvaluatingMessageListProcessor(expression);
}
/**
* Evaluate the expression provided on the unmarked messages (a collection) in the group, and delegate to the
* {@link MessagingTemplate} to send dowstream.
*/
public void processAndSend(MessageGroup group, MessagingTemplate messagingTemplate, MessageChannel outputChannel) {
Object newPayload = process(group.getUnmarked());
messagingTemplate.send(outputChannel, MessageBuilder.withPayload(newPayload).build());
@Override
protected Object aggregatePayloads(MessageGroup group) {
return processor.process(group.getUnmarked());
}
}

View File

@@ -44,7 +44,7 @@ import org.springframework.integration.transformer.MessageTransformationExceptio
* @author Dave Syer
* @since 2.0
*/
public class AbstractExpressionEvaluatingMessageListProcessor implements BeanFactoryAware {
public class ExpressionEvaluatingMessageListProcessor implements BeanFactoryAware, MessageListProcessor {
private final ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true));
@@ -57,7 +57,7 @@ public class AbstractExpressionEvaluatingMessageListProcessor implements BeanFac
/**
* Create an {@link ExpressionEvaluatingMessageProcessor} for the given expression String.
*/
public AbstractExpressionEvaluatingMessageListProcessor(String expression) {
public ExpressionEvaluatingMessageListProcessor(String expression) {
try {
this.expression = parser.parseExpression(expression);
this.getEvaluationContext().addPropertyAccessor(new MapAccessor());
@@ -116,7 +116,7 @@ public class AbstractExpressionEvaluatingMessageListProcessor implements BeanFac
* Processes the Message by evaluating the expression with that Message as the root object. The expression
* evaluation result Object will be returned.
*/
protected Object process(Collection<? extends Message<?>> messages) {
public Object process(Collection<? extends Message<?>> messages) {
return this.evaluateExpression(this.expression, messages, this.expectedType);
}

View File

@@ -23,7 +23,7 @@ import org.springframework.integration.store.MessageGroup;
*
* @author Dave Syer
*/
public class ExpressionEvaluatingReleaseStrategy extends AbstractExpressionEvaluatingMessageListProcessor implements
public class ExpressionEvaluatingReleaseStrategy extends ExpressionEvaluatingMessageListProcessor implements
ReleaseStrategy {
public ExpressionEvaluatingReleaseStrategy(String expression) {

View File

@@ -1,160 +0,0 @@
/*
* Copyright 2002-2010 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.springframework.integration.aggregator;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import org.springframework.aop.support.AopUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.integration.annotation.Header;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
/**
* Convenience helper that looks for an appropriate method handling a list of messages and returns null if not found.
*
* @author Dave Syer
*
* @since 2.0
*/
public class MessageListMethodAdapterHelper {
public MessageListProcessor getAdapter(Object candidate, Class<? extends Annotation> annotationType) {
Method method = findAggregatorMethod(candidate, annotationType);
if (method == null) {
return null;
}
return new MessageListMethodAdapter(candidate, method);
}
public Method findAggregatorMethod(Object candidate, Class<? extends Annotation> annotationType) {
Class<?> targetClass = AopUtils.getTargetClass(candidate);
if (targetClass == null) {
targetClass = candidate.getClass();
}
Method method = this.findAnnotatedMethod(targetClass, annotationType);
if (method == null) {
method = this.findSinglePublicMethod(targetClass);
}
return method;
}
private Method findAnnotatedMethod(final Class<?> targetClass, final Class<? extends Annotation> annotationType) {
final AtomicReference<Method> annotatedMethod = new AtomicReference<Method>();
ReflectionUtils.doWithMethods(targetClass, new ReflectionUtils.MethodCallback() {
public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
Annotation annotation = AnnotationUtils.findAnnotation(method, annotationType);
if (annotation != null) {
Assert.isNull(annotatedMethod.get(), "found more than one method on target class [" + targetClass
+ "] with the annotation type [" + annotationType.getName() + "]");
annotatedMethod.set(method);
}
}
});
return annotatedMethod.get();
}
private Method findSinglePublicMethod(Class<?> targetClass) {
Set<Method> methods = new HashSet<Method>();
for (Method method : targetClass.getMethods()) {
if (!method.getDeclaringClass().equals(Object.class)) {
methods.add(method);
}
}
removeListIncompatibleMethodsFrom(methods);
removeVoidMethodsFrom(methods);
removeUnfittingFrom(methods);
if (methods.size() > 1) {
throw new IllegalArgumentException("Class [" + targetClass + "] contains more than one public Method.");
}
return methods.isEmpty() ? null : methods.iterator().next();
}
private void removeListIncompatibleMethodsFrom(Set<Method> candidates) {
removeMethodsMatchingSelector(candidates, new MethodSelector() {
public boolean select(Method method) {
int found = 0;
for (Class<?> parameterClass : method.getParameterTypes()) {
if (Collection.class.isAssignableFrom(parameterClass)) {
found++;
}
}
return found != 1;
}
});
}
private void removeVoidMethodsFrom(Set<Method> candidates) {
removeMethodsMatchingSelector(candidates, new MethodSelector() {
public boolean select(Method method) {
return method.getReturnType().getName().equals("void");
}
});
}
private Set<Method> removeUnfittingFrom(Set<Method> candidates) {
return removeMethodsMatchingSelector(candidates, new MethodSelector() {
public boolean select(Method method) {
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
Class<?>[] parameterTypes = method.getParameterTypes();
return (!isFittinglyAnnotated(parameterTypes, parameterAnnotations));
}
});
}
private boolean isFittinglyAnnotated(Class<?>[] parameterTypes, Annotation[][] parameterAnnotations) {
int candidateParametersFound = 0;
for (int i = 0; i < parameterTypes.length; i++) {
Class<?> parameterType = parameterTypes[i];
if (Collection.class.isAssignableFrom(parameterType)) {
boolean headerAnnotationFound = false;
for (Annotation annotation : parameterAnnotations[i]) {
if (annotation instanceof Header) {
headerAnnotationFound = true;
}
}
if (!headerAnnotationFound) {
candidateParametersFound++;
}
}
}
return candidateParametersFound == 1;
}
private Set<Method> removeMethodsMatchingSelector(Set<Method> candidates, MethodSelector selector) {
Set<Method> removed = new HashSet<Method>();
Iterator<Method> iterator = candidates.iterator();
while (iterator.hasNext()) {
Method method = iterator.next();
if (selector.select(method)) {
iterator.remove();
removed.add(method);
}
}
return removed;
}
private interface MethodSelector {
boolean select(Method method);
}
}

View File

@@ -20,7 +20,7 @@ import java.util.Collection;
import org.springframework.integration.Message;
/**
* @author dsyer
* @author Dave Syer
*
*/
public interface MessageListProcessor {

View File

@@ -43,7 +43,7 @@ public class MethodInvokingMessageGroupProcessor extends AbstractAggregatingMess
* @param target the object to wrap
*/
public MethodInvokingMessageGroupProcessor(Object target) {
this.adapter = new MessageListMethodAdapterHelper().getAdapter(target, Aggregator.class);
this.adapter = getAdapter(target, Aggregator.class);
Assert.notNull(this.adapter, "No aggregator method could be found for object of type: "+target.getClass());
}
@@ -55,7 +55,7 @@ public class MethodInvokingMessageGroupProcessor extends AbstractAggregatingMess
* @param methodName the name of the method to invoke
*/
public MethodInvokingMessageGroupProcessor(Object target, String methodName) {
this.adapter = new MessageListMethodAdapter(target, methodName);
this.adapter = new MethodInvokingMessageListProcessor(target, methodName);
}
/**
@@ -65,7 +65,7 @@ public class MethodInvokingMessageGroupProcessor extends AbstractAggregatingMess
* @param method the method to invoke
*/
public MethodInvokingMessageGroupProcessor(Object target, Method method) {
this.adapter = new MessageListMethodAdapter(target, method);
this.adapter = new MethodInvokingMessageListProcessor(target, method);
}
@Override

View File

@@ -38,13 +38,13 @@ import org.springframework.util.ReflectionUtils;
* @author Iwein Fuld
* @author Dave Syer
*/
public class MessageListMethodAdapter implements MessageListProcessor {
public class MethodInvokingMessageListProcessor implements MessageListProcessor {
private final DefaultMethodInvoker invoker;
protected final Method method;
public MessageListMethodAdapter(Object object, String methodName) {
public MethodInvokingMessageListProcessor(Object object, String methodName) {
Assert.notNull(object, "'object' must not be null");
Assert.notNull(methodName, "'methodName' must not be null");
this.method = ReflectionUtils.findMethod(object.getClass(), methodName, new Class<?>[]{List.class});
@@ -53,7 +53,7 @@ public class MessageListMethodAdapter implements MessageListProcessor {
this.invoker = new DefaultMethodInvoker(object, this.method);
}
public MessageListMethodAdapter(Object object, Method method) {
public MethodInvokingMessageListProcessor(Object object, Method method) {
Assert.notNull(object, "'object' must not be null");
Assert.notNull(method, "'method' must not be null");
Assert.isTrue(method.getParameterTypes().length == 1

View File

@@ -32,15 +32,15 @@ import org.springframework.util.Assert;
*/
public class MethodInvokingReleaseStrategy implements ReleaseStrategy {
private final MessageListMethodAdapter adapter;
private final MethodInvokingMessageListProcessor adapter;
public MethodInvokingReleaseStrategy(Object object, Method method) {
adapter = new MessageListMethodAdapter(object, method);
adapter = new MethodInvokingMessageListProcessor(object, method);
this.assertMethodReturnsBoolean();
}
public MethodInvokingReleaseStrategy(Object object, String methodName) {
adapter = new MessageListMethodAdapter(object, methodName);
adapter = new MethodInvokingMessageListProcessor(object, methodName);
this.assertMethodReturnsBoolean();
}

View File

@@ -8,10 +8,12 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.hamcrest.CoreMatchers;
import org.hamcrest.Description;
import org.hamcrest.Factory;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
import org.hamcrest.collection.IsMapContaining;
import org.hamcrest.core.IsEqual;
import org.junit.Before;
import org.junit.Test;
@@ -20,7 +22,7 @@ import org.mockito.Matchers;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.integration.Message;
import org.springframework.integration.core.GenericMessage;
import org.springframework.integration.core.MessageBuilder;
import org.springframework.integration.core.MessageChannel;
import org.springframework.integration.core.MessagingTemplate;
import org.springframework.integration.store.MessageGroup;
@@ -45,11 +47,10 @@ public class ExpressionEvaluatingMessageGroupProcessorTests {
List<Message<?>> messages = new ArrayList<Message<?>>();
@Before
@SuppressWarnings("unchecked")
public void setup() {
messages.clear();
for (int i = 0; i < 5; i++) {
messages.add(new GenericMessage(i + 1));
messages.add(MessageBuilder.withPayload(i + 1).setHeader("foo", "bar").build());
}
}
@@ -61,6 +62,14 @@ public class ExpressionEvaluatingMessageGroupProcessorTests {
verify(outputChannel).send(messageWithPayload(5));
}
@Test
public void testProcessAndCheckHeaders() throws Exception {
when(group.getUnmarked()).thenReturn(messages);
processor = new ExpressionEvaluatingMessageGroupProcessor("#root");
processor.processAndSend(group, template, outputChannel);
verify(outputChannel).send(messageWithHeader("foo", "bar"));
}
@Test
public void testProcessAndSendWithProjectionExpressionEvaluated() throws Exception {
when(group.getUnmarked()).thenReturn(messages);
@@ -86,12 +95,16 @@ public class ExpressionEvaluatingMessageGroupProcessorTests {
verify(outputChannel).send(messageWithPayload(3 + 4 + 5));
}
private Message<?> messageWithHeader(String key, Object value) {
return Matchers.argThat(MessageMatcher.hasHeader(IsMapContaining.hasEntry(key, value)));
}
private Message<?> messageWithPayload(Matcher<?> matcher) {
return Matchers.argThat(PayloadMatcher.hasPayload(matcher));
return Matchers.argThat(MessageMatcher.hasPayload(matcher));
}
private Message<?> messageWithPayload(int i) {
return Matchers.argThat(PayloadMatcher.hasPayload(IsEqual.equalTo(i)));
return Matchers.argThat(MessageMatcher.hasPayload(IsEqual.equalTo(i)));
}
/*
@@ -105,16 +118,19 @@ public class ExpressionEvaluatingMessageGroupProcessorTests {
return result;
}
private static class PayloadMatcher extends TypeSafeMatcher<Message<?>> {
private static class MessageMatcher extends TypeSafeMatcher<Message<?>> {
private final Matcher<?> matcher;
private final Matcher<?> payloadMatcher;
private final Matcher<?> headerMatcher;
/**
* @param matcher
*/
PayloadMatcher(Matcher<?> matcher) {
MessageMatcher(Matcher<?> matcher, Matcher<?> headerMatcher) {
super();
this.matcher = matcher;
this.payloadMatcher = matcher;
this.headerMatcher = headerMatcher;
}
/**
@@ -122,7 +138,7 @@ public class ExpressionEvaluatingMessageGroupProcessorTests {
*/
@Override
public boolean matchesSafely(Message<?> message) {
return matcher.matches(message.getPayload());
return payloadMatcher.matches(message.getPayload()) && headerMatcher.matches(message.getHeaders());
}
/**
@@ -130,13 +146,18 @@ public class ExpressionEvaluatingMessageGroupProcessorTests {
*/
//@Override
public void describeTo(Description description) {
description.appendText("a Message with payload: ").appendDescriptionOf(matcher);
description.appendText("a Message with payload: ").appendDescriptionOf(payloadMatcher);
description.appendText(" and headers: ").appendDescriptionOf(headerMatcher);
}
@Factory
public static <T> Matcher<Message<?>> hasPayload(Matcher<T> payloadMatcher) {
return new PayloadMatcher(payloadMatcher);
return new MessageMatcher(payloadMatcher, CoreMatchers.anything());
}
@Factory
public static <T> Matcher<Message<?>> hasHeader(Matcher<T> headerMatcher) {
return new MessageMatcher(CoreMatchers.anything(), headerMatcher);
}
}

View File

@@ -38,7 +38,7 @@ import org.springframework.integration.MessageHandlingException;
import org.springframework.integration.MessageRejectedException;
import org.springframework.integration.aggregator.CorrelatingMessageHandler;
import org.springframework.integration.aggregator.CorrelationStrategy;
import org.springframework.integration.aggregator.MessageListMethodAdapter;
import org.springframework.integration.aggregator.MethodInvokingMessageListProcessor;
import org.springframework.integration.aggregator.ReleaseStrategy;
import org.springframework.integration.aggregator.MethodInvokingReleaseStrategy;
import org.springframework.integration.core.MessageBuilder;
@@ -114,7 +114,7 @@ public class AggregatorParserTests {
DirectFieldAccessor accessor = new DirectFieldAccessor(consumer);
Method expectedMethod = TestAggregatorBean.class.getMethod("createSingleMessageFromGroup", List.class);
assertEquals("The MethodInvokingAggregator is not injected with the appropriate aggregation method",
expectedMethod, ((MessageListMethodAdapter) new DirectFieldAccessor(accessor
expectedMethod, ((MethodInvokingMessageListProcessor) new DirectFieldAccessor(accessor
.getPropertyValue("outputProcessor")).getPropertyValue("adapter")).getMethod());
assertEquals("The AggregatorEndpoint is not injected with the appropriate ReleaseStrategy instance",
releaseStrategy, accessor.getPropertyValue("releaseStrategy"));

View File

@@ -1,6 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project-modules id="moduleCoreId" project-version="1.5.0">
<wb-module deploy-name="loanshark">
<dependent-module deploy-path="/WEB-INF/lib" handle="module:/resource/spring-integration-ip/spring-integration-ip">
<dependency-type>uses</dependency-type>
</dependent-module>
<dependent-module deploy-path="/WEB-INF/lib" handle="module:/resource/spring-integration-stream/spring-integration-stream">
<dependency-type>uses</dependency-type>
</dependent-module>
<property name="context-root" value="loanshark"/>
<wb-resource deploy-path="/" source-path="src/main/webapp"/>
<property name="java-output-path" value="/target/classes"/>