package org.javers.guava; import com.google.common.collect.Multimap; import org.javers.common.exception.JaversException; import org.javers.core.diff.NodePair; import org.javers.core.diff.appenders.CorePropertyChangeAppender; import org.javers.core.diff.changetype.map.EntryAdded; import org.javers.core.diff.changetype.map.EntryChange; import org.javers.core.diff.changetype.map.EntryRemoved; import org.javers.core.diff.changetype.map.MapChange; import org.javers.core.metamodel.object.DehydrateMapFunction; import org.javers.core.metamodel.object.GlobalIdFactory; import org.javers.core.metamodel.object.OwnerContext; import org.javers.core.metamodel.object.PropertyOwnerContext; import org.javers.core.metamodel.property.Property; import org.javers.core.metamodel.type.*; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Objects; import static java.util.Collections.EMPTY_LIST; import static org.javers.common.exception.JaversExceptionCode.VALUE_OBJECT_IS_NOT_SUPPORTED_AS_MAP_KEY; /** * Compares Guava Multimaps. * <br/> * * It's automatically registered, if Guava is detected on a classpath. * * @author akrystian */ class MultimapChangeAppender extends CorePropertyChangeAppender<MapChange>{ private final TypeMapper typeMapper; private final GlobalIdFactory globalIdFactory; MultimapChangeAppender(TypeMapper typeMapper, GlobalIdFactory globalIdFactory){ this.typeMapper = typeMapper; this.globalIdFactory = globalIdFactory; } @Override public boolean supports(JaversType propertyType) { if (!(propertyType instanceof MultimapType)){ return false; } MapContentType mapContentType = typeMapper.getMapContentType((KeyValueType) propertyType); if (mapContentType.getKeyType() instanceof ValueObjectType){ throw new JaversException(VALUE_OBJECT_IS_NOT_SUPPORTED_AS_MAP_KEY, propertyType); } return true; } @Override public MapChange calculateChanges(NodePair pair, Property property){ Multimap left = (Multimap)pair.getLeftPropertyValue(property); Multimap right = (Multimap)pair.getRightPropertyValue(property); MultimapType multimapType = ((JaversProperty) property).getType(); OwnerContext owner = new PropertyOwnerContext(pair.getGlobalId(), property.getName()); List<EntryChange> entryChanges = calculateChanges(multimapType, left, right, owner); if (!entryChanges.isEmpty()){ renderNotParametrizedWarningIfNeeded(multimapType.getKeyType(), "key", "Multimap", property); renderNotParametrizedWarningIfNeeded(multimapType.getValueType(), "value", "Multimap", property); return new MapChange(pair.getGlobalId(), property.getName(), entryChanges); }else{ return null; } } private List<EntryChange> calculateChanges(MultimapType multimapType, Multimap left, Multimap right, OwnerContext owner){ DehydrateMapFunction dehydrateFunction = getDehydrateMapFunction(multimapType); Multimap leftMultimap = multimapType.map(left, dehydrateFunction, owner); Multimap rightMultimap = multimapType.map(right, dehydrateFunction, owner); List<EntryChange> changes = new ArrayList<>(); for (Object commonKey : Multimaps.commonKeys(leftMultimap, rightMultimap)){ Collection leftValues = leftMultimap.get(commonKey); Collection rightValues = rightMultimap.get(commonKey); Collection difference = difference(leftValues, rightValues); difference.addAll(difference(rightValues, leftValues)); if (difference.size() > 0){ calculateValueChanges(changes, commonKey, leftValues, rightValues); } } calculateKeyChanges(leftMultimap, rightMultimap, changes); return changes; } /** * Difference that handle properly collections with duplicates. */ private static Collection difference(Collection first, Collection second){ if (first == null) { return EMPTY_LIST; } if (second == null) { return first; } Collection difference = new ArrayList<>(first); for (Object current : second){ difference.remove(current); } return difference; } private void calculateKeyChanges(Multimap leftMultimap, Multimap rightMultimap, List<EntryChange> changes){ for (Object addedKey : Multimaps.keysDifference(rightMultimap, leftMultimap)){ Collection difference = difference(rightMultimap.get(addedKey), leftMultimap.get(addedKey)); for (Object addedValue : difference){ changes.add(new EntryAdded(addedKey, addedValue)); } } for (Object removedKey : Multimaps.keysDifference(leftMultimap, rightMultimap)){ for (Object removedValue : difference(leftMultimap.get(removedKey), rightMultimap.get(removedKey))){ changes.add(new EntryRemoved(removedKey, removedValue)); } } } private void calculateValueChanges(List<EntryChange> changes, Object commonKey, Collection leftVal, Collection rightVal){ if (Objects.equals(leftVal, rightVal)){ return; } final Collection valuesRemoved = difference(leftVal, rightVal); if(valuesRemoved.size() > 0){ for (Object addedValue : valuesRemoved){ changes.add( new EntryRemoved(commonKey, addedValue)); } } final Collection valuesAdded = difference(rightVal, leftVal); if(valuesAdded.size() > 0){ for (Object addedValue : valuesAdded){ changes.add( new EntryAdded(commonKey, addedValue)); } } } private DehydrateMapFunction getDehydrateMapFunction(MultimapType multimapType){ JaversType keyType = typeMapper.getJaversType(multimapType.getKeyType()); JaversType valueType = typeMapper.getJaversType(multimapType.getValueType()); MapContentType multiMapContentType = new MapContentType(keyType, valueType); return new DehydrateMapFunction(globalIdFactory, multiMapContentType); } }