/******************************************************************************
* 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.Iterator;
import java.util.List;
import org.eclipse.gef.common.collections.MultisetChangeListener.Change;
import com.google.common.collect.HashMultiset;
import com.google.common.collect.Multiset;
import com.google.common.collect.Multisets;
import javafx.beans.InvalidationListener;
/**
* A utility class to support change notifications for an
* {@link ObservableMultiset}.
*
* @author anyssen
*
* @param <E>
* The element type of the {@link ObservableMultiset}.
*
*/
public class MultisetListenerHelper<E> {
/**
* A simple implementation of an {@link MultisetChangeListener.Change}.
*
* @author anyssen
*
* @param <E>
* The element type of the source {@link ObservableMultiset}.
*/
public static class AtomicChange<E>
extends MultisetChangeListener.Change<E> {
private int cursor = -1;
private ElementarySubChange<E>[] elementarySubChanges;
private Multiset<E> previousContents;
/**
* Creates a new {@link MultisetListenerHelper.AtomicChange} that
* represents a change comprising a single elementary sub-change.
*
* @param source
* The source {@link ObservableMultiset} from which the
* change originated.
* @param previousContents
* The previous contents of the {@link ObservableMultiset}
* before the change was applied.
* @param elementarySubChange
* The elementary sub-change that has been applied.
*/
@SuppressWarnings("unchecked")
public AtomicChange(ObservableMultiset<E> source,
Multiset<E> previousContents,
ElementarySubChange<E> elementarySubChange) {
super(source);
this.previousContents = previousContents;
this.elementarySubChanges = new ElementarySubChange[] {
elementarySubChange };
}
/**
* Creates a new {@link MultisetListenerHelper.AtomicChange} that
* represents a change comprising multiple elementary sub-changesO.
*
* @param source
* The source {@link ObservableMultiset} from which the
* change originated.
* @param previousContents
* The previous contents of the {@link ObservableMultiset}
* before the change was applied.
* @param elementarySubChanges
* The elementary sub-changes that have been applied as part
* of this change.
*/
@SuppressWarnings("unchecked")
public AtomicChange(ObservableMultiset<E> source,
Multiset<E> previousContents,
List<ElementarySubChange<E>> elementarySubChanges) {
super(source);
this.previousContents = previousContents;
this.elementarySubChanges = elementarySubChanges
.toArray(new ElementarySubChange[] {});
}
/**
* Creates a new {@link MultisetListenerHelper.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 ObservableMultiset} to re-fire change events of their wrapped
* {@link ObservableMultiset} with themselves as source.
*
* @param source
* The new source {@link ObservableMultiset}.
* @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(ObservableMultiset<E> source,
MultisetChangeListener.Change<? extends E> change) {
super(source);
// copy previous contents
this.previousContents = HashMultiset
.create(change.getPreviousContents());
// 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 = new ArrayList<>();
while (change.next()) {
elementarySubChanges
.add(new ElementarySubChange<E>(change.getElement(),
change.getRemoveCount(), change.getAddCount()));
}
change.reset();
this.elementarySubChanges = elementarySubChanges
.toArray(new ElementarySubChange[] {});
}
private void checkCursor() {
String methodName = Thread.currentThread().getStackTrace()[2]
.getMethodName();
if (cursor == -1) {
throw new IllegalStateException("Need to call next() before "
+ methodName + "() can be called.");
} else if (cursor >= elementarySubChanges.length) {
throw new IllegalStateException("May only call " + methodName
+ "() if next() returned true.");
}
}
@Override
public int getAddCount() {
checkCursor();
return elementarySubChanges[cursor].getAddCount();
}
@Override
public E getElement() {
checkCursor();
return elementarySubChanges[cursor].getElement();
}
@Override
public Multiset<E> getPreviousContents() {
return Multisets.unmodifiableMultiset(previousContents);
}
@Override
public int getRemoveCount() {
checkCursor();
return elementarySubChanges[cursor].getRemoveCount();
}
@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();
}
}
/**
* An elementary change related to a single element of a {@link Multiset}.
*
* @param <E>
* The element type of the {@link ObservableMultiset}.
*/
public static class ElementarySubChange<E> {
private int addCount;
private E element;
private int removeCount;
/**
* Constructs a new elementary sub-change with the given values.
*
* @param element
* The element that was added or removed.
* @param removeCount
* The number of occurrences that were removed.
* @param addCount
* The number of occurrences that were added.
*/
public ElementarySubChange(E element, int removeCount, int addCount) {
this.element = element;
this.removeCount = removeCount;
this.addCount = addCount;
}
/**
* Returns the number of occurrences that have been added for the
* respective element as part of this elementary sub-change.
*
* @return The number of added occurrences.
*/
public int getAddCount() {
return addCount;
}
/**
* Returns the element that has been altered by this elementary
* sub-change.
*
* @return The changed element.
*/
public E getElement() {
return element;
}
/**
* Returns the number of occurrences that have been removed for the
* respective element as part of this elementary sub-change.
*
* @return The number of removed occurrences.
*/
public int getRemoveCount() {
return removeCount;
}
@Override
public String toString() {
if (addCount > 0) {
return "Added " + addCount + " occurrences of " + element + ".";
} else {
return "Removed " + removeCount + " occurrences of " + element
+ ".";
}
}
}
private List<InvalidationListener> invalidationListeners = null;
private boolean lockInvalidationListeners;
private boolean lockMultisetChangeListeners;
private List<MultisetChangeListener<? super E>> multisetChangeListeners = null;
private ObservableMultiset<E> source;
/**
* Constructs a new {@link MultisetListenerHelper} for the given source
* {@link ObservableMultiset}.
*
* @param source
* The {@link ObservableMultiset} to use as source in change
* notifications.
*/
public MultisetListenerHelper(ObservableMultiset<E> source) {
this.source = source;
}
/**
* Adds a new {@link InvalidationListener} to this
* {@link MultisetListenerHelper}. 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 SetMultimapChangeListener} to this
* {@link MultisetListenerHelper}. 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(MultisetChangeListener<? super E> listener) {
if (multisetChangeListeners == null) {
multisetChangeListeners = 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 (lockMultisetChangeListeners) {
multisetChangeListeners = new ArrayList<>(multisetChangeListeners);
}
multisetChangeListeners.add(listener);
}
/**
* Notifies all attached {@link InvalidationListener}s and
* {@link MultisetChangeListener}s about the change.
*
* @param change
* The change to notify listeners about.
*/
public void fireValueChangedEvent(
MultisetChangeListener.Change<? extends E> change) {
notifyInvalidationListeners();
if (change != null) {
notifyMultisetChangeListeners(change);
}
}
/**
* Returns the source {@link ObservableMultiset} this
* {@link MultisetListenerHelper} is bound to, which is used in change
* notifications.
*
* @return The source {@link ObservableMultiset}.
*/
protected ObservableMultiset<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 MultisetChangeListener}s about the related
* change.
*
* @param change
* The applied change.
*/
protected void notifyMultisetChangeListeners(Change<? extends E> change) {
if (multisetChangeListeners != null) {
try {
lockMultisetChangeListeners = true;
for (MultisetChangeListener<? super E> l : multisetChangeListeners) {
change.reset();
try {
l.onChanged(change);
} catch (Exception e) {
Thread.currentThread().getUncaughtExceptionHandler()
.uncaughtException(Thread.currentThread(), e);
}
}
} finally {
lockMultisetChangeListeners = false;
}
}
}
/**
* Removes the given {@link InvalidationListener} from this
* {@link MultisetListenerHelper}. 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 MultisetChangeListener} from this
* {@link MultisetListenerHelper}. If its was registered more than once,
* removes one occurrence.
*
* @param listener
* The listener to remove.
*/
public void removeListener(MultisetChangeListener<? super E> listener) {
if (multisetChangeListeners == 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 (lockMultisetChangeListeners) {
multisetChangeListeners = new ArrayList<>(multisetChangeListeners);
}
// 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<MultisetChangeListener<? super E>> iterator = multisetChangeListeners
.iterator(); iterator.hasNext();) {
if (iterator.next().equals(listener)) {
iterator.remove();
break;
}
}
if (multisetChangeListeners.isEmpty()) {
multisetChangeListeners = null;
}
}
}