INT-2218 - Chain Parser Validation Improvements

Components within Chain: Add parser validation

For reference see: https://jira.springsource.org/browse/INT-2218

INT-2218 - Code review changes

INT-2218 - Fix HttpOutboundGatewayParserTests

INT-2218 - Remove input-channel validation

Was already covered by PR #592
This commit is contained in:
Gunnar Hillert
2012-09-12 10:07:18 -04:00
committed by Gary Russell
parent c9f73a8a49
commit 7055845424
10 changed files with 455 additions and 7 deletions

View File

@@ -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<BeanMetadataElement> handlerList = new ManagedList<BeanMetadataElement>();
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<Element> 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);

View File

@@ -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"));

View File

@@ -75,3 +75,10 @@ resequencer=\
<int:chain input-channel="input"> \
<int:resequencer input-channel="fail"/> \
</int:chain>
resequencer-poller=\
<int:chain input-channel="input"> \
<int:resequencer> \
<int:poller fixed-rate="5000" max-messages-per-poll="10" />\
</int:resequencer> \
</int:chain>

View File

@@ -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 <chain/>) 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;
}
}

View File

@@ -0,0 +1,17 @@
xmlheaders=\
<?xml version="1.0" encoding="UTF-8"?> \
<beans xmlns="http://www.springframework.org/schema/beans" \
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" \
xmlns:int="http://www.springframework.org/schema/integration" \
xmlns:int-file="http://www.springframework.org/schema/integration/file" \
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd \
http://www.springframework.org/schema/integration/file http://www.springframework.org/schema/integration/file/spring-integration-file.xsd \
http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd">
xmlfooter= </beans>
file-oubound-gateway=<int:chain input-channel="fileOutboundGatewayInsideChain" output-channel="nullChannel">\
<int-file:outbound-gateway id="myFileOutboundGateway" request-channel="request" directory="${java.io.tmpdir}/anyDir" delete-source-files="true"/>\
</int:chain>
file-oubound-gateway-input-channel=<int:chain input-channel="fileOutboundGatewayInsideChain" output-channel="nullChannel">\
<int-file:outbound-gateway id="myFileOutboundGateway" input-channel="request" directory="${java.io.tmpdir}/anyDir" delete-source-files="true"/>\
</int:chain>

View File

@@ -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 <chain/>) 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;
}
}

View File

@@ -0,0 +1,15 @@
xmlheaders=\
<?xml version="1.0" encoding="UTF-8"?> \
<beans xmlns="http://www.springframework.org/schema/beans" \
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" \
xmlns:int="http://www.springframework.org/schema/integration" \
xmlns:int-http="http://www.springframework.org/schema/integration/http" \
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd \
http://www.springframework.org/schema/integration/http http://www.springframework.org/schema/integration/http/spring-integration-http.xsd \
http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd">
xmlfooter= </beans>
http-oubound-gateway=<int:chain id="httpChain" input-channel="httpOutboundGatewayInsideChain" output-channel="nullChannel">\
<int-http:outbound-gateway request-channel="requestChannel" url="http://google.com/" http-method="POST"/>\
</int:chain>

View File

@@ -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 <chain/>) 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;
}
}

View File

@@ -0,0 +1,30 @@
xmlheaders=\
<?xml version="1.0" encoding="UTF-8"?> \
<beans xmlns="http://www.springframework.org/schema/beans" \
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" \
xmlns:int="http://www.springframework.org/schema/integration" \
xmlns:int-xml="http://www.springframework.org/schema/integration/xml" \
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd \
http://www.springframework.org/schema/integration/xml http://www.springframework.org/schema/integration/xml/spring-integration-xml.xsd \
http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd">
xmlfooter= </beans>
xpath-transformer=<int:chain input-channel="input">\
<int-xml:xpath-transformer input-channel="fail" xpath-expression="/person/@name" />\
</int:chain>
xpath-transformer-id=<int:chain input-channel="input">\
<int-xml:xpath-transformer id="fail" xpath-expression="/person/@name" />\
</int:chain>
xpath-router-order=<int:chain input-channel="input">\
<int-xml:xpath-router order="1">\
<int-xml:xpath-expression expression="/name"/>\
</int-xml:xpath-router>\
</int:chain>
xpath-transformer-success=<int:chain input-channel="input">\
<int-xml:xpath-transformer xpath-expression="/person/@name" />\
</int:chain>
xpath-transformer-poller=<int:chain input-channel="input">\
<int-xml:xpath-transformer xpath-expression="/person/@name">\
<int:poller id="poller" fixed-rate="5000" max-messages-per-poll="10" />\
</int-xml:xpath-transformer>\
</int:chain>

