Created
April 13, 2020 14:39
-
-
Save Stwissel/ff2dca14074058e80c374bf6cfd0897e to your computer and use it in GitHub Desktop.
Utility class to generate valid XML in Java, streaming style, see https://wissel.net/blog/2020/04/simplexmldoc-revisited.html for details
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* ========================================================================= * | |
* 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