001package co.codewizards.cloudstore.core.collection;
002
003import static co.codewizards.cloudstore.core.util.Util.*;
004import static java.util.Objects.*;
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 = requireNonNull(source, "source");
041                this.dest = requireNonNull(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}