Skip to content

Commit

Permalink
Support pos attribute for add #5
Browse files Browse the repository at this point in the history
Roundtrip test verified with XMLSpy compare
  • Loading branch information
donmendelson committed Feb 17, 2021
1 parent 5fb9610 commit 4c5e7bf
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 63 deletions.
6 changes: 3 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>11</java.version>
<junit.version>5.5.1</junit.version>
<log4j.version>2.13.3</log4j.version>
<saxon.version>9.9.1-6</saxon.version>
<junit.version>5.7.1</junit.version>
<log4j.version>2.14.0</log4j.version>
<saxon.version>10.3</saxon.version>
<ignoreSigningInformation>true</ignoreSigningInformation>
</properties>

Expand Down
6 changes: 5 additions & 1 deletion src/main/java/io/fixprotocol/xml/PatchOpsListener.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicBoolean;

Expand All @@ -37,6 +36,8 @@
import org.w3c.dom.Node;
import org.w3c.dom.Text;

import static io.fixprotocol.xml.XmlDiffListener.Event.Pos.*;

/**
* Writes XML diffs as patch operations specified by IETF RFC 5261
*
Expand Down Expand Up @@ -92,6 +93,9 @@ public void accept(Event t) {
} else if (t.getValue() instanceof Element) {
// add element
addElement.setAttribute("sel", t.getXpath());
if (t.getPos() != append) {
addElement.setAttribute("pos", t.getPos().toString());
}
// will import child text node if it exists (deep copy)
Element newValue = (Element) document.importNode(t.getValue(), true);
addElement.appendChild(newValue);
Expand Down
60 changes: 41 additions & 19 deletions src/main/java/io/fixprotocol/xml/XmlDiff.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
*/
package io.fixprotocol.xml;

import static io.fixprotocol.xml.XmlDiffListener.Event.Difference.ADD;
import static io.fixprotocol.xml.XmlDiffListener.Event.Difference.REMOVE;
import static io.fixprotocol.xml.XmlDiffListener.Event.Difference.REPLACE;
import static io.fixprotocol.xml.XmlDiffListener.Event.Difference.*;
import static io.fixprotocol.xml.XmlDiffListener.Event.Pos.*;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
Expand Down Expand Up @@ -47,6 +47,7 @@
import org.xml.sax.SAXException;
import io.fixprotocol.xml.XmlDiffListener.Event;
import io.fixprotocol.xml.XmlDiffListener.Event.Difference;
import io.fixprotocol.xml.XmlDiffListener.Event.Pos;

