/* * Copyright (C) 2006-2016 DLR, Germany * * All rights reserved * * http://www.rcenvironment.de/ */ package de.rcenvironment.core.utils.common.xml.impl; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.StringReader; import java.io.StringWriter; import java.util.LinkedList; import java.util.List; import javax.xml.transform.Source; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMResult; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; import org.apache.commons.io.IOUtils; import org.apache.commons.io.LineIterator; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import de.rcenvironment.core.utils.common.xml.EMappingMode; import de.rcenvironment.core.utils.common.xml.XMLException; import de.rcenvironment.core.utils.common.xml.XMLMapperConstants; import de.rcenvironment.core.utils.common.xml.XMLMappingInformation; import de.rcenvironment.core.utils.common.xml.XMLNamespaceContext; import de.rcenvironment.core.utils.common.xml.XSLTErrorHandler; import de.rcenvironment.core.utils.common.xml.api.XMLMapperService; import de.rcenvironment.core.utils.common.xml.api.XMLSupportService; import de.rcenvironment.toolkit.utils.text.TextLinesReceiver; import net.sf.saxon.serialize.MessageEmitter; /** * Default implementation of the XML Mapper. * * @author Brigitte Boden * @author Markus Litz, Markus Kunde, Arne Bachmann (some code adapted from old class XMLHelper) */ public class XMLMapperServiceImpl implements XMLMapperService { /** * XPath delimiter string (slash instead of backslash). */ public static final String XPATH_DELIMITER = "/"; /** * TransformerFactory for XSL-Transformation. */ private TransformerFactory tFactory = null; private XMLSupportService xmlSupport; private Log log = LogFactory.getLog(getClass()); public XMLMapperServiceImpl() { tFactory = TransformerFactory.newInstance("net.sf.saxon.TransformerFactoryImpl", null); tFactory.setErrorListener(new XSLTErrorHandler()); } /** * OSGI binding method. * * @param service The service. */ public void bindXMLSupportService(XMLSupportService service) { xmlSupport = service; } @Override public void transformXMLFileWithXSLT(File sourceFile, File resultFile, File xsltFile, TextLinesReceiver logReceiver) throws XMLException { Transformer transformer = null; try (FileOutputStream resultFileOutputStream = new FileOutputStream(resultFile)) { transformer = tFactory.newTransformer(new StreamSource(xsltFile)); transformer.setErrorListener(new XSLTErrorHandler()); MessageEmitter em = new MessageEmitter(); // PipedReader reader = new PipedReader(); StringWriter writer = new StringWriter(); em.setWriter(writer); ((net.sf.saxon.Controller) transformer).setMessageEmitter(em); synchronized (XMLMapperConstants.GLOBAL_MAPPING_LOCK) { StreamSource streamSource = new StreamSource(sourceFile); transformer.transform(streamSource, new StreamResult(resultFileOutputStream)); em.close(); writer.close(); } resultFileOutputStream.close(); StringReader reader = new StringReader(writer.toString()); LineIterator it = IOUtils.lineIterator(reader); while (it.hasNext()) { String line = it.nextLine(); if (logReceiver != null) { logReceiver.addLine("XSL:Message:" + line); } log.info("XSL:Message:" + line); } reader.close(); } catch (TransformerException | IOException e) { throw new XMLException(e); } } /** * Does the mapping between the elements of a source document and a target document. (adapted from old XMLMapper) Set to protected to * reduce usage of Document instances in components to enable global locking. * * @param sourceDoc The source document whose elements should be mapped. * @param targetDoc The target document. * @param mappingsDoc A document with a list of mapping rules. * @throws XPathExpressionException Thrown if XPath could not be evaluated. * @throws XMLException Mapping error. * */ protected void transformXMLFileWithXMLMappingInformation(Document sourceDoc, Document targetDoc, Document mappingsDoc) throws XPathExpressionException, XMLException { List<XMLMappingInformation> mappings = readXMLMapping(mappingsDoc); // XPath object for querying the source document final XPath xpath = XPathFactory.newInstance().newXPath(); xpath.setNamespaceContext(new XMLNamespaceContext(sourceDoc)); // Loop over all mapping rules for (XMLMappingInformation mapInfo : mappings) { final EMappingMode mappingMode = mapInfo.getMode(); final String sourceXPath = mapInfo.getSourceXPath(); final String targetXPath = mapInfo.getTargetXPath(); if (mappingMode == EMappingMode.DeleteOnly) { xmlSupport.deleteElement(targetDoc, targetXPath); continue; } final NodeList sourceNodes = (NodeList) xpath.evaluate(sourceXPath, sourceDoc, XPathConstants.NODESET); if (sourceNodes.getLength() == 0) { throw new XMLException("No source elements found for map:source='" + sourceXPath); } switch (mappingMode) { case Append: final Document mappingDoc = createXPathMappings(sourceNodes.item(0), mapInfo); transformXMLFileWithXMLMappingInformation(sourceDoc, targetDoc, mappingDoc); continue; case Delete: xmlSupport.deleteElement(targetDoc, targetXPath); break; default: throw new XMLException("Unknown mapping mode: '" + mappingMode.toString() + "'"); } // Get target parent node. If target parent node doesn't exist then create it final String[] targetPath = targetXPath.split(XPATH_DELIMITER); final StringBuilder tmpPath = new StringBuilder(); for (int i = 0; i < targetPath.length - 1; i++) { if (targetPath[i].length() > 0) { tmpPath.append(XPATH_DELIMITER).append(targetPath[i]); } } final String targetParentPath = tmpPath.toString(); String targetNodeName; Node targetParentNode; if (targetParentPath.length() == 0) { // The target node is the document root node targetParentNode = xmlSupport.createElementTree(targetDoc, targetXPath); targetNodeName = ""; } else { // The target node is a child node targetParentNode = xmlSupport.createElementTree(targetDoc, targetParentPath); targetNodeName = targetPath[targetPath.length - 1]; } // Loop over all source nodes and import them into the target doc for (int sourceIndex = 0; sourceIndex < sourceNodes.getLength(); sourceIndex++) { final Element sourceElement = (Element) sourceNodes.item(sourceIndex); final Element importElement = (Element) targetDoc.importNode(sourceElement, /* deep */true); Node targetElement; if (targetNodeName.length() == 0) { targetElement = targetParentNode; } else { targetElement = xmlSupport.createElement(targetDoc, targetNodeName); targetParentNode.appendChild(targetElement); } // Copy the attributes of the source element to the target element final NamedNodeMap attrs = importElement.getAttributes(); for (int i = 0; i < attrs.getLength(); i++) { final Attr importAttr = (Attr) targetDoc.importNode(attrs.item(i), true); targetElement.getAttributes().setNamedItem(importAttr); } // Move all the children while (importElement.hasChildNodes()) { targetElement.appendChild(importElement.getFirstChild()); } } } } /** * Does the mapping between the elements of a source document and a target document. * * @param sourceFile The name of the source document whose elements should be mapped. * @param targetFile The name of the target document. * @param mappingsFile A document with a list of mapping rules. * @throws XPathExpressionException Thrown if XPath could not be evaluated. * @throws XMLException Mapping error. * */ @Override public void transformXMLFileWithXMLMappingInformation(File sourceFile, File targetFile, File mappingsFile) throws XPathExpressionException, XMLException { Document mappingsDoc = xmlSupport.readXMLFromFile(mappingsFile); transformXMLFileWithXMLMappingInformation(sourceFile, targetFile, mappingsDoc); } @Override public void transformXMLFileWithXMLMappingInformation(File sourceFile, File targetFile, Document mappingsDoc) throws XPathExpressionException, XMLException { synchronized (XMLMapperConstants.GLOBAL_MAPPING_LOCK) { Document sourceDoc = xmlSupport.readXMLFromFile(sourceFile); Document targetDoc; if (targetFile.exists()) { targetDoc = xmlSupport.readXMLFromFile(targetFile); } else { targetDoc = xmlSupport.createDocument(); } transformXMLFileWithXMLMappingInformation(sourceDoc, targetDoc, mappingsDoc); xmlSupport.writeXMLtoFile(targetDoc, targetFile); } } /** * Reads the mapping information from a mapping file and builds a list of mapping rules. Visibility is protected instead of private to * make it testable. * * @param mappingDoc The mapping file as DOM document. * @return Returns a list of XMLMappingInformation objects, which contain the mapping rules. * @throws XMLException Mapping error. * */ protected List<XMLMappingInformation> readXMLMapping(Document mappingsDoc) throws XMLException { final List<XMLMappingInformation> mappings = new LinkedList<XMLMappingInformation>(); try { final XPath xpath = XPathFactory.newInstance().newXPath(); xpath.setNamespaceContext(new XMLNamespaceContext(mappingsDoc)); final NodeList nodeList = (NodeList) xpath.evaluate("/map:mappings/map:mapping", mappingsDoc, XPathConstants.NODESET); for (int i = 0; i < nodeList.getLength(); i++) { final XMLMappingInformation mapInfo = new XMLMappingInformation(); final Element current = (Element) nodeList.item(i); // Read in the attributes of the current map:mapping element final NamedNodeMap attrs = current.getAttributes(); for (int j = 0; j < attrs.getLength(); j++) { final Attr mapAttr = (Attr) attrs.item(j); if (mapAttr.getName().equals("mode")) { if (mapAttr.getValue().equals("delete")) { mapInfo.setMode(EMappingMode.Delete); } else if (mapAttr.getValue().equals("delete-only")) { mapInfo.setMode(EMappingMode.DeleteOnly); } else if (mapAttr.getValue().equals("append")) { mapInfo.setMode(EMappingMode.Append); } else { throw new XMLException("Unknown mapping mode: '" + mapAttr.getValue() + "'"); } } } // <source> element final Node source = (Node) xpath.evaluate("map:source", current, XPathConstants.NODE); if (source != null) { mapInfo.setSourceXPath(source.getTextContent().trim()); if (mapInfo.getSourceXPath().length() == 0) { throw new XMLException("Empty <map:source> element found in mapping file"); } } else if (mapInfo.getMode() != EMappingMode.DeleteOnly) { throw new XMLException("No <map:source> element found in mapping file"); } // <target> element final Node target = (Node) xpath.evaluate("map:target", current, XPathConstants.NODE); if (target != null) { mapInfo.setTargetXPath(target.getTextContent().trim()); if (mapInfo.getTargetXPath().length() == 0) { throw new XMLException("Empty <map:target> element found in mapping file"); } } else { throw new XMLException("No <map:target> element found in mapping file"); } mappings.add(mapInfo); } } catch (final XPathExpressionException e) { throw new XMLException( "XML mapping error. No mapping nodes (/map:mappings/map:mapping) found in the mapping file." + " Please ensure that your mapping file contains the corresponding nodes and uses the corresponding namespace" + " (xmlns:map=\"http://www.rcenvironment.de/2015/mapping\")", e); } return mappings; } /** * Creates a mapping document for the 'append' mapping mode. This mapping document contains mapping rules for every leaf element of a * given source node. (adapted from old XML mapper) * * @param sourceNode The source element for whose leafs the mapping rules must be created. * @param mapInfo Current mapping information with source and target XPaths * @return Returns a new mapping document with mapping rules for all leafs of the current element. */ private Document createXPathMappings(final Node sourceNode, final XMLMappingInformation mapInfo) throws XMLException { try { final String sourceString = xmlSupport.writeXMLToString((Element) sourceNode); final Source source = new StreamSource(new StringReader(sourceString)); final Document mappingDoc = xmlSupport.createDocument(); final DOMResult result = new DOMResult(mappingDoc); // Create new transformer with parameters. StreamSource inStreamXPath = new StreamSource(getClass().getClassLoader().getResourceAsStream("CreateXPaths.xslt")); final Transformer transformer = tFactory.newTransformer(inStreamXPath); transformer.setErrorListener(new XSLTErrorHandler()); transformer.setParameter("sourceXPath", mapInfo.getSourceXPath()); transformer.setParameter("targetXPath", mapInfo.getTargetXPath()); transformer.transform(source, result); return mappingDoc; } catch (TransformerException e) { throw new XMLException("Failed to create XPath mapping", e); } } }