From 88de8117aa23f204fb20ce6528a5166a47f035ed Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 4 Nov 2013 17:30:46 +0200 Subject: [PATCH] INT-3140: Add `#xpath()` SpEL Function Support JIRA: https://jira.springsource.org/browse/INT-3140 * Introduce `XPathUtils` * Add `#xpath()` tests * Add documentation INT-3140: Polishing according PR comments INT-3140 Polishing - Doc and javadoc polishing - Fix some compiler warnings (not all xpath) --- .../AbstractIntegrationNamespaceHandler.java | 25 ++ .../integration/config/ChainParserTests.java | 3 +- .../integration/json/JsonPathTests.java | 15 +- .../SpelTransformerIntegrationTests.java | 2 +- .../integration/xml/xpath/XPathUtils.java | 131 ++++++++++ .../xml/xpath/XPathTests-context.xml | 39 +++ .../integration/xml/xpath/XPathTests.java | 227 ++++++++++++++++++ src/reference/docbook/spel.xml | 10 +- src/reference/docbook/transformer.xml | 7 + src/reference/docbook/whats-new.xml | 5 +- src/reference/docbook/xml.xml | 36 +++ 11 files changed, 484 insertions(+), 16 deletions(-) create mode 100644 spring-integration-xml/src/main/java/org/springframework/integration/xml/xpath/XPathUtils.java create mode 100644 spring-integration-xml/src/test/java/org/springframework/integration/xml/xpath/XPathTests-context.xml create mode 100644 spring-integration-xml/src/test/java/org/springframework/integration/xml/xpath/XPathTests.java diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/AbstractIntegrationNamespaceHandler.java b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/AbstractIntegrationNamespaceHandler.java index dd65c4337f..ba610212cf 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/AbstractIntegrationNamespaceHandler.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/AbstractIntegrationNamespaceHandler.java @@ -174,6 +174,31 @@ public abstract class AbstractIntegrationNamespaceHandler implements NamespaceHa } } + String xpathBeanName = "xpath"; + alreadyRegistered = false; + if (parserContext.getRegistry() instanceof ListableBeanFactory) { + alreadyRegistered = ((ListableBeanFactory) parserContext.getRegistry()).containsBean(xpathBeanName); + } + else { + alreadyRegistered = parserContext.getRegistry().isBeanNameInUse(xpathBeanName); + } + if (!alreadyRegistered) { + Class xpathClass = null; + try { + xpathClass = ClassUtils.forName(IntegrationNamespaceUtils.BASE_PACKAGE + ".xml.xpath.XPathUtils", + parserContext.getReaderContext().getBeanClassLoader()); + } + catch (ClassNotFoundException e) { + logger.debug("SpEL function '#xpath' isn't registered: there is no spring-integration-xml.jar on the classpath."); + } + + if (xpathClass != null) { + IntegrationNamespaceUtils.registerSpelFunctionBean(parserContext.getRegistry(), xpathBeanName, + IntegrationNamespaceUtils.BASE_PACKAGE + ".xml.xpath.XPathUtils", "evaluate"); + } + } + + this.doRegisterBuiltInBeans(parserContext); } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/ChainParserTests.java b/spring-integration-core/src/test/java/org/springframework/integration/config/ChainParserTests.java index d07774fd09..bcc3c2fccb 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/ChainParserTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/ChainParserTests.java @@ -301,6 +301,7 @@ public class ChainParserTests { final AtomicReference log = new AtomicReference(); when(logger.isWarnEnabled()).thenReturn(true); doAnswer(new Answer() { + @Override public Object answer(InvocationOnMock invocation) throws Throwable { log.set((String) invocation.getArguments()[0]); return null; @@ -394,7 +395,7 @@ public class ChainParserTests { assertTrue(this.beanFactory.containsBean("recipientListRouterChain$child.recipientListRouterWithinChain.handler")); MessageHandlerChain chain = this.beanFactory.getBean("headerEnricherChain.handler", MessageHandlerChain.class); - List handlers = TestUtils.getPropertyValue(chain, "handlers", List.class); + List handlers = TestUtils.getPropertyValue(chain, "handlers", List.class); assertTrue(handlers.get(0) instanceof MessageTransformingHandler); assertEquals("headerEnricherChain$child.headerEnricherWithinChain", TestUtils.getPropertyValue(handlers.get(0), "componentName")); diff --git a/spring-integration-core/src/test/java/org/springframework/integration/json/JsonPathTests.java b/spring-integration-core/src/test/java/org/springframework/integration/json/JsonPathTests.java index 7fae338604..8e8be5bdec 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/json/JsonPathTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/json/JsonPathTests.java @@ -28,14 +28,11 @@ import java.io.IOException; import java.util.Map; import java.util.Scanner; +import org.hamcrest.Matchers; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; -import org.hamcrest.Matchers; - -import com.jayway.jsonpath.Criteria; -import com.jayway.jsonpath.Filter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -51,8 +48,12 @@ import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.support.AnnotationConfigContextLoader; +import com.jayway.jsonpath.Criteria; +import com.jayway.jsonpath.Filter; + /** * @author Artem Bilan + * @author Gary Russell * @since 3.0 */ @ContextConfiguration(classes = JsonPathTests.JsonPathTestsContextConfiguration.class, loader = AnnotationConfigContextLoader.class) @@ -69,7 +70,9 @@ public class JsonPathTests { public static void setUp() throws IOException { ClassPathResource jsonResource = new ClassPathResource("JsonPathTests.json", JsonPathTests.class); JSON_FILE = jsonResource.getFile(); - JSON = new Scanner(JSON_FILE).useDelimiter("\\Z").next(); + Scanner scanner = new Scanner(JSON_FILE); + JSON = scanner.useDelimiter("\\Z").next(); + scanner.close(); testMessage = new GenericMessage(JSON); } @@ -208,7 +211,7 @@ public class JsonPathTests { public static class JsonPathTestsContextConfiguration { @Bean - public Filter jsonPathFilter() { + public Filter jsonPathFilter() { return Filter.filter(Criteria.where("isbn").exists(true).and("category").ne("fiction")); } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/transformer/SpelTransformerIntegrationTests.java b/spring-integration-core/src/test/java/org/springframework/integration/transformer/SpelTransformerIntegrationTests.java index 056624fc97..bba626a873 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/transformer/SpelTransformerIntegrationTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/transformer/SpelTransformerIntegrationTests.java @@ -171,7 +171,7 @@ public class SpelTransformerIntegrationTests { @Override public Class[] getSpecificTargetClasses() { - return new Class[] {Foo.class}; + return new Class[] {Foo.class}; } @Override diff --git a/spring-integration-xml/src/main/java/org/springframework/integration/xml/xpath/XPathUtils.java b/spring-integration-xml/src/main/java/org/springframework/integration/xml/xpath/XPathUtils.java new file mode 100644 index 0000000000..d536879c86 --- /dev/null +++ b/spring-integration-xml/src/main/java/org/springframework/integration/xml/xpath/XPathUtils.java @@ -0,0 +1,131 @@ +/* + * 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.xml.xpath; + + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.w3c.dom.Document; +import org.w3c.dom.Node; + +import org.springframework.integration.xml.DefaultXmlPayloadConverter; +import org.springframework.integration.xml.XmlPayloadConverter; +import org.springframework.util.Assert; +import org.springframework.xml.xpath.NodeMapper; +import org.springframework.xml.xpath.XPathException; +import org.springframework.xml.xpath.XPathExpression; +import org.springframework.xml.xpath.XPathExpressionFactory; + +/** + * Utility class for 'xpath' support. + * + * @author Artem Bilan + * @since 3.0 + */ +public final class XPathUtils { + + public static final String STRING = "string"; + + public static final String BOOLEAN = "boolean"; + + public static final String NUMBER = "number"; + + public static final String NODE = "node"; + + public static final String NODE_LIST = "node_list"; + + public static final String DOCUMENT_LIST = "document_list"; + + private static List RESULT_TYPES = Arrays.asList(STRING, BOOLEAN, NUMBER, NODE, NODE_LIST, DOCUMENT_LIST); + + private static XmlPayloadConverter converter = new DefaultXmlPayloadConverter(); + + private static DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + + /** + * Utility method to evaluate an xpath on the provided object. + * Delegates evaluation to an {@link XPathExpression}. + * Note this method provides the {@code #xpath()} SpEL function. + * + * + * @param o the xml Object for evaluaton. + * @param xpath an 'xpath' expression String. + * @param resultArg an optional parameter to represent the result type of the xpath evaluation. + * Only one argument is allowed, which can be an instance of {@link org.springframework.xml.xpath.NodeMapper} or + * one of these String constants: "string", "boolean", "number", "node" or "node_list". + * @return the result of the xpath expression evaluation. + * @throws IllegalArgumentException - if the provided arguments aren't appropriate types or values; + * @throws MessagingException - if the provided object can't be converted to a {@link Node}; + * @throws XPathException - if the xpath expression can't be evaluated. + */ + @SuppressWarnings({"unchecked"}) + public static T evaluate(Object o, String xpath, Object... resultArg) { + Object resultType = null; + if (resultArg != null && resultArg.length > 0) { + Assert.isTrue(resultArg.length == 1, "'resultArg' can contains only one element."); + Assert.noNullElements(resultArg, "'resultArg' can't contains 'null' elements."); + resultType = resultArg[0]; + } + + XPathExpression expression = XPathExpressionFactory.createXPathExpression(xpath); + Node node = converter.convertToNode(o); + + if (resultType == null) { + return (T) expression.evaluateAsString(node); + } + else if (resultType instanceof NodeMapper) { + return (T) expression.evaluateAsObject(node, (NodeMapper) resultType); + } + else if (resultType instanceof String && RESULT_TYPES.contains(resultType)) { + String resType = (String) resultType; + if (DOCUMENT_LIST.equals(resType)) { + List nodeList = (List) XPathEvaluationType.NODE_LIST_RESULT.evaluateXPath(expression, node); + try { + DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); + List documents = new ArrayList(nodeList.size()); + for (Node n : nodeList) { + Document document = documentBuilder.newDocument(); + document.appendChild(document.importNode(n, true)); + documents.add(document); + } + return (T) documents; + } + catch (ParserConfigurationException e) { + throw new XPathException("Unable to create 'documentBuilder'.", e); + } + } + else { + XPathEvaluationType evaluationType = XPathEvaluationType.valueOf(resType.toUpperCase() + "_RESULT"); + return (T) evaluationType.evaluateXPath(expression, node); + } + } + else { + throw new IllegalArgumentException("'resultArg[0]' can be an instance of 'NodeMapper' " + + "or one of supported String constants: " + RESULT_TYPES); + } + } + + private XPathUtils() { + } + +} diff --git a/spring-integration-xml/src/test/java/org/springframework/integration/xml/xpath/XPathTests-context.xml b/spring-integration-xml/src/test/java/org/springframework/integration/xml/xpath/XPathTests-context.xml new file mode 100644 index 0000000000..961aac8eca --- /dev/null +++ b/spring-integration-xml/src/test/java/org/springframework/integration/xml/xpath/XPathTests-context.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-integration-xml/src/test/java/org/springframework/integration/xml/xpath/XPathTests.java b/spring-integration-xml/src/test/java/org/springframework/integration/xml/xpath/XPathTests.java new file mode 100644 index 0000000000..fab8b6fb39 --- /dev/null +++ b/spring-integration-xml/src/test/java/org/springframework/integration/xml/xpath/XPathTests.java @@ -0,0 +1,227 @@ +/* + * 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.xml.xpath; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; +import static org.springframework.integration.xml.xpath.XPathUtils.evaluate; + +import java.util.Date; +import java.util.List; + +import org.hamcrest.Matchers; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.w3c.dom.DOMException; +import org.w3c.dom.Document; +import org.w3c.dom.Node; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.integration.Message; +import org.springframework.integration.MessageChannel; +import org.springframework.integration.MessagingException; +import org.springframework.integration.core.PollableChannel; +import org.springframework.integration.message.GenericMessage; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.integration.xml.source.StringSourceFactory; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.xml.xpath.NodeMapper; + +/** + * @author Artem Bilan + * @since 3.0 + */ +@ContextConfiguration +@RunWith(SpringJUnit4ClassRunner.class) +public class XPathTests { + + private static final String XML = ""; + + @Autowired + private PollableChannel channelA; + + @Autowired + private PollableChannel channelB; + + @Autowired + private PollableChannel channelZ; + + @Autowired + private MessageChannel xpathTransformerInput; + + @Autowired + private MessageChannel xpathFilterInput; + + @Autowired + private MessageChannel xpathSplitterInput; + + @Autowired + private MessageChannel xpathRouterInput; + + @Test + @SuppressWarnings("unchecked") + public void testXPathUtils() { + Object result = evaluate(XML, "/parent/child/@name"); + assertEquals("test", result); + + result = evaluate(XML, "/parent/child/@name", "string"); + assertEquals("test", result); + + result = evaluate(XML, "/parent/child/@age", "number"); + assertEquals((double) 42, result); + + result = evaluate(XML, "/parent/child/@married = 'true'", "boolean"); + assertEquals(Boolean.TRUE, result); + + result = evaluate(XML, "/parent/child", "node"); + assertThat(result, Matchers.instanceOf(Node.class)); + Node node = (Node) result; + assertEquals("child", node.getLocalName()); + assertEquals("test", node.getAttributes().getNamedItem("name").getTextContent()); + assertEquals("42", node.getAttributes().getNamedItem("age").getTextContent()); + assertEquals("true", node.getAttributes().getNamedItem("married").getTextContent()); + + result = evaluate("", "/parent/child", "node_list"); + assertThat(result, Matchers.instanceOf(List.class)); + List nodeList = (List) result; + assertEquals(2, nodeList.size()); + Node node1 = nodeList.get(0); + Node node2 = nodeList.get(1); + assertEquals("child", node1.getLocalName()); + assertEquals("foo", node1.getAttributes().getNamedItem("name").getTextContent()); + assertEquals("child", node2.getLocalName()); + assertEquals("bar", node2.getAttributes().getNamedItem("name").getTextContent()); + + result = evaluate("", "/parent/child", "document_list"); + assertThat(result, Matchers.instanceOf(List.class)); + List documentList = (List) result; + assertEquals(2, documentList.size()); + Node document1 = documentList.get(0); + Node document2 = documentList.get(1); + assertEquals("child", document1.getFirstChild().getLocalName()); + assertEquals("foo", document1.getFirstChild().getAttributes().getNamedItem("name").getTextContent()); + assertEquals("child", document2.getFirstChild().getLocalName()); + assertEquals("bar", document2.getFirstChild().getAttributes().getNamedItem("name").getTextContent()); + + result = evaluate(XML, "/parent/child/@name", new TestNodeMapper()); + assertEquals("test-mapped", result); + + try { + evaluate(new Date(), "/parent/child"); + fail("MessagingException expected."); + } + catch (Exception e) { + assertThat(e, Matchers.instanceOf(MessagingException.class)); + assertThat(e.getMessage(), Matchers.containsString("unsupported payload type")); + } + + try { + evaluate(XML, "/parent/child", "string", "number"); + fail("MessagingException expected."); + } + catch (Exception e) { + assertThat(e, Matchers.instanceOf(IllegalArgumentException.class)); + assertEquals("'resultArg' can contains only one element.", e.getMessage()); + } + + try { + evaluate(XML, "/parent/child", "foo"); + fail("MessagingException expected."); + } + catch (Exception e) { + assertThat(e, Matchers.instanceOf(IllegalArgumentException.class)); + assertEquals("'resultArg[0]' can be an instance of 'NodeMapper' or " + + "one of supported String constants: [string, boolean, number, node, node_list, document_list]", e.getMessage()); + } + + } + + @Test + public void testInt3140Transformer() { + Message message = MessageBuilder.withPayload("") + .setHeader("xpath", "/person/@age") + .build(); + + this.xpathTransformerInput.send(message); + + Message receive = this.channelA.receive(1000); + assertNotNull(receive); + assertEquals("42-mapped", receive.getPayload()); + } + + @Test + public void testInt3140Filter() { + this.xpathFilterInput.send(new GenericMessage("outputOne")); + this.xpathFilterInput.send(new GenericMessage("outputOne")); + + Message receive = this.channelA.receive(1000); + assertNotNull(receive); + assertEquals("outputOne", receive.getPayload()); + + receive = this.channelZ.receive(1000); + assertNotNull(receive); + assertEquals("outputOne", receive.getPayload()); + } + + @Test + public void testInt3140Splitter() { + StringSourceFactory stringSourceFactory = new StringSourceFactory(); + this.xpathSplitterInput.send(new GenericMessage("book1book2")); + + Message receive = this.channelA.receive(1000); + assertNotNull(receive); + assertThat(stringSourceFactory.createSource(receive.getPayload()).toString(), Matchers.containsString("book1")); + + receive = this.channelA.receive(1000); + assertNotNull(receive); + assertThat(stringSourceFactory.createSource(receive.getPayload()).toString(), Matchers.containsString("book2")); + } + + + @Test + public void testInt3140Router() { + this.xpathRouterInput.send(new GenericMessage("A")); + this.xpathRouterInput.send(new GenericMessage("B")); + this.xpathRouterInput.send(new GenericMessage("X")); + + Message receive = this.channelA.receive(1000); + assertNotNull(receive); + assertEquals("A", receive.getPayload()); + + receive = this.channelB.receive(1000); + assertNotNull(receive); + assertEquals("B", receive.getPayload()); + + receive = this.channelZ.receive(1000); + assertNotNull(receive); + assertEquals("X", receive.getPayload()); + } + + + public static class TestNodeMapper implements NodeMapper { + + @Override + public String mapNode(Node node, int nodeNum) throws DOMException { + return node.getTextContent() + "-mapped"; + } + + } + +} diff --git a/src/reference/docbook/spel.xml b/src/reference/docbook/spel.xml index 157ea4ca0b..6518653bb5 100644 --- a/src/reference/docbook/spel.xml +++ b/src/reference/docbook/spel.xml @@ -176,13 +176,11 @@ For more information regarding JSON see 'JSON Transformers' in . + + #xpath - to evaluate an 'xpath' on some provided object. + For more information regarding xml and xpath see . + diff --git a/src/reference/docbook/transformer.xml b/src/reference/docbook/transformer.xml index 495664b267..9caf015cc2 100644 --- a/src/reference/docbook/transformer.xml +++ b/src/reference/docbook/transformer.xml @@ -361,6 +361,13 @@ public class Foo { In addition to JSON Transformers, Spring Integration provides a built-in #jsonPath SpEL function for use in expressions. For more information see . + + #xpath SpEL Function + + + Since version 3.0, Spring Integration also provides a built-in #xpath + SpEL function for use in expressions. For more information see . +
diff --git a/src/reference/docbook/whats-new.xml b/src/reference/docbook/whats-new.xml index 08372f48d3..2cbea47e51 100644 --- a/src/reference/docbook/whats-new.xml +++ b/src/reference/docbook/whats-new.xml @@ -117,8 +117,9 @@ SpEL Functions Support To customize the SpEL EvaluationContext with static - Method functions the new <spel-function/> - component is introduced. For more information see . + Method functions, the new <spel-function/> + component is introduced. Two built-in functions are also provided (#jsonPath + and #xpath). For more information see .
diff --git a/src/reference/docbook/xml.xml b/src/reference/docbook/xml.xml index 427d4c0723..5ee55ef17a 100644 --- a/src/reference/docbook/xml.xml +++ b/src/reference/docbook/xml.xml @@ -34,6 +34,9 @@ XPath Filter + + #xpath SpEL Function + Validating Filter @@ -1253,6 +1256,39 @@
+
+ #xpath SpEL Function + + Spring Integration, since version 3.0, provides the #xpath + built-in SpEL function, which invokes the static method XPathUtils.evaluate(...). + This method delegates to an org.springframework.xml.xpath.XPathExpression. + The following shows some usage examples: + + + + + + + + + +]]> + #xpath also supports a third optional parameter for converting the result of the xpath evaluation. + It can be + one of the String constants 'string', 'boolean', 'number', + 'node', 'node_list' and 'document_list' or an + org.springframework.xml.xpath.NodeMapper instance. + By default the #xpath SpEL function returns a String representation of the xpath evaluation. + + + To enable the #xpath SpEL function, simply add the spring-integration-xml.jar + to the CLASSPATH; there is no need to declare any component(s) from the Spring Integration Xml Namespace. + + + For more information see . + +
+
XML Validating Filter