/* * Copyright 2002-2016 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.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.integration.splitter.AbstractMessageSplitter; 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; /** * Message Splitter that uses an {@link XPathExpression} to split a * {@link Document}, {@link File} or {@link String} payload into a {@link NodeList}. * 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 * @author Gary Russell */ 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>()); } 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); } public void setCreateDocuments(boolean createDocuments) { this.createDocuments = createDocuments; } @Override public String getComponentType() { return "xml:xpath-splitter"; } public void setDocumentBuilder(DocumentBuilderFactory documentBuilderFactory) { Assert.notNull(documentBuilderFactory, "DocumentBuilderFactory must not be null"); this.documentBuilderFactory = documentBuilderFactory; } public void setXmlPayloadConverter(XmlPayloadConverter xmlPayloadConverter) { Assert.notNull(xmlPayloadConverter, "XmlPayloadConverter must not be null"); 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 { Object payload = message.getPayload(); Object result = null; if (payload instanceof Node) { result = splitNode((Node) payload); } else { Document document = this.xmlPayloadConverter.convertToDocument(payload); Assert.notNull(document, "unsupported payload type [" + payload.getClass().getName() + "]"); result = splitDocument(document); } return result; } catch (ParserConfigurationException e) { throw new MessageConversionException(message, "failed to create DocumentBuilder", e); } catch (Exception e) { throw new MessageHandlingException(message, "failed to split Message payload", e); } } @SuppressWarnings("unchecked") private Object splitDocument(Document document) throws Exception { 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<>((Iterator<Node>) nodes, 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(); }); } } 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); } } else { List<Node> nodeList = this.xpathExpression.evaluateAsNodeList(node); if (this.createDocuments) { return convertNodesToDocuments(nodeList); } return nodeList; } } private List<Node> convertNodesToDocuments(List<Node> nodes) throws ParserConfigurationException { DocumentBuilder documentBuilder = getNewDocumentBuilder(); List<Node> documents = new ArrayList<Node>(nodes.size()); for (Node node : nodes) { 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.documentBuilderFactoryMonitor) { return this.documentBuilderFactory.newDocumentBuilder(); } } private final class NodeListIterator implements Iterator<Node> { private final DocumentBuilder documentBuilder; private final NodeList nodeList; private int index; NodeListIterator(NodeList nodeList) throws ParserConfigurationException { this.nodeList = nodeList; if (XPathMessageSplitter.this.createDocuments) { this.documentBuilder = getNewDocumentBuilder(); } else { this.documentBuilder = null; } } @Override public boolean hasNext() { return this.index < this.nodeList.getLength(); } @Override public Node next() { if (!hasNext()) { return null; } Node node = this.nodeList.item(this.index++); if (this.documentBuilder != null) { node = convertNodeToDocument(this.documentBuilder, node); } return node; } @Override public void remove() { throw new UnsupportedOperationException("Operation not supported"); } } }