From c17bddc34f7dc456b29f118718aaf6888184cb0b Mon Sep 17 00:00:00 2001 From: David Syer Date: Wed, 4 Aug 2010 20:20:02 +0000 Subject: [PATCH] INT-1281: BeanFactoryTypeConverter gives access in SpEL expressions to standard BeanFactory type conversion --- .../handler/AbstractMessageProcessor.java | 20 +++- .../ExpressionEvaluatingMessageProcessor.java | 1 + .../util/BeanFactoryTypeConverter.java | 103 ++++++++++++++++++ ...essionEvaluatingMessageProcessorTests.java | 45 +++++++- .../MethodInvokingMessageProcessorTests.java | 47 +++++++- 5 files changed, 204 insertions(+), 12 deletions(-) create mode 100644 spring-integration-core/src/main/java/org/springframework/integration/util/BeanFactoryTypeConverter.java diff --git a/spring-integration-core/src/main/java/org/springframework/integration/handler/AbstractMessageProcessor.java b/spring-integration-core/src/main/java/org/springframework/integration/handler/AbstractMessageProcessor.java index e1e55393f2..862f71fffc 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/handler/AbstractMessageProcessor.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/handler/AbstractMessageProcessor.java @@ -16,26 +16,38 @@ package org.springframework.integration.handler; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.core.convert.ConversionService; import org.springframework.expression.EvaluationException; import org.springframework.expression.Expression; import org.springframework.expression.spel.support.StandardEvaluationContext; -import org.springframework.expression.spel.support.StandardTypeConverter; import org.springframework.integration.Message; import org.springframework.integration.MessageHandlingException; +import org.springframework.integration.util.BeanFactoryTypeConverter; /** * @author Mark Fisher * @since 2.0 */ -public abstract class AbstractMessageProcessor implements MessageProcessor { +public abstract class AbstractMessageProcessor implements MessageProcessor, BeanFactoryAware { private final StandardEvaluationContext evaluationContext = new StandardEvaluationContext(); - + + private final BeanFactoryTypeConverter typeConverter = new BeanFactoryTypeConverter(); + + public AbstractMessageProcessor() { + evaluationContext.setTypeConverter(typeConverter); + } + + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + typeConverter.setBeanFactory(beanFactory); + } public void setConversionService(ConversionService conversionService) { if (conversionService != null) { - this.evaluationContext.setTypeConverter(new StandardTypeConverter(conversionService)); + typeConverter.setConversionService(conversionService); } } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/handler/ExpressionEvaluatingMessageProcessor.java b/spring-integration-core/src/main/java/org/springframework/integration/handler/ExpressionEvaluatingMessageProcessor.java index a52d792846..689e70df2c 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/handler/ExpressionEvaluatingMessageProcessor.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/handler/ExpressionEvaluatingMessageProcessor.java @@ -70,6 +70,7 @@ public class ExpressionEvaluatingMessageProcessor extends AbstractMessageProcess * Specify a BeanFactory in order to enable resolution via @beanName in the expression. */ public void setBeanFactory(final BeanFactory beanFactory) { + super.setBeanFactory(beanFactory); if (beanFactory != null) { this.getEvaluationContext().setBeanResolver(new SimpleBeanResolver(beanFactory)); } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/util/BeanFactoryTypeConverter.java b/spring-integration-core/src/main/java/org/springframework/integration/util/BeanFactoryTypeConverter.java new file mode 100644 index 0000000000..8ef4817b53 --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/util/BeanFactoryTypeConverter.java @@ -0,0 +1,103 @@ +/* + * Copyright 2002-2008 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.util; + +import java.beans.PropertyEditor; + +import org.springframework.beans.BeansException; +import org.springframework.beans.SimpleTypeConverter; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.support.ConversionServiceFactory; +import org.springframework.expression.TypeConverter; + +public class BeanFactoryTypeConverter implements TypeConverter, BeanFactoryAware { + + private SimpleTypeConverter delegate = new SimpleTypeConverter(); + + private static ConversionService defaultConversionService; + + private ConversionService conversionService; + + public BeanFactoryTypeConverter() { + synchronized (this) { + if (defaultConversionService == null) { + defaultConversionService = ConversionServiceFactory.createDefaultConversionService(); + } + } + this.conversionService = defaultConversionService; + } + + public BeanFactoryTypeConverter(ConversionService conversionService) { + this.conversionService = conversionService; + } + + public void setConversionService(ConversionService conversionService) { + this.conversionService = conversionService; + } + + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + if (beanFactory instanceof ConfigurableBeanFactory) { + Object typeConverter = ((ConfigurableBeanFactory) beanFactory).getTypeConverter(); + if (typeConverter instanceof SimpleTypeConverter) { + delegate = (SimpleTypeConverter) typeConverter; + } + } + } + + public boolean canConvert(Class sourceType, Class targetType) { + if (conversionService.canConvert(sourceType, targetType)) { + return true; + } + if (!String.class.isAssignableFrom(sourceType) && !String.class.isAssignableFrom(targetType)) { + // PropertyEditor cannot convert non-Strings + return false; + } + if (!String.class.isAssignableFrom(sourceType)) { + return delegate.findCustomEditor(sourceType, null) != null || delegate.getDefaultEditor(sourceType) != null; + } + return delegate.findCustomEditor(targetType, null) != null || delegate.getDefaultEditor(targetType) != null; + } + + public boolean canConvert(TypeDescriptor sourceTypeDescriptor, TypeDescriptor targetTypeDescriptor) { + if (conversionService.canConvert(sourceTypeDescriptor, targetTypeDescriptor)) { + return true; + } + // TODO: what does this mean? This method is not used in SpEL so probably ignorable? + Class sourceType = sourceTypeDescriptor.getObjectType(); + Class targetType = targetTypeDescriptor.getObjectType(); + return canConvert(sourceType, targetType); + } + + public Object convertValue(Object value, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (targetType.getType() == Void.class || targetType.getType() == Void.TYPE) { + return null; + } + if (conversionService.canConvert(sourceType, targetType)) { + return conversionService.convert(value, sourceType, targetType); + } + if (!String.class.isAssignableFrom(sourceType.getType())) { + PropertyEditor editor = delegate.findCustomEditor(sourceType.getType(), null); + editor.setValue(value); + return editor.getAsText(); + } + return delegate.convertIfNecessary(value, targetType.getType()); + } + +} \ No newline at end of file diff --git a/spring-integration-core/src/test/java/org/springframework/integration/handler/ExpressionEvaluatingMessageProcessorTests.java b/spring-integration-core/src/test/java/org/springframework/integration/handler/ExpressionEvaluatingMessageProcessorTests.java index c27aa97183..af43d78bcc 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/handler/ExpressionEvaluatingMessageProcessorTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/handler/ExpressionEvaluatingMessageProcessorTests.java @@ -14,6 +14,9 @@ package org.springframework.integration.handler; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -22,10 +25,10 @@ import org.junit.Rule; import org.junit.Test; import org.junit.internal.matchers.TypeSafeMatcher; import org.junit.rules.ExpectedException; - import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.support.StaticApplicationContext; +import org.springframework.core.io.Resource; import org.springframework.expression.EvaluationException; import org.springframework.integration.core.GenericMessage; import org.springframework.integration.core.StringMessage; @@ -50,6 +53,28 @@ public class ExpressionEvaluatingMessageProcessorTests { assertEquals("foo", processor.processMessage(new StringMessage("foo"))); } + @Test + public void testProcessMessageWithParameterCoercion() { + ExpressionEvaluatingMessageProcessor processor = new ExpressionEvaluatingMessageProcessor("#target.stringify(payload)"); + processor.getEvaluationContext().setVariable("target", new TestTarget()); + assertEquals("2", processor.processMessage(new StringMessage("2"))); + } + + @Test + public void testProcessMessageWithVoidResult() { + ExpressionEvaluatingMessageProcessor processor = new ExpressionEvaluatingMessageProcessor("#target.ping(payload)"); + processor.getEvaluationContext().setVariable("target", new TestTarget()); + assertEquals(null, processor.processMessage(new StringMessage("2"))); + } + + @Test + public void testProcessMessageWithParameterCoercionToNonPrimitive() { + ExpressionEvaluatingMessageProcessor processor = new ExpressionEvaluatingMessageProcessor("#target.find(payload)"); + processor.getEvaluationContext().setVariable("target", new TestTarget()); + String result = (String) processor.processMessage(new StringMessage("classpath:*.properties")); + assertTrue("Wrong result: "+result, result.contains("log4j.properties")); + } + @Test public void testProcessMessageWithDollarInBrackets() { ExpressionEvaluatingMessageProcessor processor = new ExpressionEvaluatingMessageProcessor("headers['$id']"); @@ -162,6 +187,22 @@ public class ExpressionEvaluatingMessageProcessorTests { } } + @SuppressWarnings("unused") + private static class TestTarget { + + public String stringify(int number) { + return number+""; + } + + public String find(Resource[] resources) { + return Arrays.asList(resources).toString(); + } + + public void ping(String input) { + } + + } + @SuppressWarnings("serial") private static final class CheckedException extends Exception { @@ -169,5 +210,5 @@ public class ExpressionEvaluatingMessageProcessorTests { super(string); } } - + } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/handler/MethodInvokingMessageProcessorTests.java b/spring-integration-core/src/test/java/org/springframework/integration/handler/MethodInvokingMessageProcessorTests.java index de51cc18de..fd102911c6 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/handler/MethodInvokingMessageProcessorTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/handler/MethodInvokingMessageProcessorTests.java @@ -32,7 +32,6 @@ import org.junit.Rule; import org.junit.Test; import org.junit.internal.matchers.TypeSafeMatcher; import org.junit.rules.ExpectedException; -import org.springframework.core.convert.ConversionFailedException; import org.springframework.integration.Message; import org.springframework.integration.MessageHandlingException; import org.springframework.integration.annotation.Header; @@ -150,6 +149,14 @@ public class MethodInvokingMessageProcessorTests { assertEquals("testing-1", result); } + @Test + public void testPayloadCoercedToString() { + MethodInvokingMessageProcessor processor = new MethodInvokingMessageProcessor( + new TestBean(), "acceptPayloadAndReturnObject"); + Object result = processor.processMessage(new GenericMessage(123456789)); + assertEquals("123456789-1", result); + } + @Test public void payloadAsMethodParameterAndMessageAsReturnValue() { MethodInvokingMessageProcessor processor = new MethodInvokingMessageProcessor( @@ -260,7 +267,7 @@ public class MethodInvokingMessageProcessorTests { @Test public void testProcessMessageBadExpression() throws Exception { - expected.expect(new ExceptionCauseMatcher(ConversionFailedException.class)); + expected.expect(new ExceptionCauseMatcher(NumberFormatException.class)); AnnotatedTestService service = new AnnotatedTestService(); Method method = service.getClass().getMethod("integerMethod", Integer.class); MethodInvokingMessageProcessor processor = new MethodInvokingMessageProcessor(service, method); @@ -310,7 +317,7 @@ public class MethodInvokingMessageProcessorTests { @Test public void filterSelectsAnnotationMethodsOnly() { - AmbiguousMethodBean bean = new AmbiguousMethodBean(); + OverloadedMethodBean bean = new OverloadedMethodBean(); MethodInvokingMessageProcessor processor = new MethodInvokingMessageProcessor(bean, ServiceActivator.class); processor.processMessage(MessageBuilder.withPayload(123).build()); assertNotNull(bean.lastArg); @@ -320,7 +327,7 @@ public class MethodInvokingMessageProcessorTests { @Test public void filterSelectsNonVoidReturningMethodsOnly() { - AmbiguousMethodBean bean = new AmbiguousMethodBean(); + OverloadedMethodBean bean = new OverloadedMethodBean(); MethodInvokingMessageProcessor processor = new MethodInvokingMessageProcessor(bean, "foo", true); processor.processMessage(MessageBuilder.withPayload(true).build()); assertNotNull(bean.lastArg); @@ -328,7 +335,16 @@ public class MethodInvokingMessageProcessorTests { assertEquals("true", bean.lastArg); } - + @Test + public void testOverloadedNonVoidReturningMethodsWithExactMatchForType() { + AmbiguousMethodBean bean = new AmbiguousMethodBean(); + MethodInvokingMessageProcessor processor = new MethodInvokingMessageProcessor(bean, "foo", true); + processor.processMessage(MessageBuilder.withPayload("true").build()); + assertNotNull(bean.lastArg); + assertEquals(String.class, bean.lastArg.getClass()); + assertEquals("true", bean.lastArg); + } + private static class ExceptionCauseMatcher extends TypeSafeMatcher { private Throwable cause; private Class type; @@ -462,7 +478,6 @@ public class MethodInvokingMessageProcessorTests { this.lastArg = b; } - @ServiceActivator public String foo(String s) { this.lastArg = s; return s; @@ -475,4 +490,24 @@ public class MethodInvokingMessageProcessorTests { } + /** + * Method names create ambiguities, but the MethodResolver implementation + * should filter out based on the annotation or the 'requiresReply' flag. + */ + @SuppressWarnings("unused") + private static class OverloadedMethodBean { + + private volatile Object lastArg = null; + + public void foo(boolean b) { + this.lastArg = b; + } + + @ServiceActivator + public String foo(String s) { + this.lastArg = s; + return s; + } + + } }