package co.codewizards.cloudstore.core.collection; import static co.codewizards.cloudstore.core.util.AssertUtil.*; import static co.codewizards.cloudstore.core.util.Util.*; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; /** * Helper {@linkplain #merge(List, List) merging} a given source-{@code List} into a given destination-{@code List}. * * @author Marco หงุ่ยตระกูล-Schulze - marco at codewizards dot co * * @param <E> the element type. * @param <K> the key type (either the same as the element or a key contained in each list element). */ public abstract class ListMerger<E, K> { private List<E> source; private List<E> dest; private Map<K, List<E>> sourceKey2elements; private Map<K, List<E>> destKey2elements; /** * Merge the given source into the given destination. * <p> * After this operation, both lists are semantically equal. This does not mean that their * {@code equals(...)} method returns true, though! This is, because the lists are merged * based on a key which might be wrapped by the elements. The elements are not required * to correctly implement {@code equals(...)}. * * @param source the source from which to copy. Must not be <code>null</code>. * @param dest the destination into which to write. Must not be <code>null</code>. */ public void merge(final List<E> source, final List<E> dest) { this.source = assertNotNull(source, "source"); this.dest = assertNotNull(dest, "dest"); populateSourceKey2element(); populateDestKey2element(); final List<E> destElementsToRemove = new LinkedList<>(); for (E destElement : dest) { final K sourceKey = getKey(destElement); final List<E> sourceElements = nullToEmptyList(sourceKey2elements.get(sourceKey)); final List<E> destElements = nullToEmptyList(destKey2elements.get(sourceKey)); final int elementsToRemoveQty = destElements.size() - sourceElements.size(); if (elementsToRemoveQty > 0) { for (int i = 0; i < elementsToRemoveQty; ++i) { final E removed = destElements.remove(0); destElementsToRemove.add(removed); } } } // dest.removeAll(destElementsToRemove); // removeAll(...) does *not* work, because it removes all occurrences that are equal to one element-to-be-removed! // Instead we want to remove exactly one element for each element to be removed! The following 2 lines do this: for (final E e : destElementsToRemove) dest.remove(e); int index = -1; for (final E sourceElement : source) { ++index; final K sourceKey = getKey(sourceElement); final List<E> destElements = nullToEmptyList(destKey2elements.get(sourceKey)); E destElement = dest.size() <= index ? null : dest.get(index); K destKey = destElement == null ? null : getKey(destElement); if (equal(sourceKey, destKey)) { update(dest, index, sourceElement, destElement); destElements.remove(destElement); continue; } destElement = null; destKey = null; if (! destElements.isEmpty()) { destElement = destElements.remove(0); final int lastIndexOf = dest.lastIndexOf(destElement); dest.remove(lastIndexOf); dest.add(index, destElement); update(dest, index, sourceElement, destElement); continue; } add(dest, index, sourceElement); } } private <T> List<T> nullToEmptyList(final List<T> list) { return list == null ? Collections.<T>emptyList() : list; } /** * Add the given {@code element} to the given destination-{@code List} {@code dest} at the specified {@code index}. * <p> * The default implementation simply calls: {@code dest.add(index, element);} * * @param dest the destination. Never <code>null</code>. * @param index the index at which the new element should be added. * @param element the element to be added. */ protected void add(final List<E> dest, final int index, final E element) { dest.add(index, element); } /** * Get the key from the given {@code element}. * <p> * If the element is the same as the key, this method should return the given {@code element}. * @param element the element from which to extract the key. May be <code>null</code>, if the * source or the destination {@code List} contains <code>null</code> elements. * @return the key. May be <code>null</code>. */ protected abstract K getKey(E element); /** * Update the the given {@code destElement} with the data from {@code sourceElement}; or replace it altogether. * <p> * Depending on whether the elements wrap the actual information in a mutable way, or whether they * are immutable, this method may either copy the data from the {@code sourceElement} into the {@code destElement} * or instead invoke {@link List#set(int, Object) dest.set(index, sourceElement)}. * <p> * <b>Important:</b> This method is only invoked, if the {@link #getKey(Object) key} of both * {@code sourceElement} and {@code destElement} is the same! * * @param dest the destination {@code List}. Never <code>null</code>. * @param index the index in {@code dest} addressing the element to be replaced. * @param sourceElement the source from which to copy. May be <code>null</code>, if the source contains * <code>null</code> elements. * @param destElement the destination into which to write. May be <code>null</code>, if the destination contains * <code>null</code> elements. */ protected abstract void update(List<E> dest, int index, E sourceElement, E destElement); protected void populateSourceKey2element() { sourceKey2elements = new HashMap<>(); for (final E element : source) { final K key = getKey(element); List<E> elements = sourceKey2elements.get(key); if (elements == null) { elements = new LinkedList<>(); sourceKey2elements.put(key, elements); } elements.add(element); } } protected void populateDestKey2element() { destKey2elements = new HashMap<>(); for (E element : dest) { final K key = getKey(element); List<E> elements = destKey2elements.get(key); if (elements == null) { elements = new LinkedList<>(); destKey2elements.put(key, elements); } elements.add(element); } } }