Jakarta Equivalence Relation

The Need

Given a big and complex (objects in objects, maps, arrays, lists, child object etc) object structure.

And I would like to check weather two set of objects are equals or not.

I could not use equals() because it is ialready implemented but it not complete (not all attribute included) and I could not decide whether it is a featrure or bug. Plus the input is two arrays of object and as we all know Java array implementation gives a shit to equals.

Secondary need: once a difference is found I would like to know where it is failed on the object hierarchy exactly. Just think about a little bit. If something is not equal you still need to know why.

Let’s start in small

 1public interface EquivalenceRelation {
 2  public final static EquivalenceRelation DEFAULT = new EquivalenceRelation() {
 3    public boolean areEquals(Object o1, Object o2) {
 4      return o1 == o2 || o1 != null && o1.equals(o2);
 5    }
 6  };
 7
 8  public boolean areEquals(Object o1, Object o2);
 9}

So far so good. But we should not forget about “reporting” differences.

 1public abstract class ReportingEquivalenceRelation implements EquivalenceRelation {
 2  final List<String> messages = new ArrayList<String>();
 3  int indent = -1;
 4  static String END_MARKER = "<!--diff -->";
 5  static String INDENT = "   ";
 6
 7  public ReportingEquivalenceRelation() {
 8  }
 9
10  public ReportingEquivalenceRelation(String title) {
11    addMessage(title);
12  }
13
14  public boolean areEquals(Object o1, Object o2) {
15    indent();
16    boolean res = _areEquals(o1, o2);
17    unindent();
18    if (!res && !alreadyEndMarked()) {
19      addMessage(END_MARKER);
20    }
21    return res;
22  }
23
24  private boolean alreadyEndMarked() {
25    return messages.size() > 0 && messages.get(messages.size() - 1).endsWith(END_MARKER);
26  }
27
28  public abstract boolean _areEquals(Object o1, Object o2);
29
30  public List<String> getMessages() {
31    return messages;
32  }
33
34  public String getMessage() {
35    return Joiner.on("\n").join(messages);
36  }
37
38  public void addMessage(String s) {
39    messages.add(indentation() + s);
40  }
41
42  private String indentation() {
43    String res = "";
44    for (int i = 0; i < indent; i++) {
45      res += INDENT;
46    }
47    return res;
48  }
49
50  public void indent() {
51    indent++;
52  }
53
54  public void unindent() {
55    indent--;
56  }
57}

How to implement in a generic way? Trivial answer: use some kind of reflection. Working directly with reflection in java is pain. But luckily there are alternatives, wrappers like Jakarta Beanutils.

