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
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
- Physical software project structure
- Service layer
- Data access layer
- Spring MVC
- Web view
- Toolbox
- Testing
- Jawr, webjars, bootstrap, Spring setup trick
- Jakarta Equivalence Relation
- Difficult to test example refactoring