CollectionDiffer.java
package org.codefilarete.stalactite.engine.diff;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.function.Function;
import org.codefilarete.tool.Duo;
import org.codefilarete.tool.collection.Iterables;
import org.codefilarete.tool.collection.KeepOrderMap;
import org.codefilarete.tool.collection.KeepOrderSet;
import org.codefilarete.tool.collection.PairIterator.UntilBothIterator;
import org.codefilarete.tool.function.Predicates;
import org.codefilarete.tool.trace.MutableInt;
import static org.codefilarete.stalactite.engine.diff.State.ADDED;
import static org.codefilarete.stalactite.engine.diff.State.HELD;
import static org.codefilarete.stalactite.engine.diff.State.REMOVED;
import static org.codefilarete.stalactite.engine.runtime.onetomany.AbstractOneToManyWithAssociationTableEngine.INDEXED_COLLECTION_FIRST_INDEX_VALUE;
/**
* A class to compute the differences between 2 collections of objects: addition, removal or held
*
* @param <C> bean type
* @author Guillaume Mary
*/
public class CollectionDiffer<C> {
private final Function<C, ?> idProvider;
public CollectionDiffer(Function<C, ?> idProvider) {
this.idProvider = idProvider;
}
/**
* Computes the differences between 2 sets. Comparison between objects will be done through identifier provider
* given at construction time
*
* @param before the "source" {@link Collection}
* @param after the modified {@link Collection}
* @param <I> the type of the payload onto comparison will be done
* @return a set of differences between the 2 collections, never null, empty if the 2 sets are empty. If no
* modification, all instances will be {@link State#HELD}.
*/
public <I> KeepOrderSet<Diff<C>> diff(Collection<C> before, Collection<C> after) {
Map<I, C> beforeMappedOnIdentifier = Iterables.map(before, (Function<C, I>) idProvider, Function.identity(), KeepOrderMap::new);
Map<I, C> afterMappedOnIdentifier = Iterables.map(after, (Function<C, I>) idProvider, Function.identity(), KeepOrderMap::new);
KeepOrderSet<Diff<C>> result = new KeepOrderSet<>();
for (Entry<I, C> entry : beforeMappedOnIdentifier.entrySet()) {
C afterId = afterMappedOnIdentifier.get(entry.getKey());
if (afterId != null) {
result.add(new Diff<>(HELD, entry.getValue(), afterId));
} else {
result.add(new Diff<>(REMOVED, entry.getValue(), null));
}
}
afterMappedOnIdentifier.keySet().removeAll(beforeMappedOnIdentifier.keySet());
for (Entry<I, C> identifiedEntry : afterMappedOnIdentifier.entrySet()) {
result.add(new Diff<>(ADDED, null, identifiedEntry.getValue()));
}
return result;
}
/**
* Computes the differences between 2 collections by taking order in collections into account. Comparison between
* objects will be done through identifier provider given at construction time.
*
* @param before the "source" {@link Collection}
* @param after the modified {@link Collection}
* @return a set of differences between the 2 collections, never null, empty if the 2 sets are empty. If no
* modification, all instances will be {@link State#HELD}.
*/
public KeepOrderSet<IndexedDiff<C>> diffOrdered(Collection<C> before, Collection<C> after) {
// building Map of indexes per object
Map<C, Set<Integer>> beforeIndexes = new KeepOrderMap<>();
Map<C, Set<Integer>> afterIndexes = new KeepOrderMap<>();
MutableInt beforeIndex = new MutableInt(INDEXED_COLLECTION_FIRST_INDEX_VALUE - 1); // -1 because ModifiableInt.increment(..) increments value before giving value
before.forEach(o -> beforeIndexes.computeIfAbsent(o, k -> new HashSet<>()).add(beforeIndex.increment()));
MutableInt afterIndex = new MutableInt(INDEXED_COLLECTION_FIRST_INDEX_VALUE - 1); // -1 because ModifiableInt.increment(..) increments value before giving value
after.forEach(o -> afterIndexes.computeIfAbsent(o, k -> new HashSet<>()).add(afterIndex.increment()));
KeepOrderSet<IndexedDiff<C>> result = new KeepOrderSet<>();
// Removed instances are found with a simple minus
Set<C> removeds = Iterables.minus(beforeIndexes.keySet(), afterIndexes.keySet());
removeds.forEach(e -> result.add(new IndexedDiff<>(REMOVED, e, null, beforeIndexes.get(e), new HashSet<>())));
// Added instances are found with a simple minus (reverse order of removed)
Set<C> addeds = Iterables.minus(afterIndexes.keySet(), beforeIndexes.keySet(), (Function<Collection<? extends C>, Set<C>>) KeepOrderSet::new);
addeds.forEach(e -> result.add(new IndexedDiff<>(ADDED, null, e, new HashSet<>(), afterIndexes.get(e))));
// There are several cases for "held" instances (those existing on both sides)
// - if they are more instances in the new set, then those new are ADDED (with their new index)
// - if they are fewer instances in the set, then the missing ones are REMOVED (with their old index)
// - common instances are HELD (with their index)
// This principle is applied with an Iterator of pairs of indexes : pairs contain before and after index.
// - Pairs with a missing left or right value are declared added and removed, respectively
// - Pairs with both values are declared held
Set<C> helds = Iterables.intersect(afterIndexes.keySet(), beforeIndexes.keySet());
helds.forEach(e -> {
// NB: even if Integer can't be inherited, PairIterator is a Iterator<? extends X, ? extends X>
Iterable<Duo<Integer, Integer>> indexPairs = () -> new UntilBothIterator<>(beforeIndexes.get(e), afterIndexes.get(e));
// NB: These instances may not be added to result, it depends on iteration
IndexedDiff<C> removed = new IndexedDiff<>(REMOVED, e, null);
Object id = idProvider.apply(e);
IndexedDiff<C> held = new IndexedDiff<>(HELD,
// Is this can be more efficient ? shouldn't we compute a Map of i vs before/after instead of iterating on before/after for each held ?
// ... benchmark should be done
Iterables.find(before, c -> Predicates.equalOrNull(idProvider.apply(c), id)),
Iterables.find(after, c -> Predicates.equalOrNull(idProvider.apply(c), id)));
IndexedDiff<C> added = new IndexedDiff<>(ADDED, null, e);
for (Duo<? extends Integer, ? extends Integer> indexPair : indexPairs) {
if (indexPair.getLeft() != null && indexPair.getRight() != null) {
held.addSourceIndex(indexPair.getLeft());
held.addReplacerIndex(indexPair.getRight());
} else if (indexPair.getRight() == null) {
removed.addSourceIndex(indexPair.getLeft());
} else if (indexPair.getLeft() == null) { // unnecessary "if" since this case is the obvious one
added.addReplacerIndex(indexPair.getRight());
}
}
// adding result of iteration to final result
if (!removed.getSourceIndexes().isEmpty()) {
result.add(removed);
}
if (!held.getReplacerIndexes().isEmpty()) {
result.add(held);
}
if (!added.getReplacerIndexes().isEmpty()) {
result.add(added);
}
});
return result;
}
}