package gov.nasa.jpl.mbee.mdk.util; import com.nomagic.magicdraw.core.Application; import com.nomagic.uml2.ext.magicdraw.classes.mdkernel.NamedElement; import gov.nasa.jpl.mbee.mdk.util.Pair; import java.util.*; import java.util.function.BiFunction; import java.util.function.BiPredicate; /** * Created by igomes on 6/30/16. */ public class Changelog<K, V> extends HashMap<Changelog.ChangeType, Map<K, V>> implements Cloneable { private boolean shouldLogChanges; public boolean shouldLogChanges() { return shouldLogChanges; } public void setShouldLogChanges(boolean shouldLogChanges) { this.shouldLogChanges = shouldLogChanges; } // ConcurrentHashMap was decided against as it doesn't accept null values (or keys) and while we could fill it with junk we don't expect // too much concurrency with Changelogs, so just syncing it shouldn't be an issue. public Map<K, V> createMap() { return Collections.synchronizedMap(new LinkedHashMap<K, V>()); } @Override public Map<K, V> get(Object key) { Map<K, V> map = super.get(key); if (map == null && key instanceof ChangeType) { super.put((ChangeType) key, map = createMap()); } return map; } @Override public Changelog<K, V> clone() { Changelog<K, V> clonedChangelog = new Changelog<>(); for (ChangeType changeType : ChangeType.values()) { Map<K, V> map = clonedChangelog.get(changeType); for (Map.Entry<K, V> entry : get(changeType).entrySet()) { map.put(entry.getKey(), entry.getValue()); } } return clonedChangelog; } public Changelog<K, V> and(Changelog<K, V> secondChangelog) { Changelog<K, V> combinedChangelog = this.clone(); for (ChangeType changeType : ChangeType.values()) { for (Map.Entry<K, V> entry : secondChangelog.get(changeType).entrySet()) { combinedChangelog.addChange(entry.getKey(), entry.getValue(), changeType); } } return combinedChangelog; } public <W> Changelog<K, V> and(Changelog<K, W> secondChangelog, BiFunction<K, W, V> converter) { Changelog<K, V> combinedChangelog = this.clone(); for (ChangeType changeType : ChangeType.values()) { for (Map.Entry<K, W> entry : secondChangelog.get(changeType).entrySet()) { combinedChangelog.addChange(entry.getKey(), converter.apply(entry.getKey(), entry.getValue()), changeType); } } return combinedChangelog; } public <W> void findConflicts(Changelog<K, W> changelog, BiPredicate<Change<V>, Change<W>> conflictCondition, Map<K, Pair<Change<V>, Change<W>>> conflictedChanges, Map<K, Pair<Change<V>, Change<W>>> unconflictedChanges) { Set<K> keySet = new HashSet<>(); keySet.addAll(this.flattenedKeyset()); keySet.addAll(changelog.flattenedKeyset()); for (K key : keySet) { Change<V> vChange = null; Change<W> wChange = null; for (ChangeType changeType : ChangeType.values()) { // must use containsKey instead of get, because null is an acceptable value in this paradigm if (vChange == null && this.get(changeType).containsKey(key)) { vChange = new Change<>(this.get(changeType).get(key), changeType); } if (wChange == null && changelog.get(changeType).containsKey(key)) { wChange = new Change<>(changelog.get(changeType).get(key), changeType); } } (conflictCondition.test(vChange, wChange) ? conflictedChanges : unconflictedChanges).put(key, new Pair<>(vChange, wChange)); } } // it is desired that all ChangeTypes are mutually exclusive // enforces that a change can only exist under a single ChangetType at any point public void addChange(K k, V v, ChangeType changeType) { switch (changeType) { case CREATED: Map<K, V> deletedElements = get(Changelog.ChangeType.DELETED); if (deletedElements.containsKey(k)) { deletedElements.remove(k); if (shouldLogChanges) { Application.getInstance().getGUILog().log("Undeleted: " + k + " - " + (v instanceof NamedElement ? " " + ((NamedElement) v).getName() : "<>")); } } else { get(Changelog.ChangeType.CREATED).put(k, v); get(Changelog.ChangeType.UPDATED).remove(k); if (shouldLogChanges) { Application.getInstance().getGUILog().log("Created: " + k + " - " + (v instanceof NamedElement ? " " + ((NamedElement) v).getName() : "<>")); } } break; case DELETED: Map<K, V> createdChanges = get(Changelog.ChangeType.CREATED); // minimizes extraneous deletions if (createdChanges.containsKey(k)) { createdChanges.remove(k); if (shouldLogChanges) { Application.getInstance().getGUILog().log("Unadded: " + k + " - " + (v instanceof NamedElement ? ((NamedElement) v).getName() : "<>")); } } else { get(Changelog.ChangeType.UPDATED).remove(k); get(Changelog.ChangeType.DELETED).put(k, v); if (shouldLogChanges) { Application.getInstance().getGUILog().log("Deleted: " + k + " - " + (v instanceof NamedElement ? ((NamedElement) v).getName() : "<>")); } } break; case UPDATED: if (!get(Changelog.ChangeType.CREATED).containsKey(k) && !get(Changelog.ChangeType.DELETED).containsKey(k)) { get(Changelog.ChangeType.UPDATED).put(k, v); if (shouldLogChanges) { Application.getInstance().getGUILog().log("Updated: " + k + " - " + (v instanceof NamedElement ? " " + ((NamedElement) v).getName() : "<>")); } } break; default: throw new IllegalArgumentException("Unhandled ChangeType for putting."); } } public boolean isEmpty() { for (ChangeType changeType : ChangeType.values()) { if (!get(changeType).isEmpty()) { return false; } } return true; } public int flattenedSize() { int size = 0; for (ChangeType changeType : ChangeType.values()) { size += get(changeType).size(); } return size; } public Set<K> flattenedKeyset() { Set<K> keySet = new LinkedHashSet<>(); for (ChangeType changeType : ChangeType.values()) { keySet.addAll(get(changeType).keySet()); } return keySet; } public enum ChangeType { CREATED, UPDATED, DELETED } public static class Change<C> { private final C changed; private final ChangeType type; public Change(C changed, ChangeType type) { this.changed = changed; this.type = type; } public C getChanged() { return changed; } public ChangeType getType() { return type; } } }