Skip to content

Instantly share code, notes, and snippets.

@Stwissel
Created April 13, 2020 14:39
Show Gist options
  • Save Stwissel/ff2dca14074058e80c374bf6cfd0897e to your computer and use it in GitHub Desktop.
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
/**
* ========================================================================= *
* 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