blob: ab96e71eea1d18bfa8091c22080e67bdfbb064dd [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.netbeans.nbbuild;
import java.io.ByteArrayInputStream;
import java.io.CharConversionException;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentType;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import org.xml.sax.EntityResolver;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
/**
* Utility class collecting library methods related to XML processing.
* @author Petr Kuzel, Jesse Glick
*/
public final class XMLUtil extends Object {
private XMLUtil() {}
@SuppressWarnings("unchecked")
private static final ThreadLocal<DocumentBuilder>[] builderTL = (ThreadLocal<DocumentBuilder>[]) new ThreadLocal<?>[4];
static {
for (int i = 0; i < 4; i++) {
builderTL[i] = new ThreadLocal<>();
}
}
/**
* @see #rethrowHandler
* @see #nullResolver
*/
public static Document parse (
InputSource input,
boolean validate,
boolean namespaceAware,
ErrorHandler errorHandler,
EntityResolver entityResolver
) throws IOException, SAXException {
int index = (validate ? 0 : 1) + (namespaceAware ? 0 : 2);
DocumentBuilder builder = builderTL[index].get();
if (builder == null) {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setValidating(validate);
factory.setNamespaceAware(namespaceAware);
try {
builder = factory.newDocumentBuilder();
} catch (ParserConfigurationException ex) {
throw new SAXException(ex);
}
builderTL[index].set(builder);
}
if (errorHandler != null) {
builder.setErrorHandler(errorHandler);
}
if (entityResolver != null) {
builder.setEntityResolver(entityResolver);
}
try {
return builder.parse(input);
} finally {
builder.setErrorHandler(null);
builder.setEntityResolver(null);
}
}
public static ErrorHandler rethrowHandler() {
return new ErrorHandler() {
public void warning(SAXParseException exception) throws SAXException {throw exception;}
public void error(SAXParseException exception) throws SAXException {throw exception;}
public void fatalError(SAXParseException exception) throws SAXException {throw exception;}
};
}
public static EntityResolver nullResolver() {
return new EntityResolver() {
@Override public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
return new InputSource(new ByteArrayInputStream(new byte[0]));
}
};
}
public static Document createDocument(String rootQName) throws DOMException {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
try {
return factory.newDocumentBuilder().getDOMImplementation().createDocument(null, rootQName, null);
} catch (ParserConfigurationException ex) {
throw (DOMException)new DOMException(DOMException.NOT_SUPPORTED_ERR, "Cannot create parser").initCause(ex); // NOI18N
}
}
// Cf. org.openide.xml.XMLUtil.
private static final String IDENTITY_XSLT_WITH_INDENT =
"<xsl:stylesheet version='1.0' " + // NOI18N
"xmlns:xsl='http://www.w3.org/1999/XSL/Transform' " + // NOI18N
"xmlns:xalan='http://xml.apache.org/xslt' " + // NOI18N
"exclude-result-prefixes='xalan'>" + // NOI18N
"<xsl:output method='xml' indent='yes' xalan:indent-amount='4'/>" + // NOI18N
"<xsl:template match='@*|node()'>" + // NOI18N
"<xsl:copy>" + // NOI18N
"<xsl:apply-templates select='@*|node()'/>" + // NOI18N
"</xsl:copy>" + // NOI18N
"</xsl:template>" + // NOI18N
"</xsl:stylesheet>"; // NOI18N
public static void write(Document doc, OutputStream out) throws IOException {
// XXX note that this may fail to write out namespaces correctly if the document
// is created with namespaces and no explicit prefixes; however no code in
// this package is likely to be doing so
try {
Transformer t = TransformerFactory.newInstance().newTransformer(
new StreamSource(new StringReader(IDENTITY_XSLT_WITH_INDENT)));
DocumentType dt = doc.getDoctype();
if (dt != null) {
String pub = dt.getPublicId();
if (pub != null) {
t.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, pub);
}
t.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, dt.getSystemId());
}
t.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); // NOI18N
Source source = new DOMSource(doc);
Result result = new StreamResult(out);
t.transform(source, result);
} catch (Exception | TransformerFactoryConfigurationError e) {
throw new IOException(e);
}
}
public static void write(Element el, OutputStream out) throws IOException {
try {
Transformer t = TransformerFactory.newInstance().newTransformer(
new StreamSource(new StringReader(IDENTITY_XSLT_WITH_INDENT)));
t.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); // NOI18N
t.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
Source source = new DOMSource(el);
Result result = new StreamResult(out);
t.transform(source, result);
} catch (Exception | TransformerFactoryConfigurationError e) {
throw new IOException(e);
}
}
/**
* Search for an XML element in the direct children of a parent.
* DOM provides a similar method but it does a recursive search
* which we do not want. It also gives a node list and we want
* only one result.
* @param parent a parent element
* @param name the intended local name
* @param namespace the intended namespace (or null)
* @return the one child element with that name, or null if none or more than one
*/
public static Element findElement(Element parent, String name, String namespace) {
Element result = null;
NodeList l = parent.getChildNodes();
for (int i = 0; i < l.getLength(); i++) {
if (l.item(i).getNodeType() == Node.ELEMENT_NODE) {
Element el = (Element)l.item(i);
if ((namespace == null && name.equals(el.getTagName())) ||
(namespace != null && name.equals(el.getLocalName()) &&
namespace.equals(el.getNamespaceURI()))) {
if (result == null) {
result = el;
} else {
return null;
}
}
}
}
return result;
}
/**
* Extract nested text from an element.
* Currently does not handle coalescing text nodes, CDATA sections, etc.
* @param parent a parent element
* @return the nested text, or null if none was found
*/
static String findText(Element parent) {
NodeList l = parent.getChildNodes();
for (int i = 0; i < l.getLength(); i++) {
if (l.item(i).getNodeType() == Node.TEXT_NODE) {
Text text = (Text)l.item(i);
return text.getNodeValue();
}
}
return null;
}
/**
* Find all direct child elements of an element.
* More useful than {@link Element#getElementsByTagNameNS} because it does
* not recurse into recursive child elements.
* Children which are all-whitespace text nodes or comments are ignored; others cause
* an exception to be thrown.
* @param parent a parent element in a DOM tree
* @return a list of direct child elements (may be empty)
* @throws IllegalArgumentException if there are non-element children besides whitespace
*/
static List<Element> findSubElements(Element parent) throws IllegalArgumentException {
NodeList l = parent.getChildNodes();
List<Element> elements = new ArrayList<>(l.getLength());
for (int i = 0; i < l.getLength(); i++) {
Node n = l.item(i);
if (n.getNodeType() == Node.ELEMENT_NODE) {
elements.add((Element)n);
} else if (n.getNodeType() == Node.TEXT_NODE) {
String text = ((Text)n).getNodeValue();
if (text.trim().length() > 0) {
throw new IllegalArgumentException("non-ws text encountered in " + parent + ": " + text); // NOI18N
}
} else if (n.getNodeType() == Node.COMMENT_NODE) {
// OK, ignore
} else {
throw new IllegalArgumentException("unexpected non-element child of " + parent + ": " + n); // NOI18N
}
}
return elements;
}
static String toElementContent(String val) throws CharConversionException {
if (val == null) {
throw new CharConversionException("null"); // NOI18N
}
if (checkContentCharacters(val)) {
return val;
}
StringBuilder buf = new StringBuilder();
for (int i = 0; i < val.length(); i++) {
char ch = val.charAt(i);
if ('<' == ch) {
buf.append("&lt;");
continue;
} else if ('&' == ch) {
buf.append("&amp;");
continue;
} else if (('>' == ch) && (i > 1) && (val.charAt(i - 2) == ']') && (val.charAt(i - 1) == ']')) {
buf.append("&gt;");
continue;
}
buf.append(ch);
}
return buf.toString();
}
private static boolean checkContentCharacters(String chars)
throws CharConversionException {
boolean escape = false;
for (int i = 0; i < chars.length(); i++) {
char ch = chars.charAt(i);
if (((int) ch) <= 93) { // we are UNICODE ']'
switch (ch) {
case 0x9:
case 0xA:
case 0xD:
continue;
case '>': // only ]]> is dangerous
if (escape) {
continue;
}
escape = (i > 0) && (chars.charAt(i - 1) == ']');
continue;
case '<':
case '&':
escape = true;
continue;
default:
if (((int) ch) < 0x20) {
throw new CharConversionException("Invalid XML character &#" + ((int) ch) + ";.");
}
}
}
}
return escape == false;
}
}