blob: 2699eebb350d455d0472d6e60ace53937f7e178b [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.apache.brooklyn.util.core.xstream;
import java.io.IOException;
import java.io.StringReader;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import com.sun.org.apache.xerces.internal.xni.parser.XMLErrorHandler;
import com.sun.org.apache.xml.internal.utils.DefaultErrorHandler;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.w3c.dom.Document;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import com.google.common.annotations.Beta;
import org.xml.sax.SAXParseException;
public class XmlUtil {
/**
* Thread-local storage for sharing the {@link DocumentBuilder}, to avoid repeated construction.
* See {@linkplain http://stackoverflow.com/questions/9828254/is-documentbuilderfactory-thread-safe-in-java-5}.
*/
private static class SharedDocumentBuilder {
private static ThreadLocal<DocumentBuilder> instance = new ThreadLocal<DocumentBuilder>();
/** xpath in particular prints to stderr and then throws or swallows; do the same, but without printing to stderr */
public static DocumentBuilder getSwallowingOrThrowingErrors(boolean throwIfWarning, boolean throwIfError, boolean throwIfFatal) {
ErrorHandler eh = new ErrorHandler() {
@Override
public void warning(SAXParseException exception) throws SAXException {
if (throwIfWarning) throw exception;
}
@Override
public void error(SAXParseException exception) throws SAXException {
if (throwIfError) throw exception;
}
@Override
public void fatalError(SAXParseException exception) throws SAXException {
if (throwIfFatal) throw exception;
}
};
return get(eh);
}
public static DocumentBuilder get() {
return get(null);
}
public static DocumentBuilder get(ErrorHandler errorHandler) {
DocumentBuilder result = instance.get();
if (result == null) {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
try {
result = factory.newDocumentBuilder();
} catch (ParserConfigurationException e) {
throw Exceptions.propagate(e);
}
if (errorHandler!=null) result.setErrorHandler(errorHandler);
instance.set(result);
} else {
result.reset();
}
return result;
}
}
public static String xpath(String xml, String xpath) {
return (String) xpath(xml, xpath, XPathConstants.STRING);
}
public static Object xpath(String xml, String xpath, QName returnType) {
return xpath(SharedDocumentBuilder.get(), xml, xpath, returnType);
}
public static Object xpath(DocumentBuilder builder, String xml, String xpath, QName returnType) {
try {
Document doc = builder.parse(new InputSource(new StringReader(xml)));
XPathFactory xPathfactory = XPathFactory.newInstance();
XPathExpression expr = xPathfactory.newXPath().compile(xpath);
return expr.evaluate(doc, returnType);
} catch (SAXException e) {
throw Exceptions.propagate(e);
} catch (IOException e) {
throw Exceptions.propagate(e);
} catch (XPathExpressionException e) {
throw Exceptions.propagate(e);
}
}
public static Object xpathHandlingIllegalChars(String xml, String xpath) {
return xpathHandlingIllegalChars(xml, xpath, XPathConstants.STRING);
}
/**
* Executes the given xpath on the given xml. If this fails becaues the xml is invalid
* (e.g. contains "&#x1b;"), then it will attempt to escape such illegal characters
* and try again. Note that the *escaped* values may be contained in the returned result!
* The escaping used is the prefix "BR_UNICODE_"; if that string is already in the xml,
* then it will replace that with "NOT_BR_UNICODE_".
*/
@Beta
public static Object xpathHandlingIllegalChars(String xml, String xpath, QName returnType) {
try {
return xpath(SharedDocumentBuilder.getSwallowingOrThrowingErrors(false, false, true),
xml, xpath, returnType);
} catch (Exception e) {
SAXException saxe = Exceptions.getFirstThrowableOfType(e, SAXException.class);
if (saxe != null && saxe.toString().contains("&#")) {
// Looks like illegal chars (e.g. xstream converts unicode char 27 to "&#x1b;",
// which is not valid in XML! Try again with an escaped xml.
Escaper escaper = new Escaper();
String xmlCleaned = escaper.escape(xml);
try {
Object result = xpath(xmlCleaned, xpath, returnType);
if (result instanceof String) {
return escaper.unescape((String)result);
} else {
return result;
}
} catch (Exception e2) {
Exceptions.propagateIfFatal(e2);
}
}
throw e;
}
}
/**
* Replaces things like "&#x1b;" with "BR_UNICODE_x1b". This is because xstream happily writes
* out such characters (which are not valid in xml), but xpath fails when parsing them.
*/
@Beta
protected static class Escaper {
public String escape(String string) {
String unicodeRegex = "&#([x0-9a-fA-f]{1,5});";
return string.replaceAll("BR_UNICODE_", "NOT_BR_UNICODE_")
.replaceAll(unicodeRegex, "BR_UNICODE_$1;");
}
public String unescape(String string) {
String unicodeRegex = "(?<!NOT_)BR_UNICODE_([x0-9a-fA-F]{1,5})";
return string
.replaceAll(unicodeRegex, "&#$1")
.replaceAll("NOT_BR_UNICODE_", "BR_UNICODE_");
}
}
}