All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.mastfrog.http.harness.difference.ReflectionDifferencer Maven / Gradle / Ivy

/*
 * The MIT License
 *
 * Copyright 2022 Mastfrog Technologies.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.mastfrog.http.harness.difference;

import static com.mastfrog.http.harness.difference.DifferenceKind.INSERTION;
import com.mastfrog.util.strings.Strings;
import static java.lang.Math.abs;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.function.BiConsumer;

/**
 * General-purpose differencer. Mows through the fields of two objects
 * (presumably of the same type), recursively, collecting differences between
 * them.
 *
 * @author Tim Boudreau
 */
final class ReflectionDifferencer implements Differencer {

    private final List> differencers = new ArrayList<>();

    ReflectionDifferencer() {
        // Differences for any case where one item is null and the other is not
        differencers.add(new OneNullDifferencer());
        // Compares numbers and includes a delta in the difference text
        differencers.add(new NumberDifferencer());
        // For performance
        differencers.add(new BooleanDifferencer());
        // Compares strings and gives useful before/after comparison output
        differencers.add(new StringDifferencer());
        // Compares things like Enum and Class which must be instance equality
        differencers.add(new InstanceEqualityDifferencer());
        // Compares maps and objects which have been converted into maps for
        // comparison
        differencers.add(new MapDifferencer());
        // Compares arrays by converting them into lists and using collection
        // differencing
        differencers.add(new ArrayDifferencer());
        // Reflectively drills through a random type, grabbing field values
        // and recursively comparing those
        differencers.add(new DissectingDifferencer());
        // Compares sets
        differencers.add(new SetDifferencer());
        // Compares collections, with special diff-generation handling for lists
        differencers.add(new CollectionDifferencer());
        // Compares objects on the value of toString()
        differencers.add(new ToStringDifferencer());
    }

    private static boolean isPrimitiveLike(Object o) {
        return o instanceof String || o instanceof Number
                || o instanceof Boolean || o instanceof Character
                || o instanceof BigInteger || o instanceof BigDecimal
                || o instanceof CharSequence;
    }

    public Map>> difference(Object a, Object b) {
        DifferencesBuilder>>> db
                = DifferencesBuilder.root();
        difference("", a, b, db);
        return db.build();
    }

