package org.openbel.framework.test;
import static org.openbel.framework.common.BELUtilities.asPath;
import static javax.xml.xpath.XPathConstants.NODESET;
import static junit.framework.Assert.assertTrue;
import static org.custommonkey.xmlunit.XMLAssert.assertXMLEqual;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import javax.xml.namespace.NamespaceContext;
import javax.xml.transform.TransformerException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.custommonkey.xmlunit.Diff;
import org.custommonkey.xmlunit.ElementNameAndAttributeQualifier;
import org.custommonkey.xmlunit.ElementQualifier;
import org.custommonkey.xmlunit.Transform;
import org.custommonkey.xmlunit.XMLUnit;
import org.junit.BeforeClass;
import org.junit.Test;
import org.w3c.dom.CharacterData;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.openbel.framework.tools.DocumentConverter;
public class TestDocumentConverter {
private static final String BEL_NAMESPACE_URI = "http://belframework.org/schema/1.0/xbel";
private static File corpusDir, cwd, nornalizeXbelXslt;
@BeforeClass
public static void setup() throws URISyntaxException {
corpusDir = new File(asPath("..", "docs", "xbel", "corpus"));
assertTrue(corpusDir.isDirectory());
cwd = new File(System.getProperty("user.dir"));
assertTrue(cwd.isDirectory());
nornalizeXbelXslt = new File(TestDocumentConverter.class.getClassLoader()
.getResource("normalize-xbel.xslt").toURI());
}
/*
* Checks that Document Converter's conversion of docs/xbel/corpus/small_corpus.bel to
* XBEL format is similar to docs/xbel/corpus/small_corpus.xbel.
*/
@Test
public void testThatConvertedSmallCorpusIsCorrect() throws IOException, SAXException,
TransformerException, XPathExpressionException {
final String msg = "testThatConvertedSmallCorpusIsCorrect";
File testXbelFile = null;
InputStream controlInputStream = null, testInputStream = null;
boolean deleteTemp = false;
try {
final File smallCorpusBelFile = new File(corpusDir, "small_corpus.bel");
assertTrue(smallCorpusBelFile.isFile());
final File controlXbelFile = new File(corpusDir, "small_corpus.xbel");
assertTrue(controlXbelFile.isFile());
testXbelFile = File.createTempFile("test", ".xbel", cwd);
assertTrue(testXbelFile.isFile());
// Convert small_corpus.bel to XBEL format
final String[] args = new String[] {
smallCorpusBelFile.getAbsolutePath(),
"-t", "BEL",
"-o", testXbelFile.getAbsolutePath(), "--no-preserve",
"--verbose", "--debug"
};
System.out.println("Running DocumentConverter with arguments " + Arrays.toString(args));
DocumentConverter.main(args);
controlInputStream = new FileInputStream(controlXbelFile);
testInputStream = new FileInputStream(testXbelFile);
final XPathFactory factory = XPathFactory.newInstance();
final XPathMemo xpath = new XPathMemo(factory.newXPath());
final Document control = transformControl(xpath, controlInputStream);
final Document test = transformTest(testInputStream);
final Diff diff = new Diff(control, test,
null, new XbelQualifier());
assertXMLEqual(msg, diff, true);
deleteTemp = true;
} finally {
if (controlInputStream != null) {
try {
controlInputStream.close();
} catch (IOException ex) {}
}
if (testInputStream != null) {
try {
testInputStream.close();
} catch (IOException ex) {}
}
if (testXbelFile != null && deleteTemp) {
testXbelFile.delete();
}
}
}
/*
* Checks that Document Converter, when run twice (i.e. XBEL -> BEL -> XBEL), outputs a
* a similar XBEL document to the original XBEL. The check is performed for:
* docs/xbel/corpus/{small,tiny,micro}_corpus.xbel.
*/
@Test
public void testThatXbelToBelToXbelIsIdentity() throws IOException, SAXException, TransformerException,
XPathExpressionException {
final String msg = "testThatXbelToBelToXbelIsIdentity";
final XPathFactory factory = XPathFactory.newInstance();
final XPathMemo xpath = new XPathMemo(factory.newXPath());
final File smallCorpusFile = new File(corpusDir, "small_corpus.xbel");
assertTrue(smallCorpusFile.isFile());
testThatXbelToBelToXbelIsIdentity(xpath, smallCorpusFile, msg + "(small_corpus.xbel)");
final File tinyCorpusFile = new File(corpusDir, "tiny_corpus.xbel");
assertTrue(tinyCorpusFile.isFile());
testThatXbelToBelToXbelIsIdentity(xpath, tinyCorpusFile, msg + "(tiny_corpus.xbel)");
final File microCorpusFile = new File(corpusDir, "micro_corpus.xbel");
assertTrue(microCorpusFile.isFile());
testThatXbelToBelToXbelIsIdentity(xpath, microCorpusFile, msg + "(micro_corpus.xbel)");
}
private void testThatXbelToBelToXbelIsIdentity(final XPathMemo xpath, final File controlXbelFile,
final String msg)
throws IOException, SAXException, TransformerException, XPathExpressionException {
File testBelFile = null, testXbelFile = null;
InputStream controlInputStream = null, testInputStream = null;
boolean deleteTemp = false;
try {
// Convert from .xbel to .bel
testBelFile = File.createTempFile("test", ".bel", cwd);
assertTrue(testBelFile.isFile());
final String[] args1 = new String[] {
controlXbelFile.getAbsolutePath(),
"-t", "XBEL",
"-o", testBelFile.getAbsolutePath(), "--no-preserve",
"--verbose", "--debug"
};
System.out.println("Running DocumentConverter with arguments " + Arrays.toString(args1));
DocumentConverter.main(args1);
// Convert from .bel to .xbel
testXbelFile = File.createTempFile("test", ".xbel", cwd);
assertTrue(testXbelFile.isFile());
final String[] args2 = new String[] {
testBelFile.getAbsolutePath(),
"-t", "BEL",
"-o", testXbelFile.getAbsolutePath(), "--no-preserve",
"--verbose", "--debug"
};
System.out.println("Running DocumentConverter with arguments " + Arrays.toString(args2));
DocumentConverter.main(args2);
controlInputStream = new FileInputStream(controlXbelFile);
testInputStream = new FileInputStream(testXbelFile);
final Document control = transformControl(xpath, controlInputStream);
final Document test = transformTest(testInputStream);
final Diff diff = new Diff(control, test, null,
new XbelQualifier());
assertXMLEqual(msg, diff, true);
deleteTemp = true;
} finally {
if (controlInputStream != null) {
try {
controlInputStream.close();
} catch (IOException ex) {}
}
if (testInputStream != null) {
try {
testInputStream.close();
} catch (IOException ex) {}
}
if (testXbelFile != null && deleteTemp) {
testXbelFile.delete();
}
if (testBelFile != null && deleteTemp) {
testBelFile.delete();
}
}
}
/*
* The next two methods apply transformations of the control
* (one of docs/xbel/corpus/*.xbel) and test (output of Document Converter)
* documents, respectively. This is necessary to put both documents in a common form
* so that they can be compared more easily.
*/
private static Document transformControl(final XPathMemo xpath, final InputStream is)
throws IOException, SAXException, TransformerException, XPathExpressionException {
final Document d = XMLUnit.buildControlDocument(new InputSource(is));
mergeAnnotationGroupsIntoLeaves(xpath, d);
return new Transform(d, nornalizeXbelXslt).getResultDocument();
}
private static Document transformTest(final InputStream is)
throws IOException, SAXException, TransformerException {
final Document d = XMLUnit.buildTestDocument(new InputSource(is));
return new Transform(d, nornalizeXbelXslt).getResultDocument();
}
/*
* For each bel:statementGroup element, merge its bel:annotationGroup children into the
* bel:annotationGroup children of any descendant bel:statement element.
*/
private static void mergeAnnotationGroupsIntoLeaves(final XPathMemo xpath, final Document d)
throws XPathExpressionException {
xpath.getXPath().setNamespaceContext(new BelNamespaceContext());
final XPathExpression statementExp = xpath.compile(
"/bel:document//bel:statementGroup/bel:statement");
final XPathExpression annotationGroupExp = xpath.compile("bel:annotationGroup");
for (Node statement : new NodeListIterable((NodeList) statementExp.evaluate(d, NODESET))) {
final NodeList annotationGroups = (NodeList) annotationGroupExp.evaluate(statement, NODESET);
if (annotationGroups.getLength() == 0) {
final Element annotationGroup = d.createElementNS(BEL_NAMESPACE_URI, "annotationGroup");
statement.appendChild(annotationGroup);
mergeAncestorAnnotationGroups(xpath, (Element) statement, annotationGroup);
} else {
for (Node annotationGroup : new NodeListIterable(annotationGroups)) {
mergeAncestorAnnotationGroups(
xpath, (Element) statement, (Element) annotationGroup);
}
}
}
}
private static final void mergeAncestorAnnotationGroups(
final XPathMemo xpath,
final Element statement,
final Element annotationGroup) throws XPathExpressionException {
final XPathExpression annotationGroupExp = xpath.compile("bel:annotationGroup");
Node node = statement;
while ((node = node.getParentNode()) != null &&
! (node.getNodeName().equals("document") &&
node.getNamespaceURI().equals(BEL_NAMESPACE_URI))) {
for (Node el : new NodeListIterable((NodeList) annotationGroupExp.evaluate(node, NODESET))) {
mergeInto(el, annotationGroup);
}
}
}
private static final void mergeInto(final Node from, final Node to) {
for (Node node : new NodeListIterable(from.getChildNodes())) {
to.appendChild(node.cloneNode(true));
}
}
/*
* Declares to the XSLT transform that it should use "bel" as
* the namespace prefix for the BEL namespace URI.
*/
private static class BelNamespaceContext implements NamespaceContext {
@Override
public String getNamespaceURI(String prefix) {
if ("bel".equals(prefix)) {
return BEL_NAMESPACE_URI;
}
return null;
}
@Override
public String getPrefix(String namespace) {
if (BEL_NAMESPACE_URI.equals(namespace)) {
return "bel";
}
return null;
}
@SuppressWarnings("rawtypes")
@Override
public Iterator getPrefixes(String namespace) {
return null;
}
}
/*
* A wrapper around NodeList that allows it to be iterated over by a <code>foreach</code>
* loop.
*/
private static final class NodeListIterable implements Iterable<Node>, Iterator<Node> {
private final NodeList nodeList;
private final int nodeListSize;
private int index;
public NodeListIterable(final NodeList nodeList) {
this.nodeList = nodeList;
this.nodeListSize = nodeList.getLength();
this.index = 0;
}
@Override
public Iterator<Node> iterator() {
return this;
}
@Override
public boolean hasNext() {
return index < nodeListSize;
}
@Override
public Node next() {
return nodeList.item(index++);
}
@Override
public void remove() {}
}
/*
* A wrapper around <code>XPath</code> with a cache of compiled <code>XPathExpression</code>s.
*/
private static final class XPathMemo {
private XPath xpath;
private Map<String, XPathExpression> memo;
public XPathMemo(final XPath xpath) {
this.xpath = xpath;
this.memo = new HashMap<String, XPathExpression>();
}
public XPathExpression compile(final String expression) throws XPathExpressionException {
if (memo.containsKey(expression)) {
return memo.get(expression);
}
final XPathExpression exp = xpath.compile(expression);
memo.put(expression, exp);
return exp;
}
public XPath getXPath() {
return xpath;
}
}
/*
* An xmlunit ElementQualifier used to determine whether XML nodes are comparable.
*/
private static class XbelQualifier implements ElementQualifier {
private static final ElementNameAndAttributeQualifier NAME_ATTR_QUAL
= new ElementNameAndAttributeQualifier();
@Override
public boolean qualifyForComparison(final Element control, final Element test) {
// qualifyForComparison() checks that the control and test elements have
// the same structure (the same element names and attributes, the same
// number of child nodes, and the corresponding child nodes have the same
// structure).
if (! NAME_ATTR_QUAL.qualifyForComparison(control, test)) {
return false;
} else if (! (control.hasChildNodes() && test.hasChildNodes())) {
return (control.hasChildNodes() == test.hasChildNodes());
}
final Iterator<Node> controlChildren = new NodeListIterable(control.getChildNodes()),
testChildren = new NodeListIterable(test.getChildNodes());
// StringBuilders to store the accumulated content of text/cdata section nodes.
final StringBuilder controlTextBldr = new StringBuilder(),
testTextBldr = new StringBuilder();
// State used to control the loop:
boolean lastControlChildWasText = false, lastTestChildWasText = false;
boolean wantNextControlChild = true;
Node controlChild = null, testChild = null;
// Simultaneously loop through the children of the control and test elements.
// Check for anything that would prevent the control and test children from
// qualifying for comparison. The check for child element nodes is to run
// qualifyForComparison() recursively on the children.
for (;;) {
if (wantNextControlChild) {
controlChild = controlChildren.next();
if (controlChild == null) {
wantNextControlChild = false;
} else {
final short nodeType = controlChild.getNodeType();
if (nodeType == Node.TEXT_NODE || nodeType == Node.CDATA_SECTION_NODE) {
// Collect the data in consecutive text and cdata sections
final String value = ((CharacterData) controlChild).getData();
if (value != null) {
controlTextBldr.append(value.trim());
}
lastControlChildWasText = true;
} else if (nodeType == Node.ELEMENT_NODE) {
wantNextControlChild = false;
}
}
} else {
testChild = testChildren.next();
if (testChild == null) {
if (controlChild != null) {
return false;
} else if (lastControlChildWasText != lastTestChildWasText) {
return false;
} else if (lastTestChildWasText &&
! controlTextBldr.toString().equals(testTextBldr.toString())) {
return false;
}
// If there are no more control or test child nodes then
// control and test qualify for comparison, so return true.
return true;
}
final short nodeType = testChild.getNodeType();
if (nodeType == Node.TEXT_NODE || nodeType == Node.CDATA_SECTION_NODE) {
// Collect the data in consecutive text and cdata sections
final String value = ((CharacterData) testChild).getData();
if (value != null) {
testTextBldr.append(value.trim());
}
lastTestChildWasText = true;
} else if (nodeType == Node.ELEMENT_NODE) {
if (lastControlChildWasText != lastTestChildWasText) {
return false;
} else if (lastTestChildWasText) {
if (! controlTextBldr.toString().equals(testTextBldr.toString())) {
return false;
}
controlTextBldr.delete(0, controlTextBldr.length());
testTextBldr.delete(0, testTextBldr.length());
lastTestChildWasText = false;
}
if (! qualifyForComparison((Element) controlChild, (Element) testChild)) {
return false;
}
wantNextControlChild = true;
lastControlChildWasText = false;
}
}
}
}
}
}