(NB: Even if some of the classes used are not familiar you could check in github - see later - or trivial enough - like FIterable)

  1public class JakartaEquivalenceRelation extends ReportingEquivalenceRelation {
  2  private Set<Pair<Integer, Integer>> registry = new HashSet<Pair<Integer, Integer>>();
  3
  4  public JakartaEquivalenceRelation() {
  5  }
  6
  7  public JakartaEquivalenceRelation(String title) {
  8    super(title);
  9  }
 10
 11  @Override
 12  public boolean _areEquals(Object o1, Object o2) {
 13    if (o1 == o2) {
 14      return true;
 15    }
 16    if (null == o1 || null == o2) {
 17      return false;
 18    }
 19    if (isPrimitive(o1) || isPrimitive(o2)) {
 20      return equalsPrimitive(o1, o2);
 21    }
 22    return beanEquals(o1, o2);
 23  }
 24
 25  static Predicate<PropertyDescriptor> class_attribute = AppPredicates.propEq("name", "class");
 26  static Predicate<PropertyDescriptor> no_class_attribute = Predicates.not(class_attribute);
 27  static Function<PropertyDescriptor, String> byName = AppFunction.byProp("name");
 28
 29  private boolean beanEquals(Object o1, Object o2) {
 30    register(o1, o2);
 31    if (isArray(o1)) {
 32      return equalsArrays(o1, o2);
 33    }
 34    if (isList(o1)) {
 35      return equalsList(o1, o2);
 36    }
 37    if (isSet(o1)) {
 38      throw new RuntimeException("Set equvalence isnot supported");
 39    }
 40    if (isMap(o1)) {
 41      return equalsMap(o1, o2);
 42    }
 43    //plain object
 44    Map<String, PropertyDescriptor> pd1 = 
 45                FIterable.from(getPropertyDescriptors(o1)).filter(no_class_attribute).asMap(byName);
 46    Map<String, PropertyDescriptor> pd2 = 
 47                FIterable.from(getPropertyDescriptors(o2)).filter(no_class_attribute).asMap(byName);
 48    if (pd1.size() != pd2.size()) {
 49      addMessage("size diff");
 50      return false;
 51    }
 52    if (!(pd1.keySet().equals(pd2.keySet()))) {
 53      addMessage("nr of bean properties diff");
 54      return false;
 55    }
 56    for (String prop : pd1.keySet()) {
 57      Object p1 = JakartaPropertyUtils.getProperty(o1, prop);
 58      Object p2 = JakartaPropertyUtils.getProperty(o2, prop);
 59      addMessage(rs("[{}:`{}` vs `{}`]", prop, simplify(p1), simplify(p2)));
 60      if (isRegistered(p1, p2)) {
 61        continue;
 62      }
 63      if (!areEquals(p1, p2)) {
 64        return false;
 65      }
 66    }
 67    return true;
 68  }
 69
 70  private boolean isMap(Object o1) {
 71    return o1 instanceof Map;
 72  }
 73
 74  private boolean isSet(Object o1) {
 75    return o1 instanceof Set;
 76  }
 77
 78  private boolean isList(Object o1) {
 79    return o1 instanceof List;
 80  }
 81
 82  private Object simplify(Object p1) {
 83    if (isPrimitive(p1)) {
 84      return p1;
 85    }
 86    return "<complex>";
 87  }
 88
 89  private boolean isRegistered(Object o1, Object o2) {
 90    Pair<Integer, Integer> pos = Pair.of(System.identityHashCode(o1), System.identityHashCode(o2));
 91    return registry.contains(pos);
 92  }
 93
 94  private void register(Object o1, Object o2) {
 95    Pair<Integer, Integer> pos = Pair.of(System.identityHashCode(o1), System.identityHashCode(o2));
 96    registry.add(pos);
 97  }
 98
 99  private boolean equalsPrimitive(Object o1, Object o2) {
100    return o1.equals(o2);
101  }
102
103  private boolean isPrimitive(Object o1) {
104    return o1 == null
105        || o1.getClass().isPrimitive()
106        || o1 instanceof Boolean
107        || java.lang.Number.class.isAssignableFrom(o1.getClass())
108        || java.lang.String.class.isAssignableFrom(o1.getClass())
109        || java.lang.Object.class.equals(o1.getClass())
110        || o1 instanceof Date;
111  }
112
113  private boolean equalsMap(Object o1, Object o2) {
114    addMessage("MAP");
115    Map<?, ?> a = (Map<?, ?>) o1;
116    Map<?, ?> b = (Map<?, ?>) o2;
117    if (a.size() != b.size()) {
118      addMessage("size diff");
119      return false;
120    }
121    for (Object k : a.keySet()) {
122      Object va = a.get(k);
123      Object vb = b.get(k);
124      addMessage(rs("[{}:`{}` vs `{}`]", k, va, vb));
125      boolean r = areEquals(va, vb);
126      if (!r) {
127        return false;
128      }
129    }
130    return true;
131  }
132
133  private boolean equalsList(Object o1, Object o2) {
134    addMessage("LIST");
135    List<?> l1 = (List<?>) o1;
136    List<?> l2 = (List<?>) o2;
137    if (l1.size() != l2.size()) {
138      addMessage("size diff");
139      return false;
140    }
141    for (int i = 0; i < l1.size(); i++) {
142      addMessage(i + ".");
143      boolean r = areEquals(l1.get(i), l2.get(i));
144      if (!r) {
145        return false;
146      }
147    }
148    return true;
149  }
150
151  private boolean isArray(Object o1) {
152    return o1.getClass().isArray();
153  }
154
155  private boolean equalsArrays(Object o1, Object o2) {
156    addMessage("ARRAY");
157    Object[] oa1 = (Object[]) o1;
158    Object[] oa2 = (Object[]) o2;
159    if (oa1.length != oa2.length) {
160      addMessage("Length diff");
161      return false;
162    }
163    for (int i = 0; i < oa1.length; i++) {
164      addMessage(i + ".");
165      boolean r = areEquals(oa1[i], oa2[i]);
166      if (!r) {
167        return false;
168      }
169    }
170    return true;
171  }
172}

Download full source code with test from github


Technology stack
Jan 29, 2014
comments powered by Disqus

Links

Cool

RSS