/** * Copyright (c) Codice Foundation * <p> * This is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser * General Public License as published by the Free Software Foundation, either version 3 of the * License, or any later version. * <p> * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. A copy of the GNU Lesser General Public License * is distributed along with this program and can be found at * <http://www.gnu.org/licenses/lgpl.html>. */ package org.codice.ddf.platform.util; import java.io.StringReader; import java.io.StringWriter; import java.io.Writer; import java.util.Map.Entry; import java.util.function.BiFunction; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamConstants; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; import javax.xml.transform.OutputKeys; import javax.xml.transform.Result; import javax.xml.transform.Source; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Node; /** * Utility for handling XML */ public class XMLUtils { private static final Logger LOGGER = LoggerFactory.getLogger(XMLUtils.class); protected static volatile XMLInputFactory xmlInputFactory; private static synchronized void initializeXMLInputFactory() { if (xmlInputFactory == null) { xmlInputFactory = XMLInputFactory.newInstance(); } xmlInputFactory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, Boolean.FALSE); xmlInputFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, Boolean.FALSE); xmlInputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, Boolean.FALSE); // This disables DTDs entirely for that factory xmlInputFactory.setProperty(XMLInputFactory.IS_COALESCING, Boolean.FALSE); } /** * Formats XML into a String * * @param sourceXml to transform a given Source * @param transformProperties settings for transformer * @return XML string */ public static String format(Source sourceXml, TransformerProperties transformProperties) { Writer buffer = new StringWriter(); Result streamResult = new StreamResult(buffer); transformation(sourceXml, transformProperties, streamResult); return buffer.toString(); } /** * @param nodeXml to transform a given Node * @param transformerProperties settings for transformer * @return XML String */ public static String format(Node nodeXml, TransformerProperties transformerProperties) { return format(new DOMSource(nodeXml), transformerProperties); } /** * Nicely formats XML into a String * * @param sourceXml to transform a given Source * @return XML string */ public static String prettyFormat(Source sourceXml) { TransformerProperties transformerProperties = new TransformerProperties(); transformerProperties.addOutputProperty(OutputKeys.INDENT, "yes"); transformerProperties.addOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); return format(sourceXml, transformerProperties); } /** * Nicely formats XML into a String * * @param nodeXml to transform a given Node * @return XML string */ public static String prettyFormat(Node nodeXml) { return prettyFormat(new DOMSource(nodeXml)); } /** * Transforms XML into a Result * * @param sourceXml to transform a given Source * @param transformProperties settings for transformer * @param result Result to transform into * @return XML Result */ public static Result transform(Source sourceXml, TransformerProperties transformProperties, Result result) { transformation(sourceXml, transformProperties, result); return result; } /** * @param nodeXml to transform a given Node * @param transformerProperties settings for transformer * @param result Result to transform into * @return XML Result */ public static Result transform(Node nodeXml, TransformerProperties transformerProperties, Result result) { return transform(new DOMSource(nodeXml), transformerProperties, result); } /** * @param xml The XML whose root namespace you want * @return Root Namespace */ public static String getRootNamespace(String xml) { if (xml == null) { return null; } return processElements(xml, (result, xmlStreamReader) -> { result.set(xmlStreamReader.getNamespaceURI()); return false; }); } private static void transformation(Source sourceXml, TransformerProperties transformProperties, Result result) { ClassLoader tccl = Thread.currentThread() .getContextClassLoader(); Thread.currentThread() .setContextClassLoader(XMLUtils.class.getClassLoader()); try { TransformerFactory transFactory = TransformerFactory.newInstance(); Transformer transformer = transFactory.newTransformer(); for (Entry<String, String> entry : transformProperties.getOutputProperties()) { transformer.setOutputProperty(entry.getKey(), entry.getValue()); } if (transformProperties.getErrorListener() != null) { transformer.setErrorListener(transformProperties.getErrorListener()); } transformer.transform(sourceXml, result); } catch (TransformerException e) { LOGGER.debug("Unable to transform XML.", e); } finally { Thread.currentThread() .setContextClassLoader(tccl); } } /** * Iterate through the elements of an XML document. The processor calls * the processElementFunction for each element. * Call result.set() to change the value that will be returned. * <p> * If the function returns true, processing continues to the next element. * When the last element in the document is processed, the value in the result * is returned. * <p> * If the lambda function returns false, processing stops. The value of the result is returned. * <p> * If the function encounters a processing exception, processing stops and null is returned. * * @param xml The XML to process * @param processElementFunction Function that accepts an instance of XMLStreamReader and result holder. * The function must return a boolean. * @return <T> The result of the processing */ public static <T> T processElements(String xml, BiFunction<ResultHolder<T>, XMLStreamReader, Boolean> processElementFunction) { initializeXMLInputFactory(); XMLStreamReader xmlStreamReader = null; ResultHolder<T> result = new ResultHolder<>(); boolean keepProcessing = true; try (StringReader strReader = new StringReader(xml)) { synchronized (XMLUtils.class) { xmlStreamReader = xmlInputFactory.createXMLStreamReader(strReader); } while (keepProcessing && xmlStreamReader.hasNext()) { int event = xmlStreamReader.next(); if (event == XMLStreamConstants.START_ELEMENT) { keepProcessing = processElementFunction.apply(result, xmlStreamReader); } } } catch (XMLStreamException e) { result.set(null); LOGGER.debug("{} ", XMLUtils.class.getSimpleName(), e); } finally { if (xmlStreamReader != null) { try { xmlStreamReader.close(); } catch (XMLStreamException e) { // ignore } } } return result.get(); } /** * This class is used with the processElements method. Inside the function, set the value * of the result holder. That value is then returned by the processElementsFunction. */ public static class ResultHolder<T> { T value; public ResultHolder() { } public T get() { return value; } public void set(T value) { this.value = value; } public boolean isEmpty() { return get() == null; } public void setIfEmpty(T value) { if (isEmpty()) { set(value); } } } }