/*
* Copyright (c) MuleSoft, Inc. All rights reserved. http://www.mulesoft.com
* The software in this package is published under the terms of the CPAL v1.0
* license, a copy of which has been included with this distribution in the
* LICENSE.txt file.
*/
package org.mule.runtime.config.spring;
import static org.mule.runtime.config.spring.parsers.XmlMetadataAnnotations.METADATA_ANNOTATIONS_KEY;
import org.mule.runtime.config.spring.parsers.DefaultXmlMetadataAnnotations;
import org.mule.runtime.config.spring.parsers.XmlMetadataAnnotations;
import org.mule.runtime.core.util.SystemUtils;
import org.mule.runtime.core.util.xmlsecurity.XMLSecureFactories;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.LinkedHashMap;
import java.util.Stack;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.xml.DefaultDocumentLoader;
import org.springframework.beans.factory.xml.DocumentLoader;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.UserDataHandler;
import org.xml.sax.Attributes;
import org.xml.sax.EntityResolver;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
/**
* Alternative to Spring's default document loader that uses <b>SAX</b> to add metadata to the <b>DOM</b> elements that are the
* result of the default parser.
*
* @since 3.8.0
*/
final public class MuleDocumentLoader implements DocumentLoader {
private static final String DEFER_NODE_EXPANSION_FEATURE_KEY = "http://apache.org/xml/features/dom/defer-node-expansion";
private static final UserDataHandler COPY_METADATA_ANNOTATIONS_DATA_HANDLER = new UserDataHandler() {
@Override
public void handle(short operation, String key, Object data, Node src, Node dst) {
if (operation == NODE_IMPORTED || operation == NODE_CLONED) {
dst.setUserData(METADATA_ANNOTATIONS_KEY, src.getUserData(METADATA_ANNOTATIONS_KEY), this);
}
}
};
private final DocumentLoader defaultLoader = new DefaultDocumentLoader();
private final XmlMetadataAnnotationsFactory metadataFactory;
public MuleDocumentLoader(XmlMetadataAnnotationsFactory metadataFactory) {
this.metadataFactory = metadataFactory;
}
public MuleDocumentLoader() {
this.metadataFactory = new DefaultXmlMetadataFactory();
}
/**
* Load the {@link Document} at the supplied {@link InputSource} using the standard JAXP-configured XML parser.
*/
@Override
public Document loadDocument(InputSource inputSource, EntityResolver entityResolver, ErrorHandler errorHandler,
int validationMode, boolean namespaceAware)
throws Exception {
ByteArrayOutputStream output = new ByteArrayOutputStream();
try (InputStream inputStream = inputSource.getByteStream()) {
IOUtils.copy(inputStream, output);
}
InputSource defaultInputSource = new InputSource(new ByteArrayInputStream(output.toByteArray()));
InputSource enrichInputSource = new InputSource(new ByteArrayInputStream(output.toByteArray()));
Document doc = defaultLoader.loadDocument(defaultInputSource, entityResolver, errorHandler, validationMode, namespaceAware);
createSaxAnnotator(doc).parse(enrichInputSource);
return doc;
}
protected XMLReader createSaxAnnotator(Document doc) throws ParserConfigurationException, SAXException {
SAXParserFactory saxParserFactory = XMLSecureFactories.createDefault().getSAXParserFactory();
SAXParser saxParser = saxParserFactory.newSAXParser();
XMLReader documentReader = saxParser.getXMLReader();
documentReader.setContentHandler(new XmlMetadataAnnotator(doc, metadataFactory));
return documentReader;
}
private final class DefaultXmlMetadataFactory implements XmlMetadataAnnotationsFactory {
@Override
public XmlMetadataAnnotations create(Locator locator) {
DefaultXmlMetadataAnnotations annotations = new DefaultXmlMetadataAnnotations();
annotations.setLineNumber(locator.getLineNumber());
return annotations;
}
}
/**
* SAX filter that builds the metadata that will annotate the built nodes.
*/
public final static class XmlMetadataAnnotator extends DefaultHandler {
private Locator locator;
private DomWalkerElement walker;
private XmlMetadataAnnotationsFactory metadataFactory;
private Stack<XmlMetadataAnnotations> annotationsStack = new Stack<>();
private XmlMetadataAnnotator(Document doc, XmlMetadataAnnotationsFactory metadataFactory) {
this.walker = new DomWalkerElement(doc.getDocumentElement());
this.metadataFactory = metadataFactory;
}
@Override
public void setDocumentLocator(Locator locator) {
super.setDocumentLocator(locator);
this.locator = locator;
}
@Override
public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
walker = walker.walkIn();
XmlMetadataAnnotations metadataBuilder = metadataFactory.create(locator);
LinkedHashMap<String, String> attsMap = new LinkedHashMap<>();
for (int i = 0; i < atts.getLength(); ++i) {
attsMap.put(atts.getQName(i), atts.getValue(i));
}
metadataBuilder.appendElementStart(qName, attsMap);
annotationsStack.push(metadataBuilder);
}
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
annotationsStack.peek().appendElementBody(new String(ch, start, length).trim());
}
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
XmlMetadataAnnotations metadataAnnotations = annotationsStack.pop();
metadataAnnotations.appendElementEnd(qName);
if (!annotationsStack.isEmpty()) {
annotationsStack.peek()
.appendElementBody(SystemUtils.LINE_SEPARATOR + metadataAnnotations.getElementString() + SystemUtils.LINE_SEPARATOR);
}
walker.getParentNode().setUserData(METADATA_ANNOTATIONS_KEY, metadataAnnotations, COPY_METADATA_ANNOTATIONS_DATA_HANDLER);
walker = walker.walkOut();
}
}
/**
* Allows for sequential navigation of a DOM tree.
*/
private final static class DomWalkerElement {
private final DomWalkerElement parent;
private final Node node;
private int childIndex = 0;
public DomWalkerElement(Node node) {
this.parent = null;
this.node = node;
}
private DomWalkerElement(DomWalkerElement parent, Node node) {
this.parent = parent;
this.node = node;
}
public DomWalkerElement walkIn() {
Node nextChild = node.getChildNodes().item(childIndex++);
while (nextChild != null && nextChild.getNodeType() != Node.ELEMENT_NODE) {
nextChild = node.getChildNodes().item(childIndex++);
}
return new DomWalkerElement(this, nextChild);
}
public DomWalkerElement walkOut() {
Node nextSibling = parent.node.getNextSibling();
while (nextSibling != null && nextSibling.getNodeType() != Node.ELEMENT_NODE) {
nextSibling = nextSibling.getNextSibling();
}
return new DomWalkerElement(parent.parent, nextSibling);
}
public Node getParentNode() {
return parent.node;
}
}
}