/******************************************************************************
* Copyright (c) 2016 itemis AG and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Alexander Nyßen (itemis AG) - initial API and implementation
*
*******************************************************************************/
package org.eclipse.gef.common.collections;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import org.eclipse.gef.common.collections.ListListenerHelperEx.ElementarySubChange;
import com.google.common.collect.ForwardingList;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.SetMultimap;
import javafx.beans.InvalidationListener;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
/**
* A replacement for the (internal) observable list wrapper returned by
* {@link FXCollections#observableList(java.util.List)} to fix the following
* issues:
* <ul>
* <li>Change notifications are fired from sort() and sort(Comparator) even if
* no change occurred (JI-9029606): fixed by properly guarding the change
* notification calls within.</li>
* <li>Invalidation listeners are notified from within clear() (on JavaSE-1.7
* only), even if no change resulted.</li>
* <li>Invalidation listeners are notified from within set(int, E),
* setAll(Collection), and setAll(E...), even if the call has no effect
* (JI-9029640, JI-9029642).</li>
* <li>Change objects are not immutable
* (https://bugs.openjdk.java.net/browse/JDK-8092504): fixed by using
* {@link ListListenerHelperEx} as a replacement for ListListenerHelper.</li>
* </ul>
*
* @author anyssen
* @param <E>
* The element type of the {@link ObservableList}.
*
*/
class ObservableListWrapperEx<E> extends ForwardingList<E>
implements ObservableList<E> {
private ListListenerHelperEx<E> helper = new ListListenerHelperEx<>(this);
private List<E> backingList;
/**
* Creates a new {@link ObservableList} wrapping the given {@link List}.
*
* @param list
* The {@link List} to wrap into the newly created
* {@link ObservableListWrapperEx}.
*/
public ObservableListWrapperEx(List<E> list) {
this.backingList = list;
}
@Override
public boolean add(E element) {
List<E> previousContents = delegateCopy();
boolean result = super.add(element);
if (result) {
helper.fireValueChangedEvent(
new ListListenerHelperEx.AtomicChange<>(this,
previousContents,
ListListenerHelperEx.ElementarySubChange.added(
Collections.singletonList(element),
previousContents.size(),
previousContents.size() + 1)));
}
return result;
}
@Override
public void add(int index, E element) {
List<E> previousContents = delegateCopy();
super.add(index, element);
helper.fireValueChangedEvent(
new ListListenerHelperEx.AtomicChange<>(this, previousContents,
ListListenerHelperEx.ElementarySubChange.added(
Collections.singletonList(element), index,
index + 1)));
}
@Override
public boolean addAll(Collection<? extends E> collection) {
List<E> previousContents = delegateCopy();
boolean result = super.addAll(collection);
helper.fireValueChangedEvent(new ListListenerHelperEx.AtomicChange<>(
this, previousContents,
ListListenerHelperEx.ElementarySubChange.added(
new ArrayList<>(collection), previousContents.size(),
previousContents.size() + collection.size())));
return result;
}
@SuppressWarnings("unchecked")
@Override
public boolean addAll(E... elements) {
return addAll(Arrays.asList(elements));
}
@Override
public boolean addAll(int index, Collection<? extends E> elements) {
List<E> previousContents = delegateCopy();
boolean result = super.addAll(index, elements);
helper.fireValueChangedEvent(
new ListListenerHelperEx.AtomicChange<>(this, previousContents,
ListListenerHelperEx.ElementarySubChange.added(
new ArrayList<>(elements), index,
index + elements.size())));
return result;
}
@Override
public void addListener(InvalidationListener listener) {
helper.addListener(listener);
}
@Override
public void addListener(ListChangeListener<? super E> listener) {
helper.addListener(listener);
}
@Override
public void clear() {
List<E> previousContents = delegateCopy();
super.clear();
if (!previousContents.isEmpty()) {
helper.fireValueChangedEvent(
new ListListenerHelperEx.AtomicChange<>(this,
previousContents,
ListListenerHelperEx.ElementarySubChange
.removed(previousContents, 0, 0)));
}
}
@Override
protected List<E> delegate() {
return backingList;
}
/**
* Returns a copy of the delegate {@link List}, which is used for change
* notifications.
*
* @return A copy of the backing {@link List}.
*/
protected List<E> delegateCopy() {
return new ArrayList<>(backingList);
}
@Override
public E remove(int index) {
List<E> previousContents = delegateCopy();
E result = super.remove(index);
helper.fireValueChangedEvent(
new ListListenerHelperEx.AtomicChange<>(this, previousContents,
ListListenerHelperEx.ElementarySubChange.removed(
Collections.singletonList(result), index,
index)));
return result;
}
@Override
public void remove(int from, int to) {
List<E> previousContents = delegateCopy();
List<E> removed = new ArrayList<>();
for (int i = to - 1; i >= from; i--) {
removed.add(0, super.remove(i));
}
helper.fireValueChangedEvent(new ListListenerHelperEx.AtomicChange<>(
this, previousContents, ListListenerHelperEx.ElementarySubChange
.removed(removed, from, from)));
}
@SuppressWarnings("unchecked")
@Override
public boolean remove(Object object) {
List<E> previousContents = delegateCopy();
if (super.remove(object)) {
// XXX: if remove was successful, its safe to cast here
int index = previousContents.indexOf(object);
helper.fireValueChangedEvent(
new ListListenerHelperEx.AtomicChange<>(this,
previousContents,
ListListenerHelperEx.ElementarySubChange.removed(
Collections.singletonList((E) object),
index, index)));
return true;
}
return false;
}
@Override
public boolean removeAll(Collection<?> collection) {
List<E> previousContents = delegateCopy();
if (super.removeAll(collection)) {
// check which have been removed
List<ElementarySubChange<E>> elementaryChanges = new ArrayList<>();
ElementarySubChange<E> currentElementaryChange = null;
int removeCount = 0;
for (E e : previousContents) {
if (collection.contains(e)) {
// create a new elementary change, if elements are not
// 'continuous' (ensure that the count of elements that have
// already been deleted by preceding elementary changes is
// subtracted from the index)
if (currentElementaryChange == null
|| previousContents.indexOf(e)
- currentElementaryChange.getFrom() > 1) {
if (currentElementaryChange != null) {
removeCount += currentElementaryChange.getRemoved()
.size();
}
int index = previousContents.indexOf(e) - removeCount;
currentElementaryChange = ElementarySubChange.removed(
Collections.singletonList(e), index, index);
elementaryChanges.add(currentElementaryChange);
} else {
// replace current elementary change (i.e. append the
// removed element)
List<E> removed = new ArrayList<>(
currentElementaryChange.getRemoved());
removed.add(e);
int index = currentElementaryChange.getFrom();
elementaryChanges.remove(currentElementaryChange);
currentElementaryChange = ElementarySubChange
.removed(removed, index, index);
elementaryChanges.add(currentElementaryChange);
}
}
}
// determine lowest index that was removed (will be used as from and
// to index)
helper.fireValueChangedEvent(
new ListListenerHelperEx.AtomicChange<>(this,
previousContents, elementaryChanges));
return true;
}
return false;
}
@SuppressWarnings("unchecked")
@Override
public boolean removeAll(E... elements) {
return removeAll(Arrays.asList(elements));
}
@Override
public void removeListener(InvalidationListener listener) {
helper.removeListener(listener);
}
@Override
public void removeListener(ListChangeListener<? super E> listener) {
helper.removeListener(listener);
}
@Override
public boolean retainAll(Collection<?> collection) {
List<E> previousContents = delegateCopy();
if (super.retainAll(collection)) {
// check which have been removed
List<ElementarySubChange<E>> elementaryChanges = new ArrayList<>();
ElementarySubChange<E> currentElementaryChange = null;
int removeCount = 0;
for (E e : previousContents) {
if (!collection.contains(e)) {
// create a new elementary change, if elements are not
// 'continuous' (ensure that the count of elements that have
// already been deleted by preceding elementary changes is
// subtracted from the index)
if (currentElementaryChange == null
|| previousContents.indexOf(e)
- currentElementaryChange.getFrom() > 1) {
if (currentElementaryChange != null) {
removeCount += currentElementaryChange.getRemoved()
.size();
}
int index = previousContents.indexOf(e) - removeCount;
currentElementaryChange = ElementarySubChange.removed(
Collections.singletonList(e), index, index);
elementaryChanges.add(currentElementaryChange);
} else {
// replace current elementary change (i.e. append the
// removed element)
List<E> removed = new ArrayList<>(
currentElementaryChange.getRemoved());
removed.add(e);
int index = currentElementaryChange.getFrom();
elementaryChanges.remove(currentElementaryChange);
currentElementaryChange = ElementarySubChange
.removed(removed, index, index);
elementaryChanges.add(currentElementaryChange);
}
}
}
// determine lowest index that was removed (will be used as from and
// to index)
helper.fireValueChangedEvent(
new ListListenerHelperEx.AtomicChange<>(this,
previousContents, elementaryChanges));
return true;
}
return false;
}
@SuppressWarnings("unchecked")
@Override
public boolean retainAll(E... elements) {
return retainAll(Arrays.asList(elements));
}
@Override
public E set(int index, E element) {
List<E> previousContents = delegateCopy();
if (get(index) != element) {
E result = super.remove(index);
super.add(index, element);
helper.fireValueChangedEvent(
new ListListenerHelperEx.AtomicChange<>(this,
previousContents,
ElementarySubChange.replaced(
Collections.singletonList(result),
Collections.singletonList(element), index,
index + 1)));
return result;
}
return element;
}
@Override
public boolean setAll(Collection<? extends E> collection) {
List<E> previousContents = delegateCopy();
if (!previousContents.equals(collection)) {
delegate().clear();
delegate().addAll(collection);
helper.fireValueChangedEvent(
new ListListenerHelperEx.AtomicChange<>(this,
previousContents, ElementarySubChange.replaced(
previousContents, delegate(), 0, size())));
return true;
}
return false;
}
@SuppressWarnings("unchecked")
@Override
public boolean setAll(E... elements) {
return setAll(Arrays.asList(elements));
}
/**
* Sorts the elements of this {@link ObservableListWrapperEx} using the
* default comparator.
*/
public void sort() {
sort(null);
}
/**
* Sorts the elements of this {@link ObservableListWrapperEx} using the
* given {@link Comparator}.
*
* @param c
* The {@link Comparator} to use.
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public void sort(Comparator<? super E> c) {
// TODO: This algorithm is not very elaborated, as computation of
// permutation indexes is done independent of the sort itself, and we
// need to iterate over the complete list to compute the previous
// indexes (so we can properly handle elements with multiple
// occurrences).
List<E> previousContents = delegateCopy();
SetMultimap<E, Integer> previousIndexes = HashMultimap.create();
for (int i = 0; i < previousContents.size(); i++) {
previousIndexes.put(previousContents.get(i), i);
}
// List.sort(Comparator) was introduced in 1.8; we use list iterator
// directly here, so we stay compatible with 1.7
// TODO: change to using List.sort(Comparator) when dropping support for
// JavaSE-1.7.
Object[] a = delegate().toArray();
int[] permutation = new int[a.length];
Arrays.sort(a, (Comparator) c);
ListIterator<E> iterator = delegate().listIterator();
// keep track if list was actually changed
boolean changed = false;
for (int i = 0; i < a.length; i++) {
E current = iterator.next();
if (current != a[i]) {
changed = true;
iterator.set((E) a[i]);
}
// build-up permutation (for change notification)
Iterator<Integer> previousIndexIterator = previousIndexes
.get((E) a[i]).iterator();
permutation[previousIndexIterator.next()] = i;
previousIndexIterator.remove();
}
if (changed) {
helper.fireValueChangedEvent(
new ListListenerHelperEx.AtomicChange<>(this,
previousContents,
ListListenerHelperEx.ElementarySubChange
.<E> permutated(permutation, 0, a.length)));
}
}
// TODO: overwrite replaceAll(UnaryOperator) as well, as soon as we drop
// Java 7 support.
}