From a0951c8fb4890bfe224d2ad4d2fdee33fd9752b2 Mon Sep 17 00:00:00 2001 From: Mark Fisher Date: Wed, 21 Sep 2011 11:47:05 -0400 Subject: [PATCH] initial implementation of XPathFilter --- .../IntegrationXmlNamespaceHandler.java | 3 +- .../xml/config/XPathFilterParser.java | 88 ++++++++ .../RegexTestXPathMessageSelector.java | 104 ++++++++++ .../xml/config/spring-integration-xml-2.1.xsd | 195 +++++++++++++----- .../config/XPathFilterParserTests-context.xml | 68 ++++++ .../xml/config/XPathFilterParserTests.java | 156 ++++++++++++++ 6 files changed, 562 insertions(+), 52 deletions(-) create mode 100644 spring-integration-xml/src/main/java/org/springframework/integration/xml/config/XPathFilterParser.java create mode 100644 spring-integration-xml/src/main/java/org/springframework/integration/xml/selector/RegexTestXPathMessageSelector.java create mode 100644 spring-integration-xml/src/test/java/org/springframework/integration/xml/config/XPathFilterParserTests-context.xml create mode 100644 spring-integration-xml/src/test/java/org/springframework/integration/xml/config/XPathFilterParserTests.java diff --git a/spring-integration-xml/src/main/java/org/springframework/integration/xml/config/IntegrationXmlNamespaceHandler.java b/spring-integration-xml/src/main/java/org/springframework/integration/xml/config/IntegrationXmlNamespaceHandler.java index 57ebcb5d0c..e584f99201 100644 --- a/spring-integration-xml/src/main/java/org/springframework/integration/xml/config/IntegrationXmlNamespaceHandler.java +++ b/spring-integration-xml/src/main/java/org/springframework/integration/xml/config/IntegrationXmlNamespaceHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * Copyright 2002-2011 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. @@ -31,6 +31,7 @@ public class IntegrationXmlNamespaceHandler extends AbstractIntegrationNamespace registerBeanDefinitionParser("xpath-transformer", new XPathTransformerParser()); registerBeanDefinitionParser("xpath-header-enricher", new XPathHeaderEnricherParser()); registerBeanDefinitionParser("xpath-router", new XPathRouterParser()); + registerBeanDefinitionParser("xpath-filter", new XPathFilterParser()); registerBeanDefinitionParser("xpath-selector", new XPathSelectorParser()); registerBeanDefinitionParser("xpath-expression", new XPathExpressionParser()); registerBeanDefinitionParser("xpath-splitter", new XPathMessageSplitterParser()); diff --git a/spring-integration-xml/src/main/java/org/springframework/integration/xml/config/XPathFilterParser.java b/spring-integration-xml/src/main/java/org/springframework/integration/xml/config/XPathFilterParser.java new file mode 100644 index 0000000000..65f5fc6116 --- /dev/null +++ b/spring-integration-xml/src/main/java/org/springframework/integration/xml/config/XPathFilterParser.java @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2011 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 org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.integration.config.FilterFactoryBean; +import org.springframework.integration.config.xml.AbstractConsumerEndpointParser; +import org.springframework.integration.config.xml.IntegrationNamespaceUtils; +import org.springframework.integration.xml.selector.BooleanTestXPathMessageSelector; +import org.springframework.integration.xml.selector.RegexTestXPathMessageSelector; +import org.springframework.integration.xml.selector.StringValueTestXPathMessageSelector; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Parser for the <xpath-filter> element. + * + * @author Mark Fisher + * @since 2.1 + */ +public class XPathFilterParser extends AbstractConsumerEndpointParser { + + private XPathExpressionParser xpathParser = new XPathExpressionParser(); + + + @Override + protected BeanDefinitionBuilder parseHandler(Element element, ParserContext parserContext) { + final BeanDefinitionBuilder selectorBuilder = BeanDefinitionBuilder.genericBeanDefinition(); + selectorBuilder.getBeanDefinition().setBeanClass(BooleanTestXPathMessageSelector.class); + this.configureXPathExpression(element, selectorBuilder, parserContext); + if (element.hasAttribute("match-value")) { + selectorBuilder.addConstructorArgValue(element.getAttribute("match-value")); + String matchType = element.getAttribute("match-type"); + if ("exact".equals(matchType)) { + selectorBuilder.getBeanDefinition().setBeanClass(StringValueTestXPathMessageSelector.class); + } + else if ("case-insensitive".equals(matchType)) { + selectorBuilder.getBeanDefinition().setBeanClass(StringValueTestXPathMessageSelector.class); + selectorBuilder.addPropertyValue("caseSensitive", false); + } + else if ("regex".equals(matchType)) { + selectorBuilder.getBeanDefinition().setBeanClass(RegexTestXPathMessageSelector.class); + } + } + BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(FilterFactoryBean.class); + builder.addPropertyValue("targetObject", selectorBuilder.getBeanDefinition()); + IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "discard-channel"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "throw-exception-on-rejection"); + return builder; + } + + private void configureXPathExpression(Element element, BeanDefinitionBuilder selectorBuilder, ParserContext parserContext) { + String xPathExpressionRef = element.getAttribute("xpath-expression-ref"); + NodeList xPathExpressionNodes = element.getElementsByTagNameNS(element.getNamespaceURI(), "xpath-expression"); + Assert.isTrue(xPathExpressionNodes.getLength() <= 1, "At most one xpath-expression child may be specified."); + boolean xPathExpressionChildPresent = xPathExpressionNodes.getLength() == 1; + boolean xPathReferencePresent = StringUtils.hasText(xPathExpressionRef); + Assert.isTrue(xPathExpressionChildPresent ^ xPathReferencePresent, + "Exactly one of 'xpath-expression' or 'xpath-expression-ref' is required."); + if (xPathExpressionChildPresent) { + BeanDefinition beanDefinition = xpathParser.parse((Element) xPathExpressionNodes.item(0), parserContext); + selectorBuilder.addConstructorArgValue(beanDefinition); + } + else { + selectorBuilder.addConstructorArgReference(xPathExpressionRef); + } + } + +} diff --git a/spring-integration-xml/src/main/java/org/springframework/integration/xml/selector/RegexTestXPathMessageSelector.java b/spring-integration-xml/src/main/java/org/springframework/integration/xml/selector/RegexTestXPathMessageSelector.java new file mode 100644 index 0000000000..1dd23b7834 --- /dev/null +++ b/spring-integration-xml/src/main/java/org/springframework/integration/xml/selector/RegexTestXPathMessageSelector.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2011 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.selector; + +import java.util.Map; + +import org.w3c.dom.Node; + +import org.springframework.integration.Message; +import org.springframework.integration.core.MessageSelector; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.xml.xpath.XPathExpression; + +/** + * XPath {@link MessageSelector} that tests if a provided String value + * matches a given Regular Expression. + * + * @author Mark Fisher + * @since 2.1 + */ +public class RegexTestXPathMessageSelector extends AbstractXPathMessageSelector { + + private final String regex; + + + /** + * Creates a selector which attempts to match the given regex and supports multiple namespaces. + * + * @param expression XPath expression as a String + * @param namespaces Map of namespaces where the keys are namespace prefixes + * @param regex regular expression to match + */ + public RegexTestXPathMessageSelector(String expression, Map namespaces, String regex) { + super(expression, namespaces); + Assert.notNull(regex, "regex must not be null"); + this.regex = regex; + } + + /** + * Creates a selector which attempts to match the given regex and supports a single namespace. + * + * @param expression XPath expression as a String + * @param prefix namespace prefix + * @param namespace namespace URI + * @param regex regular expression to match + */ + public RegexTestXPathMessageSelector(String expression, String prefix, String namespace, String regex) { + super(expression, prefix, namespace); + Assert.notNull(regex, "regex must not be null"); + this.regex = regex; + } + + /** + * Creates a non-namespaced selector which attempts to match the given regex. + * + * @param expression XPath expression as a String + * @param regex regular expression to match + */ + public RegexTestXPathMessageSelector(String expression, String regex) { + super(expression); + Assert.notNull(regex, "regex must not be null"); + this.regex = regex; + } + + /** + * Creates a selector which attempts to match the given regex against the evaluation result + * of the provided {@link XPathExpression}. + * + * @param expression XPath expression + * @param regex regular expression to match + */ + public RegexTestXPathMessageSelector(XPathExpression expression, String regex) { + super(expression); + Assert.notNull(regex, "regex must not be null"); + this.regex = regex; + } + + + /** + * Evaluate the payload and return true if the value returned by the + * {@link XPathExpression} matches the regex. + */ + public boolean accept(Message message) { + Node nodeToTest = getConverter().convertToNode(message.getPayload()); + String xPathResult = getXPathExpresion().evaluateAsString(nodeToTest); + return StringUtils.hasText(xPathResult) ? xPathResult.matches(this.regex) : false; + } + +} diff --git a/spring-integration-xml/src/main/resources/org/springframework/integration/xml/config/spring-integration-xml-2.1.xsd b/spring-integration-xml/src/main/resources/org/springframework/integration/xml/config/spring-integration-xml-2.1.xsd index 35f85d7553..b743625446 100644 --- a/spring-integration-xml/src/main/resources/org/springframework/integration/xml/config/spring-integration-xml-2.1.xsd +++ b/spring-integration-xml/src/main/resources/org/springframework/integration/xml/config/spring-integration-xml-2.1.xsd @@ -446,11 +446,81 @@ + + + + Defines an XPath-based Message Filter. If the XPath expression will evaluate to a boolean, + no configuration attributes are required. If the XPath expression will evaluate to a String, + a "match-value" should be provided against which the evaluation result will be matched. + There are three options for the "match-type": exact, case-insensitive, and regex. These + correspond to the equals, equals-ignore-case, and matches operations on java.lang.String, + respectively. When providing a 'match-type' value of 'regex', the value provided in + 'match-value' must be a valid Regular Expression. + + + + + + + + + + The XPath expression to evaluate. + + + + + + + + Reference to an XPath expression instance to evaluate. + + + + + + + + + + + + String value to be matched against the XPath evaluation result. If this is not provided, + then the XPath evaluation MUST produce a boolean result directly. + + + + + + + Type of match to apply between the XPath evaluation result and the 'match-value'. + Default is "exact". + + + + + + + + + + + + + + + + + + + + Defines an XPath selector. + NOTE: this element is deprecated as of 2.1. Please use <xpath-filter> instead. @@ -547,62 +617,85 @@ + + + Defines an XML validating filter. + + + + + + + + Reference to a custom 'org.springframework.xml.validation.XmlValidator' strategy + + + + + + + + + + + + + + + + + + + + + + + + + + Base type for XML filters. + + + + + + + + + + + + + + + + - Defines an XML validating filter. + Message Channel where you want accepted messages to be sent. + + + + + - - - - - - - - - - - - - - - - - Allows you to plug-in custom 'org.springframework.xml.validation.XmlValidator' strategy - - - - - - - - - - - - - Allows you to point to a Message Channel where you want discarded messages to be sent. - - - - - - - - - - - - - - - - - - - - + + + + + Message Channel where you want rejected messages to be sent. + + + + + + + + + + diff --git a/spring-integration-xml/src/test/java/org/springframework/integration/xml/config/XPathFilterParserTests-context.xml b/spring-integration-xml/src/test/java/org/springframework/integration/xml/config/XPathFilterParserTests-context.xml new file mode 100644 index 0000000000..6ea3dc7a6a --- /dev/null +++ b/spring-integration-xml/src/test/java/org/springframework/integration/xml/config/XPathFilterParserTests-context.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-integration-xml/src/test/java/org/springframework/integration/xml/config/XPathFilterParserTests.java b/spring-integration-xml/src/test/java/org/springframework/integration/xml/config/XPathFilterParserTests.java new file mode 100644 index 0000000000..5425a56a8d --- /dev/null +++ b/spring-integration-xml/src/test/java/org/springframework/integration/xml/config/XPathFilterParserTests.java @@ -0,0 +1,156 @@ +/* + * Copyright 2002-2011 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.assertEquals; +import static org.junit.Assert.assertNull; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.integration.Message; +import org.springframework.integration.MessageChannel; +import org.springframework.integration.channel.QueueChannel; +import org.springframework.integration.core.PollableChannel; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.integration.xml.util.XmlTestUtil; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.w3c.dom.Document; + +/** + * @author Mark Fisher + * @since 2.1 + */ +@ContextConfiguration +@RunWith(SpringJUnit4ClassRunner.class) +public class XPathFilterParserTests { + + @Autowired + private ApplicationContext context; + + @Test + public void simpleStringExpressionBoolean() throws Exception { + MessageChannel inputChannel = context.getBean("booleanFilterInput", MessageChannel.class); + QueueChannel replyChannel = new QueueChannel(); + PollableChannel discardChannel = context.getBean("booleanFilterRejections", PollableChannel.class); + Message shouldBeAccepted = MessageBuilder.withPayload("outputOne").setReplyChannel(replyChannel).build(); + Message shouldBeRejected = MessageBuilder.withPayload("outputOne").setReplyChannel(replyChannel).build(); + inputChannel.send(shouldBeAccepted); + inputChannel.send(shouldBeRejected); + assertEquals(shouldBeAccepted, replyChannel.receive(0)); + assertEquals(shouldBeRejected, discardChannel.receive(0)); + assertNull(replyChannel.receive(0)); + assertNull(discardChannel.receive(0)); + } + + @Test + public void stringExpressionWithNamespaceBoolean() throws Exception { + MessageChannel inputChannel = context.getBean("booleanFilterWithNamespaceInput", MessageChannel.class); + QueueChannel replyChannel = new QueueChannel(); + PollableChannel discardChannel = context.getBean("booleanFilterWithNamespaceRejections", PollableChannel.class); + Document docToAccept = XmlTestUtil.getDocumentForString("outputOne"); + Document docToReject = XmlTestUtil.getDocumentForString("outputOne"); + Message shouldBeAccepted = MessageBuilder.withPayload(docToAccept).setReplyChannel(replyChannel).build(); + Message shouldBeRejected = MessageBuilder.withPayload(docToReject).setReplyChannel(replyChannel).build(); + inputChannel.send(shouldBeAccepted); + inputChannel.send(shouldBeRejected); + assertEquals(shouldBeAccepted, replyChannel.receive(0)); + assertEquals(shouldBeRejected, discardChannel.receive(0)); + assertNull(replyChannel.receive(0)); + assertNull(discardChannel.receive(0)); + } + + @Test + public void stringExpressionWithNestedMapBoolean() throws Exception { + MessageChannel inputChannel = context.getBean("nestedNamespaceMapFilterInput", MessageChannel.class); + QueueChannel replyChannel = new QueueChannel(); + PollableChannel discardChannel = context.getBean("nestedNamespaceMapFilterRejections", PollableChannel.class); + Document docToAccept = XmlTestUtil.getDocumentForString("outputOne"); + Document docToReject = XmlTestUtil.getDocumentForString("outputOne"); + Message shouldBeAccepted = MessageBuilder.withPayload(docToAccept).setReplyChannel(replyChannel).build(); + Message shouldBeRejected = MessageBuilder.withPayload(docToReject).setReplyChannel(replyChannel).build(); + inputChannel.send(shouldBeAccepted); + inputChannel.send(shouldBeRejected); + assertEquals(shouldBeAccepted, replyChannel.receive(0)); + assertEquals(shouldBeRejected, discardChannel.receive(0)); + assertNull(replyChannel.receive(0)); + assertNull(discardChannel.receive(0)); + } + + @Test + public void stringExpressionWithNamespaceString() throws Exception { + MessageChannel inputChannel = context.getBean("stringFilterWithNamespaceInput", MessageChannel.class); + QueueChannel replyChannel = new QueueChannel(); + PollableChannel discardChannel = context.getBean("stringFilterWithNamespaceRejections", PollableChannel.class); + Document docToAccept = XmlTestUtil.getDocumentForString("outputOne"); + Document docToReject = XmlTestUtil.getDocumentForString("outputOne"); + Message shouldBeAccepted = MessageBuilder.withPayload(docToAccept).setReplyChannel(replyChannel).build(); + Message shouldBeRejected = MessageBuilder.withPayload(docToReject).setReplyChannel(replyChannel).build(); + inputChannel.send(shouldBeAccepted); + inputChannel.send(shouldBeRejected); + assertEquals(shouldBeAccepted, replyChannel.receive(0)); + assertEquals(shouldBeRejected, discardChannel.receive(0)); + assertNull(replyChannel.receive(0)); + assertNull(discardChannel.receive(0)); + } + + @Test + public void stringExpressionIgnoresCase() throws Exception { + MessageChannel inputChannel = context.getBean("stringFilterIgnoresCaseInput", MessageChannel.class); + QueueChannel replyChannel = new QueueChannel(); + PollableChannel discardChannel = context.getBean("stringFilterIgnoresCaseRejections", PollableChannel.class); + Document docToAccept1 = XmlTestUtil.getDocumentForString("OUTPUTONE"); + Document docToAccept2 = XmlTestUtil.getDocumentForString("outputOne"); + Document docToReject = XmlTestUtil.getDocumentForString("outputTwo"); + Message shouldBeAccepted1 = MessageBuilder.withPayload(docToAccept1).setReplyChannel(replyChannel).build(); + Message shouldBeAccepted2 = MessageBuilder.withPayload(docToAccept2).setReplyChannel(replyChannel).build(); + Message shouldBeRejected = MessageBuilder.withPayload(docToReject).setReplyChannel(replyChannel).build(); + inputChannel.send(shouldBeAccepted1); + inputChannel.send(shouldBeAccepted2); + inputChannel.send(shouldBeRejected); + assertEquals(shouldBeAccepted1, replyChannel.receive(0)); + assertEquals(shouldBeAccepted2, replyChannel.receive(0)); + assertEquals(shouldBeRejected, discardChannel.receive(0)); + assertNull(replyChannel.receive(0)); + assertNull(discardChannel.receive(0)); + } + + @Test + public void stringExpressionRegex() throws Exception { + MessageChannel inputChannel = context.getBean("stringFilterRegexInput", MessageChannel.class); + QueueChannel replyChannel = new QueueChannel(); + PollableChannel discardChannel = context.getBean("stringFilterRegexRejections", PollableChannel.class); + Document docToAccept1 = XmlTestUtil.getDocumentForString("aBcDeFgHiJk"); + Document docToAccept2 = XmlTestUtil.getDocumentForString("xyz"); + Document docToReject = XmlTestUtil.getDocumentForString("abc123"); + Message shouldBeAccepted1 = MessageBuilder.withPayload(docToAccept1).setReplyChannel(replyChannel).build(); + Message shouldBeAccepted2 = MessageBuilder.withPayload(docToAccept2).setReplyChannel(replyChannel).build(); + Message shouldBeRejected = MessageBuilder.withPayload(docToReject).setReplyChannel(replyChannel).build(); + inputChannel.send(shouldBeAccepted1); + inputChannel.send(shouldBeAccepted2); + inputChannel.send(shouldBeRejected); + assertEquals(shouldBeAccepted1, replyChannel.receive(0)); + assertEquals(shouldBeAccepted2, replyChannel.receive(0)); + assertEquals(shouldBeRejected, discardChannel.receive(0)); + assertNull(replyChannel.receive(0)); + assertNull(discardChannel.receive(0)); + } + +}