/******************************************************************************
* Copyright (c) 2015, 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.beans.binding;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.gef.common.beans.value.ObservableSetMultimapValue;
import org.eclipse.gef.common.collections.ObservableSetMultimap;
import org.eclipse.gef.common.collections.SetMultimapChangeListener;
import org.eclipse.gef.common.collections.SetMultimapListenerHelper;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimaps;
import javafx.beans.value.ChangeListener;
/**
* A utility class to support change notifications for an
* {@link ObservableSetMultimap}.
*
* @author anyssen
* @param <K>
* The key type of the {@link ObservableSetMultimap}.
* @param <V>
* The value type of the {@link ObservableSetMultimap}.
*
*/
public class SetMultimapExpressionHelper<K, V>
extends SetMultimapListenerHelper<K, V> {
private List<ChangeListener<? super ObservableSetMultimap<K, V>>> changeListeners = null;
private ObservableSetMultimapValue<K, V> observableValue = null;
private ObservableSetMultimap<K, V> currentValue = null;
private boolean lockChangeListeners;
/**
* Constructs a new {@link SetMultimapExpressionHelper} for the given source
* {@link ObservableSetMultimapValue}.
*
* @param observableValue
* The observableValue {@link ObservableSetMultimap}, which is
* used in change notifications.
*/
public SetMultimapExpressionHelper(
ObservableSetMultimapValue<K, V> observableValue) {
super(observableValue);
this.observableValue = observableValue;
this.currentValue = observableValue.getValue();
}
/**
* Adds a new {@link ChangeListener} to this
* {@link SetMultimapExpressionHelper}. 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(
ChangeListener<? super ObservableSetMultimap<K, V>> listener) {
if (changeListeners == null) {
changeListeners = 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 (lockChangeListeners) {
changeListeners = new ArrayList<>(changeListeners);
}
changeListeners.add(listener);
}
/**
* Fires notifications to all attached
* {@link javafx.beans.InvalidationListener InvalidationListeners},
* {@link javafx.beans.value.ChangeListener ChangeListeners}, and
* {@link SetMultimapChangeListener SetMultimapChangeListeners}.
*
*/
public void fireValueChangedEvent() {
final ObservableSetMultimap<K, V> oldValue = currentValue;
currentValue = observableValue.getValue();
notifyListeners(oldValue, currentValue);
}
/**
* Fires notifications to all attached
* {@link javafx.beans.InvalidationListener InvalidationListeners} and
* {@link SetMultimapChangeListener SetMultimapChangeListeners}.
*
* @param change
* The change that needs to be propagated.
*/
@Override
public void fireValueChangedEvent(
SetMultimapChangeListener.Change<? extends K, ? extends V> change) {
if (change != null) {
notifyInvalidationListeners();
// XXX: We do not notify change listeners here, as the identity of
// the observed value did not change (see
// https://bugs.openjdk.java.net/browse/JDK-8089169)
notifySetMultimapChangeListeners(
new AtomicChange<>(observableValue, change));
}
}
private void notifyListeners(ObservableSetMultimap<K, V> oldValue,
ObservableSetMultimap<K, V> currentValue) {
if (currentValue != oldValue) {
notifyInvalidationListeners();
if (changeListeners != null) {
try {
lockChangeListeners = true;
for (ChangeListener<? super ObservableSetMultimap<K, V>> l : changeListeners) {
l.changed(observableValue, oldValue, currentValue);
}
} finally {
lockChangeListeners = false;
}
}
if (oldValue == null || !oldValue.equals(currentValue)) {
notifySetMultimapListeners(oldValue, currentValue);
}
}
}
private void notifySetMultimapListeners(
ObservableSetMultimap<K, V> oldValue,
ObservableSetMultimap<K, V> currentValue) {
if (currentValue == null) {
List<ElementarySubChange<K, V>> elementaryChanges = new ArrayList<>();
// all entries have been removed
for (Map.Entry<K, Set<V>> entries : Multimaps.asMap(oldValue)
.entrySet()) {
elementaryChanges.add(new ElementarySubChange<>(
entries.getKey(), entries.getValue(),
Collections.<V> emptySet()));
}
notifySetMultimapChangeListeners(
new SetMultimapListenerHelper.AtomicChange<>(getSource(),
HashMultimap.<K, V> create(oldValue),
elementaryChanges));
} else if (oldValue == null) {
List<ElementarySubChange<K, V>> elementaryChanges = new ArrayList<>();
// all entries have been added
for (Map.Entry<K, Set<V>> entries : Multimaps.asMap(currentValue)
.entrySet()) {
elementaryChanges.add(new ElementarySubChange<>(
entries.getKey(), Collections.<V> emptySet(),
entries.getValue()));
}
notifySetMultimapChangeListeners(
new SetMultimapListenerHelper.AtomicChange<>(getSource(),
HashMultimap.<K, V> create(), elementaryChanges));
} else {
List<ElementarySubChange<K, V>> elementaryChanges = new ArrayList<>();
// compute changed/removed values
for (Map.Entry<K, Set<V>> entries : Multimaps.asMap(oldValue)
.entrySet()) {
K key = entries.getKey();
Set<V> oldValues = entries.getValue();
if (currentValue.containsKey(key)) {
Set<V> newValues = currentValue.get(key);
// compute add/removed values
Set<V> addedValues = new HashSet<>(newValues);
addedValues.removeAll(oldValues);
Set<V> removedValues = new HashSet<>(oldValues);
removedValues.removeAll(newValues);
if (!addedValues.isEmpty() || !removedValues.isEmpty()) {
elementaryChanges.add(new ElementarySubChange<>(
entries.getKey(), removedValues, addedValues));
}
} else {
elementaryChanges
.add(new ElementarySubChange<>(entries.getKey(),
oldValues, Collections.<V> emptySet()));
}
}
// compute added values
for (Map.Entry<K, Set<V>> entries : Multimaps.asMap(currentValue)
.entrySet()) {
K key = entries.getKey();
if (!oldValue.containsKey(key)) {
elementaryChanges.add(new ElementarySubChange<>(key,
Collections.<V> emptySet(), entries.getValue()));
}
}
notifySetMultimapChangeListeners(
new SetMultimapListenerHelper.AtomicChange<>(getSource(),
HashMultimap.<K, V> create(oldValue),
elementaryChanges));
}
}
/**
* Removes the given {@link ChangeListener} from this
* {@link SetMultimapChangeListener}. If it was registered more than once,
* removes only one occurrence.
*
* @param listener
* The {@link ChangeListener} to remove.
*/
public void removeListener(
ChangeListener<? super ObservableSetMultimap<K, V>> listener) {
// 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 (lockChangeListeners) {
changeListeners = new ArrayList<>(changeListeners);
}
// 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()); remove() may thus not be
// used.
for (Iterator<ChangeListener<? super ObservableSetMultimap<K, V>>> iterator = changeListeners
.iterator(); iterator.hasNext();) {
if (iterator.next().equals(listener)) {
iterator.remove();
break;
}
}
if (changeListeners.isEmpty()) {
changeListeners = null;
}
}
}