Diff Configuration

JaVers’ diff algorithm has a pluggable construction. It consists of the core comparators suite and optionally, custom comparators.

You can fine-tune how the whole algorithm works by registering custom comparators for certain types (custom comparators overrides core comparators).

For comparing Lists, JaVers has two core comparators, pick one.

List comparing algorithms

Choose between two algorithms for comparing list: SIMPLE (default) or Levenshtein distance.

Generally, we recommend using Levenshtein, because it’s smarter. But use it with caution, it could be slow for long lists, say more then 300 elements.

The main advantage of SIMPLE algorithm is speed, it has linear computation complexity. The main disadvantage is a verbose output.

You can switch to Levenshtein in JaversBuilder:

    Javers javers = JaversBuilder
        .javers()
        .withListCompareAlgorithm(ListCompareAlgorithm.LEVENSHTEIN_DISTANCE)
        .build();

Simple vs Levenshtein algorithm

SIMPLE algorithm generates changes for shifted elements (in case when elements are inserted or removed in the middle of a list). On the contrary, Levenshtein algorithm calculates short and clear change list even in case when elements are shifted. It doesn’t care about index changes for shifted elements.

For example, when we remove one element from a list:

javers.compare(['a','b','c','d','e'],
               ['a','c','d','e'])

the change list will be different, depending on chosen algorithm:

Output from Simple algorithm Output from Levenshtein algorithm
(1). 'b'>>'c'
(2). 'c'>>'d'
(3). 'd'>>'e'
(4). removed:'e'
(1). removed: 'b'

But when both lists have the same size:

javers.compare(['a','b','c','d'],
               ['a','g','e','i'])

the change list will the same:

Simple algorithm Levenshtein algorithm
(1). 'b'>>'g'
(2). 'c'>>'e'
(3). 'd'>>'i'
(1). 'b'>>'g'
(2). 'c'>>'e'
(3). 'd'>>'i'

More about Levenshtein distance

The idea is based on the Levenshtein edit distance algorithm, usually used for comparing Strings. That is answering the question what changes should be done to go from one String to another?

Since a list of characters (i.e. String) is equal to a list of objects up to isomorphism we can use the same algorithm for finding the Levenshtein edit distance for list of objects.

The algorithm is based on computing the shortest path in a DAG. It takes both O(nm) space and time. Further work should improve it to take O(n) space and O(nm) time (n and m being the length of both compared lists).

Custom Comparators

There are cases where JaVers’ default diff algorithm isn’t appropriate. A good example is custom collections, like Guava’s Multimap, which are not connected with Java Collections API.

Let’s focus on Guava’s Multimap. JaVers doesn’t support it out of the box, because Multimap is not a subtype of java.util.Map. Still, Multimap is quite popular and you could expect to have your objects with Multimaps compared by JaVers.

JaVers is meant to be lightweight and can’t depend on the large Guava library. Without a custom comparator, JaVers maps Multimap as ValueType and compares its internal fields property-by-property. This isn’t very useful. What we would expect is MapType and a list of MapChanges as a diff result.

Custom comparators come to the rescue, as they give you full control over the JaVers diff algorithm. You can register a custom comparator for any type (class or interface) to bypass the JaVers type system and diff algorithm.

JaVers maps classes with custom comparators as CustomTypes, which pretty much means I don’t care what it is.

Implementation

All you have to do is implement the CustomPropertyComparator interface:

/**
 * @param <T> custom type, e.g. Multimap
 * @param <C> concrete type of PropertyChange returned by a comparator
 */
public interface CustomPropertyComparator<T, C extends PropertyChange> {
    /**
     * @param left left (or old) property value
     * @param right right (or current) property value
     * @param affectedId Id of domain object being compared
     * @param property property being compared
     * @return should return null if compared objects have no differences
     */
    C compare(T left, T right, GlobalId affectedId, Property property);
}

and register it with JaversBuilder.registerCustomComparator().

Implementation should calculate a diff between two values of CustomType and return a result as a concrete Change subclass, for example:

public class GuavaCustomComparator implements CustomPropertyComparator<Multimap, MapChange> {
    public MapChange compare(Multimap left, Multimap right, GlobalId affectedId, Property property) {
        ... // omitted
    }
}

Register the custom comparator instance in JaversBuilder, for example:

JaversBuilder.javers()
    .registerCustomComparator(new GuavaCustomComparator(), Multimap.class).build()

Custom way to compare Value types
The same rules apply if you want to change JaVers’ default diff algorithm for existing Value type, for example BigDecimal.

For Values, JaVers simply uses equals(). If this isn’t appropriate for you, override it with a Custom comparator. For example, JaVers provides CustomBigDecimalComparator, which rounds BigDecimals before comparing them:

/**
 * Compares BigDecimals with custom precision.
 * Before comparing, values are rounded (HALF_UP) to required scale.
 * <br/><br/>
 *
 * Usage example:
 * <pre>
 * JaversBuilder.javers()
 *     .registerCustomComparator(new CustomBigDecimalComparator(2), BigDecimal).build();
 * </pre>
 */
public class CustomBigDecimalComparator
    implements CustomPropertyComparator<BigDecimal, ValueChange>
{
    private int significantDecimalPlaces;

    public CustomBigDecimalComparator(int significantDecimalPlaces) {
        this.significantDecimalPlaces = significantDecimalPlaces;
    }

    @Override
    public ValueChange compare(BigDecimal left, BigDecimal right, GlobalId affectedId,
        Property property)
    {
        BigDecimal leftRounded = left.setScale(significantDecimalPlaces, ROUND_HALF_UP);
        BigDecimal rightRounded = right.setScale(significantDecimalPlaces, ROUND_HALF_UP);

        if (leftRounded.equals(rightRounded)){
            return null;
        }

        return new ValueChange(affectedId, property, left, right);
    }
}