Utility class to generate valid XML in Java, streaming style, see https://wissel.net/blog/2020/04/simplexmldoc-revisited.html for details
/** | |
* ========================================================================= * | |
* Copyright (C) 2006, 2007 TAO Consulting Pte <http://www.taoconsulting.sg/> * | |
* Copyright (C) 2011 IBM Corporation ( http://www.ibm.com/ ) * | |
* Copyright (C) 2020 HCL ( http://www.hcl.com/ ) * | |
* All rights reserved. * | |
* ========================================================================== * | |
* * | |
* Licensed 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 com.hcl.domino.keep.tools; | |
import java.io.OutputStream; | |
import java.io.PrintWriter; | |
import java.text.SimpleDateFormat; | |
import java.util.ArrayList; | |
import java.util.Date; | |
import java.util.HashMap; | |
import java.util.Locale; | |
import java.util.Map; | |
import java.util.Stack; | |
import java.util.TreeMap; | |
import javax.xml.XMLConstants; | |
import javax.xml.transform.OutputKeys; | |
import javax.xml.transform.Transformer; | |
import javax.xml.transform.TransformerConfigurationException; | |
import javax.xml.transform.TransformerFactory; | |
import javax.xml.transform.sax.SAXTransformerFactory; | |
import javax.xml.transform.sax.TransformerHandler; | |
import javax.xml.transform.stream.StreamResult; | |
import org.slf4j.Logger; | |
import org.slf4j.LoggerFactory; | |
import org.xml.sax.Attributes; | |
import org.xml.sax.SAXException; | |
import org.xml.sax.helpers.AttributesImpl; | |
/** | |
* Convenience class to write out an XML document using a stream and a SAX | |
* parser. A stack is used to keep track of open and closed tag and on document | |
* close all tags are properly closed. It doesn't guarantee the XML you might | |
* want or need, but it will be valid XML. The SAX Parser esures correct | |
* encoding of the tags and values | |
* | |
* @author stw | |
*/ | |
public class SimpleXMLDoc { | |
/** | |
* @author Stephan H. Wissel | |
* Element with namespace for XML document creation | |
*/ | |
public class Element { | |
private final String elementName; | |
private final Map<String, String> attributes = new TreeMap<>(); | |
/** | |
* @param elementName Name of Element including namespace prefix | |
*/ | |
public Element(final String elementName) { | |
this.elementName = elementName; | |
} | |
/** | |
* @param attributeName Attribute name including namespace prefix | |
* @param attributeDate Date to be converted | |
* @return fluent | |
*/ | |
public Element addAttribute(final String attributeName, final Date attributeDate) { | |
final String attributeValue = SimpleXMLDoc.DATEFORMATTER.format(attributeDate); | |
this.attributes.put(attributeName, attributeValue); | |
return this; | |
} | |
/** | |
* @param attributeName Attribute name including namespace prefix | |
* @param attributeValue Attribute value | |
* @return fluent | |
*/ | |
public Element addAttribute(final String attributeName, final String attributeValue) { | |
this.attributes.put(attributeName, attributeValue); | |
return this; | |
} | |
/** | |
* Returns all attributes of the given Element/Element | |
* | |
* @return attributes, can be empty | |
*/ | |
public Attributes getAttributes() { | |
final AttributesImpl atts = new AttributesImpl(); | |
this.attributes.forEach((key, value) -> { | |
atts.addAttribute(XMLConstants.NULL_NS_URI, key, key, "CDATA", value); | |
}); | |
return atts; | |
} | |
/** | |
* @return the elementName | |
*/ | |
public String getElementName() { | |
return this.elementName; | |
} | |
} | |
private static final Logger logger = LoggerFactory.getLogger(SimpleXMLDoc.class); | |
// this.tagValue = DATEFORMATTER.format(date); | |
private static final SimpleDateFormat DATEFORMATTER = new SimpleDateFormat("EE', 'd' 'MMM' 'yyyy' 'H':'m':'s' 'z", Locale.UK); | |
private TransformerHandler hd = null; // Handler for SAX output | |
private String docTypeSystem = null; // XML System type (optional) | |
private String docTypePublic = null; // XML Public type (optional) | |
private boolean documentStarted = false; // Flag for the document | |
private String xmlStyleSheet = null; // Stylesheet (optional) | |
private final OutputStream out; // Where the result goes | |
private final Stack<String> xmlElementStack = new Stack<>(); // Keeping track of all open / closed XML tags | |
private final ArrayList<String> comments = new ArrayList<>(); // We can't write comments before we have the first element | |
private final Map<String, String> namespaces = new HashMap<>(); | |
/** | |
* Default constructor, creates empty document | |
* | |
* @param out Where the document goes | |
*/ | |
public SimpleXMLDoc(final OutputStream out) { | |
this.out = out; | |
} | |
/** | |
* @param element - an XML Element | |
* @param elementValue - Lot of String to encapsulate | |
*/ | |
public void addCdataElement(final Element element, final String elementValue) { | |
try { | |
this.openElement(element); | |
this.hd.startCDATA(); | |
this.addTagContent(elementValue); | |
this.hd.endCDATA(); | |
this.closeElement(1); | |
} catch (final SAXException e) { | |
SimpleXMLDoc.logger.error(e.getMessage(), e); | |
} | |
} | |
/** | |
* @param elementName | |
* @param elementValue | |
*/ | |
public void addCdataElement(final String elementName, final String elementValue) { | |
final Element tag = this.element(elementName); | |
this.addCdataElement(tag, elementValue); | |
} | |
/** | |
* Adds a comment to the result. Comments are only written after the root | |
* element, so we cache them if necessary | |
* | |
* @param comment | |
* The comment text | |
*/ | |
public void addComment(final String comment) { | |
if (!this.documentStarted) { | |
this.comments.add(comment); | |
} else { | |
try { | |
this.hd.comment(comment.toCharArray(), 0, comment.length()); | |
} catch (final SAXException e) { | |
SimpleXMLDoc.logger.error(e.getMessage(), e); | |
} | |
} | |
} | |
/** | |
* @param element The tag to add | |
* @return fluent SimpleXMLDoc | |
*/ | |
public SimpleXMLDoc addEmptyElement(final Element element) { | |
return this.addSimpleElement(element, null); | |
} | |
/** | |
* @param elementName | |
* @return fluent SimpleXMLDoc | |
*/ | |
public SimpleXMLDoc addEmptyElement(final String elementName) { | |
final Element tag = this.element(elementName); | |
return this.addSimpleElement(tag, null); | |
} | |
/** | |
* Add namespace to a document | |
* | |
* @param key Namespace abbreviation without : | |
* @param uri Namespace URL | |
* @return fluent SimpleXMLDoc | |
*/ | |
public SimpleXMLDoc addNamespace(final String key, final String uri) { | |
this.namespaces.put(key, uri); | |
return this; | |
} | |
/** | |
* @param element | |
* @param elementValue | |
* @return fluent SimpleXMLDoc | |
*/ | |
public SimpleXMLDoc addSimpleElement(final Element element, final String elementValue) { | |
this.openElement(element); | |
this.addTagContent(elementValue); | |
this.closeElement(1); | |
return this; | |
} | |
/** | |
* @param elementName | |
* @param elementValue | |
* @return fluent SimpleXMLDoc | |
*/ | |
public SimpleXMLDoc addSimpleElement(final String elementName, final String elementValue) { | |
final Element tag = this.element(elementName); | |
return this.addSimpleElement(tag, elementValue); | |
} | |
/** | |
* Closes the document and ensures that all tags are closed | |
*/ | |
public void closeDocument() { | |
this.closeElement(-1); // Make sure all tages are closes | |
// Closing of the document, | |
try { | |
this.hd.endDocument(); | |
} catch (final SAXException e) { | |
SimpleXMLDoc.logger.error(e.getMessage(), e); | |
} | |
} | |
/** | |
* @param howMany | |
*/ | |
public void closeElement(final int howMany) { | |
if (howMany < 0) { | |
while (!this.xmlElementStack.empty()) { | |
final String closeTag = this.xmlElementStack.pop(); | |
try { | |
this.hd.endElement("", "", closeTag); | |
} catch (final SAXException e) { | |
SimpleXMLDoc.logger.error(e.getMessage(), e); | |
} | |
} | |
} else { | |
for (int i = 0; i < howMany; i++) { | |
if (!this.xmlElementStack.empty()) { | |
final String closeTag = this.xmlElementStack.pop(); | |
try { | |
this.hd.endElement("", "", closeTag); | |
} catch (final SAXException e) { | |
SimpleXMLDoc.logger.error(e.getMessage(), e); | |
} | |
} else { | |
break; // No point looping | |
} | |
} | |
} | |
} | |
/** | |
* @param lastElementToClose | |
* @return closes was successful | |
*/ | |
public boolean closeElement(final String lastElementToClose) { | |
boolean result = false; | |
while (!this.xmlElementStack.empty()) { | |
final String closeTag = this.xmlElementStack.pop(); | |
try { | |
this.hd.endElement("", "", closeTag); | |
} catch (final SAXException e) { | |
SimpleXMLDoc.logger.error(e.getMessage(), e); | |
} | |
if (closeTag.equals(lastElementToClose)) { | |
result = true; | |
break; | |
} | |
} | |
return result; | |
} | |
/** | |
* @param element | |
* @param date | |
* @return fluent SimpleXMLDoc | |
*/ | |
public SimpleXMLDoc dateElement(final Element element, final Date date) { | |
if (date == null) { | |
return this; | |
} | |
// "Sat, 26 Mar 2005 11:22:20 GMT"; | |
final String datestring = SimpleXMLDoc.DATEFORMATTER.format(date); | |
return this.addSimpleElement(element, datestring); | |
} | |
/** | |
* @param elementName | |
* @param date | |
* @return fluent SimpleXMLDoc | |
*/ | |
public SimpleXMLDoc dateElement(final String elementName, final Date date) { | |
final Element tag = this.element(elementName); | |
return this.dateElement(tag, date); | |
} | |
/** | |
* @param name - Name for the tag | |
* @return a new Element with that name | |
*/ | |
public Element element(final String name) { | |
return new Element(name); | |
} | |
/** | |
* @return Public doc type if any | |
*/ | |
public String getDocTypePublic() { | |
return this.docTypePublic; | |
} | |
/** | |
* @return System doctyep if any | |
*/ | |
public String getDocTypeSystem() { | |
return this.docTypeSystem; | |
} | |
/** | |
* @return Stylesheet if any | |
*/ | |
public String getXmlStyleSheet() { | |
return this.xmlStyleSheet; | |
} | |
/** | |
* Starts a tag with a given tag element | |
* that contains name and attributes | |
* | |
* @param element - Element to open | |
* @return Fluent SimpleXMLDoc | |
*/ | |
public SimpleXMLDoc openElement(final Element element) { | |
if (!this.documentStarted) { | |
this.initializeDoc(); | |
} | |
final String tagName = element.getElementName(); | |
try { | |
this.hd.startElement(XMLConstants.NULL_NS_URI, tagName, tagName, element.getAttributes()); | |
this.xmlElementStack.push(tagName); // Memorize that we opened it! | |
// If there are deferred comments process them | |
if (!this.comments.isEmpty()) { | |
this.comments.forEach(comment -> this.addComment(comment)); | |
this.comments.clear(); | |
} | |
} catch (final SAXException e) { | |
SimpleXMLDoc.logger.error(e.getMessage(), e); | |
} | |
return this; | |
} | |
/** | |
* @param elementName | |
* @return fluent SimpleXMLDoc | |
*/ | |
public SimpleXMLDoc openElement(final String elementName) { | |
final Element tag = this.element(elementName); | |
return this.openElement(tag); | |
} | |
/** | |
* @param docTypePublic | |
* @return fluent SimpleXMLDoc | |
*/ | |
public SimpleXMLDoc setDocTypePublic(final String docTypePublic) { | |
this.docTypePublic = docTypePublic; | |
return this; | |
} | |
/** | |
* @param docTypeSystem | |
* @return fluent SimpleXMLDoc | |
*/ | |
public SimpleXMLDoc setDocTypeSystem(final String docTypeSystem) { | |
this.docTypeSystem = docTypeSystem; | |
return this; | |
} | |
/** | |
* @param xmlStyleSheet | |
* @return fluent SimpleXMLDoc | |
*/ | |
public SimpleXMLDoc setXmlStyleSheet(final String xmlStyleSheet) { | |
this.xmlStyleSheet = xmlStyleSheet; | |
return this; | |
} | |
private SimpleXMLDoc addTagContent(final String content) { | |
if (content != null) { | |
try { | |
this.hd.characters(content.toCharArray(), 0, content.length()); | |
} catch (final SAXException e) { | |
SimpleXMLDoc.logger.error(e.getMessage(), e); | |
} | |
} | |
return this; | |
} | |
/** | |
* Starts a document if not yet there | |
*/ | |
private void initializeDoc() { | |
final PrintWriter pw = new PrintWriter(this.out); // out comes from outside | |
final StreamResult streamResult = new StreamResult(pw); | |
// Factory pattern at work | |
final SAXTransformerFactory tf = (SAXTransformerFactory) TransformerFactory | |
.newInstance(); | |
try { | |
this.hd = tf.newTransformerHandler(); | |
final Transformer serializer = this.hd.getTransformer(); | |
serializer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); | |
serializer.setOutputProperty(OutputKeys.METHOD, "xml"); | |
serializer.setOutputProperty(OutputKeys.INDENT, "yes"); | |
if (this.docTypeSystem != null) { | |
serializer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, | |
this.docTypeSystem); | |
} | |
if (this.docTypePublic != null) { | |
serializer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, | |
this.docTypePublic); | |
} | |
this.hd.setResult(streamResult); | |
// This creates the empty document | |
this.hd.startDocument(); | |
this.namespaces.forEach((k, v) -> { | |
try { | |
this.hd.startPrefixMapping(k, v); | |
} catch (final SAXException e) { | |
SimpleXMLDoc.logger.error(e.getMessage(), e); | |
} | |
}); | |
// Get a processing instruction | |
if (this.xmlStyleSheet != null) { | |
this.hd.processingInstruction("xml-stylesheet", "type=\"text/xsl\" href=\"" + this.xmlStyleSheet + "\""); | |
} | |
this.documentStarted = true; // We memorise that | |
} catch (final TransformerConfigurationException e) { | |
SimpleXMLDoc.logger.error(e.getMessage(), e); | |
this.documentStarted = false; | |
} catch (final SAXException e) { | |
SimpleXMLDoc.logger.error(e.getMessage(), e); | |
this.documentStarted = false; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment