diff --git a/src/main/java/de/danielbechler/diff/BeanDiffer.java b/src/main/java/de/danielbechler/diff/BeanDiffer.java index e490d517..dad7f71c 100644 --- a/src/main/java/de/danielbechler/diff/BeanDiffer.java +++ b/src/main/java/de/danielbechler/diff/BeanDiffer.java @@ -16,6 +16,9 @@ package de.danielbechler.diff; +import java.util.HashSet; +import java.util.Set; + import de.danielbechler.diff.accessor.*; import de.danielbechler.diff.introspect.*; import de.danielbechler.diff.node.*; @@ -29,6 +32,7 @@ final class BeanDiffer extends AbstractDiffer { private Introspector introspector = new StandardIntrospector(); + private Set visitedInstances = new HashSet(); BeanDiffer() { @@ -64,6 +68,8 @@ else if (instances.getType() == null) } else { + if(checkAlreadyVisited(instances)) + return node; return compareBean(parentNode, instances); } return node; @@ -143,4 +149,18 @@ void setIntrospector(final Introspector introspector) Assert.notNull(introspector, "introspector"); this.introspector = introspector; } + + private boolean checkAlreadyVisited(Instances instances) + { + InstanceOutline outline = InstanceOutline.from(instances); + if(outline != null) { + if(visitedInstances.contains(outline)) { + // if line below is commented in test case bidirectionalGraphStackOverflow creates a stack overflow + // visitedInstances.remove(outline); + return true; + } + visitedInstances.add(outline); + } + return false; + } } diff --git a/src/main/java/de/danielbechler/diff/InstanceOutline.java b/src/main/java/de/danielbechler/diff/InstanceOutline.java new file mode 100644 index 00000000..4e4915e4 --- /dev/null +++ b/src/main/java/de/danielbechler/diff/InstanceOutline.java @@ -0,0 +1,69 @@ +package de.danielbechler.diff; + +public class InstanceOutline { + + private Object comparisonObject; + private Object working; + private Object base; + + public InstanceOutline() { + super(); + } + + public InstanceOutline(Object comparisonObject, Object working, Object base) { + super(); + this.comparisonObject = comparisonObject; + this.working = working; + this.base = base; + } + + + public static InstanceOutline from(Instances instances) { + if(instances.getWorking() == null) + return null; + if(instances.getBase() == null) + return null; + Object comparisonObject = instances.getComparisonObject(); + if(comparisonObject == null) + return null; + return new InstanceOutline(comparisonObject, instances.getWorking(), instances.getBase()); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((base == null) ? 0 : base.hashCode()); + result = prime * result + ((comparisonObject == null) ? 0 : comparisonObject.hashCode()); + result = prime * result + ((working == null) ? 0 : working.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + InstanceOutline other = (InstanceOutline) obj; + if (base == null) { + if (other.base != null) + return false; + } else if (!base.equals(other.base)) + return false; + if (comparisonObject == null) { + if (other.comparisonObject != null) + return false; + } else if (!comparisonObject.equals(other.comparisonObject)) + return false; + if (working == null) { + if (other.working != null) + return false; + } else if (!working.equals(other.working)) + return false; + return true; + } + +} diff --git a/src/main/java/de/danielbechler/diff/Instances.java b/src/main/java/de/danielbechler/diff/Instances.java index 221459ef..cafdf839 100644 --- a/src/main/java/de/danielbechler/diff/Instances.java +++ b/src/main/java/de/danielbechler/diff/Instances.java @@ -169,4 +169,11 @@ public PropertyPath getPropertyPath(final Node parentNode) .withElement(sourceAccessor.getPathElement()) .build(); } + + public Object getComparisonObject() + { + if(sourceAccessor == null) + return null; + return sourceAccessor.getComparisonObject(); + } } diff --git a/src/main/java/de/danielbechler/diff/accessor/AbstractAccessor.java b/src/main/java/de/danielbechler/diff/accessor/AbstractAccessor.java index d4a17422..1f4dd703 100644 --- a/src/main/java/de/danielbechler/diff/accessor/AbstractAccessor.java +++ b/src/main/java/de/danielbechler/diff/accessor/AbstractAccessor.java @@ -54,4 +54,9 @@ public void setIgnored(final boolean ignored) { this.ignored = ignored; } + + public Object getComparisonObject() { + // answer the default, redefine in subclass as appropriate + return null; + } } diff --git a/src/main/java/de/danielbechler/diff/accessor/Accessor.java b/src/main/java/de/danielbechler/diff/accessor/Accessor.java index b5807692..e820d5d7 100644 --- a/src/main/java/de/danielbechler/diff/accessor/Accessor.java +++ b/src/main/java/de/danielbechler/diff/accessor/Accessor.java @@ -24,4 +24,6 @@ public interface Accessor extends PropertyDescriptor void set(Object target, Object value); void unset(Object target); + + Object getComparisonObject(); } diff --git a/src/main/java/de/danielbechler/diff/accessor/CollectionItemAccessor.java b/src/main/java/de/danielbechler/diff/accessor/CollectionItemAccessor.java index 10c1ed0e..78bb8dc1 100644 --- a/src/main/java/de/danielbechler/diff/accessor/CollectionItemAccessor.java +++ b/src/main/java/de/danielbechler/diff/accessor/CollectionItemAccessor.java @@ -95,4 +95,9 @@ public void unset(final Object target) targetCollection.remove(referenceItem); } } + + @Override + public Object getComparisonObject() { + return referenceItem; + } } diff --git a/src/main/java/de/danielbechler/diff/accessor/MapEntryAccessor.java b/src/main/java/de/danielbechler/diff/accessor/MapEntryAccessor.java index c660e01a..76f6cd8e 100644 --- a/src/main/java/de/danielbechler/diff/accessor/MapEntryAccessor.java +++ b/src/main/java/de/danielbechler/diff/accessor/MapEntryAccessor.java @@ -89,4 +89,9 @@ public void unset(final Object target) targetMap.remove(getReferenceKey()); } } + + @Override + public Object getComparisonObject() { + return getReferenceKey(); + } } diff --git a/src/main/java/de/danielbechler/diff/accessor/PropertyAccessor.java b/src/main/java/de/danielbechler/diff/accessor/PropertyAccessor.java index 95bbba80..d87569ac 100644 --- a/src/main/java/de/danielbechler/diff/accessor/PropertyAccessor.java +++ b/src/main/java/de/danielbechler/diff/accessor/PropertyAccessor.java @@ -19,6 +19,7 @@ import de.danielbechler.diff.accessor.exception.*; import de.danielbechler.diff.path.*; import de.danielbechler.util.*; + import org.slf4j.*; import java.lang.reflect.*; @@ -189,4 +190,13 @@ public Element getPathElement() { return new NamedPropertyElement(this.propertyName); } + + @Override + public Object getComparisonObject() { + if(Classes.isSimpleType(getType())) { + // ignore, because no comparison on the level of user-defined objects + return null; + } + return getPropertyName(); + } } diff --git a/src/main/java/de/danielbechler/diff/node/DefaultNode.java b/src/main/java/de/danielbechler/diff/node/DefaultNode.java index b390939c..6077bccd 100644 --- a/src/main/java/de/danielbechler/diff/node/DefaultNode.java +++ b/src/main/java/de/danielbechler/diff/node/DefaultNode.java @@ -387,4 +387,10 @@ else if (getChildren().size() > 1) sb.append(" }"); return sb.toString(); } + + @Override + public Object getComparisonObject() { + // not needed here, but has to be implemented, because of the Accessor interface + throw new UnsupportedOperationException(); + } } diff --git a/src/test/java/de/danielbechler/diff/graph/GraphTest.java b/src/test/java/de/danielbechler/diff/graph/GraphTest.java new file mode 100644 index 00000000..fd9d5214 --- /dev/null +++ b/src/test/java/de/danielbechler/diff/graph/GraphTest.java @@ -0,0 +1,364 @@ +package de.danielbechler.diff.graph; + +import org.junit.Test; + + + +import de.danielbechler.diff.ObjectDiffer; +import de.danielbechler.diff.ObjectDifferFactory; +import de.danielbechler.diff.node.Node; +import de.danielbechler.diff.visitor.PrintingVisitor; + +public class GraphTest +{ + + @Test + public void basicNode() + { + // works correctly + MyNode base = new MyNode(); + MyNode a = new MyNode("a"); + base.setDirectReference(a); + + MyNode modified = new MyNode(); + MyNode modifiedA = new MyNode("ax"); + modified.setDirectReference(modifiedA); + + ObjectDiffer objectDiffer = ObjectDifferFactory.getInstance(); + Node root = objectDiffer.compare(modified, base); + root.visit(new PrintingVisitor(modified, base)); + + /* + * expected output: + * + * Property at path '.directReference.value' has changed from [ a ] to [ ax ] + */ + } + + @Test + public void basicNodeWithDirectReferences() + { + // works correctly + MyNode base = new MyNode(); + MyNode a = new MyNode("a"); + base.setDirectReference(a); + + MyNode modified = new MyNode(); + MyNode modifiedA = new MyNode("ax"); + modified.setDirectReference(modifiedA); + + ObjectDiffer objectDiffer = ObjectDifferFactory.getInstance(); + Node root = objectDiffer.compare(modified, base); + root.visit(new PrintingVisitor(modified, base)); + + /* + * expected output: + * + * Property at path '.directReference.value' has changed from [ a ] to [ ax ] + */ + } + + @Test + public void basicBidirectionalWithChildren() + { + // works correctly + + MyNode base = new MyNode(1); + MyNode a = new MyNode(2, "a"); + base.getChildren().add(a); + MyNode b = new MyNode(3, "b"); + base.getChildren().add(b); + a.setDirectReference(b); + b.setDirectReference(a); + + MyNode modified = new MyNode(1); + MyNode modifiedA = new MyNode(2, "ax"); + modified.getChildren().add(modifiedA); + MyNode modifiedB = new MyNode(3, "by"); + modified.getChildren().add(modifiedB); + modifiedA.setDirectReference(modifiedB); + modifiedB.setDirectReference(modifiedA); + + ObjectDiffer objectDiffer = ObjectDifferFactory.getInstance(); + Node root = objectDiffer.compare(modified, base); + root.visit(new PrintingVisitor(modified, base)); + + /* + * expected output: + * + * Property at path '.children.[com.bisnode.platform.javaobjectdiff.model.MyNode@21].directReference.directReference.value' has changed from [ a ] to [ ax ] + * Property at path '.children.[com.bisnode.platform.javaobjectdiff.model.MyNode@21].directReference.value' has changed from [ b ] to [ by ] + */ + } + + @Test + public void basicBidirectionalWithChildrenAndMaps() + { + // works correctly + + MyNode base = new MyNode(1); + MyNode a = new MyNode(2, "a"); + base.getMap().put("a", "a"); + MyNode b = new MyNode(3, "b"); + base.getChildren().add(b); + a.setDirectReference(b); + b.setDirectReference(a); + a.getMap().put("node-b", b); + a.getMap().put("node-x", b); + b.getMap().put("node-a", a); + + MyNode modified = new MyNode(1); + MyNode modifiedA = new MyNode(2, "ax"); + modified.getMap().put("a", "az"); + MyNode modifiedB = new MyNode(3, "by"); + modified.getChildren().add(modifiedB); + modifiedA.setDirectReference(modifiedB); + modifiedB.setDirectReference(modifiedA); + modifiedA.getMap().put("node-b", modifiedB); + modifiedB.getMap().put("node-a", modifiedA); + + ObjectDiffer objectDiffer = ObjectDifferFactory.getInstance(); + Node root = objectDiffer.compare(modified, base); + root.visit(new PrintingVisitor(modified, base)); + + /* + * expected output: + * + * Property at path '.children.[com.bisnode.platform.javaobjectdiff.model.MyNode@22[id=3]].directReference.directReference.map.{node-a}.map.{node-x}' with value [ com.bisnode.platform.javaobjectdiff.model.MyNode@22[id=3] ] has been removed + * Property at path '.children.[com.bisnode.platform.javaobjectdiff.model.MyNode@22[id=3]].directReference.directReference.map.{node-a}.map.{node-b}.value' has changed from [ b ] to [ by ] + * Property at path '.children.[com.bisnode.platform.javaobjectdiff.model.MyNode@22[id=3]].directReference.directReference.map.{node-a}.value' has changed from [ a ] to [ ax ] + * Property at path '.children.[com.bisnode.platform.javaobjectdiff.model.MyNode@22[id=3]].directReference.directReference.value' has changed from [ b ] to [ by ] + * Property at path '.children.[com.bisnode.platform.javaobjectdiff.model.MyNode@22[id=3]].directReference.map.{node-x}' with value [ com.bisnode.platform.javaobjectdiff.model.MyNode@22[id=3] ] has been removed + * Property at path '.children.[com.bisnode.platform.javaobjectdiff.model.MyNode@22[id=3]].directReference.value' has changed from [ a ] to [ ax ] + * Property at path '.children.[com.bisnode.platform.javaobjectdiff.model.MyNode@22[id=3]].value' has changed from [ b ] to [ by ] + * Property at path '.map.{a}' has changed from [ a ] to [ az ] + */ + } + + @Test + public void basicBidirectionalWithoutChildren() + { + // works correctly + + MyNode a = new MyNode(1, "a"); + MyNode b = new MyNode(2, "b"); + a.setDirectReference(b); + b.setDirectReference(a); + + MyNode modifiedA = new MyNode(1, "ax"); + MyNode modifiedB = new MyNode(2, "by"); + modifiedA.setDirectReference(modifiedB); + modifiedB.setDirectReference(modifiedA); + + ObjectDiffer objectDiffer = ObjectDifferFactory.getInstance(); + Node root = objectDiffer.compare(modifiedA, a); + root.visit(new PrintingVisitor(modifiedA, a)); + + /* + * expected output: + * + * Property at path '.directReference.directReference.value' has changed from [ a ] to [ ax ] + * Property at path '.directReference.value' has changed from [ b ] to [ by ] + */ + } + + @Test + public void basicNodeWithDirectReferences2() + { + // works correctly + MyNode base = new MyNode("base"); + MyNode a = new MyNode("a"); + base.setDirectReference(a); + + MyNode modified = new MyNode("modified"); + MyNode modifiedA = new MyNode("ax"); + modified.setDirectReference(modifiedA); + + ObjectDiffer objectDiffer = ObjectDifferFactory.getInstance(); + Node root = objectDiffer.compare(modified, base); + root.visit(new PrintingVisitor(modified, base)); + + /* + * expected output: + * + * Property at path '.directReference.value' has changed from [ a ] to [ ax ] + * Property at path '.value' has changed from [ base ] to [ modified ] + */ + } + + @Test + public void basicBidirectionalNodeWithChildNodes() + { + // does not detect any changes since no primary key defined for each node + MyNode base = new MyNode(); + MyNode a = new MyNode("a"); + MyNode b = new MyNode("b"); + base.getChildren().add(a); + base.getChildren().add(b); + + MyNode modified = new MyNode(); + MyNode modifiedA = new MyNode("a"); + MyNode modifiedB = new MyNode("bx"); + modified.getChildren().add(modifiedA); + modified.getChildren().add(modifiedB); + + ObjectDiffer objectDiffer = ObjectDifferFactory.getInstance(); + Node root = objectDiffer.compare(modified, base); + root.visit(new PrintingVisitor(modified, base)); + } + + @Test + public void basicBidirectionalNodeWithChildNodesWithIds() + { + MyNode base = new MyNode(1); + MyNode a = new MyNode(2, "a"); + MyNode b = new MyNode(3, "b"); + base.getChildren().add(a); + base.getChildren().add(b); + + MyNode modified = new MyNode(1); + MyNode modifiedA = new MyNode(2, "ax"); + MyNode modifiedB = new MyNode(3, "by"); + modified.getChildren().add(modifiedA); + modified.getChildren().add(modifiedB); + + ObjectDiffer objectDiffer = ObjectDifferFactory.getInstance(); + Node root = objectDiffer.compare(modified, base); + root.visit(new PrintingVisitor(modified, base)); + + /* + * expected output: + * + * Property at path '.children.[com.bisnode.platform.javaobjectdiff.model.MyNode@21].value' has changed from [ a ] to [ ax ] + * Property at path '.children.[com.bisnode.platform.javaobjectdiff.model.MyNode@22].value' has changed from [ b ] to [ by ] + */ + } + + @Test + public void simpleGraph() + { + // works correctly + + MyNode base = new MyNode(1); + MyNode a = new MyNode(2, base, "a"); + + MyNode modified = new MyNode(1); + MyNode modifiedA = new MyNode(2, modified, "a"); + + ObjectDiffer objectDiffer = ObjectDifferFactory.getInstance(); + Node root = objectDiffer.compare(modified, base); + root.visit(new PrintingVisitor(modified, base)); + + /* + * expected output: nothing changed + */ + + base = new MyNode(1); + a = new MyNode(2, base, "a"); + + modified = new MyNode(1); + modifiedA = new MyNode(2, modified, "ax"); + + objectDiffer = ObjectDifferFactory.getInstance(); + root = objectDiffer.compare(modified, base); + root.visit(new PrintingVisitor(modified, base)); + + /* + * expected output: + * + * Property at path '.children.[com.bisnode.platform.javaobjectdiff.model.MyNode@21].parent.children.[com.bisnode.platform.javaobjectdiff.model.MyNode@21].value' has changed from [ a ] to [ ax ] + */ + } + + @Test + public void simpleGraphExtended() + { + // works correctly + + MyNode base = new MyNode(1); + + MyNode a = new MyNode(2, base, "a"); + MyNode b = new MyNode(3, base, "b"); + + MyNode modified = new MyNode(1); + + MyNode modifiedA = new MyNode(2, modified, "a"); + MyNode modifiedB = new MyNode(3, modified, "bx"); + + ObjectDiffer objectDiffer = ObjectDifferFactory.getInstance(); + Node root = objectDiffer.compare(modified, base); + root.visit(new PrintingVisitor(modified, base)); + + /* + * expected output: + * + * Property at path '.children.[com.bisnode.platform.javaobjectdiff.model.MyNode@21[id=2]].parent.children.[com.bisnode.platform.javaobjectdiff.model.MyNode@22[id=3]].value' has changed from [ b ] to [ bx ] + * Property at path '.children.[com.bisnode.platform.javaobjectdiff.model.MyNode@22[id=3]].value' has changed from [ b ] to [ bx ] + */ + } + + + + @Test + public void bidirectionalGraphStackOverflow() + { + // works correctly + + MyNode base = new MyNode(1); + + MyNode a = new MyNode(2, base, "a"); + MyNode b = new MyNode(3, base, "b"); + a.setDirectReference(b); + b.setDirectReference(a); + base.getChildren().add(a); + base.getChildren().add(b); + + MyNode aa = new MyNode(4, a, "aa"); + a.getChildren().add(aa); + + MyNode ba = new MyNode(5, b, "ba"); + b.getChildren().add(ba); + + aa.getChildren().add(ba); + ba.getChildren().add(aa); + + MyNode baa = new MyNode(6, ba, "baa"); + ba.getChildren().add(baa); + + + MyNode modified = new MyNode(1); + + MyNode modifiedA = new MyNode(2, modified, "a"); + MyNode modifiedB = new MyNode(3, modified, "b"); + modifiedA.setDirectReference(modifiedB); + modifiedB.setDirectReference(modifiedA); + modified.getChildren().add(modifiedA); + modified.getChildren().add(modifiedA); + + MyNode modifiedAA = new MyNode(4, modifiedA, "aa"); + modifiedA.getChildren().add(modifiedAA); + + MyNode modifiedBA = new MyNode(5, modifiedB, "ba-x"); + modifiedB.getChildren().add(modifiedBA); + + modifiedAA.getChildren().add(modifiedBA); + modifiedBA.getChildren().add(modifiedAA); + + MyNode modifieBAA = new MyNode(6, modifiedBA, "baa-y"); + modifiedBA.getChildren().add(modifieBAA); + + ObjectDiffer objectDiffer = ObjectDifferFactory.getInstance(); + + Node root = objectDiffer.compare(modified, base); + root.visit(new PrintingVisitor(modified, base)); + + /* + * expected output: + * + * Property at path '[com.bisnode.platform.javaobjectdiff.model.MyNode@25[id=6]].parent.value' has changed from [ ba ] to [ ba-x ] + * Property at path '[com.bisnode.platform.javaobjectdiff.model.MyNode@25[id=6]].value' has changed from [ baa ] to [ baa-y ] + * Property at path '[com.bisnode.platform.javaobjectdiff.model.MyNode@24[id=5]].value' has changed from [ ba ] to [ ba-x ] + */ + } + +} diff --git a/src/test/java/de/danielbechler/diff/graph/MyNode.java b/src/test/java/de/danielbechler/diff/graph/MyNode.java new file mode 100644 index 00000000..ec665a7c --- /dev/null +++ b/src/test/java/de/danielbechler/diff/graph/MyNode.java @@ -0,0 +1,120 @@ +package de.danielbechler.diff.graph; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + + +public class MyNode { + + private int id = -1; + private MyNode parent = null; + private List children = new ArrayList(); + private MyNode directReference = null; + private Map map = new HashMap(); + private String value = null; + + public MyNode() { + super(); + } + + public MyNode(MyNode parent) { + super(); + this.parent = parent; + this.parent.getChildren().add(this); + } + + public MyNode(int id, MyNode parent, String value) { + super(); + this.id = id; + this.parent = parent; + this.parent.getChildren().add(this); + this.value = value; + } + + public MyNode(String value) { + super(); + this.value = value; + } + + public MyNode(int id) { + super(); + this.id = id; + } + + public MyNode(int id, String value) { + super(); + this.id = id; + this.value = value; + } + + public MyNode getParent() { + return parent; + } + + public void setParent(MyNode parent) { + this.parent = parent; + } + + public MyNode getDirectReference() { + return directReference; + } + + public void setDirectReference(MyNode directReference) { + this.directReference = directReference; + } + + public List getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + MyNode other = (MyNode) obj; + if (id != other.id) + return false; + return true; + } + + @Override + public String toString() { + return super.toString() + "[id=" + id + "]"; + } + + public Map getMap() { + return map; + } + + public void setMap(Map map) { + this.map = map; + } + + +}