View File

@@ -13,10 +13,10 @@
progression. For example, it is fairly common to provide a Transformer before other components. Similarly, when
providing a <emphasis>Filter</emphasis> before some other component in a chain, you are essentially creating a
<ulink url="http://www.eaipatterns.com/MessageSelector.html">Selective Consumer</ulink>. In either case, the
chain only requires a single <code>input-channel</code> and a single <code>output-channel</code> eliminating
chain only requires a single <code>input-channel</code> and a single <code>output-channel</code> eliminating
the need to define channels for each individual component.
<tip>
Spring Integration's <interfacename>Filter</interfacename> provides a boolean property <methodname>throwExceptionOnRejection</methodname>.
Spring Integration's <interfacename>Filter</interfacename> provides a boolean property <methodname>throwExceptionOnRejection</methodname>.
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 @@
</para>
<para>
The &lt;header-enricher&gt; 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 <interfacename>Transformer</interfacename>
of "bar" on the message. A header enricher is a specialization of <interfacename>Transformer</interfacename>
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.
</para>
@@ -84,11 +84,52 @@
<int:logging-channel-adapter level="INFO" log-full-message="true"/>
</int:chain>]]></programlisting>
</para>
<para>
<para><emphasis>Disallowed Attributes and Elements</emphasis></para>
<para>
It is important to note that certain attributes, such as
<emphasis role="bold">order</emphasis> and <emphasis role="bold">input-channel</emphasis>
are not allowed to be specified on components used within a
<emphasis>chain</emphasis>. The same is true for the <emphasis role="bold">poller</emphasis>
sub-element.
</para>
<important>
<para>
For the <emphasis>Spring Integration</emphasis> 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.
</para>
<para>
These XML namespace parser constraints were added with
<emphasis>Spring Integration 2.2</emphasis>. The XML namespace parser
will throw an <classname>BeanDefinitionParsingException</classname> if you try to use disallowed
attributes and elements.
</para>
</important>
<para>
The <emphasis>id</emphasis> attribute, however, is allowed to be specified.
In fact, the <link linkend='delayer'><emphasis>Delayer</emphasis></link>
component actually requires the <emphasis>id</emphasis> attribute to be present.
</para>
<para>
In most other cases, the <emphasis>id</emphasis> will generally be
ignored but may still add value for documentation purposes, and may also
be used for providing more meaningful log messages.
</para>
<note>
Currently, the XML Schema of the <emphasis>Spring Integration</emphasis> Core module
prevents you from setting the <emphasis>id</emphasis> attribute for
Core components within a Message Handler Chain. This may be relaxed in future,
to provide the benefits described above.
</note>
<para><emphasis>Calling a Chain from within a Chain</emphasis></para>
<para>
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 &lt;gateway&gt; element.
For example:
</para>
<programlisting language="xml"><![CDATA[ <int:chain id="main-chain" input-channel="in" output-channel="out">
<int:header-enricher>
<int:header name="name" value="Many" />
@@ -117,7 +158,7 @@
<bean class="org.foo.SampleService" />
</int:service-activator>
</int:chain>]]></programlisting>
<para>
In the above example the <emphasis>nested-chain-a</emphasis> will be called at the end of
<emphasis>main-chain</emphasis> processing by the 'gateway' element configured there. While in
<emphasis>nested-chain-a</emphasis> a call to a <emphasis>nested-chain-b</emphasis> will be made