diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/ChainParser.java b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/ChainParser.java index 0e8681e3c0..67601cbf59 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/ChainParser.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/ChainParser.java @@ -13,10 +13,13 @@ package org.springframework.integration.config.xml; +import java.util.List; + import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; +import org.springframework.beans.BeanMetadataElement; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; import org.springframework.beans.factory.support.BeanDefinitionBuilder; @@ -24,6 +27,8 @@ import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; import org.springframework.beans.factory.support.ManagedList; import org.springframework.beans.factory.xml.ParserContext; import org.springframework.integration.handler.MessageHandlerChain; +import org.springframework.util.StringUtils; +import org.springframework.util.xml.DomUtils; /** * Parser for the <chain> element. @@ -32,14 +37,14 @@ import org.springframework.integration.handler.MessageHandlerChain; * @author Iwein Fuld * @author Oleg Zhurakousky * @author Artem Bilan + * @author Gunnar Hillert */ public class ChainParser extends AbstractConsumerEndpointParser { @Override - @SuppressWarnings("unchecked") protected BeanDefinitionBuilder parseHandler(Element element, ParserContext parserContext) { BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MessageHandlerChain.class); - ManagedList handlerList = new ManagedList(); + ManagedList handlerList = new ManagedList(); NodeList children = element.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { Node child = children.item(i); @@ -63,12 +68,38 @@ public class ChainParser extends AbstractConsumerEndpointParser { return builder; } + private void validateChild(Element element, ParserContext parserContext) { + + final Object source = parserContext.extractSource(element); + + final String order = element.getAttribute(IntegrationNamespaceUtils.ORDER); + + if (StringUtils.hasText(order)) { + parserContext.getReaderContext().error(IntegrationNamespaceUtils.createElementDescription(element) + " must not define " + + "an 'order' attribute when used within a chain.", source); + } + + final List pollerChildElements = DomUtils + .getChildElementsByTagName(element, "poller"); + + if (!pollerChildElements.isEmpty()) { + parserContext.getReaderContext().error(IntegrationNamespaceUtils.createElementDescription(element) + " must not define " + + "a 'poller' sub-element when used within a chain.", source); + } + + } + private BeanDefinitionHolder parseChild(Element element, ParserContext parserContext, BeanDefinition parentDefinition) { + BeanDefinitionHolder holder = null; + if ("bean".equals(element.getLocalName())) { holder = parserContext.getDelegate().parseBeanDefinitionElement(element, parentDefinition); } else { + + this.validateChild(element, parserContext); + BeanDefinition beanDefinition = parserContext.getDelegate().parseCustomElement(element, parentDefinition); if (beanDefinition == null) { parserContext.getReaderContext().error("child BeanDefinition must not be null", element); diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ChainElementsFailureTests.java b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ChainElementsFailureTests.java index 5ad8773131..b9b9f3113c 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ChainElementsFailureTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ChainElementsFailureTests.java @@ -20,8 +20,10 @@ import java.util.Properties; import static org.junit.Assert.fail; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import org.junit.Test; import org.springframework.beans.factory.config.PropertiesFactoryBean; +import org.springframework.beans.factory.parsing.BeanDefinitionParsingException; import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; import org.springframework.beans.factory.xml.XmlBeanDefinitionStoreException; import org.springframework.context.ApplicationContext; @@ -208,6 +210,23 @@ public class ChainElementsFailureTests { } } + @Test + public void chainResequencerPoller() throws Exception { + + try { + this.bootStrap("resequencer-poller"); + fail("Expected a XmlBeanDefinitionStoreException to be thrown."); + } + catch (BeanDefinitionParsingException e) { + final String expectedMessage = "Configuration problem: " + + "'int:resequencer' must not define a 'poller' sub-element " + + "when used within a chain."; + final String actualMessage = e.getMessage(); + assertTrue("Error message did not start with '" + expectedMessage + + "' but instead returned: '" + actualMessage + "'", actualMessage.startsWith(expectedMessage)); + } + } + private ApplicationContext bootStrap(String configProperty) throws Exception { PropertiesFactoryBean pfb = new PropertiesFactoryBean(); pfb.setLocation(new ClassPathResource("org/springframework/integration/config/xml/chain-elements-config.properties")); diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/chain-elements-config.properties b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/chain-elements-config.properties index 6ba79a0767..9b06d9624d 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/chain-elements-config.properties +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/chain-elements-config.properties @@ -75,3 +75,10 @@ resequencer=\ \ \ + +resequencer-poller=\ + \ + \ + \ + \ + \ No newline at end of file diff --git a/spring-integration-file/src/test/java/org/springframework/integration/file/config/ChainElementsTests.java b/spring-integration-file/src/test/java/org/springframework/integration/file/config/ChainElementsTests.java new file mode 100644 index 0000000000..50228e23f4 --- /dev/null +++ b/spring-integration-file/src/test/java/org/springframework/integration/file/config/ChainElementsTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2012 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.file.config; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.ByteArrayInputStream; +import java.util.Properties; + +import org.junit.Test; +import org.springframework.beans.factory.config.PropertiesFactoryBean; +import org.springframework.beans.factory.parsing.BeanDefinitionParsingException; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.beans.factory.xml.XmlBeanDefinitionStoreException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.InputStreamResource; + + +/** + * @author Gunnar Hillert + * @since 2.2 + */ +public class ChainElementsTests { + + @Test + public void chainOutboundGateway() throws Exception { + + try { + this.bootStrap("file-oubound-gateway"); + fail("Expected a BeanDefinitionParsingException to be thrown."); + } + catch (BeanDefinitionParsingException e) { + final String expectedMessage = "Configuration problem: The " + + "'request-channel' attribute isn't allowed for a nested (e.g. " + + "inside a ) endpoint element: 'int-file:outbound-gateway' " + + "with id='myFileOutboundGateway'."; + final String actualMessage = e.getMessage(); + assertTrue("Error message did not start with '" + expectedMessage + + "' but instead returned: '" + actualMessage + "'", actualMessage.startsWith(expectedMessage)); + } + + } + + @Test + public void chainOutboundGatewayWithInputChannel() throws Exception { + + try { + this.bootStrap("file-oubound-gateway-input-channel"); + fail("Expected a XmlBeanDefinitionStoreException to be thrown."); + } + catch (XmlBeanDefinitionStoreException e) { + assertEquals("cvc-complex-type.3.2.2: Attribute 'input-channel' is not" + + " allowed to appear in element 'int-file:outbound-gateway'.", e.getCause().getMessage()); + } + + } + + private ApplicationContext bootStrap(String configProperty) throws Exception { + PropertiesFactoryBean pfb = new PropertiesFactoryBean(); + pfb.setLocation(new ClassPathResource("org/springframework/integration/file/config/chain-elements-config.properties")); + pfb.afterPropertiesSet(); + Properties prop = pfb.getObject(); + StringBuffer buffer = new StringBuffer(); + buffer.append(prop.getProperty("xmlheaders")).append(prop.getProperty(configProperty)).append(prop.getProperty("xmlfooter")); + ByteArrayInputStream stream = new ByteArrayInputStream(buffer.toString().getBytes()); + + GenericApplicationContext ac = new GenericApplicationContext(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(ac); + reader.setValidationMode(XmlBeanDefinitionReader.VALIDATION_XSD); + reader.loadBeanDefinitions(new InputStreamResource(stream)); + ac.refresh(); + return ac; + } + +} diff --git a/spring-integration-file/src/test/java/org/springframework/integration/file/config/chain-elements-config.properties b/spring-integration-file/src/test/java/org/springframework/integration/file/config/chain-elements-config.properties new file mode 100644 index 0000000000..cedd28d53a --- /dev/null +++ b/spring-integration-file/src/test/java/org/springframework/integration/file/config/chain-elements-config.properties @@ -0,0 +1,17 @@ +xmlheaders=\ + \ + +xmlfooter= + +file-oubound-gateway=\ + \ + +file-oubound-gateway-input-channel=\ + \ + diff --git a/spring-integration-http/src/test/java/org/springframework/integration/http/config/ChainElementsTests.java b/spring-integration-http/src/test/java/org/springframework/integration/http/config/ChainElementsTests.java new file mode 100644 index 0000000000..f1b6be5721 --- /dev/null +++ b/spring-integration-http/src/test/java/org/springframework/integration/http/config/ChainElementsTests.java @@ -0,0 +1,74 @@ +/* + * Copyright 2002-2012 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.http.config; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.ByteArrayInputStream; +import java.util.Properties; + +import org.junit.Test; +import org.springframework.beans.factory.config.PropertiesFactoryBean; +import org.springframework.beans.factory.parsing.BeanDefinitionParsingException; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.InputStreamResource; + + +/** + * @author Gunnar Hillert + * @since 2.2 + */ +public class ChainElementsTests { + + @Test + public void chainOutboundGateway() throws Exception { + + try { + this.bootStrap("http-oubound-gateway"); + fail("Expected a BeanDefinitionParsingException to be thrown."); + } + catch (BeanDefinitionParsingException e) { + final String expectedMessage = "Configuration problem: The " + + "'request-channel' attribute isn't allowed for a nested " + + "(e.g. inside a ) endpoint element: 'int-http:outbound-gateway'."; + final String actualMessage = e.getMessage(); + assertTrue("Error message did not start with '" + expectedMessage + + "' but instead returned: '" + actualMessage + "'", actualMessage.startsWith(expectedMessage)); + } + + } + + private ApplicationContext bootStrap(String configProperty) throws Exception { + PropertiesFactoryBean pfb = new PropertiesFactoryBean(); + pfb.setLocation(new ClassPathResource("org/springframework/integration/http/config/chain-elements-config.properties")); + pfb.afterPropertiesSet(); + Properties prop = pfb.getObject(); + StringBuffer buffer = new StringBuffer(); + buffer.append(prop.getProperty("xmlheaders")).append(prop.getProperty(configProperty)).append(prop.getProperty("xmlfooter")); + ByteArrayInputStream stream = new ByteArrayInputStream(buffer.toString().getBytes()); + GenericApplicationContext ac = new GenericApplicationContext(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(ac); + reader.setValidationMode(XmlBeanDefinitionReader.VALIDATION_XSD); + reader.loadBeanDefinitions(new InputStreamResource(stream)); + ac.refresh(); + return ac; + } + +} diff --git a/spring-integration-http/src/test/java/org/springframework/integration/http/config/chain-elements-config.properties b/spring-integration-http/src/test/java/org/springframework/integration/http/config/chain-elements-config.properties new file mode 100644 index 0000000000..0f55d86c92 --- /dev/null +++ b/spring-integration-http/src/test/java/org/springframework/integration/http/config/chain-elements-config.properties @@ -0,0 +1,15 @@ +xmlheaders=\ + \ + +xmlfooter= + +http-oubound-gateway=\ + \ + + diff --git a/spring-integration-xml/src/test/java/org/springframework/integration/xml/config/ChainElementsTests.java b/spring-integration-xml/src/test/java/org/springframework/integration/xml/config/ChainElementsTests.java new file mode 100644 index 0000000000..5243710c4f --- /dev/null +++ b/spring-integration-xml/src/test/java/org/springframework/integration/xml/config/ChainElementsTests.java @@ -0,0 +1,122 @@ +/* + * Copyright 2002-2012 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.xml.config; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.ByteArrayInputStream; +import java.util.Properties; + +import org.junit.Test; +import org.springframework.beans.factory.config.PropertiesFactoryBean; +import org.springframework.beans.factory.parsing.BeanDefinitionParsingException; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.InputStreamResource; + + +/** + * @author Gunnar Hillert + * @since 2.2 + */ +public class ChainElementsTests { + + @Test + public void chainXPathTransformer() throws Exception { + + try { + this.bootStrap("xpath-transformer"); + fail("Expected a BeanDefinitionParsingException to be thrown."); + } + catch (BeanDefinitionParsingException e) { + final String expectedMessage = "Configuration problem: " + + "The 'input-channel' attribute isn't allowed for a nested " + + "(e.g. inside a ) endpoint element: 'int-xml:xpath-transformer'."; + final String actualMessage = e.getMessage(); + assertTrue("Error message did not start with '" + expectedMessage + + "' but instead returned: '" + actualMessage + "'", actualMessage.startsWith(expectedMessage)); + } + + } + + @Test + public void chainXPathTransformerId() throws Exception { + + this.bootStrap("xpath-transformer-id"); + + } + + @Test + public void chainXPathRouterOrder() throws Exception { + + try { + this.bootStrap("xpath-router-order"); + fail("Expected a BeanDefinitionParsingException to be thrown."); + } + catch (BeanDefinitionParsingException e) { + final String expectedMessage = "Configuration problem: " + + "'int-xml:xpath-router' must not define an 'order' attribute " + + "when used within a chain."; + final String actualMessage = e.getMessage(); + assertTrue("Error message did not start with '" + expectedMessage + + "' but instead returned: '" + actualMessage + "'", actualMessage.startsWith(expectedMessage)); + } + + } + + @Test + public void chainXPathTransformerPoller() throws Exception { + + try { + this.bootStrap("xpath-transformer-poller"); + fail("Expected a BeanDefinitionParsingException to be thrown."); + } + catch (BeanDefinitionParsingException e) { + final String expectedMessage = "Configuration problem: " + + "'int-xml:xpath-transformer' must not define a 'poller' " + + "sub-element when used within a chain."; + final String actualMessage = e.getMessage(); + assertTrue("Error message did not start with '" + expectedMessage + + "' but instead returned: '" + actualMessage + "'", actualMessage.startsWith(expectedMessage)); + } + } + + @Test + public void chainXPathTransformerSuccess() throws Exception { + this.bootStrap("xpath-transformer-success"); + } + + private ApplicationContext bootStrap(String configProperty) throws Exception { + PropertiesFactoryBean pfb = new PropertiesFactoryBean(); + pfb.setLocation(new ClassPathResource("org/springframework/integration/xml/config/chain-elements-config.properties")); + pfb.afterPropertiesSet(); + Properties prop = pfb.getObject(); + StringBuffer buffer = new StringBuffer(); + buffer.append(prop.getProperty("xmlheaders")).append(prop.getProperty(configProperty)).append(prop.getProperty("xmlfooter")); + ByteArrayInputStream stream = new ByteArrayInputStream(buffer.toString().getBytes()); + + GenericApplicationContext ac = new GenericApplicationContext(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(ac); + reader.setValidationMode(XmlBeanDefinitionReader.VALIDATION_XSD); + reader.loadBeanDefinitions(new InputStreamResource(stream)); + ac.refresh(); + return ac; + } + +} diff --git a/spring-integration-xml/src/test/java/org/springframework/integration/xml/config/chain-elements-config.properties b/spring-integration-xml/src/test/java/org/springframework/integration/xml/config/chain-elements-config.properties new file mode 100644 index 0000000000..0b894faf95 --- /dev/null +++ b/spring-integration-xml/src/test/java/org/springframework/integration/xml/config/chain-elements-config.properties @@ -0,0 +1,30 @@ +xmlheaders=\ + \ + +xmlfooter= + +xpath-transformer=\ + \ + +xpath-transformer-id=\ + \ + +xpath-router-order=\ + \ + \ + \ + +xpath-transformer-success=\ + \ + +xpath-transformer-poller=\ + \ + \ + \ + \ No newline at end of file diff --git a/src/reference/docbook/chain.xml b/src/reference/docbook/chain.xml index edc3c7062d..06a1886a0b 100644 --- a/src/reference/docbook/chain.xml +++ b/src/reference/docbook/chain.xml @@ -13,10 +13,10 @@ progression. For example, it is fairly common to provide a Transformer before other components. Similarly, when providing a Filter before some other component in a chain, you are essentially creating a Selective Consumer. In either case, the - chain only requires a single input-channel and a single output-channel eliminating + chain only requires a single input-channel and a single output-channel eliminating the need to define channels for each individual component. - Spring Integration's Filter provides a boolean property throwExceptionOnRejection. + Spring Integration's Filter provides a boolean property throwExceptionOnRejection. When providing multiple Selective Consumers on the same point-to-point channel with different acceptance criteria, this value should be set to 'true' (the default is false) so that the dispatcher will know that the Message was rejected and as a result will attempt to pass the Message on to other subscribers. If the Exception were not @@ -68,7 +68,7 @@ The <header-enricher> element used in the above example will set a message header named "foo" with a value - of "bar" on the message. A header enricher is a specialization of Transformer + of "bar" on the message. A header enricher is a specialization of Transformer that touches only header values. You could obtain the same result by implementing a MessageHandler that did the header modifications and wiring that as a bean, but the header-enricher is obviously a simpler option. @@ -84,11 +84,52 @@ ]]> - + Disallowed Attributes and Elements + + It is important to note that certain attributes, such as + order and input-channel + are not allowed to be specified on components used within a + chain. The same is true for the poller + sub-element. + + + + For the Spring Integration core components, the + XML Schema itself will enforce some of these constraints. However, for non-core + components or your own custom components, these constraints are enforced + by the XML namespace parser, not by the XML Schema. + + + These XML namespace parser constraints were added with + Spring Integration 2.2. The XML namespace parser + will throw an BeanDefinitionParsingException if you try to use disallowed + attributes and elements. + + + + The id attribute, however, is allowed to be specified. + In fact, the Delayer + component actually requires the id attribute to be present. + + + In most other cases, the id will generally be + ignored but may still add value for documentation purposes, and may also + be used for providing more meaningful log messages. + + + Currently, the XML Schema of the Spring Integration Core module + prevents you from setting the id attribute for + Core components within a Message Handler Chain. This may be relaxed in future, + to provide the benefits described above. + + Calling a Chain from within a Chain + + Sometimes you need to make a nested call to another chain from within a chain and then come back and continue execution within the original chain. To accomplish this you can utilize a Messaging Gateway by including a <gateway> element. For example: + @@ -117,7 +158,7 @@ ]]> - + In the above example the nested-chain-a will be called at the end of main-chain processing by the 'gateway' element configured there. While in nested-chain-a a call to a nested-chain-b will be made