Skip to content

Commit 17bed22

Browse files
committed
Fixed #15 #16 (Circular References)
1 parent a81758e commit 17bed22

25 files changed

+1143
-630
lines changed

src/main/java/de/danielbechler/diff/AbstractDiffer.java

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,19 @@
1616

1717
package de.danielbechler.diff;
1818

19+
import de.danielbechler.diff.node.*;
1920
import de.danielbechler.util.*;
2021

2122
/** @author Daniel Bechler */
22-
abstract class AbstractDiffer implements Differ, Configurable
23+
abstract class AbstractDiffer<T extends Node> implements Differ<T>, Configurable
2324
{
25+
static final ThreadLocal<CircularReferenceDetector> CIRCULAR_REFERENCE_DETECTOR_THREAD_LOCAL;
26+
27+
static
28+
{
29+
CIRCULAR_REFERENCE_DETECTOR_THREAD_LOCAL = new CircularReferenceDetectorThreadLocal();
30+
}
31+
2432
private DelegatingObjectDiffer delegate;
2533

2634
protected AbstractDiffer()
@@ -33,6 +41,41 @@ protected AbstractDiffer(final DelegatingObjectDiffer delegate)
3341
this.delegate = delegate;
3442
}
3543

44+
@Override
45+
public final T compare(final Node parentNode, final Instances instances)
46+
{
47+
final Object working = instances.getWorking();
48+
final CircularReferenceDetector circularReferenceDetector = CIRCULAR_REFERENCE_DETECTOR_THREAD_LOCAL.get();
49+
final boolean destroyOnReturn = circularReferenceDetector.isNew();
50+
T node;
51+
try
52+
{
53+
circularReferenceDetector.push(working);
54+
try
55+
{
56+
node = internalCompare(parentNode, instances);
57+
}
58+
finally
59+
{
60+
circularReferenceDetector.remove(working);
61+
}
62+
}
63+
catch (CircularReferenceDetector.CircularReferenceException e)
64+
{
65+
node = newNode(parentNode, instances);
66+
node.setState(Node.State.CIRCULAR);
67+
}
68+
if (destroyOnReturn)
69+
{
70+
CIRCULAR_REFERENCE_DETECTOR_THREAD_LOCAL.remove();
71+
}
72+
return node;
73+
}
74+
75+
protected abstract T internalCompare(Node parentNode, Instances instances);
76+
77+
protected abstract T newNode(Node parentNode, Instances instances);
78+
3679
public final DelegatingObjectDiffer getDelegate()
3780
{
3881
return delegate;
@@ -48,4 +91,13 @@ public final Configuration getConfiguration()
4891
{
4992
return delegate.getConfiguration();
5093
}
94+
95+
private static final class CircularReferenceDetectorThreadLocal extends ThreadLocal<CircularReferenceDetector>
96+
{
97+
@Override
98+
protected CircularReferenceDetector initialValue()
99+
{
100+
return new CircularReferenceDetector();
101+
}
102+
}
51103
}

src/main/java/de/danielbechler/diff/BeanDiffer.java

Lines changed: 13 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,20 @@
1616

1717
package de.danielbechler.diff;
1818

19-
import java.util.HashSet;
20-
import java.util.Set;
21-
2219
import de.danielbechler.diff.accessor.*;
2320
import de.danielbechler.diff.introspect.*;
2421
import de.danielbechler.diff.node.*;
2522
import de.danielbechler.util.*;
2623