/**
* Utility to report differences between two XML files
Expand Down Expand Up @@ -200,6 +201,12 @@ public void diff(InputStream is1, String xpathString1, InputStream is2, String x
}
}

/**
* Compare XML elements with regard to order of children.
*
* @param areElementsOrdered if {@code true} order is significant for comparison and add instructions are to insert in
* a specific position. Otherwise, order is insignificant for comparison, and adds are appended to existing children.
*/
public void setAreElementsOrdered(boolean areElementsOrdered) {
this.areElementsOrdered = areElementsOrdered;
}
Expand All @@ -213,14 +220,23 @@ public void setAreElementsOrdered(boolean areElementsOrdered) {
public void setListener(XmlDiffListener listener) {
this.listener = listener;
}

private void addElement(Element element)

// append or prepend
private void appendElement(Element elementToAdd, Pos pos)
throws DOMException, UnsupportedEncodingException, TransformerException {
final String xpath = XpathUtil.getFullXPath(element.getParentNode());
String xpath= XpathUtil.getFullXPath(elementToAdd.getParentNode());
// Copies an element with its attributes (deep copy)
Node elementCopy = elementToAdd.cloneNode(true);
listener.accept(Event.add(xpath, elementCopy, pos));
}

// insert before or after
private void insertElement(Element elementToAdd, Node location, Pos pos)
throws DOMException, UnsupportedEncodingException, TransformerException {
String xpath= XpathUtil.getFullXPath(location);
// Copies an element with its attributes (deep copy)
Node elementCopy = element.cloneNode(true);
listener.accept(new Event(ADD, xpath, elementCopy));
Node elementCopy = elementToAdd.cloneNode(true);
listener.accept(Event.add(xpath, elementCopy, pos));
}

private boolean diffAttributes(NamedNodeMap attributes1, NamedNodeMap attributes2) {
Expand Down Expand Up @@ -256,13 +272,13 @@ private boolean diffAttributes(NamedNodeMap attributes1, NamedNodeMap attributes
switch (difference) {
case ADD:
listener.accept(
new Event(ADD, XpathUtil.getFullXPath(attributesArray2.get(index2).getOwnerElement()),
attributesArray2.get(index2)));
Event.add(XpathUtil.getFullXPath(attributesArray2.get(index2).getOwnerElement()),
attributesArray2.get(index2), append));
index2 = Math.min(index2 + 1, attributesArray2.size());
isEqual = false;
break;
case REPLACE:
listener.accept(new Event(REPLACE, XpathUtil.getFullXPath(attributesArray1.get(index1)),
listener.accept(Event.replace(XpathUtil.getFullXPath(attributesArray1.get(index1)),
attributesArray2.get(index2), attributesArray1.get(index1)));
index1 = Math.min(index1 + 1, attributesArray1.size());
index2 = Math.min(index2 + 1, attributesArray2.size());
Expand All @@ -273,7 +289,7 @@ private boolean diffAttributes(NamedNodeMap attributes1, NamedNodeMap attributes
index2 = Math.min(index2 + 1, attributesArray2.size());
break;
case REMOVE:
listener.accept(new Event(REMOVE, XpathUtil.getFullXPath(attributesArray1.get(index1))));
listener.accept(Event.remove(XpathUtil.getFullXPath(attributesArray1.get(index1))));
index1 = Math.min(index1 + 1, attributesArray1.size());
isEqual = false;
break;
Expand Down Expand Up @@ -301,6 +317,7 @@ private boolean diffChildElements(Element element1, Element element2)
int index2 = 0;
boolean isEqual = true;
while (index1 < elementsArray1.size() || index2 < elementsArray2.size()) {

Difference difference = Difference.EQUAL;
if (index1 == elementsArray1.size()) {
difference = ADD;
Expand All @@ -320,12 +337,16 @@ private boolean diffChildElements(Element element1, Element element2)
}
switch (difference) {
case ADD:
addElement(elementsArray2.get(index2));
if (areElementsOrdered && index1 < elementsArray1.size()) {
insertElement(elementsArray2.get(index2), elementsArray1.get(index1), before);
} else {
appendElement(elementsArray2.get(index2), append);
}
index2 = Math.min(index2 + 1, elementsArray2.size());
isEqual = false;
break;
case REPLACE:
listener.accept(new Event(REPLACE, XpathUtil.getFullXPath(elementsArray1.get(index1)),
listener.accept(Event.replace(XpathUtil.getFullXPath(elementsArray1.get(index1)),
elementsArray2.get(index2), elementsArray1.get(index1)));
index1 = Math.min(index1 + 1, elementsArray1.size());
index2 = Math.min(index2 + 1, elementsArray2.size());
Expand All @@ -336,8 +357,7 @@ private boolean diffChildElements(Element element1, Element element2)
index2 = Math.min(index2 + 1, elementsArray2.size());
break;
case REMOVE:
listener.accept(new Event(REMOVE, XpathUtil.getFullXPath(elementsArray1.get(index1)),
elementsArray1.get(index1)));
listener.accept(Event.remove(XpathUtil.getFullXPath(elementsArray1.get(index1))));
index1 = Math.min(index1 + 1, elementsArray1.size());
isEqual = false;
break;
Expand Down Expand Up @@ -366,18 +386,20 @@ private boolean diffText(Element element1, Element element2) {

if (child1 != null && Node.TEXT_NODE == child1.getNodeType()) {
if (child2 == null || Node.TEXT_NODE != child2.getNodeType()) {
listener.accept(new Event(REMOVE, XpathUtil.getFullXPath(element1), child1));

// remove
listener.accept(Event.remove(XpathUtil.getFullXPath(child1)));
} else {
int valueCompare = child1.getNodeValue().trim().compareTo(child2.getNodeValue().trim());

if (valueCompare != 0) {
listener.accept(new Event(REPLACE, XpathUtil.getFullXPath(element1), child2, child1));
listener.accept(Event.replace(XpathUtil.getFullXPath(element1), child2, child1));
} else {
return true;
}
}
} else if (child2 != null && Node.TEXT_NODE == child2.getNodeType()) {
listener.accept(new Event(ADD, XpathUtil.getFullXPath(element2), child2));
listener.accept(Event.add(XpathUtil.getFullXPath(element2), child2, append));
}
return false;
}
Expand Down
58 changes: 36 additions & 22 deletions src/main/java/io/fixprotocol/xml/XmlDiffListener.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,48 +39,62 @@ enum Difference {
ADD, EQUAL, REMOVE, REPLACE
}

private final Difference difference;
private final Node oldValue;
private final Node value;
private final String xpath;

/**
* Constructor for REMOVE event
*
* @param difference type of event; must not be null
* @param xpath node to remove; must not be null
* Position of ADD
*/
Event(Difference difference, String xpath) {
this(difference, xpath, null, null);
enum Pos {
after, append, before, prepend
}

/**
* Constructor for ADD event
* Return an Event to ADD
*
* @param difference type of event; must not be null
* @param xpath parent of the added node; must not be null
* @param value node value of element or attribute; must not be null
*/
Event(Difference difference, String xpath, Node value) {
this(difference, xpath, value, null);
Objects.requireNonNull(value, "Node to add missing");
static Event add(String xpath, Node value, Pos pos) {
return new Event(Difference.ADD, xpath, Objects.requireNonNull(value, "Node to add missing"),
null, pos);
}

/**
* Constructor for REPLACE event
* Return an Event to REMOVE
*
* @param xpath node to remove; must not be null
*/
static Event remove(String xpath) {
return new Event(Difference.REMOVE, xpath, null, null, null);
}

/**
* Return an Event to REPLACE
*
* @param difference type of event; must not be null
* @param xpath node target of change; must not be null
* @param value node new value of element or attribute
* @param oldValue previous node value
*/
Event(Difference difference, String xpath, Node value, Node oldValue) {
Objects.requireNonNull(difference, "Difference type missing");
Objects.requireNonNull(xpath, "Xpath missing");
this.difference = difference;
this.xpath = xpath;
static Event replace(String xpath, Node value, Node oldValue) {
return new Event(Difference.REPLACE, xpath, value,
Objects.requireNonNull(oldValue, "Old node missing"), null);
}

private final Difference difference;
private final Node oldValue;
private final Pos pos;
private final Node value;
private final String xpath;

private Event(Difference difference, String xpath, Node value, Node oldValue, Pos pos) {
this.difference = Objects.requireNonNull(difference, "Difference type missing");
this.xpath = Objects.requireNonNull(xpath, "Xpath missing");
this.value = value;
this.oldValue = oldValue;
this.pos = pos;
}

public Pos getPos() {
return pos;
}

Difference getDifference() {
Expand Down
32 changes: 27 additions & 5 deletions src/main/java/io/fixprotocol/xml/XmlMerge.java
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,11 @@ private void add(Document doc, XPath xpathEvaluator, Element patchOpElement)
throws XPathExpressionException {
String xpathExpression = patchOpElement.getAttribute("sel");
String attribute = patchOpElement.getAttribute("type");
String pos = patchOpElement.getAttribute("pos");

final XPathExpression compiled = xpathEvaluator.compile(xpathExpression);
Node parent = (Node) compiled.evaluate(doc, XPathConstants.NODE);
if (parent == null) {
Node siteNode = (Node) compiled.evaluate(doc, XPathConstants.NODE);
if (siteNode == null) {
throw new XPathExpressionException(
"No target for Xpath expression in 'sel' for add; " + xpathExpression);
}
Expand All @@ -158,11 +159,11 @@ private void add(Document doc, XPath xpathEvaluator, Element patchOpElement)
for (int i = 0; i < children.getLength(); i++) {
Node child = children.item(i);
if (Node.TEXT_NODE == child.getNodeType()) {
value = patchOpElement.getNodeValue();
value = patchOpElement.getTextContent();
break;
}
}
((Element) parent).setAttribute(attribute.substring(1), value);
((Element) siteNode).setAttribute(attribute.substring(1), value);
} else {
Element value = null;
NodeList children = patchOpElement.getChildNodes();
Expand All @@ -174,7 +175,28 @@ private void add(Document doc, XPath xpathEvaluator, Element patchOpElement)
}
}
Node imported = doc.importNode(value, true);
parent.appendChild(imported);
switch (pos) {
case "prepend":
// siteNode is parent - make first child
siteNode.insertBefore(imported, siteNode.getFirstChild());
break;
case "before":
// insert as sibling before siteNode
siteNode.getParentNode().insertBefore(imported, siteNode);
break;
case "after":
// insert as sibling after siteNode
Node nextSibling = siteNode.getNextSibling();
if (nextSibling != null) {
siteNode.getParentNode().insertBefore(imported, nextSibling);
} else {
siteNode.getParentNode().appendChild(imported);
}
break;
default:
// siteNode is parent - make last child
siteNode.appendChild(imported);
}
}
}

Expand Down
Loading

0 comments on commit 4c5e7bf

Please sign in to comment.