/*******************************************************************************
* 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.beans.binding;
import java.lang.ref.WeakReference;
import java.util.HashSet;
import org.eclipse.gef.common.collections.MultisetChangeListener;
import org.eclipse.gef.common.collections.ObservableMultiset;
import org.eclipse.gef.common.collections.ObservableSetMultimap;
import org.eclipse.gef.common.collections.SetMultimapChangeListener;
import com.google.common.collect.Multiset;
import com.google.common.collect.SetMultimap;
import javafx.beans.WeakListener;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.binding.SetBinding;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.ObservableSet;
/**
* A utility class that augments {@link Bindings} with functionality related to
* {@link Multiset} and {@link SetMultimap}.
*
* @author anyssen
*
*/
public class BindingUtils {
private static class BidirectionalMultisetContentBinding<E>
implements MultisetChangeListener<E>, WeakListener {
private final WeakReference<ObservableMultiset<E>> multiset1Ref;
private final WeakReference<ObservableMultiset<E>> multiset2Ref;
private boolean updating = false;
public BidirectionalMultisetContentBinding(
ObservableMultiset<E> multiset1,
ObservableMultiset<E> multiset2) {
multiset1Ref = new WeakReference<>(multiset1);
multiset2Ref = new WeakReference<>(multiset2);
}
@SuppressWarnings("unchecked")
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (other == null
|| !(other instanceof BidirectionalMultisetContentBinding)) {
return false;
}
try {
BidirectionalMultisetContentBinding<E> otherBinding = (BidirectionalMultisetContentBinding<E>) other;
ObservableMultiset<E> multiset1 = multiset1Ref.get();
ObservableMultiset<E> multiset2 = multiset2Ref.get();
ObservableMultiset<E> otherMultiset1 = otherBinding.multiset1Ref
.get();
ObservableMultiset<E> otherMultiset2 = otherBinding.multiset2Ref
.get();
// The actual direction of the bidirectional binding is not
// significant, thus we can ignore it here
return (((multiset1 == otherMultiset1)
&& (multiset2 == otherMultiset2))
|| ((multiset1 == otherMultiset2)
&& (multiset2 == otherMultiset1)));
} catch (ClassCastException e) {
return false;
}
}
@Override
public int hashCode() {
// XXX: As we rely on equality to remove a binding again, we have to
// ensure the hash code of two bindings that target the same
// properties is the same (which we do by using a constant).
return 0;
}
@Override
public void onChanged(Change<? extends E> change) {
if (!updating) {
final ObservableMultiset<E> multiset1 = multiset1Ref.get();
final ObservableMultiset<E> multiset2 = multiset2Ref.get();
if ((multiset1 == null) || (multiset2 == null)) {
if (multiset1 != null) {
multiset1.removeListener(this);
}
if (multiset2 != null) {
multiset2.removeListener(this);
}
} else {
try {
updating = true;
final Multiset<E> destination = multiset1 == change
.getMultiset() ? multiset2 : multiset1;
// we use replaceValues() to perform an atomic change
// here (and thus don't use the added and removed values
// from the change)
while (change.next()) {
destination.setCount(change.getElement(),
destination.count(change.getElement()),
destination.count(change.getElement())
+ change.getAddCount()
- change.getRemoveCount());
}
} finally {
updating = false;
}
}
}
}
@Override
public boolean wasGarbageCollected() {
return (multiset1Ref.get() == null) || (multiset2Ref.get() == null);
}
}
private static class BidirectionalSetMultimapContentBinding<K, V>
implements SetMultimapChangeListener<K, V>, WeakListener {
private final WeakReference<ObservableSetMultimap<K, V>> setMultimap1Ref;
private final WeakReference<ObservableSetMultimap<K, V>> setMultimap2Ref;
private boolean updating = false;
public BidirectionalSetMultimapContentBinding(
ObservableSetMultimap<K, V> setMultimap1,
ObservableSetMultimap<K, V> setMultimap2) {
setMultimap1Ref = new WeakReference<>(setMultimap1);
setMultimap2Ref = new WeakReference<>(setMultimap2);
}
@SuppressWarnings("unchecked")
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (other == null
|| !(other instanceof BidirectionalSetMultimapContentBinding)) {
return false;
}
try {
BidirectionalSetMultimapContentBinding<K, V> otherBinding = (BidirectionalSetMultimapContentBinding<K, V>) other;
ObservableSetMultimap<K, V> setMultimap1 = setMultimap1Ref
.get();
ObservableSetMultimap<K, V> setMultimap2 = setMultimap2Ref
.get();
ObservableSetMultimap<K, V> otherSetMultimap1 = otherBinding.setMultimap1Ref
.get();
ObservableSetMultimap<K, V> otherSetMultimap2 = otherBinding.setMultimap2Ref
.get();
// The actual direction of the bidirectional binding is not
// significant, thus we can ignore it here
return (((setMultimap1 == otherSetMultimap1)
&& (setMultimap2 == otherSetMultimap2))
|| ((setMultimap1 == otherSetMultimap2)
&& (setMultimap2 == otherSetMultimap1)));
} catch (ClassCastException e) {
return false;
}
}
@Override
public int hashCode() {
// XXX: As we rely on equality to remove a binding again, we have to
// ensure the hash code of two bindings that target the same
// properties is the same (which we do by using a constant).
return 0;
}
@Override
public void onChanged(Change<? extends K, ? extends V> change) {
if (!updating) {
final ObservableSetMultimap<K, V> setMultimap1 = setMultimap1Ref
.get();
final ObservableSetMultimap<K, V> setMultimap2 = setMultimap2Ref
.get();
if ((setMultimap1 == null) || (setMultimap2 == null)) {
if (setMultimap1 != null) {
setMultimap1.removeListener(this);
}
if (setMultimap2 != null) {
setMultimap2.removeListener(this);
}
} else {
try {
updating = true;
final SetMultimap<K, V> source = setMultimap1 == change
.getSetMultimap() ? setMultimap1 : setMultimap2;
final SetMultimap<K, V> destination = setMultimap1 == change
.getSetMultimap() ? setMultimap2 : setMultimap1;
// we use replaceValues() to perform an atomic change
// here (and thus don't use the added and removed values
// from the change)
while (change.next()) {
destination.replaceValues(change.getKey(),
new HashSet<>(source.get(change.getKey())));
}
} finally {
updating = false;
}
}
}
}
@Override
public boolean wasGarbageCollected() {
return (setMultimap1Ref.get() == null)
|| (setMultimap2Ref.get() == null);
}
}
private static class UnidirectionalMultisetContentBinding<E>
implements MultisetChangeListener<E>, WeakListener {
private final WeakReference<Multiset<E>> multisetRef;
public UnidirectionalMultisetContentBinding(Multiset<E> multiset) {
this.multisetRef = new WeakReference<>(multiset);
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (other == null
|| !(other instanceof UnidirectionalMultisetContentBinding)) {
return false;
}
try {
@SuppressWarnings("unchecked")
UnidirectionalMultisetContentBinding<E> otherBinding = ((UnidirectionalMultisetContentBinding<E>) other);
Multiset<E> multiset = multisetRef.get();
Multiset<E> otherMultiset = otherBinding.multisetRef.get();
return multiset == otherMultiset;
} catch (ClassCastException e) {
return false;
}
}
@Override
public int hashCode() {
// XXX: As we rely on equality to remove a binding again, we have to
// ensure the hash code of two bindings that target the same
// property is the same (which we do by using a constant).
return 0;
}
@Override
public void onChanged(Change<? extends E> change) {
// This cast is safe, as a
// UnidirectionalSetMultimapContentBinding<K, V> will only be used
// for a SetMultimap<K, V>.
while (change.next()) {
final Multiset<E> destination = multisetRef.get();
if (destination == null) {
change.getMultiset().removeListener(this);
} else {
// we use replaceValues() to perform an atomic change here
// (and
// thus don't use the added and removed values from the
// change)
destination.setCount(change.getElement(),
destination.count(change.getElement()),
destination.count(change.getElement())
+ change.getAddCount()
- change.getRemoveCount());
}
}
}
@Override
public boolean wasGarbageCollected() {
return multisetRef.get() == null;
}
}
private static class UnidirectionalSetMultimapContentBinding<K, V>
implements SetMultimapChangeListener<K, V>, WeakListener {
private final WeakReference<SetMultimap<K, V>> setMultimapRef;
public UnidirectionalSetMultimapContentBinding(
SetMultimap<K, V> setMultimap) {
this.setMultimapRef = new WeakReference<>(setMultimap);
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (other == null
|| !(other instanceof UnidirectionalSetMultimapContentBinding)) {
return false;
}
try {
@SuppressWarnings("unchecked")
UnidirectionalSetMultimapContentBinding<K, V> otherBinding = ((UnidirectionalSetMultimapContentBinding<K, V>) other);
SetMultimap<K, V> setMultimap = setMultimapRef.get();
SetMultimap<K, V> otherSetMultimap = otherBinding.setMultimapRef
.get();
return setMultimap == otherSetMultimap;
} catch (ClassCastException e) {
return false;
}
}
@Override
public int hashCode() {
// XXX: As we rely on equality to remove a binding again, we have to
// ensure the hash code of two bindings that target the same
// property is the same (which we do by using a constant).
return 0;
}
@SuppressWarnings("unchecked")
@Override
public void onChanged(Change<? extends K, ? extends V> change) {
// This cast is safe, as a
// UnidirectionalSetMultimapContentBinding<K, V> will only be used
// for a SetMultimap<K, V>.
final SetMultimap<K, V> source = (SetMultimap<K, V>) change
.getSetMultimap();
while (change.next()) {
final SetMultimap<K, V> destination = setMultimapRef.get();
if (destination == null) {
change.getSetMultimap().removeListener(this);
} else {
// we use replaceValues() to perform an atomic change here
// (and
// thus don't use the added and removed values from the
// change)
destination.replaceValues(change.getKey(),
new HashSet<>(source.get(change.getKey())));
}
}
}
@Override
public boolean wasGarbageCollected() {
return setMultimapRef.get() == null;
}
}
/**
* Creates a unidirectional content binding from the given source
* {@link Multiset} to the given target {@link ObservableMultiset}.
*
* @param <E>
* The element type of the given {@link Multiset} and
* {@link ObservableMultiset}.
* @param source
* The {@link Multiset} whose content to update when the given
* {@link ObservableMultiset} changes.
* @param target
* The {@link ObservableMultiset} whose content is to be
* observed.
*/
public static <E> void bindContent(Multiset<E> source,
ObservableMultiset<? extends E> target) {
if (source == null) {
throw new NullPointerException("Cannot bind null value.");
}
if (target == null) {
throw new NullPointerException("Cannot bind to null value.");
}
if (source == target) {
throw new IllegalArgumentException("Cannot bind source to itself.");
}
if (source instanceof ObservableMultiset) {
// ensure we use an atomic operation in case the source multiset is
// observable.
((ObservableMultiset<E>) source).replaceAll(target);
} else {
source.clear();
source.addAll(target);
}
final UnidirectionalMultisetContentBinding<E> contentBinding = new UnidirectionalMultisetContentBinding<>(
source);
// clear any previous bindings
target.removeListener(contentBinding);
// add new binding as listener
target.addListener(contentBinding);
}
/**
* Creates a unidirectional content binding from the given source
* {@link SetMultimap} to the given target {@link ObservableSetMultimap}.
*
* @param <K>
* The key type of the given {@link SetMultimap} and
* {@link ObservableSetMultimap}.
* @param <V>
* The value type of the given {@link SetMultimap} and
* {@link ObservableSetMultimap}.
* @param source
* The {@link SetMultimap} whose content to update when the given
* {@link ObservableSetMultimap} changes.
* @param target
* The {@link ObservableSetMultimap} whose content is to be
* observed.
*/
public static <K, V> void bindContent(SetMultimap<K, V> source,
ObservableSetMultimap<? extends K, ? extends V> target) {
if (source == null) {
throw new NullPointerException("Cannot bind null value.");
}
if (target == null) {
throw new NullPointerException("Cannot bind to null value.");
}
if (source == target) {
throw new IllegalArgumentException("Cannot bind source to itself.");
}
if (source instanceof ObservableSetMultimap) {
// ensure we use an atomic operation in case the source set-multimap
// is
// observable.
((ObservableSetMultimap<K, V>) source).replaceAll(target);
} else {
source.clear();
source.putAll(target);
}
final UnidirectionalSetMultimapContentBinding<K, V> contentBinding = new UnidirectionalSetMultimapContentBinding<>(
source);
// clear any previous bindings
target.removeListener(contentBinding);
// add new binding as listener
target.addListener(contentBinding);
}
/**
* Creates a bidirectional content binding between the given
* {@link ObservableMultiset ObservableMultisets}.
*
* @param <E>
* The element type of the given {@link ObservableMultiset
* ObservableMultisets}.
* @param source
* The first participant of the bidirectional binding. Its
* contents will be initially replaced with that of the second
* participant before both are synchronized.
* @param target
* The second participant of the bidirectional binding. Its
* contents will be initially taken to update the contents of the
* first participant before both are synchronized.
*/
public static <E> void bindContentBidirectional(
ObservableMultiset<E> source, ObservableMultiset<E> target) {
if (source == null) {
throw new NullPointerException("Cannot bind null value.");
}
if (target == null) {
throw new NullPointerException("Cannot bind to null value.");
}
if (source == target) {
throw new IllegalArgumentException("Cannot bind source to itself.");
}
source.replaceAll(target);
final BidirectionalMultisetContentBinding<E> contentBinding = new BidirectionalMultisetContentBinding<>(
source, target);
// clear any previous bindings
source.removeListener(contentBinding);
target.removeListener(contentBinding);
// add new binding as listener
source.addListener(contentBinding);
target.addListener(contentBinding);
}
/**
* Creates a unidirectional content binding between the given
* {@link ObservableSetMultimap ObservableSetMultimaps}.
*
* @param <K>
* The key type of the given {@link ObservableSetMultimap
* ObservableSetMultimaps}.
* @param <V>
* The value type of the given {@link ObservableSetMultimap
* ObservableSetMultimaps}.
* @param source
* The first participant of the bidirectional binding. Its
* contents will be initially replaced with that of the second
* participant before both are synchronized.
* @param target
* The second participant of the bidirectional binding. Its
* contents will be initially taken to update the contents of the
* first participant before both are synchronized.
*/
public static <K, V> void bindContentBidirectional(
ObservableSetMultimap<K, V> source,
ObservableSetMultimap<K, V> target) {
if (source == null) {
throw new NullPointerException("Cannot bind null value.");
}
if (target == null) {
throw new NullPointerException("Cannot bind to null value.");
}
if (source == target) {
throw new IllegalArgumentException("Cannot bind source to itself.");
}
source.replaceAll(target);
final BidirectionalSetMultimapContentBinding<K, V> contentBinding = new BidirectionalSetMultimapContentBinding<>(
source, target);
// clear any previous bindings
source.removeListener(contentBinding);
target.removeListener(contentBinding);
// add new binding as listener
source.addListener(contentBinding);
target.addListener(contentBinding);
}
/**
* Removes an existing content binding from the given source
* {@link Multiset} to the given target {@link ObservableMultiset}.
*
* @param <E>
* The element types of the {@link Multiset} and
* {@link ObservableMultiset}.
* @param source
* The {@link Multiset} whose content should no longer be updated
* when the given {@link ObservableMultiset} changes.
* @param target
* The {@link ObservableMultiset} whose content is no longer to
* be observed.
*/
public static <E> void unbindContent(Multiset<E> source,
ObservableMultiset<? extends E> target) {
if (source == null) {
throw new NullPointerException("Cannot unbind null value.");
}
if (target == null) {
throw new NullPointerException("Cannot unbind from null value.");
}
if (source == target) {
throw new IllegalArgumentException(
"Cannot unbind source to itself.");
}
target.removeListener(
new UnidirectionalMultisetContentBinding<>(source));
}
/**
* Removes an existing unidirectional content binding from the given source
* {@link SetMultimap} to the given target {@link ObservableSetMultimap}.
*
* @param <K>
* The key type of the given {@link SetMultimap} and
* {@link ObservableSetMultimap}.
* @param <V>
* The value type of the given {@link SetMultimap} and
* {@link ObservableSetMultimap}.
* @param source
* The {@link SetMultimap} whose content is no longer to update
* when the given {@link ObservableSetMultimap} changes.
* @param target
* The {@link ObservableSetMultimap} whose content is no longer
* to be observed.
*/
public static <K, V> void unbindContent(SetMultimap<K, V> source,
ObservableSetMultimap<? extends K, ? extends V> target) {
if (source == null) {
throw new NullPointerException("Cannot unbind null value.");
}
if (target == null) {
throw new NullPointerException("Cannot unbind from null value.");
}
if (source == target) {
throw new IllegalArgumentException(
"Cannot unbind source to itself.");
}
target.removeListener(
new UnidirectionalSetMultimapContentBinding<>(source));
}
/**
* Removes a bidirectional content binding between the given
* {@link ObservableMultiset ObservableMultisets}. .
*
* @param <E>
* The element type of the given {@link ObservableMultiset
* ObservableMultisets}.
* @param multiset1
* The first participant of the bidirectional binding.
* @param multiset2
* The second participant of the bidirectional binding.
*/
public static <E> void unbindContentBidirectional(
ObservableMultiset<E> multiset1, ObservableMultiset<E> multiset2) {
if (multiset1 == null) {
throw new NullPointerException("Cannot bind null value.");
}
if (multiset2 == null) {
throw new NullPointerException("Cannot bind to null value.");
}
if (multiset1 == multiset2) {
throw new IllegalArgumentException("Cannot bind source to itself.");
}
final BidirectionalMultisetContentBinding<E> contentBinding = new BidirectionalMultisetContentBinding<>(
multiset1, multiset2);
multiset1.removeListener(contentBinding);
multiset2.removeListener(contentBinding);
}
/**
* Removes a bidirectional content binding between the given
* {@link ObservableSetMultimap ObservableSetMultimaps}.
*
* @param <K>
* The key type of the given {@link ObservableSetMultimap
* ObservableSetMultimaps}.
* @param <V>
* The value type of the given {@link ObservableSetMultimap
* ObservableSetMultimaps}.
* @param source
* The first participant of the bidirectional binding.
* @param target
* The second participant of the bidirectional binding.
*/
public static <K, V> void unbindContentBidirectional(
ObservableSetMultimap<K, V> source,
ObservableSetMultimap<K, V> target) {
if (source == null) {
throw new NullPointerException("Cannot bind null value.");
}
if (target == null) {
throw new NullPointerException("Cannot bind to null value.");
}
if (source == target) {
throw new IllegalArgumentException("Cannot bind source to itself.");
}
final BidirectionalSetMultimapContentBinding<K, V> contentBinding = new BidirectionalSetMultimapContentBinding<>(
source, target);
source.removeListener(contentBinding);
target.removeListener(contentBinding);
}
/**
* Creates a new {@link ObjectBinding} that contains the values mapped to
* the specified key.
*
* @param setMultimap
* The {@link ObservableSetMultimap} from which the values are to
* be retrieved.
* @param <K>
* The key type of the {@link ObservableSetMultimap}.
* @param <V>
* The value type of the {@link ObservableSetMultimap}.
*
*
* @param key
* the key of the mapping
* @return A new {@code ObjectBinding}.
*/
public static <K, V> SetBinding<V> valuesAt(
final ObservableSetMultimap<K, V> setMultimap, final K key) {
if (setMultimap == null) {
throw new UnsupportedOperationException(
"setMultimap may not be null.");
}
return new SetBinding<V>() {
{
super.bind(setMultimap);
}
@Override
protected ObservableSet<V> computeValue() {
return FXCollections.observableSet(setMultimap.get(key));
}
@Override
public void dispose() {
super.unbind(setMultimap);
}
@Override
public ObservableList<?> getDependencies() {
return FXCollections.singletonObservableList(setMultimap);
}
};
}
/**
* Creates a new {@link ObjectBinding} that contains the values mapped to
* the specified key.
*
* @param <K>
* The key type of the {@link ObservableSetMultimap}.
* @param <V>
* The value type of the {@link ObservableSetMultimap}.
*
* @param setMultimap
* The {@link ObservableSetMultimap} from which the values are to
* be retrieved.
* @param key
* the key of the mapping
* @return A new {@code ObjectBinding}.
*/
public static <K, V> SetBinding<V> valuesAt(
final ObservableSetMultimap<K, V> setMultimap,
final ObservableValue<K> key) {
if (setMultimap == null) {
throw new UnsupportedOperationException(
"setMultimap may not be null.");
}
if (key == null) {
throw new UnsupportedOperationException("key may not be null");
}
return new SetBinding<V>() {
{
super.bind(setMultimap);
}
@Override
protected ObservableSet<V> computeValue() {
return FXCollections
.observableSet(setMultimap.get(key.getValue()));
}
@Override
public void dispose() {
super.unbind(setMultimap);
}
@Override
public ObservableList<?> getDependencies() {
return FXCollections.unmodifiableObservableList(
FXCollections.observableArrayList(setMultimap, key));
}
};
}
}