ch.inftec.ju.util.xml.XmlUtils Maven / Gradle / Ivy
package ch.inftec.ju.util.xml;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.net.URL;
import java.util.GregorianCalendar;
import java.util.TimeZone;
import javax.xml.XMLConstants;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.datatype.DatatypeConfigurationException;
import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.XMLGregorianCalendar;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import org.xml.sax.InputSource;
import org.xml.sax.SAXParseException;
import ch.inftec.ju.util.AssertUtil;
import ch.inftec.ju.util.IOUtil;
import ch.inftec.ju.util.JuException;
import ch.inftec.ju.util.JuRuntimeException;
import ch.inftec.ju.util.collection.Cache;
import ch.inftec.ju.util.collection.Caches;
import ch.inftec.ju.util.function.Function;
/**
* Utility class containing XML related helper methods.
* @author tgdmemae
*
*/
public class XmlUtils {
/**
* Needed to create XMLGregorianCalendar instances
*/
private static DatatypeFactory df = null;
/**
* Gets an instance of a DatatypeFactory. This is needed to create XMLGregorianCalendar instances.
* @return DatatypeFactory instance
*/
private static synchronized DatatypeFactory getDatetypeFactory() {
if (df == null) {
try {
df = DatatypeFactory.newInstance();
} catch (DatatypeConfigurationException dce) {
throw new IllegalStateException(
"Exception while obtaining DatatypeFactory instance", dce);
}
}
return df;
}
/**
* Don't instantiate.
*/
private XmlUtils() {
throw new AssertionError("use only statically");
}
/**
* Loads and parses an XML into a DOM structure. This method doesn't perform schema
* validation.
* @param xmlUrl URL to the XML
* @return Document instance
* @throws JuException If the XML cannot be loaded
*/
public static Document loadXml(URL xmlUrl) throws JuException {
return XmlUtils.loadXml(xmlUrl, null);
}
/**
* Loads the specified XML resource into a DOM structure and wraps it with an XPathGetter.
* @param xmlUrl URL to the XML
* @return XPathGetter instance on the document
* @throws JuException If the XML cannot be loaded
*/
public static XPathGetter loadXmlAsXPathGetter(URL xmlUrl) throws JuException {
return new XPathGetter(XmlUtils.loadXml(xmlUrl));
}
/**
* Loads and parses an XML into a DOM structure.
* @param xmlUrl URL to the XML
* @param schemaUrl URL to an optional XML validation schema. If null, no validation is done.
* @return Document instance
* @throws JuException If the XML cannot be loaded
*/
public static Document loadXml(URL xmlUrl, URL schemaUrl) throws JuException {
if (xmlUrl == null) {
throw new NullPointerException("xmlUrl must not be null");
}
InputStream xmlStream = null;
try {
xmlStream = new BufferedInputStream(xmlUrl.openStream());
return XmlUtils.loadXml(new InputSource(xmlStream), XmlUtils.loadSchema(schemaUrl), false);
} catch (Exception ex) {
throw new JuException(String.format("Couldn't load XML from URL: %s (Schema URL: %s)",
xmlUrl, schemaUrl), ex);
} finally {
IOUtil.close(xmlStream);
}
}
/**
* Loads and parses an XML from an InputStream into a DOM structure.
*
* The input stream will be wrapped in a BufferedInputStream and closed at the end.
* @param inputStream InputStream of the XML
* @param schema Optional Schema. If null, no validation is performed
* @return Document instance
* @throws JuException If the XML cannot be loaded or fails validation
*/
public static Document loadXml(InputStream inputStream, Schema schema) throws JuException {
InputStream xmlStream = null;
try {
xmlStream = new BufferedInputStream(inputStream);
return XmlUtils.loadXml(new InputSource(xmlStream), schema, false);
} catch (Exception ex) {
throw new JuException("Couldn't load XML from InputStream", ex);
} finally {
IOUtil.close(xmlStream);
}
}
/**
* Loads and parses an XML into a DOM structure.
*
* @param xmlSource
* InputStream of the XML
* @param schema
* Optional Schema. If null, no validation is performed
* @return Document instance
* @throws JuRuntimeException
* If the XML cannot be loaded or fails validation
*/
public static Document loadXml(InputSource xmlSource, Schema schema, boolean nameSpaceAware) {
try {
DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
docBuilderFactory.setNamespaceAware(nameSpaceAware);
// There's a bug in JDK >= 6, ignoring whitespace is not working
// docBuilderFactory.setIgnoringElementContentWhitespace(true);
DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
// Parse XML
Document doc = docBuilder.parse(xmlSource);
// Remove whitespace manually
XmlUtils.removeWhitespaceNodes(doc.getDocumentElement());
// Validate if we have a Schema
if (schema != null) {
schema.newValidator().validate(new DOMSource(doc));
}
return doc;
} catch (Exception ex) {
throw new JuRuntimeException("Couldn't load XML", ex);
}
}
/**
* Removes all Whitespace Noted from the specified element.
* @param e Element
*/
private static void removeWhitespaceNodes(Element e) {
NodeList children = e.getChildNodes();
for (int i = children.getLength() - 1; i >= 0; i--) {
Node child = children.item(i);
if (child instanceof Text
&& ((Text) child).getData().trim().length() == 0) {
e.removeChild(child);
} else if (child instanceof Element) {
removeWhitespaceNodes((Element) child);
}
}
}
/**
* Loads an XML from a String.
* @param xmlString XML String
* @param schema Optional Schema. If null, no validation is performed
* @return Document instance
*/
public static Document loadXml(String xmlString, Schema schema) {
return XmlUtils.loadXml(new InputSource(new StringReader(xmlString)), schema, false);
}
/**
* Loads an XML from a String.
* @param xmlString XML String
* @param schema Optional Schema. If null, no validation is performed
* @param nameSpaceAware whether the Document should be namespace aware
* @return Document instance
* @throws JuException If the String cannot be converted to a DOM Document
*/
// Introduced this method as we had problems in ESW to validate a document unless it was nameSpaceAware...
public static Document loadXml(String xmlString, Schema schema, boolean nameSpaceAware) throws JuException {
return XmlUtils.loadXml(new InputSource(new StringReader(xmlString)), schema, nameSpaceAware);
}
/**
* Loads an XML Schema from the specified url.
* @param url URL to load schema from
* @return Schema instance or null if the url is null
*/
public static Schema loadSchema(URL url) {
if (url == null) return null;
try {
SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
return schemaFactory.newSchema(url);
} catch (Exception ex) {
throw new JuRuntimeException("Couldn't load XML schema: " + url, ex);
}
}
/**
* Indents the specified XML.
* @param xmlString XML string to be indented
* @param includeXmlDeclaration If true, the <?xml ... ?> declaration is included.
* @return Indented XML
*/
public static String indentXml(String xmlString, boolean includeXmlDeclaration) {
Document doc = XmlUtils.loadXml(xmlString, null);
return XmlUtils.toString(doc, includeXmlDeclaration, true);
}
/**
* Converts an XML to a String.
*
* This will create an XML string with encoding="UTF-8" and a standalone declaration,
* as specified in the Document.
*
* Indentation will be two blanks - if true.
* @param document XML Document
* @param includeXmlDeclaration If true, the <?xml ... ?> declaration is included.
* @param indent If true, result will be indented (pretty-printed), using two blanks
* for child indentation
* @return String representation of the XML
* @throws JuRuntimeException If the conversion fails
*/
public static String toString(Document document, boolean includeXmlDeclaration, boolean indent) throws JuRuntimeException {
try {
TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer = tf.newTransformer();
transformer.setOutputProperty(OutputKeys.STANDALONE, "yes");
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, (includeXmlDeclaration ? "no" : "yes"));
if (indent) {
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
}
try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
transformer.transform(new DOMSource(document), new StreamResult(os));
return new String(os.toByteArray(), "UTF-8");
}
} catch (Exception ex) {
throw new JuRuntimeException("Couldn't convert XML to String", ex);
}
}
/**
* Creates a new XmlBuilder to construct an XML document based on DOM (document
* object model).
* @param rootElementName Name of the root element
* @return XmlBuilder of the root element to construct the XML
*/
public static XmlBuilder buildXml(String rootElementName) {
return XmlBuilder.createRootBuilder(rootElementName, null, null);
}
/**
* Creates a new XmlBuilder to construct an XML document based on DOM (document
* object model) using the specified namespace
* @param rootElementName Name of the root element
* @return XmlBuilder of the root element to construct the XML
*/
public static XmlBuilder buildXml(String rootElementName, String namespacePrefix, String namespaceUri) {
return XmlBuilder.createRootBuilder(rootElementName, namespacePrefix, namespaceUri);
}
// private static class SchemaLoader {
// @SuppressWarnings("unchecked") // TODO: Use new apache commons collection API
// private final Map schemaCache = new LRUMap(XmlUtils.SCHEMA_CACHE_MAX_SIZE);
//
// synchronized Schema getSchema(URL schemaUrl) throws ServiceDbRuntimeException {
// // Try to get the schema
// Schema schema = this.schemaCache.get(schemaUrl);
//
// try {
// if (schema == null) {
// _log.info(String.format("Loading XML Schema: %s (current cache size: %d)", schemaUrl, schemaCache.size()));
// SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
// schema = schemaFactory.newSchema(schemaUrl);
// this.schemaCache.put(schemaUrl, schema);
// }
//
// return schema;
// } catch (Exception ex) {
// throw new ServiceDbRuntimeException("Couldn't load XML Schema: " + schemaUrl, ex);
// }
// }
// }
/**
* Returns a MarshallerBuilder that can be used to perform XML marshalling and unmarshalling.
*/
public static MarshallerBuilder marshaller() {
return new MarshallerBuilder();
}
public static class MarshallerBuilder {
private final Logger logger = LoggerFactory.getLogger(MarshallerBuilder.class);
private boolean cacheJaxbContext = true;
private static Cache cache;
private static int MAX_CACHE_SIZE = 100;
private boolean formattedOutput = false;
private Schema schema;
private JuNamespacePrefixMapper prefixMapper;
private NamespacePrefixMapperAdapter prefixMapperAdapter;
/**
* Whether to produce formatted output.
*
* Default is false.
* @param formattedOutput If true, XML output will be formatted / indented.
*/
public MarshallerBuilder formattedOutput(boolean formattedOutput) {
this.formattedOutput = formattedOutput;
return this;
}
/**
* Specifies an XML Schema to be used to validate the XML when marshalling or
* unmarshalling.
* @param schemaUrl URL to XML Schema (XSD)
*/
public MarshallerBuilder schema(URL schemaUrl) {
this.schema = XmlUtils.loadSchema(schemaUrl);
return this;
}
/**
* Sets the prefix for the specified namespace URI.
*
* NOTE: This relies on an SUN internal implementation class of JAXB so it might not
* work on all JDKs. If platform independent behavior is required, you should specify the namespaces
* using the package annotation package-info.java, e.g.
*
*
* {@literal @}javax.xml.bind.annotation.XmlSchema(
* namespace = "urn:inftec.ch/ju/util/xml/ns/main"
* , elementFormDefault = javax.xml.bind.annotation.XmlNsForm.QUALIFIED
* , xmlns = {
* {@literal @}XmlNs(namespaceURI = "urn:inftec.ch/ju/util/xml/ns/main", prefix="def")}
* )
* package ch.inftec.ju.util.xml.ns.main;
* import javax.xml.bind.annotation.XmlNs;
*
*
* @param prefix Namespace prefix, e.g. 'ns'. Use null for default namespace
* @param namespaceUri Namespace URI, e.g. 'urn:inftec.ch/ns'
*/
public MarshallerBuilder setNamespacePrefix(String prefix, String namespaceUri) {
if (this.prefixMapper == null) {
this.prefixMapper = new JuNamespacePrefixMapper();
}
this.prefixMapper.setPrefix(prefix, namespaceUri);
return this;
}
/**
* Sets a NamespacePrefixMapper adapter. This can/must be used if the JAXB implementation in use is not compatible
* with the JAXB implementation used by JU.
*/
public MarshallerBuilder setNamespacePrefixMapper(NamespacePrefixMapperAdapter prefixMapperAdapter) {
this.prefixMapperAdapter = prefixMapperAdapter;
return this;
}
/**
* Marshals the specified Object to a String, i.e. converts it to
* an XML.
* @param o Object to be marshalled
* @return XML String
*/
public String marshalToString(Object o) {
try (StringWriter writer = new StringWriter()) {
Object obj = o;
if (o instanceof JAXBElement) {
JAXBElement> e = (JAXBElement>)o;
obj = e.getValue();
}
logger.debug("Marshalling " + obj.getClass());
JAXBContext context = this.loadContext(obj.getClass().getPackage().getName());
javax.xml.bind.Marshaller marshaller = context.createMarshaller();
// Use the Schema to make sure the marshalled String is valid according the the Schema (if any)
marshaller.setSchema(this.schema);
// Configure Marshaller
if (this.formattedOutput) {
marshaller.setProperty(javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
}
if (this.prefixMapper != null) {
// Use the default namespace prefix mapper adapter if none has been specified explicitly
AssertUtil.assertNotNull("Prefix Mapper must be specified when using prefix mapping", this.prefixMapperAdapter);
// if (this.prefixMapperAdapter == null) {
// AssertUtil.
// this.prefixMapperAdapter = new DefaultNamespacePrefixMapperAdapter();
// }
marshaller.setProperty(
this.prefixMapperAdapter.getPropertyName()
, this.prefixMapperAdapter.getNamespacePrefixMapperImplementation(this.prefixMapper));
}
marshaller.marshal(o, writer);
String xmlString = writer.toString();
logger.debug("Marshalling done");
return xmlString;
} catch (Exception ex) {
throw new JuRuntimeException("Couldn't marshal object %s (using Schema: %s)", ex, o, null);//this.schema), ex);
}
}
/**
* Unmarshals the specified XML string, i.e. converts it into an object instance.
*
* Use this method if you're not sure which objects you will get.
* @param xmlString XML string to be unmarshalled
* @param objClass Object of the same package as the expected object, used to
* initialize the JAXBContext.
* @return Unmarshalled object. If by unmarshalling, we get a JAXBElement, the actual object
* will be unwrapped and returned
*/
public Object unmarshalRaw(String xmlString, Class> objClass) {
try (StringReader reader = new StringReader(xmlString)) {
JAXBContext context = this.loadContext(objClass.getPackage().getName());
Unmarshaller unmarshaller = context.createUnmarshaller();
// Set Schema to validate (if any)
unmarshaller.setSchema(this.schema);
Object obj = unmarshaller.unmarshal(reader);
if (obj instanceof JAXBElement) {
// Unwrap object
obj = ((JAXBElement>)obj).getValue();
}
return obj;
} catch (Exception ex) {
logger.debug("Marshalling of String failed: " + xmlString);
throw new JuRuntimeException("Couldn't unmarshal XML String (XML is dumped as debug log)", ex);
}
}
private JAXBContext loadContext(String contextPath) throws JAXBException {
if (this.cacheJaxbContext) {
// We'll cache the JAXBContext to avoid loading it every time we need it.
// JAXBContext is ThreadSave, but we'll need to create a separate Marshaller / Unmarshaller
// every time we do marshalling / unmarshalling and must not use the
// convenience method context.marshal / unmarshal.
synchronized(this) {
if (cache == null) {
cache = Caches.simpleBoundedCache(MAX_CACHE_SIZE, new Function() {
@Override
public JAXBContext apply(String key) {
return createContext(key);
}
});
}
}
return cache.get(contextPath);
} else {
return createContext(contextPath);
}
}
private JAXBContext createContext(String contextPath) {
logger.debug("Creating JAXBContext for path {}", contextPath);
try {
return JAXBContext.newInstance(contextPath);
} catch (JAXBException ex) {
throw new JuRuntimeException("Couldn't create JAXB context", ex);
}
}
/**
* Unmarshals the specified XML string, i.e. converts it into an object instance.
* @param xmlString XML string to be unmarshalled
* @param objClass Expected Object class used to build JAXBContext and evaluate validation Schema
* @return Unmarshalled object
* @param Type of expected object
*/
public T unmarshal(String xmlString, Class objClass) {
@SuppressWarnings("unchecked")
T object = (T)this.unmarshalRaw(xmlString, objClass);
return object;
}
/**
* Unmarshals the specified XML, i.e. converts it into an object instance.
* @param xmlUrl URL to XML to be unmarshalled
* @param objClass Expected Object class used to build JAXBContext and evaluate validation Schema
* @return Unmarshalled object
* @param Type of expected object
*/
public T unmarshal(URL xmlUrl, Class objClass) {
String xmlString = new IOUtil().loadTextFromUrl(xmlUrl);
@SuppressWarnings("unchecked")
T object = (T)this.unmarshalRaw(xmlString, objClass);
return object;
}
/**
* Helper interface to provide a JAXB implementation specific instance of the NamespacePrefixMapper.
* @author [email protected]
*
*/
public interface NamespacePrefixMapperAdapter {
/**
* Gets the property name for the NamespacePrefixMapper property of the JAXB marshaller.
*/
String getPropertyName();
/**
* Gets an implementation of the NamespacePrefixMapper.
*
* A JU specific PrefixMapper will be provided as a parameter, so the call to getPreferredPrefix
* can just be forwarded to this mapper.
* @param prefixMapper JU specific Prefix mapper
* @return JAXB implementation specific mapper
*/
Object getNamespacePrefixMapperImplementation(PrefixMapper prefixMapper);
/**
* JU specific prefix mapper.
* @author [email protected]
*
*/
interface PrefixMapper {
/**
* Evaluates the preferred prefix
* @param namespaceUri XSD namespace URI
* @param suggestion Suggested prefix
* @param requirePrefix If a prefix is required
* @return Preferred prefix
*/
String getPreferredPrefix(String namespaceUri, String suggestion, boolean requirePrefix);
}
}
}
/**
* Validates the specified XML String using the XSD Schema.
* @param xml XML to validate
* @param schema Schema instance
* @throws JuException If the validation fails
*/
public static void validate(String xml, Schema schema) throws JuException {
try {
schema.newValidator().validate(new DOMSource(XmlUtils.loadXml(xml, null)));
} catch (SAXParseException ex) {
throw new JuException("Parse exception: " + ex.getMessage(), ex);
} catch (Exception ex) {
throw new JuException("Validation failed", ex);
}
}
/**
* Validates the specified XML String using the XSD Schema.
* @param xml XML to validate
* @param schema Schema instance
* @param nameSpaceAware Whether validation should be namespace aware
* @throws JuException If the validation fails
*/
public static void validate(String xml, Schema schema, boolean nameSpaceAware) throws JuException {
try {
schema.newValidator().validate(new DOMSource(XmlUtils.loadXml(xml, null, nameSpaceAware)));
} catch (SAXParseException ex) {
throw new JuException("Parse exception: " + ex.getMessage(), ex);
} catch (Exception ex) {
throw new JuException("Validation failed", ex);
}
}
/**
* Converts a java.util.Date into an instance of XMLGregorianCalendar.
*
* The timezone of the calendar will be set to GMT to enforce Zulu-Time-Format
* in the XML messages.
* @param date Instance of java.util.Date or a null reference
* @return XMLGregorianCalendar instance whose value is based upon the
* value in the date parameter. If the date parameter is null then
* this method will simply return null.
*/
public static XMLGregorianCalendar asXMLGregorianCalendar(java.util.Date date) {
if (date == null) {
return null;
} else {
GregorianCalendar gc = new GregorianCalendar();
gc.setTime(date);
gc.setTimeZone(TimeZone.getTimeZone("GMT")); // Set time zone to GMT to enforce Zulu-Time in XML
return getDatetypeFactory().newXMLGregorianCalendar(gc);
}
}
/**
* Converts an XMLGregorianCalendar to an instance of java.util.Date
*
* @param xgc Instance of XMLGregorianCalendar or a null reference
* @return java.util.Date instance whose value is based upon the
* value in the xgc parameter. If the xgc parameter is null then
* this method will simply return null.
*/
public static java.util.Date asDate(XMLGregorianCalendar xgc) {
if (xgc == null) {
return null;
} else {
return xgc.toGregorianCalendar().getTime();
}
}
}