INT-3599/INT-2042: Improve XPathMessageSplitter

JIRA: https://jira.spring.io/browse/INT-2042,
https://jira.spring.io/browse/INT-3599

* Expose `outputProperties` for the `XPathMessageSplitter`
* Add `iterator` mode for the `XPathMessageSplitter`

Address PR comments

Doc Polishing
This commit is contained in:
Artem Bilan
2015-04-14 21:30:39 +03:00
committed by Gary Russell
parent 8059566b9b
commit 256ff4c46f
9 changed files with 306 additions and 73 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2015 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.
@@ -20,7 +20,8 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.config.BeanDefinition;
import org.w3c.dom.Element;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser;
import org.springframework.beans.factory.xml.BeanDefinitionParserDelegate;
@@ -29,13 +30,13 @@ import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.util.xml.DomUtils;
import org.springframework.xml.xpath.XPathExpressionFactory;
import org.w3c.dom.Element;
/**
* Parser for the <xpath-expression> element.
*
* @author Jonas Partner
* @author Soby Chacko
* @author Artem Bilan
*/
public class XPathExpressionParser extends AbstractSingleBeanDefinitionParser {
@@ -58,6 +59,15 @@ public class XPathExpressionParser extends AbstractSingleBeanDefinitionParser {
protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
String expression = element.getAttribute("expression");
Assert.hasText(expression, "The 'expression' attribute is required.");
builder.setFactoryMethod("createXPathExpression");
builder.addConstructorArgValue(expression);
parseAndPopulateNamespaceMap(element, parserContext, builder);
}
static void parseAndPopulateNamespaceMap(Element element, ParserContext parserContext,
BeanDefinitionBuilder builder) {
String nsPrefix = element.getAttribute("ns-prefix");
String nsUri = element.getAttribute("ns-uri");
String namespaceMapRef = element.getAttribute("namespace-map");
@@ -73,36 +83,32 @@ public class XPathExpressionParser extends AbstractSingleBeanDefinitionParser {
if (prefixProvided || namespaceProvided) {
Assert.isTrue(prefixProvided && namespaceProvided,
"Both 'ns-prefix' and 'ns-uri' must be specified if one is specified.");
Assert.isTrue(!namespaceMapProvided, "It is not valid to specify both, the namespace attributes ('ns-prefix' and 'ns-uri') and the 'namespace-map' attribute.");
Assert.isTrue(!mapSubElementProvided, "It is not valid to specify both, the namespace attributes ('ns-prefix' and 'ns-uri') and the 'map' sub-element.");
} else if (mapSubElementProvided) {
Assert.isTrue(!namespaceMapProvided, "It is not valid to specify both, the 'namespace-map' attribute and the 'map' sub-element.");
Assert.isTrue(!namespaceMapProvided, "It is not valid to specify both, " +
"the namespace attributes ('ns-prefix' and 'ns-uri') and the 'namespace-map' attribute.");
Assert.isTrue(!mapSubElementProvided, "It is not valid to specify both, " +
"the namespace attributes ('ns-prefix' and 'ns-uri') and the 'map' sub-element.");
}
else if (mapSubElementProvided) {
Assert.isTrue(!namespaceMapProvided, "It is not valid to specify both, " +
"the 'namespace-map' attribute and the 'map' sub-element.");
}
builder.setFactoryMethod("createXPathExpression");
builder.addConstructorArgValue(expression);
if (prefixProvided) {
Map<String, String> namespaceMap = new HashMap<String, String>(1);
namespaceMap.put(nsPrefix, nsUri);
builder.addConstructorArgValue(namespaceMap);
} else if (namespaceMapProvided) {
}
else if (namespaceMapProvided) {
builder.addConstructorArgReference(namespaceMapRef);
} else if (mapSubElementProvided) {
}
else if (mapSubElementProvided) {
Element mapElement = mapElements.get(0);
if (mapElement != null) {
builder.addConstructorArgValue(this.parseNamespaceMapElement(
mapElement, parserContext, builder.getBeanDefinition()));
BeanDefinitionParserDelegate beanParser = parserContext.getDelegate();
beanParser.initDefaults(mapElement.getOwnerDocument().getDocumentElement(), beanParser);
builder.addConstructorArgValue(beanParser.parseMapElement(mapElement, builder.getRawBeanDefinition()));
}
}
}
protected Map<?,?> parseNamespaceMapElement(Element element, ParserContext parserContext, BeanDefinition parentDefinition) {
BeanDefinitionParserDelegate beanParser = parserContext.getDelegate();
beanParser.initDefaults(element.getOwnerDocument().getDocumentElement(), beanParser);
return beanParser.parseMapElement(element, parentDefinition);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2015 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.
@@ -19,7 +19,6 @@ 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.xml.AbstractConsumerEndpointParser;
@@ -34,8 +33,6 @@ import org.springframework.util.StringUtils;
*/
public class XPathMessageSplitterParser extends AbstractConsumerEndpointParser {
private final XPathExpressionParser xpathParser = new XPathExpressionParser();
@Override
protected BeanDefinitionBuilder parseHandler(Element element, ParserContext parserContext) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(XPathMessageSplitter.class);
@@ -46,8 +43,9 @@ public class XPathMessageSplitterParser extends AbstractConsumerEndpointParser {
boolean hasReference = StringUtils.hasText(xPathExpressionRef);
Assert.isTrue(hasChild ^ hasReference, "Exactly one of 'xpath-expression' or 'xpath-expression-ref' is required.");
if (hasChild) {
BeanDefinition beanDefinition = this.xpathParser.parse((Element) xPathExpressionNodes.item(0), parserContext);
builder.addConstructorArgValue(beanDefinition);
Element xpathExpressionElement = (Element) xPathExpressionNodes.item(0);
builder.addConstructorArgValue(xpathExpressionElement.getAttribute("expression"));
XPathExpressionParser.parseAndPopulateNamespaceMap(xpathExpressionElement, parserContext, builder);
}
else {
builder.addConstructorArgReference(xPathExpressionRef);
@@ -55,6 +53,8 @@ public class XPathMessageSplitterParser extends AbstractConsumerEndpointParser {
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "doc-builder-factory", "documentBuilder");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "create-documents");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "apply-sequence");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "iterator");
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "output-properties");
return builder;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2010 the original author or authors.
* Copyright 2002-2015 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.
@@ -19,27 +19,39 @@ package org.springframework.integration.xml.splitter;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessagingException;
import org.springframework.integration.splitter.AbstractMessageSplitter;
import org.springframework.integration.util.Function;
import org.springframework.integration.util.FunctionIterator;
import org.springframework.integration.xml.DefaultXmlPayloadConverter;
import org.springframework.integration.xml.XmlPayloadConverter;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandlingException;
import org.springframework.messaging.converter.MessageConversionException;
import org.springframework.util.Assert;
import org.springframework.xml.namespace.SimpleNamespaceContext;
import org.springframework.xml.transform.StringResult;
import org.springframework.xml.xpath.XPathException;
import org.springframework.xml.xpath.XPathExpression;
import org.springframework.xml.xpath.XPathExpressionFactory;
@@ -49,20 +61,30 @@ import org.springframework.xml.xpath.XPathExpressionFactory;
* The return value will be either Strings or {@link Node}s depending on the
* received payload type. Additionally, node types will be converted to
* Documents if the 'createDocuments' property is set to <code>true</code>.
*
*
* @author Jonas Partner
* @author Mark Fisher
* @author Artem Bilan
*/
public class XPathMessageSplitter extends AbstractMessageSplitter {
private final TransformerFactory transformerFactory = TransformerFactory.newInstance();
private final Object documentBuilderFactoryMonitor = new Object();
private final XPathExpression xpathExpression;
private javax.xml.xpath.XPathExpression jaxpExpression;
private volatile boolean createDocuments;
private volatile DocumentBuilderFactory documentBuilderFactory;
private volatile XmlPayloadConverter xmlPayloadConverter = new DefaultXmlPayloadConverter();
private volatile Properties outputProperties;
private volatile boolean iterator = true;
public XPathMessageSplitter(String expression) {
this(expression, new HashMap<String, String>());
@@ -70,9 +92,22 @@ public class XPathMessageSplitter extends AbstractMessageSplitter {
public XPathMessageSplitter(String expression, Map<String, String> namespaces) {
this(XPathExpressionFactory.createXPathExpression(expression, namespaces));
XPath xpath = XPathFactory.newInstance().newXPath();
SimpleNamespaceContext namespaceContext = new SimpleNamespaceContext();
namespaceContext.setBindings(namespaces);
xpath.setNamespaceContext(namespaceContext);
try {
this.jaxpExpression = xpath.compile(expression);
}
catch (XPathExpressionException e) {
throw new org.springframework.xml.xpath.XPathParseException(
"Could not compile [" + expression + "] to a XPathExpression: " + e.getMessage(), e);
}
}
public XPathMessageSplitter(XPathExpression xpathExpression) {
Assert.notNull(xpathExpression, "'xpathExpression' must not be null.");
this.xpathExpression = xpathExpression;
this.documentBuilderFactory = DocumentBuilderFactory.newInstance();
this.documentBuilderFactory.setNamespaceAware(true);
@@ -97,6 +132,37 @@ public class XPathMessageSplitter extends AbstractMessageSplitter {
this.xmlPayloadConverter = xmlPayloadConverter;
}
/**
* The {@code iterator} mode: {@code true} (default) to return an {@link Iterator}
* for splitting {@code payload}, {@code false} to return a {@link List}.
* @param iterator {@code boolean} flag for iterator mode. Default to {@code true}.
* @since 4.2
*/
public void setIterator(boolean iterator) {
this.iterator = iterator;
}
/**
* A set of output properties that will be
* used to override any of the same properties in affect
* for the transformation.
* @param outputProperties the {@link Transformer} output properties.
* @see Transformer#setOutputProperties(Properties)
* @since 4.2
*/
public void setOutputProperties(Properties outputProperties) {
this.outputProperties = outputProperties;
}
@Override
protected void doInit() {
super.doInit();
if (this.iterator && this.jaxpExpression == null) {
logger.info("The 'iterator' option isn't available for an external XPathExpression. Will be ignored");
this.iterator = false;
}
}
@Override
protected Object splitMessage(Message<?> message) {
try {
@@ -113,51 +179,134 @@ public class XPathMessageSplitter extends AbstractMessageSplitter {
return result;
}
catch (ParserConfigurationException e) {
throw new MessagingException(message, "failed to create DocumentBuilder", e);
throw new MessageConversionException(message, "failed to create DocumentBuilder", e);
}
catch (Exception e) {
throw new MessagingException(message, "failed to split Message payload", e);
throw new MessageHandlingException(message, "failed to split Message payload", e);
}
}
@SuppressWarnings("unchecked")
private Object splitDocument(Document document) throws Exception {
List<Node> nodes = splitNode(document);
Transformer transformer = TransformerFactory.newInstance().newTransformer();
List<String> splitStrings = new ArrayList<String>(nodes.size());
for (Node nodeFromList : nodes) {
StringResult result = new StringResult();
transformer.transform(new DOMSource(nodeFromList), result);
splitStrings.add(result.toString());
Object nodes = splitNode(document);
final Transformer transformer;
synchronized (this.transformerFactory) {
transformer = this.transformerFactory.newTransformer();
}
if (this.outputProperties != null) {
transformer.setOutputProperties(this.outputProperties);
}
if (nodes instanceof List) {
List<Node> items = (List<Node>) nodes;
List<String> splitStrings = new ArrayList<String>(items.size());
for (Node nodeFromList : items) {
StringResult result = new StringResult();
transformer.transform(new DOMSource(nodeFromList), result);
splitStrings.add(result.toString());
}
return splitStrings;
}
else {
return new FunctionIterator<Node, String>((Iterator<Node>) nodes, new Function<Node, String>() {
@Override
public String apply(Node node) {
StringResult result = new StringResult();
try {
transformer.transform(new DOMSource(node), result);
}
catch (TransformerException e) {
throw new IllegalStateException("failed to create DocumentBuilder", e);
}
return result.toString();
}
});
}
return splitStrings;
}
private List<Node> splitNode(Node node) throws ParserConfigurationException {
List<Node> nodeList = this.xpathExpression.evaluateAsNodeList(node);
if (nodeList.size() == 0) {
throw new IllegalArgumentException("failed to split message with XPath expression: " + this.xpathExpression);
private Object splitNode(Node node) throws ParserConfigurationException {
if (this.iterator) {
try {
NodeList nodeList = (NodeList) this.jaxpExpression.evaluate(node, XPathConstants.NODESET);
return new NodeListIterator(nodeList);
}
catch (XPathExpressionException e) {
throw new XPathException("Could not evaluate XPath expression:" + e.getMessage(), e);
}
}
if (this.createDocuments) {
return convertNodesToDocuments(nodeList);
else {
List<Node> nodeList = this.xpathExpression.evaluateAsNodeList(node);
if (this.createDocuments) {
return convertNodesToDocuments(nodeList);
}
return nodeList;
}
return nodeList;
}
private List<Node> convertNodesToDocuments(List<Node> nodes) throws ParserConfigurationException {
DocumentBuilder documentBuilder = this.getNewDocumentBuilder();
DocumentBuilder documentBuilder = getNewDocumentBuilder();
List<Node> documents = new ArrayList<Node>(nodes.size());
for (Node node : nodes) {
Document document = documentBuilder.newDocument();
document.appendChild(document.importNode(node, true));
Document document = convertNodeToDocument(documentBuilder, node);
documents.add(document);
}
return documents;
}
private Document convertNodeToDocument(DocumentBuilder documentBuilder, Node node) {
Document document = documentBuilder.newDocument();
document.appendChild(document.importNode(node, true));
return document;
}
private DocumentBuilder getNewDocumentBuilder() throws ParserConfigurationException {
synchronized (this.documentBuilderFactory) {
synchronized (this.documentBuilderFactoryMonitor) {
return this.documentBuilderFactory.newDocumentBuilder();
}
}
private class NodeListIterator implements Iterator<Node> {
private final DocumentBuilder documentBuilder;
private final NodeList nodeList;
private int index;
public NodeListIterator(NodeList nodeList) throws ParserConfigurationException {
this.nodeList = nodeList;
if (XPathMessageSplitter.this.createDocuments) {
this.documentBuilder = getNewDocumentBuilder();
}
else {
this.documentBuilder = null;
}
}
@Override
public boolean hasNext() {
return index < nodeList.getLength();
}
@Override
public Node next() {
if (!hasNext())
return null;
Node node = nodeList.item(index++);
if (this.documentBuilder != null) {
node = convertNodeToDocument(this.documentBuilder, node);
}
return node;
}
@Override
public void remove() {
throw new UnsupportedOperationException("Operation not supported");
}
}
}

View File

@@ -749,6 +749,32 @@
<xsd:union memberTypes="xsd:boolean xsd:string" />
</xsd:simpleType>
</xsd:attribute>
<xsd:attribute name="output-properties">
<xsd:annotation>
<xsd:documentation>
A set of output properties that will be used to override any of the same properties
in effect for the transformation.
</xsd:documentation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="java.util.Properties"/>
</tool:annotation>
</xsd:appinfo>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="iterator" default="true">
<xsd:annotation>
<xsd:documentation>
The iterator mode: 'true' (default) to return an 'java.util.Iterator'
for splitting 'payload', 'false to return a 'java.util.List'.
Note: the 'list' contains transformed nodes whereas with the
'iterator' each node is transformed while iterating.
</xsd:documentation>
</xsd:annotation>
<xsd:simpleType>
<xsd:union memberTypes="xsd:boolean xsd:string" />
</xsd:simpleType>
</xsd:attribute>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
@@ -862,14 +888,14 @@
<xsd:attribute name="expression" type="xsd:string" use="optional"/>
<xsd:attribute name="value" type="xsd:string" use="optional"/>
</xsd:complexType>
<xsd:simpleType name="resultType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="DOMResult"/>
<xsd:enumeration value="StringResult"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="evaluationType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="BOOLEAN_RESULT"/>
@@ -879,12 +905,12 @@
<xsd:enumeration value="NODE_LIST_RESULT"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="schemaType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="xml-schema"/>
<xsd:enumeration value="relax-ng"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:schema>

View File

@@ -1,20 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/integration/xml"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:si="http://www.springframework.org/schema/integration"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:si="http://www.springframework.org/schema/integration"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:util="http://www.springframework.org/schema/util"
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
http://www.springframework.org/schema/integration/xml
http://www.springframework.org/schema/integration/xml/spring-integration-xml.xsd">
http://www.springframework.org/schema/integration/xml/spring-integration-xml.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">
<si:channel id="output">
<si:queue/>
</si:channel>
<xpath-splitter id="xpathSplitter" input-channel="input" apply-sequence="false" create-documents="true">
<util:properties id="outputProperties">
<beans:prop key="#{T (javax.xml.transform.OutputKeys).OMIT_XML_DECLARATION}">yes</beans:prop>
</util:properties>
<xpath-splitter id="xpathSplitter"
input-channel="input"
apply-sequence="false"
create-documents="true"
output-properties="outputProperties"
iterator="false">
<xpath-expression expression="/orders/order"/>
</xpath-splitter>

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014 the original author or authors.
* Copyright 2014-2015 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.
@@ -18,6 +18,8 @@ package org.springframework.integration.xml.config;
import static org.junit.Assert.*;
import java.util.Properties;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -39,10 +41,15 @@ public class XPathSplitterParserTests {
@Autowired @Qualifier("xpathSplitter.handler")
private MessageHandler xpathSplitter;
@Autowired @Qualifier("outputProperties")
private Properties outputProperties;
@Test
public void testXpathSplitterConfig() {
assertTrue(TestUtils.getPropertyValue(this.xpathSplitter, "createDocuments", Boolean.class));
assertFalse(TestUtils.getPropertyValue(this.xpathSplitter, "applySequence", Boolean.class));
assertFalse(TestUtils.getPropertyValue(this.xpathSplitter, "iterator", Boolean.class));
assertSame(this.outputProperties, TestUtils.getPropertyValue(this.xpathSplitter, "outputProperties"));
assertEquals("/orders/order",
TestUtils.getPropertyValue(this.xpathSplitter,
"xpathExpression.xpathExpression.xpath.m_patternString",

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2009 the original author or authors.
* Copyright 2002-2015 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.
@@ -27,19 +27,18 @@ import org.junit.Test;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessagingException;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.integration.handler.ReplyRequiredException;
import org.springframework.integration.xml.util.XmlTestUtil;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandlingException;
import org.springframework.messaging.support.GenericMessage;
/**
* @author Jonas Partner
*/
public class XPathMessageSplitterTests {
private String splittingXPath = "/orders/order";
private XPathMessageSplitter splitter;
private QueueChannel replyChannel = new QueueChannel();
@@ -47,8 +46,10 @@ public class XPathMessageSplitterTests {
@Before
public void setUp(){
String splittingXPath = "/orders/order";
splitter = new XPathMessageSplitter(splittingXPath);
splitter.setOutputChannel(replyChannel);
splitter.setRequiresReply(true);
}
@@ -64,7 +65,7 @@ public class XPathMessageSplitterTests {
}
}
@Test(expected = MessagingException.class)
@Test(expected = ReplyRequiredException.class)
public void splitDocumentThatDoesNotMatch() throws Exception {
Document doc = XmlTestUtil.getDocumentForString("<wrongDocument/>");
splitter.handleMessage(new GenericMessage<Document>(doc));
@@ -95,7 +96,7 @@ public class XPathMessageSplitterTests {
}
}
@Test(expected = MessagingException.class)
@Test(expected = MessageHandlingException.class)
public void invalidPayloadType() {
splitter.handleMessage(new GenericMessage<Integer>(123));
}