Created
September 19, 2014 21:22
-
-
Save kimble/158009ee8198f939d2ea to your computer and use it in GitHub Desktop.
NoXML
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
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