001package co.codewizards.cloudstore.core.collection; 002 003import static co.codewizards.cloudstore.core.util.AssertUtil.*; 004import static co.codewizards.cloudstore.core.util.Util.*; 005 006import java.util.Collections; 007import java.util.HashMap; 008import java.util.LinkedList; 009import java.util.List; 010import java.util.Map; 011 012/** 013 * Helper {@linkplain #merge(List, List) merging} a given source-{@code List} into a given destination-{@code List}. 014 * 015 * @author Marco หงุ่ยตระกูล-Schulze - marco at codewizards dot co 016 * 017 * @param <E> the element type. 018 * @param <K> the key type (either the same as the element or a key contained in each list element). 019 */ 020public abstract class ListMerger<E, K> { 021 022 private List<E> source; 023 private List<E> dest; 024 025 private Map<K, List<E>> sourceKey2elements; 026 private Map<K, List<E>> destKey2elements; 027 028 /** 029 * Merge the given source into the given destination. 030 * <p> 031 * After this operation, both lists are semantically equal. This does not mean that their 032 * {@code equals(...)} method returns true, though! This is, because the lists are merged 033 * based on a key which might be wrapped by the elements. The elements are not required 034 * to correctly implement {@code equals(...)}. 035 * 036 * @param source the source from which to copy. Must not be <code>null</code>. 037 * @param dest the destination into which to write. Must not be <code>null</code>. 038 */ 039 public void merge(final List<E> source, final List<E> dest) { 040 this.source = assertNotNull(source, "source"); 041 this.dest = assertNotNull(dest, "dest"); 042 043 populateSourceKey2element(); 044 populateDestKey2element(); 045 046 final List<E> destElementsToRemove = new LinkedList<>(); 047 for (E destElement : dest) { 048 final K sourceKey = getKey(destElement); 049 final List<E> sourceElements = nullToEmptyList(sourceKey2elements.get(sourceKey)); 050 final List<E> destElements = nullToEmptyList(destKey2elements.get(sourceKey)); 051 052 final int elementsToRemoveQty = destElements.size() - sourceElements.size(); 053 if (elementsToRemoveQty > 0) { 054 for (int i = 0; i < elementsToRemoveQty; ++i) { 055 final E removed = destElements.remove(0); 056 destElementsToRemove.add(removed); 057 } 058 } 059 } 060// dest.removeAll(destElementsToRemove); 061 // removeAll(...) does *not* work, because it removes all occurrences that are equal to one element-to-be-removed! 062 // Instead we want to remove exactly one element for each element to be removed! The following 2 lines do this: 063 for (final E e : destElementsToRemove) 064 dest.remove(e); 065 066 int index = -1; 067 for (final E sourceElement : source) { 068 ++index; 069 final K sourceKey = getKey(sourceElement); 070 final List<E> destElements = nullToEmptyList(destKey2elements.get(sourceKey)); 071 072 E destElement = dest.size() <= index ? null : dest.get(index); 073 K destKey = destElement == null ? null : getKey(destElement); 074 if (equal(sourceKey, destKey)) { 075 update(dest, index, sourceElement, destElement); 076 destElements.remove(destElement); 077 continue; 078 } 079 destElement = null; destKey = null; 080 081 if (! destElements.isEmpty()) { 082 destElement = destElements.remove(0); 083 final int lastIndexOf = dest.lastIndexOf(destElement); 084 dest.remove(lastIndexOf); 085 dest.add(index, destElement); 086 update(dest, index, sourceElement, destElement); 087 continue; 088 } 089 090 add(dest, index, sourceElement); 091 } 092 } 093 094 private <T> List<T> nullToEmptyList(final List<T> list) { 095 return list == null ? Collections.<T>emptyList() : list; 096 } 097 098 /** 099 * Add the given {@code element} to the given destination-{@code List} {@code dest} at the specified {@code index}. 100 * <p> 101 * The default implementation simply calls: {@code dest.add(index, element);} 102 * 103 * @param dest the destination. Never <code>null</code>. 104 * @param index the index at which the new element should be added. 105 * @param element the element to be added. 106 */ 107 protected void add(final List<E> dest, final int index, final E element) { 108 dest.add(index, element); 109 } 110 111 /** 112 * Get the key from the given {@code element}. 113 * <p> 114 * If the element is the same as the key, this method should return the given {@code element}. 115 * @param element the element from which to extract the key. May be <code>null</code>, if the 116 * source or the destination {@code List} contains <code>null</code> elements. 117 * @return the key. May be <code>null</code>. 118 */ 119 protected abstract K getKey(E element); 120 121 /** 122 * Update the the given {@code destElement} with the data from {@code sourceElement}; or replace it altogether. 123 * <p> 124 * Depending on whether the elements wrap the actual information in a mutable way, or whether they 125 * are immutable, this method may either copy the data from the {@code sourceElement} into the {@code destElement} 126 * or instead invoke {@link List#set(int, Object) dest.set(index, sourceElement)}. 127 * <p> 128 * <b>Important:</b> This method is only invoked, if the {@link #getKey(Object) key} of both 129 * {@code sourceElement} and {@code destElement} is the same! 130 * 131 * @param dest the destination {@code List}. Never <code>null</code>. 132 * @param index the index in {@code dest} addressing the element to be replaced. 133 * @param sourceElement the source from which to copy. May be <code>null</code>, if the source contains 134 * <code>null</code> elements. 135 * @param destElement the destination into which to write. May be <code>null</code>, if the destination contains 136 * <code>null</code> elements. 137 */ 138 protected abstract void update(List<E> dest, int index, E sourceElement, E destElement); 139 140 protected void populateSourceKey2element() { 141 sourceKey2elements = new HashMap<>(); 142 for (final E element : source) { 143 final K key = getKey(element); 144 List<E> elements = sourceKey2elements.get(key); 145 if (elements == null) { 146 elements = new LinkedList<>(); 147 sourceKey2elements.put(key, elements); 148 } 149 elements.add(element); 150 } 151 } 152 153 protected void populateDestKey2element() { 154 destKey2elements = new HashMap<>(); 155 for (E element : dest) { 156 final K key = getKey(element); 157 List<E> elements = destKey2elements.get(key); 158 if (elements == null) { 159 elements = new LinkedList<>(); 160 destKey2elements.put(key, elements); 161 } 162 elements.add(element); 163 } 164 } 165}