blob: 6983e5945de5aa6282ebaa75efa3bd3a9dd934a8 [file] [log] [blame]
/*
* Copyright (c) 2010-2020 BSI Business Systems Integration AG.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* BSI Business Systems Integration AG - initial API and implementation
*/
package org.eclipse.scout.sdk.core.util;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import javax.xml.XMLConstants;
import javax.xml.namespace.NamespaceContext;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.eclipse.scout.sdk.core.log.SdkLog;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
/**
* <h3>{@link Xml}</h3><br>
* Contains helper methods to deal with XML related topics.
*
* @since 6.1.0
*/
public final class Xml {
private Xml() {
}
/**
* Transforms the specified {@link Document} into a {@link String}.
*
* @param document
* The document to transform.
* @param format
* If the document should be formatted ({@code true}) or not ({@code false}).
* @return The specified {@link Document} as {@link StringBuilder} optionally formatted.
* @throws TransformerException
* if there is an error during transformation
*/
public static StringBuilder documentToString(Document document, boolean format) throws TransformerException {
try (StringWriter out = new StringWriter()) {
Transformer transformer = createTransformer(format);
transformer.transform(new DOMSource(document), new StreamResult(out));
return new StringBuilder(out.getBuffer());
}
catch (IOException e) {
throw new TransformerException(e);
}
}
/**
* Reads the content of the specified xml file into a XML {@link Document}.
*
* @param xmlFile
* A {@link Path} pointing to the xml file. Must not be {@code null}.
* @return A {@link Document} with the content of the xml file.
* @throws IOException
* if there is an exception reading the file into the document.
*/
public static Document get(Path xmlFile) throws IOException {
try (InputStream in = Files.newInputStream(Ensure.notNull(xmlFile), StandardOpenOption.READ)) {
return get(in);
}
}
/**
* Reads the content of the specified {@link URL} into a XML {@link Document}.
*
* @param url
* The {@link URL} from where the content should be read. Must not be {@code null}.
* @return A {@link Document} with the XML content of the url.
* @throws IOException
* if there is an exception reading the file into the document.
*/
public static Document get(URL url) throws IOException {
// use document builder to download the stream content because it correctly handles the xml encoding as specified in the xml declaration.
try (InputStream in = Ensure.notNull(url).openStream()) {
return get(in);
}
}
/**
* Converts the specified {@link String} into a XML {@link Document}.
*
* @param rawXmlAsString
* The {@link String} holding the XML content. Must not be blank (see {@link Strings#isBlank(CharSequence)}.
* @return A {@link Document} with the XML content of the specified {@link String}.
* @throws IOException
* if there is an exception converting the {@link String}.
*/
public static Document get(String rawXmlAsString) throws IOException {
try (Reader r = new StringReader(Ensure.notBlank(rawXmlAsString))) {
return createDocumentBuilder().parse(new InputSource(r));
}
catch (SAXException | ParserConfigurationException e) {
throw new IOException(e);
}
}
private static Document get(InputStream in) throws IOException {
try {
return createDocumentBuilder().parse(in);
}
catch (SAXException | ParserConfigurationException e) {
throw new IOException(e);
}
}
/**
* Gets the first child {@link Element} of the specified parent {@link Element} having the specified local tag name
* (ignoring namespaces).
*
* @param parent
* The parent {@link Element}
* @param tagName
* The local tag name (see {@link Node#getLocalName()}
* @return The first Element with this tag name or {@code null} if no such {@link Element} exists.
*/
public static Optional<Element> firstChildElement(Node parent, String tagName) {
if (parent == null) {
return Optional.empty();
}
if (tagName == null) {
return Optional.empty();
}
NodeList children = parent.getChildNodes();
for (int i = 0; i < children.getLength(); ++i) {
Node n = children.item(i);
String nodeName = n.getLocalName();
if (n.getNodeType() == Node.ELEMENT_NODE && Objects.equals(nodeName, tagName)) {
return Optional.of((Element) n);
}
}
return Optional.empty();
}
/**
* Evaluates the specified xPath string on the specified {@link Document}.
*
* @param xPath
* The xPath expression
* @param applyToDocument
* The {@link Document} to apply the xPath to.
* @return All {@link Element}s that match the specified xPath expression.
* @throws XPathExpressionException
* if there is an error during XPath evaluation
*/
public static List<Element> evaluateXPath(String xPath, Node applyToDocument) throws XPathExpressionException {
return evaluateXPath(xPath, applyToDocument, null);
}
/**
* Evaluates the specified xPath string on the specified {@link Document}.
*
* @param xPath
* The xPath expression
* @param applyToDocument
* The {@link Document} to apply the xPath to.
* @param prefix
* The single namespace prefix that was used in the specified xPath expression
* @param namespace
* The namespace the specified prefix maps to.
* @return All {@link Element}s that match the specified xPath expression.
* @throws XPathExpressionException
* if there is an error during XPath evaluation
*/
public static List<Element> evaluateXPath(String xPath, Node applyToDocument, String prefix, String namespace) throws XPathExpressionException {
return evaluateXPath(xPath, applyToDocument, singletonMap(prefix, namespace));
}
/**
* Evaluates the specified xPath string on the specified {@link Document}.
*
* @param xPath
* The xPath expression
* @param applyToDocument
* The {@link Document} to apply the xPath to.
* @param usedPrefixToNamespaceMap
* A {@link Map} defining all namespace prefixes used in the specified xPath and their corresponding
* namespace.
* @return All {@link Element}s that match the specified xPath expression.
* @throws XPathExpressionException
* if there is an error during XPath evaluation
*/
public static List<Element> evaluateXPath(String xPath, Node applyToDocument, Map<String, String> usedPrefixToNamespaceMap) throws XPathExpressionException {
if (applyToDocument == null || Strings.isBlank(xPath)) {
return emptyList();
}
XPathFactory xPathfactory = XPathFactory.newInstance();
XPath xpath = xPathfactory.newXPath();
xpath.setNamespaceContext(new NamespaceContext() {
@Override
public String getNamespaceURI(String prefix) {
if (usedPrefixToNamespaceMap != null) {
String ns = usedPrefixToNamespaceMap.get(prefix);
if (ns != null) {
return ns;
}
}
return applyToDocument.lookupNamespaceURI(prefix);
}
@Override
public Iterator<String> getPrefixes(String val) {
return singletonList(getPrefix(val)).iterator();
}
@Override
public String getPrefix(String uri) {
if (usedPrefixToNamespaceMap != null) {
for (Entry<String, String> entry : usedPrefixToNamespaceMap.entrySet()) {
if (entry.getValue().equals(uri)) {
return entry.getKey();
}
}
}
return applyToDocument.lookupPrefix(uri);
}
});
XPathExpression expr = xpath.compile(xPath);
NodeList result = (NodeList) expr.evaluate(applyToDocument, XPathConstants.NODESET);
int size = result.getLength();
if (size < 1) {
return emptyList();
}
List<Element> elements = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
Node n = result.item(i);
if (n.getNodeType() == Node.ELEMENT_NODE) {
elements.add((Element) n);
}
}
return elements;
}
/**
* @return A safe pre configured {@link DocumentBuilderFactory}. All external entities are disabled to prevent XXE.
*/
public static DocumentBuilderFactory createDocumentBuilderFactory() {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
Map<String, Boolean> features = new HashMap<>(5);
features.put("http://apache.org/xml/features/disallow-doctype-decl", Boolean.TRUE);
features.put("http://xml.org/sax/features/external-general-entities", Boolean.FALSE);
features.put("http://xml.org/sax/features/external-parameter-entities", Boolean.FALSE);
features.put("http://apache.org/xml/features/nonvalidating/load-external-dtd", Boolean.FALSE);
features.put(XMLConstants.FEATURE_SECURE_PROCESSING, Boolean.TRUE);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);
dbf.setNamespaceAware(true); // required!
try {
dbf.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
}
catch (IllegalArgumentException e) {
SdkLog.debug("Attribute '{}' is not supported in the current DocumentBuilderFactory: {}", XMLConstants.ACCESS_EXTERNAL_DTD, dbf.getClass().getName(), e);
}
try {
dbf.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
}
catch (IllegalArgumentException e) {
SdkLog.debug("Attribute '{}' is not supported in the current DocumentBuilderFactory: {}", XMLConstants.ACCESS_EXTERNAL_SCHEMA, dbf.getClass().getName(), e);
}
for (Entry<String, Boolean> a : features.entrySet()) {
String feature = a.getKey();
boolean enabled = a.getValue();
try {
dbf.setFeature(feature, enabled);
}
catch (ParserConfigurationException e) {
SdkLog.debug("Feature '{}' is not supported in the current XML parser. Skipping.", feature, e);
}
}
return dbf;
}
/**
* Creates a new {@link DocumentBuilder} to create a DOM of an XML file.<br>
* Use e.g. {@link DocumentBuilder#parse(InputStream)} to create a new {@link Document}.
*
* @return The created builder. All external entities are disabled to prevent XXE.
* @throws ParserConfigurationException
* if a {@link DocumentBuilder} cannot be created which satisfies the configuration requested.
*/
public static DocumentBuilder createDocumentBuilder() throws ParserConfigurationException {
return createDocumentBuilderFactory().newDocumentBuilder();
}
/**
* Creates a new {@link Transformer}.<br>
* Use {@link Transformer#transform(javax.xml.transform.Source, javax.xml.transform.Result)} to transform an XML
* document.
*
* @param format
* {@code true} to have the document formatted (indent) during transformation. <code>false</otherwise>.
* @return The created {@link Transformer}. All external entities are disabled to prevent XXE.
* @throws TransformerConfigurationException
* When it is not possible to create a Transformer instance.
*/
public static Transformer createTransformer(boolean format) throws TransformerConfigurationException {
TransformerFactory tf = TransformerFactory.newInstance();
int indent = 2;
try {
tf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
}
catch (TransformerConfigurationException e) {
SdkLog.debug("Feature '{}' is not supported in the current TransformerFactory: {}", XMLConstants.FEATURE_SECURE_PROCESSING, tf.getClass().getName(), e);
}
Map<String, Object> attribs = new HashMap<>(3);
attribs.put(XMLConstants.ACCESS_EXTERNAL_DTD, "");
attribs.put(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
if (format) {
attribs.put("indent-number", indent);
}
for (Entry<String, Object> a : attribs.entrySet()) {
try {
tf.setAttribute(a.getKey(), a.getValue());
}
catch (IllegalArgumentException e) {
SdkLog.debug("Attribute '{}' is not supported in the current TransformerFactory: {}", a.getKey(), tf.getClass().getName(), e);
}
}
Transformer transformer = tf.newTransformer();
Map<String, String> outputProps = new HashMap<>(4);
outputProps.put(OutputKeys.ENCODING, StandardCharsets.UTF_8.name());
outputProps.put(OutputKeys.METHOD, "xml");
if (format) {
outputProps.put(OutputKeys.INDENT, "yes");
outputProps.put("{http://xml.apache.org/xslt}indent-amount", Integer.toString(indent));
}
else {
outputProps.put(OutputKeys.INDENT, "no");
}
for (Entry<String, String> o : outputProps.entrySet()) {
try {
transformer.setOutputProperty(o.getKey(), o.getValue());
}
catch (IllegalArgumentException e) {
SdkLog.debug("Error applying output property '{}' on transformer of class '{}'.", o.getKey(), transformer.getClass().getName(), e);
}
}
return transformer;
}
}