2724
/**
28-
* Used to find differences between objects that were not handled by one of the other (specialized) {@link Differ Differs}.
25+
* Used to find differences between objects that were not handled by one of the other (specialized) {@link
26+
* Differ Differs}.
2927
*
3028
* @author Daniel Bechler
3129
*/
32-
final class BeanDiffer extends AbstractDiffer
30+
final class BeanDiffer extends AbstractDiffer<Node>
3331
{
3432
private Introspector introspector = new StandardIntrospector();
35-
private Set<InstanceOutline> visitedInstances = new HashSet<InstanceOutline>();
3633

3734
BeanDiffer()
3835
{
@@ -55,9 +52,10 @@ Node compare(final Object working, final Object base)
5552
return compare(Node.ROOT, Instances.of(new RootAccessor(), working, base));
5653
}
5754

58-
public Node compare(final Node parentNode, final Instances instances)
55+
@Override
56+
protected Node internalCompare(final Node parentNode, final Instances instances)
5957
{
60-
final Node node = new DefaultNode(parentNode, instances.getSourceAccessor(), instances.getType());
58+
final Node node = newNode(parentNode, instances);
6159
if (getDelegate().isIgnored(node))
6260
{
6361
node.setState(Node.State.IGNORED);
@@ -68,16 +66,20 @@ else if (instances.getType() == null)
6866
}
6967
else
7068
{
71-
if(checkAlreadyVisited(instances))
72-
return node;
7369
return compareBean(parentNode, instances);
7470
}
7571
return node;
7672
}
7773

74+
@Override
75+
protected Node newNode(final Node parentNode, final Instances instances)
76+
{
77+
return new DefaultNode(parentNode, instances.getSourceAccessor(), instances.getType());
78+
}
79+
7880
private Node compareBean(final Node parentNode, final Instances instances)
7981
{
80-
final Node node = new DefaultNode(parentNode, instances.getSourceAccessor(), instances.getType());
82+
final Node node = newNode(parentNode, instances);
8183
if (instances.hasBeenAdded())
8284
{
8385
node.setState(Node.State.ADDED);
@@ -149,18 +151,4 @@ void setIntrospector(final Introspector introspector)
149151
Assert.notNull(introspector, "introspector");
150152
this.introspector = introspector;
151153
}
152-
153-
private boolean checkAlreadyVisited(Instances instances)
154-
{
155-
InstanceOutline outline = InstanceOutline.from(instances);
156-
if(outline != null) {
157-
if(visitedInstances.contains(outline)) {
158-
// if line below is commented in test case bidirectionalGraphStackOverflow creates a stack overflow
159-
// visitedInstances.remove(outline);
160-
return true;
161-
}
162-
visitedInstances.add(outline);
163-
}
164-
return false;
165-
}
166154
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
* Copyright 2012 Daniel Bechler
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package de.danielbechler.diff;
18+
19+
import java.util.*;
20+
21+
/** @author Daniel Bechler */
22+
final class CircularReferenceDetector
23+
{
24+
private final Deque<Object> stack = new LinkedList<Object>();
25+
26+
private boolean isNew = true;
27+
28+
public CircularReferenceDetector()
29+
{
30+
}
31+
32+
public boolean isNew()
33+
{
34+
return isNew;
35+
}
36+
37+
public void push(final Object instance)
38+
{
39+
if (instance == null)
40+
{
41+
return;
42+
}
43+
if (isNew)
44+
{
45+
isNew = false;
46+
}
47+
if (knows(instance))
48+
{
49+
throw new CircularReferenceException();
50+
}
51+
stack.addLast(instance);
52+
}
53+
54+
public boolean knows(final Object needle)
55+
{
56+
for (final Object object : stack)
57+
{
58+
if (object == needle)
59+
{
60+
return true;
61+
}
62+
}
63+
return false;
64+
}
65+
66+
public void remove(final Object instance)
67+
{
68+
if (instance == null)
69+
{
70+
return;
71+
}
72+
if (stack.getLast() == instance)
73+
{
74+
stack.removeLast();
75+
}
76+
else
77+
{
78+
throw new IllegalArgumentException("Detected inconsistency in enter/leave sequence. Must always be LIFO.");
79+
}
80+
}
81+
82+
public int size()
83+
{
84+
return stack.size();
85+
}
86+
87+
public static class CircularReferenceException extends RuntimeException
88+
{
89+
private static final long serialVersionUID = 1L;
90+
91+
public CircularReferenceException()
92+
{
93+
}
94+
95+
@Override
96+
public Throwable fillInStackTrace()
97+
{
98+
return null;
99+
}
100+
}
101+
}

src/main/java/de/danielbechler/diff/CollectionDiffer.java

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
*
2828
* @author Daniel Bechler
2929
*/
30-
final class CollectionDiffer extends AbstractDiffer
30+
final class CollectionDiffer extends AbstractDiffer<CollectionNode>
3131
{
3232
public CollectionDiffer()
3333
{
@@ -44,7 +44,8 @@ public CollectionNode compare(final Collection<?> working, final Collection<?> b
4444
return compare(Node.ROOT, Instances.of(new RootAccessor(), working, base));
4545
}
4646

47-
public CollectionNode compare(final Node parentNode, final Instances instances)
47+
@Override
48+
protected CollectionNode internalCompare(final Node parentNode, final Instances instances)
4849
{
4950
final CollectionNode node = new CollectionNode(parentNode, instances.getSourceAccessor(), instances.getType());
5051
if (getDelegate().isIgnored(node))
@@ -74,7 +75,15 @@ else if (instances.areSame())
7475
return node;
7576
}
7677

77-
private void handleItems(final CollectionNode collectionNode, final Instances instances, final Iterable<?> items)
78+
@Override
79+
protected CollectionNode newNode(final Node parentNode, final Instances instances)
80+
{
81+
return new CollectionNode(parentNode, instances.getSourceAccessor(), instances.getType());
82+
}
83+
84+
private void handleItems(final CollectionNode collectionNode,
85+
final Instances instances,
86+
final Iterable<?> items)
7887
{
7988
for (final Object item : items)
8089
{
@@ -91,7 +100,9 @@ else if (getConfiguration().isReturnable(child))
91100
}
92101
}
93102

94-
private Node compareItem(final CollectionNode collectionNode, final Instances instances, final Object item)
103+
private Node compareItem(final CollectionNode collectionNode,
104+
final Instances instances,
105+
final Object item)
95106
{
96107
final Accessor accessor = collectionNode.accessorForItem(item);
97108
return getDelegate().delegate(collectionNode, instances.access(accessor));

src/main/java/de/danielbechler/diff/Configuration.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public class Configuration implements NodeInspector
3434
private final Collection<Class<?>> equalsOnlyTypes = new LinkedHashSet<Class<?>>(10);
3535
private boolean returnUnchangedNodes = false;
3636
private boolean returnIgnoredNodes = false;
37+
private boolean returnCircularNodes = true;
3738

3839
public Configuration withCategory(final String category)
3940
{
@@ -95,6 +96,18 @@ public Configuration withoutUntouchedNodes()
9596
return this;
9697
}
9798

99+
public Configuration withCircularNodes()
100+
{
101+
this.returnCircularNodes = true;
102+
return this;
103+
}
104+
105+
public Configuration withoutCircularNodes()
106+
{
107+
this.returnCircularNodes = false;
108+
return this;
109+
}
110+
98111
@Override
99112
public boolean isIgnored(final Node node)
100113
{
@@ -170,14 +183,18 @@ public boolean isEqualsOnly(final Node node)
170183
@Override
171184
public boolean isReturnable(final Node node)
172185
{
173-
if (node.getState() == Node.State.UNTOUCHED)
186+
if (node.isUntouched())
174187
{
175188
return returnUnchangedNodes;
176189
}
177-
else if (node.getState() == Node.State.IGNORED)
190+
else if (node.isIgnored())
178191
{
179192
return returnIgnoredNodes;
180193
}
194+
else if (node.isCircular())
195+
{
196+
return returnCircularNodes;
197+
}
181198
return true;
182199
}
183200
}

src/main/java/de/danielbechler/diff/Differ.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import de.danielbechler.diff.node.*;
2020

2121
/** @author Daniel Bechler */
22-
public interface Differ
22+
interface Differ<T extends Node>
2323
{
24-
Node compare(Node parentNode, Instances instances);
24+
T compare(Node parentNode, Instances instances);
2525
}

0 commit comments

Comments
 (0)