/******************************************************************************
* 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.Collections;
import java.util.Iterator;
import java.util.List;
import javafx.beans.InvalidationListener;
import javafx.collections.ListChangeListener;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
/**
* A utility class to support change notifications for an {@link ObservableList}
* , replacing the JavaFX-internal {@code ListChangeListener} helper class.
*
* @author anyssen
*
* @param <E>
* The element type of the {@link ObservableList}.
*
*/
public class ListListenerHelperEx<E> {
/**
* A simple implementation of an
* {@link javafx.collections.ListChangeListener.Change}.
*
* @author anyssen
*
* @param <E>
* The element type of the source {@link ObservableList}.
*/
public static class AtomicChange<E> extends ListChangeListener.Change<E> {
private int cursor = -1;
private ElementarySubChange<E>[] elementarySubChanges;
private List<E> previousContents;
/**
* Creates a new {@link ListListenerHelperEx.AtomicChange} that
* represents a change comprising a single elementary sub-change.
*
* @param source
* The source {@link ObservableList} from which the change
* originated.
* @param previousContents
* The previous contents of the {@link ObservableList} before
* the change was applied.
* @param elementarySubChange
* The elementary sub-change that has been applied.
*/
@SuppressWarnings("unchecked")
public AtomicChange(ObservableList<E> source, List<E> previousContents,
ElementarySubChange<E> elementarySubChange) {
super(source);
this.previousContents = previousContents;
this.elementarySubChanges = new ElementarySubChange[] {
elementarySubChange };
}
/**
* Creates a new {@link ListListenerHelperEx.AtomicChange} that
* represents a change comprising multiple elementary sub-changesO.
*
* @param source
* The source {@link ObservableList} from which the change
* originated.
* @param previousContents
* The previous contents of the {@link ObservableList} before
* the change was applied.
* @param elementarySubChanges
* The elementary sub-changes that have been applied as part
* of this change.
*/
@SuppressWarnings("unchecked")
public AtomicChange(ObservableList<E> source, List<E> previousContents,
List<ElementarySubChange<E>> elementarySubChanges) {
super(source);
this.previousContents = previousContents;
this.elementarySubChanges = elementarySubChanges
.toArray(new ElementarySubChange[] {});
}
/**
* Creates a new {@link ListListenerHelperEx.AtomicChange} for the
* passed in source, based on the data provided in the passed-in change.
* <p>
* This is basically used to allow properties wrapping an
* {@link ObservableList} to re-fire change events of their wrapped
* {@link ObservableList} with themselves as source.
*
* @param source
* The new source {@link ObservableList}.
* @param change
* The change to infer a new change from. It is expected that
* the change is in initial state. In either case it will be
* reset to initial state.
*/
@SuppressWarnings("unchecked")
public AtomicChange(ObservableList<E> source,
ListChangeListener.Change<? extends E> change) {
super(source);
// copy previous contents
this.previousContents = new ArrayList<>(
CollectionUtils.getPreviousContents(change));
// retrieve elementary sub-changes by iterating them
// TODO: we could introduce an initialized field inside Change
// already, so we could check the passed in change is not already
// initialized
List<ElementarySubChange<E>> elementarySubChanges = getElementaryChanges(
change);
this.elementarySubChanges = elementarySubChanges
.toArray(new ElementarySubChange[] {});
}
private void checkCursor() {
checkCursor("");
}
private void checkCursor(String args) {
String methodName = Thread.currentThread().getStackTrace()[2]
.getMethodName();
if (methodName.equals("checkCursor")) {
methodName = Thread.currentThread().getStackTrace()[3]
.getMethodName();
}
if (cursor == -1) {
throw new IllegalStateException("Need to call next() before "
+ methodName + "(" + args + ") can be called.");
} else if (cursor >= elementarySubChanges.length) {
throw new IllegalStateException("May only call " + methodName
+ "(" + args + ") if next() returned true.");
}
}
@Override
public int getAddedSize() {
checkCursor();
return super.getAddedSize();
}
@Override
public List<E> getAddedSubList() {
checkCursor();
return elementarySubChanges[cursor].getAdded();
}
@Override
public int getFrom() {
checkCursor();
return elementarySubChanges[cursor].getFrom();
}
@Override
public int[] getPermutation() {
checkCursor();
return elementarySubChanges[cursor].getPermutation();
}
@Override
public int getPermutation(int i) {
checkCursor("int");
return super.getPermutation(i);
}
/**
* Returns the previous contents of the observable list before the
* change was applied.
*
* @return An unmodifiable list containing the previous contents of the
* list.
*/
public List<E> getPreviousContents() {
return Collections.unmodifiableList(previousContents);
}
@Override
public List<E> getRemoved() {
checkCursor();
return elementarySubChanges[cursor].getRemoved();
}
@Override
public int getRemovedSize() {
checkCursor();
return super.getRemovedSize();
}
@Override
public int getTo() {
checkCursor();
return elementarySubChanges[cursor].getTo();
}
@Override
public boolean next() {
cursor++;
return cursor < elementarySubChanges.length;
}
@Override
public void reset() {
cursor = -1;
}
@Override
public String toString() {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < elementarySubChanges.length; i++) {
sb.append(elementarySubChanges[i].toString());
if (i < elementarySubChanges.length - 1) {
sb.append(" ");
}
}
return sb.toString();
}
@Override
public boolean wasAdded() {
checkCursor();
return !getAddedSubList().isEmpty();
}
@Override
public boolean wasPermutated() {
checkCursor();
return super.wasPermutated();
}
@Override
public boolean wasRemoved() {
checkCursor();
return super.wasRemoved();
}
@Override
public boolean wasReplaced() {
checkCursor();
return super.wasReplaced();
}
@Override
public boolean wasUpdated() {
checkCursor();
return super.wasUpdated();
}
}
/**
* An abstract elementary change of an {@link ObservableList}
*
* @param <E>
* The element type of the list.
*/
public static class ElementarySubChange<E> {
/**
* The kind of change that is performed to the {@link ObservableList}.
*
* @author anyssen
*
*/
public enum Kind {
/**
* Addition of elements.
*/
ADD,
/**
* Removal of elements.
*/
REMOVE,
/**
* Replacement of elements.
*/
REPLACE,
/**
* Permutation of elements.
*/
PERMUTATE
}
/**
* Creates a new
* {@link org.eclipse.gef.common.collections.ListListenerHelperEx.ElementarySubChange}
* representing an addition.
*
* @param <E>
* The element type of the {@link ObservableList}.
*
* @param from
* The start index of the change.
* @param to
* The end index of the change.
* @param added
* The elements that were added during this change.
* @return An
* {@link org.eclipse.gef.common.collections.ListListenerHelperEx.ElementarySubChange}
* representing the change.
*/
public static <E> ElementarySubChange<E> added(List<? extends E> added,
int from, int to) {
return new ElementarySubChange<>(Kind.ADD, from, to, null, added,
null);
}
/**
* Creates a new
* {@link org.eclipse.gef.common.collections.ListListenerHelperEx.ElementarySubChange}
* representing a permutation.
*
* @param <E>
* The element type of the {@link ObservableList}.
* @param permutation
* A mapping of prior indexes to current ones.
*
* @param from
* The start index of the change.
* @param to
* The end index of the change.
* @return An
* {@link org.eclipse.gef.common.collections.ListListenerHelperEx.ElementarySubChange}
* representing the change.
*/
public static <E> ElementarySubChange<E> permutated(int[] permutation,
int from, int to) {
return new ElementarySubChange<>(Kind.PERMUTATE, from, to, null,
null, permutation);
}
/**
* Creates a new
* {@link org.eclipse.gef.common.collections.ListListenerHelperEx.ElementarySubChange}
* representing a removal.
*
* @param <E>
* The element type of the {@link ObservableList}.
*
* @param from
* The start index of the change.
* @param to
* The end index of the change.
* @param removed
* The elements that were removed during this change.
* @return An
* {@link org.eclipse.gef.common.collections.ListListenerHelperEx.ElementarySubChange}
* representing the change.
*/
public static <E> ElementarySubChange<E> removed(
List<? extends E> removed, int from, int to) {
return new ElementarySubChange<>(Kind.REMOVE, from, to, removed,
null, null);
}
/**
* Creates a new
* {@link org.eclipse.gef.common.collections.ListListenerHelperEx.ElementarySubChange}
* representing a replacement.
*
* @param <E>
* The element type of the {@link ObservableList}.
* @param removed
* The elements that were removed.
*
* @param from
* The start index of the change.
* @param to
* The end index of the change.
* @param added
* The elements that were added during this change.
* @return An
* {@link org.eclipse.gef.common.collections.ListListenerHelperEx.ElementarySubChange}
* representing the change.
*/
public static <E> ElementarySubChange<E> replaced(
List<? extends E> removed, List<? extends E> added, int from,
int to) {
return new ElementarySubChange<>(Kind.REPLACE, from, to, removed,
added, null);
}
private Kind kind;
private List<E> removed;
private List<E> added;
private int from;
private int to;
private int[] permutation;
/**
* Creates a new
* {@link org.eclipse.gef.common.collections.ListListenerHelperEx.ElementarySubChange}
* .
*
* @param kind
* The kind of change.
*
* @param from
* The start index of the change.
* @param to
* The end index of the change.
* @param removed
* The elements that were removed.
* @param added
* The elements that were added.
* @param permutation
* A mapping of prior indexes to current ones.
*/
public ElementarySubChange(Kind kind, int from, int to,
List<? extends E> removed, List<? extends E> added,
int[] permutation) {
this.kind = kind;
this.from = from;
this.to = to;
if (removed != null) {
this.removed = new ArrayList<>(removed);
}
if (added != null) {
this.added = new ArrayList<>(added);
}
if (permutation != null) {
this.permutation = permutation;
}
}
/**
* Returns the elements that were added by this change.
*
* @return The added elements.
*/
public List<E> getAdded() {
if (added == null) {
return Collections.emptyList();
}
return added;
}
/**
* Returns the index at which elements were added/removed/re-ordered.
*
* @return The start index.
*/
public int getFrom() {
return from;
}
/**
* Returns the kind of change.
*
* @return The change kind.
*/
public Kind getKind() {
return kind;
}
/**
* Returns a mapping of previous indexes to current ones
*
* @return An integer array representing a mapping of previous indexes
* to current indexes.
*/
public int[] getPermutation() {
if (permutation == null) {
return new int[] {};
}
return permutation;
}
/**
* Returns the elements that were removed by this change.
*
* @return The removed elements.
*/
public List<E> getRemoved() {
if (removed == null) {
return Collections.emptyList();
}
return removed;
}
/**
* Returns the index up to which (excluding) elements were
* added/removed/re-ordered.
*
* @return The end index.
*/
public int getTo() {
return to;
}
@Override
public String toString() {
if (Kind.REPLACE.equals(kind)) {
return "Replaced" + getRemoved() + " by " + getAdded() + " at "
+ getFrom() + ".";
} else if (Kind.ADD.equals(kind)) {
return "Added" + getAdded() + " at " + getFrom() + ".";
} else if (Kind.REMOVE.equals(kind)) {
return "Removed" + getRemoved() + " at " + getFrom() + ".";
} else if (Kind.PERMUTATE.equals(kind)) {
return "Permutated by " + Arrays.toString(getPermutation())
+ ".";
}
return super.toString();
}
}
/**
* Infers the elementary changes constituting the change of the
* {@link ObservableList}.
*
* @param <E>
* The element type of the {@link ObservableList} that was
* changed.
* @param change
* The (atomic) change to infer elementary changes from.
* @return A list of elementary changes.
*/
protected static <E> List<ElementarySubChange<E>> getElementaryChanges(
ListChangeListener.Change<? extends E> change) {
List<ElementarySubChange<E>> elementarySubChanges = new ArrayList<>();
while (change.next()) {
if (change.wasReplaced()) {
elementarySubChanges.add(ElementarySubChange.replaced(
change.getRemoved(), change.getAddedSubList(),
change.getFrom(), change.getTo()));
} else if (change.wasRemoved()) {
elementarySubChanges.add(ElementarySubChange.removed(
change.getRemoved(), change.getFrom(), change.getTo()));
} else if (change.wasAdded()) {
elementarySubChanges.add(ElementarySubChange.added(
new ArrayList<>(change.getAddedSubList()),
change.getFrom(), change.getTo()));
} else if (change.wasPermutated()) {
// find permutation
int[] permutation = CollectionUtils.getPermutation(change);
elementarySubChanges.add(ElementarySubChange.<E> permutated(
permutation, change.getFrom(), change.getTo()));
}
}
change.reset();
return elementarySubChanges;
}
private List<InvalidationListener> invalidationListeners = null;
private boolean lockInvalidationListeners;
private boolean lockListChangeListeners;
private List<ListChangeListener<? super E>> listChangeListeners = null;
private ObservableList<E> source;
/**
* Constructs a new {@link ListListenerHelperEx} for the given source
* {@link ObservableList}.
*
* @param source
* The {@link ObservableList} to use as source in change
* notifications.
*/
public ListListenerHelperEx(ObservableList<E> source) {
this.source = source;
}
/**
* Adds a new {@link InvalidationListener} to this
* {@link ListListenerHelperEx}. If the same listener is added more than
* once, it will be registered more than once and will receive multiple
* change events.
*
* @param listener
* The listener to add.
*/
public void addListener(InvalidationListener listener) {
if (invalidationListeners == null) {
invalidationListeners = new ArrayList<>();
}
// XXX: Prevent ConcurrentModificationExceptions (in case listeners are
// added during notifications); as we only create a new multi-set in the
// locked case, memory should not be waisted.
if (lockInvalidationListeners) {
invalidationListeners = new ArrayList<>(invalidationListeners);
}
invalidationListeners.add(listener);
}
/**
* Adds a new {@link ListChangeListener} to this
* {@link ListListenerHelperEx}. If the same listener is added more than
* once, it will be registered more than once and will receive multiple
* change events.
*
* @param listener
* The listener to add.
*/
public void addListener(ListChangeListener<? super E> listener) {
if (listChangeListeners == null) {
listChangeListeners = new ArrayList<>();
}
// XXX: Prevent ConcurrentModificationExceptions (in case listeners are
// added during notifications); as we only create a new multi-set in the
// locked case, memory should not be waisted.
if (lockListChangeListeners) {
listChangeListeners = new ArrayList<>(listChangeListeners);
}
listChangeListeners.add(listener);
}
/**
* Notifies all attached {@link InvalidationListener}s and
* {@link ListChangeListener}s about the change.
*
* @param change
* The change to notify listeners about.
*/
public void fireValueChangedEvent(
ListChangeListener.Change<? extends E> change) {
notifyInvalidationListeners();
if (change != null) {
notifyListChangeListeners(change);
}
}
/**
* Returns the source {@link ObservableList} this
* {@link ListListenerHelperEx} is bound to, which is used in change
* notifications.
*
* @return The source {@link ObservableList}.
*/
protected ObservableList<E> getSource() {
return source;
}
/**
* Notifies all registered {@link InvalidationListener}s.
*/
protected void notifyInvalidationListeners() {
if (invalidationListeners != null) {
try {
lockInvalidationListeners = true;
for (InvalidationListener l : invalidationListeners) {
try {
l.invalidated(source);
} catch (Exception e) {
Thread.currentThread().getUncaughtExceptionHandler()
.uncaughtException(Thread.currentThread(), e);
}
}
} finally {
lockInvalidationListeners = false;
}
}
}
/**
* Notifies the attached {@link ListChangeListener}s about the related
* change.
*
* @param change
* The applied change.
*/
protected void notifyListChangeListeners(Change<? extends E> change) {
if (listChangeListeners != null) {
try {
lockListChangeListeners = true;
for (ListChangeListener<? super E> l : listChangeListeners) {
change.reset();
try {
l.onChanged(change);
} catch (Exception e) {
Thread.currentThread().getUncaughtExceptionHandler()
.uncaughtException(Thread.currentThread(), e);
}
}
} finally {
lockListChangeListeners = false;
}
}
}
/**
* Removes the given {@link InvalidationListener} from this
* {@link ListListenerHelperEx}. If its was registered more than once,
* removes one occurrence.
*
* @param listener
* The listener to remove.
*/
public void removeListener(InvalidationListener listener) {
if (invalidationListeners == null) {
return;
}
// XXX: Prevent ConcurrentModificationExceptions (in case listeners are
// added during notifications); as we only create a new multi-set in the
// locked case, memory should not be waisted.
if (lockInvalidationListeners) {
invalidationListeners = new ArrayList<>(invalidationListeners);
}
// XXX: We have to ignore the hash code when removing listeners, as
// otherwise unbinding will be broken (JavaFX bindings violate the
// contract between equals() and hashCode(): JI-9028554); remove() may
// thus not be used.
for (Iterator<InvalidationListener> iterator = invalidationListeners
.iterator(); iterator.hasNext();) {
if (iterator.next().equals(listener)) {
iterator.remove();
break;
}
}
if (invalidationListeners.isEmpty()) {
invalidationListeners = null;
}
}
/**
* Removes the given {@link ListChangeListener} from this
* {@link ListListenerHelperEx}. If its was registered more than once,
* removes one occurrence.
*
* @param listener
* The listener to remove.
*/
public void removeListener(ListChangeListener<? super E> listener) {
if (listChangeListeners == null) {
return;
}
// XXX: Prevent ConcurrentModificationExceptions (in case listeners are
// added during notifications); as we only create a new multi-set in the
// locked case, memory should not be waisted.
if (lockListChangeListeners) {
listChangeListeners = new ArrayList<>(listChangeListeners);
}
// XXX: We have to ignore the hash code when removing listeners, as
// otherwise unbinding will be broken (JavaFX bindings violate the
// contract between equals() and hashCode(): JI-9028554); remove() may
// thus not be used.
for (Iterator<ListChangeListener<? super E>> iterator = listChangeListeners
.iterator(); iterator.hasNext();) {
if (iterator.next().equals(listener)) {
iterator.remove();
break;
}
}
if (listChangeListeners.isEmpty()) {
listChangeListeners = null;
}
}
}