Skip to content

Instantly share code, notes, and snippets.

@kimble
Created September 19, 2014 21:22
Show Gist options
  • Save kimble/158009ee8198f939d2ea to your computer and use it in GitHub Desktop.
Save kimble/158009ee8198f939d2ea to your computer and use it in GitHub Desktop.
NoXML
package com.developerb.noxml;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.io.ByteSource;
import org.joda.time.LocalDate;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
/**
* Born out of a burning hatred for Java XML APIs and namespaced XML files.
*
* @author Kim A. Betti
*/
public class NoXML {
private final DocumentBuilder docBuilder;
private final Transformer transformer;
public NoXML(Feature... features) throws ParserConfigurationException, TransformerConfigurationException {
DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
docBuilderFactory.setNamespaceAware(true);
Transformer transformer = TransformerFactory.newInstance().newTransformer();
for (Feature feature : features) {
feature.applyTo(transformer);
}
this.docBuilder = docBuilderFactory.newDocumentBuilder();
this.transformer = transformer;
}
public Cursor from(final String xml) throws Exception {
return from(new ByteSource() {
@Override
public InputStream openStream() throws IOException {
return new ByteArrayInputStream(xml.getBytes());
}
});
}
public Cursor from(ByteSource source) throws Exception {
try (InputStream stream = source.openStream()) {
final Document document = docBuilder.parse(stream);
return new Cursor(document.getDocumentElement());
}
}
public static interface Converter<R> {
R transform(Cursor cursor) throws Ex;
}
public class Cursor {
private final Node node;
private final List<Cursor> ancestors;
private final int index;
Cursor(Node node) {
this(new ArrayList<Cursor>(), node, 0);
}
Cursor(List<Cursor> ancestors, Node node, int index) {
Preconditions.checkNotNull(ancestors, "Ancestors can't be null");
Preconditions.checkArgument(index >= 0, "Index must be greater or equal to zero");
Preconditions.checkNotNull(node, "Node can't be null");
this.ancestors = ancestors;
this.index = index;
this.node = node;
}
public Cursor to(String firstName, String... remainingNames) throws Ex {
Cursor cursor = to(firstName);
for (String nextName : remainingNames) {
cursor = cursor.to(nextName);
}
return cursor;
}
public Cursor update(String tagName, String updatedValue) throws Ex {
to(tagName).text(updatedValue);
return this;
}
public Cursor to(String tagName) throws Ex {
Node found = null;
final NodeList childNodes = node.getChildNodes();
for (int i = 0; i < childNodes.getLength(); i++) {
final Node childNode = childNodes.item(i);
final String localName = childNode.getLocalName();
if (localName != null && localName.equalsIgnoreCase(tagName)) {
if (found != null) {
throw new Ambiguous(this, tagName);
}
else {
found = childNode;
}
}
}
if (found == null) {
throw new MissingNode(this, tagName, childNodes);
}
else {
final List<Cursor> newAncestorList = newAncestorList();
return new Cursor(newAncestorList, found, 0);
}
}
public Cursor to(int position, String tagName) throws MissingNode {
int count = 0;
final NodeList childNodes = node.getChildNodes();
for (int i = 0; i < childNodes.getLength(); i++) {
final Node childNode = childNodes.item(i);
final String localName = childNode.getLocalName();
if (localName != null && localName.equalsIgnoreCase(tagName)) {
count++;
if (count == position + 1) {
final List<Cursor> newAncestorList = newAncestorList();
return new Cursor(newAncestorList, childNode, position);
}
}
}
throw new MissingNode(this, tagName, childNodes); // Todo... position..
}
private List<Cursor> newAncestorList() {
final List<Cursor> newAncestorList = Lists.newArrayList(ancestors);
newAncestorList.add(this);
return newAncestorList;
}
public int count(String tagName) {
int count = 0;
final NodeList childNodes = node.getChildNodes();
for (int i = 0; i < childNodes.getLength(); i++) {
final Node childNode = childNodes.item(i);
final String localName = childNode.getLocalName();
if (localName != null && localName.equalsIgnoreCase(tagName)) {
count++;
}
}
return count;
}
public <R> R convertCurrent(Converter<R> converter) throws Ex {
return converter.transform(this);
}
public LocalDate localDate(String format) {
DateTimeFormatter formatter = DateTimeFormat.forPattern(format);
return formatter.parseLocalDate(text());
}
public boolean hasText() {
return isNotBlank(text());
}
public String text() {
return node.getTextContent();
}
public String name() {
return node.getLocalName();
}
@Override
public String toString() {
return describePath();
}
public String describePath() {
StringBuilder builder = new StringBuilder("");
for (Cursor ancestor : ancestors) {
builder.append(ancestor.name());
if (ancestor.index > 0) {
builder.append("[")
.append(ancestor.index)
.append("]");
}
builder.append(" >> ");
}
builder.append(name());
if (index > 0) {
builder.append("[")
.append(index)
.append("]");
}
return builder.toString();
}
public Cursor text(String updatedText) {
node.setTextContent(updatedText);
return this;
}
public String dump() throws Ex {
try {
StringWriter sw = new StringWriter();
transformer.transform(new DOMSource(node), new StreamResult(sw));
return sw.toString();
}
catch (Exception ex) {
throw new Ex(this, "Technical difficulties", ex);
}
}
}
public static class Ex extends Exception {
protected Ex(Cursor cursor, String message) {
super(cursor.describePath() + " -- " + message);
}
public Ex(Cursor cursor, String message, Throwable cause) {
super(cursor.describePath() + " -- " + message, cause);
}
}
public static class MissingNode extends Ex {
MissingNode(Cursor cursor, String needle, NodeList childNodes) {
super(cursor, "Unable to find '" + needle + "' - Did you mean: " + summarize(childNodes) + "?");
}
public MissingNode(Cursor cursor, String needle) {
super(cursor, "Unable to find '" + needle + "'");
}
private static String summarize(NodeList childNodes) {
final Set<String> names = Sets.newTreeSet();
for (int i=0; i<childNodes.getLength(); i++) {
final Node item = childNodes.item(i);
if (item.getLocalName() != null) {
names.add(item.getLocalName());
}
}
return Joiner.on(", ").join(names);
}
}
public static class Ambiguous extends Ex {
Ambiguous(Cursor cursor, String needle) {
super(cursor, "Expected to find a single instance of " + needle);
}
}
public static enum Feature {
DUMP_INDENTED_XML {
@Override
void applyTo(Transformer t) {
t.setOutputProperty(OutputKeys.INDENT, "yes");
}
},
DUMP_WITHOUT_XML_DECLARATION {
@Override
void applyTo(Transformer t) {
t.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
}
}
;
abstract void applyTo(Transformer t);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment