IN PROGRESS - issue INT-656: MethodParameterMessageMapper - javadoc vs code
http://jira.springframework.org/browse/INT-656 Made unannotated Properties and Map work, refactored MethodParameterMessageMapper a bit
This commit is contained in:
@@ -18,9 +18,11 @@ package org.springframework.integration.message;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.core.GenericTypeResolver;
|
||||
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
|
||||
@@ -36,41 +38,80 @@ import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Prepares arguments for handler methods. The method parameters are matched
|
||||
* against the Message payload as well as its headers. If a method parameter
|
||||
* is annotated with {@link Header @Header}, the annotation's value will be
|
||||
* used as a header name. If such an annotation contains no value, then the
|
||||
* parameter name will be used as long as the information is available in the
|
||||
* class file (requires compilation with debug settings for parameter names).
|
||||
* If the {@link Header @Header} annotation is not present, then the parameter
|
||||
* will typically match the Message payload. However, if a Map or Properties
|
||||
* object is expected, and the paylaod is not itself assignable to that type,
|
||||
* then the MessageHeaders' values will be passed in the case of a Map-typed
|
||||
* parameter, or the MessageHeaders' String-based values will be passed in the
|
||||
* case of a Properties-typed parameter.
|
||||
* against the Message payload as well as its headers. If a method parameter is
|
||||
* annotated with {@link Header @Header}, the annotation's value will be used as
|
||||
* a header name. If such an annotation contains no value, then the parameter
|
||||
* name will be used as long as the information is available in the class file
|
||||
* (requires compilation with debug settings for parameter names). If the
|
||||
* {@link Header @Header} annotation is not present, then the parameter will
|
||||
* typically match the Message payload. However, if a Map or Properties object
|
||||
* is expected, and the paylaod is not itself assignable to that type, then the
|
||||
* MessageHeaders' values will be passed in the case of a Map-typed parameter,
|
||||
* or the MessageHeaders' String-based values will be passed in the case of a
|
||||
* Properties-typed parameter.
|
||||
*
|
||||
* @author Mark Fisher
|
||||
* @author Iwein Fuld
|
||||
*/
|
||||
public class MethodParameterMessageMapper implements InboundMessageMapper<Object[]>, OutboundMessageMapper<Object[]> {
|
||||
|
||||
private final Method method;
|
||||
|
||||
private volatile MethodParameterMetadata[] parameterMetadata;
|
||||
private final MethodParameterMetadata[] parameterMetadata;
|
||||
|
||||
private final MethodParameterMetadata payloadParameterMetadata;
|
||||
|
||||
private final ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
|
||||
|
||||
|
||||
public MethodParameterMessageMapper(Method method) {
|
||||
Assert.notNull(method, "method must not be null");
|
||||
this.method = method;
|
||||
this.initializeParameterMetadata();
|
||||
parameterMetadata = this.initializeParameterMetadata();
|
||||
payloadParameterMetadata = getPayloadParameterFrom(parameterMetadata);
|
||||
}
|
||||
|
||||
private MethodParameterMetadata getPayloadParameterFrom(MethodParameterMetadata[] parameterMetadata) {
|
||||
Set<MethodParameterMetadata> payloadCandidates = new HashSet<MethodParameterMetadata>();
|
||||
int messagesFound = 0;
|
||||
for (MethodParameterMetadata metadata : parameterMetadata) {
|
||||
if (metadata.getHeaderAnnotation() == null && !metadata.hasHeadersAnnotation()
|
||||
&& !Message.class.isAssignableFrom(metadata.getParameterType())) {
|
||||
payloadCandidates.add(metadata);
|
||||
} else if (Message.class.isAssignableFrom(metadata.getParameterType())){
|
||||
messagesFound++;
|
||||
}
|
||||
}
|
||||
if (payloadCandidates.size() > 1) {
|
||||
Iterator<MethodParameterMetadata> iterator = payloadCandidates.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Class<?> type = iterator.next().getParameterType();
|
||||
if (Map.class.isAssignableFrom(type) || Properties.class.isAssignableFrom(type)) {
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
Assert.isTrue(payloadCandidates.size()+messagesFound <= 1, "Could not find at most one message or payload parameter among ["
|
||||
+ parameterMetadata + "] ended up with the candidates [" + payloadCandidates + "]");
|
||||
MethodParameterMetadata methodParameterMetadata = payloadCandidates.toArray(new MethodParameterMetadata[1])[0];
|
||||
return methodParameterMetadata;
|
||||
}
|
||||
|
||||
private MethodParameterMetadata[] initializeParameterMetadata() {
|
||||
Class<?>[] paramTypes = this.method.getParameterTypes();
|
||||
MethodParameterMetadata[] parameterMetadata = new MethodParameterMetadata[paramTypes.length];
|
||||
for (int i = 0; i < paramTypes.length; i++) {
|
||||
MethodParameterMetadata metadata = new MethodParameterMetadata(this.method, i);
|
||||
metadata.initParameterNameDiscovery(this.parameterNameDiscoverer);
|
||||
GenericTypeResolver.resolveParameterType(metadata, this.method.getDeclaringClass());
|
||||
parameterMetadata[i] = metadata;
|
||||
}
|
||||
return parameterMetadata;
|
||||
}
|
||||
|
||||
public Message<?> toMessage(Object[] parameters) {
|
||||
Assert.isTrue(!ObjectUtils.isEmpty(parameters), "parameter array is required");
|
||||
Assert.isTrue(parameters.length == this.parameterMetadata.length,
|
||||
"wrong number of parameters: expected " + this.parameterMetadata.length
|
||||
+ ", received " + parameters.length);
|
||||
Assert.isTrue(parameters.length == this.parameterMetadata.length, "wrong number of parameters: expected "
|
||||
+ this.parameterMetadata.length + ", received " + parameters.length);
|
||||
Message<?> message = null;
|
||||
Object payload = null;
|
||||
Map<String, Object> headers = new HashMap<String, Object>();
|
||||
@@ -78,7 +119,11 @@ public class MethodParameterMessageMapper implements InboundMessageMapper<Object
|
||||
Object value = parameters[i];
|
||||
MethodParameterMetadata metadata = this.parameterMetadata[i];
|
||||
Header headerAnnotation = metadata.getHeaderAnnotation();
|
||||
if (headerAnnotation != null) {
|
||||
if (metadata == payloadParameterMetadata) {
|
||||
Assert.notNull(value, "payload object must not be null");
|
||||
payload = value;
|
||||
}
|
||||
else if (headerAnnotation != null) {
|
||||
String headerName = metadata.getHeaderName();
|
||||
boolean required = headerAnnotation.required();
|
||||
if (value != null) {
|
||||
@@ -97,8 +142,8 @@ public class MethodParameterMessageMapper implements InboundMessageMapper<Object
|
||||
message = (Message<?>) value;
|
||||
}
|
||||
else {
|
||||
Assert.notNull(value, "payload object must not be null");
|
||||
payload = value;
|
||||
Assert.isTrue(metadata.isMapOrProperties());
|
||||
this.addHeadersAnnotatedParameterToMap(value, headers);
|
||||
}
|
||||
}
|
||||
if (message != null) {
|
||||
@@ -121,51 +166,35 @@ public class MethodParameterMessageMapper implements InboundMessageMapper<Object
|
||||
MethodParameterMetadata metadata = this.parameterMetadata[i];
|
||||
Class<?> expectedType = metadata.getParameterType();
|
||||
Header headerAnnotation = metadata.getHeaderAnnotation();
|
||||
if (headerAnnotation != null) {
|
||||
if (metadata == payloadParameterMetadata) {
|
||||
args[i] = message.getPayload();
|
||||
}
|
||||
else if (headerAnnotation != null) {
|
||||
String headerName = metadata.getHeaderName();
|
||||
Object value = message.getHeaders().get(headerName);
|
||||
if (value == null && headerAnnotation.required()) {
|
||||
throw new MessageHandlingException(message,
|
||||
"required header '" + headerName + "' not available");
|
||||
throw new MessageHandlingException(message, "required header '" + headerName + "' not available");
|
||||
}
|
||||
args[i] = value;
|
||||
}
|
||||
else if (metadata.hasHeadersAnnotation()) {
|
||||
else if (metadata.isMapOrProperties()) {
|
||||
if (Properties.class.isAssignableFrom(expectedType)) {
|
||||
args[i] = this.getStringTypedHeaders(message);
|
||||
args[i] = this.getStringTypedHeaders(message);
|
||||
}
|
||||
else {
|
||||
args[i] = message.getHeaders();
|
||||
args[i] = message.getHeaders();
|
||||
}
|
||||
}
|
||||
else if (expectedType.isAssignableFrom(message.getClass())
|
||||
&& Message.class.isAssignableFrom(expectedType)) {
|
||||
args[i] = message;
|
||||
}
|
||||
else {
|
||||
args[i] = message.getPayload();
|
||||
Assert.isTrue(expectedType.isAssignableFrom(message.getClass())
|
||||
&& Message.class.isAssignableFrom(expectedType),
|
||||
"Argument is neither header or payload, so it should be of type message.");
|
||||
args[i] = message;
|
||||
}
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
private void initializeParameterMetadata() {
|
||||
boolean foundMessageOrPayload = false;
|
||||
Class<?>[] paramTypes = this.method.getParameterTypes();
|
||||
this.parameterMetadata = new MethodParameterMetadata[paramTypes.length];
|
||||
for (int i = 0; i < parameterMetadata.length; i++) {
|
||||
MethodParameterMetadata metadata = new MethodParameterMetadata(this.method, i);
|
||||
metadata.initParameterNameDiscovery(this.parameterNameDiscoverer);
|
||||
GenericTypeResolver.resolveParameterType(metadata, this.method.getDeclaringClass());
|
||||
if (metadata.getHeaderAnnotation() == null && !metadata.hasHeadersAnnotation()) {
|
||||
// this is either a Message or the Object to be used as a Message payload
|
||||
Assert.isTrue(!foundMessageOrPayload, "only one Message or payload parameter is allowed");
|
||||
foundMessageOrPayload = true;
|
||||
}
|
||||
parameterMetadata[i] = metadata;
|
||||
}
|
||||
}
|
||||
|
||||
private Properties getStringTypedHeaders(Message<?> message) {
|
||||
Properties properties = new Properties();
|
||||
MessageHeaders headers = message.getHeaders();
|
||||
@@ -183,38 +212,45 @@ public class MethodParameterMessageMapper implements InboundMessageMapper<Object
|
||||
Map map = (Map) value;
|
||||
for (Iterator iter = map.entrySet().iterator(); iter.hasNext();) {
|
||||
Map.Entry entry = (Map.Entry) iter.next();
|
||||
Assert.isTrue(entry.getKey() instanceof String,
|
||||
"Map annotated with @Headers must have String-typed keys");
|
||||
Assert.isTrue(entry.getKey() instanceof String, "Map annotated with @Headers must have String-typed keys");
|
||||
headers.put((String) entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class MethodParameterMetadata extends MethodParameter {
|
||||
|
||||
private volatile Header _headerAnnotation;
|
||||
|
||||
private volatile boolean _hasHeadersAnnotation;
|
||||
|
||||
|
||||
private MethodParameterMetadata(Method method, int index) {
|
||||
super(method, index);
|
||||
}
|
||||
|
||||
Header getHeaderAnnotation() {
|
||||
if (this._headerAnnotation != null) {
|
||||
return this._headerAnnotation;
|
||||
}
|
||||
if (this.getParameterAnnotations() == null) {
|
||||
return null;
|
||||
}
|
||||
for (Object o : this.getParameterAnnotations()) {
|
||||
if (o instanceof Header) {
|
||||
this._headerAnnotation = (Header) o;
|
||||
return this._headerAnnotation;
|
||||
}
|
||||
else if (Headers.class.isInstance(o)) {
|
||||
Assert.isAssignable(Map.class, this.getParameterType(),
|
||||
"parameter with the @Headers annotation must be assignable to java.util.Map");
|
||||
this._hasHeadersAnnotation = true;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Header getHeaderAnnotation() {
|
||||
return this._headerAnnotation;
|
||||
}
|
||||
|
||||
boolean hasHeadersAnnotation() {
|
||||
return this._hasHeadersAnnotation;
|
||||
}
|
||||
|
||||
boolean isMapOrProperties() {
|
||||
if (Properties.class.isAssignableFrom(this.getParameterType())
|
||||
|| Map.class.isAssignableFrom(this.getParameterType())) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
String getHeaderName() {
|
||||
@@ -229,24 +265,5 @@ public class MethodParameterMessageMapper implements InboundMessageMapper<Object
|
||||
}
|
||||
return paramName;
|
||||
}
|
||||
|
||||
boolean hasHeadersAnnotation() {
|
||||
if (this._hasHeadersAnnotation) {
|
||||
return true;
|
||||
}
|
||||
if (this.getParameterAnnotations() == null) {
|
||||
return false;
|
||||
}
|
||||
for (Object o : this.getParameterAnnotations()) {
|
||||
if (Headers.class.isInstance(o)) {
|
||||
Assert.isAssignable(Map.class, this.getParameterType(),
|
||||
"parameter with the @Headers annotation must be assignable to java.util.Map");
|
||||
this._hasHeadersAnnotation = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ public class MethodInvokingMessageHandlerTests {
|
||||
endpoint.setTrigger(new IntervalTrigger(10));
|
||||
context.registerEndpoint("testEndpoint", endpoint);
|
||||
context.refresh();
|
||||
String result = queue.poll(1000, TimeUnit.MILLISECONDS);
|
||||
String result = queue.poll(2000, TimeUnit.MILLISECONDS);
|
||||
assertNotNull(result);
|
||||
assertEquals("testing", result);
|
||||
context.stop();
|
||||
|
||||
@@ -16,8 +16,7 @@
|
||||
|
||||
package org.springframework.integration.message;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashMap;
|
||||
@@ -32,6 +31,7 @@ import org.springframework.integration.core.Message;
|
||||
|
||||
/**
|
||||
* @author Mark Fisher
|
||||
* @author Iwein Fuld
|
||||
*/
|
||||
public class MethodParameterMessageMapperFromMessageTests {
|
||||
|
||||
@@ -109,6 +109,36 @@ public class MethodParameterMessageMapperFromMessageTests {
|
||||
assertEquals("foo", result.getProperty("prop1"));
|
||||
assertEquals("bar", result.getProperty("prop2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fromMessageWithPropertiesAndObjectMethod() throws Exception {
|
||||
Method method = TestService.class.getMethod("propertiesHeadersAndPayload", Properties.class, Object.class);
|
||||
MethodParameterMessageMapper mapper = new MethodParameterMessageMapper(method);
|
||||
Message<String> message = MessageBuilder.withPayload("test")
|
||||
.setHeader("prop1", "foo").setHeader("prop2", "bar").build();
|
||||
Object[] args = mapper.fromMessage(message);
|
||||
Properties result = (Properties) args[0];
|
||||
assertEquals(2, result.size());
|
||||
assertEquals("foo", result.getProperty("prop1"));
|
||||
assertEquals("bar", result.getProperty("prop2"));
|
||||
assertEquals("test", args[1]);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void fromMessageWithMapAndObjectMethod() throws Exception {
|
||||
Method method = TestService.class.getMethod("mapHeadersAndPayload", Map.class, Object.class);
|
||||
MethodParameterMessageMapper mapper = new MethodParameterMessageMapper(method);
|
||||
Message<String> message = MessageBuilder.withPayload("test")
|
||||
.setHeader("prop1", "foo").setHeader("prop2", "bar").build();
|
||||
Object[] args = mapper.fromMessage(message);
|
||||
Map result = (Map) args[0];
|
||||
//Map also contains id and timestamp
|
||||
assertEquals(4, result.size());
|
||||
assertEquals("foo", result.get("prop1"));
|
||||
assertEquals("bar", result.get("prop2"));
|
||||
assertEquals("test", args[1]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fromMessageWithPropertiesMethodAndPropertiesPayload() throws Exception {
|
||||
@@ -192,6 +222,10 @@ public class MethodParameterMessageMapperFromMessageTests {
|
||||
public Properties propertiesHeaders(@Headers Properties properties) {
|
||||
return properties;
|
||||
}
|
||||
|
||||
public Object propertiesHeadersAndPayload(Properties headers, Object payload) {
|
||||
return payload;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map mapPayload(Map map) {
|
||||
@@ -202,11 +236,14 @@ public class MethodParameterMessageMapperFromMessageTests {
|
||||
public Map mapHeaders(@Headers Map map) {
|
||||
return map;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public Object mapHeadersAndPayload(Map headers, Object payload) {
|
||||
return payload;
|
||||
}
|
||||
|
||||
public Integer integerMethod(Integer i) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user