diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/ConsumerEndpointFactoryBean.java b/spring-integration-core/src/main/java/org/springframework/integration/config/ConsumerEndpointFactoryBean.java index e1e80c4620..fd5ab55024 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/ConsumerEndpointFactoryBean.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/ConsumerEndpointFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2013 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. @@ -66,6 +66,8 @@ public class ConsumerEndpointFactoryBean private volatile boolean autoStartup = true; + private volatile int phase = 0; + private volatile MessageChannel inputChannel; private volatile ConfigurableBeanFactory beanFactory; @@ -112,6 +114,10 @@ public class ConsumerEndpointFactoryBean this.autoStartup = autoStartup; } + public void setPhase(int phase) { + this.phase = phase; + } + public void setBeanName(String beanName) { this.beanName = beanName; } @@ -239,6 +245,7 @@ public class ConsumerEndpointFactoryBean this.endpoint.setBeanName(this.beanName); this.endpoint.setBeanFactory(this.beanFactory); this.endpoint.setAutoStartup(this.autoStartup); + this.endpoint.setPhase(this.phase); this.endpoint.afterPropertiesSet(); this.initialized = true; } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/SourcePollingChannelAdapterFactoryBean.java b/spring-integration-core/src/main/java/org/springframework/integration/config/SourcePollingChannelAdapterFactoryBean.java index dd1fcc4779..5ade341e0d 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/SourcePollingChannelAdapterFactoryBean.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/SourcePollingChannelAdapterFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2013 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. @@ -48,6 +48,8 @@ public class SourcePollingChannelAdapterFactoryBean implements FactoryBean or is required.", element); } - if (isInnerDef) { + if (hasInnerDef) { + if (hasRef || hasExpression) { + parserContext.getReaderContext().error( + "Neither 'ref' nor 'expression' are permitted when an inner bean () is configured on element " + + IntegrationNamespaceUtils.createElementDescription(element) + ".", source); + return null; + } if (hasMethod) { result = this.parseMethodInvokingSource(innnerBeanDef, methodName, element, parserContext); } @@ -70,15 +81,34 @@ public class DefaultInboundChannelAdapterParser extends AbstractPollingInboundCh result = innnerBeanDef; } } - else if (isExpression) { - if (hasMethod) { + else if (hasScriptElement) { + if (hasRef || hasMethod || hasExpression) { parserContext.getReaderContext().error( - "The 'method' attribute can't be used with 'expression' attribute.", element); + "Neither 'ref' and 'method' nor 'expression' are permitted when an inner script element is configured on element " + + IntegrationNamespaceUtils.createElementDescription(element) + ".", source); + return null; } - String expressionBeanName = this.parseExpression(expressionString, element, parserContext); - result = new RuntimeBeanReference(expressionBeanName); + BeanDefinition scriptBeanDefinition = parserContext.getDelegate().parseCustomElement(scriptElement); + BeanDefinitionBuilder sourceBuilder = BeanDefinitionBuilder.genericBeanDefinition( + IntegrationNamespaceUtils.BASE_PACKAGE + ".scripting.ScriptExecutingMessageSource"); + sourceBuilder.addConstructorArgValue(scriptBeanDefinition); + this.parseHeaderExpressions(sourceBuilder, element, parserContext); + result = sourceBuilder.getBeanDefinition(); } - else if (isRef) { + else if (hasExpression || hasExpressionElement) { + if (hasRef || hasMethod) { + parserContext.getReaderContext().error( + "The 'ref' and 'method' attributes can't be used with 'expression' attribute or inner .", element); + return null; + } + if (hasExpression & hasExpressionElement) { + parserContext.getReaderContext().error( + "Exactly one of the 'expression' attribute or inner is required.", element); + return null; + } + result = this.parseExpression(expressionString, expressionElement, element, parserContext); + } + else if (hasRef) { BeanMetadataElement sourceValue = new RuntimeBeanReference(sourceRef); if (hasMethod) { result = this.parseMethodInvokingSource(sourceValue, methodName, element, parserContext); @@ -87,31 +117,44 @@ public class DefaultInboundChannelAdapterParser extends AbstractPollingInboundCh result = sourceValue; } } - else { - parserContext.getReaderContext().error("One of the following is required: " + - "'ref' attribute, 'expression' attribute, or an inner-bean definition.", element); - } + return result; } - private BeanMetadataElement parseMethodInvokingSource(BeanMetadataElement targetObject, String methodName, Element element, ParserContext parserContext) { + private BeanMetadataElement parseMethodInvokingSource(BeanMetadataElement targetObject, String methodName, Element element, + ParserContext parserContext) { BeanDefinitionBuilder sourceBuilder = BeanDefinitionBuilder.genericBeanDefinition(MethodInvokingMessageSource.class); sourceBuilder.addPropertyValue("object", targetObject); sourceBuilder.addPropertyValue("methodName", methodName); this.parseHeaderExpressions(sourceBuilder, element, parserContext); - String sourceRef = BeanDefinitionReaderUtils.registerWithGeneratedName( - sourceBuilder.getBeanDefinition(), parserContext.getRegistry()); - return new RuntimeBeanReference(sourceRef); + return sourceBuilder.getBeanDefinition(); } - private String parseExpression(String expressionString, Element element, ParserContext parserContext) { + private BeanMetadataElement parseExpression(String expressionString, Element expressionElement, Element element, + ParserContext parserContext) { BeanDefinitionBuilder sourceBuilder = BeanDefinitionBuilder.genericBeanDefinition(ExpressionEvaluatingMessageSource.class); - RootBeanDefinition expressionDef = new RootBeanDefinition(ExpressionFactoryBean.class); - expressionDef.getConstructorArgumentValues().addGenericArgumentValue(expressionString); + + BeanDefinition expressionDef = null; + + if (StringUtils.hasText(expressionString)) { + expressionDef = new RootBeanDefinition(ExpressionFactoryBean.class); + expressionDef.getConstructorArgumentValues().addGenericArgumentValue(expressionString); + } + else { + BeanDefinitionBuilder dynamicExpressionBuilder = BeanDefinitionBuilder.genericBeanDefinition( + DynamicExpression.class); + String key = expressionElement.getAttribute("key"); + String expressionSourceReference = expressionElement.getAttribute("source"); + dynamicExpressionBuilder.addConstructorArgValue(key); + dynamicExpressionBuilder.addConstructorArgReference(expressionSourceReference); + expressionDef = dynamicExpressionBuilder.getBeanDefinition(); + } + sourceBuilder.addConstructorArgValue(expressionDef); sourceBuilder.addConstructorArgValue(null); + this.parseHeaderExpressions(sourceBuilder, element, parserContext); - return BeanDefinitionReaderUtils.registerWithGeneratedName(sourceBuilder.getBeanDefinition(), parserContext.getRegistry()); + return sourceBuilder.getBeanDefinition(); } private void parseHeaderExpressions(BeanDefinitionBuilder builder, Element element, ParserContext parserContext) { @@ -120,24 +163,8 @@ public class DefaultInboundChannelAdapterParser extends AbstractPollingInboundCh ManagedMap headerExpressions = new ManagedMap(); for (Element headerElement : headerElements) { String headerName = headerElement.getAttribute("name"); - String headerValue = headerElement.getAttribute("value"); - String headerExpression = headerElement.getAttribute("expression"); - boolean hasValue = StringUtils.hasText(headerValue); - boolean hasExpression = StringUtils.hasText(headerExpression); - if (!(hasValue ^ hasExpression)) { - parserContext.getReaderContext().error("exactly one of 'value' or 'expression' is required on a header sub-element", - parserContext.extractSource(headerElement)); - continue; - } - RootBeanDefinition expressionDef = null; - if (hasValue) { - expressionDef = new RootBeanDefinition(LiteralExpression.class); - expressionDef.getConstructorArgumentValues().addGenericArgumentValue(headerValue); - } - else { - expressionDef = new RootBeanDefinition(ExpressionFactoryBean.class); - expressionDef.getConstructorArgumentValues().addGenericArgumentValue(headerExpression); - } + BeanDefinition expressionDef = IntegrationNamespaceUtils.createExpressionDefinitionFromValueOrExpression("value", + "expression", parserContext, headerElement, true); headerExpressions.put(headerName, expressionDef); } builder.addPropertyValue("headerExpressions", headerExpressions); diff --git a/spring-integration-core/src/main/resources/org/springframework/integration/config/xml/spring-integration-3.0.xsd b/spring-integration-core/src/main/resources/org/springframework/integration/config/xml/spring-integration-3.0.xsd index c17fd2b225..7ea44de1ea 100644 --- a/spring-integration-core/src/main/resources/org/springframework/integration/config/xml/spring-integration-3.0.xsd +++ b/spring-integration-core/src/main/resources/org/springframework/integration/config/xml/spring-integration-3.0.xsd @@ -800,6 +800,9 @@ Defines a Channel Adapter that receives from a MessageSource and sends to a MessageChannel. + Note, when using the 'expression' attribute, or 'expression' or 'script' sub-element, + there is not yet a root Message object and therefore + the 'payload' and 'headers' properties are not available in the expression or script. @@ -807,10 +810,16 @@ - + + + + - + + + + @@ -1039,6 +1048,16 @@ + + + + + The Lifecycle attribute determining the start/stop order + of the underlying MessageHandlerChain. + + + + diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/ChannelAdapterParserTests-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/config/ChannelAdapterParserTests-context.xml index 64f907f504..7133c6dde5 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/ChannelAdapterParserTests-context.xml +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/ChannelAdapterParserTests-context.xml @@ -15,21 +15,22 @@ - + - + - + - diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/ChannelAdapterParserTests.java b/spring-integration-core/src/test/java/org/springframework/integration/config/ChannelAdapterParserTests.java index 534bfd3811..0d4f1625bf 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/ChannelAdapterParserTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/ChannelAdapterParserTests.java @@ -17,10 +17,14 @@ package org.springframework.integration.config; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import org.hamcrest.Matchers; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -30,6 +34,8 @@ import org.springframework.context.support.AbstractApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.integration.Message; import org.springframework.integration.MessageChannel; +import org.springframework.integration.MessageDeliveryException; +import org.springframework.integration.MessageDispatchingException; import org.springframework.integration.channel.DirectChannel; import org.springframework.integration.core.PollableChannel; import org.springframework.integration.endpoint.EventDrivenConsumer; @@ -73,6 +79,7 @@ public class ChannelAdapterParserTests { Object adapter = this.applicationContext.getBean(beanName); assertNotNull(adapter); assertTrue(adapter instanceof SourcePollingChannelAdapter); + assertEquals(-1, ((SourcePollingChannelAdapter) adapter).getPhase()); this.applicationContext.start(); Message message = channel.receive(1000); assertNotNull(message); @@ -110,10 +117,22 @@ public class ChannelAdapterParserTests { Object adapter = this.applicationContext.getBean(beanName + ".adapter"); assertNotNull(adapter); assertTrue(adapter instanceof EventDrivenConsumer); + assertFalse(((EventDrivenConsumer) adapter).isAutoStartup()); + assertEquals(-1, ((EventDrivenConsumer) adapter).getPhase()); TestConsumer consumer = (TestConsumer) this.applicationContext.getBean("consumer"); assertNull(consumer.getLastMessage()); Message message = new GenericMessage("test"); - assertTrue(((MessageChannel) channel).send(message)); + try { + ((MessageChannel) channel).send(message); + fail("MessageDispatchingException is expected."); + } + catch (Exception e) { + assertThat(e, Matchers.instanceOf(MessageDeliveryException.class)); + assertThat(e.getCause(), Matchers.instanceOf(MessageDispatchingException.class)); + } + + ((EventDrivenConsumer) adapter).start(); + ((MessageChannel) channel).send(message); assertNotNull(consumer.getLastMessage()); assertEquals(message, consumer.getLastMessage()); } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/InboundChannelAdapterExpressionTests-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/InboundChannelAdapterExpressionTests-context.xml index 41d3c2c6f7..95befb3b25 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/InboundChannelAdapterExpressionTests-context.xml +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/InboundChannelAdapterExpressionTests-context.xml @@ -41,6 +41,15 @@ + + + + + + + + + diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/InboundChannelAdapterExpressionTests.java b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/InboundChannelAdapterExpressionTests.java index 6777d49477..2400000305 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/InboundChannelAdapterExpressionTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/InboundChannelAdapterExpressionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * Copyright 2002-2013 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. @@ -38,6 +38,7 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; /** * @author Mark Fisher + * @author Artem Bilan * @since 2.0 */ @ContextConfiguration @@ -109,7 +110,7 @@ public class InboundChannelAdapterExpressionTests { public void headerExpressions() { SourcePollingChannelAdapter adapter = context.getBean("headerExpressionsProducer", SourcePollingChannelAdapter.class); assertFalse(adapter.isAutoStartup()); - Map headerExpressions = TestUtils.getPropertyValue(adapter, "source.headerExpressions", Map.class); + Map headerExpressions = TestUtils.getPropertyValue(adapter, "source.headerExpressions", Map.class); assertEquals(2, headerExpressions.size()); assertEquals("6 * 7", headerExpressions.get("foo").getExpressionString()); assertEquals("x", headerExpressions.get("bar").getExpressionString()); @@ -117,4 +118,12 @@ public class InboundChannelAdapterExpressionTests { assertEquals("x", headerExpressions.get("bar").getValue()); } + @Test + public void testInt2867InnerExpression() { + SourcePollingChannelAdapter adapter = context.getBean("expressionElement", SourcePollingChannelAdapter.class); + Expression expression = TestUtils.getPropertyValue(adapter, "source.expression", Expression.class); + assertEquals("'Hello World!'", expression.getExpressionString()); + + } + } diff --git a/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/ScriptExecutingMessageSource.java b/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/ScriptExecutingMessageSource.java new file mode 100644 index 0000000000..6b782f1f7a --- /dev/null +++ b/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/ScriptExecutingMessageSource.java @@ -0,0 +1,41 @@ +/* + * Copyright 2013 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.scripting; + +import org.springframework.integration.endpoint.AbstractMessageSource; + +/** + * The {@link org.springframework.integration.core.MessageSource} strategy implementation + * to produce a {@link org.springframework.integration.Message} from underlying + * {@linkplain #scriptMessageProcessor} for polling endpoints. + * + * @author Artem Bilan + * @since 3.0 + */ +class ScriptExecutingMessageSource extends AbstractMessageSource { + + private final AbstractScriptExecutingMessageProcessor scriptMessageProcessor; + + public ScriptExecutingMessageSource(AbstractScriptExecutingMessageProcessor scriptMessageProcessor) { + this.scriptMessageProcessor = scriptMessageProcessor; + } + + @Override + protected Object doReceive() { + return scriptMessageProcessor.processMessage(null); + } +} diff --git a/spring-integration-scripting/src/test/java/org/springframework/integration/scripting/config/jsr223/Jsr223InboundChannelAdapterTests-context.xml b/spring-integration-scripting/src/test/java/org/springframework/integration/scripting/config/jsr223/Jsr223InboundChannelAdapterTests-context.xml new file mode 100644 index 0000000000..a7b15ec229 --- /dev/null +++ b/spring-integration-scripting/src/test/java/org/springframework/integration/scripting/config/jsr223/Jsr223InboundChannelAdapterTests-context.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + +
+ + + diff --git a/spring-integration-scripting/src/test/java/org/springframework/integration/scripting/config/jsr223/Jsr223InboundChannelAdapterTests.java b/spring-integration-scripting/src/test/java/org/springframework/integration/scripting/config/jsr223/Jsr223InboundChannelAdapterTests.java new file mode 100644 index 0000000000..756512422b --- /dev/null +++ b/spring-integration-scripting/src/test/java/org/springframework/integration/scripting/config/jsr223/Jsr223InboundChannelAdapterTests.java @@ -0,0 +1,67 @@ +/* + * Copyright 2013 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.scripting.config.jsr223; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +import java.util.Date; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.hamcrest.Matchers; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.integration.Message; +import org.springframework.integration.core.PollableChannel; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * @author Artem Bilan + * @since 2.0 + */ +@ContextConfiguration +@RunWith(SpringJUnit4ClassRunner.class) +public class Jsr223InboundChannelAdapterTests { + + @Autowired + @Qualifier("inbound-channel-adapter-channel") + private PollableChannel inboundChannelAdapterChannel; + + @Test + public void testInt2867InboundChannelAdapter() throws Exception { + Message message = this.inboundChannelAdapterChannel.receive(2000); + assertNotNull(message); + Object payload = message.getPayload(); + assertThat(payload, Matchers.instanceOf(Date.class)); + assertTrue(((Date) payload).before(new Date())); + assertEquals("bar", message.getHeaders().get("foo")); + + message = this.inboundChannelAdapterChannel.receive(1000); + assertNotNull(message); + + message = this.inboundChannelAdapterChannel.receive(10); + assertNull(message); + } + +} diff --git a/src/reference/docbook/channel-adapter.xml b/src/reference/docbook/channel-adapter.xml index 5913e823b9..d8868b2267 100644 --- a/src/reference/docbook/channel-adapter.xml +++ b/src/reference/docbook/channel-adapter.xml @@ -29,6 +29,9 @@ ]]> + + Also see . + If no poller is provided, then a single default poller must be registered within the context. @@ -121,6 +124,9 @@ of the <inbound-channel-adapter> or <outbound-channel-adapter> element. Therefore, if the "channel" is not provided, the "id" is required. + +
+ Channel Adapter Expressions and Scripts Like many other Spring Integration components, the <inbound-channel-adapter> and <outbound-channel-adapter> also provide support for SpEL expression evaluation. To use SpEL, provide the @@ -130,6 +136,37 @@ evaluation result is a non-null value, while the expression for an <outbound-channel-adapter> must be the equivalent of a void returning method invocation. + + Starting with Spring Integration 3.0, an <int:inbound-channel-adapter/> can also be configured + with a SpEL <expression/> (or even with <script/>) sub-element, for when + more sophistication is required than can be achieved with the simple 'expression' attribute. + If you provide a script as a Resource using the location + attribute, you can also set the refresh-check-delay allowing the resource to be refreshed + periodically. + If you want the script to be checked on each poll, you would need to coordinate this setting with the poller's trigger: + + + + +]]> + + Also see the cacheSeconds property on the ReloadableResourceBundleExpressionSource + when using the <expression/> sub-element. + For more information regarding expressions see , and for scripts - + and . + + + + The <int:inbound-channel-adapter/> is an endpoint that starts a message flow + via periodic triggering to poll some underlying MessageSource. Since, at the + time of polling, there is not yet a message object, expressions and scripts don't + have access to a root Message, so there are no payload + or headers properties that are available in most other messaging SpEL expressions. + Of course, the script can + generate and return a complete Message object with headers and payload, or just + a payload, which will be added to a message with basic headers. + +
diff --git a/src/reference/docbook/whats-new.xml b/src/reference/docbook/whats-new.xml index 9398473e5d..5f433134db 100644 --- a/src/reference/docbook/whats-new.xml +++ b/src/reference/docbook/whats-new.xml @@ -75,8 +75,15 @@ 'Tail' Support File 'tail'ing inbound channel adapters are now provided to generate messages when - lines are added to the end of text files. - . + lines are added to the end of text files; see . + + +
+ Inbound Channel Adapter Script Support + + The <int:inbound-channel-adapter/> now supports <expression/> + and <script/> sub-elements to create a + MessageSource; see .