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)
This commit is contained in:
Artem Bilan
2013-11-04 17:30:46 +02:00
committed by Gary Russell
parent fa59b50bdc
commit 88de8117aa
11 changed files with 484 additions and 16 deletions

View File

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

View File

@@ -301,6 +301,7 @@ public class ChainParserTests {
final AtomicReference<String> log = new AtomicReference<String>();
when(logger.isWarnEnabled()).thenReturn(true);
doAnswer(new Answer<Object>() {
@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"));

View File

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

View File

@@ -171,7 +171,7 @@ public class SpelTransformerIntegrationTests {
@Override
public Class<?>[] getSpecificTargetClasses() {
return new Class[] {Foo.class};
return new Class<?>[] {Foo.class};
}
@Override

View File

@@ -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<String> 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> 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<Node> nodeList = (List<Node>) XPathEvaluationType.NODE_LIST_RESULT.evaluateXPath(expression, node);
try {
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
List<Node> documents = new ArrayList<Node>(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() {
}
}

View File

@@ -0,0 +1,39 @@
<?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"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd">
<int:channel id="channelA">
<int:queue/>
</int:channel>
<int:channel id="channelB">
<int:queue/>
</int:channel>
<int:channel id="channelZ">
<int:queue/>
</int:channel>
<int:transformer input-channel="xpathTransformerInput" output-channel="channelA"
expression="#xpath(payload, headers.xpath, @testNodeMapper)"/>
<int:filter input-channel="xpathFilterInput" output-channel="channelA" discard-channel="channelZ"
expression="#xpath(payload, '/name', 'boolean')"/>
<int:splitter input-channel="xpathSplitterInput" output-channel="channelA"
expression="#xpath(payload, '//book', 'document_list')"/>
<int:router input-channel="xpathRouterInput"
expression="#xpath(payload, '/name')"
resolution-required="false"
default-output-channel="channelZ">
<int:mapping value="A" channel="channelA"/>
<int:mapping value="B" channel="channelB"/>
</int:router>
<bean id="testNodeMapper" class="org.springframework.integration.xml.xpath.XPathTests$TestNodeMapper"/>
</beans>

View File

@@ -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 = "<parent><child name='test' age='42' married='true'/></parent>";
@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 name='foo'/><child name='bar'/></parent>", "/parent/child", "node_list");
assertThat(result, Matchers.instanceOf(List.class));
List<Node> nodeList = (List<Node>) 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 name='foo'/><child name='bar'/></parent>", "/parent/child", "document_list");
assertThat(result, Matchers.instanceOf(List.class));
List<Document> documentList = (List<Document>) 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("<person name='John Doe' age='42' married='true'/>")
.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<Object>("<name>outputOne</name>"));
this.xpathFilterInput.send(new GenericMessage<Object>("<other>outputOne</other>"));
Message<?> receive = this.channelA.receive(1000);
assertNotNull(receive);
assertEquals("<name>outputOne</name>", receive.getPayload());
receive = this.channelZ.receive(1000);
assertNotNull(receive);
assertEquals("<other>outputOne</other>", receive.getPayload());
}
@Test
public void testInt3140Splitter() {
StringSourceFactory stringSourceFactory = new StringSourceFactory();
this.xpathSplitterInput.send(new GenericMessage<Object>("<books><book>book1</book><book>book2</book></books>"));
Message<?> receive = this.channelA.receive(1000);
assertNotNull(receive);
assertThat(stringSourceFactory.createSource(receive.getPayload()).toString(), Matchers.containsString("<book>book1</book>"));
receive = this.channelA.receive(1000);
assertNotNull(receive);
assertThat(stringSourceFactory.createSource(receive.getPayload()).toString(), Matchers.containsString("<book>book2</book>"));
}
@Test
public void testInt3140Router() {
this.xpathRouterInput.send(new GenericMessage<Object>("<name>A</name>"));
this.xpathRouterInput.send(new GenericMessage<Object>("<name>B</name>"));
this.xpathRouterInput.send(new GenericMessage<Object>("<name>X</name>"));
Message<?> receive = this.channelA.receive(1000);
assertNotNull(receive);
assertEquals("<name>A</name>", receive.getPayload());
receive = this.channelB.receive(1000);
assertNotNull(receive);
assertEquals("<name>B</name>", receive.getPayload());
receive = this.channelZ.receive(1000);
assertNotNull(receive);
assertEquals("<name>X</name>", receive.getPayload());
}
public static class TestNodeMapper implements NodeMapper<String> {
@Override
public String mapNode(Node node, int nodeNum) throws DOMException {
return node.getTextContent() + "-mapped";
}
}
}

View File

@@ -176,13 +176,11 @@
</note>
For more information regarding JSON see 'JSON Transformers' in <xref linkend="transformer"/>.
</listitem>
<listitem>
<emphasis role="bold">#xpath</emphasis> - to evaluate an 'xpath' on some provided object.
For more information regarding xml and xpath see <xref linkend="xml"/>.
</listitem>
<!--<listitem>
<emphasis>#xpath</emphasis> - TBD
</listitem>
<listitem>
<emphasis>#request</emphasis> - TBD
</listitem>
<listitem>
<emphasis>#auth</emphasis> - TBD
</listitem>-->
</itemizedlist>

View File

@@ -361,6 +361,13 @@ public class Foo {
In addition to JSON Transformers, Spring Integration provides a built-in <emphasis>#jsonPath</emphasis>
SpEL function for use in expressions. For more information see <xref linkend="spel"/>.
</para>
<para>
<emphasis role="bold">#xpath SpEL Function</emphasis>
</para>
<para>
Since version <emphasis>3.0</emphasis>, Spring Integration also provides a built-in <emphasis>#xpath</emphasis>
SpEL function for use in expressions. For more information see <xref linkend="xpath-spel-function"/>.
</para>
</section>
<section id="transformer-annotation">

View File

@@ -117,8 +117,9 @@
<title>SpEL Functions Support</title>
<para>
To customize the SpEL <interfacename>EvaluationContext</interfacename> with static
<classname>Method</classname> functions the new <code>&lt;spel-function/&gt;</code>
component is introduced. For more information see <xref linkend="spel-functions" />.
<classname>Method</classname> functions, the new <code>&lt;spel-function/&gt;</code>
component is introduced. Two built-in functions are also provided (<code>#jsonPath</code>
and <code>#xpath</code>). For more information see <xref linkend="spel-functions" />.
</para>
</section>
<section id="3.0-spel-property-accessors">

View File

@@ -34,6 +34,9 @@
<listitem>
<para><emphasis><link linkend='xml-xpath-filter'>XPath Filter</link></emphasis></para>
</listitem>
<listitem>
<para><emphasis><link linkend='xpath-spel-function'>#xpath SpEL Function</link></emphasis></para>
</listitem>
<listitem>
<para><emphasis><link linkend='xml-validating-filter'>Validating Filter</link></emphasis></para>
</listitem>
@@ -1253,6 +1256,39 @@
</calloutlist></para>
</section>
<section id="xpath-spel-function">
<title>#xpath SpEL Function</title>
<para>
Spring Integration, since version <emphasis>3.0</emphasis>, provides the <code>#xpath</code>
built-in SpEL function, which invokes the static method <code>XPathUtils.evaluate(...)</code>.
This method delegates to an <interfacename>org.springframework.xml.xpath.XPathExpression</interfacename>.
The following shows some usage examples:
<programlisting language="xml"><![CDATA[<transformer expression="#xpath(payload, '/name')"/>
<filter expression="#xpath(payload, headers.xpath, 'boolean')"/>
<splitter expression="#xpath(payload, '//book', 'document_list')"/>
<router expression="#xpath(payload, '/person/@age', 'number')">
<mapping channel="output1" value="16"/>
<mapping channel="output2" value="45"/>
</router>]]></programlisting>
<code>#xpath</code> also supports a third optional parameter for converting the result of the xpath evaluation.
It can be
one of the String constants <code>'string'</code>, <code>'boolean'</code>, <code>'number'</code>,
<code>'node'</code>, <code>'node_list'</code> and <code>'document_list'</code> or an
<interfacename>org.springframework.xml.xpath.NodeMapper</interfacename> instance.
By default the <code>#xpath</code> SpEL function returns a String representation of the xpath evaluation.
</para>
<note>
To enable the <code>#xpath</code> SpEL function, simply add the <code>spring-integration-xml.jar</code>
to the CLASSPATH; there is no need to declare any component(s) from the Spring Integration Xml Namespace.
</note>
<para>
For more information see <xref linkend="spel"/>.
</para>
</section>
<section id="xml-validating-filter">
<title>XML Validating Filter</title>
<para>