    @Override
    public 

void difference(String name, Object a, Object b, DifferencesBuilder

bldr) { if (Objects.equals(a, b)) { return; } for (Differencer diff : differencers) { if (diff.differenceIfPossible(name, a, b, bldr)) { return; } } bldr.add(Difference.create(a, b)); } private static abstract class MatchingDifferencer implements Differencer { private final Class type; MatchingDifferencer(Class type) { this.type = type; } @Override public boolean canDifference(R a, R b) { return type.isInstance(a) && type.isInstance(b); } } private static final class OneNullDifferencer implements Differencer { @Override public

void difference(String name, Object a, Object b, DifferencesBuilder

bldr) { bldr.add(DifferenceKind.CHANGE.newDifference(a, b)); } @Override public boolean canDifference(R a, R b) { return (a == null) != (b == null); } } private static final class StringDifferencer extends MatchingDifferencer { StringDifferencer() { super(CharSequence.class); } @Override public

void difference(String name, CharSequence a, CharSequence b, DifferencesBuilder

bldr) { if (!Strings.charSequencesEqual(a, b)) { bldr.add(new StringDifference(a, b)); } } } private static final class NumberDifferencer extends MatchingDifferencer { NumberDifferencer() { super(Number.class); } @Override public

void difference(String name, Number a, Number b, DifferencesBuilder

bldr) { boolean isChange; if (isFloatingPoint(a, b)) { double aval = a.doubleValue(); double bval = b.doubleValue(); isChange = abs(aval - bval) != 0D; } else { long aval = a.longValue(); long bval = b.longValue(); isChange = abs(aval - bval) != 0L; } if (isChange) { bldr.add(new NumericDifference(a, b)); } } private static boolean isFloatingPoint(Number a, Number b) { return isFloatingPoint(a) || isFloatingPoint(b); } private static boolean isFloatingPoint(Number num) { return num instanceof Float || num instanceof Double; } } static class InstanceEqualityDifferencer implements Differencer { @Override public

void difference(String name, Object a, Object b, DifferencesBuilder

bldr) { if (a != b) { bldr.add(DifferenceKind.CHANGE.newDifference(a, b)); } } @Override public boolean canDifference(R a, R b) { return a != null && b != null && canDifference(a) && canDifference(b) && a.getClass() == b.getClass(); } private boolean canDifference(Object o) { return o instanceof Class || o instanceof Enum; } } private static final class BooleanDifferencer extends MatchingDifferencer { BooleanDifferencer() { super(Boolean.class); } @Override public

void difference(String name, Boolean a, Boolean b, DifferencesBuilder

bldr) { if (a.booleanValue() != b.booleanValue()) { bldr.add(DifferenceKind.CHANGE.newDifference(a, b)); } } } private final class ArrayDifferencer implements Differencer { @Override public

void difference(String name, Object a, Object b, DifferencesBuilder

bldr) { if (a != null && a.getClass().isArray() && b != null && b.getClass().isArray()) { List alist = arrayToList(a); List blist = arrayToList(b); ReflectionDifferencer.this.difference(name, alist, blist, bldr); } } @Override public boolean canDifference(R a, R b) { return a != null && a.getClass().isArray() && b != null && b.getClass().isArray(); } } private final class DissectingDifferencer extends MatchingDifferencer { DissectingDifferencer() { super(Object.class); } @Override public

void difference(String name, Object a, Object b, DifferencesBuilder

bldr) { Map amap = toMap(a); Map bmap = toMap(b); ReflectionDifferencer.this.difference(name, amap, bmap, bldr); } private boolean isCollectionOrArrayMap(Object o) { return o != null && (o.getClass().isArray() || o instanceof Map || o instanceof Collection); } @Override public boolean canDifference(R a, R b) { return !isPrimitiveLike(a) && !isPrimitiveLike(b) && !isCollectionOrArrayMap(a) && !isCollectionOrArrayMap(b); } Map toMap(Object o) { Map m = new LinkedHashMap<>(); dissect(o, m::put); return m; } private boolean dissect(Object o, BiConsumer c) { if (!isPrimitiveLike(o)) { dissect(o, o.getClass(), c); return true; } else { c.accept("value", o); } return false; } private void dissect(Object o, Class type, BiConsumer c) { if (type == Object.class || type == Class.class) { return; } try { Field[] flds = type.getDeclaredFields(); for (Field f : flds) { if ((f.getModifiers() & Modifier.STATIC) == 0) { f.setAccessible(true); Object value = f.get(o); c.accept(f.getName(), value); } } dissect(o, type.getSuperclass(), c); } catch (Exception | Error e) { e.printStackTrace(); } } } private final class MapDifferencer extends MatchingDifferencer> { MapDifferencer() { super(Map.class); } @Override public

void difference(String name, Map a, Map b, DifferencesBuilder

bldr) { withKeyDivergence(a, b, (removed, added) -> { for (Object rem : removed) { bldr.add(DifferenceKind.DELETION.newDifference(rem)); } for (Object add : added) { bldr.add(DifferenceKind.INSERTION.newDifference(add)); } Set remainder = intersection(a.keySet(), b.keySet()); for (Object key : remainder) { String kname = key.toString(); DifferencesBuilder> ch = bldr.child(kname); try { ReflectionDifferencer.this.difference(kname, a.get(key), b.get(key), ch); } finally { ch.build(); } } }); } } private final class SetDifferencer extends MatchingDifferencer> { SetDifferencer() { super(Set.class); } @Override public

void difference(String name, Set a, Set b, DifferencesBuilder

bldr) { Set aset = setFrom(a); Set bset = setFrom(b); withKeyDivergence(aset, bset, (removed, added) -> { for (Object o : bset) { bldr.add(name + ".added", INSERTION.newDifference(o)); } for (Object o : aset) { bldr.add(name + ".removed", INSERTION.newDifference(o)); } }); } } private final class CollectionDifferencer extends MatchingDifferencer> { CollectionDifferencer() { super(Collection.class); } @Override @SuppressWarnings("unchecked") public

void difference(String name, Collection a, Collection b, DifferencesBuilder

bldr) { if (a instanceof List && b instanceof List) { try { // Sort by toString()? new ParallelIterator<>((List) a, (List) b) .getChanges() .forEach(diff -> { bldr.add(diff); diff.addChildDifferences(ReflectionDifferencer.this, bldr); }); if (a.size() != b.size()) { bldr.add("size", DifferenceKind.CHANGE.newDifference(a.size(), b.size())); } return; } catch (IllegalStateException ex) { // The algorithm is not duplicate-tolerant, and this may be // thrown if the collection is not a set } } List aa = toList(a); List bb = toList(b); if (aa.size() != bb.size()) { bldr.add("size", DifferenceKind.CHANGE.newDifference(aa.size(), bb.size())); } for (int i = 0; i < Math.min(aa.size(), bb.size()); i++) { Object aaa = aa.get(i); Object bbb = bb.get(i); if (!Objects.equals(aaa, bbb)) { ReflectionDifferencer.this.difference(Integer.toString(i), aaa, bbb, bldr.child(Integer.toString(i))); } } Set aset = setFrom(aa); Set bset = setFrom(bb); withKeyDivergence(aset, bset, (removed, added) -> { for (Object o : bset) { bldr.add("added", DifferenceKind.INSERTION.newDifference(o)); } for (Object o : aset) { bldr.add("removed", DifferenceKind.DELETION.newDifference(o)); } }); } } static List arrayToList(Object o) { int size = Array.getLength(o); List objs = new ArrayList<>(size); for (int i = 0; i < size; i++) { objs.add(Array.get(o, i)); } return objs; } static List toList(Collection what) { return new ArrayList<>(what); } private static boolean keysComparable(Map aKeys) { for (Map.Entry e : aKeys.entrySet()) { if (!(e.getKey() instanceof Comparable)) { return false; } } return true; } static boolean isComparable(Collection coll) { for (Object o : coll) { if (!(o instanceof Comparable)) { return false; } } return true; } static Map copyOf(Map map) { if (keysComparable(map)) { return new TreeMap<>(map); } else { return new LinkedHashMap<>(map); } } static Set setFrom(Collection set) { if (isComparable(set)) { return new TreeSet<>(set); } else { return new LinkedHashSet<>(set); } } static Set absent(Set from, Set in) { Set fromCopy = setFrom(from); fromCopy.removeAll(in); return fromCopy; } static Set added(Set from, Set to) { Set inCopy = setFrom(to); inCopy.removeAll(from); return inCopy; } static Set intersection(Set a, Set b) { Set set = setFrom(a); set.retainAll(b); return set; } static void withKeyDivergence(Map a, Map b, BiConsumer, Set> c) { Map aa = copyOf(a); Map bb = copyOf(b); Set akeys = aa.keySet(); Set bkeys = bb.keySet(); Set removals = absent(bkeys, akeys); Set additions = added(akeys, bkeys); c.accept(removals, additions); } static void withKeyDivergence(Set a, Set b, BiConsumer, Set> c) { Set akeys = setFrom(a); Set bkeys = setFrom(b); Set removals = absent(bkeys, akeys); Set additions = added(akeys, bkeys); c.accept(removals, additions); } private static final class ToStringDifferencer extends MatchingDifferencer { ToStringDifferencer() { super(Object.class); } @Override public

void difference(String name, Object a, Object b, DifferencesBuilder

bldr) { String astr = Objects.toString(a); String bstr = Objects.toString(b); if (!astr.equals(bstr)) { bldr.add(DifferenceKind.CHANGE.newDifference(a, b)); } } } }